diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs index 1f3b98e0..fa71bdcd 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs @@ -12,7 +12,6 @@ using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; -using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Helpers; @@ -28,8 +27,8 @@ public class ProfileHelper( ConfigServer configServer ) { - protected static readonly FrozenSet _gameEditionsWithFreeRefresh = ["edge_of_darkness", "unheard_edition"]; - protected readonly InventoryConfig _inventoryConfig = configServer.GetConfig(); + protected static readonly FrozenSet GameEditionsWithFreeRefresh = ["edge_of_darkness", "unheard_edition"]; + protected readonly InventoryConfig InventoryConfig = configServer.GetConfig(); /// /// Remove/reset a completed quest condition from players profile quest data @@ -41,12 +40,10 @@ public class ProfileHelper( foreach (var questId in questConditionId) { var conditionId = questId.Value; - var profileQuest = pmcData.Quests.FirstOrDefault(q => q.QId == conditionId); + var profileQuest = pmcData.Quests?.FirstOrDefault(q => q.QId == conditionId); - if (profileQuest != null) // Remove condition - { - profileQuest.CompletedConditions.Remove(conditionId); - } + // Remove condition + profileQuest?.CompletedConditions?.Remove(conditionId); } } @@ -73,14 +70,14 @@ public class ProfileHelper( return output; } - var FullProfileClone = cloner.Clone(GetFullProfile(sessionId)); + var fullProfileClone = cloner.Clone(GetFullProfile(sessionId))!; // Sanitize any data the client can not receive - SanitizeProfileForClient(FullProfileClone); + SanitizeProfileForClient(fullProfileClone); // PMC must be at array index 0, scav at 1 - output.Add(FullProfileClone.CharacterData.PmcData); - output.Add(FullProfileClone.CharacterData.ScavData); + output.Add(fullProfileClone.CharacterData!.PmcData!); + output.Add(fullProfileClone.CharacterData!.ScavData!); return output; } @@ -93,7 +90,7 @@ public class ProfileHelper( { // Remove `loyaltyLevel` from `TradersInfo`, as otherwise it causes the client to not // properly calculate the player's `loyaltyLevel` - foreach (var trader in clonedProfile.CharacterData.PmcData.TradersInfo.Values) + foreach (var trader in clonedProfile.CharacterData?.PmcData?.TradersInfo.Values!) { trader.LoyaltyLevel = null; } @@ -103,24 +100,28 @@ public class ProfileHelper( /// Check if a nickname is used by another profile loaded by the server /// /// nickname request object - /// Session id + /// Session id /// True if already in use - public bool IsNicknameTaken(ValidateNicknameRequestData nicknameRequest, MongoId sessionID) + public bool IsNicknameTaken(ValidateNicknameRequestData nicknameRequest, MongoId sessionId) { var allProfiles = saveServer.GetProfiles().Values; // Find a profile that doesn't have same session id but has same name - return allProfiles.Any(p => - ProfileHasInfoProperty(p) - && !StringsMatch(p.ProfileInfo.ProfileId, sessionID) - && // SessionIds dont match - StringsMatch(p.CharacterData.PmcData.Info.LowerNickname.ToLowerInvariant(), nicknameRequest.Nickname.ToLowerInvariant()) + return allProfiles.Any(profile => + // Valid profile + ProfileHasInfoProperty(profile) + && profile.ProfileInfo?.ProfileId != sessionId + // SessionIds dont match + && StringsMatch( + profile.CharacterData?.PmcData?.Info?.LowerNickname?.ToLowerInvariant()!, + nicknameRequest.Nickname?.ToLowerInvariant()! + ) ); // Nicknames do } protected bool ProfileHasInfoProperty(SptProfile profile) { - return profile?.CharacterData?.PmcData?.Info != null; + return profile.CharacterData?.PmcData?.Info != null; } protected bool StringsMatch(string stringA, string stringB) @@ -131,18 +132,18 @@ public class ProfileHelper( /// /// Add experience to a PMC inside the players profile /// - /// Session id + /// Session id /// Experience to add to PMC character - public void AddExperienceToPmc(MongoId sessionID, int experienceToAdd) + public void AddExperienceToPmc(MongoId sessionId, int experienceToAdd) { - var pmcData = GetPmcProfile(sessionID); - if (pmcData != null) + var pmcData = GetPmcProfile(sessionId); + if (pmcData?.Info != null) { pmcData.Info.Experience += experienceToAdd; } else { - logger.Error($"Profile {sessionID} does not exist"); + logger.Error($"Profile {sessionId} does not exist"); } } @@ -201,22 +202,22 @@ public class ProfileHelper( Mods = [], ReceivedGifts = [], BlacklistedItemTemplates = [], - FreeRepeatableRefreshUsedCount = new(), - Migrations = new(), - CultistRewards = new(), + FreeRepeatableRefreshUsedCount = [], + Migrations = [], + CultistRewards = [], PendingPrestige = null, - ExtraRepeatableQuests = new(), + ExtraRepeatableQuests = [], }; } /// /// Get full representation of a players profile json /// - /// Profile id to get + /// Profile id to get /// SptProfile object - public SptProfile GetFullProfile(MongoId sessionID) + public SptProfile GetFullProfile(MongoId sessionId) { - return saveServer.GetProfile(sessionID); + return saveServer.GetProfile(sessionId); } /// @@ -232,23 +233,18 @@ public class ProfileHelper( logger.Error($"Account {accountId} does not exist"); } - return saveServer.GetProfiles().FirstOrDefault(p => p.Value?.ProfileInfo?.Aid == aid).Value; + return saveServer.GetProfiles().FirstOrDefault(p => p.Value.ProfileInfo?.Aid == aid).Value; } /// /// Retrieve a ChatRoomMember formatted profile for the given session ID /// - /// The session ID to return the profile for + /// The session ID to return the profile for /// - public SearchFriendResponse? GetChatRoomMemberFromSessionId(MongoId sessionID) + public SearchFriendResponse? GetChatRoomMemberFromSessionId(MongoId sessionId) { - var pmcProfile = GetFullProfile(sessionID)?.CharacterData?.PmcData; - if (pmcProfile == null) - { - return null; - } - - return GetChatRoomMemberFromPmcProfile(pmcProfile); + var pmcProfile = GetFullProfile(sessionId).CharacterData?.PmcData; + return pmcProfile is null ? null : GetChatRoomMemberFromPmcProfile(pmcProfile); } /// @@ -256,15 +252,15 @@ public class ProfileHelper( /// /// The PMC profile data to format into a ChatRoomMember structure /// - public SearchFriendResponse? GetChatRoomMemberFromPmcProfile(PmcData pmcProfile) + public SearchFriendResponse GetChatRoomMemberFromPmcProfile(PmcData pmcProfile) { return new SearchFriendResponse { - Id = pmcProfile.Id.Value, + Id = pmcProfile.Id!.Value, Aid = pmcProfile.Aid, Info = new UserDialogDetails { - Nickname = pmcProfile.Info.Nickname, + Nickname = pmcProfile.Info!.Nickname, Side = pmcProfile.Info.Side, Level = pmcProfile.Info.Level, MemberCategory = pmcProfile.Info.MemberCategory, @@ -276,11 +272,11 @@ public class ProfileHelper( /// /// Get a PMC profile by its session id /// - /// Profile id to return + /// Profile id to return /// PmcData object - public PmcData? GetPmcProfile(MongoId sessionID) + public PmcData? GetPmcProfile(MongoId sessionId) { - return GetFullProfile(sessionID)?.CharacterData?.PmcData; + return GetFullProfile(sessionId).CharacterData?.PmcData; } /// @@ -297,11 +293,11 @@ public class ProfileHelper( /// /// Get a full profiles scav-specific sub-profile /// - /// Profiles id + /// Profiles id /// IPmcData object - public PmcData? GetScavProfile(MongoId sessionID) + public PmcData? GetScavProfile(MongoId sessionId) { - return saveServer.GetProfile(sessionID).CharacterData?.ScavData; + return saveServer.GetProfile(sessionId).CharacterData?.ScavData; } /// @@ -340,12 +336,12 @@ public class ProfileHelper( /// /// is this profile flagged for data removal /// - /// Profile id + /// Profile id /// True if profile is to be wiped of data/progress /// TODO: logic doesn't feel right to have IsWiped being nullable - protected bool IsWiped(MongoId sessionID) + protected bool IsWiped(MongoId sessionId) { - return saveServer.GetProfile(sessionID)?.ProfileInfo?.IsWiped ?? false; + return saveServer.GetProfile(sessionId).ProfileInfo?.IsWiped ?? false; } /// @@ -355,15 +351,15 @@ public class ProfileHelper( /// profile without secure container public PmcData RemoveSecureContainer(PmcData profile) { - var items = profile.Inventory.Items; - var secureContainer = items.FirstOrDefault(i => i.SlotId == "SecuredContainer"); + var items = profile.Inventory?.Items; + var secureContainer = items?.FirstOrDefault(i => i.SlotId == "SecuredContainer"); if (secureContainer is not null) { // Find secure container + children - var secureContainerAndChildrenIds = items.GetItemWithChildrenTpls(secureContainer.Id).ToHashSet(); + var secureContainerAndChildrenIds = items?.GetItemWithChildrenTpls(secureContainer.Id).ToHashSet(); // Remove secure container + its children - items.RemoveAll(x => secureContainerAndChildrenIds.Contains(x.Id)); + items?.RemoveAll(x => (secureContainerAndChildrenIds?.Contains(x.Id) ?? false)); } return profile; @@ -379,7 +375,7 @@ public class ProfileHelper( public void FlagGiftReceivedInProfile(MongoId playerId, string giftId, int maxCount) { var profileToUpdate = GetFullProfile(playerId); - profileToUpdate.SptData.ReceivedGifts ??= []; + profileToUpdate.SptData!.ReceivedGifts ??= []; var giftData = profileToUpdate.SptData.ReceivedGifts.FirstOrDefault(g => g.GiftId == giftId); if (giftData != null) @@ -410,17 +406,7 @@ public class ProfileHelper( public bool PlayerHasReceivedMaxNumberOfGift(MongoId playerId, string giftId, int maxGiftCount) { var profile = GetFullProfile(playerId); - if (profile == null) - { - if (logger.IsLogEnabled(LogLevel.Debug)) - { - logger.Debug($"Unable to gift {giftId}, Profile: {playerId} does not exist"); - } - - return false; - } - - var giftDataFromProfile = profile.SptData.ReceivedGifts?.FirstOrDefault(g => g.GiftId == giftId); + var giftDataFromProfile = profile.SptData?.ReceivedGifts?.FirstOrDefault(g => g.GiftId == giftId); if (giftDataFromProfile == null) { return false; @@ -437,7 +423,7 @@ public class ProfileHelper( /// Was Includes in Node so might not be exact? public void IncrementStatCounter(CounterKeyValue[] counters, string keyToIncrement) { - var stat = counters.FirstOrDefault(c => c.Key.Contains(keyToIncrement)); + var stat = counters.FirstOrDefault(c => c.Key != null && c.Key.Contains(keyToIncrement)); if (stat != null) { stat.Value++; @@ -452,7 +438,7 @@ public class ProfileHelper( /// True if player has skill at elite level public bool HasEliteSkillLevel(SkillTypes skill, PmcData pmcProfile) { - var profileSkills = pmcProfile.Skills.Common; + var profileSkills = pmcProfile.Skills?.Common; if (profileSkills == null) { return false; @@ -488,7 +474,7 @@ public class ProfileHelper( return; } - var profileSkills = pmcProfile?.Skills?.Common; + var profileSkills = pmcProfile.Skills?.Common; if (profileSkills == null) { logger.Warning($"Unable to add: {pointsToAddToSkill} points to {skill}, Profile has no skills"); @@ -508,13 +494,13 @@ public class ProfileHelper( pointsToAddToSkill *= skillProgressRate; } - if (_inventoryConfig.SkillGainMultipliers.TryGetValue(skill.ToString(), out _)) + if (InventoryConfig.SkillGainMultipliers.TryGetValue(skill.ToString(), out _)) { - pointsToAddToSkill *= _inventoryConfig.SkillGainMultipliers[skill.ToString()]; + pointsToAddToSkill *= InventoryConfig.SkillGainMultipliers[skill.ToString()]; } profileSkill.Progress += pointsToAddToSkill; - profileSkill.Progress = Math.Min(profileSkill?.Progress ?? 0D, 5100); // Prevent skill from ever going above level 51 (5100) + profileSkill.Progress = Math.Min(profileSkill.Progress, 5100); // Prevent skill from ever going above level 51 (5100) profileSkill.PointsEarnedDuringSession += pointsToAddToSkill; @@ -524,11 +510,11 @@ public class ProfileHelper( /// /// Is the provided session id for a developer account /// - /// Profile id to check + /// Profile id to check /// True if account is developer - public bool IsDeveloperAccount(MongoId sessionID) + public bool IsDeveloperAccount(MongoId sessionId) { - return GetFullProfile(sessionID)?.ProfileInfo?.Edition?.ToLowerInvariant().StartsWith("spt developer") ?? false; + return GetFullProfile(sessionId).ProfileInfo?.Edition?.ToLowerInvariant().StartsWith("spt developer") ?? false; } /// @@ -574,7 +560,7 @@ public class ProfileHelper( public bool HasAccessToRepeatableFreeRefreshSystem(PmcData pmcProfile) { - return _gameEditionsWithFreeRefresh.Contains(pmcProfile.Info.GameVersion); + return GameEditionsWithFreeRefresh.Contains(pmcProfile.Info?.GameVersion ?? string.Empty); } /// @@ -586,8 +572,8 @@ public class ProfileHelper( { // Find all pockets in profile, may be multiple as they could have equipment stand // (1 pocket for each upgrade level of equipment stand) - var pockets = pmcProfile.Inventory.Items.Where(i => i.SlotId == "Pockets"); - if (!pockets.Any()) + var pockets = pmcProfile.Inventory?.Items?.Where(i => i.SlotId == "Pockets"); + if (pockets is null || !pockets.Any()) { logger.Error($"Unable to replace profile: {pmcProfile.Id} pocket tpl with: {newPocketTpl} as Pocket item could not be found."); return; @@ -611,11 +597,11 @@ public class ProfileHelper( foreach (var itemId in profile.Inventory?.FavoriteItems ?? []) { // When viewing another users profile, the client expects a full item with children, so get that - var itemAndChildren = profile.Inventory.Items.GetItemWithChildren(itemId); + var itemAndChildren = profile.Inventory?.Items?.GetItemWithChildren(itemId); if (itemAndChildren?.Count > 0) { // To get the client to actually see the items, we set the main item's parent to null, so it's treated as a root item - var clonedItems = cloner.Clone(itemAndChildren); + var clonedItems = cloner.Clone(itemAndChildren)!; clonedItems.First().ParentId = null; fullFavorites.AddRange(clonedItems); @@ -627,73 +613,78 @@ public class ProfileHelper( public void AddHideoutCustomisationUnlock(SptProfile fullProfile, Reward reward, string source) { - if (fullProfile?.CustomisationUnlocks == null) + if (reward.Target is null) { - fullProfile.CustomisationUnlocks = []; + logger.Error("Unable to add hideout customisation unlock, reward.Target is null."); + return; } - if (fullProfile?.CustomisationUnlocks?.Any(u => u.Id == reward.Target) ?? false) + fullProfile.CustomisationUnlocks ??= []; + + if (fullProfile.CustomisationUnlocks?.Any(u => u.Id == reward.Target) ?? false) { logger.Warning( - $"Profile: {fullProfile.ProfileInfo.ProfileId} already has hideout customisation reward: {reward.Target}, skipping" + $"Profile: {fullProfile.ProfileInfo?.ProfileId ?? "`ProfileId is null`"} already has hideout customisation reward: {reward.Target}, skipping" ); return; } var customisationTemplateDb = databaseService.GetTemplates().Customization; - var matchingCustomisation = customisationTemplateDb.GetValueOrDefault(reward.Target, null); - if (matchingCustomisation is not null) + if (!customisationTemplateDb.TryGetValue(reward.Target, out var template)) { - var rewardToStore = new CustomisationStorage - { - Id = new MongoId(reward.Target), - Source = source, - Type = null, - }; - - switch (matchingCustomisation.Parent) - { - case CustomisationTypeId.MANNEQUIN_POSE: - rewardToStore.Type = CustomisationType.MANNEQUIN_POSE; - break; - case CustomisationTypeId.GESTURES: - rewardToStore.Type = CustomisationType.GESTURE; - break; - case CustomisationTypeId.FLOOR: - rewardToStore.Type = CustomisationType.FLOOR; - break; - case CustomisationTypeId.DOG_TAGS: - rewardToStore.Type = CustomisationType.DOG_TAG; - break; - case CustomisationTypeId.CEILING: - rewardToStore.Type = CustomisationType.CEILING; - break; - case CustomisationTypeId.WALL: - rewardToStore.Type = CustomisationType.WALL; - break; - case CustomisationTypeId.ENVIRONMENT_UI: - rewardToStore.Type = CustomisationType.ENVIRONMENT; - break; - case CustomisationTypeId.SHOOTING_RANGE_MARK: - rewardToStore.Type = CustomisationType.SHOOTING_RANGE_MARK; - break; - case CustomisationTypeId.VOICE: - rewardToStore.Type = CustomisationType.VOICE; - break; - case CustomisationTypeId.LIGHT: - rewardToStore.Type = CustomisationType.LIGHT; - break; - case CustomisationTypeId.UPPER: - rewardToStore.Type = CustomisationType.UPPER; - break; - default: - logger.Error($"Unhandled customisation unlock type: {matchingCustomisation.Parent} not added to profile"); - return; - } - - fullProfile.CustomisationUnlocks.Add(rewardToStore); + logger.Error("Unable to find customisation reward template"); + return; } + + var rewardToStore = new CustomisationStorage + { + Id = new MongoId(reward.Target), + Source = source, + Type = null, + }; + + switch (template.Parent) + { + case CustomisationTypeId.MANNEQUIN_POSE: + rewardToStore.Type = CustomisationType.MANNEQUIN_POSE; + break; + case CustomisationTypeId.GESTURES: + rewardToStore.Type = CustomisationType.GESTURE; + break; + case CustomisationTypeId.FLOOR: + rewardToStore.Type = CustomisationType.FLOOR; + break; + case CustomisationTypeId.DOG_TAGS: + rewardToStore.Type = CustomisationType.DOG_TAG; + break; + case CustomisationTypeId.CEILING: + rewardToStore.Type = CustomisationType.CEILING; + break; + case CustomisationTypeId.WALL: + rewardToStore.Type = CustomisationType.WALL; + break; + case CustomisationTypeId.ENVIRONMENT_UI: + rewardToStore.Type = CustomisationType.ENVIRONMENT; + break; + case CustomisationTypeId.SHOOTING_RANGE_MARK: + rewardToStore.Type = CustomisationType.SHOOTING_RANGE_MARK; + break; + case CustomisationTypeId.VOICE: + rewardToStore.Type = CustomisationType.VOICE; + break; + case CustomisationTypeId.LIGHT: + rewardToStore.Type = CustomisationType.LIGHT; + break; + case CustomisationTypeId.UPPER: + rewardToStore.Type = CustomisationType.UPPER; + break; + default: + logger.Error($"Unhandled customisation unlock type: {template.Parent} not added to profile"); + return; + } + + fullProfile.CustomisationUnlocks?.Add(rewardToStore); } /// @@ -702,12 +693,16 @@ public class ProfileHelper( /// Edition of profile desired, e.g. "Standard" /// Side of profile desired, e.g. "Bear" /// - public TemplateSide GetProfileTemplateForSide(string accountEdition, string side) + public TemplateSide? GetProfileTemplateForSide(string accountEdition, string side) { var profileTemplates = databaseService.GetProfileTemplates(); // Get matching profile 'type' e.g. 'standard' - profileTemplates.TryGetValue(accountEdition, out var matchingProfileTemplate); + if (!profileTemplates.TryGetValue(accountEdition, out var matchingProfileTemplate)) + { + logger.Error($"Unable to find profile template for account edition: {accountEdition} and side: {side}"); + return null; + } // Get matching profile by 'side' e.g. USEC return string.Equals(side, "bear", StringComparison.OrdinalIgnoreCase) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ProfileValidatorHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ProfileValidatorHelper.cs index c4f042c8..5a0a17df 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ProfileValidatorHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ProfileValidatorHelper.cs @@ -2,6 +2,7 @@ using SPTarkov.Server.Core.Exceptions.Items; using SPTarkov.Server.Core.Extensions; 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.Profile; using SPTarkov.Server.Core.Models.Enums; @@ -21,7 +22,7 @@ public class ProfileValidatorHelper( TraderStore traderStore ) { - protected readonly CoreConfig _coreConfig = configServer.GetConfig(); + protected readonly CoreConfig CoreConfig = configServer.GetConfig(); /// /// Checks profile inventory for items that do not exist inside the items DB @@ -31,44 +32,98 @@ public class ProfileValidatorHelper( /// Thrown if RemoveModItemsFromProfile is false. /// Thrown if RemoveModItemsFromProfile is false. /// Thrown if RemoveModItemsFromProfile is false. - public void CheckForOrphanedModdedItems(MongoId sessionId, SptProfile fullProfile) + /// Exceptions thrown are from called methods, this method does not throw exceptions directly, but they are possible. + public void CheckForOrphanedModdedData(MongoId sessionId, SptProfile fullProfile) + { + RemoveInvalidItems(sessionId, fullProfile); + RemoveInvalidUserBuilds(fullProfile); + RemoveInvalidDialogRecords(fullProfile); + RemoveInvalidClothing(fullProfile); + RemoveInvalidRepeatableQuests(fullProfile); + RemoveInvalidTraderPurchases(fullProfile); + } + + /// + /// Removes all invalid item ids from the provided profile + /// + /// SessionId to check + /// Full profile to check + /// Thrown if RemoveModItemsFromProfile is false. + protected void RemoveInvalidItems(MongoId sessionId, SptProfile fullProfile) { var itemsDb = databaseService.GetItems(); - var pmcProfile = fullProfile.CharacterData.PmcData; + var pmcProfile = fullProfile.CharacterData?.PmcData; + + var invalidItemIds = pmcProfile + ?.Inventory?.Items?.Where(item => !itemsDb.ContainsKey(item.Template)) + .Select(item => item.Id) + .ToList(); + + // No invalid items + if (invalidItemIds is null || invalidItemIds.Count == 0) + { + return; + } - 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) + 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); + pmcProfile?.RemoveItem(invalidItemId, sessionId); } else { throw new InvalidModdedItemException(serverLocalisationService.GetText("fixer-mod_item_found", invalidItemId.ToString())); } } + } - if (fullProfile.UserBuildData is not null) + /// + /// Checks for and removes invalid user builds containing items that no longer exist + /// + /// Full profile to check + protected void RemoveInvalidUserBuilds(SptProfile fullProfile) + { + // No user build data to remove + if (fullProfile.UserBuildData is 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(); + return; } + var itemsDb = databaseService.GetItems(); + + // 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(); + } + + /// + /// Check for and remove invalid user dialogs + /// + /// Full profile to check + /// Thrown if RemoveModItemsFromProfile is false. + protected void RemoveInvalidDialogRecords(SptProfile fullProfile) + { + if (fullProfile.DialogueRecords is null) + { + return; + } + + var itemsDb = databaseService.GetItems(); + // Iterate over dialogs, looking for messages with items not found in item db, remove message if item found foreach (var dialog in fullProfile.DialogueRecords) { @@ -96,47 +151,79 @@ public class ProfileValidatorHelper( 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 + // We've found an item to remove, but the remove config isn't enabled, throw an exception + if (!CoreConfig.Fixes.RemoveModItemsFromProfile) { 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()) + message.Items.Data.Remove(itemToRemove); + logger.Warning( + $"Item: {itemToRemove.Template} has resulted in the deletion of message: {message.Id} from dialog: {dialog.Key}" ); } } } + } - foreach (var repeatable in fullProfile.CharacterData.PmcData.RepeatableQuests ?? []) + /// + /// Check for and remove invalid clothing items + /// + /// Full profile to check + /// Thrown if RemoveModItemsFromProfile is false. + protected void RemoveInvalidClothing(SptProfile fullProfile) + { + var clothingDb = databaseService.GetTemplates().Customization; + + // We're removing element, ToList to allow that to occur + var clothingItems = fullProfile + .CustomisationUnlocks?.Where(customisation => customisation.Type == CustomisationType.SUITE) + .ToList(); + + // Nothing to remove + if (clothingItems is null || clothingItems.Count == 0) + { + return; + } + + foreach (var clothingItem in clothingItems) + { + // Valid item, skip + if (clothingDb.ContainsKey(clothingItem.Id)) + { + continue; + } + + // Found a clothing item to remove but the fixer isn't enabled, throw an exception + if (!CoreConfig.Fixes.RemoveModItemsFromProfile) + { + throw new InvalidModdedClothingException( + serverLocalisationService.GetText("fixer-clothing_item_found", clothingItem.ToString()) + ); + } + + fullProfile.CustomisationUnlocks?.Remove(clothingItem); + logger.Warning($"Non-default clothing purchase: {clothingItem} removed from profile"); + } + } + + /// + /// Check for and remove invalid repeatable quests + /// + /// Full profile to check + /// Thrown if RemoveModItemsFromProfile is false. + protected void RemoveInvalidRepeatableQuests(SptProfile fullProfile) + { + // Nothing to remove + if (fullProfile.CharacterData?.PmcData?.RepeatableQuests is null) + { + return; + } + + var itemsDb = databaseService.GetItems(); + foreach (var repeatable in fullProfile.CharacterData.PmcData.RepeatableQuests) { if (repeatable.ActiveQuests is null) { @@ -148,20 +235,19 @@ public class ProfileValidatorHelper( { 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 + // We found a trader that doesn't exist, but the fixer isnt enabled, throw an exception + if (!CoreConfig.Fixes.RemoveModItemsFromProfile) { throw new InvalidModdedTraderException( serverLocalisationService.GetText("fixer-trader_found", activeQuest.TraderId.ToString()) ); } + repeatable.ActiveQuests.Remove(activeQuest); + logger.Warning( + $"Non-default quest: {activeQuest.Id} from trader: {activeQuest.TraderId} removed from RepeatableQuests list in profile" + ); + continue; } @@ -173,7 +259,7 @@ public class ProfileValidatorHelper( // 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))) + if (successReward.Items?.Any(item => !itemsDb.ContainsKey(item.Template)) ?? false) { logger.Warning( $"Non-default repeatable quest: {activeQuest.Id} from trader: {activeQuest.TraderId} removed from RepeatableQuests list in profile" @@ -183,18 +269,33 @@ public class ProfileValidatorHelper( } } } + } - foreach (var (traderId, _) in fullProfile.TraderPurchases.Where(traderPurchase => !DoesTraderExist(traderPurchase.Key))) + /// + /// Check for and remove invalid trader purchases from traders that no longer exist + /// + /// Full profile to check + /// Thrown if RemoveModItemsFromProfile is false. + protected void RemoveInvalidTraderPurchases(SptProfile fullProfile) + { + var purchases = fullProfile.TraderPurchases?.Where(traderPurchase => !DoesTraderExist(traderPurchase.Key)); + + // Nothing to remove + if (purchases is null || !purchases.Any()) { - if (_coreConfig.Fixes.RemoveModItemsFromProfile) - { - logger.Warning($"Non-default trader: {traderId} purchase removed from traderPurchases list in profile"); - fullProfile.TraderPurchases.Remove(traderId); - } - else + return; + } + + foreach (var (traderId, _) in purchases) + { + // We have purchases to remove and the fixer isn't enabled, throw an exception + if (!CoreConfig.Fixes.RemoveModItemsFromProfile) { throw new InvalidModdedTraderException(serverLocalisationService.GetText("fixer-trader_found", traderId.ToString())); } + + logger.Warning($"Non-default trader: {traderId} purchase removed from traderPurchases list in profile"); + fullProfile.TraderPurchases?.Remove(traderId); } } @@ -214,7 +315,7 @@ public class ProfileValidatorHelper( { logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.Template.ToString())); - if (_coreConfig.Fixes.RemoveModItemsFromProfile) + if (CoreConfig.Fixes.RemoveModItemsFromProfile) { logger.Warning($"Item: {item.Template} has resulted in the deletion of {buildType} build: {build.Name}"); @@ -234,7 +335,7 @@ public class ProfileValidatorHelper( { logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.Template.ToString())); - if (_coreConfig.Fixes.RemoveModItemsFromProfile) + if (CoreConfig.Fixes.RemoveModItemsFromProfile) { logger.Warning($"Item: {item.Template} has resulted in the deletion of {buildType} build: {build.Name}"); @@ -270,7 +371,7 @@ public class ProfileValidatorHelper( { logger.Error(serverLocalisationService.GetText("fixer-mod_item_found", item.TemplateId.ToString())); - if (_coreConfig.Fixes.RemoveModItemsFromProfile) + if (CoreConfig.Fixes.RemoveModItemsFromProfile) { logger.Warning($"Item: {item.TemplateId} has resulted in the deletion of magazine build: {magazineBuild.Name}"); diff --git a/Libraries/SPTarkov.Server.Core/Services/ProfileValidatorService.cs b/Libraries/SPTarkov.Server.Core/Services/ProfileValidatorService.cs index 70b00e49..e9b3fb73 100644 --- a/Libraries/SPTarkov.Server.Core/Services/ProfileValidatorService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/ProfileValidatorService.cs @@ -70,7 +70,7 @@ public class ProfileValidatorService( profile.Deserialize(JsonUtil.JsonSerializerOptionsNoIndent) ?? throw new InvalidOperationException($"Could not deserialize the profile."); - profileValidatorHelper.CheckForOrphanedModdedItems(new Models.Common.MongoId(profileId), sptReadyProfile); + profileValidatorHelper.CheckForOrphanedModdedData(new Models.Common.MongoId(profileId), sptReadyProfile); } catch (Exception ex) {