From d8bbbbeef346451d92b79a2c1b143ad1f66c8493 Mon Sep 17 00:00:00 2001 From: Archangel Date: Thu, 5 Feb 2026 23:00:08 +0100 Subject: [PATCH 01/11] Bump version --- Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.props b/Build.props index f2f94437..3f2b95c6 100644 --- a/Build.props +++ b/Build.props @@ -1,7 +1,7 @@ - 4.0.12 + 4.0.13 a12b34 0000000000 LOCAL From 90c577bd29f9fbf7480258fcb73d11fe4dca0816 Mon Sep 17 00:00:00 2001 From: Archangel Date: Fri, 6 Feb 2026 22:41:13 +0100 Subject: [PATCH 02/11] Various allocation & GC improvements --- .../SPT_Data/configs/core.json | 2 +- .../Utils/Logger/Handlers/BaseLogHandler.cs | 33 +++++++++++++------ .../Utils/Logger/SptLogger.cs | 17 ++++++---- .../Utils/Logger/SptLoggerConfiguration.cs | 27 ++++++++++++++- SPTarkov.Server/runtimeconfig.template.json | 5 +++ 5 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 SPTarkov.Server/runtimeconfig.template.json diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json index 47196e06..9f3d1a8c 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/core.json @@ -5,7 +5,7 @@ "profileSaveIntervalSeconds": 60, "sptFriendNickname": "SPT", "allowProfileWipe": true, - "enableNoGCRegions": true, + "enableNoGCRegions": false, "noGCRegionMaxMemoryGB": 4, "noGCRegionMaxLOHMemoryGB": 3, "bsgLogging": { diff --git a/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/BaseLogHandler.cs b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/BaseLogHandler.cs index 2857ad98..6a4877da 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/BaseLogHandler.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Logger/Handlers/BaseLogHandler.cs @@ -8,19 +8,32 @@ public abstract class BaseLogHandler : ILogHandler protected string FormatMessage(string processedMessage, SptLogMessage message, BaseSptLoggerReference reference) { - var formattedMessage = reference - .Format.Replace("%date%", message.LogTime.ToString("yyyy-MM-dd")) - .Replace("%time%", message.LogTime.ToString("HH:mm:ss.fff")) - .Replace("%message%", processedMessage) - .Replace("%loggerShort%", message.Logger.Split('.').Last()) - .Replace("%logger%", message.Logger) - .Replace("%tid%", message.threadId.ToString()) - .Replace("%tname%", message.threadName) - .Replace("%level%", Enum.GetName(message.LogLevel)); + var format = reference.GetCompiledFormat(); + + var formattedMessage = string.Format( + null, + format, + message.LogTime.ToString("yyyy-MM-dd"), + message.LogTime.ToString("HH:mm:ss.fff"), + processedMessage, + GetLoggerShortName(message.Logger), + message.Logger, + message.threadId, + message.threadName, + message.LogLevel.ToString() + ); + if (message.Exception != null) { - formattedMessage += $"\n{message.Exception.Message}\n{message.Exception.StackTrace}"; + return string.Concat(formattedMessage, "\n", message.Exception.Message, "\n", message.Exception.StackTrace); } + return formattedMessage; } + + protected string GetLoggerShortName(string logger) + { + var lastDotIndex = logger.AsSpan().LastIndexOf('.'); + return lastDotIndex >= 0 ? logger.Substring(lastDotIndex + 1) : logger; + } } diff --git a/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLogger.cs b/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLogger.cs index 29b7d599..0113fd6c 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLogger.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLogger.cs @@ -13,7 +13,7 @@ public class SptLogger : ISptLogger private const string ConfigurationPath = "./sptLogger.json"; private const string ConfigurationPathDev = "./sptLogger.Development.json"; - private SptLoggerConfiguration _config; + private static SptLoggerConfiguration? _config = null; public SptLogger(FileUtil fileUtil, JsonUtil jsonUtil, SptLoggerQueueManager loggerQueueManager) { @@ -27,13 +27,16 @@ public class SptLogger : ISptLogger _ => false, }; - if (!ProgramStatics.DEBUG() || IsReleaseType) + if (_config is null) { - LoadConfig(fileUtil, jsonUtil, ConfigurationPath); - } - else - { - LoadConfig(fileUtil, jsonUtil, ConfigurationPathDev); + if (!ProgramStatics.DEBUG() || IsReleaseType) + { + LoadConfig(fileUtil, jsonUtil, ConfigurationPath); + } + else + { + LoadConfig(fileUtil, jsonUtil, ConfigurationPathDev); + } } if (_config == null) diff --git a/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs b/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs index ac5affcd..11183fb1 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Logger/SptLoggerConfiguration.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Text; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; @@ -28,7 +29,31 @@ public abstract class BaseSptLoggerReference public LogLevel LogLevel { get; set; } [JsonPropertyName("format")] - public string Format { get; set; } + public required string Format { get; set; } + + private string? _cachedFormat; + private CompositeFormat? _compiledFormat; + + public virtual CompositeFormat GetCompiledFormat() + { + if (_cachedFormat != Format) + { + var convertedFormat = Format + .Replace("%date%", "{0}") + .Replace("%time%", "{1}") + .Replace("%message%", "{2}") + .Replace("%loggerShort%", "{3}") + .Replace("%logger%", "{4}") + .Replace("%tid%", "{5}") + .Replace("%tname%", "{6}") + .Replace("%level%", "{7}"); + + _compiledFormat = CompositeFormat.Parse(convertedFormat); + _cachedFormat = Format; + } + + return _compiledFormat!; + } } public class SptLoggerFilter diff --git a/SPTarkov.Server/runtimeconfig.template.json b/SPTarkov.Server/runtimeconfig.template.json new file mode 100644 index 00000000..1cf37778 --- /dev/null +++ b/SPTarkov.Server/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.GC.ConserveMemory": 5 + } +} From 4b1cad1c902bc6b0691c1f36c31616585b741810 Mon Sep 17 00:00:00 2001 From: Archangel Date: Sat, 7 Feb 2026 00:13:50 +0100 Subject: [PATCH 03/11] Improve server bundle loading Speeds up SPT server initialization and reduces allocations when a lot of bundle mods are active --- .../Loaders/BundleLoader.cs | 43 +++++++++++-------- .../Services/BundleHashCacheService.cs | 5 +-- .../SPTarkov.Server.Core/Utils/HashUtil.cs | 29 +++++++++++++ 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Loaders/BundleLoader.cs b/Libraries/SPTarkov.Server.Core/Loaders/BundleLoader.cs index c7641ffe..55d5b766 100644 --- a/Libraries/SPTarkov.Server.Core/Loaders/BundleLoader.cs +++ b/Libraries/SPTarkov.Server.Core/Loaders/BundleLoader.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Collections.Concurrent; +using System.Text.Json.Serialization; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Spt.Mod; @@ -35,37 +36,43 @@ public class BundleInfo(string modPath, BundleManifestEntry bundle, uint bundleH [Injectable(InjectionType.Singleton)] public class BundleLoader(ISptLogger logger, JsonUtil jsonUtil, BundleHashCacheService bundleHashCacheService) { - private readonly Dictionary _bundles = []; + private readonly ConcurrentDictionary _bundles = []; public async Task LoadBundlesAsync(SptMod mod) { await bundleHashCacheService.HydrateCache(); var modPath = mod.GetModPath(); - var modBundles = await jsonUtil.DeserializeFromFileAsync( Path.Join(Directory.GetCurrentDirectory(), modPath, "bundles.json") ); - var bundleManifests = modBundles?.Manifest ?? []; + var relativeModPath = modPath.Replace('\\', '/'); + var bundlesPath = Path.Join(relativeModPath, "bundles"); - foreach (var bundleManifest in bundleManifests) + if (modBundles?.Manifest is null) { - var relativeModPath = modPath.Replace('\\', '/'); - - var bundleLocalPath = Path.Join(relativeModPath, "bundles", bundleManifest.Key).Replace('\\', '/'); - - if (!File.Exists(bundleLocalPath)) - { - logger.Warning($"Could not find bundle {bundleManifest.Key} for mod {mod.ModMetadata.Name}"); - continue; - } - - var bundleHash = await bundleHashCacheService.CalculateMatchAndStoreHash(bundleLocalPath); - - AddBundle(bundleManifest.Key, new BundleInfo(relativeModPath, bundleManifest, bundleHash)); + logger.Warning($"Could not find manifest for mod {mod.ModMetadata.Name}, skipping!"); + return; } + await Parallel.ForEachAsync( + modBundles.Manifest, + async (bundleManifest, ct) => + { + var bundleLocalPath = Path.Join(bundlesPath, bundleManifest.Key).Replace('\\', '/'); + + if (!File.Exists(bundleLocalPath)) + { + logger.Warning($"Could not find bundle {bundleManifest.Key} for mod {mod.ModMetadata.Name}"); + return; + } + + var bundleHash = await bundleHashCacheService.CalculateMatchAndStoreHash(bundleLocalPath); + AddBundle(bundleManifest.Key, new BundleInfo(relativeModPath, bundleManifest, bundleHash)); + } + ); + await bundleHashCacheService.WriteCache(); } diff --git a/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs b/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs index 05f56432..745add9d 100644 --- a/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs @@ -1,5 +1,4 @@ using SPTarkov.DI.Annotations; -using SPTarkov.Server.Core.Models.Eft.Profile; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Utils; @@ -79,7 +78,7 @@ public class BundleHashCacheService(ISptLogger logger, J if (!MatchWithStoredHash(BundlePath, hash)) { - await StoreValue(BundlePath, await CalculateHash(BundlePath)); + await StoreValue(BundlePath, hash); } return hash; @@ -87,7 +86,7 @@ public class BundleHashCacheService(ISptLogger logger, J protected async Task CalculateHash(string BundlePath) { - return hashUtil.GenerateCrc32ForData(await fileUtil.ReadFileAsBytesAsync(BundlePath)); + return await hashUtil.GenerateCrc32ForFileAsync(BundlePath); } protected bool MatchWithStoredHash(string BundlePath, uint hash) diff --git a/Libraries/SPTarkov.Server.Core/Utils/HashUtil.cs b/Libraries/SPTarkov.Server.Core/Utils/HashUtil.cs index d3b46ada..1331efc3 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/HashUtil.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/HashUtil.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.IO.Hashing; using System.Security.Cryptography; using System.Text; @@ -18,6 +19,34 @@ public class HashUtil(RandomUtil _randomUtil) return Crc32.HashToUInt32(data); } + /// + /// Generates a CRC32 hash for a file, reading in chunks using a pooled buffer to reduce allocations. + /// + /// The path to the file + /// The CRC32 hash as a uint + public async Task GenerateCrc32ForFileAsync(string filePath) + { + var crc = new Crc32(); + await using var stream = File.OpenRead(filePath); + + // Rent from pool to avoid repeated allocations for each file read + var buffer = ArrayPool.Shared.Rent(81920); + try + { + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer)) > 0) + { + crc.Append(buffer.AsSpan(0, bytesRead)); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + return crc.GetCurrentHashAsUInt32(); + } + /// /// Create a hash for the data parameter /// From 537ffad7f69c55aa34f26c2c9c56852d060e9bf9 Mon Sep 17 00:00:00 2001 From: Archangel Date: Sat, 7 Feb 2026 17:09:06 +0100 Subject: [PATCH 04/11] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e07a2a0a..4034171a 100644 --- a/.gitignore +++ b/.gitignore @@ -426,3 +426,6 @@ fabric.properties # Automatically generated file containing metadata on build Libraries/SPTarkov.Server.Core/Utils/ProgramStatics.Generated.cs + +# Not necessary to be included in the repo +Testing/**/Properties/launchSettings.json \ No newline at end of file From 9f189f2e158e91d63e5904712c1241e529c03c8d Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Sat, 7 Feb 2026 19:45:31 +0100 Subject: [PATCH 05/11] Adjustments for Int/Cha progression in Hideout (#737) * Fixup of some skill handling in hideout * fix format * add limited backwards compat and fix repair int value * more logging and fixes * fix charisma gain from repair * revert divisor into multiplier * run csharpier format * undo formating/unnecessary files * add debug guards --------- Co-authored-by: rootdarkarchon --- .../SPT_Data/configs/repair.json | 8 ++---- .../Controllers/HideoutController.cs | 3 +++ .../Controllers/InsuranceController.cs | 5 +++- .../Helpers/ProfileHelper.cs | 6 +++++ .../Models/Spt/Config/RepairConfig.cs | 5 ++-- .../Services/RepairService.cs | 27 ++++++++++++++----- 6 files changed, 38 insertions(+), 16 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json index ac2155f0..bea9e5d0 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json @@ -3,12 +3,8 @@ "applyRandomizeDurabilityLoss": true, "armorKitSkillPointGainPerRepairPointMultiplier": 0.05, "repairKitIntellectGainMultiplier": { - "weapon": 0.045, - "armor": 0.03 - }, - "maxIntellectGainPerRepair": { - "kit": 0.6, - "trader": 0.6 + "weapon": 0.111, + "armor": 0.077 }, "weaponTreatment": { "critSuccessChance": 0.1, diff --git a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index c770bf01..dcbeb709 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs @@ -687,6 +687,9 @@ public class HideoutController( ); pmcData.Hideout.Production[request.RecipeId].SptIsScavCase = true; + // reward charisma based on skill progress rate for each scav production start + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Charisma, 1, true); + return output; } diff --git a/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs b/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs index 00ec9e5b..e2b61a70 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/InsuranceController.cs @@ -774,7 +774,10 @@ public class InsuranceController( } } - profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Charisma, itemsToInsureCount * 0.01); + // give charisma skill points based on the total price of the insured items divded by 200000rub, multiplied by skill progress rate + double intSkillPoints = (itemsToPay.Sum(c => c.Count ?? 0) / 200000); + logger.Debug($"Insured {itemsToPay.Sum(c => c.Count ?? 0)} value, granting {intSkillPoints} {SkillTypes.Charisma} skill points."); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Charisma, intSkillPoints, true); return output; } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs index 2cf2017b..9b8c2ece 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs @@ -7,6 +7,7 @@ using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.Profile; 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; @@ -509,6 +510,11 @@ public class ProfileHelper( profileSkill.PointsEarnedDuringSession += pointsToAddToSkill; + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Added: {pointsToAddToSkill} points to skill: {skill}, new progress value is: {profileSkill.Progress}"); + } + profileSkill.LastAccess = timeUtil.GetTimeStamp(); } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs index c2330b41..3ec79693 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs @@ -26,8 +26,9 @@ public record RepairConfig : BaseConfig /// /// How much INT can be given to player per repair action /// - [JsonPropertyName("maxIntellectGainPerRepair")] - public required MaxIntellectGainValues MaxIntellectGainPerRepair { get; set; } + [Obsolete("Only for backwards compatibility, does nothing")] + [JsonIgnore] + public MaxIntellectGainValues MaxIntellectGainPerRepair { get; set; } = new(); [JsonPropertyName("weaponTreatment")] public required WeaponTreatmentRepairValues WeaponTreatment { get; set; } diff --git a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs index 137d0fed..edbc2cc9 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs @@ -191,12 +191,26 @@ public class RepairService( profileHelper.AddSkillPointsToPlayer(pmcData, vestSkillToLevel, pointsToAddToVestSkill.GetValueOrDefault(0)); } + // Handle trader repair - gives charisma based on (repair cost/10 * skill progress rate) + if (!repairDetails.RepairedByKit.GetValueOrDefault(true) && repairDetails.RepairCost.HasValue) + { + var charismaFromRepair = repairDetails.RepairCost.Value / 10000; + logger.Debug($"Added: {charismaFromRepair} {SkillTypes.Charisma}"); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Charisma, charismaFromRepair, true); + } + // Handle giving INT to player - differs if using kit/trader and weapon vs armor var intellectGainedFromRepair = GetIntellectGainedFromRepair(repairDetails); if (intellectGainedFromRepair > 0) { - logger.Debug($"Added: {intellectGainedFromRepair} intellect skill"); - profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Intellect, intellectGainedFromRepair); + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug( + $"Added: {intellectGainedFromRepair} {SkillTypes.Intellect}, {intellectGainedFromRepair * 0.1} {SkillTypes.Charisma}" + ); + } + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Intellect, intellectGainedFromRepair, true); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Charisma, intellectGainedFromRepair * 0.1, true); } } @@ -204,12 +218,11 @@ public class RepairService( { if (repairDetails.RepairedByKit.GetValueOrDefault(false)) { - // Weapons/armor have different multipliers + // Weapons/armor have different divisors var intRepairMultiplier = itemHelper.IsOfBaseclass(repairDetails.RepairedItem.Template, BaseClasses.WEAPON) ? RepairConfig.RepairKitIntellectGainMultiplier.Weapon : RepairConfig.RepairKitIntellectGainMultiplier.Armor; - // Limit gain to a max value defined in config.maxIntellectGainPerRepair if (repairDetails.RepairPoints is null) { logger.Error( @@ -217,11 +230,11 @@ public class RepairService( ); } - return Math.Min(repairDetails.RepairPoints.Value * intRepairMultiplier, RepairConfig.MaxIntellectGainPerRepair.Kit); + return repairDetails.RepairAmount.Value * intRepairMultiplier; } - // Trader repair - Not as accurate as kit, needs data from live - return Math.Min(repairDetails.RepairAmount.Value / 10, RepairConfig.MaxIntellectGainPerRepair.Trader); + // Trader repair does not give INT + return 0; } /// From c4082321e9413036247291b0cae7f4afd5d4c6cc Mon Sep 17 00:00:00 2001 From: Archangel Date: Sat, 7 Feb 2026 19:47:40 +0100 Subject: [PATCH 06/11] Update obsolete --- .../SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs index 3ec79693..fd451497 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/RepairConfig.cs @@ -26,7 +26,7 @@ public record RepairConfig : BaseConfig /// /// How much INT can be given to player per repair action /// - [Obsolete("Only for backwards compatibility, does nothing")] + [Obsolete("Removed in SPT 4.1 - Only for backwards compatibility, does nothing")] [JsonIgnore] public MaxIntellectGainValues MaxIntellectGainPerRepair { get; set; } = new(); From effb5cc0e4b7a387409a361a32decce447da198b Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Fri, 13 Feb 2026 13:59:45 +0100 Subject: [PATCH 07/11] Add AdjustSkillExpForLowLevels to AddSkillPointsToPlayer (#738) * Add AdjustSkillExpForLowLevels to AddSkillPointsToPlayer * remove unused AdjustSkillExpForLowLevels * format unit tests * fix suggestions * Revert "remove unused AdjustSkillExpForLowLevels" This reverts commit 43e0ab654378ca7ba1140648b1624730d637073b. * mark as obsolete * fix log message --------- Co-authored-by: rootdarkarchon --- .../Helpers/ProfileHelper.cs | 83 +++++++++++++++++-- .../Helpers/QuestHelper.cs | 1 + .../Tests/Services/ProfileHelperTests.cs | 67 +++++++++++++++ 3 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 Testing/UnitTests/Tests/Services/ProfileHelperTests.cs diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs index 9b8c2ece..93ff6428 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs @@ -494,30 +494,103 @@ public class ProfileHelper( return; } + // already max level, no need to do any further calculations + if (profileSkill.Progress >= 5100) + { + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Player already has max level in skill: {skill}, not adding points"); + } + + profileSkill.LastAccess = timeUtil.GetTimeStamp(); + return; + } + if (useSkillProgressRateMultiplier) { var skillProgressRate = databaseService.GetGlobals().Configuration.SkillsSettings.SkillProgressRate; pointsToAddToSkill *= skillProgressRate; } - if (InventoryConfig.SkillGainMultipliers.TryGetValue(skill.ToString(), out _)) + if (InventoryConfig.SkillGainMultipliers.TryGetValue(skill.ToString(), out var multiplier)) { - pointsToAddToSkill *= InventoryConfig.SkillGainMultipliers[skill.ToString()]; + pointsToAddToSkill *= multiplier; } - profileSkill.Progress += pointsToAddToSkill; + var adjustedSkillProgress = AdjustSkillExpForLowLevels(profileSkill.Progress, pointsToAddToSkill); + profileSkill.Progress += adjustedSkillProgress; profileSkill.Progress = Math.Min(profileSkill.Progress, 5100); // Prevent skill from ever going above level 51 (5100) - profileSkill.PointsEarnedDuringSession += pointsToAddToSkill; + profileSkill.PointsEarnedDuringSession += adjustedSkillProgress; if (logger.IsLogEnabled(LogLevel.Debug)) { - logger.Debug($"Added: {pointsToAddToSkill} points to skill: {skill}, new progress value is: {profileSkill.Progress}"); + logger.Debug($"Added: {adjustedSkillProgress} points to skill: {skill}, new progress value is: {profileSkill.Progress}"); } profileSkill.LastAccess = timeUtil.GetTimeStamp(); } + /// + /// This method calculates the adjusted skill progression for lower levels. + /// + /// Current internal progress value of the skill, used to determine current level + /// The amount of visual progress to add + /// Scaled skill progress according to level + /// + /// It expects to be passed on a value as expected per the visual progress on the UI. + /// It will return scaled internal progress according to the current skill level, to match Tarkovs skill progression curve. + /// So passing on "0.4" will always yield +0.4 progress on the UI for the player. + /// + public double AdjustSkillExpForLowLevels(double currentProgress, double visualProgressAmount) + { + var level = Math.Floor(currentProgress / 100d); + + if (level >= 9) + { + return visualProgressAmount; + } + + double internalAdded = 0; + + // See "CalculateExpOnFirstLevels" in client for original logic + // loop until all visual progress has been used up + while (visualProgressAmount > 0) + { + // scale to apply for levels 1-10, decreasing as level goes higher + var uiMax = 10d * (level + 1d); + var factor = 100d / uiMax; + + // remaining internal points in this level + var inLevel = currentProgress % 100d; + var internalRemaining = 100d - inLevel; + + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"currentLevelRemainingProgress: {internalRemaining}"); + } + + // visual needed to fill the rest of this internal level + var visualToLevelUp = internalRemaining / factor; + + var spendVisual = Math.Min(visualProgressAmount, visualToLevelUp); + var addInternal = spendVisual * factor; + + if (logger.IsLogEnabled(LogLevel.Debug)) + { + logger.Debug($"Progress To Add Adjusted For Level: {addInternal}"); + } + + internalAdded += addInternal; + currentProgress += addInternal; + visualProgressAmount -= spendVisual; + + level = Math.Floor(currentProgress / 100d); + } + + return internalAdded; + } + /// /// Is the provided session id for a developer account /// diff --git a/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs index 1e98865c..987b2110 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/QuestHelper.cs @@ -110,6 +110,7 @@ public class QuestHelper( /// the skill experience is being added to /// the amount of experience being added to the skill /// the adjusted skill progress gain + [Obsolete("Will be removed in 4.1: Use ProfileHelper.AdjustSkillExpForLowLevels instead.")] public int AdjustSkillExpForLowLevels(CommonSkill profileSkill, int progressAmount) { // TODO: what used this? can't find any uses in node diff --git a/Testing/UnitTests/Tests/Services/ProfileHelperTests.cs b/Testing/UnitTests/Tests/Services/ProfileHelperTests.cs new file mode 100644 index 00000000..a6dd960a --- /dev/null +++ b/Testing/UnitTests/Tests/Services/ProfileHelperTests.cs @@ -0,0 +1,67 @@ +using NUnit.Framework; +using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Servers; +using SPTarkov.Server.Core.Services; +using SPTarkov.Server.Core.Utils; +using UnitTests.Mock; + +namespace UnitTests.Tests.Services; + +[TestFixture] +public class ProfileHelperTests +{ + private ProfileHelper _sut; + + [SetUp] + public void Setup() + { + _sut = new ProfileHelper( + new MockLogger(), + new SPTarkov.Server.Core.Utils.Cloners.FastCloner(), + DI.GetInstance().GetService(), + DI.GetInstance().GetService(), + DI.GetInstance().GetService(), + DI.GetInstance().GetService(), + DI.GetInstance().GetService(), + DI.GetInstance().GetService() + ); + } + + [Test] + [TestCaseSource(nameof(GetAdjustSkillExpForLowLevelsTestData))] + public void AdjustSkillExpForLowLevels(double startingProgress, double addedProgress, double expectedAdjustedProgress) + { + var result = _sut.AdjustSkillExpForLowLevels(startingProgress, addedProgress); + Assert.AreEqual(expectedAdjustedProgress, result, 0.001); + } + + private static IEnumerable GetAdjustSkillExpForLowLevelsTestData() + { + // tests for levels 1-10 with +1.0 progress added + yield return [0000, 01.0, 10.000]; // 1, 10.0 XP on UI, +1 => 1.0/ 10.0 = 10.000/100 internal + yield return [0100, 01.0, 05.000]; // 2, 20.0 XP on UI, +1 => 1.0/ 20.0 = 5.000/100 internal + yield return [0200, 01.0, 03.333]; // 3, 30.0 XP on UI, +1 => 1.0/ 30.0 = 3.333/100 internal + yield return [0300, 01.0, 02.500]; // 4, 40.0 XP on UI, +1 => 1.0/ 40.0 = 2.500/100 internal + yield return [0400, 01.0, 02.000]; // 5, 50.0 XP on UI, +1 => 1.0/ 50.0 = 2.000/100 internal + yield return [0500, 01.0, 01.667]; // 6, 60.0 XP on UI, +1 => 1.0/ 60.0 = 1.667/100 internal + yield return [0600, 01.0, 01.428]; // 7, 70.0 XP on UI, +1 => 1.0/ 70.0 = 1.428/100 internal + yield return [0700, 01.0, 01.250]; // 8, 80.0 XP on UI, +1 => 1.0/ 80.0 = 1.250/100 internal + yield return [0800, 01.0, 01.111]; // 9, 90.0 XP on UI, +1 => 1.0/ 90.0 = 1.111/100 internal + yield return [0900, 01.0, 01.000]; // 10, 100.0 XP on UI, +1 => 1.0/100.0 = 1.000/100 internal => no scaling + yield return [1000, 01.0, 01.000]; // 11, 100.0 XP on UI, +1 => 1.0/100.0 = 1.000/100 internal => no scaling + // level boundary tests for partial progress with +4.0 progress added + yield return [0098, 04.0, 21.000]; // 1-> 2, 98 = 9.8/ 10, +4.0 = 9.8-> 10 (+2, -0.2), remaining +3.8 (3.8/ 20) = 19.000/100 => 21.000 total + yield return [0198, 04.0, 14.000]; // 2-> 3, 198 = 19.6/ 20, +4.0 = 19.6-> 20 (+2, -0.4), remaining +3.6 (3.6/ 30) = 12.000/100 => 14.000 total + yield return [0298, 04.0, 10.500]; // 3-> 4, 298 = 29.4/ 30, +4.0 = 29.4-> 30 (+2, -0.6), remaining +3.4 (3.4/ 40) = 8.500/100 => 10.500 total + yield return [0398, 04.0, 08.400]; // 4-> 5, 398 = 39.2/ 40, +4.0 = 39.2-> 40 (+2, -0.8), remaining +3.2 (3.2/ 50) = 6.400/100 => 8.400 total + yield return [0498, 04.0, 07.000]; // 5-> 6, 498 = 49.0/ 50, +4.0 = 49.0-> 50 (+2, -1.0), remaining +3.0 (3.0/ 60) = 5.000/100 => 7.000 total + yield return [0598, 04.0, 06.000]; // 6-> 7, 598 = 58.8/ 60, +4.0 = 58.8-> 60 (+2, -1.2), remaining +2.8 (2.8/ 70) = 4.000/100 => 6.000 total + yield return [0698, 04.0, 05.250]; // 7-> 8, 698 = 68.6/ 70, +4.0 = 68.6-> 70 (+2, -1.4), remaining +2.6 (2.6/ 80) = 3.250/100 => 5.250 total + yield return [0798, 04.0, 04.667]; // 8-> 9, 798 = 78.4/ 80, +4.0 = 78.4-> 80 (+2, -1.6), remaining +2.4 (2.4/ 90) = 2.667/100 => 4.667 total + yield return [0898, 04.0, 04.200]; // 9->10, 898 = 88.2/ 90, +4.0 = 88.2-> 90 (+2, -1.8), remaining +2.2 (2.2/100) = 2.200/100 => 4.200 total + yield return [0998, 04.0, 04.000]; // 10->11, 998 = 98.0/100, +4.0 = 98.0->100 (+2, -2.0), remaining +2.0 (2.0/100) = 2.000/100 => 4.000 total + yield return [1098, 04.0, 04.000]; // 11->12, 1098 = 98.0/100, +4.0 = 98.0->100 (+2, -2.0), remaining +2.0 (2.0/100) = 2.000/100 => 4.000 total + // test multi level boundary jumps + yield return [050, 30.0, 166.667]; // 1-> 3, 50 = 5.0/ 10, +3.0 = 5.0-> 10 (+50, -5.0), remaining +25: 0.0->20 (+100, -20), remaining +5 (5.0/30) = 16.667/100 => 166.667 total + } +} From 9ddbdbf5d48efca21e9b8c8c0b60cc133f40deb5 Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 16 Feb 2026 17:13:35 +0100 Subject: [PATCH 08/11] Use ConcurrentDictionary --- .../Services/BundleHashCacheService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs b/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs index 745add9d..fa96eeaa 100644 --- a/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs @@ -1,5 +1,6 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Models.Utils; +using System.Collections.Concurrent; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Services; @@ -9,7 +10,7 @@ public class BundleHashCacheService(ISptLogger logger, J { protected const string _bundleHashCachePath = "./user/cache/"; protected const string _cacheName = "bundleHashCache.json"; - protected Dictionary _bundleHashes = []; + protected ConcurrentDictionary _bundleHashes = []; private readonly SemaphoreSlim _writeLock = new(1, 1); public async Task HydrateCache() @@ -27,7 +28,7 @@ public class BundleHashCacheService(ISptLogger logger, J return; } - _bundleHashes = await jsonUtil.DeserializeFromFileAsync>(fullCachePath) ?? []; + _bundleHashes = await jsonUtil.DeserializeFromFileAsync>(fullCachePath) ?? []; } public async Task WriteCache() @@ -63,7 +64,7 @@ public class BundleHashCacheService(ISptLogger logger, J protected async Task StoreValue(string bundlePath, uint hash) { - _bundleHashes[bundlePath] = hash; + _bundleHashes.TryAdd(bundlePath, hash); logger.Debug($"Bundle: {bundlePath} hash stored in cache"); } From 8cdce4e54a4b66f058265072fca29b825f8e3091 Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Mon, 16 Feb 2026 16:16:03 +0000 Subject: [PATCH 09/11] Format Style Fixes --- .../SPTarkov.Server.Core/Services/BundleHashCacheService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs b/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs index fa96eeaa..b5828454 100644 --- a/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/BundleHashCacheService.cs @@ -1,6 +1,6 @@ -using SPTarkov.DI.Annotations; +using System.Collections.Concurrent; +using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Models.Utils; -using System.Collections.Concurrent; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Services; From b8f9ab8646d800c5c3fe9be5ae2340f964db41fc Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Wed, 18 Feb 2026 14:43:56 +0100 Subject: [PATCH 10/11] Various Skill Progress Hideout Adjustments (#739) * Adjust ArmorKitSkillPointGainPerRepairPointMultiplier evaluation * Adjust GetWeaponRepairSkillPoints * adjust WeaponRepair Treatment gain values and intellect gain values * enable scaling for hideout upgrade * Adjust HandleRecipe * adjust skill points addition for PrestigeHelper and RewardHelper * format * adjust doc * adjust doc * amend some ABI changes * clarify bool usage * adjust UpdateFuel, WaterFilters and AirFilters * add HideoutManagement skill progression to scav case --------- Co-authored-by: rootdarkarchon --- .../SPT_Data/configs/hideout.json | 3 ++- .../SPT_Data/configs/repair.json | 8 +++--- .../Controllers/HideoutController.cs | 26 ++++++++++++------- .../Controllers/InventoryController.cs | 8 +++++- .../Helpers/HideoutHelper.cs | 6 ++--- .../Helpers/PrestigeHelper.cs | 9 ++++++- .../Helpers/ProfileHelper.cs | 25 ++++++++++++++++-- .../Helpers/RewardHelper.cs | 5 +++- .../Models/Spt/Config/HideoutConfig.cs | 10 +++++-- .../Services/RepairService.cs | 19 +++++--------- 10 files changed, 82 insertions(+), 37 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/hideout.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/hideout.json index 30f80bb8..9b42fcda 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/hideout.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/hideout.json @@ -5,7 +5,8 @@ "inRaid": 60, "outOfRaid": 10 }, - "expCraftAmount": 10, + "craftingExpAmount": 12.5, + "craftingExpForHoursOfCrafting": 3.75, "overrideCraftTimeSeconds": -1, "overrideBuildTimeSeconds": -1, "updateProfileHideoutWhenActiveWithinMinutes": 90, diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json index bea9e5d0..83b46c3b 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/repair.json @@ -1,17 +1,17 @@ { "priceMultiplier": 1, "applyRandomizeDurabilityLoss": true, - "armorKitSkillPointGainPerRepairPointMultiplier": 0.05, + "armorKitSkillPointGainPerRepairPointMultiplier": 0.1, "repairKitIntellectGainMultiplier": { - "weapon": 0.111, - "armor": 0.077 + "weapon": 0.1, + "armor": 0.1 }, "weaponTreatment": { "critSuccessChance": 0.1, "critSuccessAmount": 4, "critFailureChance": 0.1, "critFailureAmount": 4, - "pointGainMultiplier": 0.6 + "pointGainMultiplier": 0.2 }, "repairKit": { "armor": { diff --git a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index dcbeb709..2a137c75 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs @@ -217,7 +217,8 @@ public class HideoutController( profileHelper.AddSkillPointsToPlayer( pmcData, SkillTypes.HideoutManagement, - globals.Configuration.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade + globals.Configuration.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade, + true ); } @@ -687,8 +688,9 @@ public class HideoutController( ); pmcData.Hideout.Production[request.RecipeId].SptIsScavCase = true; - // reward charisma based on skill progress rate for each scav production start + // reward charisma and hideout management based on skill progress rate for each scav production start profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Charisma, 1, true); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 1, true); return output; } @@ -822,7 +824,7 @@ public class HideoutController( } // Variables for management of skill - var craftingExpAmount = 0; + double craftingExpAmount = 0; var counterHoursCrafting = GetCustomSptHoursCraftingTaskConditionCounter(pmcData, recipe); var totalCraftingHours = counterHoursCrafting.Value; @@ -863,19 +865,19 @@ public class HideoutController( // Check if the recipe is the same as the last one - get bonus when crafting same thing multiple times var area = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == recipe.AreaType); if (area is not null && request.RecipeId != area.LastRecipe) - // 1 point per craft upon the end of production for alternating between 2 different crafting recipes in the same module + // 5 points per craft upon the end of production for alternating between 2 different crafting recipes in the same module { - craftingExpAmount += HideoutConfig.ExpCraftAmount; // Default is 10 + craftingExpAmount += HideoutConfig.CraftingExpAmount; // Default is 12.5, scaled (at 0.4 scale => 5 points per alternating craft) } // Update variable with time spent crafting item(s) - // 1 point per 8 hours of crafting + // 1.5 (3.75 w/ applying default 0.4 scale) points per 8 hours of crafting totalCraftingHours += recipe.ProductionTime; if (totalCraftingHours / HideoutConfig.HoursForSkillCrafting >= 1) { // Spent enough time crafting to get a bonus xp multiplier var multiplierCrafting = Math.Floor(totalCraftingHours.Value / HideoutConfig.HoursForSkillCrafting); - craftingExpAmount += (int)(1 * multiplierCrafting); + craftingExpAmount += (HideoutConfig.CraftingExpForHoursOfCrafting * multiplierCrafting); totalCraftingHours -= HideoutConfig.HoursForSkillCrafting * multiplierCrafting; } @@ -943,12 +945,18 @@ public class HideoutController( // Add Crafting skill to player profile if (craftingExpAmount > 0) { - profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Crafting, craftingExpAmount); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Crafting, craftingExpAmount, true); + // TODO: verify this is still giving intellect skill points on live var intellectAmountToGive = 0.5 * Math.Round((double)(craftingExpAmount / 15)); if (intellectAmountToGive > 0) { - profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Intellect, intellectAmountToGive); + profileHelper.AddSkillPointsToPlayer( + pmcData, + SkillTypes.Intellect, + intellectAmountToGive, + useSkillProgressRateMultiplier: false + ); } } diff --git a/Libraries/SPTarkov.Server.Core/Controllers/InventoryController.cs b/Libraries/SPTarkov.Server.Core/Controllers/InventoryController.cs index 2bab4785..eea35a1f 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/InventoryController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/InventoryController.cs @@ -277,7 +277,13 @@ public class InventoryController( } // TODO: update this with correct calculation using values from globals json - profileHelper.AddSkillPointsToPlayer(fullProfile.CharacterData.PmcData, SkillTypes.Intellect, 0.05 * itemTpls.Count()); + // TODO: verify this is still giving intellect skill points on live + profileHelper.AddSkillPointsToPlayer( + fullProfile.CharacterData.PmcData, + SkillTypes.Intellect, + 0.05 * itemTpls.Count(), + useSkillProgressRateMultiplier: false + ); } /// diff --git a/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs index ce408040..6595da79 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/HideoutHelper.cs @@ -723,7 +723,7 @@ public class HideoutHelper( // Fuel consumed / 10 is over 1, add hideout management skill point if (pmcData is not null && Math.Floor(pointsConsumed / 10) >= 1) { - profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 1); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 2, useSkillProgressRateMultiplier: true); pointsConsumed -= 10; } @@ -925,7 +925,7 @@ public class HideoutHelper( // Check units consumed for possible increment of hideout mgmt skill point if (pmcData is not null && Math.Floor(pointsConsumed / 10) >= 1) { - profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 1); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 2, useSkillProgressRateMultiplier: true); pointsConsumed -= 10; } @@ -1076,7 +1076,7 @@ public class HideoutHelper( // check unit consumed for increment skill point if (pmcData is not null && Math.Floor(pointsConsumed / 10) >= 1) { - profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 1); + profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 2, useSkillProgressRateMultiplier: true); pointsConsumed -= 10; } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/PrestigeHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/PrestigeHelper.cs index a912c83f..8a76862a 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/PrestigeHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/PrestigeHelper.cs @@ -195,7 +195,14 @@ public class PrestigeHelper( case RewardType.Skill: if (Enum.TryParse(reward.Target, out SkillTypes result)) { - profileHelper.AddSkillPointsToPlayer(newProfile.CharacterData!.PmcData!, result, reward.Value.GetValueOrDefault(0)); + // skill reward values are always 100 (+1 level), so adjustment for low levels will give a wrong result + profileHelper.AddSkillPointsToPlayer( + newProfile.CharacterData!.PmcData!, + result, + reward.Value.GetValueOrDefault(0), + useSkillProgressRateMultiplier: false, + adjustSkillExpForLowLevels: false + ); } else { diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs index 93ff6428..ea171181 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs @@ -461,7 +461,7 @@ public class ProfileHelper( } /// - /// Add points to a specific skill in player profile + /// Add points to a specific skill in player profile, adjusted for low levels by default /// /// Player profile with skill /// Skill to add points to @@ -473,6 +473,25 @@ public class ProfileHelper( double pointsToAddToSkill, bool useSkillProgressRateMultiplier = false ) + { + AddSkillPointsToPlayer(pmcProfile, skill, pointsToAddToSkill, useSkillProgressRateMultiplier, true); + } + + /// + /// Add points to a specific skill in player profile + /// + /// Player profile with skill + /// Skill to add points to + /// Points to add + /// Skills are multiplied by a value in globals, default is off to maintain compatibility with legacy code + /// Skills are multiplied by a multiplier for lower levels; if false, treats every level as requiring 100 points + public void AddSkillPointsToPlayer( + PmcData pmcProfile, + SkillTypes skill, + double pointsToAddToSkill, + bool useSkillProgressRateMultiplier = false, + bool adjustSkillExpForLowLevels = true + ) { if (pointsToAddToSkill < 0D) { @@ -517,7 +536,9 @@ public class ProfileHelper( pointsToAddToSkill *= multiplier; } - var adjustedSkillProgress = AdjustSkillExpForLowLevels(profileSkill.Progress, pointsToAddToSkill); + var adjustedSkillProgress = adjustSkillExpForLowLevels + ? AdjustSkillExpForLowLevels(profileSkill.Progress, pointsToAddToSkill) + : pointsToAddToSkill; profileSkill.Progress += adjustedSkillProgress; profileSkill.Progress = Math.Min(profileSkill.Progress, 5100); // Prevent skill from ever going above level 51 (5100) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs index 9a7530bd..0124a638 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs @@ -73,10 +73,13 @@ public class RewardHelper( { case RewardType.Skill: // This needs to use the passed in profileData, as it could be the scav profile + // skill reward values are always 100 (+1 level), so adjustment for low levels will give a wrong result profileHelper.AddSkillPointsToPlayer( profileData, Enum.Parse(reward.Target), - reward.Value.GetValueOrDefault(0) + reward.Value.GetValueOrDefault(0), + useSkillProgressRateMultiplier: false, + adjustSkillExpForLowLevels: false ); break; case RewardType.Experience: diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/HideoutConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/HideoutConfig.cs index 1699cbe5..379d3e40 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/HideoutConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/HideoutConfig.cs @@ -24,8 +24,14 @@ public record HideoutConfig : BaseConfig [JsonPropertyName("hoursForSkillCrafting")] public int HoursForSkillCrafting { get; set; } - [JsonPropertyName("expCraftAmount")] - public int ExpCraftAmount { get; set; } + [Obsolete("Will be removed in 4.1, use CraftingExpAmount")] + public int ExpCraftAmount { get; set; } = 0; + + [JsonPropertyName("craftingExpAmount")] + public double CraftingExpAmount { get; set; } + + [JsonPropertyName("craftingExpForHoursOfCrafting")] + public double CraftingExpForHoursOfCrafting { get; set; } [JsonPropertyName("overrideCraftTimeSeconds")] public int OverrideCraftTimeSeconds { get; set; } diff --git a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs index edbc2cc9..37164278 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs @@ -185,10 +185,12 @@ public class RepairService( ); } + // Every 10 points of repair gives 1 skill point scaled by skillProgressRate + // ArmorKitSkillPointGainPerRepairPointMultiplier is 0.1 var pointsToAddToVestSkill = repairDetails.RepairPoints * RepairConfig.ArmorKitSkillPointGainPerRepairPointMultiplier; logger.Debug($"Added: {pointsToAddToVestSkill} {vestSkillToLevel} skill"); - profileHelper.AddSkillPointsToPlayer(pmcData, vestSkillToLevel, pointsToAddToVestSkill.GetValueOrDefault(0)); + profileHelper.AddSkillPointsToPlayer(pmcData, vestSkillToLevel, pointsToAddToVestSkill.GetValueOrDefault(0), true); } // Handle trader repair - gives charisma based on (repair cost/10 * skill progress rate) @@ -245,18 +247,9 @@ public class RepairService( protected double GetWeaponRepairSkillPoints(RepairDetails repairDetails) { var random = new Random(); - // This formula and associated configs is calculated based on 30 repairs done on live - // The points always came out 2-aligned, which is why there's a divide/multiply by 2 with ceil calls - var gainMult = RepairConfig.WeaponTreatment.PointGainMultiplier; - - // First we get a baseline based on our repair amount, and gain multiplier with a bit of rounding - var step1 = Math.Ceiling(repairDetails.RepairAmount.Value / 2) * gainMult; - - // Then we have to get the next even number - var step2 = Math.Ceiling(step1 / 2) * 2; - - // Then multiply by 2 again to hopefully get to what live would give us - var skillPoints = step2 * 2; + // Every 5 points repaired with kit should give 0.4 skill points, so PointGainMultiplier is 0.2 + // The return value is later scaled in AddSkillPointsToPlayer, i.e. 1 skill point returned here = 0.4 skill points added + var skillPoints = repairDetails.RepairAmount.GetValueOrDefault(0) * RepairConfig.WeaponTreatment.PointGainMultiplier; // You can both crit fail and succeed at the same time, for fun (Balances out to 0 with default settings) // Add a random chance to crit-fail From 2891fd41fd07b6150a2192ac0d24adb93eb72862 Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 2 Mar 2026 17:36:33 +0000 Subject: [PATCH 11/11] Improved `ConfigServer` obsolete messages --- Libraries/SPTarkov.Server.Core/Servers/ConfigServer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Servers/ConfigServer.cs b/Libraries/SPTarkov.Server.Core/Servers/ConfigServer.cs index 24558f51..611e9d1a 100644 --- a/Libraries/SPTarkov.Server.Core/Servers/ConfigServer.cs +++ b/Libraries/SPTarkov.Server.Core/Servers/ConfigServer.cs @@ -10,7 +10,7 @@ using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Servers; [Injectable(InjectionType.Singleton)] -[Obsolete("This class will be removed in SPT 4.2 in favor for directly injecting the configuration into classes")] +[Obsolete("This class will be removed in SPT 4.1 in favor for directly injecting the configuration into classes")] public class ConfigServer { protected readonly FrozenSet acceptableFileExtensions = ["json", "jsonc"]; @@ -31,7 +31,7 @@ public class ConfigServer } } - [Obsolete("This method will be removed in SPT 4.2 in favor for directly injecting the configuration into classes")] + [Obsolete("This method will be removed in SPT 4.1 in favor for directly injecting the configuration into classes")] public T GetConfig() where T : BaseConfig {