From 74cf552d48df50e1ee563401f206685735655502 Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 1 Nov 2025 12:20:06 +0000 Subject: [PATCH 01/35] Added check for player listed weapons on flea, calculate the cost of the weapon+mods and use this value as the comparison against player listing price --- .../SPTarkov.Server.Core/Controllers/RagfairController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs index 7e36dcd1..df9108d8 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs @@ -774,6 +774,12 @@ public class RagfairController( // Get average of items quality+children var qualityMultiplier = itemHelper.GetItemQualityModifierForItems(offer.Items, true); + // Player may be listing a custom weapon with non-standard mods, calculate the average price of the listed weapons' mods + if (itemHelper.IsOfBaseclass(offerRootItem.Template, BaseClasses.WEAPON)) + { + averageOfferPriceSingleItem = ragfairPriceService.GetPresetPriceByChildren(offer.Items); + } + // Check for and apply item price modifer if it exists in config if (RagfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(offerRootItem.Template, out var itemPriceModifer)) { @@ -789,6 +795,7 @@ public class RagfairController( playerListedPriceInRub, qualityMultiplier ); + offer.SellResults = ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal); // Subtract flea market fee from stash From 088e7a156c0d51dcdbc17be2697e5d7f82bef4d1 Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 1 Nov 2025 12:22:37 +0000 Subject: [PATCH 02/35] Do not apply ItemPriceMultiplier values to weapon price --- .../Controllers/RagfairController.cs | 10 ++++++---- .../Services/RagfairPriceService.cs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs index df9108d8..dc508139 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs @@ -779,11 +779,13 @@ public class RagfairController( { averageOfferPriceSingleItem = ragfairPriceService.GetPresetPriceByChildren(offer.Items); } - - // Check for and apply item price modifer if it exists in config - if (RagfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(offerRootItem.Template, out var itemPriceModifer)) + else { - averageOfferPriceSingleItem *= itemPriceModifer; + // Check for and apply item price modifer if it exists in config + if (RagfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(offerRootItem.Template, out var itemPriceModifer)) + { + averageOfferPriceSingleItem *= itemPriceModifer; + } } // Multiply single item price by quality diff --git a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs index 4c5668ea..bdb8fbf9 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs @@ -531,7 +531,7 @@ public class RagfairPriceService( /// /// weapon plus mods /// price of weapon in roubles - protected double GetPresetPriceByChildren(IEnumerable weaponWithChildren) + public double GetPresetPriceByChildren(IEnumerable weaponWithChildren) { var priceTotal = 0d; foreach (var item in weaponWithChildren) From a5d17cbadf3cd240e633a791fb1eaee07e6a6a08 Mon Sep 17 00:00:00 2001 From: CWX Date: Sun, 2 Nov 2025 18:33:27 +0000 Subject: [PATCH 03/35] update SptVersion as 4.0.3 already released --- Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.props b/Build.props index e18896e5..758a093c 100644 --- a/Build.props +++ b/Build.props @@ -1,7 +1,7 @@ - 4.0.2 + 4.0.3 a12b34 0000000000 LOCAL From 5eae048d99f0449a3050760369fc1c7e6b808e6f Mon Sep 17 00:00:00 2001 From: Chomp Date: Sun, 2 Nov 2025 19:47:54 +0000 Subject: [PATCH 04/35] Fixed bug in `GetPresetPriceByChildren` where root item was having be a combination of static and dynamic price + fixed root item not always being found --- .../SPTarkov.Server.Core/Services/RagfairPriceService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs index bdb8fbf9..494fd6c3 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs @@ -537,9 +537,11 @@ public class RagfairPriceService( foreach (var item in weaponWithChildren) { // Root item uses static price - if (item.ParentId == null) + if (item.ParentId == null || string.Equals(item.ParentId, "hideout", StringComparison.OrdinalIgnoreCase)) { priceTotal += GetStaticPriceForItem(item.Template) ?? 0; + + continue; } priceTotal += GetFleaPriceForItem(item.Template); From 479ff9cc9df3558d4d642fd8996c2281b1972a0f Mon Sep 17 00:00:00 2001 From: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:24:46 -0800 Subject: [PATCH 05/35] Force spawn starting zombies on all maps - This fixes some maps not spawning zombies at raid start --- .../SPT_Data/configs/seasonalevents.json | 255 ++++++++++++------ 1 file changed, 170 insertions(+), 85 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 19955484..05a3070c 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -1264,7 +1264,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1284,7 +1285,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1304,7 +1306,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [], "Supports": null, @@ -1321,7 +1324,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [], "Supports": null, @@ -1338,7 +1342,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [], "Supports": null, @@ -1355,7 +1360,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1375,7 +1381,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1395,7 +1402,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1415,7 +1423,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1450,7 +1459,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1485,7 +1495,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1513,7 +1524,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1541,7 +1553,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1569,7 +1582,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1597,7 +1611,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1625,7 +1640,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1653,7 +1669,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -1902,7 +1919,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -1921,7 +1939,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -1940,7 +1959,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [], "Supports": null, "Time": 9999, @@ -1956,7 +1976,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [], "Supports": null, "Time": 9999, @@ -1972,7 +1993,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [], "Supports": null, "Time": 9999, @@ -1988,7 +2010,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2007,7 +2030,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2026,7 +2050,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2045,7 +2070,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2079,7 +2105,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2113,7 +2140,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2140,7 +2168,8 @@ "BossName": "infectedAssault", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2167,7 +2196,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2194,7 +2224,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2221,7 +2252,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2248,7 +2280,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -2275,7 +2308,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "SpawnMode": [ "regular", "pve" @@ -3262,7 +3296,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3283,7 +3318,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3304,7 +3340,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [], "Supports": null, @@ -3322,7 +3359,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3343,7 +3381,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [], "Supports": null, @@ -3361,7 +3400,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [], "Supports": null, @@ -3379,7 +3419,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3400,7 +3441,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3421,7 +3463,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3457,7 +3500,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3500,7 +3544,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3529,7 +3574,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3558,7 +3604,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3587,7 +3634,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3616,7 +3664,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3645,7 +3694,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -3674,7 +3724,8 @@ "BossPlayer": false, "BossZone": "BotZoneFloor1,BotZoneFloor2,BotZoneBasement", "Delay": 0, - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5590,7 +5641,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5610,7 +5662,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5630,7 +5683,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5650,7 +5704,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5670,7 +5725,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5690,7 +5746,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5710,7 +5767,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5730,7 +5788,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5750,7 +5809,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5792,7 +5852,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5834,7 +5895,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5869,7 +5931,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5904,7 +5967,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5939,7 +6003,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -5974,7 +6039,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6009,7 +6075,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6044,7 +6111,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6281,7 +6349,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6301,7 +6370,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6321,7 +6391,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6341,7 +6412,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6361,7 +6433,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6381,7 +6454,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6401,7 +6475,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6421,7 +6496,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6441,7 +6517,8 @@ "BossName": "infectedCivil", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6483,7 +6560,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6525,7 +6603,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6560,7 +6639,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6595,7 +6675,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6630,7 +6711,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6665,7 +6747,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6700,7 +6783,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", @@ -6735,7 +6819,8 @@ "BossName": "infectedPmc", "BossPlayer": false, "BossZone": "", - "IgnoreMaxBots": false, + "ForceSpawn": true, + "IgnoreMaxBots": true, "RandomTimeSpawn": false, "SpawnMode": [ "regular", From 26856006ff48e3d1fdde6b74cb12a17c7a5baaed Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 3 Nov 2025 17:37:39 +0000 Subject: [PATCH 06/35] Fixed repairkit resource values going into negatives --- .../Services/RepairService.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs index 86210fa2..225d5167 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs @@ -29,6 +29,7 @@ public class RepairService( PaymentService paymentService, ProfileHelper profileHelper, RepairHelper repairHelper, + InventoryHelper inventoryHelper, ServerLocalisationService serverLocalisationService, ConfigServer configServer, WeightedRandomHelper weightedRandomHelper @@ -292,6 +293,7 @@ public class RepairService( ); // Find and use repair kit defined in body + List kitIdsToDelete = []; foreach (var repairKit in repairKits) { var repairKitInInventory = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == repairKit.Id); @@ -305,10 +307,22 @@ public class RepairService( AddMaxResourceToKitIfMissing(repairKitDetails, repairKitInInventory); - // reduce usages on repairkit used + // Reduce usages on repairkit used + // TODO - correctly reduce kit resource to 0 and then move to next kit and use that repairKitInInventory.Upd.RepairKit.Resource -= repairKitReductionAmount; output.ProfileChanges[sessionId].Items.ChangedItems.Add(repairKitInInventory); + + if (repairKitInInventory.Upd.RepairKit.Resource <= 0) + { + // Repair kit was all used up, flag to delete outside of loop + kitIdsToDelete.Add(repairKit.Id); + } + } + + foreach (var kitId in kitIdsToDelete) + { + inventoryHelper.RemoveItem(pmcData, kitId, sessionId, output); } return new RepairDetails From de55d173ed5a1578f816357d54b8435bf86f0582 Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 3 Nov 2025 18:43:26 +0000 Subject: [PATCH 07/35] Improved accuracy of resources removed from repair kits when used Fixed invalid ID values being logged --- .../Services/RepairService.cs | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs index 225d5167..6b175ffc 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs @@ -50,14 +50,16 @@ public class RepairService( var itemToRepair = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == repairItemDetails.Id); if (itemToRepair is null) { - logger.Error(serverLocalisationService.GetText("repair-unable_to_find_item_in_inventory_cant_repair", repairItemDetails.Id)); + logger.Error( + serverLocalisationService.GetText("repair-unable_to_find_item_in_inventory_cant_repair", repairItemDetails.Id.ToString()) + ); } var priceCoef = traderHelper.GetLoyaltyLevel(traderId, pmcData).RepairPriceCoefficient; var traderRepairDetails = traderHelper.GetTrader(traderId, sessionID)?.Repair; if (traderRepairDetails is null) { - logger.Error(serverLocalisationService.GetText("repair-unable_to_find_trader_details_by_id", traderId)); + logger.Error(serverLocalisationService.GetText("repair-unable_to_find_trader_details_by_id", traderId.ToString())); } var repairQualityMultiplier = traderRepairDetails.Quality; @@ -81,7 +83,7 @@ public class RepairService( var itemRepairCost = items[itemToRepair.Template].Properties.RepairCost; if (itemRepairCost is null) { - logger.Error(serverLocalisationService.GetText("repair-unable_to_find_item_repair_cost", itemToRepair.Template)); + logger.Error(serverLocalisationService.GetText("repair-unable_to_find_item_repair_cost", itemToRepair.Template.ToString())); } var repairCost = Math.Round(itemRepairCost.Value * repairItemDetails.Count.Value * repairRate.Value * RepairConfig.PriceMultiplier); @@ -167,7 +169,9 @@ public class RepairService( if (!itemDetails.Key) { // No item found - logger.Error(serverLocalisationService.GetText("repair-unable_to_find_item_in_db", repairDetails.RepairedItem.Template)); + logger.Error( + serverLocalisationService.GetText("repair-unable_to_find_item_in_db", repairDetails.RepairedItem.Template.ToString()) + ); return; } @@ -176,7 +180,9 @@ public class RepairService( var vestSkillToLevel = isHeavyArmor ? SkillTypes.HeavyVests : SkillTypes.LightVests; if (repairDetails.RepairPoints is null) { - logger.Error(serverLocalisationService.GetText("repair-item_has_no_repair_points", repairDetails.RepairedItem.Template)); + logger.Error( + serverLocalisationService.GetText("repair-item_has_no_repair_points", repairDetails.RepairedItem.Template.ToString()) + ); } var pointsToAddToVestSkill = repairDetails.RepairPoints * RepairConfig.ArmorKitSkillPointGainPerRepairPointMultiplier; @@ -206,7 +212,9 @@ public class RepairService( // Limit gain to a max value defined in config.maxIntellectGainPerRepair if (repairDetails.RepairPoints is null) { - logger.Error(serverLocalisationService.GetText("repair-item_has_no_repair_points", repairDetails.RepairedItem.Template)); + logger.Error( + serverLocalisationService.GetText("repair-item_has_no_repair_points", repairDetails.RepairedItem.Template.ToString()) + ); } return Math.Min(repairDetails.RepairPoints.Value * intRepairMultiplier, RepairConfig.MaxIntellectGainPerRepair.Kit); @@ -273,23 +281,24 @@ public class RepairService( var itemToRepair = pmcData.Inventory.Items.FirstOrDefault(x => x.Id == itemToRepairId); if (itemToRepair is null) { - logger.Error(serverLocalisationService.GetText("repair-item_not_found_unable_to_repair", itemToRepairId)); + logger.Error(serverLocalisationService.GetText("repair-item_not_found_unable_to_repair", itemToRepairId.ToString())); } var itemsDb = databaseService.GetItems(); var itemToRepairDetails = itemsDb[itemToRepair.Template]; var repairItemIsArmor = itemToRepairDetails.Properties.ArmorMaterial is not null; - var repairAmount = repairKits[0].Count / GetKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData); - var shouldApplyDurabilityLoss = ShouldRepairKitApplyDurabilityLoss(pmcData, RepairConfig.ApplyRandomizeDurabilityLoss); + + // Amount to add to gun as durability + var repairAmountTotal = repairKits[0].Count / GetKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData); repairHelper.UpdateItemDurability( itemToRepair, itemToRepairDetails, repairItemIsArmor, - repairAmount.Value, + repairAmountTotal.Value, true, 1, - shouldApplyDurabilityLoss + ShouldRepairKitApplyDurabilityLoss(pmcData, RepairConfig.ApplyRandomizeDurabilityLoss) ); // Find and use repair kit defined in body @@ -299,25 +308,29 @@ public class RepairService( var repairKitInInventory = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == repairKit.Id); if (repairKitInInventory is null) { - logger.Error(serverLocalisationService.GetText("repair-repair_kit_not_found_in_inventory", repairKit.Id)); + logger.Error(serverLocalisationService.GetText("repair-repair_kit_not_found_in_inventory", repairKit.Id.ToString())); } - var repairKitDetails = itemsDb[repairKitInInventory.Template]; - var repairKitReductionAmount = repairKit.Count; + var repairKitDbDetails = itemsDb[repairKitInInventory.Template]; + AddMaxResourceToKitIfMissing(repairKitDbDetails, repairKitInInventory); - AddMaxResourceToKitIfMissing(repairKitDetails, repairKitInInventory); + if (repairKitInInventory.Upd.RepairKit.Resource <= repairKit.Count) + { + // Repair kit will be fully used up + // Flag kit for deletion + kitIdsToDelete.Add(repairKit.Id); - // Reduce usages on repairkit used - // TODO - correctly reduce kit resource to 0 and then move to next kit and use that - repairKitInInventory.Upd.RepairKit.Resource -= repairKitReductionAmount; + // Move on to next repair kit + continue; + } + + // Repair kit had enough resources to repair in one go + // Update server item resource value + repairKitInInventory.Upd.RepairKit.Resource -= repairKit.Count; output.ProfileChanges[sessionId].Items.ChangedItems.Add(repairKitInInventory); - if (repairKitInInventory.Upd.RepairKit.Resource <= 0) - { - // Repair kit was all used up, flag to delete outside of loop - kitIdsToDelete.Add(repairKit.Id); - } + break; } foreach (var kitId in kitIdsToDelete) @@ -330,7 +343,7 @@ public class RepairService( RepairPoints = repairKits[0].Count, RepairedItem = itemToRepair, RepairedItemIsArmor = repairItemIsArmor, - RepairAmount = repairAmount, + RepairAmount = repairAmountTotal, RepairedByKit = true, }; } @@ -362,7 +375,7 @@ public class RepairService( var destructability = 1 + armorMaterial.Destructibility; var armorClass = itemToRepairDetails.Properties.ArmorClass.Value; var armorClassDivisor = globals.Configuration.RepairSettings.ArmorClassDivisor; - var armorClassMultiplier = 1.0 + armorClass / armorClassDivisor; + var armorClassMultiplier = 1.0 + (armorClass / armorClassDivisor); return durabilityPointCostArmor * armorBonus * destructability * armorClassMultiplier; } @@ -428,7 +441,7 @@ public class RepairService( { if (logger.IsLogEnabled(LogLevel.Debug)) { - logger.Debug($"Repair kit: {repairKitInInventory.Id} in inventory lacks upd object, adding"); + logger.Debug($"Repair kit: {repairKitInInventory.Id.ToString()} in inventory lacks upd object, adding"); } repairKitInInventory.Upd = new Upd { RepairKit = new UpdRepairKit { Resource = maxRepairAmount } }; @@ -564,7 +577,9 @@ public class RepairService( if (repairDetails.RepairPoints is null) { - logger.Error(serverLocalisationService.GetText("repair-item_has_no_repair_points", repairDetails.RepairedItem.Template)); + logger.Error( + serverLocalisationService.GetText("repair-item_has_no_repair_points", repairDetails.RepairedItem.Template.ToString()) + ); } var durabilityToRestorePercent = repairDetails.RepairPoints / template.Properties.MaxDurability; From 5f20768f4e4a4b376afdd79cf14fada1f61d10bf Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 09:43:30 +0000 Subject: [PATCH 08/35] Early exit `GetItemQualityModifier` when item `Upd` object is null --- .../Helpers/ItemHelper.cs | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs index 32fcd9e8..a30409f0 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs @@ -537,6 +537,7 @@ public class ItemHelper( { if (IsOfBaseclass(itemWithChildren.First().Template, BaseClasses.WEAPON)) { + // Only root of weapon has durability return Math.Round(GetItemQualityModifier(itemWithChildren.First()), 5); } @@ -594,42 +595,44 @@ public class ItemHelper( return -1; } - if (item.Upd is not null) + if (item.Upd is null) { - if (item.Upd.MedKit is not null) - { - // Meds - result = (item.Upd.MedKit.HpResource ?? 0) / (itemDetails.Properties?.MaxHpResource ?? 0); - } - else if (item.Upd.Repairable is not null) - { - result = GetRepairableItemQualityValue(itemDetails, item.Upd.Repairable, item); - } - else if (item.Upd.FoodDrink is not null) - { - result = (item.Upd.FoodDrink.HpPercent ?? 0) / (itemDetails.Properties?.MaxResource ?? 0); - } - else if (item.Upd.Key?.NumberOfUsages > 0 && itemDetails.Properties?.MaximumNumberOfUsage > 0) - { - // keys - keys count upwards, not down like everything else - var maxNumOfUsages = itemDetails.Properties.MaximumNumberOfUsage; - result = (maxNumOfUsages ?? 0 - item.Upd.Key.NumberOfUsages) / maxNumOfUsages ?? 0; - } - else if (item.Upd.Resource?.UnitsConsumed > 0) - { - // E.g. fuel tank - result = (item.Upd.Resource.Value ?? 0) / (itemDetails.Properties?.MaxResource ?? 0); - } - else if (item.Upd.RepairKit is not null) - { - result = (item.Upd.RepairKit.Resource ?? 0) / (itemDetails.Properties?.MaxRepairResource ?? 0); - } + return result; + } - if (result == 0) - // make item non-zero but still very low - { - result = 0.01; - } + if (item.Upd.MedKit is not null) + { + // Meds + result = (item.Upd.MedKit.HpResource ?? 0) / (itemDetails.Properties?.MaxHpResource ?? 0); + } + else if (item.Upd.Repairable is not null) + { + result = GetRepairableItemQualityValue(itemDetails, item.Upd.Repairable, item); + } + else if (item.Upd.FoodDrink is not null) + { + result = (item.Upd.FoodDrink.HpPercent ?? 0) / (itemDetails.Properties?.MaxResource ?? 0); + } + else if (item.Upd.Key?.NumberOfUsages > 0 && itemDetails.Properties?.MaximumNumberOfUsage > 0) + { + // keys - keys count upwards, not down like everything else + var maxNumOfUsages = itemDetails.Properties.MaximumNumberOfUsage; + result = (maxNumOfUsages ?? 0 - item.Upd.Key.NumberOfUsages) / maxNumOfUsages ?? 0; + } + else if (item.Upd.Resource?.UnitsConsumed > 0) // Item is less than 100% usage + { + // E.g. fuel tank + result = (item.Upd.Resource.Value ?? 0) / (itemDetails.Properties?.MaxResource ?? 0); + } + else if (item.Upd.RepairKit is not null) + { + result = (item.Upd.RepairKit.Resource ?? 0) / (itemDetails.Properties?.MaxRepairResource ?? 0); + } + + if (result == 0) + // make item non-zero but still very low + { + result = 0.01; } return result; From ca3c085e76f5cc2e13b23b88e9b349a4fcb75316 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 09:45:16 +0000 Subject: [PATCH 09/35] Improved sell chance calculation inside `CalculateSellChance`, removes default 10% sell chance on any offer --- Libraries/SPTarkov.Server.Core/Helpers/RagfairSellHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RagfairSellHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RagfairSellHelper.cs index 7a276cb7..72bbc1c6 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RagfairSellHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RagfairSellHelper.cs @@ -37,7 +37,7 @@ public class RagfairSellHelper( // Modifier gets applied twice to either penalize or incentivize over/under pricing (Probably a cleaner way to do this) var sellModifier = averageOfferPriceRub / playerListedPriceRub * sellConfig.SellMultiplier; - var sellChance = Math.Round(baseSellChancePercent * sellModifier * Math.Pow(sellModifier, 3) + 10); // Power of 3 + var sellChance = Math.Round(baseSellChancePercent * sellModifier * (Math.Pow(sellModifier, 3) + 10)); // Power of 3 // Adjust sell chance if below config value if (sellChance < sellConfig.MinSellChancePercent) From 21e5b5f0fc2e19c9da7a773ab75787ca81a7d4be Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 09:47:22 +0000 Subject: [PATCH 10/35] Renamed variable to improve clarity --- .../SPTarkov.Server.Core/Controllers/RagfairController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs index dc508139..f7bb313d 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs @@ -772,7 +772,7 @@ public class RagfairController( var offerRootItem = offer.Items.FirstOrDefault(x => x.Id == offerRequest.Items[0]); // Get average of items quality+children - var qualityMultiplier = itemHelper.GetItemQualityModifierForItems(offer.Items, true); + var qualityMiltiplierForPlayerOffer = itemHelper.GetItemQualityModifierForItems(offer.Items, true); // Player may be listing a custom weapon with non-standard mods, calculate the average price of the listed weapons' mods if (itemHelper.IsOfBaseclass(offerRootItem.Template, BaseClasses.WEAPON)) @@ -789,13 +789,14 @@ public class RagfairController( } // Multiply single item price by quality - averageOfferPriceSingleItem *= qualityMultiplier; + // Target price is adjusted to match quality of player item to create better comparison + averageOfferPriceSingleItem *= qualityMiltiplierForPlayerOffer; // Packs are reduced to the average price of a single item in the pack vs the averaged single price of an item var sellChancePercent = ragfairSellHelper.CalculateSellChance( averageOfferPriceSingleItem.Value, playerListedPriceInRub, - qualityMultiplier + qualityMiltiplierForPlayerOffer ); offer.SellResults = ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal); From ca7733246d57b9e5c97d09f71631764c5ea15078 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 10:11:44 +0000 Subject: [PATCH 11/35] Fixed fuel listings being either min or max, now chooses variable amount --- .../SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs index 2d2d95ce..681e009f 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs @@ -742,7 +742,10 @@ public class RagfairOfferGenerator( if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.FUEL)) { var totalCapacity = itemDetails.Properties.MaxResource; - var remainingFuel = Math.Round((double)totalCapacity * maxMultiplier); + + // Randomise multi between value in config and 1 (100%) + var randomisedMulti = randomUtil.GetDouble(maxMultiplier, 1); + var remainingFuel = Math.Round((double)totalCapacity * randomisedMulti); rootItem.Upd.Resource = new UpdResource { UnitsConsumed = totalCapacity - remainingFuel, Value = remainingFuel }; } } From 288164caafbd3501740ecb31be2b26a11a0c85ba Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 10:13:01 +0000 Subject: [PATCH 12/35] Adjusted fuel listings config lowered min possible fuel amount in listing Higher chance for <100% fuel in listing Single item listings for fuel --- .../SPTarkov.Server.Assets/SPT_Data/configs/ragfair.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/ragfair.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/ragfair.json index 206ac93e..20e3d219 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/ragfair.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/ragfair.json @@ -194,14 +194,14 @@ }, "5d650c3e815116009f6201d2": { "_name": "FUEL", - "conditionChance": 0.12, + "conditionChance": 0.23, "current": { "max": 1, "min": 1 }, "max": { "max": 1, - "min": 0.7 + "min": 0.1 } }, "644120aa86ffbe10ee032b6f": { @@ -323,7 +323,8 @@ "57bef4c42459772e8d35a53b", "55802f4a4bdc2ddb688b4569", "616eb7aea207f41933308f46", - "543be5cb4bdc2deb348b4568" + "543be5cb4bdc2deb348b4568", + "5d650c3e815116009f6201d2" ], "showDefaultPresetsOnly": true, "stackablePercent": { From 009260a3fea7e40684f81d30b671075636c2779a Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 14:03:01 +0000 Subject: [PATCH 13/35] Added ak-50 default preset --- .../SPT_Data/configs/item.json | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json index f6de617c..98c903c0 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json @@ -475,6 +475,74 @@ "_name": "FN SCAR-H X-17 762x51 default", "_parent": "679f5f42ca975ceee4001927", "_type": "Preset" + }, + { + "_changeWeaponName": false, + "_encyclopedia": "67d0576f29f580ebc10efd08", + "_id": "690a065b64b2a3bab0a986da", + "_items": [ + { + "_id": "690a06bc6c65be6d001b4cd1", + "_tpl": "67d0576f29f580ebc10efd08", + "upd": { + "Repairable": { + "Durability": 82, + "MaxDurability": 88 + } + } + }, + { + "_id": "690a06bc6c65be6d001b4d0a", + "_tpl": "67d4178bffb910d21f04720a", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_barrel" + }, + { + "_id": "690a06bc6c65be6d001b4d18", + "_tpl": "67d417c023ec241bb70d4896", + "parentId": "690a06bc6c65be6d001b4d0a", + "slotId": "mod_gas_block" + }, + { + "_id": "690a06bc6c65be6d001b4d32", + "_tpl": "67d41883f378a36c4706eeb7", + "parentId": "690a06bc6c65be6d001b4d0a", + "slotId": "mod_muzzle" + }, + { + "_id": "690a06bc6c65be6d001b4d3b", + "_tpl": "67d416e19bd76ef20f0e743b", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_reciever" + }, + { + "_id": "690a06bc6c65be6d001b4d43", + "_tpl": "5aa66a9be5b5b0214e506e89", + "parentId": "690a06bc6c65be6d001b4d3b", + "slotId": "mod_scope" + }, + { + "_id": "690a06bc6c65be6d001b4d4b", + "_tpl": "5aa66be6e5b5b0214e506e97", + "parentId": "690a06bc6c65be6d001b4d43", + "slotId": "mod_scope" + }, + { + "_id": "690a06bc6c65be6d001b4d5c", + "_tpl": "6087e663132d4d12c81fd96b", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_pistol_grip" + }, + { + "_id": "690a06bc6c65be6d001b4d65", + "_tpl": "67d418d0ffb910d21f04720e", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_magazine" + } + ], + "_name": "AK-50 .50 BMG sniper rifle default", + "_parent": "679f5f42ca975ceee4001927", + "_type": "Preset" } ], "handbookPriceOverride": { From 9643ff065e8e8ad1ed7d4597dcdac92a774caa1d Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Tue, 4 Nov 2025 14:03:53 +0000 Subject: [PATCH 14/35] Format Style Fixes --- .../SPT_Data/configs/item.json | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json index 98c903c0..40d72430 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json @@ -476,70 +476,70 @@ "_parent": "679f5f42ca975ceee4001927", "_type": "Preset" }, - { + { "_changeWeaponName": false, "_encyclopedia": "67d0576f29f580ebc10efd08", "_id": "690a065b64b2a3bab0a986da", "_items": [ - { - "_id": "690a06bc6c65be6d001b4cd1", - "_tpl": "67d0576f29f580ebc10efd08", - "upd": { - "Repairable": { - "Durability": 82, - "MaxDurability": 88 - } - } - }, - { - "_id": "690a06bc6c65be6d001b4d0a", - "_tpl": "67d4178bffb910d21f04720a", - "parentId": "690a06bc6c65be6d001b4cd1", - "slotId": "mod_barrel" - }, - { - "_id": "690a06bc6c65be6d001b4d18", - "_tpl": "67d417c023ec241bb70d4896", - "parentId": "690a06bc6c65be6d001b4d0a", - "slotId": "mod_gas_block" - }, - { - "_id": "690a06bc6c65be6d001b4d32", - "_tpl": "67d41883f378a36c4706eeb7", - "parentId": "690a06bc6c65be6d001b4d0a", - "slotId": "mod_muzzle" - }, - { - "_id": "690a06bc6c65be6d001b4d3b", - "_tpl": "67d416e19bd76ef20f0e743b", - "parentId": "690a06bc6c65be6d001b4cd1", - "slotId": "mod_reciever" - }, - { - "_id": "690a06bc6c65be6d001b4d43", - "_tpl": "5aa66a9be5b5b0214e506e89", - "parentId": "690a06bc6c65be6d001b4d3b", - "slotId": "mod_scope" - }, - { - "_id": "690a06bc6c65be6d001b4d4b", - "_tpl": "5aa66be6e5b5b0214e506e97", - "parentId": "690a06bc6c65be6d001b4d43", - "slotId": "mod_scope" - }, - { - "_id": "690a06bc6c65be6d001b4d5c", - "_tpl": "6087e663132d4d12c81fd96b", - "parentId": "690a06bc6c65be6d001b4cd1", - "slotId": "mod_pistol_grip" - }, - { - "_id": "690a06bc6c65be6d001b4d65", - "_tpl": "67d418d0ffb910d21f04720e", - "parentId": "690a06bc6c65be6d001b4cd1", - "slotId": "mod_magazine" - } - ], + { + "_id": "690a06bc6c65be6d001b4cd1", + "_tpl": "67d0576f29f580ebc10efd08", + "upd": { + "Repairable": { + "Durability": 82, + "MaxDurability": 88 + } + } + }, + { + "_id": "690a06bc6c65be6d001b4d0a", + "_tpl": "67d4178bffb910d21f04720a", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_barrel" + }, + { + "_id": "690a06bc6c65be6d001b4d18", + "_tpl": "67d417c023ec241bb70d4896", + "parentId": "690a06bc6c65be6d001b4d0a", + "slotId": "mod_gas_block" + }, + { + "_id": "690a06bc6c65be6d001b4d32", + "_tpl": "67d41883f378a36c4706eeb7", + "parentId": "690a06bc6c65be6d001b4d0a", + "slotId": "mod_muzzle" + }, + { + "_id": "690a06bc6c65be6d001b4d3b", + "_tpl": "67d416e19bd76ef20f0e743b", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_reciever" + }, + { + "_id": "690a06bc6c65be6d001b4d43", + "_tpl": "5aa66a9be5b5b0214e506e89", + "parentId": "690a06bc6c65be6d001b4d3b", + "slotId": "mod_scope" + }, + { + "_id": "690a06bc6c65be6d001b4d4b", + "_tpl": "5aa66be6e5b5b0214e506e97", + "parentId": "690a06bc6c65be6d001b4d43", + "slotId": "mod_scope" + }, + { + "_id": "690a06bc6c65be6d001b4d5c", + "_tpl": "6087e663132d4d12c81fd96b", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_pistol_grip" + }, + { + "_id": "690a06bc6c65be6d001b4d65", + "_tpl": "67d418d0ffb910d21f04720e", + "parentId": "690a06bc6c65be6d001b4cd1", + "slotId": "mod_magazine" + } + ], "_name": "AK-50 .50 BMG sniper rifle default", "_parent": "679f5f42ca975ceee4001927", "_type": "Preset" From 942f1641e6d589a7963d8104c541e4a04bcd9ed9 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 14:06:00 +0000 Subject: [PATCH 15/35] Reset ak-50 dura values --- Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json index 40d72430..f5b88272 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json @@ -486,8 +486,8 @@ "_tpl": "67d0576f29f580ebc10efd08", "upd": { "Repairable": { - "Durability": 82, - "MaxDurability": 88 + "Durability": 100, + "MaxDurability": 100 } } }, From 6196d1203fc44e1bede794e29a1111930178c4e5 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 18:44:05 +0000 Subject: [PATCH 16/35] Fixed incorrect ak-50 parent id --- Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json index f5b88272..fefaea86 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json @@ -541,7 +541,7 @@ } ], "_name": "AK-50 .50 BMG sniper rifle default", - "_parent": "679f5f42ca975ceee4001927", + "_parent": "690a06bc6c65be6d001b4cd1", "_type": "Preset" } ], From 2eca8df2fa373f5725b9e8f5d4e1390aebc06b02 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 21:43:34 +0000 Subject: [PATCH 17/35] Default `continuous` to false --- Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index 8b53d89d..311e455b 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs @@ -953,7 +953,7 @@ public class HideoutController( // Continuous crafts have special handling in EventOutputHolder.updateOutputProperties() hideoutProduction.SptIsComplete = true; - hideoutProduction.SptIsContinuous = recipe.Continuous; + hideoutProduction.SptIsContinuous = recipe.Continuous ?? false; // Continuous recipes need the craft time refreshed as it gets created once on initial craft and stays the same regardless of what // production.json is set to From c13193d4c4b03323b6e60eb045e96e6d1f8bd82b Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 22:54:38 +0000 Subject: [PATCH 18/35] Updated `ReplaceBotHostility` to insert hostility section if botrole not found instead of skipping --- .../SPTarkov.Server.Core/Services/SeasonalEventService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs index 9d44c771..e99d7699 100644 --- a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs @@ -651,6 +651,9 @@ public class SeasonalEventService( ); if (matchingBaseSettings is null) { + // Doesn't exist, add it + locationBase.Base.BotLocationModifier.AdditionalHostilitySettings.Append(settings); + continue; } From 21102b76803b5935f972a67c4b5b1ad323c802b4 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 4 Nov 2025 22:56:53 +0000 Subject: [PATCH 19/35] Disabled zombies during halloween Enabled summon event during halloween Added peacefulZryachiyEvent hostility settings for summon event --- .../SPT_Data/configs/seasonalevents.json | 22 ++++++++++++++++--- .../Services/SeasonalEventService.cs | 5 +++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 05a3070c..1c9df212 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -9892,7 +9892,7 @@ "endMonth": "11", "name": "halloween", "settings": { - "enableSummoning": false, + "enableSummoning": true, "removeEntryRequirement": [ "laboratory" ], @@ -9902,7 +9902,7 @@ "laboratory" ], "disableWaves": [], - "enabled": true, + "enabled": false, "mapInfectionAmount": { "Sandbox": 25, "factory4": 50, @@ -13979,6 +13979,22 @@ "UsecPlayerBehaviour": "AlwaysEnemies" } ] - } + }, + "summon": { + "default": [ + { + "AlwaysEnemies": [], + "AlwaysFriends": [], + "BearPlayerBehaviour": "AlwaysFriends", + "BotRole": "peacefullZryachiyEvent", + "ChancedEnemies": [], + "SavageEnemyChance": 0, + "BearEnemyChance": 0, + "UsecEnemyChance": 0, + "SavagePlayerBehaviour": "AlwaysFriends", + "UsecPlayerBehaviour": "AlwaysFriends" + } + ] + } } } diff --git a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs index e99d7699..3cabdb77 100644 --- a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs @@ -772,6 +772,11 @@ public class SeasonalEventService( protected void EnableHalloweenSummonEvent() { databaseService.GetGlobals().Configuration.EventSettings.EventActive = true; + + if (SeasonalEventConfig.HostilitySettingsForEvent.TryGetValue("summon", out var botData)) + { + ReplaceBotHostility(botData); + } } protected void ConfigureZombies(ZombieSettings zombieSettings) From 0e675129a0132bbc852acb72648ac1b4be3c3d04 Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Tue, 4 Nov 2025 22:57:41 +0000 Subject: [PATCH 20/35] Format Style Fixes --- .../SPT_Data/configs/seasonalevents.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 1c9df212..819f7e33 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -13980,21 +13980,21 @@ } ] }, - "summon": { - "default": [ - { + "summon": { + "default": [ + { "AlwaysEnemies": [], - "AlwaysFriends": [], + "AlwaysFriends": [], "BearPlayerBehaviour": "AlwaysFriends", "BotRole": "peacefullZryachiyEvent", "ChancedEnemies": [], "SavageEnemyChance": 0, - "BearEnemyChance": 0, - "UsecEnemyChance": 0, + "BearEnemyChance": 0, + "UsecEnemyChance": 0, "SavagePlayerBehaviour": "AlwaysFriends", "UsecPlayerBehaviour": "AlwaysFriends" } - ] - } + ] + } } } From 4eecb485c631592625dbb23c30ee54f835ff34a6 Mon Sep 17 00:00:00 2001 From: Archangel Date: Wed, 5 Nov 2025 14:41:28 +0100 Subject: [PATCH 21/35] Mark various removed implementations as obsolete --- Libraries/SPTarkov.Server.Core/DI/ServiceLocator.cs | 2 ++ Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/DI/ServiceLocator.cs b/Libraries/SPTarkov.Server.Core/DI/ServiceLocator.cs index a2265fa9..1bc77c17 100644 --- a/Libraries/SPTarkov.Server.Core/DI/ServiceLocator.cs +++ b/Libraries/SPTarkov.Server.Core/DI/ServiceLocator.cs @@ -6,8 +6,10 @@ /// /// This should not be used at all when having direct access to DI. /// +[Obsolete("This will be removed in the next version of SPT in favor of DI injecting patches")] public static class ServiceLocator { + [Obsolete("This will be removed in the next version of SPT in favor of DI injecting patches")] public static IServiceProvider ServiceProvider { get; private set; } internal static void SetServiceProvider(IServiceProvider provider) diff --git a/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs b/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs index 4f504f2a..e18b18c9 100644 --- a/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs +++ b/Libraries/SPTarkov.Server.Core/Servers/SaveServer.cs @@ -31,6 +31,7 @@ public class SaveServer( protected const string profileFilepath = "user/profiles/"; // onLoad = require("../bindings/SaveLoad"); + [Obsolete("This will be removed in the next version of SPT")] protected readonly Dictionary> onBeforeSaveCallbacks = new(); protected readonly ConcurrentDictionary profiles = new(); @@ -42,6 +43,7 @@ public class SaveServer( /// /// ID for the save callback /// Callback to execute prior to running SaveServer.saveProfile() + [Obsolete("This will be removed in the next version of SPT")] public void AddBeforeSaveCallback(string id, Func callback) { onBeforeSaveCallbacks[id] = callback; @@ -51,6 +53,7 @@ public class SaveServer( /// Remove a callback from being executed prior to saving profile in SaveServer.saveProfile() /// /// ID of Callback to remove + [Obsolete("This will be removed in the next version of SPT")] public void RemoveBeforeSaveCallback(string id) { onBeforeSaveCallbacks.Remove(id); From 3a5f285fc5c7008ab7577a37b7653f3ada7eee5d Mon Sep 17 00:00:00 2001 From: Archangel Date: Wed, 5 Nov 2025 14:56:21 +0100 Subject: [PATCH 22/35] Add obsolete markers --- Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs b/Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs index c3dbda21..e99a4a10 100644 --- a/Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs +++ b/Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs @@ -3,6 +3,7 @@ namespace SPTarkov.Reflection.Patching; /// /// Cache of active patches for mod developers to use for compatibility reasons /// +[Obsolete("Patches will be injectable through IEnumerable in SPT 4.1, making this redundant")] public static class ModPatchCache { private static readonly List _activePatches = []; @@ -22,6 +23,7 @@ public static class ModPatchCache /// /// This should never be called before PreSptLoad is completed, otherwise could be empty. /// + [Obsolete("Patches will be injectable through IEnumerable in SPT 4.1, making this redundant")] public static IReadOnlyList GetActivePatches() { // We're not exposing _activePatches so it cant be altered outside of this class. Do NOT implement this as a property. @@ -38,6 +40,7 @@ public static class ModPatchCache /// /// This should never be called before PreSptLoad is completed, otherwise could be empty. /// + [Obsolete("Patches will be injectable through IEnumerable in SPT 4.1, making this redundant")] public static List GetActivePatchedMethodNames() { var result = new List(); From 8a6e4a18054e431d0799d5fce289c67aa824c7c4 Mon Sep 17 00:00:00 2001 From: Chomp Date: Wed, 5 Nov 2025 15:41:22 +0000 Subject: [PATCH 23/35] Fixed floor unlock pointing to wrong quest #679 --- .../SPT_Data/database/hideout/customisation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/hideout/customisation.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/hideout/customisation.json index 77625dac..b1452911 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/hideout/customisation.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/hideout/customisation.json @@ -475,7 +475,7 @@ "visibilityConditions": [], "globalQuestCounterId": "", "parentId": "", - "target": "66058cc208308761cf390993", + "target": "68341f6fe2e7ef70a3060a0a", "status": [ 4 ], From 8ad953a224c8dcab0a5199130f369b2b7a3abdef Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 6 Nov 2025 13:31:44 +0000 Subject: [PATCH 24/35] Added system to semi-randomly rotate goon spawns across various maps Removed knight from weekly boss system --- .../SPT_Data/configs/bot.json | 13 +++- .../Models/Spt/Config/BotConfig.cs | 15 +++++ .../Services/LocationLifecycleService.cs | 65 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json index 2218ec0b..51528993 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json @@ -2895,10 +2895,19 @@ "bossKilla", "bossKojaniy", "bossSanitar", - "bossKolontay", - "bossKnight" + "bossKolontay" ], "resetDay": "Monday" }, + "goonSpawnSystem": { + "enabled": true, + "locationPool": [ + "bigmap", + "woods", + "shoreline", + "lighthouse" + ], + "spawnChance": 35 + }, "replaceScavWith": "assault" } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs index 0bfddd17..498b4443 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs @@ -137,6 +137,21 @@ public record BotConfig : BaseConfig /// [JsonPropertyName("replaceScavWith")] public required WildSpawnType ReplaceScavWith { get; set; } + + [JsonPropertyName("goonSpawnSystem")] + public required GoonSpawnSystem GoonSpawnSystem { get; set; } +} + +public record GoonSpawnSystem +{ + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + [JsonPropertyName("locationPool")] + public IEnumerable LocationPool { get; set; } + + [JsonPropertyName("spawnChance")] + public double SpawnChance { get; set; } } public record WeeklyBossSettings diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index ba94bcb1..7c68fdaa 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -56,12 +56,15 @@ public class LocationLifecycleService( protected readonly RagfairConfig RagfairConfig = configServer.GetConfig(); protected readonly HideoutConfig HideoutConfig = configServer.GetConfig(); protected readonly PmcConfig PMCConfig = configServer.GetConfig(); + protected readonly BotConfig BotConfig = configServer.GetConfig(); protected readonly LostOnDeathConfig LostOnDeathConfig = configServer.GetConfig(); protected const string Pmc = "pmc"; protected const string Savage = "savage"; protected const string Scav = "scav"; + public object botConfig { get; private set; } + /// /// Check player type for pmc or scav /// @@ -320,6 +323,11 @@ public class LocationLifecycleService( return locationBaseClone; } + if (BotConfig.GoonSpawnSystem.Enabled) + { + AdjustGoonMapSpawns(); + } + // Add custom PMCs to map every time its run pmcWaveGenerator.ApplyWaveChangesToMap(locationBaseClone); @@ -348,6 +356,63 @@ public class LocationLifecycleService( return locationBaseClone; } + /// + /// Goons will spawn on one map each hour, changing randomly based on a consistent seed made from current utc year + utc hour + /// + /// LocationIds to always ignore when choosing a spawn + protected void AdjustGoonMapSpawns(HashSet? locationBlacklist = null) + { + locationBlacklist ??= ["hideout", "develop"]; + + // Reset all maps with goons to 0% spawn, ignore blacklisted locations + var allLocations = databaseService.GetLocations().GetDictionary(); + foreach (var (locationId, location) in allLocations) + { + if (!locationBlacklist.Contains(locationId) && location?.Base?.BossLocationSpawn is not null) + { + foreach (var goonSpawn in location.Base.BossLocationSpawn.Where(x => x.BossName == "bossKnight")) + { + goonSpawn.BossChance = 0; + } + } + } + + var now = DateTime.UtcNow; + + // Create consistent seed for hour (use prime) + var seed = (now.Year * 1009) + now.Hour; + + // Init Random class with unique seed + var random = new Random(seed); + + // Filter locations pool + var validLocationIds = BotConfig + .GoonSpawnSystem.LocationPool.Where(locationId => + !locationBlacklist.Contains(locationId) + && allLocations.TryGetValue(locationId, out var location) + && location?.Base?.BossLocationSpawn is not null + ) + .ToList(); + + if (validLocationIds.Count == 0) + { + logger.Error("Unable to adjust goon spawn chance, no valid locations found"); + + return; + } + + // Choose a spawn location for goons + var chosenMapId = validLocationIds[random.Next(0, validLocationIds.Count)]; + var chosenMap = allLocations[chosenMapId]; + + // "Where" just incase there's multiple knight spawns for some reason + var goonSpawns = chosenMap.Base.BossLocationSpawn.Where(x => x.BossName == "bossKnight"); + foreach (var goonSpawn in goonSpawns) + { + goonSpawn.BossChance = BotConfig.GoonSpawnSystem.SpawnChance; + } + } + /// /// Handle client/match/local/end /// From f89db9eda0007e603910092727e1da7a52610bb1 Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Thu, 6 Nov 2025 13:32:36 +0000 Subject: [PATCH 25/35] Format Style Fixes --- .../SPT_Data/configs/bot.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json index 51528993..909db44f 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json @@ -2900,14 +2900,14 @@ "resetDay": "Monday" }, "goonSpawnSystem": { - "enabled": true, - "locationPool": [ - "bigmap", - "woods", - "shoreline", - "lighthouse" - ], - "spawnChance": 35 + "enabled": true, + "locationPool": [ + "bigmap", + "woods", + "shoreline", + "lighthouse" + ], + "spawnChance": 35 }, "replaceScavWith": "assault" } From e19183582479f4059301209479606a8d3ee0f738 Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 6 Nov 2025 13:32:59 +0000 Subject: [PATCH 26/35] Improved error message when server fails to start --- SPTarkov.Server/Program.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SPTarkov.Server/Program.cs b/SPTarkov.Server/Program.cs index 3c7c4e3f..e18843c8 100644 --- a/SPTarkov.Server/Program.cs +++ b/SPTarkov.Server/Program.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Sockets; -using System.Runtime; using System.Runtime.InteropServices; using System.Security.Authentication; using System.Text; @@ -44,7 +43,9 @@ public static class Program catch (Exception e) { Console.WriteLine("========================================================================================================="); - Console.WriteLine("The server has unexpectedly stopped, please check your log files and reach out to #spt-support in discord"); + Console.WriteLine( + "The server has unexpectedly stopped, reach out to #spt-support in discord, create a support thread by following the instructions found in #support-guidelines. Also include a screenshot of this message + the below error" + ); Console.WriteLine(e); Console.WriteLine("========================================================================================================="); Console.WriteLine("Press any key to exit..."); From 73b710eb7dc60b7e038ac2b142ed1c81129ec892 Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 6 Nov 2025 13:35:11 +0000 Subject: [PATCH 27/35] Removed unused code --- .../SPTarkov.Server.Core/Services/LocationLifecycleService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 7c68fdaa..3b17ae59 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -63,8 +63,6 @@ public class LocationLifecycleService( protected const string Savage = "savage"; protected const string Scav = "scav"; - public object botConfig { get; private set; } - /// /// Check player type for pmc or scav /// From 76182ba4110f59c31d960c71ff8c0144d4725a7f Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 6 Nov 2025 14:12:37 +0000 Subject: [PATCH 28/35] Updatd `GetWeeklyBoss` to use prime number when generating seed --- Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs b/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs index 528c5d29..5a94157c 100644 --- a/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs @@ -216,7 +216,7 @@ public class PostDbLoadService( // Create a consistent seed for the week using the year and the day of the year of above monday chosen // This results in seed being identical for the week - var seed = startOfWeek.Year * 1000 + startOfWeek.DayOfYear; + var seed = startOfWeek.Year * 1009 + startOfWeek.DayOfYear; // Init Random class with unique seed var random = new Random(seed); From 8580c12b3ff62396d22abaa38d37abdfb8d49400 Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 6 Nov 2025 15:56:44 +0000 Subject: [PATCH 29/35] Updated `record Trader.Assort` to be nullable --- .../SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs index 51166c55..efd7f79a 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs @@ -9,7 +9,7 @@ namespace SPTarkov.Server.Core.Models.Eft.Common.Tables; public record Trader { [JsonPropertyName("assort")] - public required TraderAssort Assort { get; set; } + public TraderAssort? Assort { get; set; } [JsonPropertyName("base")] public required TraderBase Base { get; init; } From bbf8465cf781fa508b2ad36a48fd80080b927541 Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 6 Nov 2025 16:26:34 +0000 Subject: [PATCH 30/35] Revert "Updated `record Trader.Assort` to be nullable" This reverts commit 8580c12b3ff62396d22abaa38d37abdfb8d49400. --- .../SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs index efd7f79a..51166c55 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Trader.cs @@ -9,7 +9,7 @@ namespace SPTarkov.Server.Core.Models.Eft.Common.Tables; public record Trader { [JsonPropertyName("assort")] - public TraderAssort? Assort { get; set; } + public required TraderAssort Assort { get; set; } [JsonPropertyName("base")] public required TraderBase Base { get; init; } From ad70ed15804247a5bdda630f3a5abe8bd6e5a4ae Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 6 Nov 2025 16:27:14 +0000 Subject: [PATCH 31/35] Added empty assort json to ensure LK is consistent with other traders --- .../database/traders/638f541a29ffd1183d187f57/assort.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Libraries/SPTarkov.Server.Assets/SPT_Data/database/traders/638f541a29ffd1183d187f57/assort.json diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/traders/638f541a29ffd1183d187f57/assort.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/traders/638f541a29ffd1183d187f57/assort.json new file mode 100644 index 00000000..a73375d0 --- /dev/null +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/traders/638f541a29ffd1183d187f57/assort.json @@ -0,0 +1,5 @@ +{ + "barter_scheme": {}, + "items": [], + "loyal_level_items": {} +} From 9cedaeec2ec4325277b74d65faf6c195b2c680c0 Mon Sep 17 00:00:00 2001 From: Yui Date: Thu, 6 Nov 2025 14:13:14 -0300 Subject: [PATCH 32/35] Checks missing `LoyalLevelItems` for trader item on flea offer generation --- .../SPT_Data/database/locales/server/en.json | 1 + .../Generators/RagfairOfferGenerator.cs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json index f20bbbad..70307af1 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json @@ -623,6 +623,7 @@ "ragfair-invalid_player_offer_request": "Unable to place offer, request is invalid", "ragfair-item_not_in_db_unable_to_generate_dynamic_stack_count": "Item with tpl: %s not found in db. Unable to generate a dynamic stack count", "ragfair-missing_barter_scheme": "generateFleaOffersForTrader() Failed to find barterScheme for item id: {{itemId}} tpl: {{tpl}} on {{name}}", + "ragfair-missing_loyal_level_item": "generateFleaOffersForTrader() Failed to find LoyalLevelItem for item id: {{itemId}} tpl: {{tpl}} on {{name}}", "ragfair-no_trader_assorts_cant_generate_flea_offers": "Unable to generate flea offers for trader %s, no assort found", "ragfair-offer_no_longer_exists": "Offer no longer exists", "ragfair-offer_not_found_in_profile": "Could not find offer with id: {{offerId}} in profile: {{profileId}} to remove", diff --git a/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs index 681e009f..27d88274 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs @@ -588,7 +588,21 @@ public class RagfairOfferGenerator( } var barterSchemeItems = barterScheme[0]; - var loyalLevel = assortsClone.LoyalLevelItems[item.Id]; + if (!assortsClone.LoyalLevelItems.TryGetValue(item.Id, out var loyalLevel)) + { + logger.Warning( + localisationService.GetText( + "ragfair-missing_loyal_level_item", + new + { + itemId = item.Id, + tpl = item.Template, + name = trader.Base.Nickname, + } + ) + ); + continue; + } var createOfferDetails = new CreateFleaOfferDetails { From b467247bb2cabe41d64630a3dfec2242cbfbbac5 Mon Sep 17 00:00:00 2001 From: CWX Date: Thu, 6 Nov 2025 22:29:18 +0000 Subject: [PATCH 33/35] Add BBQ Torch to Blacklist, was on BossLoot --- Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json index fefaea86..f03a48ed 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json @@ -61,7 +61,8 @@ "679baa2c61f588ae2b062a24", "679baa4f59b8961f370dd683", "679baa5a59b8961f370dd685", - "679baa9091966fe40408f149" + "679baa9091966fe40408f149", + "67ab3d4b83869afd170fdd3f" ], "bossItems": [ "6275303a9f372d6ea97f9ec7", From c96aedf8d43f00671cc2494b5ec8cc5d8a75a129 Mon Sep 17 00:00:00 2001 From: Chomp Date: Fri, 7 Nov 2025 13:24:18 +0000 Subject: [PATCH 34/35] Fixed error in `ValidateQuestAssortUnlocksExist` --- Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs b/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs index 5a94157c..a3392f79 100644 --- a/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs @@ -717,6 +717,11 @@ public class PostDbLoadService( continue; } + if (traderData.QuestAssort is null) + { + continue; + } + // Merge started/success/fail quest assorts into one dictionary var mergedQuestAssorts = new Dictionary(); mergedQuestAssorts = mergedQuestAssorts From 1cfe1592cce1f42573e79c1a0ea6792451b60bd5 Mon Sep 17 00:00:00 2001 From: Chomp Date: Fri, 7 Nov 2025 13:52:35 +0000 Subject: [PATCH 35/35] Improved how hideout crafts are matched during reward unlock process #684 --- .../Helpers/RewardHelper.cs | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs index c30bd872..9a7530bd 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RewardHelper.cs @@ -229,18 +229,7 @@ public class RewardHelper( // "TraderId" holds area ID that will be used to craft unlocked item var desiredHideoutAreaType = (HideoutAreas)int.Parse(craftUnlockReward.TraderId.ToString()); - var matchingProductions = GetMatchingProductions(desiredHideoutAreaType, questId, craftUnlockReward); - - // More/less than single match, above filtering wasn't strict enough - if (matchingProductions.Count != 1) - // Multiple matches were found, last ditch attempt to match by questid (value we add manually to production.json via `gen:productionquests` command) - { - matchingProductions = matchingProductions - .Where(prod => prod.Requirements.Any(requirement => requirement.QuestId == questId)) - .ToList(); - } - - return matchingProductions; + return GetMatchingProductions(desiredHideoutAreaType, questId, craftUnlockReward); } /// @@ -252,20 +241,35 @@ public class RewardHelper( /// Hideout crafts that match input parameters protected List GetMatchingProductions(HideoutAreas desiredHideoutAreaType, MongoId questId, Reward craftUnlockReward) { - var rewardItemTpl = craftUnlockReward.Items.FirstOrDefault()?.Template; + var rewardItemTpl = craftUnlockReward.Items?.FirstOrDefault()?.Template; + if (rewardItemTpl is null) + { + logger.Warning("Unable to get matching hideout craft as reward item tpl is missing"); - var craftingRecipes = databaseService.GetHideout().Production.Recipes; - return craftingRecipes + return []; + } + + var craftingRecipesDb = databaseService.GetHideout().Production.Recipes; + var result = craftingRecipesDb .Where(production => - // Some crafts have the questId, easy match + // Attempt to match by questId (value we add manually to production.json via `gen:productionquests` command) production.Requirements.Any(req => req.QuestId == questId) - || - // Couldn't get craft by questId, get the closest match based on information we know + ) + .ToList(); + if (result.Count == 1) + { + // One result, good match + return result; + } + + // Found more than or less than 1 craft by questId, try to get closest match based on information we know + return craftingRecipesDb + .Where(production => ( - rewardItemTpl is not null - && production.AreaType == desiredHideoutAreaType + production.AreaType == desiredHideoutAreaType && production.EndProduct == rewardItemTpl.Value && production.Requirements.Any(req => req.Type is "QuestComplete") + && production.Locked.GetValueOrDefault(false) // Craft would be locked if we're unlocking it && production.Requirements.Any(req => req.RequiredLevel == craftUnlockReward.LoyaltyLevel) ) )