using System.Collections; using System.Collections.Frozen; using System.Linq.Expressions; using System.Reflection; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Utils.Json; namespace SPTarkov.Server.Core.Utils; [Injectable(InjectionType.Singleton)] public class ImporterUtil(ISptLogger logger, FileUtil fileUtil, JsonUtil jsonUtil) { private readonly FrozenSet _directoriesToIgnore = ["./SPT_Data/database/locales/server"]; private readonly FrozenSet _filesToIgnore = ["bearsuits.json", "usecsuits.json", "archivedquests.json"]; public async Task LoadRecursiveAsync( string filePath, Func? onReadCallback = null, Func? onObjectDeserialized = null ) { var result = await LoadRecursiveAsync(filePath, typeof(T), onReadCallback, onObjectDeserialized); return (T)result; } /// /// Load files into objects recursively (asynchronous) /// /// Path to folder with files /// /// /// /// Task protected async Task LoadRecursiveAsync( string filePath, Type loadedType, Func? onReadCallback = null, Func? onObjectDeserialized = null ) { var tasks = new List(); var dictionaryLock = new Lock(); 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).ToLowerInvariant()) ) { 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, onReadCallback, onObjectDeserialized, dictionaryLock)); } // Wait for all tasks to finish await Task.WhenAll(tasks); return result; } private async Task ProcessFileAsync( string file, Type loadedType, Func? onReadCallback, Func? onObjectDeserialized, object result, Lock dictionaryLock ) { try { if (onReadCallback != null) { await onReadCallback(file); } // Get the set method to update the object var setMethod = GetSetMethod( fileUtil.StripExtension(file).ToLowerInvariant(), loadedType, out var propertyType, out var isDictionary ); var fileDeserialized = await DeserializeFileAsync(file, propertyType); if (onObjectDeserialized != null) { await onObjectDeserialized(file, fileDeserialized); } lock (dictionaryLock) { setMethod.Invoke(result, isDictionary ? [fileUtil.StripExtension(file), fileDeserialized] : [fileDeserialized]); } } catch (Exception ex) { logger.Critical($"Unable to deserialize or find properties on file '{file}'", ex); throw new Exception($"Unable to deserialize or find properties on file '{file}'", ex); } } private async Task ProcessDirectoryAsync( string directory, Type loadedType, object result, Func? onReadCallback, Func? onObjectDeserialized, Lock dictionaryLock ) { try { var directoryName = directory.Split("/").Last().Replace("_", ""); if (MongoId.IsValidMongoId(directoryName)) { // For trader MongoId directories, we need to get the parent property. Get parent directory name to find the property var parentDirectory = directory.Substring(0, directory.LastIndexOf('/')); var parentName = parentDirectory.Split("/").Last().Replace("_", ""); GetSetMethod(parentName, loadedType, out var matchedProperty, out _); var loadedData = await LoadRecursiveAsync($"{directory}/", matchedProperty, onReadCallback, onObjectDeserialized); lock (dictionaryLock) { // Traders already have a dictionary, so we only need to handle this here if (result is IDictionary dictionary) { dictionary[new MongoId(directoryName)] = loadedData; } } } else { var setMethod = GetSetMethod(directoryName, loadedType, out var matchedProperty, out var isDictionary); var loadedData = await LoadRecursiveAsync($"{directory}/", matchedProperty, onReadCallback, onObjectDeserialized); lock (dictionaryLock) { setMethod.Invoke(result, isDictionary ? [directory, loadedData] : [loadedData]); } } } catch (Exception ex) { throw new Exception($"Error processing directory '{directory}'", ex); } } private async Task DeserializeFileAsync(string file, Type propertyType) { if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(LazyLoad<>)) { return CreateLazyLoadDeserialization(file, propertyType); } return await jsonUtil.DeserializeFromFileAsync(file, 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.ToLowerInvariant(), fileUtil.StripExtension(propertyName).ToLowerInvariant(), 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; } }