From e54dcbd4d17c7137ca49a8c5e430fd314ea6ef4d Mon Sep 17 00:00:00 2001 From: Cj <161484149+CJ-SPT@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:28:54 -0400 Subject: [PATCH] Fix more warnings... (#543) * Fix more warnings... * Fix mistake --- .../Controllers/HideoutController.cs | 2 +- .../Controllers/RagfairController.cs | 2 +- .../Helpers/HandbookHelperException.cs | 10 + .../Helpers/HealthHelperException.cs | 10 + .../Helpers/HideoutHelperException.cs | 10 + .../{Items => Helpers}/ItemHelperException.cs | 2 +- .../Generators/RagfairOfferGenerator.cs | 2 +- .../RepeatableQuestRewardGenerator.cs | 2 +- .../Helpers/HandbookHelper.cs | 71 ++- .../Helpers/HealthHelper.cs | 54 ++- .../Helpers/HideoutHelper.cs | 419 +++++++++++++----- .../Helpers/ItemHelper.cs | 1 + .../Services/LocationLifecycleService.cs | 2 +- .../Services/PaymentService.cs | 8 +- .../Services/ProfileFixerService.cs | 2 +- .../Services/RagfairPriceService.cs | 2 +- 16 files changed, 435 insertions(+), 164 deletions(-) create mode 100644 Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HandbookHelperException.cs create mode 100644 Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HealthHelperException.cs create mode 100644 Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HideoutHelperException.cs rename Libraries/SPTarkov.Server.Core/Exceptions/{Items => Helpers}/ItemHelperException.cs (82%) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index 13ed43ce..80b9f73d 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs @@ -187,7 +187,7 @@ public class HideoutController( { foreach (var bonus in bonuses) { - hideoutHelper.ApplyPlayerUpgradesBonuses(pmcData, bonus); + hideoutHelper.ApplyPlayerUpgradesBonus(pmcData, bonus); } } diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs index e0f81aad..d461ee92 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs @@ -937,7 +937,7 @@ public class RagfairController( } return paymentHelper.IsMoneyTpl(requirement.Template) - ? handbookHelper.InRUB(requirement.Count.Value, requirement.Template) + ? handbookHelper.InRoubles(requirement.Count.Value, requirement.Template) : itemHelper.GetDynamicItemPrice(requirement.Template).Value * requirement.Count.Value; }); } diff --git a/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HandbookHelperException.cs b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HandbookHelperException.cs new file mode 100644 index 00000000..4209a4c0 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HandbookHelperException.cs @@ -0,0 +1,10 @@ +namespace SPTarkov.Server.Core.Exceptions.Helpers; + +public class HandbookHelperException : Exception +{ + public HandbookHelperException(string message) + : base(message) { } + + public HandbookHelperException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HealthHelperException.cs b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HealthHelperException.cs new file mode 100644 index 00000000..4364989b --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HealthHelperException.cs @@ -0,0 +1,10 @@ +namespace SPTarkov.Server.Core.Exceptions.Helpers; + +public class HealthHelperException : Exception +{ + public HealthHelperException(string message) + : base(message) { } + + public HealthHelperException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HideoutHelperException.cs b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HideoutHelperException.cs new file mode 100644 index 00000000..84e07944 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/HideoutHelperException.cs @@ -0,0 +1,10 @@ +namespace SPTarkov.Server.Core.Exceptions.Helpers; + +public class HideoutHelperException : Exception +{ + public HideoutHelperException(string message) + : base(message) { } + + public HideoutHelperException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/Libraries/SPTarkov.Server.Core/Exceptions/Items/ItemHelperException.cs b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/ItemHelperException.cs similarity index 82% rename from Libraries/SPTarkov.Server.Core/Exceptions/Items/ItemHelperException.cs rename to Libraries/SPTarkov.Server.Core/Exceptions/Helpers/ItemHelperException.cs index b0f3b065..afdedbdd 100644 --- a/Libraries/SPTarkov.Server.Core/Exceptions/Items/ItemHelperException.cs +++ b/Libraries/SPTarkov.Server.Core/Exceptions/Helpers/ItemHelperException.cs @@ -1,4 +1,4 @@ -namespace SPTarkov.Server.Core.Exceptions.Items; +namespace SPTarkov.Server.Core.Exceptions.Helpers; public class ItemHelperException : Exception { diff --git a/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs index def34f3e..b8ccc582 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs @@ -228,7 +228,7 @@ public class RagfairOfferGenerator( return currencyCount; } - return handbookHelper.InRUB(currencyCount, currencyType); + return handbookHelper.InRoubles(currencyCount, currencyType); } /// diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs index abe33ab0..f24bba3c 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/RepeatableQuestRewardGenerator.cs @@ -634,7 +634,7 @@ public class RepeatableQuestRewardGenerator( var currency = traderId == Traders.PEACEKEEPER || traderId == Traders.FENCE ? Money.EUROS : Money.ROUBLES; // Convert reward amount to Euros if necessary - var rewardAmountToGivePlayer = currency == Money.EUROS ? handbookHelper.FromRUB(rewardRoubles, Money.EUROS) : rewardRoubles; + var rewardAmountToGivePlayer = currency == Money.EUROS ? handbookHelper.FromRoubles(rewardRoubles, Money.EUROS) : rewardRoubles; // Get chosen currency + amount and return return GenerateItemReward(currency, rewardAmountToGivePlayer, rewardIndex, false); diff --git a/Libraries/SPTarkov.Server.Core/Helpers/HandbookHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/HandbookHelper.cs index 632a0b80..817bea89 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/HandbookHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/HandbookHelper.cs @@ -1,8 +1,11 @@ using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Exceptions.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Spt.Logging; +using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils.Cloners; @@ -10,7 +13,7 @@ using SPTarkov.Server.Core.Utils.Cloners; namespace SPTarkov.Server.Core.Helpers; [Injectable(InjectionType.Singleton)] -public class HandbookHelper(DatabaseService databaseService, ConfigServer configServer, ICloner cloner) +public class HandbookHelper(ISptLogger logger, DatabaseService databaseService, ConfigServer configServer, ICloner cloner) { private LookupCollection? _handbookPriceCache; protected virtual LookupCollection HandbookPriceCache @@ -18,7 +21,7 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config get { return _handbookPriceCache ??= HydrateHandbookCache(); } } - protected readonly ItemConfig _itemConfig = configServer.GetConfig(); + protected readonly ItemConfig ItemConfig = configServer.GetConfig(); /// /// Create an in-memory cache of all items with associated handbook price in handbookPriceCache class @@ -28,7 +31,7 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config var result = new LookupCollection(); var handbook = databaseService.GetHandbook(); // Add handbook overrides found in items.json config into db - foreach (var (key, priceOverride) in _itemConfig.HandbookPriceOverride) + foreach (var (key, priceOverride) in ItemConfig.HandbookPriceOverride) { var itemToUpdate = handbook.Items.FirstOrDefault(item => item.Id == key); if (itemToUpdate is null) @@ -44,11 +47,11 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config itemToUpdate = handbook.Items.FirstOrDefault(item => item.Id == key); } - itemToUpdate.Price = priceOverride.Price; + itemToUpdate!.Price = priceOverride.Price; itemToUpdate.ParentId = priceOverride.ParentId; } - var handbookDbClone = cloner.Clone(handbook); + var handbookDbClone = cloner.Clone(handbook)!; foreach (var handbookItem in handbookDbClone.Items) { result.Items.ById.TryAdd(handbookItem.Id, handbookItem.Price ?? 0); @@ -57,13 +60,23 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config result.Items.ByParent.TryAdd(handbookItem.ParentId, []); } - result.Items.ByParent.TryGetValue(handbookItem.ParentId, out var itemIds); + if (!result.Items.ByParent.TryGetValue(handbookItem.ParentId, out var itemIds)) + { + throw new HandbookHelperException( + $"Cannot add item id `{handbookItem.Id}` to parent id `{handbookItem.ParentId}`. Parent does not exist." + ); + } + itemIds.Add(handbookItem.Id); } foreach (var handbookCategory in handbookDbClone.Categories) { - result.Categories.ById.TryAdd(handbookCategory.Id, handbookCategory.ParentId); + if (!result.Categories.ById.TryAdd(handbookCategory.Id, handbookCategory.ParentId)) + { + throw new HandbookHelperException($"Unable to add `{handbookCategory.Id}`. Key already exists."); + } + if (handbookCategory.ParentId is not null) { if (!result.Categories.ByParent.TryGetValue(handbookCategory.ParentId.Value, out _)) @@ -71,7 +84,12 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config result.Categories.ByParent.TryAdd(handbookCategory.ParentId.Value, []); } - result.Categories.ByParent.TryGetValue(handbookCategory.ParentId.Value, out var itemIds); + if (!result.Categories.ByParent.TryGetValue(handbookCategory.ParentId.Value, out var itemIds)) + { + throw new HandbookHelperException( + $"Cannot add item id `{handbookCategory.Id}` to parent id `{handbookCategory.ParentId.Value}`. Parent does not exist." + ); + } itemIds.Add(handbookCategory.Id); } @@ -117,13 +135,13 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config } /// - /// Sum price of supplied items with handbook prices + /// Sum price of supplied items with handbook prices /// /// Items to Sum /// public double GetTemplatePriceForItems(IEnumerable items) { - return items.Where(item => item?.Template != null).Sum(item => GetTemplatePrice(item.Template)); + return items.Sum(item => GetTemplatePrice(item.Template)); } /// @@ -133,9 +151,17 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config /// string array public List TemplatesWithParent(MongoId parentId) { - HandbookPriceCache.Items.ByParent.TryGetValue(parentId, out var template); + if (HandbookPriceCache.Items.ByParent.TryGetValue(parentId, out var templates)) + { + return templates; + } - return template ?? []; + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Template ids with parent id `{parentId}` not found when trying to get templates by parent"); + } + + return []; } /// @@ -153,10 +179,19 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config /// /// /// string array - public List ChildrenCategories(string categoryParent) + public List ChildrenCategories(MongoId categoryParent) { - HandbookPriceCache.Categories.ByParent.TryGetValue(categoryParent, out var category); - return category ?? []; + if (HandbookPriceCache.Categories.ByParent.TryGetValue(categoryParent, out var childrenCategories)) + { + return childrenCategories; + } + + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Children categories with parent id `{categoryParent}` not found when trying to get children categories"); + } + + return []; } /// @@ -165,7 +200,7 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config /// Currency count to convert /// What current currency is /// Count in roubles - public double InRUB(double nonRoubleCurrencyCount, MongoId currencyTypeFrom) + public double InRoubles(double nonRoubleCurrencyCount, MongoId currencyTypeFrom) { return currencyTypeFrom == Money.ROUBLES ? nonRoubleCurrencyCount @@ -178,7 +213,7 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config /// roubles to convert /// Currency to convert roubles into /// currency count in desired type - public double FromRUB(double roubleCurrencyCount, MongoId currencyTypeTo) + public double FromRoubles(double roubleCurrencyCount, MongoId currencyTypeTo) { if (currencyTypeTo == Money.ROUBLES) { @@ -190,7 +225,7 @@ public class HandbookHelper(DatabaseService databaseService, ConfigServer config return price > 0 ? Math.Max(1, Math.Round(roubleCurrencyCount / price)) : 0; } - public HandbookCategory GetCategoryById(MongoId handbookId) + public HandbookCategory? GetCategoryById(MongoId handbookId) { return databaseService.GetHandbook().Categories.FirstOrDefault(category => category.Id == handbookId); } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs index 67484d55..a25c6195 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs @@ -1,4 +1,5 @@ using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Exceptions.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; @@ -10,34 +11,44 @@ using BodyPartHealth = SPTarkov.Server.Core.Models.Eft.Common.Tables.BodyPartHea namespace SPTarkov.Server.Core.Helpers; [Injectable] -public class HealthHelper(TimeUtil timeUtil, SaveServer saveServer, ProfileHelper profileHelper, ConfigServer configServer) +public class HealthHelper(TimeUtil timeUtil, ConfigServer configServer) { - protected readonly HealthConfig _healthConfig = configServer.GetConfig(); + protected readonly HealthConfig HealthConfig = configServer.GetConfig(); + protected readonly HashSet EffectsToSkip = ["Dehydration", "Exhaustion"]; /// /// Update player profile vitality values with changes from client request object /// - /// Session id + /// Session id /// Player profile to apply changes to /// Changes to apply - /// OPTIONAL - Is player dead - public void ApplyHealthChangesToProfile(MongoId sessionID, PmcData pmcProfileToUpdate, BotBaseHealth healthChanges, bool isDead = false) + public void ApplyHealthChangesToProfile(MongoId sessionId, PmcData pmcProfileToUpdate, BotBaseHealth healthChanges) { - var fullProfile = saveServer.GetProfile(sessionID); - var profileEdition = fullProfile.ProfileInfo.Edition; - var profileSide = fullProfile.CharacterData.PmcData.Info.Side; - + /* TODO: Not used here, need to check node or a live profile, commented out for now to avoid the potential alloc - Cj + var fullProfile = saveServer.GetProfile(sessionId); + var profileEdition = fullProfile.ProfileInfo?.Edition; + var profileSide = fullProfile.CharacterData?.PmcData?.Info?.Side; // Get matching 'side' e.g. USEC var matchingSide = profileHelper.GetProfileTemplateForSide(profileEdition, profileSide); - var defaultTemperature = matchingSide?.Character?.Health?.Temperature ?? new CurrentMinMax { Current = 36.6 }; + */ + + if (healthChanges.BodyParts is null) + { + throw new HealthHelperException("healthChanges.BodyParts is null when trying to apply health changes"); + } // Alter saved profiles Health with values from post-raid client data - ModifyProfileHealthProperties(pmcProfileToUpdate, healthChanges.BodyParts, ["Dehydration", "Exhaustion"]); + ModifyProfileHealthProperties(pmcProfileToUpdate, healthChanges.BodyParts, EffectsToSkip); // Adjust hydration/energy/temperature AdjustProfileHydrationEnergyTemperature(pmcProfileToUpdate, healthChanges); + if (pmcProfileToUpdate.Health is null) + { + throw new HealthHelperException("pmcProfileToUpdate.Health is null when trying to apply health changes"); + } + // Update last edited timestamp pmcProfileToUpdate.Health.UpdateTime = timeUtil.GetTimeStamp(); } @@ -56,27 +67,36 @@ public class HealthHelper(TimeUtil timeUtil, SaveServer saveServer, ProfileHelpe { foreach (var (partName, partProperties) in bodyPartChanges) { - if (!profileToAdjust.Health.BodyParts.TryGetValue(partName, out var matchingProfilePart)) + // Pattern matching null and false because otherwise the compiler throws a fit because `matchingProfilePart` + // might not be initialized, very cool + if (profileToAdjust.Health?.BodyParts?.TryGetValue(partName, out var matchingProfilePart) is null or false) { continue; } - if (_healthConfig.Save.Health) + if (partProperties.Health is null || matchingProfilePart.Health is null) + { + throw new HealthHelperException( + "partProperties.Health or matchingBodyPart.Health is null when trying to modify profile health properties" + ); + } + + if (HealthConfig.Save.Health) { // Apply hp changes to profile matchingProfilePart.Health.Current = partProperties.Health.Current == 0 - ? partProperties.Health.Maximum * _healthConfig.HealthMultipliers.Blacked + ? partProperties.Health.Maximum * HealthConfig.HealthMultipliers.Blacked : partProperties.Health.Current; matchingProfilePart.Health.Maximum = partProperties.Health.Maximum; } // Process each effect for each part - foreach (var (key, effectDetails) in partProperties.Effects) + foreach (var (key, effectDetails) in partProperties.Effects ?? []) { // Null guard - matchingProfilePart.Effects ??= new Dictionary(); + matchingProfilePart.Effects ??= new Dictionary(); // Effect already exists on limb in server profile, skip if (matchingProfilePart.Effects.ContainsKey(key)) @@ -96,7 +116,7 @@ public class HealthHelper(TimeUtil timeUtil, SaveServer saveServer, ProfileHelpe continue; } - var effectToAdd = new BodyPartEffectProperties { Time = effectDetails.Time ?? -1 }; + var effectToAdd = new BodyPartEffectProperties { Time = effectDetails?.Time ?? -1 }; // Add effect to server profile if (matchingProfilePart.Effects.TryAdd(key, effectToAdd)) { diff --git a/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs index 7369e22e..e06b1cbd 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs @@ -1,4 +1,5 @@ using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Exceptions.Helpers; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; @@ -27,32 +28,37 @@ public class HideoutHelper( ProfileHelper profileHelper, InventoryHelper inventoryHelper, ItemHelper itemHelper, - NotificationSendHelper notificationSendHelper, - NotifierHelper notifierHelper, - ICloner cloner + ICloner cloner, + MathUtil mathUtil ) { public static readonly MongoId BitcoinProductionId = new("5d5c205bd582a50d042a3c0e"); public static readonly MongoId WaterCollectorId = new("5d5589c1f934db045e6c5492"); - public const int MaxSkillPoint = 5000; /// /// Add production to profiles' Hideout.Production array /// /// Profile to add production to /// Production request - /// Session id + /// Session id /// client response - public void RegisterProduction(PmcData pmcData, HideoutSingleProductionStartRequestData productionRequest, MongoId sessionID) + public void RegisterProduction(PmcData pmcData, HideoutSingleProductionStartRequestData productionRequest, MongoId sessionId) { var recipe = databaseService .GetHideout() - .Production.Recipes.FirstOrDefault(production => production.Id == productionRequest.RecipeId); + .Production.Recipes?.FirstOrDefault(production => production.Id == productionRequest.RecipeId); + if (recipe is null) { logger.Error(serverLocalisationService.GetText("hideout-missing_recipe_in_db", productionRequest.RecipeId)); - httpResponseUtil.AppendErrorToOutput(eventOutputHolder.GetOutput(sessionID)); + httpResponseUtil.AppendErrorToOutput(eventOutputHolder.GetOutput(sessionId)); + return; + } + + if (pmcData.Hideout is null) + { + throw new HideoutHelperException($"Hideout is null when trying to register production for recipe id `{recipe.Id}`"); } // @Important: Here we need to be very exact: @@ -71,7 +77,7 @@ public class HideoutHelper( foreach (var tool in productionRequest.Tools) { - var toolItem = cloner.Clone(pmcData.Inventory.Items.FirstOrDefault(x => x.Id == tool.Id)); + var toolItem = cloner.Clone(pmcData.Inventory?.Items?.FirstOrDefault(x => x.Id == tool.Id)); if (toolItem is null) { logger.Warning($"Unable to find tool item: {tool.Id}"); @@ -82,7 +88,7 @@ public class HideoutHelper( // Make sure we only return as many as we took toolItem.AddUpd(); - toolItem.Upd.StackObjectsCount = tool.Count; + toolItem.Upd!.StackObjectsCount = tool.Count; production.SptRequiredTools.Add( new Item @@ -103,18 +109,32 @@ public class HideoutHelper( /// /// Profile to add production to /// Production request - /// Session id + /// Session id /// client response - public void RegisterProduction(PmcData pmcData, HideoutContinuousProductionStartRequestData productionRequest, MongoId sessionID) + public void RegisterProduction(PmcData pmcData, HideoutContinuousProductionStartRequestData productionRequest, MongoId sessionId) { + if (!productionRequest.RecipeId.HasValue) + { + logger.Error("RecipeId sent from client is null, skipping continuous production registration"); + return; + } + var recipe = databaseService .GetHideout() - .Production.Recipes.FirstOrDefault(production => production.Id == productionRequest.RecipeId); + .Production.Recipes?.FirstOrDefault(production => production.Id == productionRequest.RecipeId); if (recipe is null) { logger.Error(serverLocalisationService.GetText("hideout-missing_recipe_in_db", productionRequest.RecipeId)); - httpResponseUtil.AppendErrorToOutput(eventOutputHolder.GetOutput(sessionID)); + httpResponseUtil.AppendErrorToOutput(eventOutputHolder.GetOutput(sessionId)); + return; + } + + if (pmcData.Hideout is null) + { + throw new HideoutHelperException( + $"Hideout is null when trying to register production for recipe id `{productionRequest.RecipeId.Value}`" + ); } // @Important: Here we need to be very exact: @@ -156,23 +176,31 @@ public class HideoutHelper( /// /// Profile to add bonus to /// Bonus to add to profile - public void ApplyPlayerUpgradesBonuses(PmcData profileData, Bonus bonus) + public void ApplyPlayerUpgradesBonus(PmcData profileData, Bonus bonus) { + if (!bonus.TemplateId.HasValue) + { + logger.Error("Bonus template id is null"); + return; + } + // Handle additional changes some bonuses need before being added switch (bonus.Type) { case BonusType.StashSize: { // Find stash item and adjust tpl to new tpl from bonus - var stashItem = profileData.Inventory.Items.FirstOrDefault(x => x.Id == profileData.Inventory.Stash); + var stashItem = profileData.Inventory?.Items?.FirstOrDefault(x => x.Id == profileData.Inventory.Stash); if (stashItem is null) { - logger.Warning( + logger.Error( serverLocalisationService.GetText( "hideout-unable_to_apply_stashsize_bonus_no_stash_found", - profileData.Inventory.Stash + profileData.Inventory?.Stash ) ); + + return; } stashItem.Template = bonus.TemplateId.Value; @@ -180,6 +208,11 @@ public class HideoutHelper( break; } case BonusType.MaximumEnergyReserve: + if (profileData.Health?.Energy is null) + { + throw new HideoutHelperException("Profile Energy is null when trying to apply MaximumEnergyReserve"); + } + // Amend max energy in profile profileData.Health.Energy.Maximum += bonus.Value; break; @@ -189,6 +222,17 @@ public class HideoutHelper( bonus.IsProduction = null; bonus.IsVisible = null; break; + default: + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Unhandled bonus type `{bonus.Type}` when trying to apply player upgrade bonus"); + } + break; + } + + if (profileData.Bonuses is null) + { + throw new HideoutHelperException($"Profile bonuses are null when trying to add: {bonus.Type}"); } // Add bonus to player bonuses array in profile @@ -207,9 +251,14 @@ public class HideoutHelper( /// Session id public void UpdatePlayerHideout(MongoId sessionID) { - var pmcData = profileHelper.GetPmcProfile(sessionID); + var pmcData = profileHelper.GetPmcProfile(sessionID)!; var hideoutProperties = GetHideoutProperties(pmcData); + if (pmcData.Hideout is null) + { + throw new HideoutHelperException("Hideout is null when trying to update player hideout"); + } + pmcData.Hideout.SptUpdateLastRunTimestamp ??= timeUtil.GetTimeStamp(); UpdateAreasWithResources(sessionID, pmcData, hideoutProperties); @@ -224,16 +273,15 @@ public class HideoutHelper( /// Hideout-related values protected HideoutProperties GetHideoutProperties(PmcData pmcData) { - var bitcoinFarm = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.BitcoinFarm); - var bitcoinCount = (bitcoinFarm?.Slots).Count(slot => slot.Items is not null); // Get slots with an item property + var bitcoinFarm = pmcData.Hideout?.Areas?.FirstOrDefault(area => area.Type == HideoutAreas.BitcoinFarm); + var bitcoinCount = bitcoinFarm?.Slots?.Count(slot => slot.Items is not null); // Get slots with an item property + var waterCollector = pmcData.Hideout?.Areas?.FirstOrDefault(area => area.Type == HideoutAreas.WaterCollector); var hideoutProperties = new HideoutProperties { BtcFarmGcs = bitcoinCount, - IsGeneratorOn = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.Generator)?.Active ?? false, - WaterCollectorHasFilter = DoesWaterCollectorHaveFilter( - pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.WaterCollector) - ), + IsGeneratorOn = pmcData.Hideout?.Areas?.FirstOrDefault(area => area.Type == HideoutAreas.Generator)?.Active ?? false, + WaterCollectorHasFilter = DoesWaterCollectorHaveFilter(waterCollector), }; return hideoutProperties; @@ -244,13 +292,19 @@ public class HideoutHelper( /// /// Hideout area to check /// - protected static bool DoesWaterCollectorHaveFilter(BotHideoutArea waterCollector) + protected bool DoesWaterCollectorHaveFilter(BotHideoutArea? waterCollector) { + // Water collector not built + if (waterCollector is null) + { + return false; + } + // Can put filters in from L3 if (waterCollector.Level == 3) // Has filter in at least one slot { - return waterCollector.Slots.Any(slot => slot.Items is not null); + return waterCollector.Slots?.Any(slot => slot.Items is not null) ?? false; } // No Filter @@ -267,20 +321,21 @@ public class HideoutHelper( var recipes = databaseService.GetHideout().Production; // Check each production and handle edge cases if necessary - foreach (var prodId in pmcData.Hideout?.Production) + foreach (var prodId in pmcData.Hideout?.Production ?? []) { - if (pmcData.Hideout.Production.TryGetValue(prodId.Key, out var craft) && craft is null) + // Pattern matching null or false to shut the compiler up + if (pmcData.Hideout?.Production?.TryGetValue(prodId.Key, out var craft) is null or false) { // Craft value is undefined, get rid of it (could be from cancelling craft that needs cleaning up) - pmcData.Hideout.Production.Remove(prodId.Key); + pmcData.Hideout?.Production?.Remove(prodId.Key); continue; } - if (craft.Progress == null) + if (craft?.Progress is null) { logger.Warning(serverLocalisationService.GetText("hideout-craft_has_undefined_progress_value_defaulting", prodId)); - craft.Progress = 0; + craft!.Progress = 0; } // Skip processing (Don't skip continuous crafts like bitcoin farm or cultist circle) @@ -344,12 +399,24 @@ public class HideoutHelper( /// profile to update /// id of water collection production to update /// Hideout properties - protected void UpdateWaterCollectorProductionTimer(PmcData pmcData, string productionId, HideoutProperties hideoutProperties) + protected void UpdateWaterCollectorProductionTimer(PmcData pmcData, MongoId productionId, HideoutProperties hideoutProperties) { + if (pmcData.Hideout?.Production is null) + { + throw new HideoutHelperException("Hideout productions are null when trying to update water collector production timer"); + } + + if (!pmcData.Hideout.Production.TryGetValue(productionId, out var production) || production is null) + { + logger.Error($"production id: {productionId.ToString()} not found in hideout productions, what are we trying to update?"); + return; + } + var timeElapsed = GetTimeElapsedSinceLastServerTick(pmcData, hideoutProperties.IsGeneratorOn); + if (hideoutProperties.WaterCollectorHasFilter) { - pmcData.Hideout.Production[productionId].Progress += timeElapsed; + production.Progress += timeElapsed; } } @@ -360,8 +427,13 @@ public class HideoutHelper( /// Production id being crafted /// Recipe data being crafted /// - protected void UpdateProductionProgress(PmcData pmcData, string prodId, HideoutProduction recipe, HideoutProperties hideoutProperties) + protected void UpdateProductionProgress(PmcData pmcData, MongoId prodId, HideoutProduction recipe, HideoutProperties hideoutProperties) { + if (pmcData.Hideout?.Production is null) + { + throw new HideoutHelperException("Hideout productions are null when trying to update production progress"); + } + // Production is complete, no need to do any calculations if (DoesProgressMatchProductionTime(pmcData, prodId)) { @@ -372,7 +444,11 @@ public class HideoutHelper( var timeElapsed = GetTimeElapsedSinceLastServerTick(pmcData, hideoutProperties.IsGeneratorOn, recipe); // Increment progress by time passed - pmcData.Hideout.Production.TryGetValue(prodId, out var production); + if (!pmcData.Hideout.Production.TryGetValue(prodId, out var production) || production is null) + { + logger.Error($"production id: {prodId.ToString()} not found in hideout productions, what are we trying to update?"); + return; + } // Some items NEED power to craft (e.g. DSP) if (production.needFuelForAllProductionTime.GetValueOrDefault() && hideoutProperties.IsGeneratorOn) @@ -393,9 +469,18 @@ public class HideoutHelper( } } - protected void UpdateCultistCircleCraftProgress(PmcData pmcData, string prodId) + protected void UpdateCultistCircleCraftProgress(PmcData pmcData, MongoId prodId) { - pmcData.Hideout.Production.TryGetValue(prodId, out var production); + if (pmcData.Hideout?.Production is null) + { + throw new HideoutHelperException("Hideout productions are null when trying to update cultist progress"); + } + + if (!pmcData.Hideout.Production.TryGetValue(prodId, out var production) || production is null) + { + logger.Error($"Production id `{prodId.ToString()}` not found in profile, what are we trying to update?"); + return; + } // Check if we're already complete, skip if ((production.AvailableForFinish ?? false) && !production.InProgress.GetValueOrDefault(false)) @@ -440,9 +525,29 @@ public class HideoutHelper( /// Player profile /// Production id /// progress matches productionTime from recipe - protected static bool DoesProgressMatchProductionTime(PmcData pmcData, string prodId) + protected bool DoesProgressMatchProductionTime(PmcData pmcData, MongoId prodId) { - return pmcData.Hideout.Production[prodId].Progress == pmcData.Hideout.Production[prodId].ProductionTime; + if (pmcData.Hideout?.Production is null) + { + throw new HideoutHelperException("Hideout productions are null when trying to match production progress"); + } + + // Production doesn't exist or progress or production time is null + if ( + !pmcData.Hideout.Production.TryGetValue(prodId, out var production) + || production?.Progress is null + || production.ProductionTime is null + ) + { + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"ProductionId: {prodId} Trying to match progress to production time that does not exist."); + } + + return false; + } + + return production.Progress.Value.Approx(production.ProductionTime.Value); } /// @@ -450,25 +555,39 @@ public class HideoutHelper( /// /// Profile to update /// Id of scav case production to update - protected void UpdateScavCaseProductionTimer(PmcData pmcData, string productionId) + protected void UpdateScavCaseProductionTimer(PmcData pmcData, MongoId productionId) { - var timeElapsed = - timeUtil.GetTimeStamp() - - pmcData.Hideout.Production[productionId].StartTimestamp - - pmcData.Hideout.Production[productionId].Progress; + if (pmcData.Hideout?.Production is null) + { + throw new HideoutHelperException("Hideout productions are null when trying to update scav case production timer"); + } - pmcData.Hideout.Production[productionId].Progress += timeElapsed; + if (!pmcData.Hideout.Production.TryGetValue(productionId, out var production) || production is null) + { + logger.Error($"Production id `{productionId.ToString()}` not found in profile, what are we trying to update?"); + return; + } + + var currentTime = timeUtil.GetTimeStamp(); + var timeElapsed = currentTime - production.StartTimestamp - production.Progress; + + production.Progress += timeElapsed; } /// /// Iterate over hideout areas that use resources (fuel/filters etc) and update associated values /// - /// Session id + /// Session id /// Profile to update areas of /// hideout properties - protected void UpdateAreasWithResources(MongoId sessionID, PmcData pmcData, HideoutProperties hideoutProperties) + protected void UpdateAreasWithResources(MongoId sessionId, PmcData pmcData, HideoutProperties hideoutProperties) { - foreach (var area in pmcData.Hideout.Areas) + if (pmcData.Hideout is null) + { + throw new HideoutHelperException("Hideout is null when trying update areas with resources"); + } + + foreach (var area in pmcData.Hideout.Areas ?? []) { switch (area.Type) { @@ -477,18 +596,21 @@ public class HideoutHelper( { UpdateFuel(area, pmcData, hideoutProperties.IsGeneratorOn); } - break; case HideoutAreas.WaterCollector: - UpdateWaterCollector(sessionID, pmcData, area, hideoutProperties); + UpdateWaterCollector(sessionId, pmcData, area, hideoutProperties); break; - case HideoutAreas.AirFilteringUnit: if (hideoutProperties.IsGeneratorOn) { UpdateAirFilters(area, pmcData, hideoutProperties.IsGeneratorOn); } - + break; + default: + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Unhandled area type {area.Type} when trying to update areas with resources"); + } break; } } @@ -527,9 +649,10 @@ public class HideoutHelper( fuelUsedSinceLastTick *= combinedBonus; var hasFuelRemaining = false; - double pointsConsumed; - for (var i = 0; i < generatorArea.Slots.Count; i++) + for (var i = 0; i < generatorArea.Slots?.Count; i++) { + double pointsConsumed; + var generatorSlot = generatorArea.Slots[i]; if (generatorSlot?.Items is null) // No item in slot, skip @@ -545,24 +668,25 @@ public class HideoutHelper( } var fuelRemaining = fuelItemInSlot.Upd?.Resource?.Value; - if (fuelRemaining == 0) - // No fuel left, skip - { - continue; - } - // Undefined fuel, fresh fuel item and needs its max fuel amount looked up - if (fuelRemaining is null) + switch (fuelRemaining) { - var fuelItemTemplate = itemHelper.GetItem(fuelItemInSlot.Template).Value; - pointsConsumed = fuelUsedSinceLastTick ?? 0; - fuelRemaining = fuelItemTemplate.Properties.MaxResource - fuelUsedSinceLastTick; - } - else - { - // Fuel exists already, deduct fuel from item remaining value - pointsConsumed = (double)((fuelItemInSlot.Upd.Resource.UnitsConsumed ?? 0) + fuelUsedSinceLastTick); - fuelRemaining -= fuelUsedSinceLastTick; + // No fuel left, skip + case 0: + continue; + // Undefined fuel, fresh fuel item and needs its max fuel amount looked up + case null: + { + var fuelItemTemplate = itemHelper.GetItem(fuelItemInSlot.Template).Value; + pointsConsumed = fuelUsedSinceLastTick ?? 0; + fuelRemaining = fuelItemTemplate.Properties.MaxResource - fuelUsedSinceLastTick; + break; + } + default: + // Fuel exists already, deduct fuel from item remaining value + pointsConsumed = (double)((fuelItemInSlot.Upd.Resource.UnitsConsumed ?? 0) + fuelUsedSinceLastTick); + fuelRemaining -= fuelUsedSinceLastTick; + break; } // Round values to keep accuracy @@ -584,7 +708,7 @@ public class HideoutHelper( if (logger.IsLogEnabled(LogLevel.Debug)) { - logger.Debug($"Profile: {pmcData.Id} Generator has: {fuelRemaining} fuel left in slot {i + 1}"); + logger.Debug($"Profile: {pmcData!.Id} Generator has: {fuelRemaining} fuel left in slot {i + 1}"); } hasFuelRemaining = true; @@ -598,7 +722,7 @@ public class HideoutHelper( fuelUsedSinceLastTick = Math.Abs(fuelRemaining ?? 0); if (logger.IsLogEnabled(LogLevel.Debug)) { - logger.Debug($"Profile: {pmcData.Id} Generator ran out of fuel"); + logger.Debug($"Profile: {pmcData!.Id} Generator ran out of fuel"); } } @@ -624,8 +748,8 @@ public class HideoutHelper( // Canister with purified water craft exists if ( - pmcData.Hideout.Production.TryGetValue(WaterCollectorId, out var purifiedWaterCraft) - && purifiedWaterCraft.GetType() == typeof(Production) + pmcData.Hideout?.Production?.TryGetValue(WaterCollectorId, out var purifiedWaterCraft) is true + && purifiedWaterCraft?.GetType() == typeof(Production) ) { // Update craft time to account for increases in players craft time skill @@ -661,7 +785,7 @@ public class HideoutHelper( { var globalSkillsDb = databaseService.GetGlobals().Configuration.SkillsSettings; - var recipe = databaseService.GetHideout().Production.Recipes.FirstOrDefault(production => production.Id == recipeId); + var recipe = databaseService.GetHideout().Production.Recipes?.FirstOrDefault(production => production.Id == recipeId); if (recipe is null) { logger.Error(serverLocalisationService.GetText("hideout-missing_recipe_in_db", recipeId)); @@ -695,7 +819,7 @@ public class HideoutHelper( } var modifiedProductionTime = recipe.ProductionTime - timeReductionSeconds; - if (modifiedProductionTime > 0 && profileHelper.IsDeveloperAccount(pmcData.Id.Value)) + if (modifiedProductionTime > 0 && profileHelper.IsDeveloperAccount(pmcData.Id!.Value)) { modifiedProductionTime = 40; } @@ -737,7 +861,7 @@ public class HideoutHelper( } // Check all slots that take water filters until we find one with filter in it - for (var i = 0; i < waterFilterArea.Slots.Count; i++) + for (var i = 0; i < waterFilterArea.Slots?.Count; i++) { // No water filter in slot, skip if (waterFilterArea.Slots[i].Items is null) @@ -745,10 +869,15 @@ public class HideoutHelper( continue; } - var waterFilterItemInSlot = waterFilterArea.Slots[i].Items.FirstOrDefault(); + var waterFilterItemInSlot = waterFilterArea.Slots[i].Items?.FirstOrDefault(); + if (waterFilterItemInSlot is null) + { + logger.Warning($"Could not find water filter in slot index `{i}` when trying to update water filters"); + continue; + } // How many units of filter are left - var resourceValue = waterFilterItemInSlot?.Upd?.Resource?.Value; + var resourceValue = waterFilterItemInSlot.Upd?.Resource?.Value; double pointsConsumed; if (resourceValue is null) { @@ -758,7 +887,7 @@ public class HideoutHelper( } else { - pointsConsumed = (waterFilterItemInSlot.Upd.Resource.UnitsConsumed ?? 0) + filterDrainRate; + pointsConsumed = (waterFilterItemInSlot.Upd?.Resource?.UnitsConsumed ?? 0) + filterDrainRate; resourceValue -= filterDrainRate; } @@ -859,7 +988,7 @@ public class HideoutHelper( /// Seconds to produce item protected double GetTotalProductionTimeSeconds(MongoId prodId) { - return databaseService.GetHideout().Production.Recipes.FirstOrDefault(prod => prod.Id == prodId)?.ProductionTime ?? 0; + return databaseService.GetHideout().Production.Recipes?.FirstOrDefault(prod => prod.Id == prodId)?.ProductionTime ?? 0; } /// @@ -893,15 +1022,15 @@ public class HideoutHelper( var hideoutManagementConsumptionBonus = 1.0 - GetHideoutManagementConsumptionBonus(pmcData); filterDrainRate *= hideoutManagementConsumptionBonus; - for (var i = 0; i < airFilterArea.Slots.Count; i++) + for (var i = 0; i < airFilterArea.Slots?.Count; i++) { - if (airFilterArea.Slots[i]?.Items is null) + if (airFilterArea.Slots[i].Items is null) { continue; } - var resourceValue = airFilterArea.Slots[i].Items[0].Upd?.Resource is not null - ? airFilterArea.Slots[i].Items[0].Upd.Resource.Value + var resourceValue = airFilterArea.Slots[i].Items?[0].Upd?.Resource is not null + ? airFilterArea.Slots[i].Items?[0].Upd?.Resource?.Value : null; double pointsConsumed; @@ -912,7 +1041,7 @@ public class HideoutHelper( } else { - pointsConsumed = (airFilterArea.Slots[i].Items[0].Upd.Resource.UnitsConsumed ?? 0) + filterDrainRate ?? 0; + pointsConsumed = (airFilterArea.Slots[i].Items?[0].Upd?.Resource?.UnitsConsumed ?? 0) + filterDrainRate ?? 0; resourceValue -= filterDrainRate; } @@ -928,7 +1057,7 @@ public class HideoutHelper( if (resourceValue > 0) { - airFilterArea.Slots[i].Items[0].Upd = new Upd + airFilterArea.Slots[i].Items![0].Upd = new Upd { StackObjectsCount = 1, Resource = new UpdResource { Value = resourceValue, UnitsConsumed = pointsConsumed }, @@ -1013,7 +1142,7 @@ public class HideoutHelper( var coinSlotCount = GetBTCSlots(pmcData); // Full of bitcoins, halt progress - if (btcProduction.Products.Count >= coinSlotCount) + if (btcProduction.Products?.Count >= coinSlotCount) { // Set progress to 0 btcProduction.Progress = 0; @@ -1023,11 +1152,17 @@ public class HideoutHelper( var bitcoinProdData = databaseService .GetHideout() - .Production.Recipes.FirstOrDefault(production => production.Id == BitcoinProductionId); + .Production.Recipes?.FirstOrDefault(production => production.Id == BitcoinProductionId); + + if (bitcoinProdData is null) + { + logger.Error("Bitcoin production data is null when trying to update bitcoin farm"); + return; + } // BSG finally fixed their settings, they now get loaded from the settings and used in the client var adjustedCraftTime = - (profileHelper.IsDeveloperAccount(pmcData.SessionId.Value) ? 40 : bitcoinProdData.ProductionTime) + (profileHelper.IsDeveloperAccount(pmcData.SessionId!.Value) ? 40 : bitcoinProdData.ProductionTime) / (1 + (btcFarmCGs - 1) * databaseService.GetHideout().Settings.GpuBoostRate); // The progress should be adjusted based on the GPU boost rate, but the target is still the base productionTime @@ -1037,7 +1172,7 @@ public class HideoutHelper( while (btcProduction.Progress >= bitcoinProdData.ProductionTime) { - if (btcProduction.Products.Count < coinSlotCount) + if (btcProduction.Products?.Count < coinSlotCount) // Has space to add a coin to production rewards { AddBtcToProduction(btcProduction, bitcoinProdData.ProductionTime ?? 0); @@ -1059,7 +1194,7 @@ public class HideoutHelper( /// Time to craft a bitcoin protected void AddBtcToProduction(Production btcProd, double coinCraftTimeSeconds) { - btcProd.Products.Add( + btcProd.Products?.Add( new Item { Id = new MongoId(), @@ -1081,29 +1216,34 @@ public class HideoutHelper( /// Amount of time elapsed in seconds protected long? GetTimeElapsedSinceLastServerTick(PmcData pmcData, bool isGeneratorOn, HideoutProduction? recipe = null) { + if (pmcData.Hideout is null) + { + throw new HideoutHelperException("Pmc Hideout is null when trying get last elasped server tick"); + } + // Reduce time elapsed (and progress) when generator is off var timeElapsed = timeUtil.GetTimeStamp() - pmcData.Hideout.SptUpdateLastRunTimestamp; if (recipe is not null) { var hideoutArea = databaseService.GetHideout().Areas.FirstOrDefault(area => area.Type == recipe.AreaType); - if (!(hideoutArea.NeedsFuel ?? false)) + if (!(hideoutArea?.NeedsFuel ?? false)) // e.g. Lavatory works at 100% when power is on / off { return timeElapsed; } } - if (!isGeneratorOn) + if (!isGeneratorOn && timeElapsed.HasValue) { - timeElapsed = (long)(timeElapsed * databaseService.GetHideout().Settings.GeneratorSpeedWithoutFuel); + timeElapsed = (long)(timeElapsed * databaseService.GetHideout().Settings.GeneratorSpeedWithoutFuel!.Value); } return timeElapsed; } /// - /// Get a count of how many possible BTC can be gathered by the profile + /// Get a count of how much possible BTC can be gathered by the profile /// /// Profile to look up /// Coin slot count @@ -1111,7 +1251,7 @@ public class HideoutHelper( { var bitcoinProductions = databaseService .GetHideout() - .Production.Recipes.FirstOrDefault(production => production.Id == BitcoinProductionId); + .Production.Recipes?.FirstOrDefault(production => production.Id == BitcoinProductionId); var productionSlots = bitcoinProductions?.ProductionLimitCount ?? 3; // Default to 3 if none found var hasManagementSkillSlots = profileHelper.HasEliteSkillLevel(SkillTypes.HideoutManagement, pmcData); var managementSlotsCount = GetEliteSkillAdditionalBitcoinSlotCount() ?? 2; @@ -1145,7 +1285,7 @@ public class HideoutHelper( // at level 1 you already get 0.5%, so it goes up until level 50. For some reason the wiki // says that it caps at level 51 with 25% but as per dump data that is incorrect apparently var roundedLevel = Math.Floor(hideoutManagementSkill.Progress / 100); - roundedLevel = roundedLevel == 51 ? roundedLevel - 1 : roundedLevel; + roundedLevel = roundedLevel.Approx(51d) ? roundedLevel - 1 : roundedLevel; return roundedLevel * databaseService.GetGlobals().Configuration.SkillsSettings.HideoutManagement.ConsumptionReductionPerLevel @@ -1171,7 +1311,7 @@ public class HideoutHelper( // at level 1 you already get 0.5%, so it goes up until level 50. For some reason the wiki // says that it caps at level 51 with 25% but as per dump data that is incorrect apparently var roundedLevel = Math.Floor(profileSkill.Progress / 100); - roundedLevel = roundedLevel == 51 ? roundedLevel - 1 : roundedLevel; + roundedLevel = roundedLevel.Approx(51d) ? roundedLevel - 1 : roundedLevel; return roundedLevel * valuePerLevel / 100; } @@ -1200,8 +1340,18 @@ public class HideoutHelper( /// Output object to update public void GetBTC(PmcData pmcData, HideoutTakeProductionRequestData request, MongoId sessionId, ItemEventRouterResponse output) { + if (pmcData.Hideout?.Production is null) + { + throw new HideoutHelperException("Hideout productions are null when trying to retrieve bitcoin productions"); + } + + if (pmcData.Hideout?.Production?.TryGetValue(BitcoinProductionId, out var bitcoinCraft) is null or false) + { + logger.Error("Bitcoin production does not exist when trying to retrieve bitcoin productions"); + return; + } + // Get how many coins were crafted and ready to pick up - pmcData.Hideout.Production.TryGetValue(BitcoinProductionId, out var bitcoinCraft); var craftedCoinCount = bitcoinCraft?.Products?.Count; if (bitcoinCraft is null || craftedCoinCount is null) { @@ -1246,15 +1396,15 @@ public class HideoutHelper( // Is at max capacity + we collected all coins - reset production start time var coinSlotCount = GetBTCSlots(pmcData); - if (pmcData.Hideout.Production[BitcoinProductionId].Products.Count >= coinSlotCount) + if (bitcoinCraft.Products?.Count >= coinSlotCount) // Set start to now { - pmcData.Hideout.Production[BitcoinProductionId].StartTimestamp = timeUtil.GetTimeStamp(); + bitcoinCraft.StartTimestamp = timeUtil.GetTimeStamp(); } // Remove crafted coins from production in profile now they've been collected // Can only collect all coins, not individually - pmcData.Hideout.Production[BitcoinProductionId].Products = []; + bitcoinCraft.Products = []; } /// @@ -1264,7 +1414,7 @@ public class HideoutHelper( /// true if complete protected bool HideoutImprovementIsComplete(HideoutImprovement improvement) { - return improvement?.Completed ?? false; + return improvement.Completed ?? false; } /// @@ -1273,14 +1423,14 @@ public class HideoutHelper( /// Profile to adjust public void SetHideoutImprovementsToCompleted(PmcData profileData) { - foreach (var improvementId in profileData.Hideout.Improvements) + foreach (var improvementId in profileData.Hideout?.Improvements ?? []) { - if (!profileData.Hideout.Improvements.TryGetValue(improvementId.Key, out var improvementDetails)) + if (profileData.Hideout?.Improvements?.TryGetValue(improvementId.Key, out var improvementDetails) is null or false) { continue; } - if (improvementDetails.Completed == false && improvementDetails.ImproveCompleteTimestamp < timeUtil.GetTimeStamp()) + if (improvementDetails?.Completed == false && improvementDetails.ImproveCompleteTimestamp < timeUtil.GetTimeStamp()) { improvementDetails.Completed = true; } @@ -1293,28 +1443,58 @@ public class HideoutHelper( /// Player profile public void ApplyPlaceOfFameDogtagBonus(PmcData pmcData) { - var fameAreaProfile = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.PlaceOfFame); + var fameAreaProfile = pmcData.Hideout?.Areas?.FirstOrDefault(area => area.Type == HideoutAreas.PlaceOfFame); + if (fameAreaProfile is null) + { + logger.Error("Could not locate fame area in profile when trying to apply dogtag bonus"); + return; + } // Get hideout area 16 bonus array var fameAreaDb = databaseService.GetHideout().Areas.FirstOrDefault(area => area.Type == HideoutAreas.PlaceOfFame); + if (fameAreaDb is null) + { + logger.Error("Could not locate fame area in database when trying to apply dogtag bonus"); + return; + } + + if (fameAreaDb.Stages?.TryGetValue(fameAreaProfile.Level?.ToString() ?? string.Empty, out var stage) is null or false) + { + logger.Error($"Could not locate stage: {fameAreaProfile.Level?.ToString() ?? "`Level is null`"} in fame area"); + return; + } // Get SkillGroupLevelingBoost object - var combatBoostBonusDb = fameAreaDb - .Stages[fameAreaProfile.Level.ToString()] - .Bonuses.FirstOrDefault(bonus => bonus.Type.ToString() == "SkillGroupLevelingBoost"); + var combatBoostBonusDb = stage.Bonuses?.FirstOrDefault(bonus => bonus.Type.ToString() == "SkillGroupLevelingBoost"); // Get SkillGroupLevelingBoost object in profile - var combatBonusProfile = pmcData.Bonuses.FirstOrDefault(bonus => bonus.Id == combatBoostBonusDb.Id); + var combatBonusProfile = pmcData.Bonuses?.FirstOrDefault(bonus => bonus.Id == combatBoostBonusDb?.Id); + if (combatBonusProfile is null) + { + logger.Error("Could not locate: "); + return; + } // Get all slotted dogtag items - var activeDogtags = pmcData.Inventory.Items.Where(item => item?.SlotId?.StartsWith("dogtag") ?? false); + var activeDogtags = pmcData.Inventory?.Items?.Where(item => item.SlotId?.StartsWith("dogtag") ?? false); + if (activeDogtags is null) + { + logger.Warning("Could not locate any dogtag in the hall of fame when trying to apply dogtag bonus"); + return; + } // Calculate bonus percent (apply hideoutManagement bonus) var hideoutManagementSkill = pmcData.GetSkillFromProfile(SkillTypes.HideoutManagement); + if (hideoutManagementSkill is null) + { + logger.Error("Could not locate hideout management skill in profile when trying to apply dogtag bonus"); + return; + } + var hideoutManagementSkillBonusPercent = 1 + hideoutManagementSkill.Progress / 10000; // 5100 becomes 0.51, add 1 to it, 1.51 var bonus = GetDogtagCombatSkillBonusPercent(pmcData, activeDogtags) * hideoutManagementSkillBonusPercent; - // Update bonus value to above calcualted value + // Update bonus value to above calculated value combatBonusProfile.Value = Math.Round(bonus, 2); } @@ -1357,7 +1537,12 @@ public class HideoutHelper( public void RemoveHideoutWallBuffsAndDebuffs(HideoutArea wallAreaDb, PmcData pmcData) { // Smush all stage bonuses into one array for easy iteration - var wallBonuses = wallAreaDb.Stages.SelectMany(stage => stage.Value.Bonuses); + var wallBonuses = wallAreaDb.Stages?.SelectMany(stage => stage.Value.Bonuses!); + if (wallBonuses is null) + { + logger.Warning("Could not locate wall bonuses in wall area, what are we trying to remove?"); + return; + } // Get all bonus Ids that the wall adds HashSet bonusIdsToRemove = []; @@ -1372,6 +1557,6 @@ public class HideoutHelper( } // Remove the wall bonuses from profile by id - pmcData.Bonuses = pmcData.Bonuses.Where(bonus => !bonusIdsToRemove.Contains(bonus.Id)).ToList(); + pmcData.Bonuses = pmcData.Bonuses?.Where(bonus => !bonusIdsToRemove.Contains(bonus.Id)).ToList(); } } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs index d9626d1a..8dce0cf0 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Exceptions.Helpers; using SPTarkov.Server.Core.Exceptions.Items; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Common; diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index a452e85e..5198a23c 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -744,7 +744,7 @@ public class LocationLifecycleService( MergePmcAndScavEncyclopedias(serverPmcProfile, scavProfile); // Handle temp, hydration, limb hp/effects - healthHelper.ApplyHealthChangesToProfile(sessionId, serverPmcProfile, postRaidProfile.Health, isDead); + healthHelper.ApplyHealthChangesToProfile(sessionId, serverPmcProfile, postRaidProfile.Health); // Required when player loses limb in-raid and fixes it, max now stuck at 50% or less if lost multiple times var profileTemplate = profileHelper.GetProfileTemplateForSide(fullServerProfile.ProfileInfo.Edition, serverPmcProfile.Info.Side); diff --git a/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs b/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs index 21f6dba7..f62a4593 100644 --- a/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/PaymentService.cs @@ -105,8 +105,8 @@ public class PaymentService( if (payToTrader) { // Convert the amount to the trader's currency and update the sales sum. - var costOfPurchaseInCurrency = handbookHelper.FromRUB( - handbookHelper.InRUB(currencyAmount, currencyTpl), + var costOfPurchaseInCurrency = handbookHelper.FromRoubles( + handbookHelper.InRoubles(currencyAmount, currencyTpl), trader.Currency.Value.GetCurrencyTpl() ); @@ -121,7 +121,7 @@ public class PaymentService( logger.Debug(serverLocalisationService.GetText("payment-zero_price_no_payment")); // Convert the handbook price to the trader's currency and update the sales sum. - var costOfPurchaseInCurrency = handbookHelper.FromRUB( + var costOfPurchaseInCurrency = handbookHelper.FromRoubles( GetTraderItemHandbookPriceRouble(request.ItemId, requestTransactionId) ?? 0, trader.Currency.Value.GetCurrencyTpl() ); @@ -190,7 +190,7 @@ public class PaymentService( } var currencyTpl = trader.Currency.Value.GetCurrencyTpl(); - var calcAmount = handbookHelper.FromRUB(handbookHelper.InRUB(amountToSend ?? 0, currencyTpl), currencyTpl); + var calcAmount = handbookHelper.FromRoubles(handbookHelper.InRoubles(amountToSend ?? 0, currencyTpl), currencyTpl); var currencyMaxStackSize = itemHelper.GetItem(currencyTpl).Value.Properties?.StackMaxSize; if (currencyMaxStackSize is null) { diff --git a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs index 512fe6d4..2dc90d29 100644 --- a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs @@ -565,7 +565,7 @@ public class ProfileFixerService( { // no bonus, add to profile logger.Debug($"Profile has level {level} area {profileArea.Type} but no bonus found, adding {bonus.Type}"); - hideoutHelper.ApplyPlayerUpgradesBonuses(pmcProfile, bonus); + hideoutHelper.ApplyPlayerUpgradesBonus(pmcProfile, bonus); } } } diff --git a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs index dfbd6b7b..b61940ad 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs @@ -268,7 +268,7 @@ public class RagfairPriceService( // Convert to different currency if required. if (desiredCurrency != Money.ROUBLES) { - price = handbookHelper.FromRUB(price, desiredCurrency); + price = handbookHelper.FromRoubles(price, desiredCurrency); } if (price <= 0)