From 40f8ed1eb06761cc8d98e73c72c3b054ae3368ef Mon Sep 17 00:00:00 2001 From: Chomp Date: Sun, 14 Dec 2025 14:05:12 +0000 Subject: [PATCH 01/33] Improved handling of null stage data inside `AddMissingHideoutBonusesToProfile` + comment improvements --- .../Services/ProfileFixerService.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs index 2fc9bcbb..7404d328 100644 --- a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs @@ -539,33 +539,39 @@ public class ProfileFixerService( foreach (var profileArea in pmcProfile.Hideout?.Areas ?? []) { var areaType = profileArea.Type; - var level = profileArea.Level; + var currentLevel = profileArea.Level; - if (level.GetValueOrDefault(0) == 0) + if (currentLevel.GetValueOrDefault(0) == 0) { continue; } - // Get array of hideout area upgrade levels to check for bonuses + // Create array of hideout area upgrade levels player has installed // Zero indexed var areaLevelsToCheck = new List(); - for (var index = 0; index < level + 1; index++) - // Stage key is saved as string in db + for (var index = 0; index < currentLevel + 1; index++) { - areaLevelsToCheck.Add(index.ToString()); + areaLevelsToCheck.Add(index.ToString()); // Convert to string as hideout stage key is saved as string in db } - // Iterate over area levels, check for bonuses, add if needed + // Get hideout area data from db var dbArea = dbHideoutAreas?.FirstOrDefault(area => area.Type == areaType); - if (dbArea is null) + if (dbArea is null || dbArea.Stages is null) { continue; } + // Check if profile is missing any bonuses from each area level foreach (var areaLevel in areaLevelsToCheck) { - // Get areas level bonuses from db - var levelBonuses = dbArea.Stages?[areaLevel].Bonuses; + // Get areas level from db + if (!dbArea.Stages.TryGetValue(areaLevel, out var stage)) + { + continue; + } + + // Get the bonuses for this upgrade stage + var levelBonuses = stage.Bonuses; if (levelBonuses is null || levelBonuses.Count == 0) { continue; @@ -578,8 +584,8 @@ public class ProfileFixerService( var profileBonus = GetBonusFromProfile(pmcProfile.Bonuses, bonus); if (profileBonus is null) { - // no bonus, add to profile - logger.Debug($"Profile has level {level} area {profileArea.Type} but no bonus found, adding {bonus.Type}"); + // No bonus in profile, add it + logger.Debug($"Profile has level: {currentLevel} area: {profileArea.Type} but no bonus found, adding: {bonus.Type}"); hideoutHelper.ApplyPlayerUpgradesBonus(pmcProfile, bonus); } } From 250c2a539837c294d2749c5e48556f1a2c5c222d Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Sun, 14 Dec 2025 14:06:08 +0000 Subject: [PATCH 02/33] Format Style Fixes --- .../SPTarkov.Server.Core/Controllers/DialogueController.cs | 3 ++- .../Generators/BotEquipmentModGenerator.cs | 3 ++- .../SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs | 6 ++---- .../SPTarkov.Server.Core/Services/ProfileFixerService.cs | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs b/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs index bbc02015..9edc2b95 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs @@ -396,7 +396,8 @@ public class DialogueController( var checkTime = message.DateTime + (message.MaxStorageTime ?? 0); return timeNow < checkTime; }) - .ToList() ?? []; + .ToList() + ?? []; } /// diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs index 0afef244..db77bbd6 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs @@ -1822,7 +1822,8 @@ public class BotEquipmentModGenerator( .Filter.All(tpl => itemHelper.IsOfBaseclasses(tpl, whitelistedSightTypes) || itemHelper.IsOfBaseclass(tpl, BaseClasses.MOUNT) ) - ) ?? false + ) + ?? false ) // Add mod to allowed list { diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs index 47fff35e..ec97d83f 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/BotWeaponGenerator.cs @@ -716,10 +716,8 @@ public class BotWeaponGenerator( // Try to get cartridges from slots array first, if none found, try Cartridges array var cartridges = - magazineTemplate.Value.Properties.Slots.FirstOrDefault()?.Properties?.Filters?.FirstOrDefault()?.Filter ?? magazineTemplate - .Value.Properties.Cartridges.FirstOrDefault() - ?.Properties?.Filters?.FirstOrDefault() - ?.Filter; + magazineTemplate.Value.Properties.Slots.FirstOrDefault()?.Properties?.Filters?.FirstOrDefault()?.Filter + ?? magazineTemplate.Value.Properties.Cartridges.FirstOrDefault()?.Properties?.Filters?.FirstOrDefault()?.Filter; return cartridges ?? []; } diff --git a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs index 7404d328..7318044d 100644 --- a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs @@ -585,7 +585,9 @@ public class ProfileFixerService( if (profileBonus is null) { // No bonus in profile, add it - logger.Debug($"Profile has level: {currentLevel} area: {profileArea.Type} but no bonus found, adding: {bonus.Type}"); + logger.Debug( + $"Profile has level: {currentLevel} area: {profileArea.Type} but no bonus found, adding: {bonus.Type}" + ); hideoutHelper.ApplyPlayerUpgradesBonus(pmcProfile, bonus); } } From 71ad04dd863f66de3f462e138943a5aa9134c366 Mon Sep 17 00:00:00 2001 From: Archangel Date: Tue, 16 Dec 2025 20:47:39 +0100 Subject: [PATCH 03/33] Add missing prop to globals --- .../SPTarkov.Server.Assets/SPT_Data/database/globals.json | 1 + Libraries/SPTarkov.Server.Core/Models/Eft/Common/Globals.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/globals.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/globals.json index a44a6abb..3bd17bcd 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/globals.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/globals.json @@ -35191,6 +35191,7 @@ ], "active": false, "activePVE": false, + "initialFrozenDelaySec": 60, "applyFrozenEverySec": 1, "consumables": [ "67586bee39b1b82b0d0f9d06" diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Globals.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Globals.cs index 3c00b320..3072e33c 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Globals.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Globals.cs @@ -648,6 +648,9 @@ public record RunddansSettings [JsonPropertyName("applyFrozenEverySec")] public double ApplyFrozenEverySec { get; set; } + [JsonPropertyName("initialFrozenDelaySec")] + public double InitialFrozenDelaySec { get; set; } + [JsonPropertyName("consumables")] public IEnumerable Consumables { get; set; } From ad67f4b8d6b420e05a8f842b43850c192db01ab6 Mon Sep 17 00:00:00 2001 From: Archangel Date: Tue, 16 Dec 2025 20:48:12 +0100 Subject: [PATCH 04/33] Set TransitionType as client request type --- .../Services/LocationLifecycleService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 3b17ae59..5230ef34 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -104,10 +104,10 @@ public class LocationLifecycleService( ServerSettings = databaseService.GetLocationServices(), // TODO - is this per map or global? Profile = new ProfileInsuredItems { InsuredItems = playerProfile.CharacterData.PmcData.InsuredItems }, LocationLoot = GenerateLocationAndLoot(sessionId, request.Location, !request.ShouldSkipLootGeneration ?? true), - TransitionType = TransitionType.NONE, + TransitionType = request.TransitionType, Transition = new Transition { - TransitionType = TransitionType.NONE, + TransitionType = request.TransitionType, TransitionRaidId = new MongoId(), TransitionCount = 0, VisitedLocations = [], @@ -128,7 +128,7 @@ public class LocationLifecycleService( if (transitionData is not null) { logger.Success($"Player: {sessionId} is in transit to {request.Location}"); - result.Transition.TransitionType = TransitionType.COMMON; + result.Transition.TransitionType = request.TransitionType; result.Transition.TransitionRaidId = transitionData.TransitionRaidId; result.Transition.TransitionCount += 1; From 70c6786635c04f1f2aabaefef7158f5edcfb857b Mon Sep 17 00:00:00 2001 From: Archangel Date: Tue, 16 Dec 2025 20:50:40 +0100 Subject: [PATCH 05/33] Bump to 4.0.9 --- Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.props b/Build.props index fe2a13ef..fc2d5693 100644 --- a/Build.props +++ b/Build.props @@ -1,7 +1,7 @@ - 4.0.8 + 4.0.9 a12b34 0000000000 LOCAL From 761ce2530b65f01141d3ab1c20bf06c992ded007 Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 18 Dec 2025 13:54:34 +0000 Subject: [PATCH 06/33] Fixed edge case with sending rewards to player stash would result in no space error #615 Add reward to profile prior to tools used to ensure consistency with free space check --- .../Controllers/HideoutController.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index 311e455b..cd318a72 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs @@ -886,6 +886,21 @@ public class HideoutController( serverLocalisationService.GetText("inventory-no_stash_space"), BackendErrorCodes.NotEnoughSpace ); + + return; + } + + // Add the crafting result to the stash, marked as FiR + var addItemsRequest = new AddItemsDirectRequest + { + ItemsWithModsToAdd = itemAndChildrenToSendToPlayer, + FoundInRaid = true, + UseSortingTable = false, + Callback = null, + }; + inventoryHelper.AddItemsToStash(sessionID, addItemsRequest, pmcData, output); + if (output.Warnings?.Count > 0) + { return; } @@ -908,24 +923,10 @@ public class HideoutController( } } - // Add the crafting result to the stash, marked as FiR - var addItemsRequest = new AddItemsDirectRequest - { - ItemsWithModsToAdd = itemAndChildrenToSendToPlayer, - FoundInRaid = true, - UseSortingTable = false, - Callback = null, - }; - inventoryHelper.AddItemsToStash(sessionID, addItemsRequest, pmcData, output); - if (output.Warnings?.Count > 0) - { - return; - } - - // - increment skill point for crafting - // - delete the production in profile Hideout.Production + // - Increment skill point for crafting + // - Delete the production in profile Hideout.Production // Hideout Management skill - // ? use a configuration variable for the value? + // ? Use a configuration variable for the value? var globals = databaseService.GetGlobals(); profileHelper.AddSkillPointsToPlayer( pmcData, From 18f8c3b8f3ba33168cc438b9764c0b14bc72c90d Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 18 Dec 2025 13:55:34 +0000 Subject: [PATCH 07/33] Code readability improvements for `HandleRecipe()` --- .../Controllers/HideoutController.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index cd318a72..93b6c5e6 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs @@ -781,23 +781,23 @@ public class HideoutController( ItemEventRouterResponse output ) { - // Validate that we have a matching production - var productionDict = pmcData.Hideout.Production; + // Find craft/production in player profile MongoId? prodId = null; - foreach (var (productionId, production) in productionDict) + foreach (var (productionId, productionInProfile) in pmcData.Hideout.Production) { - // Skip undefined production objects - if (production is null) + // Skip undefined production objects caused by continious crafts + if (productionInProfile is null) { continue; } - if (production.RecipeId != request.RecipeId) + // Not craft we're looking for + if (productionInProfile.RecipeId != request.RecipeId) { continue; } - // Production or ScavCase + // Could be Production or ScavCase prodId = productionId; // Set to objects key break; } @@ -817,7 +817,6 @@ public class HideoutController( // Variables for management of skill var craftingExpAmount = 0; - var counterHoursCrafting = GetCustomSptHoursCraftingTaskConditionCounter(pmcData, recipe); var totalCraftingHours = counterHoursCrafting.Value; From 4daaf1fc47987fa1fa73f38f675ca10f07bd73fa Mon Sep 17 00:00:00 2001 From: Archangel Date: Thu, 18 Dec 2025 15:39:57 +0100 Subject: [PATCH 08/33] Make ItemBaseClassService more robust, stop hydrating the entire DB each time --- .../Services/ItemBaseClassService.cs | 59 +++++++++---------- .../Services/Mod/CustomItemService.cs | 4 +- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs b/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs index fedb694b..ccd76572 100644 --- a/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs @@ -16,12 +16,11 @@ public class ItemBaseClassService( ServerLocalisationService serverLocalisationService ) { - private bool _cacheGenerated; - /// /// Key = Item tpl, values = Ids of its parents /// private Dictionary> _itemBaseClassesCache = []; + private readonly Lock _itemBaseClassesLock = new(); private readonly HashSet _rootNodeIds = []; /// @@ -36,23 +35,34 @@ public class ItemBaseClassService( var items = databaseService.GetItems(); foreach (var item in items) { - if (string.Equals(item.Value.Type, "Item", StringComparison.OrdinalIgnoreCase)) - { - var itemIdToUpdate = item.Value.Id; - if (!_itemBaseClassesCache.ContainsKey(item.Value.Id)) - { - _itemBaseClassesCache[item.Value.Id] = []; - } + AddItemToCache(item.Key); + } + } - AddBaseItems(itemIdToUpdate, item.Value); + public void AddItemToCache(MongoId itemTpl) + { + logger.Debug($"Adding {itemTpl} to cache"); + + var itemDb = databaseService.GetItems(); + + if (!itemDb.TryGetValue(itemTpl, out var item)) + { + logger.Error($"Could not add {itemTpl} to cache, it does not exist in the item database!"); + return; + } + + lock (_itemBaseClassesLock) + { + if (string.Equals(item.Type, "Item", StringComparison.OrdinalIgnoreCase)) + { + _itemBaseClassesCache.TryAdd(item.Id, []); + AddBaseItems(item.Id, item); } else { - _rootNodeIds.Add(item.Key); + _rootNodeIds.Add(item.Id); } } - - _cacheGenerated = true; } /// @@ -79,11 +89,6 @@ public class ItemBaseClassService( /// true if item inherits from base class passed in public bool ItemHasBaseClass(MongoId itemTpl, IEnumerable baseClasses) { - if (!_cacheGenerated) - { - HydrateItemBaseClassCache(); - } - if (itemTpl.IsEmpty) { logger.Warning("Unable to check itemTpl base class as value passed is null"); @@ -107,8 +112,8 @@ public class ItemBaseClassService( logger.Debug(serverLocalisationService.GetText("baseclass-item_not_found", itemTpl.ToString())); } - // Not found in cache, Hydrate again - some mods add items late in server startup lifecycle - HydrateItemBaseClassCache(); + // Not found in cache, attempt to add first + AddItemToCache(itemTpl); existsInCache = _itemBaseClassesCache.TryGetValue(itemTpl, out baseClassList); } @@ -131,11 +136,6 @@ public class ItemBaseClassService( /// true if item inherits from base class passed in public bool ItemHasBaseClass(MongoId itemTpl, MongoId baseClasses) { - if (!_cacheGenerated) - { - HydrateItemBaseClassCache(); - } - if (itemTpl.IsEmpty) { logger.Warning("Unable to check itemTpl base class as value passed is null"); @@ -159,8 +159,8 @@ public class ItemBaseClassService( logger.Debug(serverLocalisationService.GetText("baseclass-item_not_found", itemTpl.ToString())); } - // Not found in cache, Hydrate again - some mods add items late in server startup lifecycle - HydrateItemBaseClassCache(); + // Not found in cache, attempt to add first + AddItemToCache(itemTpl); existsInCache = _itemBaseClassesCache.TryGetValue(itemTpl, out baseClassList); } @@ -182,11 +182,6 @@ public class ItemBaseClassService( /// array of base classes public HashSet GetItemBaseClasses(MongoId itemTpl) { - if (!_cacheGenerated) - { - HydrateItemBaseClassCache(); - } - if (!_itemBaseClassesCache.TryGetValue(itemTpl, out var value)) { return []; diff --git a/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs b/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs index 7f982923..57078670 100644 --- a/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs @@ -66,7 +66,7 @@ public class CustomItemService( AddToFleaPriceDb(newItemId, newItemDetails.FleaPriceRoubles); - itemBaseClassService.HydrateItemBaseClassCache(); + itemBaseClassService.AddItemToCache(newItemId); if (itemHelper.IsOfBaseclass(itemClone.Id, BaseClasses.WEAPON)) { @@ -112,7 +112,7 @@ public class CustomItemService( AddToFleaPriceDb(newItem.Id, newItemDetails.FleaPriceRoubles); - itemBaseClassService.HydrateItemBaseClassCache(); + itemBaseClassService.AddItemToCache(newItem.Id); if (itemHelper.IsOfBaseclass(newItem.Id, BaseClasses.WEAPON)) { From 8789000d97e6abf7b3e9f597f03c880be6dec341 Mon Sep 17 00:00:00 2001 From: Archangel Date: Thu, 18 Dec 2025 16:01:08 +0100 Subject: [PATCH 09/33] Remove overly verbose logging --- .../Services/ItemBaseClassService.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs b/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs index ccd76572..dc217d2e 100644 --- a/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs @@ -47,7 +47,7 @@ public class ItemBaseClassService( if (!itemDb.TryGetValue(itemTpl, out var item)) { - logger.Error($"Could not add {itemTpl} to cache, it does not exist in the item database!"); + logger.Debug($"Could not add {itemTpl} to cache, it does not exist in the item database!"); return; } @@ -106,12 +106,6 @@ public class ItemBaseClassService( var existsInCache = _itemBaseClassesCache.TryGetValue(itemTpl, out var baseClassList); if (!existsInCache) { - // Not found - if (logger.IsLogEnabled(LogLevel.Debug)) - { - logger.Debug(serverLocalisationService.GetText("baseclass-item_not_found", itemTpl.ToString())); - } - // Not found in cache, attempt to add first AddItemToCache(itemTpl); @@ -153,12 +147,6 @@ public class ItemBaseClassService( var existsInCache = _itemBaseClassesCache.TryGetValue(itemTpl, out var baseClassList); if (!existsInCache) { - // Not found - if (logger.IsLogEnabled(LogLevel.Debug)) - { - logger.Debug(serverLocalisationService.GetText("baseclass-item_not_found", itemTpl.ToString())); - } - // Not found in cache, attempt to add first AddItemToCache(itemTpl); From d813566f1e7b3cf7591f0ca33db6b3254e578be4 Mon Sep 17 00:00:00 2001 From: Archangel Date: Thu, 18 Dec 2025 16:22:00 +0100 Subject: [PATCH 10/33] Validate if item is in db before passing to IsOfBaseclass --- .../Generators/BotEquipmentModGenerator.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs index db77bbd6..2d8f04bd 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs @@ -1820,10 +1820,13 @@ public class BotEquipmentModGenerator( scopeSlot?.All(slot => slot.Properties.Filters.FirstOrDefault() .Filter.All(tpl => - itemHelper.IsOfBaseclasses(tpl, whitelistedSightTypes) || itemHelper.IsOfBaseclass(tpl, BaseClasses.MOUNT) + itemHelper.IsItemInDb(tpl) + && ( + itemHelper.IsOfBaseclasses(tpl, whitelistedSightTypes) + || itemHelper.IsOfBaseclass(tpl, BaseClasses.MOUNT) + ) ) - ) - ?? false + ) ?? false ) // Add mod to allowed list { From 4c660b02d547adf0d42e2e1b5991f5c205e25bc0 Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Thu, 18 Dec 2025 15:24:46 +0000 Subject: [PATCH 11/33] Format Style Fixes --- .../Generators/BotEquipmentModGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs index 2d8f04bd..77c3f916 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs @@ -1826,7 +1826,8 @@ public class BotEquipmentModGenerator( || itemHelper.IsOfBaseclass(tpl, BaseClasses.MOUNT) ) ) - ) ?? false + ) + ?? false ) // Add mod to allowed list { From 1e323f8c243688308f1f0676159d5a675fbf6b18 Mon Sep 17 00:00:00 2001 From: Archangel Date: Fri, 19 Dec 2025 13:36:06 +0100 Subject: [PATCH 12/33] Update log --- .../SPTarkov.Server.Core/Services/ItemBaseClassService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs b/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs index dc217d2e..b5085b53 100644 --- a/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs @@ -41,13 +41,11 @@ public class ItemBaseClassService( public void AddItemToCache(MongoId itemTpl) { - logger.Debug($"Adding {itemTpl} to cache"); - var itemDb = databaseService.GetItems(); if (!itemDb.TryGetValue(itemTpl, out var item)) { - logger.Debug($"Could not add {itemTpl} to cache, it does not exist in the item database!"); + logger.Error($"Could not add {itemTpl} to cache, it does not exist in the item database!"); return; } From 828df84ef35c17d26ea75ac7a9f0a4837f527849 Mon Sep 17 00:00:00 2001 From: Archangel Date: Fri, 19 Dec 2025 14:26:15 +0100 Subject: [PATCH 13/33] Add proper flag handling on TransitionType --- .../Models/Enums/TransitionType.cs | 1 + .../Services/LocationLifecycleService.cs | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Models/Enums/TransitionType.cs b/Libraries/SPTarkov.Server.Core/Models/Enums/TransitionType.cs index 4ada3029..5d3af8a2 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Enums/TransitionType.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Enums/TransitionType.cs @@ -1,5 +1,6 @@ namespace SPTarkov.Server.Core.Models.Enums; +[Flags] public enum TransitionType { NONE = 0, diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 5230ef34..1d82ab47 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -93,6 +93,21 @@ public class LocationLifecycleService( : playerProfile.CharacterData.ScavData.Skills.Common ); + var transitionType = TransitionType.NONE; + + if (request.TransitionType is TransitionType flags) + { + if (flags.HasFlag(TransitionType.COMMON)) + { + transitionType = TransitionType.COMMON; + } + + if (flags.HasFlag(TransitionType.EVENT)) + { + transitionType = TransitionType.EVENT; + } + } + // Raid is starting, adjust run times to reduce server load while player is in raid RagfairConfig.RunIntervalSeconds = RagfairConfig.RunIntervalValues.InRaid; HideoutConfig.RunIntervalSeconds = HideoutConfig.RunIntervalValues.InRaid; @@ -104,7 +119,7 @@ public class LocationLifecycleService( ServerSettings = databaseService.GetLocationServices(), // TODO - is this per map or global? Profile = new ProfileInsuredItems { InsuredItems = playerProfile.CharacterData.PmcData.InsuredItems }, LocationLoot = GenerateLocationAndLoot(sessionId, request.Location, !request.ShouldSkipLootGeneration ?? true), - TransitionType = request.TransitionType, + TransitionType = transitionType, Transition = new Transition { TransitionType = request.TransitionType, @@ -128,7 +143,7 @@ public class LocationLifecycleService( if (transitionData is not null) { logger.Success($"Player: {sessionId} is in transit to {request.Location}"); - result.Transition.TransitionType = request.TransitionType; + result.Transition.TransitionType = transitionType; result.Transition.TransitionRaidId = transitionData.TransitionRaidId; result.Transition.TransitionCount += 1; From 04c86f6d9cfcbb60e6a3f02ad8c56f25bd0cae4e Mon Sep 17 00:00:00 2001 From: Archangel Date: Fri, 19 Dec 2025 15:12:56 +0100 Subject: [PATCH 14/33] Enable events on transit, set khorovod time to 300 seconds --- .../Services/LocationLifecycleService.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 1d82ab47..796bb3bb 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -112,13 +112,25 @@ public class LocationLifecycleService( RagfairConfig.RunIntervalSeconds = RagfairConfig.RunIntervalValues.InRaid; HideoutConfig.RunIntervalSeconds = HideoutConfig.RunIntervalValues.InRaid; + var location = GenerateLocationAndLoot(sessionId, request.Location, !request.ShouldSkipLootGeneration ?? true); + + foreach (var transits in location.Transits) + { + // Handle Runddans / Khorovod event + if (transitionType == TransitionType.EVENT && databaseService.GetGlobals().Configuration.RunddansSettings.Active) + { + transits.ActivateAfterSeconds = 300; + transits.Events = true; + } + } + var result = new StartLocalRaidResponseData { // PVE_OFFLINE_xxxxxxxx_27_06_2025_20_20_44 ServerId = $"{request.Location}.{request.PlayerSide} {timeUtil.GetTimeStamp()}", // Only used for metrics in client ServerSettings = databaseService.GetLocationServices(), // TODO - is this per map or global? Profile = new ProfileInsuredItems { InsuredItems = playerProfile.CharacterData.PmcData.InsuredItems }, - LocationLoot = GenerateLocationAndLoot(sessionId, request.Location, !request.ShouldSkipLootGeneration ?? true), + LocationLoot = location, TransitionType = transitionType, Transition = new Transition { From e46474fdd2679877fe3d69f6f9bd8c20d380effb Mon Sep 17 00:00:00 2001 From: Archangel Date: Sat, 20 Dec 2025 16:28:40 +0100 Subject: [PATCH 15/33] Update if check --- .../Services/LocationLifecycleService.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 796bb3bb..78d50340 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -113,11 +113,12 @@ public class LocationLifecycleService( HideoutConfig.RunIntervalSeconds = HideoutConfig.RunIntervalValues.InRaid; var location = GenerateLocationAndLoot(sessionId, request.Location, !request.ShouldSkipLootGeneration ?? true); + var isRundansActive = databaseService.GetGlobals().Configuration.RunddansSettings.Active; - foreach (var transits in location.Transits) + // Handle Runddans / Khorovod event + if (transitionType == TransitionType.EVENT && isRundansActive) { - // Handle Runddans / Khorovod event - if (transitionType == TransitionType.EVENT && databaseService.GetGlobals().Configuration.RunddansSettings.Active) + foreach (var transits in location.Transits ?? []) { transits.ActivateAfterSeconds = 300; transits.Events = true; @@ -134,7 +135,7 @@ public class LocationLifecycleService( TransitionType = transitionType, Transition = new Transition { - TransitionType = request.TransitionType, + TransitionType = transitionType, TransitionRaidId = new MongoId(), TransitionCount = 0, VisitedLocations = [], From ce2fc8a56e539afdcac15450ad6bbe348d1c3e69 Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 20 Dec 2025 15:41:16 +0000 Subject: [PATCH 16/33] Wired up transit whitelist system for use in Khorvod event --- .../SPT_Data/configs/seasonalevents.json | 7 ++++- .../Models/Spt/Config/SeasonalEventConfig.cs | 6 +++++ .../Services/SeasonalEventService.cs | 26 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index cb298be6..7eb84a59 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -9930,7 +9930,8 @@ "adjustBotAppearances": true, "enableChristmasHideout": true, "enableSanta": true, - "enableRundansEvent": true + "enableRundansEvent": true, + "enableKhorvodEvent": true }, "startDay": "13", "startMonth": "12", @@ -9944,6 +9945,7 @@ "settings": { "adjustBotAppearances": true, "enableChristmasHideout": true, + "enableKhorvodEvent": true, "enableSanta": true }, "startDay": "1", @@ -13997,5 +13999,8 @@ } ] } + }, + "khorvodEventTransitWhitelist": { + "bigmap": [9] } } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs index b60278f5..cdfefef7 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs @@ -47,6 +47,9 @@ public record SeasonalEventConfig : BaseConfig [JsonPropertyName("hostilitySettingsForEvent")] public required Dictionary>> HostilitySettingsForEvent { get; set; } + [JsonPropertyName("khorvodEventTransitWhitelist")] + public required Dictionary> KhorvodEventTransitWhitelist { get; set; } + /// /// Ids of containers on locations that only have Christmas loot /// @@ -143,6 +146,9 @@ public record SeasonalEventSettings [JsonPropertyName("enableRundansEvent")] public bool? EnableRundansEvent { get; set; } + + [JsonPropertyName("enableKhorvodEvent")] + public bool? EnableKhorvodEvent { get; set; } } public record ZombieSettings diff --git a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs index 3656a68a..94667985 100644 --- a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs @@ -519,9 +519,35 @@ public class SeasonalEventService( EnableRunnansEvent(databaseService.GetGlobals()); } + if (eventType.Settings?.EnableKhorvodEvent ?? false) + { + AdjustTransitsToKhorvodEvent(); + } + ChangeBtrToTarColaSkin(); } + protected void AdjustTransitsToKhorvodEvent() + { + var locations = databaseService.GetLocations().GetDictionary(); + + foreach (var (locationName, locationBase) in locations) + { + if (LocationConfig.NonMaps.Contains(locationName)) + { + continue; + } + + var matchingTransitWhitelist = SeasonalEventConfig.KhorvodEventTransitWhitelist.GetValueOrDefault(locationBase.Base.Id, null); + if (matchingTransitWhitelist is null ) + { + continue; + } + + locationBase.Base.Transits = locationBase.Base.Transits.Where(t => matchingTransitWhitelist.Contains(t.Id.Value)).ToList(); + } + } + protected void EnableRunnansEvent(Globals globals) { globals.Configuration.RunddansSettings.Active = true; From 4ad97ec82ba307316ef4ef5585b65b349cc32ab1 Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Sat, 20 Dec 2025 15:42:07 +0000 Subject: [PATCH 17/33] Format Style Fixes --- .../SPT_Data/configs/seasonalevents.json | 4 +++- .../SPTarkov.Server.Core/Services/SeasonalEventService.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 7eb84a59..17bf08b9 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -14001,6 +14001,8 @@ } }, "khorvodEventTransitWhitelist": { - "bigmap": [9] + "bigmap": [ + 9 + ] } } diff --git a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs index 94667985..469e6ee9 100644 --- a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs @@ -539,7 +539,7 @@ public class SeasonalEventService( } var matchingTransitWhitelist = SeasonalEventConfig.KhorvodEventTransitWhitelist.GetValueOrDefault(locationBase.Base.Id, null); - if (matchingTransitWhitelist is null ) + if (matchingTransitWhitelist is null) { continue; } From 93e6f8c03e4567f8a13eb330803c787fcede2b38 Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 20 Dec 2025 15:46:14 +0000 Subject: [PATCH 18/33] Updated transit id whitelist --- .../SPT_Data/configs/seasonalevents.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 7eb84a59..90d1b0e3 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -14001,6 +14001,11 @@ } }, "khorvodEventTransitWhitelist": { - "bigmap": [9] + "shoreline": [24], + "lighthouse": [22], + "rezervbase": [19], + "woods": [15], + "factory4_day": [13], + "bigmap": [11] } -} +} \ No newline at end of file From 1d2168af727c690f0169a797f73822c6f428dc5b Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Sat, 20 Dec 2025 15:48:15 +0000 Subject: [PATCH 19/33] Format Style Fixes --- .../SPT_Data/configs/seasonalevents.json | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 90d1b0e3..f495ce91 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -14001,11 +14001,23 @@ } }, "khorvodEventTransitWhitelist": { - "shoreline": [24], - "lighthouse": [22], - "rezervbase": [19], - "woods": [15], - "factory4_day": [13], - "bigmap": [11] + "shoreline": [ + 24 + ], + "lighthouse": [ + 22 + ], + "rezervbase": [ + 19 + ], + "woods": [ + 15 + ], + "factory4_day": [ + 13 + ], + "bigmap": [ + 11 + ] } -} \ No newline at end of file +} From c7155630a3127006fb1b708a63cffe0b2237128b Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 20 Dec 2025 15:59:52 +0000 Subject: [PATCH 20/33] Moved logic to better location --- .../Services/LocationLifecycleService.cs | 14 +++++++++- .../Services/SeasonalEventService.cs | 26 ------------------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 78d50340..0ece0f80 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -58,6 +58,7 @@ public class LocationLifecycleService( protected readonly PmcConfig PMCConfig = configServer.GetConfig(); protected readonly BotConfig BotConfig = configServer.GetConfig(); protected readonly LostOnDeathConfig LostOnDeathConfig = configServer.GetConfig(); + protected readonly SeasonalEventConfig SeasonalEventConfig = configServer.GetConfig(); protected const string Pmc = "pmc"; protected const string Savage = "savage"; @@ -118,7 +119,18 @@ public class LocationLifecycleService( // Handle Runddans / Khorovod event if (transitionType == TransitionType.EVENT && isRundansActive) { - foreach (var transits in location.Transits ?? []) + // TODO - wire up this first part to EnableKhorvodEvent in Seasonal config + move isRundansActive check to below block + if (location.Transits != null) + { + // Get whitelist for maps transits, event should have 1 only + var matchingTransitWhitelist = SeasonalEventConfig.KhorvodEventTransitWhitelist?.GetValueOrDefault(location.Id, null); + if (matchingTransitWhitelist != null) + { + location.Transits = location.Transits.Where(transit => matchingTransitWhitelist.Contains(transit.Id.Value)).ToList(); + } + } + + foreach (var transits in location.Transits) { transits.ActivateAfterSeconds = 300; transits.Events = true; diff --git a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs index 469e6ee9..3656a68a 100644 --- a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs @@ -519,35 +519,9 @@ public class SeasonalEventService( EnableRunnansEvent(databaseService.GetGlobals()); } - if (eventType.Settings?.EnableKhorvodEvent ?? false) - { - AdjustTransitsToKhorvodEvent(); - } - ChangeBtrToTarColaSkin(); } - protected void AdjustTransitsToKhorvodEvent() - { - var locations = databaseService.GetLocations().GetDictionary(); - - foreach (var (locationName, locationBase) in locations) - { - if (LocationConfig.NonMaps.Contains(locationName)) - { - continue; - } - - var matchingTransitWhitelist = SeasonalEventConfig.KhorvodEventTransitWhitelist.GetValueOrDefault(locationBase.Base.Id, null); - if (matchingTransitWhitelist is null) - { - continue; - } - - locationBase.Base.Transits = locationBase.Base.Transits.Where(t => matchingTransitWhitelist.Contains(t.Id.Value)).ToList(); - } - } - protected void EnableRunnansEvent(Globals globals) { globals.Configuration.RunddansSettings.Active = true; From 899e6fbff0f32292bcdf1267da20fab8a241b0e0 Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 20 Dec 2025 19:11:55 +0000 Subject: [PATCH 21/33] Moved loop to be inside guard --- .../Services/LocationLifecycleService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 0ece0f80..41c4eeef 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -128,12 +128,12 @@ public class LocationLifecycleService( { location.Transits = location.Transits.Where(transit => matchingTransitWhitelist.Contains(transit.Id.Value)).ToList(); } - } - foreach (var transits in location.Transits) - { - transits.ActivateAfterSeconds = 300; - transits.Events = true; + foreach (var transits in location.Transits) + { + transits.ActivateAfterSeconds = 300; + transits.Events = true; + } } } From fe4a71a6811095edeb488422df15943d00e5e496 Mon Sep 17 00:00:00 2001 From: Archangel Date: Sat, 20 Dec 2025 20:17:20 +0100 Subject: [PATCH 22/33] Further khorovod fixes --- .../SPT_Data/configs/seasonalevents.json | 8 ++--- .../Models/Spt/Config/SeasonalEventConfig.cs | 4 +-- .../Services/LocationLifecycleService.cs | 35 +++++++++++++------ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index f495ce91..fcb3143f 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -14000,7 +14000,7 @@ ] } }, - "khorvodEventTransitWhitelist": { + "khorovodEventTransitWhitelist": { "shoreline": [ 24 ], @@ -14013,11 +14013,11 @@ "woods": [ 15 ], - "factory4_day": [ - 13 - ], "bigmap": [ 11 + ], + "interchange": [ + ] } } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs index cdfefef7..d1b5f55a 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs @@ -47,8 +47,8 @@ public record SeasonalEventConfig : BaseConfig [JsonPropertyName("hostilitySettingsForEvent")] public required Dictionary>> HostilitySettingsForEvent { get; set; } - [JsonPropertyName("khorvodEventTransitWhitelist")] - public required Dictionary> KhorvodEventTransitWhitelist { get; set; } + [JsonPropertyName("khorovodEventTransitWhitelist")] + public required Dictionary> KhorovodEventTransitWhitelist { get; set; } /// /// Ids of containers on locations that only have Christmas loot diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 41c4eeef..8a410d5c 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -116,23 +116,36 @@ public class LocationLifecycleService( var location = GenerateLocationAndLoot(sessionId, request.Location, !request.ShouldSkipLootGeneration ?? true); var isRundansActive = databaseService.GetGlobals().Configuration.RunddansSettings.Active; - // Handle Runddans / Khorovod event - if (transitionType == TransitionType.EVENT && isRundansActive) + if (transitionType == TransitionType.EVENT) { - // TODO - wire up this first part to EnableKhorvodEvent in Seasonal config + move isRundansActive check to below block - if (location.Transits != null) + // Handle Runddans / Khorovod event + if (isRundansActive && location.Transits is not null) { // Get whitelist for maps transits, event should have 1 only - var matchingTransitWhitelist = SeasonalEventConfig.KhorvodEventTransitWhitelist?.GetValueOrDefault(location.Id, null); - if (matchingTransitWhitelist != null) - { - location.Transits = location.Transits.Where(transit => matchingTransitWhitelist.Contains(transit.Id.Value)).ToList(); - } + var matchingTransitWhitelist = SeasonalEventConfig.KhorovodEventTransitWhitelist.GetValueOrDefault( + location.Id.ToLowerInvariant(), + [] + ); foreach (var transits in location.Transits) { - transits.ActivateAfterSeconds = 300; - transits.Events = true; + if (transits.Id is null) + { + continue; + } + + // ActivateAfterSeconds sets the timer on the generator, events is needed because it is checked again in the client + // To enable certain stuff for the Khorovod event + if (matchingTransitWhitelist.Contains(transits.Id.Value)) + { + transits.ActivateAfterSeconds = 20; + transits.Events = true; + } + else + { + // Disable the other transits in this event, people are only allowed to transit to certain points + transits.IsActive = false; + } } } } From 94cc2190b4b543b29eceab9df554837ad005b2bf Mon Sep 17 00:00:00 2001 From: sp-tarkov-bot Date: Sat, 20 Dec 2025 19:18:35 +0000 Subject: [PATCH 23/33] Format Style Fixes --- .../SPT_Data/configs/seasonalevents.json | 4 +--- 1 file changed, 1 insertion(+), 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 fcb3143f..7d1f306a 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -14016,8 +14016,6 @@ "bigmap": [ 11 ], - "interchange": [ - - ] + "interchange": [] } } From e314c9ae89ad5f000bb933be8ee08e27837525e6 Mon Sep 17 00:00:00 2001 From: Archangel Date: Sat, 20 Dec 2025 20:29:43 +0100 Subject: [PATCH 24/33] Reset timer back to 5 minutes --- .../SPTarkov.Server.Core/Services/LocationLifecycleService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 8a410d5c..59dbe831 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -138,7 +138,7 @@ public class LocationLifecycleService( // To enable certain stuff for the Khorovod event if (matchingTransitWhitelist.Contains(transits.Id.Value)) { - transits.ActivateAfterSeconds = 20; + transits.ActivateAfterSeconds = 300; transits.Events = true; } else From 7830d62c7d540f02f82700b56c834edafcffd48d Mon Sep 17 00:00:00 2001 From: andrei-zgirvaci Date: Sun, 21 Dec 2025 17:01:31 +0100 Subject: [PATCH 25/33] feat: add MacOS support --- SPTarkov.Server/Properties/launchSettings.json | 5 +++++ SPTarkov.Server/SPTarkov.Server.csproj | 3 +++ 2 files changed, 8 insertions(+) diff --git a/SPTarkov.Server/Properties/launchSettings.json b/SPTarkov.Server/Properties/launchSettings.json index 052c9b12..a3ec4fbb 100644 --- a/SPTarkov.Server/Properties/launchSettings.json +++ b/SPTarkov.Server/Properties/launchSettings.json @@ -9,6 +9,11 @@ "commandName": "Project", "hotReloadEnabled": false, "workingDirectory": "bin/$(Configuration)/$(TargetFramework)" + }, + "Spt Server (Mac)": { + "commandName": "Project", + "hotReloadEnabled": false, + "workingDirectory": "bin/$(Configuration)/$(TargetFramework)/osx-arm64" } } } diff --git a/SPTarkov.Server/SPTarkov.Server.csproj b/SPTarkov.Server/SPTarkov.Server.csproj index 8555185c..e3f0e007 100644 --- a/SPTarkov.Server/SPTarkov.Server.csproj +++ b/SPTarkov.Server/SPTarkov.Server.csproj @@ -25,6 +25,9 @@ SPT.Server.Linux + + SPT.Server.Mac + From 2565cafc27ecd7539156cf836ffcd237384f3dc2 Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 22 Dec 2025 14:41:30 +0100 Subject: [PATCH 26/33] Always force a ChangeRequirement dict to exist --- .../Controllers/RepeatableQuestController.cs | 3 ++- .../Models/Eft/Common/Tables/RepeatableQuests.cs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index 0b183704..a48e1288 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs @@ -607,7 +607,7 @@ public class RepeatableQuestController( fullProfile.SptData.FreeRepeatableRefreshUsedCount[repeatableTypeLower] = 0; // Create stupid redundant change requirements from quest data - generatedRepeatables.ChangeRequirement = new Dictionary(); + generatedRepeatables.ChangeRequirement = []; foreach (var quest in generatedRepeatables.ActiveQuests) { generatedRepeatables.ChangeRequirement.TryAdd( @@ -707,6 +707,7 @@ public class RepeatableQuestController( EndTime = 0, FreeChanges = hasAccess ? repeatableConfig.FreeChanges : 0, FreeChangesAvailable = hasAccess ? repeatableConfig.FreeChangesAvailable : 0, + ChangeRequirement = [], }; // Add base object that holds repeatable data to profile diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/RepeatableQuests.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/RepeatableQuests.cs index 101530bb..02535964 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/RepeatableQuests.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/RepeatableQuests.cs @@ -6,7 +6,7 @@ namespace SPTarkov.Server.Core.Models.Eft.Common.Tables; public record RepeatableQuest : Quest { [JsonPropertyName("changeCost")] - public List? ChangeCost { get; set; } + public required List ChangeCost { get; set; } [JsonPropertyName("changeStandingCost")] public int? ChangeStandingCost { get; set; } @@ -94,7 +94,7 @@ public record PmcDataRepeatableQuest /// [JsonIgnore(Condition = JsonIgnoreCondition.Never)] [JsonPropertyName("changeRequirement")] - public Dictionary? ChangeRequirement { get; set; } + public required Dictionary ChangeRequirement { get; set; } = []; [JsonPropertyName("freeChanges")] public int? FreeChanges { get; set; } @@ -106,10 +106,10 @@ public record PmcDataRepeatableQuest public record ChangeRequirement { [JsonPropertyName("changeCost")] - public List? ChangeCost { get; set; } + public required List ChangeCost { get; set; } = []; [JsonPropertyName("changeStandingCost")] - public double? ChangeStandingCost { get; set; } + public required double ChangeStandingCost { get; set; } } public record ChangeCost From 0bada5c20cac032e847e4b1d22590a052739b83e Mon Sep 17 00:00:00 2001 From: Archangel Date: Mon, 22 Dec 2025 20:48:50 +0100 Subject: [PATCH 27/33] Add migration to fix invalid repeatable quests --- .../Fixes/InvalidRepeatableQuestFix.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs diff --git a/Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs b/Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs new file mode 100644 index 00000000..b829cf25 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs @@ -0,0 +1,70 @@ +using System.Text.Json.Nodes; +using SPTarkov.DI.Annotations; + +namespace SPTarkov.Server.Core.Migration.Migrations.Fixes; + +[Injectable] +public sealed class InvalidRepeatableQuestFix : AbstractProfileMigration +{ + public override string FromVersion + { + get { return "~4.0"; } + } + + public override string ToVersion + { + get { return "~4.0"; } + } + + public override string MigrationName + { + get { return "InvalidRepeatableQuestFix"; } + } + + public override bool CanMigrate(JsonObject profile, IEnumerable previouslyRanMigrations) + { + if (profile["characters"]?["pmc"]?["RepeatableQuests"] is JsonArray repeatables) + { + foreach (var node in repeatables) + { + if (node is not JsonObject quest) + { + continue; + } + + var endTimeNode = quest["endTime"]; + var endTime = endTimeNode?.GetValue() ?? 0; + + if (endTime != 0 && quest["changeRequirement"] is null) + { + return true; + } + } + } + + return false; + } + + public override JsonObject? Migrate(JsonObject profile) + { + if (profile["characters"]?["pmc"]?["RepeatableQuests"] is JsonArray repeatables) + { + foreach (var node in repeatables) + { + if (node is not JsonObject quest) + { + continue; + } + + var endTime = quest["endTime"]?.GetValue() ?? 0; + + if (endTime != 0 && quest["changeRequirement"] is null) + { + quest["endTime"] = 0; + } + } + } + + return base.Migrate(profile); + } +} From e8d85c8565281f4eb7881e25e20c0c5c0739e0c7 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 23 Dec 2025 12:44:35 +0000 Subject: [PATCH 28/33] Updated `ArmorDurability` nullability values --- .../SPTarkov.Server.Core/Models/Spt/Config/BotDurability.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotDurability.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotDurability.cs index beb207f1..39505f00 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotDurability.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotDurability.cs @@ -65,10 +65,10 @@ public record ArmorDurability public int MinLimitPercent { get; set; } [JsonPropertyName("lowestMaxPercent")] - public int LowestMaxPercent { get; set; } + public int? LowestMaxPercent { get; set; } [JsonPropertyName("highestMaxPercent")] - public int HighestMaxPercent { get; set; } + public int? HighestMaxPercent { get; set; } } public record WeaponDurability From baa0f4510f2ba466a88234f1b0bf29b4a5f478ca Mon Sep 17 00:00:00 2001 From: Archangel Date: Tue, 23 Dec 2025 21:23:21 +0100 Subject: [PATCH 29/33] Check if fence is not locked for daily scav quest gen --- .../Controllers/RepeatableQuestController.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index a48e1288..92af6fd6 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs @@ -771,6 +771,14 @@ public class RepeatableQuestController( /// True if unlocked protected bool PlayerHasDailyScavQuestsUnlocked(PmcData pmcData) { + if (pmcData.TradersInfo.TryGetValue("579dc571d53a0658a154fbec", out TraderInfo fence)) + { + if (fence.Unlocked is not null && !fence.Unlocked.Value) + { + return false; + } + } + return pmcData.Hideout?.Areas?.FirstOrDefault(hideoutArea => hideoutArea.Type == HideoutAreas.IntelligenceCenter)?.Level >= 1; } From c6c0347145777f9639f4fdf42630f6be9dab098c Mon Sep 17 00:00:00 2001 From: Archangel Date: Tue, 23 Dec 2025 21:25:01 +0100 Subject: [PATCH 30/33] var --- .../Controllers/RepeatableQuestController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index 92af6fd6..80c269ff 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs @@ -771,7 +771,7 @@ public class RepeatableQuestController( /// True if unlocked protected bool PlayerHasDailyScavQuestsUnlocked(PmcData pmcData) { - if (pmcData.TradersInfo.TryGetValue("579dc571d53a0658a154fbec", out TraderInfo fence)) + if (pmcData.TradersInfo.TryGetValue("579dc571d53a0658a154fbec", out var fence)) { if (fence.Unlocked is not null && !fence.Unlocked.Value) { From f54ed779c5ba405a33efbb4cc15d75321ee0f5de Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 23 Dec 2025 20:42:53 +0000 Subject: [PATCH 31/33] Small change to `PlayerHasDailyScavQuestsUnlocked` --- .../Controllers/RepeatableQuestController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index 80c269ff..0a145de4 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs @@ -771,7 +771,7 @@ public class RepeatableQuestController( /// True if unlocked protected bool PlayerHasDailyScavQuestsUnlocked(PmcData pmcData) { - if (pmcData.TradersInfo.TryGetValue("579dc571d53a0658a154fbec", out var fence)) + if (pmcData.TradersInfo.TryGetValue(Traders.FENCE, out var fence)) { if (fence.Unlocked is not null && !fence.Unlocked.Value) { From 1f98c27bed88a6c5fb0d5ed3528f79feceef155f Mon Sep 17 00:00:00 2001 From: Chomp Date: Wed, 24 Dec 2025 13:55:17 +0000 Subject: [PATCH 32/33] Fixed woods > customs transit --- .../SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index 7d1f306a..fa40041f 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json @@ -14011,7 +14011,7 @@ 19 ], "woods": [ - 15 + 41 ], "bigmap": [ 11 From ba49b004bb2d495e3840294b3f7a35ed094d0e2c Mon Sep 17 00:00:00 2001 From: Chomp Date: Wed, 24 Dec 2025 14:32:33 +0000 Subject: [PATCH 33/33] Log warning when RAM is below 30GB --- Libraries/SPTarkov.Server.Core/Utils/App.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Utils/App.cs b/Libraries/SPTarkov.Server.Core/Utils/App.cs index 4c8bd14f..79381d2d 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/App.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/App.cs @@ -41,6 +41,11 @@ public class App( logger.Debug($"OS: {Environment.OSVersion.Version} | {Environment.OSVersion.Platform}"); logger.Debug($"Pagefile: {pageFileGb:F2} GB"); logger.Debug($"RAM: {totalMemoryGb:F2} GB"); + if (totalMemoryGb < 30) + { + logger.Warning($"Detected RAM ({totalMemoryGb:F2}GB) is smaller than recommended (32GB) you may experience crashes or reduced FPS on large maps"); + } + logger.Debug($"Ran as admin: {Environment.IsPrivilegedProcess}"); logger.Debug($"CPU cores: {Environment.ProcessorCount}"); logger.Debug($"PATH: {(Environment.ProcessPath ?? "null returned").Encode(EncodeType.BASE64)}");