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");
+ }
+ }
}
///