From 3419368f5391143890d76c1354657f32093dafcb Mon Sep 17 00:00:00 2001 From: Archangel Date: Sun, 15 Jun 2025 19:07:39 +0200 Subject: [PATCH] Add Transformer to Lazyload, get rid of event This will break mods and their examples! In addition: - Removes being able to add custom locales, modders should do .AddTransformer on the LazyLoaded value in the database! - Fixes up PostDBLoad methods trying to use .Value which won't work as this data will be unloaded after 30 seconds - Move all other SPT code to use .AddTransformer --- .../Services/LocaleService.cs | 73 +---------- .../Services/Mod/CustomItemService.cs | 14 +- .../Services/PostDbLoadService.cs | 123 +++++++++++------- .../Services/SeasonalEventService.cs | 12 +- .../Utils/Json/LazyLoad.cs | 43 ++++-- 5 files changed, 127 insertions(+), 138 deletions(-) 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..81ea758c 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,17 @@ 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); + } + } + finally + { + _lazyLoadTransformersLock.ExitReadLock(); } autoCleanerTimeout = new Timer( @@ -43,15 +63,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; -}