using System.Text.Json.Nodes; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Services; namespace SPTarkov.Server.Core.Migration.Migrations; [Injectable] public class InvalidPocketFix(DatabaseService databaseService) : AbstractProfileMigration { public const string DEFAULT_POCKETS = "627a4e6b255f7527fb05a0f6"; public const string UNHEARD_POCKETS = "65e080be269cbd5c5005e529"; public override string FromVersion { get { return "~4.0"; } } public override string ToVersion { get { return "~4.0"; } } public override string MigrationName { get { return "InvalidPocketFix"; } } private enum PocketStatus { Valid, Missing, Invalid, } private PocketStatus GetPmcPocketStatus(JsonObject profile) { if (profile["characters"]?["pmc"]?["Inventory"]?["items"] is not JsonArray items) { // Uninitialized profile, just pass valid return PocketStatus.Valid; } foreach (var itemNode in items) { if (itemNode is not JsonObject itemObj) { continue; } if ( itemObj.TryGetPropertyValue("slotId", out var slotNode) && slotNode is JsonValue slotValue && slotValue.TryGetValue(out var slotId) && slotId == "Pockets" ) { if ( itemObj.TryGetPropertyValue("_tpl", out var tplNode) && tplNode is JsonValue tplValue && tplValue.TryGetValue(out var template) ) { return databaseService.GetItems().ContainsKey(template) ? PocketStatus.Valid : PocketStatus.Invalid; } } } return PocketStatus.Missing; } private PocketStatus GetScavPocketStatus(JsonObject profile) { if (profile["characters"]?["scav"]?["Inventory"]?["items"] is not JsonArray items) { // Uninitialized profile, just pass valid return PocketStatus.Valid; } foreach (var itemNode in items) { if (itemNode is not JsonObject itemObj) { continue; } if ( itemObj.TryGetPropertyValue("slotId", out var slotNode) && slotNode is JsonValue slotValue && slotValue.TryGetValue(out var slotId) && slotId == "Pockets" ) { if ( itemObj.TryGetPropertyValue("_tpl", out var tplNode) && tplNode is JsonValue tplValue && tplValue.TryGetValue(out var template) ) { return databaseService.GetItems().ContainsKey(template) ? PocketStatus.Valid : PocketStatus.Invalid; } } } return PocketStatus.Missing; } private bool HasCompletedOldPatterns(JsonObject profile) { if (profile["characters"]?["pmc"]?["Quests"] is not JsonArray quests) { return false; } foreach (var questNode in quests) { if (questNode is not JsonObject questObj) { continue; } if ( questObj.TryGetPropertyValue("qid", out var qIdNode) && qIdNode is JsonValue qIdValue && qIdValue.TryGetValue(out var qId) && qId == QuestTpl.OLD_PATTERNS.ToString() && questObj.TryGetPropertyValue("status", out var statusNode) && statusNode is JsonValue statusValue && statusValue.TryGetValue(out var status) && status.Equals(nameof(QuestStatusEnum.Success), StringComparison.OrdinalIgnoreCase) ) { return true; } } return false; } private bool IsUnheardProfile(JsonObject profile) { var gameVersion = profile?["characters"]?["pmc"]?["Info"]?["GameVersion"]?.GetValue(); if (!string.IsNullOrEmpty(gameVersion)) { return gameVersion.Equals("unheard_edition", StringComparison.OrdinalIgnoreCase); } return false; } private JsonObject CreatePocketItem(string parentId, string defaultPocketTpl) { return new JsonObject { ["_id"] = new MongoId().ToString(), ["_tpl"] = defaultPocketTpl, ["parentId"] = parentId, ["slotId"] = "Pockets", }; } // Set slotId to hideout, parentId to sorting table & remove location so that the sorting table will automatically pick a location private void MoveItemsToSortingTable(JsonArray items, string sortingTableId) { foreach (var item in items.OfType()) { if ( item.TryGetPropertyValue("slotId", out var slotNode) && slotNode is JsonValue slotNodeValue && slotNodeValue.TryGetValue(out var slotId) && ( ( slotId.StartsWith("pocket", StringComparison.OrdinalIgnoreCase) // Exclude the pcokets itself && !slotId.Equals("Pockets", StringComparison.OrdinalIgnoreCase) ) // Special slots are also keyed to the pockets || slotId.StartsWith("SpecialSlot", StringComparison.OrdinalIgnoreCase) ) ) { item["slotId"] = "hideout"; item["parentId"] = sortingTableId; item.Remove("location"); } } } public override bool CanMigrate(JsonObject profile, IEnumerable previouslyRanMigrations) { if (GetPmcPocketStatus(profile) != PocketStatus.Valid || GetScavPocketStatus(profile) != PocketStatus.Valid) { return true; } return false; } public override JsonObject? Migrate(JsonObject profile) { var pmcPocketStatus = GetPmcPocketStatus(profile); var scavPocketStatus = GetScavPocketStatus(profile); if (pmcPocketStatus != PocketStatus.Valid) { var items = profile["characters"]?["pmc"]?["Inventory"]?["items"] as JsonArray; var pmcInventory = profile["characters"]?["pmc"]?["Inventory"] as JsonObject; var pmcSortingTable = pmcInventory?["sortingTable"]?.GetValue()!; var pmcEquipment = pmcInventory?["equipment"]?.GetValue(); var pmcPocketTpl = DEFAULT_POCKETS; if (IsUnheardProfile(profile) || HasCompletedOldPatterns(profile)) { pmcPocketTpl = UNHEARD_POCKETS; } if (pmcPocketStatus == PocketStatus.Missing) { if (items != null && pmcEquipment != null) { items.Add(CreatePocketItem(pmcEquipment, pmcPocketTpl)); MoveItemsToSortingTable(items, pmcSortingTable); } } else if (pmcPocketStatus == PocketStatus.Invalid) { foreach (var item in items.OfType()) { if ( item.TryGetPropertyValue("slotId", out var slotNode) && slotNode is JsonValue slotNodeValue && slotNodeValue.TryGetValue(out var slotId) && slotId == "Pockets" ) { item["_tpl"] = pmcPocketTpl; MoveItemsToSortingTable(items, pmcSortingTable); } } } } if (scavPocketStatus != PocketStatus.Valid) { var scavItems = profile["characters"]?["scav"]?["Inventory"]?["items"] as JsonArray; var scavInventory = profile["characters"]?["scav"]?["Inventory"] as JsonObject; var scavEquipment = scavInventory?["equipment"]?.GetValue(); if (scavPocketStatus == PocketStatus.Missing) { if (scavItems != null && scavEquipment != null) { scavItems.Add(CreatePocketItem(scavEquipment, DEFAULT_POCKETS)); } } else if (scavPocketStatus == PocketStatus.Invalid) { foreach (var item in scavItems.OfType()) { if ( item.TryGetPropertyValue("slotId", out var slotNode) && slotNode is JsonValue slotNodeValue && slotNodeValue.TryGetValue(out var slotId) && slotId == "Pockets" ) { item["_tpl"] = DEFAULT_POCKETS; } } } } return base.Migrate(profile); } }