using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; using Core.Models.Utils; using Core.Utils.Json; using SptCommon.Annotations; namespace Core.Utils; [Injectable(InjectionType.Singleton)] public class ImporterUtil { protected FileUtil _fileUtil; protected JsonUtil _jsonUtil; protected ISptLogger _logger; protected HashSet directoriesToIgnore = ["./Assets/database/locales/server"]; protected HashSet filesToIgnore = ["bearsuits.json", "usecsuits.json", "archivedquests.json"]; protected readonly ConcurrentDictionary lazyLoadDeserializationCache = []; public ImporterUtil(ISptLogger logger, FileUtil fileUtil, JsonUtil jsonUtil) { _logger = logger; _fileUtil = fileUtil; _jsonUtil = jsonUtil; } public Task LoadRecursiveAsync( string filepath, Action? onReadCallback = null, Action? onObjectDeserialized = null ) { return LoadRecursiveAsync(filepath, typeof(T), onReadCallback, onObjectDeserialized) .ContinueWith(res => (T) res.Result); } /** * Load files into objects recursively (asynchronous) * @param filepath Path to folder with files * @returns Promise * return T type associated with this class */ protected async Task LoadRecursiveAsync( string filepath, Type loadedType, Action? onReadCallback = null, Action? onObjectDeserialized = null ) { var tasks = new List(); var dictionaryLock = new object(); var result = Activator.CreateInstance(loadedType); // get all filepaths var files = _fileUtil.GetFiles(filepath); var directories = _fileUtil.GetDirectories(filepath); // Process files foreach (var file in files) { if (_fileUtil.GetFileExtension(file) != "json" || filesToIgnore.Contains(_fileUtil.GetFileNameAndExtension(file).ToLower())) { continue; } tasks.Add(ProcessFileAsync(file, loadedType, onReadCallback, onObjectDeserialized, result, dictionaryLock)); } // Process directories foreach (var directory in directories) { if (directoriesToIgnore.Contains(directory)) { continue; } tasks.Add(ProcessDirectoryAsync(directory, loadedType, result, dictionaryLock)); } // Wait for all tasks to finish await Task.WhenAll(tasks); return result; } private async Task ProcessFileAsync( string file, Type loadedType, Action? onReadCallback, Action? onObjectDeserialized, object result, object dictionaryLock ) { try { using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read)) { onReadCallback?.Invoke(file); // Get the set method to update the object var setMethod = GetSetMethod( _fileUtil.StripExtension(file).ToLower(), loadedType, out var propertyType, out var isDictionary ); var fileDeserialized = await DeserializeFileAsync(fs, file, propertyType); onObjectDeserialized?.Invoke(file, fileDeserialized); lock (dictionaryLock) { setMethod.Invoke( result, isDictionary ? [_fileUtil.StripExtension(file), fileDeserialized] : new object[] { fileDeserialized } ); } } } catch (Exception ex) { throw new Exception($"Unable to deserialize or find properties on file '{file}'", ex); } } private async Task ProcessDirectoryAsync( string directory, Type loadedType, object result, object dictionaryLock ) { try { var setMethod = GetSetMethod( directory.Split("/").Last().Replace("_", ""), loadedType, out var matchedProperty, out var isDictionary ); var loadedData = await LoadRecursiveAsync($"{directory}/", matchedProperty); lock (dictionaryLock) { setMethod.Invoke(result, isDictionary ? [directory, loadedData] : new object[] { loadedData }); } } catch (Exception ex) { throw new Exception($"Error processing directory '{directory}'", ex); } } private async Task DeserializeFileAsync(FileStream fs, string file, Type propertyType) { if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(LazyLoad<>)) { return CreateLazyLoadDeserialization(file, propertyType); } return await Task.Run(() => _jsonUtil.DeserializeFromFileStream(fs, propertyType)); } private object CreateLazyLoadDeserialization(string file, Type propertyType) { var genericArgument = propertyType.GetGenericArguments()[0]; var deserializeCall = Expression.Call( Expression.Constant(_jsonUtil), "DeserializeFromFile", Type.EmptyTypes, Expression.Constant(file), Expression.Constant(genericArgument) ); var typeAsExpression = Expression.TypeAs(deserializeCall, genericArgument); var expression = Expression.Lambda( typeof(Func<>).MakeGenericType(genericArgument), typeAsExpression ); var expressionDelegate = expression.Compile(); return Activator.CreateInstance(propertyType, expressionDelegate); } public MethodInfo GetSetMethod(string propertyName, Type type, out Type propertyType, out bool isDictionary) { MethodInfo setMethod; isDictionary = false; if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { propertyType = type.GetGenericArguments()[1]; setMethod = type.GetMethod("Add"); isDictionary = true; } else { var matchedProperty = type.GetProperties() .FirstOrDefault( prop => string.Equals( prop.Name.ToLower(), _fileUtil.StripExtension(propertyName).ToLower(), StringComparison.Ordinal ) ); if (matchedProperty == null) { throw new Exception( $"Unable to find property '{_fileUtil.StripExtension(propertyName)}' for type '{type.Name}'" ); } propertyType = matchedProperty.PropertyType; setMethod = matchedProperty.GetSetMethod(); } return setMethod; } }