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 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(); diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json index 2218ec0b..909db44f 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.Assets/SPT_Data/configs/item.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/item.json index f6de617c..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", @@ -475,6 +476,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": 100, + "MaxDurability": 100 + } + } + }, + { + "_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": "690a06bc6c65be6d001b4cd1", + "_type": "Preset" } ], "handbookPriceOverride": { 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": { diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 19955484..819f7e33 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", @@ -9807,7 +9892,7 @@ "endMonth": "11", "name": "halloween", "settings": { - "enableSummoning": false, + "enableSummoning": true, "removeEntryRequirement": [ "laboratory" ], @@ -9817,7 +9902,7 @@ "laboratory" ], "disableWaves": [], - "enabled": true, + "enabled": false, "mapInfectionAmount": { "Sandbox": 25, "factory4": 50, @@ -13894,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.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 ], 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.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": {} +} 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 diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs index 7e36dcd1..f7bb313d 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs @@ -772,23 +772,33 @@ 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); - // Check for and apply item price modifer if it exists in config - if (RagfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(offerRootItem.Template, out var itemPriceModifer)) + // 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 *= itemPriceModifer; + averageOfferPriceSingleItem = ragfairPriceService.GetPresetPriceByChildren(offer.Items); + } + else + { + // 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 - 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); // Subtract flea market fee from stash 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/Generators/RagfairOfferGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RagfairOfferGenerator.cs index 2d2d95ce..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 { @@ -742,7 +756,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 }; } } 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; 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) 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) ) ) 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/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); diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index ba94bcb1..3b17ae59 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -56,6 +56,7 @@ 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"; @@ -320,6 +321,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 +354,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 /// diff --git a/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs b/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs index 528c5d29..a3392f79 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); @@ -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 diff --git a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs index 4c5668ea..494fd6c3 100644 --- a/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/RagfairPriceService.cs @@ -531,15 +531,17 @@ 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) { // 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); diff --git a/Libraries/SPTarkov.Server.Core/Services/RepairService.cs b/Libraries/SPTarkov.Server.Core/Services/RepairService.cs index 86210fa2..6b175ffc 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 @@ -49,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; @@ -80,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); @@ -166,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; } @@ -175,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; @@ -205,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); @@ -272,43 +281,61 @@ 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 + List kitIdsToDelete = []; foreach (var repairKit in repairKits) { 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 - 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); + + break; + } + + foreach (var kitId in kitIdsToDelete) + { + inventoryHelper.RemoveItem(pmcData, kitId, sessionId, output); } return new RepairDetails @@ -316,7 +343,7 @@ public class RepairService( RepairPoints = repairKits[0].Count, RepairedItem = itemToRepair, RepairedItemIsArmor = repairItemIsArmor, - RepairAmount = repairAmount, + RepairAmount = repairAmountTotal, RepairedByKit = true, }; } @@ -348,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; } @@ -414,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 } }; @@ -550,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; diff --git a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs index 9d44c771..3cabdb77 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; } @@ -769,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) 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...");