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 diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/seasonalevents.json index cb298be6..fa40041f 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,23 @@ } ] } + }, + "khorovodEventTransitWhitelist": { + "shoreline": [ + 24 + ], + "lighthouse": [ + 22 + ], + "rezervbase": [ + 19 + ], + "woods": [ + 41 + ], + "bigmap": [ + 11 + ], + "interchange": [] } } 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/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/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index 311e455b..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; @@ -886,6 +885,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 +922,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, diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RepeatableQuestController.cs index 0b183704..0a145de4 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 @@ -770,6 +771,14 @@ public class RepeatableQuestController( /// True if unlocked protected bool PlayerHasDailyScavQuestsUnlocked(PmcData pmcData) { + if (pmcData.TradersInfo.TryGetValue(Traders.FENCE, out var fence)) + { + if (fence.Unlocked is not null && !fence.Unlocked.Value) + { + return false; + } + } + return pmcData.Hideout?.Areas?.FirstOrDefault(hideoutArea => hideoutArea.Type == HideoutAreas.IntelligenceCenter)?.Level >= 1; } diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs index 0afef244..77c3f916 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs @@ -1820,9 +1820,14 @@ 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 { 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/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); + } +} 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; } 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 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/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 diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/SeasonalEventConfig.cs index b60278f5..d1b5f55a 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("khorovodEventTransitWhitelist")] + public required Dictionary> KhorovodEventTransitWhitelist { 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/ItemBaseClassService.cs b/Libraries/SPTarkov.Server.Core/Services/ItemBaseClassService.cs index fedb694b..b5085b53 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,32 @@ 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) + { + 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 +87,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"); @@ -101,14 +104,8 @@ 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, 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 +128,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"); @@ -153,14 +145,8 @@ 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, 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 +168,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/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 3b17ae59..59dbe831 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"; @@ -93,21 +94,73 @@ 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; + var location = GenerateLocationAndLoot(sessionId, request.Location, !request.ShouldSkipLootGeneration ?? true); + var isRundansActive = databaseService.GetGlobals().Configuration.RunddansSettings.Active; + + if (transitionType == TransitionType.EVENT) + { + // Handle Runddans / Khorovod event + if (isRundansActive && location.Transits is not null) + { + // Get whitelist for maps transits, event should have 1 only + var matchingTransitWhitelist = SeasonalEventConfig.KhorovodEventTransitWhitelist.GetValueOrDefault( + location.Id.ToLowerInvariant(), + [] + ); + + foreach (var transits in location.Transits) + { + 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 = 300; + transits.Events = true; + } + else + { + // Disable the other transits in this event, people are only allowed to transit to certain points + transits.IsActive = false; + } + } + } + } + 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), - TransitionType = TransitionType.NONE, + LocationLoot = location, + TransitionType = transitionType, Transition = new Transition { - TransitionType = TransitionType.NONE, + TransitionType = transitionType, TransitionRaidId = new MongoId(), TransitionCount = 0, VisitedLocations = [], @@ -128,7 +181,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 = transitionType; result.Transition.TransitionRaidId = transitionData.TransitionRaidId; result.Transition.TransitionCount += 1; 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)) { diff --git a/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs b/Libraries/SPTarkov.Server.Core/Services/ProfileFixerService.cs index 2fc9bcbb..7318044d 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,10 @@ 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); } } 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)}"); 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 +