Merge tag '4.0.13'
This commit is contained in:
@@ -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
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- SPT specific -->
|
||||
<SptVersion Condition="'$(SptVersion)' == ''">4.0.12</SptVersion>
|
||||
<SptVersion Condition="'$(SptVersion)' == ''">4.0.13</SptVersion>
|
||||
<SptCommit Condition="'$(SptCommit)' == ''">a12b34</SptCommit>
|
||||
<SptBuildTime Condition="'$(SptBuildTime)' == ''">0000000000</SptBuildTime>
|
||||
<SptBuildType Condition="'$(SptBuildType)' == ''">LOCAL</SptBuildType>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"profileSaveIntervalSeconds": 60,
|
||||
"sptFriendNickname": "SPT",
|
||||
"allowProfileWipe": true,
|
||||
"enableNoGCRegions": true,
|
||||
"enableNoGCRegions": false,
|
||||
"noGCRegionMaxMemoryGB": 4,
|
||||
"noGCRegionMaxLOHMemoryGB": 3,
|
||||
"bsgLogging": {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"inRaid": 60,
|
||||
"outOfRaid": 10
|
||||
},
|
||||
"expCraftAmount": 10,
|
||||
"craftingExpAmount": 12.5,
|
||||
"craftingExpForHoursOfCrafting": 3.75,
|
||||
"overrideCraftTimeSeconds": -1,
|
||||
"overrideBuildTimeSeconds": -1,
|
||||
"updateProfileHideoutWhenActiveWithinMinutes": 90,
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
{
|
||||
"priceMultiplier": 1,
|
||||
"applyRandomizeDurabilityLoss": true,
|
||||
"armorKitSkillPointGainPerRepairPointMultiplier": 0.05,
|
||||
"armorKitSkillPointGainPerRepairPointMultiplier": 0.1,
|
||||
"repairKitIntellectGainMultiplier": {
|
||||
"weapon": 0.045,
|
||||
"armor": 0.03
|
||||
},
|
||||
"maxIntellectGainPerRepair": {
|
||||
"kit": 0.6,
|
||||
"trader": 0.6
|
||||
"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": {
|
||||
|
||||
@@ -217,7 +217,8 @@ public class HideoutController(
|
||||
profileHelper.AddSkillPointsToPlayer(
|
||||
pmcData,
|
||||
SkillTypes.HideoutManagement,
|
||||
globals.Configuration.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade
|
||||
globals.Configuration.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -687,6 +688,10 @@ public class HideoutController(
|
||||
);
|
||||
pmcData.Hideout.Production[request.RecipeId].SptIsScavCase = true;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -819,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;
|
||||
|
||||
@@ -860,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;
|
||||
}
|
||||
|
||||
@@ -940,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
@@ -460,7 +461,7 @@ public class ProfileHelper(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add points to a specific skill in player profile
|
||||
/// Add points to a specific skill in player profile, adjusted for low levels by default
|
||||
/// </summary>
|
||||
/// <param name="pmcProfile">Player profile with skill</param>
|
||||
/// <param name="skill">Skill to add points to</param>
|
||||
@@ -472,6 +473,25 @@ public class ProfileHelper(
|
||||
double pointsToAddToSkill,
|
||||
bool useSkillProgressRateMultiplier = false
|
||||
)
|
||||
{
|
||||
AddSkillPointsToPlayer(pmcProfile, skill, pointsToAddToSkill, useSkillProgressRateMultiplier, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add points to a specific skill in player profile
|
||||
/// </summary>
|
||||
/// <param name="pmcProfile">Player profile with skill</param>
|
||||
/// <param name="skill">Skill to add points to</param>
|
||||
/// <param name="pointsToAddToSkill">Points to add</param>
|
||||
/// <param name="useSkillProgressRateMultiplier">Skills are multiplied by a value in globals, default is off to maintain compatibility with legacy code</param>
|
||||
/// <param name="adjustSkillExpForLowLevels">Skills are multiplied by a multiplier for lower levels; if false, treats every level as requiring 100 points</param>
|
||||
public void AddSkillPointsToPlayer(
|
||||
PmcData pmcProfile,
|
||||
SkillTypes skill,
|
||||
double pointsToAddToSkill,
|
||||
bool useSkillProgressRateMultiplier = false,
|
||||
bool adjustSkillExpForLowLevels = true
|
||||
)
|
||||
{
|
||||
if (pointsToAddToSkill < 0D)
|
||||
{
|
||||
@@ -493,25 +513,105 @@ 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
|
||||
? AdjustSkillExpForLowLevels(profileSkill.Progress, pointsToAddToSkill)
|
||||
: 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: {adjustedSkillProgress} points to skill: {skill}, new progress value is: {profileSkill.Progress}");
|
||||
}
|
||||
|
||||
profileSkill.LastAccess = timeUtil.GetTimeStamp();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method calculates the adjusted skill progression for lower levels.
|
||||
/// </summary>
|
||||
/// <param name="currentProgress">Current internal progress value of the skill, used to determine current level</param>
|
||||
/// <param name="visualProgressAmount">The amount of visual progress to add</param>
|
||||
/// <returns>Scaled skill progress according to level</returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the provided session id for a developer account
|
||||
/// </summary>
|
||||
|
||||
@@ -110,6 +110,7 @@ public class QuestHelper(
|
||||
/// <param name="profileSkill">the skill experience is being added to</param>
|
||||
/// <param name="progressAmount">the amount of experience being added to the skill</param>
|
||||
/// <returns>the adjusted skill progress gain</returns>
|
||||
[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
|
||||
|
||||
@@ -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<SkillTypes>(reward.Target),
|
||||
reward.Value.GetValueOrDefault(0)
|
||||
reward.Value.GetValueOrDefault(0),
|
||||
useSkillProgressRateMultiplier: false,
|
||||
adjustSkillExpForLowLevels: false
|
||||
);
|
||||
break;
|
||||
case RewardType.Experience:
|
||||
|
||||
@@ -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<BundleLoader> logger, JsonUtil jsonUtil, BundleHashCacheService bundleHashCacheService)
|
||||
{
|
||||
private readonly Dictionary<string, BundleInfo> _bundles = [];
|
||||
private readonly ConcurrentDictionary<string, BundleInfo> _bundles = [];
|
||||
|
||||
public async Task LoadBundlesAsync(SptMod mod)
|
||||
{
|
||||
await bundleHashCacheService.HydrateCache();
|
||||
|
||||
var modPath = mod.GetModPath();
|
||||
|
||||
var modBundles = await jsonUtil.DeserializeFromFileAsync<BundleManifest>(
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -26,8 +26,9 @@ public record RepairConfig : BaseConfig
|
||||
/// <summary>
|
||||
/// How much INT can be given to player per repair action
|
||||
/// </summary>
|
||||
[JsonPropertyName("maxIntellectGainPerRepair")]
|
||||
public required MaxIntellectGainValues MaxIntellectGainPerRepair { get; set; }
|
||||
[Obsolete("Removed in SPT 4.1 - Only for backwards compatibility, does nothing")]
|
||||
[JsonIgnore]
|
||||
public MaxIntellectGainValues MaxIntellectGainPerRepair { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("weaponTreatment")]
|
||||
public required WeaponTreatmentRepairValues WeaponTreatment { get; set; }
|
||||
|
||||
@@ -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<string> 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<T>()
|
||||
where T : BaseConfig
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using System.Collections.Concurrent;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
|
||||
@@ -10,7 +10,7 @@ public class BundleHashCacheService(ISptLogger<BundleHashCacheService> logger, J
|
||||
{
|
||||
protected const string _bundleHashCachePath = "./user/cache/";
|
||||
protected const string _cacheName = "bundleHashCache.json";
|
||||
protected Dictionary<string, uint> _bundleHashes = [];
|
||||
protected ConcurrentDictionary<string, uint> _bundleHashes = [];
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
|
||||
public async Task HydrateCache()
|
||||
@@ -28,7 +28,7 @@ public class BundleHashCacheService(ISptLogger<BundleHashCacheService> logger, J
|
||||
return;
|
||||
}
|
||||
|
||||
_bundleHashes = await jsonUtil.DeserializeFromFileAsync<Dictionary<string, uint>>(fullCachePath) ?? [];
|
||||
_bundleHashes = await jsonUtil.DeserializeFromFileAsync<ConcurrentDictionary<string, uint>>(fullCachePath) ?? [];
|
||||
}
|
||||
|
||||
public async Task WriteCache()
|
||||
@@ -64,7 +64,7 @@ public class BundleHashCacheService(ISptLogger<BundleHashCacheService> 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");
|
||||
}
|
||||
@@ -79,7 +79,7 @@ public class BundleHashCacheService(ISptLogger<BundleHashCacheService> logger, J
|
||||
|
||||
if (!MatchWithStoredHash(BundlePath, hash))
|
||||
{
|
||||
await StoreValue(BundlePath, await CalculateHash(BundlePath));
|
||||
await StoreValue(BundlePath, hash);
|
||||
}
|
||||
|
||||
return hash;
|
||||
@@ -87,7 +87,7 @@ public class BundleHashCacheService(ISptLogger<BundleHashCacheService> logger, J
|
||||
|
||||
protected async Task<uint> CalculateHash(string BundlePath)
|
||||
{
|
||||
return hashUtil.GenerateCrc32ForData(await fileUtil.ReadFileAsBytesAsync(BundlePath));
|
||||
return await hashUtil.GenerateCrc32ForFileAsync(BundlePath);
|
||||
}
|
||||
|
||||
protected bool MatchWithStoredHash(string BundlePath, uint hash)
|
||||
|
||||
@@ -185,18 +185,34 @@ 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)
|
||||
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 +220,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 +232,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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -232,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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a CRC32 hash for a file, reading in chunks using a pooled buffer to reduce allocations.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the file</param>
|
||||
/// <returns>The CRC32 hash as a uint</returns>
|
||||
public async Task<uint> 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<byte>.Shared.Rent(81920);
|
||||
try
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
|
||||
{
|
||||
crc.Append(buffer.AsSpan(0, bytesRead));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
return crc.GetCurrentHashAsUInt32();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a hash for the data parameter
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public class SptLogger<T> : ISptLogger<T>
|
||||
|
||||
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<T> : ISptLogger<T>
|
||||
_ => 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"configProperties": {
|
||||
"System.GC.ConserveMemory": 5
|
||||
}
|
||||
}
|
||||
@@ -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<ProfileHelper>(),
|
||||
new SPTarkov.Server.Core.Utils.Cloners.FastCloner(),
|
||||
DI.GetInstance().GetService<SaveServer>(),
|
||||
DI.GetInstance().GetService<DatabaseService>(),
|
||||
DI.GetInstance().GetService<Watermark>(),
|
||||
DI.GetInstance().GetService<TimeUtil>(),
|
||||
DI.GetInstance().GetService<ServerLocalisationService>(),
|
||||
DI.GetInstance().GetService<ConfigServer>()
|
||||
);
|
||||
}
|
||||
|
||||
[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<double[]> 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user