From 93b50f2d4dbc59d34359597c9030bed428090c1b Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 18:48:00 +0000 Subject: [PATCH 1/9] Updated `_botGenerator.GeneratePlayerScav` to return a `PmcData` --- Core/Generators/BotGenerator.cs | 36 ++++++++++++++++++++++++-- Core/Generators/PlayerScavGenerator.cs | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Core/Generators/BotGenerator.cs b/Core/Generators/BotGenerator.cs index 53f5b3fe..580500a9 100644 --- a/Core/Generators/BotGenerator.cs +++ b/Core/Generators/BotGenerator.cs @@ -42,7 +42,7 @@ public class BotGenerator /// base bot template to use (e.g. assault/pmcbot) /// profile of player generating pscav /// BotBase - public BotBase GeneratePlayerScav(string sessionId, string role, string difficulty, BotType botTemplate, PmcData profile) + public PmcData GeneratePlayerScav(string sessionId, string role, string difficulty, BotType botTemplate, PmcData profile) { var bot = GetCloneOfBotBase(); bot.Info.Settings.BotDifficulty = difficulty; @@ -65,7 +65,39 @@ public class BotGenerator // Sets the name after scav name shown in parentheses bot.Info.MainProfileNickname = profile.Info.Nickname; - return bot; + return new PmcData + { + Id = bot.Id, + Aid = bot.Aid, + SessionId = bot.SessionId, + Savage = bot.Savage, + KarmaValue = bot.KarmaValue, + Info = bot.Info, + Customization = bot.Customization, + Health = bot.Health, + Inventory = bot.Inventory, + Skills = bot.Skills, + Stats = bot.Stats, + Encyclopedia = bot.Encyclopedia, + TaskConditionCounters = bot.TaskConditionCounters, + InsuredItems = bot.InsuredItems, + Hideout = bot.Hideout, + Quests = bot.Quests, + TradersInfo = bot.TradersInfo, + UnlockedInfo = bot.UnlockedInfo, + RagfairInfo = bot.RagfairInfo, + Achievements = bot.Achievements, + RepeatableQuests = bot.RepeatableQuests, + Bonuses = bot.Bonuses, + Notes = bot.Notes, + CarExtractCounts = bot.CarExtractCounts, + CoopExtractCounts = bot.CoopExtractCounts, + SurvivorClass = bot.SurvivorClass, + WishList = bot.WishList, + MoneyTransferLimitData = bot.MoneyTransferLimitData, + IsPmc = bot.IsPmc, + Prestige = new Prestige() + }; } /// diff --git a/Core/Generators/PlayerScavGenerator.cs b/Core/Generators/PlayerScavGenerator.cs index 45d4e396..0d4e8a58 100644 --- a/Core/Generators/PlayerScavGenerator.cs +++ b/Core/Generators/PlayerScavGenerator.cs @@ -100,7 +100,7 @@ public class PlayerScavGenerator var baseBotNode = ConstructBotBaseTemplate(playerScavKarmaSettings.BotTypeForLoot); AdjustBotTemplateWithKarmaSpecificSettings(playerScavKarmaSettings, baseBotNode); - var scavData = (PmcData)_botGenerator.GeneratePlayerScav( + var scavData = _botGenerator.GeneratePlayerScav( sessionID, playerScavKarmaSettings.BotTypeForLoot.ToLower(), "easy", From ac1c026399f5bf664a4d2e225d070f4b2d56f2c1 Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 18:50:57 +0000 Subject: [PATCH 2/9] Expanded stubbiness of `GenerateBot` --- Core/Generators/BotGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Core/Generators/BotGenerator.cs b/Core/Generators/BotGenerator.cs index 580500a9..e1a7bd56 100644 --- a/Core/Generators/BotGenerator.cs +++ b/Core/Generators/BotGenerator.cs @@ -144,6 +144,8 @@ public class BotGenerator { _logger.Error("NOT IMPLEMENTED BotGenerator.GenerateBot"); + bot.Inventory.Items = []; + return bot; } From 572575802fcc2ce5e0d9a7383b469673bb9042c7 Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 18:51:22 +0000 Subject: [PATCH 3/9] Improved if check --- Core/Helpers/ProfileHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Helpers/ProfileHelper.cs b/Core/Helpers/ProfileHelper.cs index 635c23a6..34dc26a6 100644 --- a/Core/Helpers/ProfileHelper.cs +++ b/Core/Helpers/ProfileHelper.cs @@ -1,4 +1,4 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Profile; @@ -353,7 +353,7 @@ public class ProfileHelper { var items = profile.Inventory.Items; var secureContainer = items.First(i => i.SlotId == "SecuredContainer"); - if (secureContainer != null) + if (secureContainer is not null) { // Find and remove container + children var childItemsInSecureContainer = _itemHelper.FindAndReturnChildrenByItems(items, secureContainer.Id); From 251ccb9a04362d7e1dec33fd36cb3071a065f4fb Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 19:03:10 +0000 Subject: [PATCH 4/9] Implemented `GetFenceInfo` --- Core/Helpers/ProfileHelper.cs | 2 +- Core/Services/FenceService.cs | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Core/Helpers/ProfileHelper.cs b/Core/Helpers/ProfileHelper.cs index 34dc26a6..da486721 100644 --- a/Core/Helpers/ProfileHelper.cs +++ b/Core/Helpers/ProfileHelper.cs @@ -352,7 +352,7 @@ public class ProfileHelper public PmcData RemoveSecureContainer(PmcData profile) { var items = profile.Inventory.Items; - var secureContainer = items.First(i => i.SlotId == "SecuredContainer"); + var secureContainer = items.FirstOrDefault(i => i.SlotId == "SecuredContainer"); if (secureContainer is not null) { // Find and remove container + children diff --git a/Core/Services/FenceService.cs b/Core/Services/FenceService.cs index d11c465c..13c1d3bd 100644 --- a/Core/Services/FenceService.cs +++ b/Core/Services/FenceService.cs @@ -1,4 +1,4 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Spt.Config; @@ -9,6 +9,14 @@ namespace Core.Services; [Injectable(InjectionType.Singleton)] public class FenceService { + private readonly DatabaseService _databaseService; + + public FenceService( + DatabaseService databaseService) + { + _databaseService = databaseService; + } + /// /// Replace main fence assort with new assort /// @@ -495,7 +503,30 @@ public class FenceService /// FenceLevel object public FenceLevel GetFenceInfo(PmcData pmcData) { - throw new NotImplementedException(); + var fenceSettings = _databaseService.GetGlobals().Configuration.FenceSettings; + pmcData.TradersInfo.TryGetValue(fenceSettings.FenceIdentifier, out var pmcFenceInfo); + + if (pmcFenceInfo is null) + { + return fenceSettings.Levels["0"]; + } + + var fenceLevels = fenceSettings.Levels.Select(x => x.Key); + var minLevel = fenceLevels.Min(); + var maxLevel = fenceLevels.Max(); + var pmcFenceLevel = Math.Floor(pmcFenceInfo.Standing.Value); + + if (pmcFenceLevel < int.Parse(minLevel)) + { + return fenceSettings.Levels[minLevel]; + } + + if (pmcFenceLevel > int.Parse(maxLevel)) + { + return fenceSettings.Levels[maxLevel]; + } + + return fenceSettings.Levels[pmcFenceLevel.ToString()]; } /// From a41fbdfc394371a1dc3ee33a26cf5367b53c5015 Mon Sep 17 00:00:00 2001 From: CWX Date: Mon, 13 Jan 2025 19:04:22 +0000 Subject: [PATCH 5/9] start TraderController --- Core/Controllers/TradeController.cs | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Core/Controllers/TradeController.cs b/Core/Controllers/TradeController.cs index adf2bc31..030add0c 100644 --- a/Core/Controllers/TradeController.cs +++ b/Core/Controllers/TradeController.cs @@ -1,16 +1,80 @@ using Core.Annotations; +using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Ragfair; using Core.Models.Eft.Trade; using Core.Models.Enums; +using Core.Models.Spt.Config; +using Core.Routers; +using Core.Servers; +using Core.Services; +using Core.Utils; +using ILogger = Core.Models.Utils.ILogger; namespace Core.Controllers; [Injectable] public class TradeController { + private readonly ILogger _logger; + private readonly DatabaseService _databaseService; + private readonly EventOutputHolder _eventOutputHolder; + private readonly TradeHelper _tradeHelper; + private readonly TimeUtil _timeUtil; + private readonly HashUtil _hashUtil; + private readonly ItemHelper _itemHelper; + private readonly ProfileHelper _profileHelper; + private readonly RagfairOfferHelper _ragfairOfferHelper; + private readonly TraderHelper _traderHelper; + // private readonly RagfairServer _ragfairServer; + private readonly HttpResponseUtil _httpResponseUtil; + private readonly LocalisationService _localisationService; + private readonly RagfairPriceService _ragfairPriceService; + // private readonly MailSendService _mailSendService; + private readonly ConfigServer _configServer; + + private readonly RagfairConfig _ragfairConfig; + private readonly TraderConfig _traderConfig; + + public TradeController + ( + ILogger logger, + DatabaseService databaseService, + EventOutputHolder eventOutputHolder, + TradeHelper tradeHelper, + TimeUtil timeUtil, + HashUtil hashUtil, + ItemHelper itemHelper, + ProfileHelper profileHelper, + RagfairOfferHelper ragfairOfferHelper, + TraderHelper traderHelper, + HttpResponseUtil httpResponseUtil, + LocalisationService localisationService, + RagfairPriceService ragfairPriceService, + ConfigServer configServer + ) + { + _logger = logger; + _databaseService = databaseService; + _eventOutputHolder = eventOutputHolder; + _tradeHelper = tradeHelper; + _timeUtil = timeUtil; + _hashUtil = hashUtil; + _itemHelper = itemHelper; + _profileHelper = profileHelper; + _ragfairOfferHelper = ragfairOfferHelper; + _traderHelper = traderHelper; + _httpResponseUtil = httpResponseUtil; + _localisationService = localisationService; + _ragfairPriceService = ragfairPriceService; + _configServer = configServer; + + _ragfairConfig = _configServer.GetConfig(ConfigTypes.RAGFAIR); + _traderConfig = _configServer.GetConfig(ConfigTypes.TRADER); + } + /// /// Handle TradingConfirm event /// From 4a56197bc897baea50492e30d178c55f3960f35f Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 19:21:27 +0000 Subject: [PATCH 6/9] Implemented `WarnOnActiveBotReloadSkill` and `UpdateProfileHealthValues` --- Core/Controllers/GameController.cs | 100 ++++++++++++++++++++++- Core/Models/Eft/Common/Tables/BotBase.cs | 13 +-- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/Core/Controllers/GameController.cs b/Core/Controllers/GameController.cs index 23a90972..d4a58705 100644 --- a/Core/Controllers/GameController.cs +++ b/Core/Controllers/GameController.cs @@ -3,6 +3,7 @@ using Core.Context; using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Game; +using Core.Models.Eft.Health; using Core.Models.Eft.Profile; using Core.Models.Enums; using Core.Models.Spt.Config; @@ -356,7 +357,11 @@ public class GameController /// Player profile private void WarnOnActiveBotReloadSkill(PmcData pmcProfile) { - throw new NotImplementedException(); + var botReloadSkill = _profileHelper.GetSkillFromProfile(pmcProfile, SkillTypes.BotReload); + if (botReloadSkill?.Progress > 0) + { + _logger.Warning(_localisationService.GetText("server_start_player_active_botreload_skill")); + } } /// @@ -365,7 +370,98 @@ public class GameController /// Profile to adjust values for private void UpdateProfileHealthValues(PmcData pmcProfile) { - throw new NotImplementedException(); + var healthLastUpdated = pmcProfile.Health.UpdateTime; + var currentTimeStamp = _timeUtil.GetTimeStamp(); + var diffSeconds = currentTimeStamp - healthLastUpdated; + + // Last update is in past + if (healthLastUpdated < currentTimeStamp) + { + // Base values + double energyRegenPerHour = 60; + double hydrationRegenPerHour = 60; + double hpRegenPerHour = 456.6; + + // Set new values, whatever is smallest + energyRegenPerHour += pmcProfile.Bonuses + .Where((bonus) => bonus.Type == BonusType.EnergyRegeneration) + .Aggregate(0d, (sum, bonus) => sum + (bonus.Value.Value)); + hydrationRegenPerHour += pmcProfile.Bonuses + .Where((bonus) => bonus.Type == BonusType.HydrationRegeneration) + .Aggregate(0d, (sum, bonus) => sum + (bonus.Value.Value)); + hpRegenPerHour += pmcProfile.Bonuses + .Where((bonus) => bonus.Type == BonusType.HealthRegeneration) + .Aggregate(0d, (sum, bonus) => sum + (bonus.Value.Value)); + + // Player has energy deficit + if (pmcProfile.Health.Energy.Current != pmcProfile.Health.Energy.Maximum) + { + // Set new value, whatever is smallest + pmcProfile.Health.Energy.Current += Math.Round(energyRegenPerHour * (diffSeconds.Value / 3600)); + if (pmcProfile.Health.Energy.Current > pmcProfile.Health.Energy.Maximum) + { + pmcProfile.Health.Energy.Current = pmcProfile.Health.Energy.Maximum; + } + } + + // Player has hydration deficit + if (pmcProfile.Health.Hydration.Current != pmcProfile.Health.Hydration.Maximum) + { + pmcProfile.Health.Hydration.Current += Math.Round(hydrationRegenPerHour * (diffSeconds.Value / 3600)); + if (pmcProfile.Health.Hydration.Current > pmcProfile.Health.Hydration.Maximum) + { + pmcProfile.Health.Hydration.Current = pmcProfile.Health.Hydration.Maximum; + } + } + + // Check all body parts + foreach (var bodyPart in pmcProfile.Health.BodyParts + .Select(bodyPartKvP => bodyPartKvP.Value)) + { + // Check part hp + if (bodyPart.Health.Current < bodyPart.Health.Maximum) + { + bodyPart.Health.Current += Math.Round(hpRegenPerHour * (diffSeconds.Value / 3600)); + } + if (bodyPart.Health.Current > bodyPart.Health.Maximum) + { + bodyPart.Health.Current = bodyPart.Health.Maximum; + } + + + if (bodyPart.Effects is null || bodyPart.Effects.Count == 0) + { + continue; + } + + // Look for effects + foreach (var effectKvP in bodyPart.Effects) { + // remove effects below 1, .e.g. bleeds at -1 + if (effectKvP.Value.Time < 1) + { + // More than 30 mins has passed + if (diffSeconds > 1800) + { + bodyPart.Effects.Remove(effectKvP.Key); + } + + continue; + } + + // Decrement effect time value by difference between current time and time health was last updated + effectKvP.Value.Time -= diffSeconds; + if (effectKvP.Value.Time < 1) + { + // Effect time was sub 1, set floor it can be + effectKvP.Value.Time = 1; + } + } + } + + // Update both values as they've both been updated + pmcProfile.Health.UpdateTime = currentTimeStamp; + } + } /// diff --git a/Core/Models/Eft/Common/Tables/BotBase.cs b/Core/Models/Eft/Common/Tables/BotBase.cs index cc1913ce..6c6f1b41 100644 --- a/Core/Models/Eft/Common/Tables/BotBase.cs +++ b/Core/Models/Eft/Common/Tables/BotBase.cs @@ -231,22 +231,11 @@ public class BotBaseHealth public CurrentMax? Hydration { get; set; } public CurrentMax? Energy { get; set; } public CurrentMax? Temperature { get; set; } - public BodyPartsHealth? BodyParts { get; set; } + public Dictionary? BodyParts { get; set; } public double? UpdateTime { get; set; } public bool? Immortal { get; set; } } -public class BodyPartsHealth -{ - public BodyPartHealth? Head { get; set; } - public BodyPartHealth? Chest { get; set; } - public BodyPartHealth? Stomach { get; set; } - public BodyPartHealth? LeftArm { get; set; } - public BodyPartHealth? RightArm { get; set; } - public BodyPartHealth? LeftLeg { get; set; } - public BodyPartHealth? RightLeg { get; set; } -} - public class BodyPartHealth { public CurrentMax? Health { get; set; } From 0a3ac15ed438b84ae2afebcbaca29eb0a52959e9 Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 19:33:52 +0000 Subject: [PATCH 7/9] Implemented `SaveActiveModsToProfile` with TODO --- Core/Controllers/GameController.cs | 79 ++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/Core/Controllers/GameController.cs b/Core/Controllers/GameController.cs index d4a58705..db7a86ec 100644 --- a/Core/Controllers/GameController.cs +++ b/Core/Controllers/GameController.cs @@ -3,9 +3,9 @@ using Core.Context; using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Game; -using Core.Models.Eft.Health; using Core.Models.Eft.Profile; using Core.Models.Enums; +using Core.Models.External; using Core.Models.Spt.Config; using Core.Servers; using Core.Services; @@ -41,6 +41,7 @@ public class GameController private readonly RaidTimeAdjustmentService _raidTimeAdjustmentService; private readonly ProfileActivityService _profileActivityService; private readonly ApplicationContext _applicationContext; + //private readonly PreSptModLoader preSptModLoader private readonly ICloner _cloner; private readonly CoreConfig _coreConfig; @@ -470,7 +471,21 @@ public class GameController /// Profile to add gifts to private void SendPraporGiftsToNewProfiles(PmcData pmcProfile) { - throw new NotImplementedException(); + var timeStampProfileCreated = pmcProfile.Info.RegistrationDate; + var oneDaySeconds = _timeUtil.GetHoursAsSeconds(24); + var currentTimeStamp = _timeUtil.GetTimeStamp(); + + // One day post-profile creation + if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds) + { + _giftService.SendPraporStartingGift(pmcProfile.SessionId, 1); + } + + // Two day post-profile creation + if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds * 2) + { + _giftService.SendPraporStartingGift(pmcProfile.SessionId, 2); + } } /// @@ -479,7 +494,36 @@ public class GameController /// Profile to add mod details to private void SaveActiveModsToProfile(SptProfile fullProfile) { - throw new NotImplementedException(); + // Add empty mod array if undefined + if (fullProfile.SptData.Mods is null) + { + fullProfile.SptData.Mods = []; + } + + // Get active mods + //var activeMods = _preSptModLoader.GetImportedModDetails(); //TODO IMPLEMENT _preSptModLoader + var activeMods = new Dictionary(); + foreach (var modKvP in activeMods) { + var modDetails = modKvP.Value; + if ( + fullProfile.SptData.Mods.Any( + (mod) => + mod.Author == modDetails.Author && + mod.Name == modDetails.Name && + mod.Version == modDetails.Version)) + { + // Exists already, skip + continue; + } + + fullProfile.SptData.Mods.Add( new ModDetails{ + Author = modDetails.Author, + DateAdded = _timeUtil.GetTimeStamp(), + Name = modDetails.Name, + Version = modDetails.Version, + Url = modDetails.Url, + }); + } } /// @@ -488,7 +532,34 @@ public class GameController /// Profile of player to get name from private void AddPlayerToPmcNames(PmcData pmcProfile) { - throw new NotImplementedException(); + var playerName = pmcProfile.Info.Nickname; + if (playerName is not null) + { + var bots = _databaseService.GetBots().Types; + + // Official names can only be 15 chars in length + if (playerName.Length > _botConfig.BotNameLengthLimit) + { + return; + } + + // Skip if player name exists already + if (bots.TryGetValue("bear", out var bearBot)) + { + if (bearBot is not null && bearBot.FirstNames.Any(x => x == playerName)) + { + bearBot.FirstNames.Add(playerName); + } + } + + if (bots.TryGetValue("bear", out var usecBot)) + { + if (usecBot is not null && usecBot.FirstNames.Any(x => x == playerName)) + { + usecBot.FirstNames.Add(playerName); + } + } + } } /// From 2a875b2566dab2a71962c8f3cac71b1e762e9cf6 Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 19:35:42 +0000 Subject: [PATCH 8/9] Fixed build --- Core/Helpers/HealthHelper.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/Helpers/HealthHelper.cs b/Core/Helpers/HealthHelper.cs index 863a8d08..989d8a91 100644 --- a/Core/Helpers/HealthHelper.cs +++ b/Core/Helpers/HealthHelper.cs @@ -1,8 +1,9 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Health; using Core.Models.Eft.Profile; +using BodyPartHealth = Core.Models.Eft.Common.Tables.BodyPartHealth; using Effects = Core.Models.Eft.Profile.Effects; using Health = Core.Models.Eft.Profile.Health; @@ -53,7 +54,7 @@ public class HealthHelper /// /// Post-raid body part data /// Player profile on server - protected void TransferPostRaidLimbEffectsToProfile(BodyPartsHealth postRaidBodyParts, PmcData profileData) + protected void TransferPostRaidLimbEffectsToProfile(Dictionary postRaidBodyParts, PmcData profileData) { throw new NotImplementedException(); } From c634de7d0e749ba01d13fac85b931f028cb3fa5b Mon Sep 17 00:00:00 2001 From: CWX Date: Mon, 13 Jan 2025 19:49:30 +0000 Subject: [PATCH 9/9] formatting and implement CheckForOrphanedModdedItems --- Core/Services/ProfileFixerService.cs | 254 +++++++++++++++++++++++---- 1 file changed, 217 insertions(+), 37 deletions(-) diff --git a/Core/Services/ProfileFixerService.cs b/Core/Services/ProfileFixerService.cs index 3726df4d..e5abd9bd 100644 --- a/Core/Services/ProfileFixerService.cs +++ b/Core/Services/ProfileFixerService.cs @@ -27,6 +27,7 @@ public class ProfileFixerService private readonly LocalisationService _localisationService; private readonly ConfigServer _configServer; private readonly CoreConfig _coreConfig; + private readonly InventoryHelper _inventoryHelper; public ProfileFixerService( ILogger logger, @@ -37,7 +38,9 @@ public class ProfileFixerService TraderHelper traderHelper, DatabaseService databaseService, LocalisationService localisationService, - ConfigServer configServer) + ConfigServer configServer, + InventoryHelper inventoryHelper + ) { _logger = logger; _hashUtil = hashUtil; @@ -48,6 +51,7 @@ public class ProfileFixerService _databaseService = databaseService; _localisationService = localisationService; _configServer = configServer; + _inventoryHelper = inventoryHelper; _coreConfig = _configServer.GetConfig(ConfigTypes.CORE); } @@ -89,7 +93,8 @@ public class ProfileFixerService } var traderDialogues = traderDialoguesKvP.Value; - foreach (var message in traderDialogues.Messages) { + foreach (var message in traderDialogues.Messages) + { // Skip any messages without attached items if (message.Items?.Data is null || message.Items?.Stash is null) { @@ -135,14 +140,15 @@ public class ProfileFixerService .GroupBy(item => item.Id) .ToDictionary(x => x.Key, x => x.ToList()); - foreach (var mappingKvP in itemMapping) { + 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}"); + _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) @@ -167,7 +173,8 @@ public class ProfileFixerService } // Iterate over all inventory items - foreach (var item in pmcProfile.Inventory.Items.Where((x) => x.SlotId is not null)) { + 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 @@ -178,7 +185,7 @@ public class ProfileFixerService 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"); + _logger.Warning($"Fixed item: {item.Id}s Tag value, removed invalid characters"); item.Upd.Tag.Name = regxp.Replace(item.Upd.Tag.Name, ""); } @@ -208,8 +215,8 @@ public class ProfileFixerService if (customizationDb[pmcProfile.Customization.Body] is null) { var defaultBody = playerIsUsec - ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecBody") - : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearBody"); + ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecBody") + : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearBody"); pmcProfile.Customization.Body = defaultBody.Id; } @@ -217,8 +224,8 @@ public class ProfileFixerService if (customizationDb[pmcProfile.Customization.Hands] is null) { var defaultHands = playerIsUsec - ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecHands") - : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearHands"); + ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecHands") + : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearHands"); pmcProfile.Customization.Hands = defaultHands.Id; } @@ -226,8 +233,8 @@ public class ProfileFixerService if (customizationDb[pmcProfile.Customization.Feet] is null) { var defaultFeet = playerIsUsec - ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecFeet") - : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearFeet"); + ? customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultUsecFeet") + : customizationDbArray.FirstOrDefault((x) => x.Name == "DefaultBearFeet"); pmcProfile.Customization.Feet = defaultFeet.Id; } } @@ -267,7 +274,8 @@ public class ProfileFixerService var achievements = _databaseService.GetAchievements(); // Loop over TaskConditionCounters objects and add once we want to remove to counterKeysToRemove - foreach (var TaskConditionCounterKvP in pmcProfile.TaskConditionCounters) { + foreach (var TaskConditionCounterKvP in pmcProfile.TaskConditionCounters) + { // Only check if profile has repeatable quests if (pmcProfile.RepeatableQuests is not null && activeRepeatableQuests.Count > 0) { @@ -286,7 +294,8 @@ public class ProfileFixerService } } - foreach (var counterKeyToRemove in taskConditionKeysToRemove) { + foreach (var counterKeyToRemove in taskConditionKeysToRemove) + { _logger.Debug($"Removed: {counterKeyToRemove} TaskConditionCounter object"); pmcProfile.TaskConditionCounters.Remove(counterKeyToRemove); } @@ -295,9 +304,10 @@ public class ProfileFixerService protected List GetActiveRepeatableQuests(List repeatableQuests) { 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); + 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; @@ -316,7 +326,6 @@ public class ProfileFixerService 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); @@ -331,7 +340,6 @@ public class ProfileFixerService /// The profile to validate quest productions for protected void VerifyQuestProductionUnlocks(PmcData pmcProfile) { - var quests = _databaseService.GetQuests(); var profileQuests = pmcProfile.Quests; @@ -351,7 +359,8 @@ public class ProfileFixerService if (productionRewards is not null) { - foreach (var reward in productionRewards) { + foreach (var reward in productionRewards) + { VerifyQuestProductionUnlock(pmcProfile, reward, quest); } } @@ -365,7 +374,8 @@ public class ProfileFixerService if (productionRewards is not null) { - foreach (var reward in productionRewards) { + foreach (var reward in productionRewards) + { VerifyQuestProductionUnlock(pmcProfile, reward, quest); } } @@ -387,9 +397,11 @@ public class ProfileFixerService if (matchingProductions.Count != 1) { - _logger.Error(_localisationService.GetText("quest-unable_to_find_matching_hideout_production", new { + _logger.Error(_localisationService.GetText("quest-unable_to_find_matching_hideout_production", new + { questName = questDetails.QuestName, - matchCount = matchingProductions.Count})); + matchCount = matchingProductions.Count + })); return; } @@ -399,7 +411,7 @@ public class ProfileFixerService if (!pmcProfile.UnlockedInfo.UnlockedProductionRecipe.Contains(matchingProductionId)) { pmcProfile.UnlockedInfo.UnlockedProductionRecipe.Add(matchingProductionId); - _logger.Debug($"Added production: { matchingProductionId} to unlocked production recipes for: { questDetails.QuestName}"); + _logger.Debug($"Added production: {matchingProductionId} to unlocked production recipes for: {questDetails.QuestName}"); } } @@ -430,7 +442,7 @@ public class ProfileFixerService { if (!slots.Any((x) => x.LocationIndex == i)) { - slots.Add(new HideoutSlot{ LocationIndex = i }); + slots.Add(new HideoutSlot { LocationIndex = i }); } } @@ -452,6 +464,8 @@ public class ProfileFixerService } } + private readonly List _areas = ["hideout", "main"]; + /** * Checks profile inventory for items that do not exist inside the items db * @param sessionId Session id @@ -459,7 +473,150 @@ public class ProfileFixerService */ public void CheckForOrphanedModdedItems(string sessionId, SptProfile fullProfile) { - throw new NotImplementedException(); + var itemsDb = _databaseService.GetItems(); + var pmcProfile = fullProfile.CharacterData.PmcData; + + // Get items placed in root of stash + // TODO: extend to other areas / sub items + var inventoryItemsToCheck = pmcProfile.Inventory.Items.Where(item => _areas.Contains(item.SlotId ?? "")); + if (inventoryItemsToCheck is not null) + { + // Check each item in inventory to ensure item exists in itemdb + foreach (var item in inventoryItemsToCheck) + { + if (itemsDb[item.Template] is not null) + { + _logger.Error(_localisationService.GetText("fixer-mod_item_found", item.Template)); + + if (_coreConfig.Fixes.RemoveModItemsFromProfile) + { + _logger.Success($"Deleting item from inventory and insurance with id: {item.Id} tpl: {item.Template}"); + + // also deletes from insured array + _inventoryHelper.RemoveItem(pmcProfile, item.Id, sessionId); + } + } + } + } + + if (fullProfile.UserBuildData is not null) + { + // Remove invalid builds from weapon, equipment and magazine build lists + var weaponBuilds = fullProfile.UserBuildData?.WeaponBuilds ?? new List(); + fullProfile.UserBuildData.WeaponBuilds = weaponBuilds.Where(build => + { + return !ShouldRemoveWeaponEquipmentBuild("weapon", build, itemsDb); + }).ToList(); + + var equipmentBuilds = fullProfile.UserBuildData.EquipmentBuilds ?? new List(); + fullProfile.UserBuildData.EquipmentBuilds = equipmentBuilds.Where(build => + { + return !ShouldRemoveWeaponEquipmentBuild("equipment", build, itemsDb); + }).ToList(); + + var magazineBuild = fullProfile.UserBuildData.MagazineBuilds ?? new List(); + fullProfile.UserBuildData.MagazineBuilds = magazineBuild.Where(build => + { + return !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 ?? false)) + { + message.HasRewards = false; + message.RewardCollected = true; + continue; + } + + // Iterate over all items in message + foreach (var item in message.Items.Data) + { + // Check item exists in itemsDb + if (itemsDb[item.Template] is null) + _logger.Error(_localisationService.GetText("fixer-mod_item_found", item.Template)); + + if (_coreConfig.Fixes.RemoveModItemsFromProfile) + { + dialog.Value.Messages.Remove(message); + _logger.Warning($"Item: {item.Template} has resulted in the deletion of message: {message.Id} from dialog {dialog}"); + } + + break; + } + } + } + + var clothing = _databaseService.GetTemplates().Customization; + foreach (var suit in fullProfile.Suits) + { + if (suit is null) + { + _logger.Error(_localisationService.GetText("fixer-clothing_item_found", suit)); + if (_coreConfig.Fixes.RemoveModItemsFromProfile) + { + fullProfile.Suits.Remove(suit); + _logger.Warning($"Non-default suit purchase: {suit} removed from profile"); + } + } + } + + foreach (var repeatable in fullProfile.CharacterData.PmcData.RepeatableQuests ?? new()) + { + foreach (var activeQuest in repeatable.ActiveQuests ?? new()) + { + if (!_traderHelper.TraderEnumHasValue(activeQuest.TraderId)) + { + _logger.Error(_localisationService.GetText("fixer-trader_found", 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); + } + + continue; + } + + foreach (var successReward in activeQuest.Rewards.Success ?? new()) + { + if (successReward.Type.ToString() == "Item") + { + foreach (var item in successReward.Items) + { + if (itemsDb[item.Template] is null) + { + _logger.Warning($"Non-default quest: {activeQuest.Id} from trader: {activeQuest.TraderId} removed from RepeatableQuests list in profile"); + repeatable.ActiveQuests.Remove(activeQuest); + } + } + } + } + } + } + + foreach (var TraderPurchase in fullProfile.TraderPurchases) + { + if (_traderHelper.TraderEnumHasValue(TraderPurchase.Key)) + { + _logger.Error(_localisationService.GetText("fixer-trader_found", TraderPurchase.Key)); + if (_coreConfig.Fixes.RemoveModItemsFromProfile) + { + _logger.Warning($"Non-default trader: {TraderPurchase.Key} purchase removed from traderPurchases list in profile"); + fullProfile.TraderPurchases.Remove(TraderPurchase.Key); + } + } + } } /** @@ -470,22 +627,45 @@ public class ProfileFixerService */ protected bool ShouldRemoveWeaponEquipmentBuild( string buildType, - WeaponBuild equipmentBuild, + UserBuild build, Dictionary itemsDb) { - // Get items not found in items db - foreach (var item in equipmentBuild.Items.Where(item => !itemsDb.ContainsKey(item.Template))) + if (buildType == "weapon") { - _logger.Error(_localisationService.GetText("fixer-mod_item_found", item.Template)); - - if (_coreConfig.Fixes.RemoveModItemsFromProfile) + // Get items not found in items db + foreach (var item in (build as WeaponBuild).Items.Where(item => !itemsDb.ContainsKey(item.Template))) { - _logger.Warning($"Item: { item.Template} has resulted in the deletion of { buildType} build: { equipmentBuild.Name}"); + _logger.Error(_localisationService.GetText("fixer-mod_item_found", item.Template)); - return true; + 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 - break; + 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(_localisationService.GetText("fixer-mod_item_found", item.Template)); + + if (_coreConfig.Fixes.RemoveModItemsFromProfile) + { + _logger.Warning($"Item: {item.Template} has resulted in the deletion of {buildType} build: {build.Name}"); + + return true; + } + + break; + } } return false; @@ -533,7 +713,7 @@ public class ProfileFixerService _logger.Error(_localisationService.GetText("fixer-trader_found", traderId)); if (_coreConfig.Fixes.RemoveInvalidTradersFromProfile) { - _logger.Warning($"Non - default trader: { traderId} removed from PMC TradersInfo in: { fullProfile.ProfileInfo.ProfileId} profile"); + _logger.Warning($"Non - default trader: {traderId} removed from PMC TradersInfo in: {fullProfile.ProfileInfo.ProfileId} profile"); fullProfile.CharacterData.PmcData.TradersInfo.Remove(traderId); } } @@ -547,7 +727,7 @@ public class ProfileFixerService _logger.Error(_localisationService.GetText("fixer-trader_found", traderId)); if (_coreConfig.Fixes.RemoveInvalidTradersFromProfile) { - _logger.Warning($"Non - default trader: {traderId} removed from Scav TradersInfo in: { fullProfile.ProfileInfo.ProfileId} profile"); + _logger.Warning($"Non - default trader: {traderId} removed from Scav TradersInfo in: {fullProfile.ProfileInfo.ProfileId} profile"); fullProfile.CharacterData.ScavData.TradersInfo.Remove(traderId); } }