diff --git a/Libraries/Core/Callbacks/DataCallbacks.cs b/Libraries/Core/Callbacks/DataCallbacks.cs index 186e21d0..26bf39c9 100644 --- a/Libraries/Core/Callbacks/DataCallbacks.cs +++ b/Libraries/Core/Callbacks/DataCallbacks.cs @@ -171,8 +171,8 @@ public class DataCallbacks( { var localeId = url.Replace("/client/locale/", ""); var locales = _databaseService.GetLocales(); - var result = locales.Global?[localeId] - ?? locales.Global?.FirstOrDefault(m => m.Key == "en").Value; + var result = locales.Global?[localeId].Value + ?? locales.Global?.FirstOrDefault(m => m.Key == "en").Value.Value; return _httpResponseUtil.GetUnclearedBody(result); } diff --git a/Libraries/Core/Models/Eft/Common/Location.cs b/Libraries/Core/Models/Eft/Common/Location.cs index 11b72db4..1b5c4ff2 100644 --- a/Libraries/Core/Models/Eft/Common/Location.cs +++ b/Libraries/Core/Models/Eft/Common/Location.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Core.Models.Eft.Common.Tables; +using Core.Utils.Json; namespace Core.Models.Eft.Common; @@ -11,15 +12,15 @@ public record Location /** Loose loot positions and item weights */ [JsonPropertyName("looseLoot")] - public LooseLoot? LooseLoot { get; set; } + public LazyLoad? LooseLoot { get; set; } /** Static loot item weights */ [JsonPropertyName("staticLoot")] - public Dictionary? StaticLoot { get; set; } + public LazyLoad>? StaticLoot { get; set; } /** Static container positions and item weights */ [JsonPropertyName("staticContainers")] - public StaticContainerDetails? StaticContainers { get; set; } + public LazyLoad? StaticContainers { get; set; } [JsonPropertyName("staticAmmo")] public Dictionary> StaticAmmo { get; set; } diff --git a/Libraries/Core/Models/Spt/Server/LocaleBase.cs b/Libraries/Core/Models/Spt/Server/LocaleBase.cs index 15f2aab6..5c582f94 100644 --- a/Libraries/Core/Models/Spt/Server/LocaleBase.cs +++ b/Libraries/Core/Models/Spt/Server/LocaleBase.cs @@ -1,18 +1,16 @@ using System.Text.Json.Serialization; +using Core.Utils.Json; namespace Core.Models.Spt.Server; public record LocaleBase { [JsonPropertyName("global")] - public Dictionary>? Global { get; set; } + public Dictionary>>? Global { get; set; } [JsonPropertyName("menu")] public Dictionary>? Menu { get; set; } [JsonPropertyName("languages")] public Dictionary? Languages { get; set; } - - [JsonPropertyName("server")] - public Dictionary>? Server { get; set; } } diff --git a/Libraries/Core/Services/I18nService.cs b/Libraries/Core/Services/I18nService.cs index 7e310aa6..0ed29c38 100644 --- a/Libraries/Core/Services/I18nService.cs +++ b/Libraries/Core/Services/I18nService.cs @@ -1,4 +1,5 @@ using Core.Utils; +using Core.Utils.Json; using SptCommon.Extensions; namespace Core.Services; @@ -13,7 +14,7 @@ public class I18nService private FileUtil _fileUtil; private string _setLocale; - private Dictionary> _loadedLocales = new(); + private Dictionary>> _loadedLocales = new(); // TODO: try convert to primary ctor public I18nService( FileUtil fileUtil, @@ -41,8 +42,10 @@ public class I18nService throw new Exception($"Localisation files in directory {_directory} not found."); foreach (var file in files) _loadedLocales.Add(_fileUtil.StripExtension(file), - _jsonUtil.Deserialize>(_fileUtil.ReadFile(file)) ?? - new Dictionary()); + new LazyLoad>( + () => _jsonUtil.Deserialize>(_fileUtil.ReadFile(file)) ?? + new Dictionary() + )); if (!_loadedLocales.ContainsKey(_defaultLocale)) throw new Exception($"The default locale '{_defaultLocale}' does not exist on the loaded locales."); @@ -74,10 +77,10 @@ public class I18nService { if (!_loadedLocales.TryGetValue(_setLocale, out var locales)) return key; - if (!locales.TryGetValue(key, out var value)) + if (!locales.Value.TryGetValue(key, out var value)) { _loadedLocales.TryGetValue(_defaultLocale, out var defaults); - defaults.TryGetValue(key, out value); + defaults.Value.TryGetValue(key, out value); return value ?? key; } diff --git a/Libraries/Core/Services/LocaleService.cs b/Libraries/Core/Services/LocaleService.cs index 217026af..f981482c 100644 --- a/Libraries/Core/Services/LocaleService.cs +++ b/Libraries/Core/Services/LocaleService.cs @@ -25,13 +25,13 @@ public class LocaleService( public Dictionary GetLocaleDb() { var desiredLocale = _databaseServer.GetTables().Locales.Global[GetDesiredGameLocale()]; - if (desiredLocale != null) return desiredLocale; + if (desiredLocale != null) return desiredLocale.Value; _logger.Warning( $"Unable to find desired locale file using locale: {GetDesiredGameLocale()} from config/locale.json, falling back to 'en'" ); - return _databaseServer.GetTables().Locales.Global["en"]; + return _databaseServer.GetTables().Locales.Global["en"].Value; } /** diff --git a/Libraries/Core/Services/LocationLifecycleService.cs b/Libraries/Core/Services/LocationLifecycleService.cs index 470b1fc3..058764ff 100644 --- a/Libraries/Core/Services/LocationLifecycleService.cs +++ b/Libraries/Core/Services/LocationLifecycleService.cs @@ -330,7 +330,7 @@ public class LocationLifecycleService locationBaseClone.Loot.AddRange(staticLoot); // Add dynamic loot to output loot - var dynamicLootDistClone = _cloner.Clone(location.LooseLoot); + var dynamicLootDistClone = _cloner.Clone(location.LooseLoot.Value); var dynamicSpawnPoints = _locationLootGenerator.GenerateDynamicLoot( dynamicLootDistClone, staticAmmoDist, diff --git a/Libraries/Core/Services/SeasonalEventService.cs b/Libraries/Core/Services/SeasonalEventService.cs index a80d641f..14087670 100644 --- a/Libraries/Core/Services/SeasonalEventService.cs +++ b/Libraries/Core/Services/SeasonalEventService.cs @@ -763,8 +763,8 @@ public class SeasonalEventService( protected void RenameBitcoin() { var enLocale = _databaseService.GetLocales().Global["en"]; - enLocale[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin"; - enLocale[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT"; + enLocale.Value[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin"; + enLocale.Value[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT"; } /// diff --git a/Libraries/Core/Utils/ImporterUtil.cs b/Libraries/Core/Utils/ImporterUtil.cs index b03ca0a6..1ce30c0c 100644 --- a/Libraries/Core/Utils/ImporterUtil.cs +++ b/Libraries/Core/Utils/ImporterUtil.cs @@ -1,6 +1,8 @@ +using System.Linq.Expressions; using System.Reflection; using SptCommon.Annotations; using Core.Models.Utils; +using Core.Utils.Json; namespace Core.Utils; @@ -12,6 +14,7 @@ public class ImporterUtil protected ISptLogger _logger; protected HashSet filesToIgnore = ["bearsuits.json", "usecsuits.json", "archivedquests.json"]; + protected HashSet directoriesToIgnore = ["./Assets/database/locales/server"]; public ImporterUtil(ISptLogger logger, FileUtil fileUtil, JsonUtil jsonUtil) { @@ -26,7 +29,8 @@ public class ImporterUtil Action? onObjectDeserialized = null ) { - return LoadRecursiveAsync(filepath, typeof(T), onReadCallback, onObjectDeserialized).ContinueWith(res => (T) res.Result); + return LoadRecursiveAsync(filepath, typeof(T), onReadCallback, onObjectDeserialized) + .ContinueWith(res => (T)res.Result); } /** @@ -70,7 +74,40 @@ public class ImporterUtil ); try { - var fileDeserialized = _jsonUtil.Deserialize(fileData, propertyType); + object fileDeserialized = null; + if (propertyType.IsGenericType && + propertyType.GetGenericTypeDefinition() == typeof(LazyLoad<>)) + { + // This expression is create a generic type delegate for lazy loading a LazyLoad type + var expression = Expression.Lambda( + // this is the expected type of the lambda which is a function of whatever generic type LazyLoad<> is + typeof(Func<>).MakeGenericType(propertyType.GetGenericArguments()), + // An expression block will have a return type and then will execute the expression + Expression.Block( + // this is the return type + propertyType.GetGenericArguments()[0], + // this is the expression + // This expression casts the result of the Call expression as the generic argument type + Expression.TypeAs( + // this expression calls the json util Deserialize method + Expression.Call( + Expression.Constant(_jsonUtil), + "Deserialize", + [], + [Expression.Constant(fileData), Expression.Constant(propertyType.GetGenericArguments()[0])] + ), + propertyType.GetGenericArguments()[0] + ) + ) + ) + .Compile(); + fileDeserialized = Activator.CreateInstance(propertyType, expression); + } + else + { + fileDeserialized = _jsonUtil.Deserialize(fileData, propertyType); + } + if (onObjectDeserialized != null) onObjectDeserialized(file, fileDeserialized); @@ -96,6 +133,7 @@ public class ImporterUtil // deep tree search foreach (var directory in directories) { + if (directoriesToIgnore.Contains(directory)) continue; tasks.Add( Task.Factory.StartNew( () => diff --git a/Libraries/Core/Utils/Json/LazyLoad.cs b/Libraries/Core/Utils/Json/LazyLoad.cs new file mode 100644 index 00000000..468eca2b --- /dev/null +++ b/Libraries/Core/Utils/Json/LazyLoad.cs @@ -0,0 +1,34 @@ +namespace Core.Utils.Json; + +public class LazyLoad(Func deserialize) +{ + private T? _result; + private bool _isLoaded; + + private Timer? autoCleanerTimeout; + private static readonly TimeSpan _autoCleanerTimeout = TimeSpan.FromSeconds(30); + + public T? Value + { + get + { + if (!_isLoaded) + { + _result = deserialize(); + _isLoaded = true; + autoCleanerTimeout = new Timer(_ => + { + _result = default; + _isLoaded = false; + autoCleanerTimeout?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + autoCleanerTimeout = null; + },null, _autoCleanerTimeout, Timeout.InfiniteTimeSpan + ); + } + + autoCleanerTimeout?.Change(_autoCleanerTimeout, Timeout.InfiniteTimeSpan); + return _result; + } + set => _result = value; + } +}