Add support for partially loading invalid profiles (#533)
* Add support for partially loading invalid profiles * Return early in exception
This commit is contained in:
@@ -106,7 +106,6 @@ public class GameController(
|
||||
{
|
||||
SendPraporGiftsToNewProfiles(pmcProfile);
|
||||
SendMechanicGiftsToNewProfile(pmcProfile);
|
||||
profileFixerService.CheckForOrphanedModdedItems(sessionId, fullProfile);
|
||||
}
|
||||
|
||||
profileFixerService.CheckForAndRemoveInvalidTraders(fullProfile);
|
||||
|
||||
@@ -86,6 +86,12 @@ public class InsuranceController(
|
||||
var insuranceTime = time ?? timeUtil.GetTimeStamp();
|
||||
|
||||
var profileInsuranceDetails = saveServer.GetProfile(sessionId).InsuranceList;
|
||||
|
||||
if (profileInsuranceDetails is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (profileInsuranceDetails.Count > 0)
|
||||
{
|
||||
if (logger.IsLogEnabled(LogLevel.Debug))
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SPTarkov.Server.Core.Exceptions.Items;
|
||||
|
||||
public class InvalidModdedClothingException : Exception
|
||||
{
|
||||
public InvalidModdedClothingException(string message)
|
||||
: base(message) { }
|
||||
|
||||
public InvalidModdedClothingException(string message, Exception innerException)
|
||||
: base(message, innerException) { }
|
||||
|
||||
public override string? StackTrace
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SPTarkov.Server.Core.Exceptions.Items;
|
||||
|
||||
public class InvalidModdedItemException : Exception
|
||||
{
|
||||
public InvalidModdedItemException(string message)
|
||||
: base(message) { }
|
||||
|
||||
public InvalidModdedItemException(string message, Exception innerException)
|
||||
: base(message, innerException) { }
|
||||
|
||||
public override string? StackTrace
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SPTarkov.Server.Core.Exceptions.Items;
|
||||
|
||||
public class InvalidModdedTraderException : Exception
|
||||
{
|
||||
public InvalidModdedTraderException(string message)
|
||||
: base(message) { }
|
||||
|
||||
public InvalidModdedTraderException(string message, Exception innerException)
|
||||
: base(message, innerException) { }
|
||||
|
||||
public override string? StackTrace
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Eft.ItemEvent;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
@@ -239,4 +242,51 @@ public static class ProfileExtensions
|
||||
bodyPart.Health.Maximum = profileTemplate.Character.Health.BodyParts[partKey].Health.Maximum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle Remove event
|
||||
/// Remove item from player inventory + insured items array
|
||||
/// Also deletes child items
|
||||
/// </summary>
|
||||
/// <param name="profile">Profile to remove item from (pmc or scav)</param>
|
||||
/// <param name="itemId">Items id to remove</param>
|
||||
/// <param name="sessionId">Session id</param>
|
||||
/// <param name="output">OPTIONAL - ItemEventRouterResponse</param>
|
||||
public static void RemoveItem(this PmcData profile, MongoId itemId, MongoId sessionId, ItemEventRouterResponse? output = null)
|
||||
{
|
||||
if (itemId.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get children of item, they get deleted too
|
||||
var itemAndChildrenToRemove = profile.Inventory.Items.GetItemWithChildren(itemId);
|
||||
if (!itemAndChildrenToRemove.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var inventoryItems = profile.Inventory.Items;
|
||||
var insuredItems = profile.InsuredItems;
|
||||
|
||||
// We have output object, inform client of root item deletion, not children
|
||||
output?.ProfileChanges[sessionId].Items.DeletedItems.Add(new DeletedItem { Id = itemId });
|
||||
|
||||
foreach (var item in itemAndChildrenToRemove)
|
||||
{
|
||||
// We expect that each inventory item and each insured item has unique "_id", respective "itemId".
|
||||
// Therefore, we want to use a NON-Greedy function and escape the iteration as soon as we find requested item.
|
||||
var inventoryIndex = inventoryItems.FindIndex(inventoryItem => inventoryItem.Id == item.Id);
|
||||
if (inventoryIndex != -1)
|
||||
{
|
||||
inventoryItems.RemoveAt(inventoryIndex);
|
||||
}
|
||||
|
||||
var insuredItemIndex = insuredItems.FindIndex(insuredItem => insuredItem.ItemId == item.Id);
|
||||
if (insuredItemIndex != -1)
|
||||
{
|
||||
insuredItems.RemoveAt(insuredItemIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Exceptions.Items;
|
||||
using SPTarkov.Server.Core.Extensions;
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
using SPTarkov.Server.Core.Models.Spt.Config;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Servers;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
|
||||
namespace SPTarkov.Server.Core.Helpers;
|
||||
|
||||
[Injectable]
|
||||
public class ProfileValidatorHelper(
|
||||
ConfigServer configServer,
|
||||
DatabaseService databaseService,
|
||||
ISptLogger<ProfileValidatorHelper> logger,
|
||||
ServerLocalisationService serverLocalisationService,
|
||||
TraderStore traderStore
|
||||
)
|
||||
{
|
||||
protected readonly CoreConfig _coreConfig = configServer.GetConfig<CoreConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// Checks profile inventory for items that do not exist inside the items DB
|
||||
/// </summary>
|
||||
/// <param name="sessionId"> Session ID </param>
|
||||
/// <param name="fullProfile"> Profile to check inventory of </param>
|
||||
public void CheckForOrphanedModdedItems(MongoId sessionId, SptProfile fullProfile)
|
||||
{
|
||||
var itemsDb = databaseService.GetItems();
|
||||
var pmcProfile = fullProfile.CharacterData.PmcData;
|
||||
|
||||
var invalidItemIds = pmcProfile.Inventory.Items.Where(item => !itemsDb.ContainsKey(item.Template)).Select(item => item.Id).ToList();
|
||||
foreach (var invalidItemId in invalidItemIds)
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Deleting item id: {invalidItemId} from inventory and insurance");
|
||||
|
||||
// Add here so we can remove below
|
||||
pmcProfile.RemoveItem(invalidItemId, sessionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidModdedItemException(serverLocalisationService.GetText("fixer-mod_item_found", invalidItemId.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
if (fullProfile.UserBuildData is not null)
|
||||
{
|
||||
// Remove invalid builds from weapon, equipment and magazine build lists
|
||||
var weaponBuilds = fullProfile.UserBuildData?.WeaponBuilds ?? [];
|
||||
fullProfile.UserBuildData.WeaponBuilds = weaponBuilds
|
||||
.Where(build => !ShouldRemoveWeaponEquipmentBuild("weapon", build, itemsDb))
|
||||
.ToList();
|
||||
|
||||
var equipmentBuilds = fullProfile.UserBuildData.EquipmentBuilds ?? [];
|
||||
fullProfile.UserBuildData.EquipmentBuilds = equipmentBuilds
|
||||
.Where(build => !ShouldRemoveWeaponEquipmentBuild("equipment", build, itemsDb))
|
||||
.ToList();
|
||||
|
||||
var magazineBuild = fullProfile.UserBuildData.MagazineBuilds ?? [];
|
||||
fullProfile.UserBuildData.MagazineBuilds = magazineBuild.Where(build => !ShouldRemoveMagazineBuild(build, itemsDb)).ToList();
|
||||
}
|
||||
|
||||
// Iterate over dialogs, looking for messages with items not found in item db, remove message if item found
|
||||
foreach (var dialog in fullProfile.DialogueRecords)
|
||||
{
|
||||
if (dialog.Value.Messages is null)
|
||||
{
|
||||
continue; // Skip dialog with no messages
|
||||
}
|
||||
|
||||
foreach (var message in dialog.Value.Messages)
|
||||
{
|
||||
if (message.Items?.Data is null)
|
||||
{
|
||||
continue; // skip messages with no items
|
||||
}
|
||||
|
||||
// Fix message with no items but have the flags to indicate items to collect
|
||||
if (message.Items.Data.Count == 0 && message.HasRewards.GetValueOrDefault(false))
|
||||
{
|
||||
message.HasRewards = false;
|
||||
message.RewardCollected = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find invalid items and remove from message
|
||||
var itemsToRemove = message.Items.Data.Where(item => !itemsDb.ContainsKey(item.Template)).ToList();
|
||||
foreach (var itemToRemove in itemsToRemove)
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
message.Items.Data.Remove(itemToRemove);
|
||||
logger.Warning(
|
||||
$"Item: {itemToRemove.Template} has resulted in the deletion of message: {message.Id} from dialog: {dialog.Key}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidModdedItemException(
|
||||
serverLocalisationService.GetText("fixer-mod_item_found", itemToRemove.Template.ToString())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var clothingDb = databaseService.GetTemplates().Customization;
|
||||
foreach (
|
||||
var clothingItem in fullProfile
|
||||
.CustomisationUnlocks.Where(customisation => customisation.Type == CustomisationType.SUITE)
|
||||
.ToList() // We're removing element, ToList to allow that to occur
|
||||
)
|
||||
{
|
||||
if (!clothingDb.ContainsKey(clothingItem.Id))
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks.Remove(clothingItem);
|
||||
logger.Warning($"Non-default clothing purchase: {clothingItem} removed from profile");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidModdedClothingException(
|
||||
serverLocalisationService.GetText("fixer-clothing_item_found", clothingItem.ToString())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var repeatable in fullProfile.CharacterData.PmcData.RepeatableQuests ?? [])
|
||||
{
|
||||
if (repeatable.ActiveQuests is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// ToList to prevent `Collection was modified` exception
|
||||
foreach (var activeQuest in repeatable.ActiveQuests.ToList())
|
||||
{
|
||||
if (!DoesTraderExist(activeQuest.TraderId))
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning(
|
||||
$"Non-default quest: {activeQuest.Id} from trader: {activeQuest.TraderId} removed from RepeatableQuests list in profile"
|
||||
);
|
||||
repeatable.ActiveQuests.Remove(activeQuest);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidModdedTraderException(
|
||||
serverLocalisationService.GetText("fixer-trader_found", activeQuest.TraderId.ToString())
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activeQuest.Rewards?["Success"] is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get Item rewards only
|
||||
foreach (var successReward in activeQuest.Rewards["Success"].Where(reward => reward.Type == RewardType.Item))
|
||||
{
|
||||
if (successReward.Items.Any(item => !itemsDb.ContainsKey(item.Template)))
|
||||
{
|
||||
logger.Warning(
|
||||
$"Non-default repeatable quest: {activeQuest.Id} from trader: {activeQuest.TraderId} removed from RepeatableQuests list in profile"
|
||||
);
|
||||
repeatable.ActiveQuests.Remove(activeQuest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (traderId, _) in fullProfile.TraderPurchases.Where(traderPurchase => !DoesTraderExist(traderPurchase.Key)))
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Non-default trader: {traderId} purchase removed from traderPurchases list in profile");
|
||||
fullProfile.TraderPurchases.Remove(traderId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidModdedTraderException(serverLocalisationService.GetText("fixer-trader_found", traderId.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a weapon build should be removed from the equipment list.
|
||||
/// </summary>
|
||||
/// <param name="buildType"> The type of build, used for logging only </param>
|
||||
/// <param name="build"> The build to check for invalid items </param>
|
||||
/// <param name="itemsDb"> The items database to use for item lookup </param>
|
||||
/// <returns> True if the build should be removed from the build list, false otherwise </returns>
|
||||
protected bool ShouldRemoveWeaponEquipmentBuild(string buildType, UserBuild build, Dictionary<MongoId, TemplateItem> itemsDb)
|
||||
{
|
||||
if (buildType == "weapon")
|
||||
// Get items not found in items db
|
||||
{
|
||||
foreach (var item in (build as WeaponBuild).Items.Where(item => !itemsDb.ContainsKey(item.Template)))
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.Template.ToString()));
|
||||
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Item: {item.Template} has resulted in the deletion of {buildType} build: {build.Name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor to be generic
|
||||
|
||||
if (buildType == "equipment")
|
||||
// Get items not found in items db
|
||||
{
|
||||
foreach (var item in (build as EquipmentBuild).Items.Where(item => !itemsDb.ContainsKey(item.Template)))
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.Template.ToString()));
|
||||
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Item: {item.Template} has resulted in the deletion of {buildType} build: {build.Name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Found a broken item
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether magazine build shou8ld be removed form the build list.
|
||||
/// </summary>
|
||||
/// <param name="magazineBuild"> The magazine build to check for validity </param>
|
||||
/// <param name="itemsDb"> The items database to use for item lookup </param>
|
||||
/// <returns> True if the build should be removed from the build list, false otherwise </returns>
|
||||
protected bool ShouldRemoveMagazineBuild(MagazineBuild magazineBuild, Dictionary<MongoId, TemplateItem> itemsDb)
|
||||
{
|
||||
foreach (var item in magazineBuild.Items)
|
||||
{
|
||||
// Magazine builds can have undefined items in them, skip those
|
||||
if (item is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check item exists in itemsDb
|
||||
if (!itemsDb.ContainsKey(item.TemplateId))
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.TemplateId.ToString()));
|
||||
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Item: {item.TemplateId} has resulted in the deletion of magazine build: {magazineBuild.Name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool DoesTraderExist(MongoId traderId)
|
||||
{
|
||||
return traderStore.GetTraderById(traderId) != null;
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,9 @@ public record Info
|
||||
|
||||
[JsonPropertyName("edition")]
|
||||
public string? Edition { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool InvalidOrUnloadableProfile { get; internal set; } = false;
|
||||
}
|
||||
|
||||
public record Characters
|
||||
|
||||
@@ -21,7 +21,7 @@ public class SaveServer(
|
||||
JsonUtil jsonUtil,
|
||||
HashUtil hashUtil,
|
||||
ServerLocalisationService serverLocalisationService,
|
||||
ProfileMigratorService profileMigratorService,
|
||||
ProfileValidatorService profileValidatorService,
|
||||
ISptLogger<SaveServer> logger,
|
||||
ConfigServer configServer
|
||||
)
|
||||
@@ -210,10 +210,16 @@ public class SaveServer(
|
||||
|
||||
if (profile is not null)
|
||||
{
|
||||
profiles[sessionID] = profileMigratorService.HandlePendingMigrations(profile);
|
||||
profiles[sessionID] = profileValidatorService.MigrateAndValidateProfile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't proceed further here as only one object in the profile has data in it.
|
||||
if (profiles[sessionID].ProfileInfo!.InvalidOrUnloadableProfile)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Run callbacks
|
||||
foreach (var callback in saveLoadRouters) // HealthSaveLoadRouter, InraidSaveLoadRouter, InsuranceSaveLoadRouter, ProfileSaveLoadRouter. THESE SHOULD EXIST IN HERE
|
||||
{
|
||||
@@ -229,6 +235,12 @@ public class SaveServer(
|
||||
/// <returns> Time taken to save the profile in seconds </returns>
|
||||
public async Task<long> SaveProfileAsync(MongoId sessionID)
|
||||
{
|
||||
// No need to save profiles that have been marked as invalid
|
||||
if (profiles[sessionID].ProfileInfo!.InvalidOrUnloadableProfile)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var filePath = $"{profileFilepath}{sessionID.ToString()}.json";
|
||||
|
||||
// Run pre-save callbacks before we save into json
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Exceptions;
|
||||
using SPTarkov.Server.Core.Extensions;
|
||||
using SPTarkov.Server.Core.Helpers;
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
@@ -512,261 +513,6 @@ public class ProfileFixerService(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks profile inventory for items that do not exist inside the items DB
|
||||
/// </summary>
|
||||
/// <param name="sessionId"> Session ID </param>
|
||||
/// <param name="fullProfile"> Profile to check inventory of </param>
|
||||
public void CheckForOrphanedModdedItems(MongoId sessionId, SptProfile fullProfile)
|
||||
{
|
||||
var itemsDb = databaseService.GetItems();
|
||||
var pmcProfile = fullProfile.CharacterData.PmcData;
|
||||
|
||||
var invalidItemIds = pmcProfile.Inventory.Items.Where(item => !itemsDb.ContainsKey(item.Template)).Select(item => item.Id).ToList();
|
||||
foreach (var invalidItemId in invalidItemIds)
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Deleting item id: {invalidItemId} from inventory and insurance");
|
||||
|
||||
// Add here so we can remove below
|
||||
inventoryHelper.RemoveItem(pmcProfile, invalidItemId, sessionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", invalidItemId.ToString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fullProfile.UserBuildData is not null)
|
||||
{
|
||||
// Remove invalid builds from weapon, equipment and magazine build lists
|
||||
var weaponBuilds = fullProfile.UserBuildData?.WeaponBuilds ?? [];
|
||||
fullProfile.UserBuildData.WeaponBuilds = weaponBuilds
|
||||
.Where(build => !ShouldRemoveWeaponEquipmentBuild("weapon", build, itemsDb))
|
||||
.ToList();
|
||||
|
||||
var equipmentBuilds = fullProfile.UserBuildData.EquipmentBuilds ?? [];
|
||||
fullProfile.UserBuildData.EquipmentBuilds = equipmentBuilds
|
||||
.Where(build => !ShouldRemoveWeaponEquipmentBuild("equipment", build, itemsDb))
|
||||
.ToList();
|
||||
|
||||
var magazineBuild = fullProfile.UserBuildData.MagazineBuilds ?? [];
|
||||
fullProfile.UserBuildData.MagazineBuilds = magazineBuild.Where(build => !ShouldRemoveMagazineBuild(build, itemsDb)).ToList();
|
||||
}
|
||||
|
||||
// Iterate over dialogs, looking for messages with items not found in item db, remove message if item found
|
||||
foreach (var dialog in fullProfile.DialogueRecords)
|
||||
{
|
||||
if (dialog.Value.Messages is null)
|
||||
{
|
||||
continue; // Skip dialog with no messages
|
||||
}
|
||||
|
||||
foreach (var message in dialog.Value.Messages)
|
||||
{
|
||||
if (message.Items?.Data is null)
|
||||
{
|
||||
continue; // skip messages with no items
|
||||
}
|
||||
|
||||
// Fix message with no items but have the flags to indicate items to collect
|
||||
if (message.Items.Data.Count == 0 && message.HasRewards.GetValueOrDefault(false))
|
||||
{
|
||||
message.HasRewards = false;
|
||||
message.RewardCollected = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find invalid items and remove from message
|
||||
var itemsToRemove = message.Items.Data.Where(item => !itemsDb.ContainsKey(item.Template)).ToList();
|
||||
foreach (var itemToRemove in itemsToRemove)
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
message.Items.Data.Remove(itemToRemove);
|
||||
logger.Warning(
|
||||
$"Item: {itemToRemove.Template} has resulted in the deletion of message: {message.Id} from dialog: {dialog.Key}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", itemToRemove.Template.ToString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var clothingDb = databaseService.GetTemplates().Customization;
|
||||
foreach (
|
||||
var clothingItem in fullProfile
|
||||
.CustomisationUnlocks.Where(customisation => customisation.Type == CustomisationType.SUITE)
|
||||
.ToList() // We're removing element, ToList to allow that to occur
|
||||
)
|
||||
{
|
||||
if (!clothingDb.ContainsKey(clothingItem.Id))
|
||||
{
|
||||
// Item in profile not found in db, not good
|
||||
logger.Error(serverLocalisationService.GetText("fixer-clothing_item_found", clothingItem.ToString()));
|
||||
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks.Remove(clothingItem);
|
||||
logger.Warning($"Non-default clothing purchase: {clothingItem} removed from profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var repeatable in fullProfile.CharacterData.PmcData.RepeatableQuests ?? [])
|
||||
{
|
||||
if (repeatable.ActiveQuests is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// ToList to prevent `Collection was modified` exception
|
||||
foreach (var activeQuest in repeatable.ActiveQuests.ToList())
|
||||
{
|
||||
if (!traderHelper.TraderExists(activeQuest.TraderId))
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning(
|
||||
$"Non-default quest: {activeQuest.Id} from trader: {activeQuest.TraderId} removed from RepeatableQuests list in profile"
|
||||
);
|
||||
repeatable.ActiveQuests.Remove(activeQuest);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-trader_found", activeQuest.TraderId.ToString()));
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activeQuest.Rewards?["Success"] is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get Item rewards only
|
||||
foreach (var successReward in activeQuest.Rewards["Success"].Where(reward => reward.Type == RewardType.Item))
|
||||
{
|
||||
if (successReward.Items.Any(item => !itemsDb.ContainsKey(item.Template)))
|
||||
{
|
||||
logger.Warning(
|
||||
$"Non-default repeatable quest: {activeQuest.Id} from trader: {activeQuest.TraderId} removed from RepeatableQuests list in profile"
|
||||
);
|
||||
repeatable.ActiveQuests.Remove(activeQuest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (traderId, _) in fullProfile.TraderPurchases.Where(traderPurchase => !traderHelper.TraderExists(traderPurchase.Key)))
|
||||
{
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Non-default trader: {traderId} purchase removed from traderPurchases list in profile");
|
||||
fullProfile.TraderPurchases.Remove(traderId);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-trader_found", traderId.ToString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a weapon build should be removed from the equipment list.
|
||||
/// </summary>
|
||||
/// <param name="buildType"> The type of build, used for logging only </param>
|
||||
/// <param name="build"> The build to check for invalid items </param>
|
||||
/// <param name="itemsDb"> The items database to use for item lookup </param>
|
||||
/// <returns> True if the build should be removed from the build list, false otherwise </returns>
|
||||
protected bool ShouldRemoveWeaponEquipmentBuild(string buildType, UserBuild build, Dictionary<MongoId, TemplateItem> itemsDb)
|
||||
{
|
||||
if (buildType == "weapon")
|
||||
// Get items not found in items db
|
||||
{
|
||||
foreach (var item in (build as WeaponBuild).Items.Where(item => !itemsDb.ContainsKey(item.Template)))
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.Template.ToString()));
|
||||
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Item: {item.Template} has resulted in the deletion of {buildType} build: {build.Name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor to be generic
|
||||
|
||||
if (buildType == "equipment")
|
||||
// Get items not found in items db
|
||||
{
|
||||
foreach (var item in (build as EquipmentBuild).Items.Where(item => !itemsDb.ContainsKey(item.Template)))
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.Template.ToString()));
|
||||
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Item: {item.Template} has resulted in the deletion of {buildType} build: {build.Name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Found a broken item
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether magazine build shou8ld be removed form the build list.
|
||||
/// </summary>
|
||||
/// <param name="magazineBuild"> The magazine build to check for validity </param>
|
||||
/// <param name="itemsDb"> The items database to use for item lookup </param>
|
||||
/// <returns> True if the build should be removed from the build list, false otherwise </returns>
|
||||
protected bool ShouldRemoveMagazineBuild(MagazineBuild magazineBuild, Dictionary<MongoId, TemplateItem> itemsDb)
|
||||
{
|
||||
foreach (var item in magazineBuild.Items)
|
||||
{
|
||||
// Magazine builds can have undefined items in them, skip those
|
||||
if (item is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check item exists in itemsDb
|
||||
if (!itemsDb.ContainsKey(item.TemplateId))
|
||||
{
|
||||
logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.TemplateId.ToString()));
|
||||
|
||||
if (_coreConfig.Fixes.RemoveModItemsFromProfile)
|
||||
{
|
||||
logger.Warning($"Item: {item.TemplateId} has resulted in the deletion of magazine build: {magazineBuild.Name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// REQUIRED for dev profiles <br />
|
||||
/// Iterate over players hideout areas and find what's built, look for missing bonuses those areas give and add them if missing
|
||||
|
||||
+21
-6
@@ -2,6 +2,7 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Extensions;
|
||||
using SPTarkov.Server.Core.Helpers;
|
||||
using SPTarkov.Server.Core.Migration;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
@@ -10,15 +11,16 @@ using SPTarkov.Server.Core.Utils;
|
||||
namespace SPTarkov.Server.Core.Services;
|
||||
|
||||
[Injectable(InjectionType.Singleton)]
|
||||
public class ProfileMigratorService(
|
||||
public class ProfileValidatorService(
|
||||
IEnumerable<IProfileMigration> profileMigrations,
|
||||
ProfileValidatorHelper profileValidatorHelper,
|
||||
TimeUtil timeUtil,
|
||||
ISptLogger<ProfileMigratorService> logger
|
||||
ISptLogger<ProfileValidatorService> logger
|
||||
)
|
||||
{
|
||||
private readonly IEnumerable<IProfileMigration> _sortedMigrations = profileMigrations.Sort();
|
||||
|
||||
public SptProfile HandlePendingMigrations(JsonObject profile)
|
||||
public SptProfile MigrateAndValidateProfile(JsonObject profile)
|
||||
{
|
||||
var profileId = profile["info"]?["id"]?.GetValue<string>();
|
||||
|
||||
@@ -35,6 +37,8 @@ public class ProfileMigratorService(
|
||||
|
||||
var ranMigrations = new List<IProfileMigration>();
|
||||
|
||||
// The initial part of the profile migrations, this allows for fixing up
|
||||
// Any incorrect typing that might not allow the profile to load
|
||||
foreach (var profileMigration in _sortedMigrations)
|
||||
{
|
||||
if (profileMigration.CanMigrate(profile, ranMigrations))
|
||||
@@ -59,10 +63,12 @@ public class ProfileMigratorService(
|
||||
sptReadyProfile =
|
||||
profile.Deserialize<SptProfile>(JsonUtil.JsonSerializerOptionsNoIndent)
|
||||
?? throw new InvalidOperationException($"Could not deserialize the profile.");
|
||||
|
||||
profileValidatorHelper.CheckForOrphanedModdedItems(new Models.Common.MongoId(profileId), sptReadyProfile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Critical($"Could not load profile: {profileId}");
|
||||
logger.Critical($"Failed to load profile with ID '{profileId}'. The profile will be marked as invalid.");
|
||||
logger.Critical(ex.ToString());
|
||||
|
||||
if (ex.StackTrace is not null)
|
||||
@@ -70,8 +76,17 @@ public class ProfileMigratorService(
|
||||
logger.Critical(ex.StackTrace);
|
||||
}
|
||||
|
||||
// Throw here, immediately stops execution of the server upon detecting a messed up profile
|
||||
throw;
|
||||
sptReadyProfile = new()
|
||||
{
|
||||
ProfileInfo = new Info
|
||||
{
|
||||
ProfileId = new Models.Common.MongoId(profileId),
|
||||
Username = profile["info"]?["username"]?.GetValue<string>() ?? "",
|
||||
InvalidOrUnloadableProfile = true,
|
||||
},
|
||||
};
|
||||
|
||||
return sptReadyProfile;
|
||||
}
|
||||
|
||||
foreach (var ranMigration in ranMigrations)
|
||||
Reference in New Issue
Block a user