Merge pull request #397 from sp-tarkov/lazyload-transformer-handling

Add Transformer to Lazyload, get rid of event
This commit is contained in:
Chomp
2025-06-15 21:00:59 +01:00
committed by GitHub
7 changed files with 131 additions and 252 deletions
@@ -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<BundleHashCacheService> _logger,
HashUtil _hashUtil,
JsonUtil _jsonUtil,
FileUtil _fileUtil
)
{
protected static readonly string _bundleHashCachePath = "./user/cache/bundleHashCache.json";
protected Dictionary<string, string> _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);
}
}
@@ -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<ModHashCacheService> _logger,
JsonUtil _jsonUtil,
HashUtil _hashUtil,
FileUtil _fileUtil
)
{
protected readonly string _modCachePath = "./user/cache/modCache.json";
protected readonly Dictionary<string, string> _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);
}
}
@@ -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<LocaleConfig>();
protected readonly Dictionary<string, Dictionary<string, string>> customClientLocales = new();
/// <summary>
/// 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
/// </summary>
/// <returns> Dictionary of locales for desired language - en/fr/cn </returns>
public Dictionary<string, string> 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");
}
/// <summary>
/// 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
/// </summary>
/// <param name="languageKey">The language key for which the locale database should be retrieved.</param>
/// <param name="localeToReturn">The resulting locale database as a dictionary, or null if the operation fails.</param>
/// <returns>True if the locale database was successfully retrieved, otherwise false.</returns>
protected bool TryGetLocaleDbWithCustomLocales(string languageKey, out Dictionary<string, string>? localeToReturn)
protected bool TryGetLocaleDb(string languageKey, out Dictionary<string, string>? 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dbLocales">The dictionary containing locale entries from the database.</param>
/// <param name="customLocales">The dictionary containing custom locale entries to be merged.</param>
/// <returns>A dictionary representing the merged result of database and custom locales.</returns>
protected Dictionary<string, string> CombineDbWithCustomLocales(Dictionary<string, string> dbLocales, Dictionary<string, string> 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;
}
}
/// <summary>
/// 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<string, Dictionary<string, string>> dictionaryToAddTo)
{
dictionaryToAddTo.TryAdd(locale, new Dictionary<string, string>());
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;
}
}
/// <summary>
/// Blank out the "test" mail message from prapor
/// </summary>
@@ -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;
});
}
}
}
@@ -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()
@@ -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;
});
}
}
/// <summary>
@@ -2,6 +2,8 @@
public class LazyLoad<T>(Func<T> deserialize)
{
private readonly List<Func<T?, T?>> _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<T>(Func<T> deserialize)
private Timer? autoCleanerTimeout;
/// <summary>
/// <see cref="OnLazyLoad" /> can be subscribed to for mods to modify. It is fired right after lazy loading is complete
/// and any modification passed to <see cref="OnLazyLoadEventArgs.Value" /> will stay for the duration of this <see cref="LazyLoad{T}"/>'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.
/// </summary>
public event EventHandler<OnLazyLoadEventArgs<T>>? OnLazyLoad;
/// <param name="transformer">Function that transforms the value</param>
public void AddTransformer(Func<T?, T?> transformer)
{
_lazyLoadTransformersLock.EnterWriteLock();
try
{
_lazyLoadTransformers.Add(transformer);
}
finally
{
_lazyLoadTransformersLock.ExitWriteLock();
}
}
public T? Value
{
@@ -23,12 +38,21 @@ public class LazyLoad<T>(Func<T> deserialize)
_result = deserialize();
_isLoaded = true;
OnLazyLoadEventArgs<T> 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<T>(Func<T> deserialize)
_autoCleanerTimeout,
Timeout.InfiniteTimeSpan
);
}
}
autoCleanerTimeout?.Change(_autoCleanerTimeout, Timeout.InfiniteTimeSpan);
return _result;
}
}
}
public class OnLazyLoadEventArgs<T>(T value) : EventArgs
{
public T Value { get; set; } = value;
}