From f6930b5a3a016da41fedc7ae0ee7a6b01743c392 Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 12:17:25 +0000 Subject: [PATCH] partially implemented `ProfileFixerService` --- Core/Services/ProfileFixerService.cs | 246 ++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 9 deletions(-) diff --git a/Core/Services/ProfileFixerService.cs b/Core/Services/ProfileFixerService.cs index 917890a7..f2a5b59d 100644 --- a/Core/Services/ProfileFixerService.cs +++ b/Core/Services/ProfileFixerService.cs @@ -1,22 +1,60 @@ -using Core.Annotations; +using Core.Annotations; +using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Hideout; using Core.Models.Eft.Profile; using Core.Models.Enums; +using Core.Utils; +using System.Text.RegularExpressions; +using ILogger = Core.Models.Utils.ILogger; namespace Core.Services; [Injectable(InjectionType.Singleton)] public class ProfileFixerService { + private readonly ILogger _logger; + private readonly HashUtil _hashUtil; + private readonly JsonUtil _jsonUtil; + private readonly ItemHelper _itemHelper; + private readonly DatabaseService _databaseService; + + public ProfileFixerService( + ILogger logger, + HashUtil hashUtil, + JsonUtil jsonUtil, + ItemHelper itemHelper, + DatabaseService databaseService) + { + _logger = logger; + _hashUtil = hashUtil; + _jsonUtil = jsonUtil; + _itemHelper = itemHelper; + _databaseService = databaseService; + } + /// /// Find issues in the pmc profile data that may cause issues and fix them /// /// profile to check and fix public void CheckForAndFixPmcProfileIssues(PmcData pmcProfile) { - throw new NotImplementedException(); + RemoveDanglingConditionCounters(pmcProfile); + RemoveDanglingTaskConditionCounters(pmcProfile); + RemoveOrphanedQuests(pmcProfile); + VerifyQuestProductionUnlocks(pmcProfile); + FixFavorites(pmcProfile); + + if (pmcProfile.Hideout is not null) + { + AddHideoutEliteSlots(pmcProfile); + } + + if (pmcProfile.Skills is not null) + { + CheckForSkillsOverMaxLevel(pmcProfile); + } } /// @@ -26,7 +64,38 @@ public class ProfileFixerService /// public void CheckForAndFixDialogueAttachments(SptProfile fullProfile) { - throw new NotImplementedException(); + foreach (var traderDialoguesKvP in fullProfile.DialogueRecords) + { + if (traderDialoguesKvP.Value.Messages is null) + { + continue; + } + + var traderDialogues = traderDialoguesKvP.Value; + foreach (var message in traderDialogues.Messages) { + // Skip any messages without attached items + if (message.Items?.Data is null || message.Items?.Stash is null) + { + continue; + } + + // Skip any messages that don't have a stashId collision with the player's equipment ID + if (message.Items?.Stash != fullProfile.CharacterData?.PmcData?.Inventory?.Equipment) + { + continue; + } + + // Otherwise we need to generate a new unique stash ID for this message's attachments + message.Items.Stash = _hashUtil.Generate(); + message.Items.Data = _itemHelper.AdoptOrphanedItems(message.Items.Stash, message.Items.Data); + + // Because `adoptOrphanedItems` sets the slotId to `hideout`, we need to re-set it to `main` to work with mail + foreach (var item in message.Items.Data.Where(item => item.SlotId == "hideout")) + { + item.SlotId = "main"; + } + } + } } /// @@ -35,7 +104,7 @@ public class ProfileFixerService /// profile to check and fix public void CheckForAndFixScavProfileIssues(PmcData scavProfile) { - throw new NotImplementedException(); + return; } /// @@ -44,7 +113,106 @@ public class ProfileFixerService /// Profile to check items of public void FixProfileBreakingInventoryItemIssues(PmcData pmcProfile) { - throw new NotImplementedException(); + // Create a mapping of all inventory items, keyed by _id value + var itemMapping = pmcProfile.Inventory.Items + .GroupBy(item => item.Id) + .ToDictionary(x => x.Key, x => x.ToList()); + + foreach (var mappingKvP in itemMapping) { + // Only one item for this id, not a dupe + if (mappingKvP.Value.Count == 1) + { + continue; + } + + _logger.Warning($"{ mappingKvP.Value.Count - 1} duplicate(s) found for item: {mappingKvP.Key}"); + var itemAJson = _jsonUtil.Serialize(mappingKvP.Value[0]); + var itemBJson = _jsonUtil.Serialize(mappingKvP.Value[1]); + if (itemAJson == itemBJson) + { + // Both items match, we can safely delete one (A) + var indexOfItemToRemove = pmcProfile.Inventory.Items.IndexOf(mappingKvP.Value[0]); + pmcProfile.Inventory.Items.RemoveAt(indexOfItemToRemove); + _logger.Warning($"Deleted duplicate item: {mappingKvP.Key}"); + } + else + { + // Items are different, replace ID with unique value + // Only replace ID if items have no children, we dont want orphaned children + var itemsHaveChildren = pmcProfile.Inventory.Items.Any((x) => x.ParentId == mappingKvP.Key); + if (!itemsHaveChildren) + { + var itemToAdjust = pmcProfile.Inventory.Items.FirstOrDefault((x) => x.Id == mappingKvP.Key); + itemToAdjust.Id = _hashUtil.Generate(); + _logger.Warning($"Replace duplicate item Id: {mappingKvP.Key} with {itemToAdjust.Id}"); + } + } + } + + // Iterate over all inventory items + foreach (var item in pmcProfile.Inventory.Items.Where((x) => x.SlotId is not null)) { + if (item.Upd is null) + { + // Ignore items without a upd object + continue; + } + + // Check items with a tags for non-alphanumeric characters and remove + Regex regxp = new Regex("[^a-zA-Z0-9 -]"); + if (item.Upd.Tag?.Name is not null && !regxp.IsMatch(item.Upd.Tag.Name)) + { + _logger.Warning($"Fixed item: { item.Id}s Tag value, removed invalid characters"); + item.Upd.Tag.Name = regxp.Replace(item.Upd.Tag.Name, ""); + } + + // Check items with StackObjectsCount (undefined) + if (item.Upd.StackObjectsCount is null) + { + _logger.Warning($"Fixed item: {item.Id}s undefined StackObjectsCount value, now set to 1"); + item.Upd.StackObjectsCount = 1; + } + } + + // Iterate over clothing + var customizationDb = _databaseService.GetTemplates().Customization; + var customizationDbArray = customizationDb.Values; + var playerIsUsec = pmcProfile.Info.Side.ToLower() == "usec"; + + // Check Head + if (customizationDb[pmcProfile.Customization.Head] is null) + { + var defaultHead = playerIsUsec + ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecHead") + : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearHead"); + pmcProfile.Customization.Head = defaultHead.Id; + } + + // check Body + if (customizationDb[pmcProfile.Customization.Body] is null) + { + var defaultBody = playerIsUsec + ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecBody") + : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearBody"); + pmcProfile.Customization.Body = defaultBody.Id; + } + + // check Hands + if (customizationDb[pmcProfile.Customization.Hands] is null) + { + var defaultHands = playerIsUsec + ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecHands") + : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearHands"); + pmcProfile.Customization.Hands = defaultHands.Id; + } + + // check Hands + if (customizationDb[pmcProfile.Customization.Feet] is null) + { + var defaultFeet = playerIsUsec + ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecFeet") + : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearFeet"); + pmcProfile.Customization.Feet = defaultFeet.Id; + } } /// @@ -54,7 +222,16 @@ public class ProfileFixerService /// profile to remove old counters from public void RemoveDanglingConditionCounters(PmcData pmcProfile) { - throw new NotImplementedException(); + if (pmcProfile.TaskConditionCounters is null) + { + return; + } + + foreach (var counterKvP in pmcProfile.TaskConditionCounters + .Where(counterKvP => counterKvP.Value.SourceId is null)) + { + pmcProfile.TaskConditionCounters.Remove(counterKvP.Key); + } } /// @@ -63,12 +240,50 @@ public class ProfileFixerService /// Player profile to check protected void RemoveDanglingTaskConditionCounters(PmcData pmcProfile) { - throw new NotImplementedException(); + if (pmcProfile.TaskConditionCounters is null) + { + return; + } + + var taskConditionKeysToRemove = new List(); + var activeRepeatableQuests = GetActiveRepeatableQuests(pmcProfile.RepeatableQuests); + var achievements = _databaseService.GetAchievements(); + + // Loop over TaskConditionCounters objects and add once we want to remove to counterKeysToRemove + foreach (var TaskConditionCounterKvP in pmcProfile.TaskConditionCounters) { + // Only check if profile has repeatable quests + if (pmcProfile.RepeatableQuests is not null && activeRepeatableQuests.Count > 0) + { + var existsInActiveRepeatableQuests = activeRepeatableQuests.Any( + (quest) => quest.Id == TaskConditionCounterKvP.Value.SourceId); + var existsInQuests = pmcProfile.Quests.Any( + (quest) => quest.QId == TaskConditionCounterKvP.Value.SourceId); + var isAchievementTracker = achievements.Any( + (quest) => quest.Id == TaskConditionCounterKvP.Value.SourceId); + + // If task conditions id is neither in activeQuests, quests or achievements - it's stale and should be cleaned up + if (!(existsInActiveRepeatableQuests || existsInQuests || isAchievementTracker)) + { + taskConditionKeysToRemove.Add(TaskConditionCounterKvP.Key); + } + } + } + + foreach (var counterKeyToRemove in taskConditionKeysToRemove) { + _logger.Debug($"Removed: {counterKeyToRemove} TaskConditionCounter object"); + pmcProfile.TaskConditionCounters.Remove(counterKeyToRemove); + } } protected List GetActiveRepeatableQuests(List repeatableQuests) { - throw new NotImplementedException(); + var activeQuests = new List(); + foreach (var repeatableQuest in repeatableQuests.Where(questType => questType.ActiveQuests?.Count > 0)) { + // daily/weekly collection has active quests in them, add to array and return + activeQuests.AddRange(repeatableQuest.ActiveQuests); + } + + return activeQuests; } /// @@ -77,7 +292,20 @@ public class ProfileFixerService /// Profile to remove dead quests from protected void RemoveOrphanedQuests(PmcData pmcProfile) { - throw new NotImplementedException(); + var quests = _databaseService.GetQuests(); + var profileQuests = pmcProfile.Quests; + + var activeRepeatableQuests = GetActiveRepeatableQuests(pmcProfile.RepeatableQuests); + + for (var i = profileQuests.Count - 1; i >= 0; i--) + { + + if (!quests.ContainsKey(profileQuests[i].QId) || activeRepeatableQuests.Any((x) => x.Id == profileQuests[i].QId)) + { + profileQuests.RemoveAt(i); + _logger.Success("Successfully removed orphaned quest that doesn't exist in quest data"); + } + } } ///