diff --git a/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs index b93e9946..7a61e598 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs @@ -22,7 +22,6 @@ public class LocationLootGenerator( MathUtil _mathUtil, HashUtil _hashUtil, ItemHelper _itemHelper, - InventoryHelper _inventoryHelper, DatabaseService _databaseService, ContainerHelper _containerHelper, PresetHelper _presetHelper, @@ -33,8 +32,8 @@ public class LocationLootGenerator( ICloner _cloner ) { - protected LocationConfig _locationConfig = _configServer.GetConfig(); - protected SeasonalEventConfig _seasonalEventConfig = _configServer.GetConfig(); + protected readonly LocationConfig _locationConfig = _configServer.GetConfig(); + protected readonly SeasonalEventConfig _seasonalEventConfig = _configServer.GetConfig(); /// Create a list of container objects with randomised loot /// Map base to generate containers for @@ -155,9 +154,10 @@ public class LocationLootGenerator( return result; } - var mapping = GetGroupIdToContainerMappings(mapData.Statics, staticRandomisableContainersOnMap); + // For each of the container groups, choose from the pool of containers, hydrate container with loot and add to result array + var mapping = GetGroupIdToContainerMappings(mapData.Statics, staticRandomisableContainersOnMap); foreach (var (key, data) in mapping) { // Count chosen was 0, skip @@ -177,7 +177,7 @@ public class LocationLootGenerator( } // EDGE CASE: These are containers without a group and have a probability < 100% - if (key == "") + if (key == string.Empty) { var containerIdsCopy = _cloner.Clone(data.ContainerIdsWithProbability); // Roll each containers probability, if it passes, it gets added @@ -235,7 +235,6 @@ public class LocationLootGenerator( } _logger.Success($"A total of: {staticLootItemCount} static items spawned"); - _logger.Success( _localisationService.GetText("location-containers_generated_success", staticContainerCount) ); @@ -254,7 +253,7 @@ public class LocationLootGenerator( staticContainer.Probability != 1 && !staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) && !_locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains( - staticContainer.Template.Items[0].Template + staticContainer.Template.Items.FirstOrDefault().Template ) ) .ToList(); @@ -271,14 +270,14 @@ public class LocationLootGenerator( staticContainer.Probability == 1 || staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) || _locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains( - staticContainer.Template.Items[0].Template + staticContainer.Template.Items.FirstOrDefault().Template ) ) .ToList(); } /// - /// Choose a number of containers based on their probabilty value to fulfil the desired count in + /// Choose a number of containers based on their probability value to fulfil the desired count in /// containerData.chosenCount /// /// Name of the group the containers are being collected for @@ -346,12 +345,12 @@ public class LocationLootGenerator( } // Add an empty group for containers without a group id but still have a < 100% chance to spawn - // Likely bad BSG data, will be fixed...eventually, example of the groupids: `NEED_TO_BE_FIXED1`,`NEED_TO_BE_FIXED_SE02`, `NEED_TO_BE_FIXED_NW_01` - mapping[""] = new ContainerGroupCount + // Likely bad BSG data, will be fixed...eventually, example of the groupIds: `NEED_TO_BE_FIXED1`,`NEED_TO_BE_FIXED_SE02`, `NEED_TO_BE_FIXED_NW_01` + mapping.Add(string.Empty, new ContainerGroupCount { ContainerIdsWithProbability = new Dictionary(), ChosenCount = -1 - }; + }); // Iterate over all containers and add to group keyed by groupId // Containers without a group go into a group with empty key "" @@ -413,12 +412,12 @@ public class LocationLootGenerator( ) { var containerClone = _cloner.Clone(staticContainer); - var containerTpl = containerClone.Template.Items[0].Template; + var containerTpl = containerClone.Template.Items.FirstOrDefault().Template; // Create new unique parent id to prevent any collisions var parentId = _hashUtil.Generate(); containerClone.Template.Root = parentId; - containerClone.Template.Items[0].Id = parentId; + containerClone.Template.Items.FirstOrDefault().Id = parentId; var containerMap = _itemHelper.GetContainerMapping(containerTpl); @@ -432,7 +431,7 @@ public class LocationLootGenerator( // Get all possible loot items for container var containerLootPool = GetPossibleLootItemsForContainer(containerTpl, staticLootDist); - // Some containers need to have items forced into it (quest keys etc) + // Some containers need to have items forced into it (quest keys etc.) var tplsForced = staticForced .Where(forcedStaticProp => forcedStaticProp.ContainerId == containerClone.Template.Id) .Select(x => x.ItemTpl); @@ -463,14 +462,9 @@ public class LocationLootGenerator( continue; } - if (tplToAdd == "5bf3e0490db83400196199af") - { - Console.WriteLine("yo"); - } - // Check if item should have children removed var items = _locationConfig.TplsToStripChildItemsFrom.Contains(tplToAdd) - ? [chosenItemWithChildren.Items[0]] // Strip children from parent + ? [chosenItemWithChildren.Items.FirstOrDefault()] // Strip children from parent : chosenItemWithChildren.Items; // look for open slot to put chosen item into @@ -516,7 +510,7 @@ public class LocationLootGenerator( } /// - /// Look up a containers itemcountDistribution data and choose an item count based on the found weights + /// Look up a containers itemCountDistribution data and choose an item count based on the found weights /// /// Container to get item count for /// staticLoot.json @@ -546,8 +540,8 @@ public class LocationLootGenerator( } foreach (var itemCountDistribution in countDistribution) - // Add each count of items into array { + // Add each count of items into array itemCountArray.Add( new ProbabilityObject( itemCountDistribution.Count.Value, @@ -588,14 +582,14 @@ public class LocationLootGenerator( foreach (var icd in itemContainerDistribution) { if (!seasonalEventActive && seasonalItemTplBlacklist.Contains(icd.Tpl)) - // Skip seasonal event items if they're not enabled { + // Skip seasonal event items if they're not enabled continue; } - - // Ensure no blacklisted lootable items are in pool + if (_itemFilterService.IsLootableItemBlacklisted(icd.Tpl)) { + // Ensure no blacklisted lootable items are in pool continue; } @@ -653,12 +647,11 @@ public class LocationLootGenerator( // Add forced loot AddForcedLoot(loot, dynamicForcedSpawnPoints, locationName, staticAmmoDist); - var allDynamicSpawnpoints = dynamicLootDist.Spawnpoints; + var allDynamicSpawnPoints = dynamicLootDist.Spawnpoints; // Draw from random distribution - var desiredSpawnpointCount = Math.Round( - GetLooseLootMultiplierForLocation(locationName) * - _randomUtil.GetNormallyDistributedRandomNumber( + var desiredSpawnPointCount = Math.Round( + GetLooseLootMultiplierForLocation(locationName) * _randomUtil.GetNormallyDistributedRandomNumber( (double) dynamicLootDist.SpawnpointCount.Mean, (double) dynamicLootDist.SpawnpointCount.Std ) @@ -667,59 +660,59 @@ public class LocationLootGenerator( // Positions not in forced but have 100% chance to spawn List guaranteedLoosePoints = []; - var blacklistedSpawnpoints = _locationConfig.LooseLootBlacklist.GetValueOrDefault(locationName); - var spawnpointArray = new ProbabilityObjectArray(_mathUtil, _cloner); + var blacklistedSpawnPoints = _locationConfig.LooseLootBlacklist.GetValueOrDefault(locationName); + var spawnPointArray = new ProbabilityObjectArray(_mathUtil, _cloner); - foreach (var spawnpoint in allDynamicSpawnpoints) + foreach (var spawnPoint in allDynamicSpawnPoints) { // Point is blacklisted, skip - if (blacklistedSpawnpoints?.Contains(spawnpoint.Template.Id) ?? false) + if (blacklistedSpawnPoints?.Contains(spawnPoint.Template.Id) ?? false) { if (_logger.IsLogEnabled(LogLevel.Debug)) { - _logger.Debug($"Ignoring loose loot location: {spawnpoint.Template.Id}"); + _logger.Debug($"Ignoring loose loot location: {spawnPoint.Template.Id}"); } continue; } // We've handled IsAlwaysSpawn above, so skip them - if (spawnpoint.Template.IsAlwaysSpawn ?? false) + if (spawnPoint.Template.IsAlwaysSpawn ?? false) { continue; } // 100%, add it to guaranteed - if (spawnpoint.Probability == 1) + if (spawnPoint.Probability == 1) { - guaranteedLoosePoints.Add(spawnpoint); + guaranteedLoosePoints.Add(spawnPoint); continue; } - spawnpointArray.Add(new ProbabilityObject(spawnpoint.Template.Id, spawnpoint.Probability ?? 0, spawnpoint)); + spawnPointArray.Add(new ProbabilityObject(spawnPoint.Template.Id, spawnPoint.Probability ?? 0, spawnPoint)); } // Select a number of spawn points to add loot to // Add ALL loose loot with 100% chance to pool - List chosenSpawnpoints = []; - chosenSpawnpoints.AddRange(guaranteedLoosePoints); + List chosenSpawnPoints = []; + chosenSpawnPoints.AddRange(guaranteedLoosePoints); - var randomSpawnpointCount = desiredSpawnpointCount - chosenSpawnpoints.Count; + var randomSpawnPointCount = desiredSpawnPointCount - chosenSpawnPoints.Count; // Only draw random spawn points if needed - if (randomSpawnpointCount > 0 && spawnpointArray.Count > 0) + if (randomSpawnPointCount > 0 && spawnPointArray.Count > 0) // Add randomly chosen spawn points { - foreach (var si in spawnpointArray.Draw((int) randomSpawnpointCount, false)) + foreach (var si in spawnPointArray.Draw((int) randomSpawnPointCount, false)) { - chosenSpawnpoints.Add(spawnpointArray.Data(si)); + chosenSpawnPoints.Add(spawnPointArray.Data(si)); } } // Filter out duplicate locationIds // prob can be done better - chosenSpawnpoints = chosenSpawnpoints.GroupBy(spawnpoint => spawnpoint.LocationId).Select(group => group.First()).ToList(); + chosenSpawnPoints = chosenSpawnPoints.GroupBy(spawnPoint => spawnPoint.LocationId).Select(group => group.First()).ToList(); // Do we have enough items in pool to fulfill requirement - var tooManySpawnPointsRequested = desiredSpawnpointCount - chosenSpawnpoints.Count > 0; + var tooManySpawnPointsRequested = desiredSpawnPointCount - chosenSpawnPoints.Count > 0; if (tooManySpawnPointsRequested) { if (_logger.IsLogEnabled(LogLevel.Debug)) @@ -729,8 +722,8 @@ public class LocationLootGenerator( "location-spawn_point_count_requested_vs_found", new { - requested = desiredSpawnpointCount + guaranteedLoosePoints.Count, - found = chosenSpawnpoints.Count, + requested = desiredSpawnPointCount + guaranteedLoosePoints.Count, + found = chosenSpawnPoints.Count, mapName = locationName } ) @@ -738,12 +731,12 @@ public class LocationLootGenerator( } } - // Iterate over spawnpoints + // Iterate over spawnPoints var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled(); var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems(); - foreach (var spawnPoint in chosenSpawnpoints) + foreach (var spawnPoint in chosenSpawnPoints) { - // Spawnpoint is invalid, skip it + // SpawnPoint is invalid, skip it if (spawnPoint.Template is null) { _logger.Warning( @@ -840,9 +833,8 @@ public class LocationLootGenerator( foreach (var itemTpl in lootToForceSingleAmountOnMap) { // Get all spawn positions for item tpl in forced loot array - var items = forcedSpawnPoints.Where(forcedSpawnPoint => forcedSpawnPoint.Template.Items[0].Template == itemTpl - ); - if (items is null || !items.Any()) + var items = forcedSpawnPoints.Where(forcedSpawnPoint => forcedSpawnPoint.Template.Items.FirstOrDefault().Template == itemTpl); + if (!items.Any()) { if (_logger.IsLogEnabled(LogLevel.Debug)) { @@ -853,15 +845,15 @@ public class LocationLootGenerator( } // Create probability array of all spawn positions for this spawn id - var spawnpointArray = new ProbabilityObjectArray(_mathUtil, _cloner); + var spawnPointArray = new ProbabilityObjectArray(_mathUtil, _cloner); foreach (var si in items) // use locationId as template.Id is the same across all items { - spawnpointArray.Add(new ProbabilityObject(si.LocationId, si.Probability ?? 0, si)); + spawnPointArray.Add(new ProbabilityObject(si.LocationId, si.Probability ?? 0, si)); } // Choose 1 out of all found spawn positions for spawn id and add to loot array - foreach (var spawnPointLocationId in spawnpointArray.Draw(1, false)) + foreach (var spawnPointLocationId in spawnPointArray.Draw(1, false)) { var itemToAdd = items.FirstOrDefault(item => item.LocationId == spawnPointLocationId); var lootItem = itemToAdd?.Template; @@ -1032,14 +1024,14 @@ public class LocationLootGenerator( if (_locationConfig.TplsToStripChildItemsFrom.Contains(chosenItem.Template)) // Strip children from parent before adding { - itemWithChildren = [itemWithChildren[0]]; + itemWithChildren = [itemWithChildren.FirstOrDefault()]; } itemWithMods.AddRange(itemWithChildren); } // Get inventory size of item - var size = _itemHelper.GetItemSize(itemWithMods, itemWithMods[0].Id); + var size = _itemHelper.GetItemSize(itemWithMods, itemWithMods.FirstOrDefault().Id); return new ContainerItem { @@ -1100,13 +1092,13 @@ public class LocationLootGenerator( // No spawn point, use default template else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.WEAPON)) { - rootItem = CreateWeaponItems(chosenTpl, staticAmmoDist, parentId, ref items); + rootItem = CreateWeaponRootAndChildren(chosenTpl, staticAmmoDist, parentId, ref items); var size = _itemHelper.GetItemSize(items, rootItem.Id); width = size.Width; height = size.Height; } - // No spawnpoint to fall back on, generate manually + // No spawnPoint to fall back on, generate manually else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) { _itemHelper.AddCartridgesToAmmoBox(items, itemTemplate); @@ -1141,7 +1133,7 @@ public class LocationLootGenerator( _itemHelper.RemapRootItemId(presetAndMods); // Use original items parentId otherwise item doesn't get added to container correctly - presetAndMods[0].ParentId = rootItem.ParentId; + presetAndMods.FirstOrDefault().ParentId = rootItem.ParentId; items = presetAndMods; } else @@ -1160,15 +1152,30 @@ public class LocationLootGenerator( return items; } - protected Item? CreateWeaponItems(string chosenTpl, Dictionary> staticAmmoDist, string? parentId, ref List items) + /// + /// Attempt to find default preset for passed in tpl and construct a weapon with children. + /// If no preset found, return chosenTpl as Item object + /// + /// Tpl of item to get preset for + /// Pool of cartridges to pick from + /// + /// Root item + children + /// Root Item + protected Item? CreateWeaponRootAndChildren( + string chosenTpl, + Dictionary> cartridgePool, + string? parentId, + ref List items) { List children = []; + + // Look up a default preset for desired weapon tpl var defaultPreset = _cloner.Clone(_presetHelper.GetDefaultPreset(chosenTpl)); if (defaultPreset?.Items is not null) { try { - children = _itemHelper.ReparentItemAndChildren(defaultPreset.Items[0], defaultPreset.Items); + children = _itemHelper.ReparentItemAndChildren(defaultPreset.Items.FirstOrDefault(), defaultPreset.Items); } catch (Exception e) { @@ -1194,7 +1201,7 @@ public class LocationLootGenerator( } else { - // RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesn't have any default presets and kills this code below as it has no chidren to reparent + // RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesn't have any default presets and kills this code below as it has no children to re-parent if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug($"createStaticLootItem() No preset found for weapon: {chosenTpl}"); @@ -1259,7 +1266,7 @@ public class LocationLootGenerator( _itemHelper.FillMagazineWithRandomCartridge( magazineWithCartridges, magTemplate, - staticAmmoDist, + cartridgePool, weaponTemplate.Properties.AmmoCaliber, 0.25, defaultWeapon.Properties.DefAmmo, @@ -1274,7 +1281,10 @@ public class LocationLootGenerator( return rootItem; } - protected void GenerateStaticMagazineItem(Dictionary> staticAmmoDist, Item? rootItem, TemplateItem itemTemplate, + protected void GenerateStaticMagazineItem( + Dictionary> staticAmmoDist, + Item? rootItem, + TemplateItem itemTemplate, List items) { List magazineWithCartridges = [rootItem]; @@ -1292,7 +1302,7 @@ public class LocationLootGenerator( } } -public class ContainerGroupCount +public record ContainerGroupCount { [JsonPropertyName("containerIdsWithProbability")] public Dictionary? ContainerIdsWithProbability diff --git a/Libraries/SPTarkov.Server.Core/Generators/PmcWaveGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/PmcWaveGenerator.cs index 655cdf7f..68b334ad 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/PmcWaveGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/PmcWaveGenerator.cs @@ -4,32 +4,16 @@ using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; -using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Generators; [Injectable] -public class PmcWaveGenerator +public class PmcWaveGenerator( + ISptLogger logger, + DatabaseService databaseService, + ConfigServer configServer) { - protected ConfigServer _configServer; - protected DatabaseService _databaseService; - protected ISptLogger _logger; - protected PmcConfig _pmcConfig; - protected RandomUtil _randomUtil; - - public PmcWaveGenerator( - ISptLogger _logger, - RandomUtil _randomUtil, - DatabaseService _databaseService, - ConfigServer _configServer - ) - { - this._logger = _logger; - this._randomUtil = _randomUtil; - this._databaseService = _databaseService; - this._configServer = _configServer; - _pmcConfig = _configServer.GetConfig(); - } + protected readonly PmcConfig _pmcConfig = configServer.GetConfig(); /// /// Add a pmc wave to a map @@ -63,13 +47,8 @@ public class PmcWaveGenerator return; } - var location = _databaseService.GetLocation(name); - if (location is null) - { - return; - } - - location.Base.BossLocationSpawn.AddRange(pmcWavesToAdd); + var location = databaseService.GetLocation(name); + location?.Base.BossLocationSpawn.AddRange(pmcWavesToAdd); } /// diff --git a/Libraries/SPTarkov.Server.Core/Services/Cache/BundleHashCacheService.cs b/Libraries/SPTarkov.Server.Core/Services/Cache/BundleHashCacheService.cs deleted file mode 100644 index 6a5ca0af..00000000 --- a/Libraries/SPTarkov.Server.Core/Services/Cache/BundleHashCacheService.cs +++ /dev/null @@ -1,58 +0,0 @@ -using SPTarkov.DI.Annotations; -using SPTarkov.Server.Core.Models.Utils; -using SPTarkov.Server.Core.Utils; -using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; - -namespace SPTarkov.Server.Core.Services.Cache; - -[Injectable] -public class BundleHashCacheService( - ISptLogger _logger, - HashUtil _hashUtil, - JsonUtil _jsonUtil, - FileUtil _fileUtil -) -{ - protected static readonly string _bundleHashCachePath = "./user/cache/bundleHashCache.json"; - protected Dictionary _bundleHashes = new(); - - public string GetStoredValue(string key) - { - _bundleHashes.TryGetValue(key, out var value); - - return value; - } - - public void StoreValue(string key, string value) - { - _bundleHashes.Add(key, value); - - _fileUtil.WriteFile(_bundleHashCachePath, _jsonUtil.Serialize(_bundleHashes)); - - if (_logger.IsLogEnabled(LogLevel.Debug)) - { - _logger.Debug($"Bundle {key} hash stored in {_bundleHashCachePath}"); - } - } - - public bool MatchWithStoredHash(string bundlePath, string hash) - { - return GetStoredValue(bundlePath) == hash; - } - - public bool CalculateAndMatchHash(string bundlePath) - { - var fileContents = _fileUtil.ReadFile(bundlePath); - var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.MD5, fileContents); - - return MatchWithStoredHash(bundlePath, generatedHash); - } - - public void CalculateAndStoreHash(string bundlePath) - { - var fileContents = _fileUtil.ReadFile(bundlePath); - var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.MD5, fileContents); - - StoreValue(bundlePath, generatedHash); - } -} diff --git a/Libraries/SPTarkov.Server.Core/Services/Cache/ModHashCacheService.cs b/Libraries/SPTarkov.Server.Core/Services/Cache/ModHashCacheService.cs deleted file mode 100644 index a42083e5..00000000 --- a/Libraries/SPTarkov.Server.Core/Services/Cache/ModHashCacheService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using SPTarkov.DI.Annotations; -using SPTarkov.Server.Core.Models.Utils; -using SPTarkov.Server.Core.Utils; -using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; - -namespace SPTarkov.Server.Core.Services.Cache; - -[Injectable] -public class ModHashCacheService( - ISptLogger _logger, - JsonUtil _jsonUtil, - HashUtil _hashUtil, - FileUtil _fileUtil -) -{ - protected readonly string _modCachePath = "./user/cache/modCache.json"; - protected readonly Dictionary _modHashes = new(); - - public string? GetStoredValue(string key) - { - _modHashes.TryGetValue(key, out var value); - - return value; - } - - public void StoreValue(string key, string value) - { - _modHashes.TryAdd(key, value); - - _fileUtil.WriteFile(_modCachePath, _jsonUtil.Serialize(_modHashes)); - - if (_logger.IsLogEnabled(LogLevel.Debug)) - { - _logger.Debug($"Mod {key} hash stored in: {_modCachePath}"); - } - } - - public bool MatchWithStoredHash(string modName, string hash) - { - return GetStoredValue(modName) == hash; - } - - public bool CalculateAndCompareHash(string modName, string modContent) - { - var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.SHA1, modContent); - - return MatchWithStoredHash(modName, generatedHash); - } - - public void CalculateAndStoreHash(string modName, string modContent) - { - var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.SHA1, modContent); - - StoreValue(modName, generatedHash); - } -} diff --git a/Libraries/SPTarkov.Server.Core/Services/DatabaseService.cs b/Libraries/SPTarkov.Server.Core/Services/DatabaseService.cs index 2614a739..d2303c7e 100644 --- a/Libraries/SPTarkov.Server.Core/Services/DatabaseService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/DatabaseService.cs @@ -26,7 +26,7 @@ public class DatabaseService( HashUtil _hashUtil ) { - protected bool isDataValid = true; + private bool _isDataValid = true; /// assets/database/ public DatabaseTables GetTables() @@ -105,13 +105,15 @@ public class DatabaseService( /// /// Desired location ID /// assets/database/locations/ - public Location GetLocation(string locationId) + public Location? GetLocation(string locationId) { var locations = GetLocations(); var desiredLocation = locations.GetByJsonProp(locationId.ToLower()); if (desiredLocation == null) { - throw new Exception(_localisationService.GetText("database-no_location_found_with_id", locationId)); + _logger.Error(_localisationService.GetText("database-no_location_found_with_id", locationId)); + + return null; } return desiredLocation; @@ -297,12 +299,14 @@ public class DatabaseService( /// /// Desired trader ID /// assets/database/traders/ - public Trader GetTrader(string traderId) + public Trader? GetTrader(string traderId) { var traders = GetTraders(); if (!traders.TryGetValue(traderId, out var desiredTrader)) { - throw new Exception(_localisationService.GetText("database-no_trader_found_with_id", traderId)); + _logger.Error(_localisationService.GetText("database-no_trader_found_with_id", traderId)); + + return null; } return desiredTrader; @@ -326,13 +330,13 @@ public class DatabaseService( { var start = Stopwatch.StartNew(); - isDataValid = + _isDataValid = ValidateTable(GetQuests(), "quest") && ValidateTable(GetTraders(), "trader") && ValidateTable(GetItems(), "item") && ValidateTable(GetCustomization(), "customization"); - if (!isDataValid) + if (!_isDataValid) { _logger.Error(_localisationService.GetText("database-invalid_data")); } @@ -370,6 +374,6 @@ public class DatabaseService( /// True if the database contains valid data, false otherwise public bool IsDatabaseValid() { - return isDataValid; + return _isDataValid; } } diff --git a/Libraries/SPTarkov.Server.Core/Services/LocaleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocaleService.cs index 77e0a892..63c28014 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocaleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocaleService.cs @@ -13,13 +13,10 @@ public class LocaleService( ConfigServer _configServer ) { - // we have to LazyLoad the data from the database and then combine it with the custom data before returning it protected readonly LocaleConfig _localeConfig = _configServer.GetConfig(); - protected readonly Dictionary> customClientLocales = new(); /// /// Get the eft globals db file based on the configured locale in config/locale.json, if not found, fall back to 'en' - /// This will contain Custom locales added by mods /// /// Dictionary of locales for desired language - en/fr/cn public Dictionary GetLocaleDb(string? language = null) @@ -29,23 +26,21 @@ public class LocaleService( : language; // if it can't get locales for language provided, default to en - if (TryGetLocaleDbWithCustomLocales(languageToUse, out var localeToReturn) || - TryGetLocaleDbWithCustomLocales("en", out localeToReturn)) + if (TryGetLocaleDb(languageToUse, out var localeToReturn) || TryGetLocaleDb("en", out localeToReturn)) { - // TODO: need to see if this needs to be cloned - return RemovePraporTestMessage(localeToReturn); + return localeToReturn; } throw new Exception($"unable to get locales from either {languageToUse} or en"); } /// - /// Attempts to retrieve the locale database for the specified language key, including custom locales if available. + /// Attempts to retrieve the locale database for the specified language key /// /// The language key for which the locale database should be retrieved. /// The resulting locale database as a dictionary, or null if the operation fails. /// True if the locale database was successfully retrieved, otherwise false. - protected bool TryGetLocaleDbWithCustomLocales(string languageKey, out Dictionary? localeToReturn) + protected bool TryGetLocaleDb(string languageKey, out Dictionary? localeToReturn) { localeToReturn = null; if (!_databaseServer.GetTables().Locales.Global.TryGetValue(languageKey, out var keyedLocales)) @@ -55,42 +50,9 @@ public class LocaleService( localeToReturn = keyedLocales.Value; - if (customClientLocales.TryGetValue(languageKey, out var customClientLocale)) - { - // there were custom locales for this language - localeToReturn = CombineDbWithCustomLocales(localeToReturn, customClientLocale); - } - return true; } - - /// - /// Combines the provided database locales with custom locales, ensuring that all entries are merged into a single dictionary. - /// Custom locale entries will overwrite existing keys from the database locales if conflicts occur. - /// - /// The dictionary containing locale entries from the database. - /// The dictionary containing custom locale entries to be merged. - /// A dictionary representing the merged result of database and custom locales. - protected Dictionary CombineDbWithCustomLocales(Dictionary dbLocales, Dictionary customLocales) - { - try - { - return dbLocales - .Concat(customLocales) - .GroupBy(kvp => kvp.Key) - .ToDictionary( - group => group.Key, - group => group.Last().Value - ); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - /// /// Gets the game locale key from the locale.json file, /// if value is 'system' get system-configured locale @@ -239,33 +201,6 @@ public class LocaleService( return GetLocaleDb().Keys.Where(x => x.StartsWith(partialKey)).ToList(); } - public void AddCustomClientLocale(string locale, string localeKey, string localeValue) - { - AddToDictionary(locale, localeKey, localeValue, customClientLocales); - } - - public void RemoveCustomClientLocale(string locale, string localeKey) - { - customClientLocales.Remove(localeKey); - } - - private void AddToDictionary(string locale, string localeKey, string localeValue, - Dictionary> dictionaryToAddTo) - { - dictionaryToAddTo.TryAdd(locale, new Dictionary()); - if (!dictionaryToAddTo.TryGetValue(locale, out var localeDictToAddTo)) - { - _logger.Error($"Unable to get custom locale dictionary keyed by: {locale}"); - - return; - } - - if (!localeDictToAddTo.TryAdd(localeKey, localeValue)) - { - localeDictToAddTo[localeKey] = localeValue; - } - } - /// /// Blank out the "test" mail message from prapor /// diff --git a/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs b/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs index 9f19ef0f..33108ecd 100644 --- a/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/Mod/CustomItemService.cs @@ -243,9 +243,17 @@ public class CustomItemService( newLocaleDetails ??= localeDetails[localeDetails.Keys.FirstOrDefault()]; - localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} Name", newLocaleDetails.Name); - localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} ShortName", newLocaleDetails.ShortName); - localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} Description", newLocaleDetails.Description); + if (databaseService.GetLocales().Global.TryGetValue(shortNameKey.Key, out var lazyLoad)) + { + lazyLoad.AddTransformer(localeData => + { + localeData.Add($"{newItemId} Name", newLocaleDetails.Name); + localeData.Add($"{newItemId} ShortName", newLocaleDetails.ShortName); + localeData.Add($"{newItemId} Description", newLocaleDetails.Description); + + return localeData; + }); + } } } diff --git a/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs b/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs index a8e0132e..e779be6d 100644 --- a/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/PostDbLoadService.cs @@ -102,6 +102,8 @@ public class PostDbLoadService( RemoveNewBeginningRequirementFromPrestige(); + RemovePraporTestMessage(); + ValidateQuestAssortUnlocksExist(); if (_seasonalEventService.IsAutomaticEventDetectionEnabled()) @@ -191,6 +193,19 @@ public class PostDbLoadService( } } + private void RemovePraporTestMessage() + { + foreach((var locale, var lazyLoad) in _databaseService.GetLocales().Global) + { + lazyLoad.AddTransformer(lazyloadedData => + { + lazyloadedData["61687e2c3e526901fa76baf9"] = ""; + + return lazyloadedData; + }); + } + } + protected void CloneExistingCraftsAndAddNew() { var hideoutCraftDb = _databaseService.GetHideout().Production; @@ -264,34 +279,38 @@ public class PostDbLoadService( continue; } - var mapLooseLoot = _databaseService.GetLocation(mapId).LooseLoot.Value; - if (mapLooseLoot is null) + _databaseService.GetLocation(mapId).LooseLoot.AddTransformer(looselootData => { - _logger.Warning( - _localisationService.GetText("location-map_has_no_loose_loot_data", mapId) - ); - - continue; - } - - foreach (var positionToAdd in positionsToAdd) - { - // Exists already, add new items to existing positions pool - var existingLootPosition = mapLooseLoot.Spawnpoints.FirstOrDefault(x => - x.Template.Id == positionToAdd.Template.Id - ); - - if (existingLootPosition is not null) + if (looselootData is null) { - existingLootPosition.Template.Items.AddRange(positionToAdd.Template.Items); - existingLootPosition.ItemDistribution.AddRange(positionToAdd.ItemDistribution); + _logger.Warning( + _localisationService.GetText("location-map_has_no_loose_loot_data", mapId) + ); - continue; + return looselootData; } - // New position, add entire object - mapLooseLoot.Spawnpoints.Add(positionToAdd); - } + foreach (var positionToAdd in positionsToAdd) + { + // Exists already, add new items to existing positions pool + var existingLootPosition = looselootData.Spawnpoints.FirstOrDefault(x => + x.Template.Id == positionToAdd.Template.Id + ); + + if (existingLootPosition is not null) + { + existingLootPosition.Template.Items.AddRange(positionToAdd.Template.Items); + existingLootPosition.ItemDistribution.AddRange(positionToAdd.ItemDistribution); + + continue; + } + + // New position, add entire object + looselootData.Spawnpoints.Add(positionToAdd); + } + + return looselootData; + }); } } @@ -396,35 +415,39 @@ public class PostDbLoadService( foreach (var (mapId, mapAdjustments) in _lootConfig.LooseLootSpawnPointAdjustments) { - var mapLooseLootData = _databaseService.GetLocation(mapId).LooseLoot.Value; - if (mapLooseLootData is null) + _databaseService.GetLocation(mapId).LooseLoot.AddTransformer(looselootData => { - _logger.Warning( - _localisationService.GetText("location-map_has_no_loose_loot_data", mapId) - ); - - continue; - } - - foreach (var (lootKey, newChanceValue) in mapAdjustments) - { - var lootPostionToAdjust = mapLooseLootData.Spawnpoints.FirstOrDefault(spawnPoint => - spawnPoint.Template.Id == lootKey - ); - if (lootPostionToAdjust is null) + if (looselootData is null) { _logger.Warning( - _localisationService.GetText( - "location-unable_to_adjust_loot_position_on_map", - new { lootKey, mapId } - ) + _localisationService.GetText("location-map_has_no_loose_loot_data", mapId) ); - continue; + return looselootData; } - lootPostionToAdjust.Probability = newChanceValue; - } + foreach (var (lootKey, newChanceValue) in mapAdjustments) + { + var lootPostionToAdjust = looselootData.Spawnpoints.FirstOrDefault(spawnPoint => + spawnPoint.Template.Id == lootKey + ); + if (lootPostionToAdjust is null) + { + _logger.Warning( + _localisationService.GetText( + "location-unable_to_adjust_loot_position_on_map", + new { lootKey, mapId } + ) + ); + + continue; + } + + lootPostionToAdjust.Probability = newChanceValue; + } + + return looselootData; + }); } } @@ -518,11 +541,11 @@ public class PostDbLoadService( } foreach (var area in _databaseService.GetHideout().Areas) - foreach (var (_, stage) in area.Stages) - // Only adjust crafts ABOVE the override - { - stage.ConstructionTime = Math.Min(stage.ConstructionTime.Value, overrideSeconds); - } + foreach (var (_, stage) in area.Stages) + // Only adjust crafts ABOVE the override + { + stage.ConstructionTime = Math.Min(stage.ConstructionTime.Value, overrideSeconds); + } } protected void UnlockHideoutLootCrateCrafts() diff --git a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs index a5c126ba..e123e671 100644 --- a/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/SeasonalEventService.cs @@ -1030,8 +1030,16 @@ public class SeasonalEventService( protected void RenameBitcoin() { - _localeService.AddCustomClientLocale("en", $"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name", "Physical SPT Coin"); - _localeService.AddCustomClientLocale("en", $"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName", "0.2SPT"); + if(_databaseService.GetLocales().Global.TryGetValue("en", out var lazyLoad)) + { + lazyLoad.AddTransformer(localeData => + { + localeData[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin"; + localeData[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT"; + + return localeData; + }); + } } /// diff --git a/Libraries/SPTarkov.Server.Core/Utils/Json/LazyLoad.cs b/Libraries/SPTarkov.Server.Core/Utils/Json/LazyLoad.cs index 92d14be0..03905ed8 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/Json/LazyLoad.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/Json/LazyLoad.cs @@ -2,6 +2,8 @@ public class LazyLoad(Func deserialize) { + private readonly List> _lazyLoadTransformers = []; + private readonly ReaderWriterLockSlim _lazyLoadTransformersLock = new(); private static readonly TimeSpan _autoCleanerTimeout = TimeSpan.FromSeconds(30); private bool _isLoaded; private T? _result; @@ -9,10 +11,23 @@ public class LazyLoad(Func deserialize) private Timer? autoCleanerTimeout; /// - /// can be subscribed to for mods to modify. It is fired right after lazy loading is complete - /// and any modification passed to will stay for the duration of this 's lifecycle + /// Adds a transformer to modify the value during lazy loading. Transformers execute + /// in registration order and the final result is cached until auto-cleanup. /// - public event EventHandler>? OnLazyLoad; + /// Function that transforms the value + public void AddTransformer(Func transformer) + { + _lazyLoadTransformersLock.EnterWriteLock(); + + try + { + _lazyLoadTransformers.Add(transformer); + } + finally + { + _lazyLoadTransformersLock.ExitWriteLock(); + } + } public T? Value { @@ -23,12 +38,21 @@ public class LazyLoad(Func deserialize) _result = deserialize(); _isLoaded = true; - OnLazyLoadEventArgs args = new(_result); - OnLazyLoad?.Invoke(this, args); - - if (args.Value != null) + _lazyLoadTransformersLock.EnterReadLock(); + try { - _result = args.Value; + foreach (var transform in _lazyLoadTransformers) + { + _result = transform(_result); + } + } + catch(Exception) + { + throw; + } + finally + { + _lazyLoadTransformersLock.ExitReadLock(); } autoCleanerTimeout = new Timer( @@ -43,15 +67,10 @@ public class LazyLoad(Func deserialize) _autoCleanerTimeout, Timeout.InfiniteTimeSpan ); - } + } autoCleanerTimeout?.Change(_autoCleanerTimeout, Timeout.InfiniteTimeSpan); return _result; } } } - -public class OnLazyLoadEventArgs(T value) : EventArgs -{ - public T Value { get; set; } = value; -}