From ad70921aeea4154bb4dbffac4e98ff0b0817c703 Mon Sep 17 00:00:00 2001 From: "hector.munoz" Date: Mon, 6 Jul 2026 17:57:04 -0600 Subject: [PATCH 1/5] CarCache + parallel batch loading --- Distance.CustomCar/Data/Car/CarBuilder.cs | 299 +++++++++++-------- Distance.CustomCar/Data/Car/CarCache.cs | 248 +++++++++++++++ Distance.CustomCar/Distance.CustomCar.csproj | 1 + 3 files changed, 430 insertions(+), 118 deletions(-) create mode 100644 Distance.CustomCar/Data/Car/CarCache.cs diff --git a/Distance.CustomCar/Data/Car/CarBuilder.cs b/Distance.CustomCar/Data/Car/CarBuilder.cs index d905494..fdb890a 100644 --- a/Distance.CustomCar/Data/Car/CarBuilder.cs +++ b/Distance.CustomCar/Data/Car/CarBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using UnityEngine; namespace Distance.CustomCar.Data.Car @@ -14,7 +15,11 @@ public class CarBuilder public void CreateCars(CarInfos infos) { infos_ = infos; - Dictionary cars = LoadAssetsBundles(); + + CarCache cache = new CarCache(); + cache.Load(); + + Dictionary cars = LoadAssetsBundles(cache); List carsInfos = new List(); @@ -38,6 +43,14 @@ public void CreateCars(CarInfos infos) } } + if (cars.Count > 0) + { + int cachedCount = cache.Files.Count; + Mod.Log.LogInfo($"Car cache: {cachedCount} files tracked, {cars.Count} prefab(s) loaded"); + } + + cache.Save(); + RegisterCars(carsInfos); } @@ -114,10 +127,9 @@ private void RegisterCars(List carsInfos) } } - private Dictionary LoadAssetsBundles() + private Dictionary LoadAssetsBundles(CarCache cache) { Dictionary assetsList = new Dictionary(); - DirectoryInfo assetsDirectory = GetLocalFolder("Assets"); DirectoryInfo globalCarsDirectory = new DirectoryInfo(Path.Combine(Resource.personalDistanceDirPath_, "CustomCars")); if (!globalCarsDirectory.Exists) @@ -135,146 +147,195 @@ private Dictionary LoadAssetsBundles() } } - /*try - { - foreach (FileInfo assetsFile in assetsDirectory.GetFiles("*", SearchOption.AllDirectories).Concat(globalCarsDirectory.GetFiles("*", SearchOption.AllDirectories)).OrderBy(x => x.Name)) - { - try - { - Assets assets = Assets.FromUnsafePath(assetsFile.FullName); - AssetBundle bundle = assets.Bundle as AssetBundle; + Dictionary currentHashes = new Dictionary(); + DirectoryInfo profileDirectory = new DirectoryInfo(Directory.GetParent(Path.GetDirectoryName(System.Reflection.Assembly.GetCallingAssembly().Location)).ToString()); + List rawFiles = new List(); + foreach (FileInfo f in profileDirectory.GetFiles("*", SearchOption.AllDirectories).Concat(globalCarsDirectory.GetFiles("*", SearchOption.AllDirectories)).OrderBy(x => x.Name)) + { + if (f.Extension == "") + rawFiles.Add(f); + } - int foundPrefabCount = 0; + List validFiles = new List(); + List validSignatures = new List(); + int skippedInvalid = 0; + foreach (FileInfo file in rawFiles) + { + string sig = CarCache.GetFileSignature(file.FullName); + currentHashes[file.FullName] = sig; + if (cache.IsFileInvalid(file.FullName, sig)) + { + skippedInvalid++; + } + else + { + validFiles.Add(file); + validSignatures.Add(sig); + } + } + if (skippedInvalid > 0) + Mod.Log.LogInfo($"Skipped {skippedInvalid} previously invalid file(s)"); - foreach (string assetName in from name in bundle.GetAllAssetNames() where name.EndsWith(".prefab", StringComparison.InvariantCultureIgnoreCase) select name) + int batchSize = 4; + for (int batchStart = 0; batchStart < validFiles.Count; batchStart += batchSize) + { + int batchEnd = Math.Min(batchStart + batchSize, validFiles.Count); + BundleLoadResult[] batchResults = new BundleLoadResult[batchEnd - batchStart]; + int batchCount = batchResults.Length; + int loaded = 0; + using (ManualResetEvent batchDone = new ManualResetEvent(false)) + { + for (int i = 0; i < batchCount; i++) + { + int index = i; + FileInfo file = validFiles[batchStart + i]; + string sig = validSignatures[batchStart + i]; + ThreadPool.QueueUserWorkItem(_ => { - GameObject carPrefab = bundle.LoadAsset(assetName); - - string assetKey = $"{assetsFile.FullName} ({assetName})"; - - if (!assetsList.ContainsKey(assetKey)) + batchResults[index] = LoadBundle(file, sig); + if (Interlocked.Increment(ref loaded) == batchCount) + batchDone.Set(); + }); + } + if (!batchDone.WaitOne(30000)) + { + Mod.Log.LogWarning($"Bundle batch {(batchStart / batchSize) + 1} timed out after 30s — incomplete results will be skipped."); + for (int i = 0; i < batchCount; i++) + { + if (batchResults[i].FilePath == null) { - assetsList.Add(assetKey, carPrefab); - foundPrefabCount++; + batchResults[i] = new BundleLoadResult + { + FilePath = validFiles[batchStart + i].FullName, + Signature = validSignatures[batchStart + i], + Error = new TimeoutException("Bundle loading timed out after 30 seconds — the file may be corrupted") + }; } } - - if (foundPrefabCount == 0) - { - Mod.Instance.Errors.Add($"Can't find a prefab in the asset bundle: {assetsFile.FullName}"); - Mod.Log.LogError($"Can't find a prefab in the asset bundle: {assetsFile.FullName}"); - } - } - catch (Exception ex) - { - Mod.Instance.Errors.Add($"Could not load assets file: {assetsFile.FullName}"); - Mod.Log.LogError($"Could not load assets file: {assetsFile.FullName}"); - Mod.Instance.Errors.Add(ex); - Mod.Log.LogError(ex); } } - } - catch(Exception ex) - { - Mod.Log.LogWarning("No Assets folder in the custom car folder."); - }*/ - /*try - { - DirectoryInfo otherAssetsDirectory = new DirectoryInfo(Path.Combine(Directory.GetParent(Directory.GetParent(Path.GetDirectoryName(System.Reflection.Assembly.GetCallingAssembly().Location)).ToString()).ToString(), "Assets")); - - foreach (FileInfo assetsFile in otherAssetsDirectory.GetFiles("*", SearchOption.AllDirectories).Concat(globalCarsDirectory.GetFiles("*", SearchOption.AllDirectories)).OrderBy(x => x.Name)) + foreach (BundleLoadResult result in batchResults) { - try - { - Assets assets = Assets.FromUnsafePath(assetsFile.FullName); - AssetBundle bundle = assets.Bundle as AssetBundle; - - int foundPrefabCount = 0; + string filePath = result.FilePath; + string signature = result.Signature; - foreach (string assetName in from name in bundle.GetAllAssetNames() where name.EndsWith(".prefab", StringComparison.InvariantCultureIgnoreCase) select name) - { - GameObject carPrefab = bundle.LoadAsset(assetName); + if (result.Bundle == null) + { + Mod.Instance.Errors.Add($"Could not load assets file: {filePath}"); + Mod.Log.LogError($"Could not load assets file: {filePath}"); + if (result.Error != null) + { + Mod.Instance.Errors.Add(result.Error); + Mod.Log.LogError(result.Error); + } + cache.MarkFileInvalid(filePath, signature); + continue; + } - string assetKey = $"{assetsFile.FullName} ({assetName})"; + AssetBundle bundle = result.Bundle; + bool isCached = cache.Files.ContainsKey(filePath) && cache.Files[filePath].Hash == signature; + string[] prefabNames; - if (!assetsList.ContainsKey(assetKey)) - { - assetsList.Add(assetKey, carPrefab); - foundPrefabCount++; - } - } + if (isCached) + { + prefabNames = cache.GetPrefabNames(filePath); + if (prefabNames.Length == 0) + prefabNames = ScanBundleForPrefabs(bundle); + } + else + { + prefabNames = ScanBundleForPrefabs(bundle); + Mod.Log.LogInfo($"New/changed file: {Path.GetFileName(filePath)} ({prefabNames.Length} prefab(s))"); + } - if (foundPrefabCount == 0) + int foundPrefabCount = 0; + foreach (string assetName in prefabNames) + { + GameObject carPrefab = bundle.LoadAsset(assetName); + string assetKey = $"{filePath} ({assetName})"; + if (!assetsList.ContainsKey(assetKey)) { - Mod.Instance.Errors.Add($"Can't find a prefab in the asset bundle: {assetsFile.FullName}"); - Mod.Log.LogError($"Can't find a prefab in the asset bundle: {assetsFile.FullName}"); + assetsList.Add(assetKey, carPrefab); + foundPrefabCount++; } } - catch (Exception ex) - { - Mod.Instance.Errors.Add($"Could not load assets file: {assetsFile.FullName}"); - Mod.Log.LogError($"Could not load assets file: {assetsFile.FullName}"); - Mod.Instance.Errors.Add(ex); - Mod.Log.LogError(ex); + + if (foundPrefabCount == 0) + { + Mod.Instance.Errors.Add($"Can't find a prefab in the asset bundle: {filePath}"); + Mod.Log.LogError($"Can't find a prefab in the asset bundle: {filePath}"); } + + cache.UpdateFile(filePath, signature, prefabNames); + bundle.Unload(false); } } - catch (Exception ex) - { - Mod.Log.LogWarning($"No Assets folder in toplevel directory."); - }*/ - - DirectoryInfo profileDirectory = new DirectoryInfo(Directory.GetParent(Path.GetDirectoryName(System.Reflection.Assembly.GetCallingAssembly().Location)).ToString()); - foreach (FileInfo assetsFile in profileDirectory.GetFiles("*", SearchOption.AllDirectories).Concat(globalCarsDirectory.GetFiles("*", SearchOption.AllDirectories)).OrderBy(x => x.Name)) + foreach (string cachedPath in new List(cache.Files.Keys)) { - if (assetsFile.Extension == "") - { - try - { - Assets assets = Assets.FromUnsafePath(assetsFile.FullName); - AssetBundle bundle = assets.Bundle as AssetBundle; - - int foundPrefabCount = 0; - - foreach (string assetName in from name in bundle.GetAllAssetNames() where name.EndsWith(".prefab", StringComparison.InvariantCultureIgnoreCase) select name) - { - GameObject carPrefab = bundle.LoadAsset(assetName); + if (!currentHashes.ContainsKey(cachedPath)) + cache.RemoveFile(cachedPath); + } - string assetKey = $"{assetsFile.FullName} ({assetName})"; + foreach (string invalidPath in new List(cache.InvalidFiles.Keys)) + { + if (!currentHashes.ContainsKey(invalidPath)) + cache.InvalidFiles.Remove(invalidPath); + } - if (!assetsList.ContainsKey(assetKey)) - { - assetsList.Add(assetKey, carPrefab); - foundPrefabCount++; - } - } + cache.CombinedHash = CarCache.ComputeCombinedHash(currentHashes); + return assetsList; + } - if (foundPrefabCount == 0) - { - Mod.Instance.Errors.Add($"Can't find a prefab in the asset bundle: {assetsFile.FullName}"); - Mod.Log.LogError($"Can't find a prefab in the asset bundle: {assetsFile.FullName}"); - } - } - catch (Exception ex) + private static BundleLoadResult LoadBundle(FileInfo file, string signature) + { + try + { + if (!CarCache.HasValidBundleSignature(file.FullName)) + return new BundleLoadResult { - Mod.Instance.Errors.Add($"Could not load assets file: {assetsFile.FullName}"); - Mod.Log.LogError($"Could not load assets file: {assetsFile.FullName}"); - Mod.Instance.Errors.Add(ex); - Mod.Log.LogError(ex); - } - } + FilePath = file.FullName, + Signature = signature, + Error = new InvalidDataException("File is not a valid Unity AssetBundle (missing UnityFS/UnityRaw/UnityWeb header)") + }; + + Assets assets = Assets.FromUnsafePath(file.FullName); + if (assets == null) + return new BundleLoadResult { FilePath = file.FullName, Signature = signature }; + return new BundleLoadResult + { + FilePath = file.FullName, + Signature = signature, + Bundle = assets.Bundle as AssetBundle + }; } + catch (Exception ex) + { + return new BundleLoadResult { FilePath = file.FullName, Signature = signature, Error = ex }; + } + } - return assetsList; + private struct BundleLoadResult + { + public string FilePath; + public string Signature; + public AssetBundle Bundle; + public Exception Error; } - public DirectoryInfo GetLocalFolder(string dir) + private static string[] ScanBundleForPrefabs(AssetBundle bundle) { - FileSystem files = new FileSystem(); - return new DirectoryInfo(Path.GetDirectoryName(Path.Combine(files.RootDirectory, dir + (dir.EndsWith($"{Path.DirectorySeparatorChar}") ? string.Empty : $"{Path.DirectorySeparatorChar}")))); + List prefabs = new List(); + foreach (string assetName in bundle.GetAllAssetNames()) + { + if (assetName.EndsWith(".prefab", StringComparison.InvariantCultureIgnoreCase)) + { + prefabs.Add(assetName); + } + } + return prefabs.ToArray(); } private CreateCarReturnInfos CreateCar(GameObject car) @@ -377,8 +438,10 @@ private void ReplaceMaterials(Renderer renderer) FillMaterialInfos(renderer, materialNames, materialProperties); - Material[] materials = renderer.materials.ToArray(); - for (int materialIndex = 0; materialIndex < renderer.materials.Length; materialIndex++) + Material[] originalMaterials = renderer.materials; + Material[] materials = new Material[originalMaterials.Length]; + Array.Copy(originalMaterials, materials, originalMaterials.Length); + for (int materialIndex = 0; materialIndex < originalMaterials.Length; materialIndex++) { if (!infos_.materials.TryGetValue(materialNames[materialIndex], out MaterialInfos materialInfo)) { @@ -395,22 +458,22 @@ private void ReplaceMaterials(Renderer renderer) Material material = UnityEngine.Object.Instantiate(materialInfo.material); if (materialInfo.diffuseIndex >= 0) { - material.SetTexture(materialInfo.diffuseIndex, renderer.materials[materialIndex].GetTexture("_MainTex")); + material.SetTexture(materialInfo.diffuseIndex, originalMaterials[materialIndex].GetTexture("_MainTex")); } if (materialInfo.normalIndex >= 0) { - material.SetTexture(materialInfo.normalIndex, renderer.materials[materialIndex].GetTexture("_BumpMap")); + material.SetTexture(materialInfo.normalIndex, originalMaterials[materialIndex].GetTexture("_BumpMap")); } if (materialInfo.emitIndex >= 0) { - material.SetTexture(materialInfo.emitIndex, renderer.materials[materialIndex].GetTexture("_EmissionMap")); + material.SetTexture(materialInfo.emitIndex, originalMaterials[materialIndex].GetTexture("_EmissionMap")); } foreach (MaterialPropertyExport property in materialProperties[materialIndex]) { - CopyMaterialProperty(renderer.materials[materialIndex], material, property); + CopyMaterialProperty(originalMaterials[materialIndex], material, property); } materials[materialIndex] = material; @@ -962,4 +1025,4 @@ private CarColors LoadDefaultColors(GameObject car) return infos_.defaultColors; } } -} \ No newline at end of file +} diff --git a/Distance.CustomCar/Data/Car/CarCache.cs b/Distance.CustomCar/Data/Car/CarCache.cs new file mode 100644 index 0000000..3d19fd7 --- /dev/null +++ b/Distance.CustomCar/Data/Car/CarCache.cs @@ -0,0 +1,248 @@ +using JsonFx.Json; +using JsonFx.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Distance.CustomCar.Data.Car +{ + public class CarCache + { + private static readonly string CachePath; + + static CarCache() + { + string modDir = Path.GetDirectoryName(typeof(CarCache).Assembly.Location); + string cacheDir = Path.Combine(modDir, "Settings"); + CachePath = Path.Combine(cacheDir, "car_cache.json"); + } + + public int Version { get; set; } = 1; + public string CombinedHash { get; set; } = string.Empty; + public Dictionary Files { get; set; } = new Dictionary(); + public Dictionary InvalidFiles { get; set; } = new Dictionary(); + + public static string GetFileSignature(string filePath) + { + if (!File.Exists(filePath)) + return string.Empty; + + var info = new FileInfo(filePath); + return $"{info.LastWriteTimeUtc.Ticks}|{info.Length}"; + } + + public static string ComputeCombinedHash(Dictionary fileSignatures) + { + var ordered = fileSignatures.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase); + var combined = new StringBuilder(); + foreach (var kv in ordered) + { + combined.Append(kv.Key); + combined.Append('='); + combined.Append(kv.Value); + combined.Append('|'); + } + return combined.ToString(); + } + + public static bool HasValidBundleSignature(string filePath) + { + try + { + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + if (stream.Length < 8) + return false; + + byte[] header = new byte[8]; + if (stream.Read(header, 0, 8) != 8) + return false; + + if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && + header[4] == 'y' && header[5] == 'F' && header[6] == 'S') + return true; + + if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && + header[4] == 'y' && header[5] == 'R' && header[6] == 'a' && header[7] == 'w') + return true; + + if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && + header[4] == 'y' && header[5] == 'W' && header[6] == 'e' && header[7] == 'b') + return true; + + return false; + } + } + catch + { + return false; + } + } + + public bool IsFileInvalid(string filePath, string currentSignature) + { + if (string.IsNullOrEmpty(currentSignature)) + return true; + + if (!InvalidFiles.TryGetValue(filePath, out string cachedSignature)) + return false; + + return cachedSignature == currentSignature; + } + + public void MarkFileInvalid(string filePath, string signature) + { + InvalidFiles[filePath] = signature; + } + + public void Load() + { + try + { + if (!File.Exists(CachePath)) + { + Mod.Log.LogInfo($"No car cache file at {CachePath}"); + return; + } + + string json = File.ReadAllText(CachePath); + var reader = new JsonReader(); + var data = reader.Read>(json); + + if (data == null || data.Count == 0) + return; + + if (data.TryGetValue("Version", out object versionVal) && versionVal != null) + Version = Convert.ToInt32(versionVal); + + if (data.TryGetValue("CombinedHash", out object combinedHashVal) && combinedHashVal != null) + CombinedHash = combinedHashVal.ToString(); + + if (data.TryGetValue("Files", out object filesVal) && filesVal is Dictionary filesDict) + { + foreach (var kv in filesDict) + { + if (kv.Value is Dictionary entryDict) + { + var entry = new CarCacheEntry(); + if (entryDict.TryGetValue("Hash", out object hashVal) && hashVal != null) + entry.Hash = hashVal.ToString(); + if (entryDict.TryGetValue("PrefabNames", out object prefabsVal) && prefabsVal is List prefabList) + { + entry.PrefabNames = prefabList.ConvertAll(x => x?.ToString() ?? string.Empty).ToArray(); + } + if (entryDict.TryGetValue("LastModified", out object modVal) && modVal != null) + { + DateTime parsed; + DateTime.TryParse(modVal.ToString(), out parsed); + entry.LastModified = parsed; + } + Files[kv.Key] = entry; + } + } + } + + if (data.TryGetValue("InvalidFiles", out object invalidVal) && invalidVal is Dictionary invalidDict) + { + foreach (var kv in invalidDict) + { + if (kv.Value != null) + InvalidFiles[kv.Key] = kv.Value.ToString(); + } + } + } + catch (Exception ex) + { + Mod.Log.LogWarning($"Failed to load car cache: {ex.Message}"); + Files.Clear(); + InvalidFiles.Clear(); + } + } + + public void Save() + { + try + { + var filesDict = new Dictionary(); + foreach (var kv in Files) + { + var entryDict = new Dictionary + { + ["Hash"] = kv.Value.Hash, + ["PrefabNames"] = new List(kv.Value.PrefabNames ?? new string[0]), + ["LastModified"] = kv.Value.LastModified.ToString("O") + }; + filesDict[kv.Key] = entryDict; + } + + var invalidDict = new Dictionary(); + foreach (var kv in InvalidFiles) + invalidDict[kv.Key] = kv.Value; + + var data = new Dictionary + { + ["Version"] = Version, + ["CombinedHash"] = CombinedHash, + ["Files"] = filesDict, + ["InvalidFiles"] = invalidDict + }; + + string cacheDir = Path.GetDirectoryName(CachePath); + if (!Directory.Exists(cacheDir)) + Directory.CreateDirectory(cacheDir); + + var writer = new JsonWriter(new DataWriterSettings { PrettyPrint = true }); + string json = writer.Write(data); + File.WriteAllText(CachePath, json); + } + catch (Exception ex) + { + Mod.Log.LogWarning($"Failed to save car cache: {ex.Message}"); + } + } + + public bool HasFileChanged(string filePath, out string currentSignature) + { + currentSignature = GetFileSignature(filePath); + + if (string.IsNullOrEmpty(currentSignature)) + return true; + + if (!Files.TryGetValue(filePath, out CarCacheEntry entry)) + return true; + + return entry.Hash != currentSignature; + } + + public void UpdateFile(string filePath, string signature, string[] prefabNames) + { + Files[filePath] = new CarCacheEntry + { + Hash = signature, + PrefabNames = prefabNames ?? new string[0], + LastModified = DateTime.UtcNow + }; + } + + public void RemoveFile(string filePath) + { + Files.Remove(filePath); + } + + public string[] GetPrefabNames(string filePath) + { + if (Files.TryGetValue(filePath, out CarCacheEntry entry) && entry.PrefabNames != null) + return entry.PrefabNames; + return new string[0]; + } + } + + public class CarCacheEntry + { + public string Hash { get; set; } = string.Empty; + public string[] PrefabNames { get; set; } = new string[0]; + public DateTime LastModified { get; set; } + } +} diff --git a/Distance.CustomCar/Distance.CustomCar.csproj b/Distance.CustomCar/Distance.CustomCar.csproj index 8259d54..bfd7096 100644 --- a/Distance.CustomCar/Distance.CustomCar.csproj +++ b/Distance.CustomCar/Distance.CustomCar.csproj @@ -61,6 +61,7 @@ + From 02f1ceb89dfe8852490da6693b8793af76b80f6b Mon Sep 17 00:00:00 2001 From: "hector.munoz" Date: Mon, 6 Jul 2026 17:59:20 -0600 Subject: [PATCH 2/5] Reflection cache, factory overloads, remove noisy logs --- Distance.CustomCar/Assets.cs | 26 +++++++++---------- .../Generic/DictionaryExtensions.cs | 12 +++++++++ Distance.CustomCar/ProfileCarColors.cs | 8 +++--- Distance.CustomCar/Section.cs | 12 +++++++++ 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/Distance.CustomCar/Assets.cs b/Distance.CustomCar/Assets.cs index 2f2d281..66c9e46 100644 --- a/Distance.CustomCar/Assets.cs +++ b/Distance.CustomCar/Assets.cs @@ -66,10 +66,7 @@ private object Load() { try { - var assetBundle = AssetBundleBridge.LoadFrom(FilePath); - Mod.Log.LogInfo($"Loaded asset bundle {FilePath}"); - - return assetBundle; + return AssetBundleBridge.LoadFrom(FilePath); } catch (Exception ex) { @@ -81,15 +78,18 @@ private object Load() internal static class AssetBundleBridge { - public static Type AssetBundleType => Kernel.FindTypeByFullName( - "UnityEngine.AssetBundle", - "UnityEngine" - ); - - private static MethodInfo LoadFromFile => AssetBundleType.GetMethod( - "LoadFromFile", - new[] { typeof(string) } - ); + private static Type _assetBundleType; + private static MethodInfo _loadFromFile; + + static AssetBundleBridge() + { + _assetBundleType = Kernel.FindTypeByFullName("UnityEngine.AssetBundle", "UnityEngine"); + _loadFromFile = _assetBundleType.GetMethod("LoadFromFile", new[] { typeof(string) }); + } + + public static Type AssetBundleType => _assetBundleType; + + private static MethodInfo LoadFromFile => _loadFromFile; public static object LoadFrom(string path) { diff --git a/Distance.CustomCar/Extensions/System/Collections/Generic/DictionaryExtensions.cs b/Distance.CustomCar/Extensions/System/Collections/Generic/DictionaryExtensions.cs index bf90ce0..f2903cb 100644 --- a/Distance.CustomCar/Extensions/System/Collections/Generic/DictionaryExtensions.cs +++ b/Distance.CustomCar/Extensions/System/Collections/Generic/DictionaryExtensions.cs @@ -1,4 +1,5 @@ #pragma warning disable RCS1110 +using System; using System.Collections.Generic; public static class DictionaryExtensions @@ -52,4 +53,15 @@ public static T GetOrCreate(this Dictionary obj, string key, return obj.GetItem(key); } + + public static T GetOrCreate(this Dictionary obj, string key, Func factory) where T : class + { + if (!obj.ContainsKey(key)) + { + T value = factory(); + obj[key] = value; + return value; + } + return (T)obj[key]; + } } \ No newline at end of file diff --git a/Distance.CustomCar/ProfileCarColors.cs b/Distance.CustomCar/ProfileCarColors.cs index ba986bb..ea4ed0f 100644 --- a/Distance.CustomCar/ProfileCarColors.cs +++ b/Distance.CustomCar/ProfileCarColors.cs @@ -23,14 +23,12 @@ protected void Awake() protected Dictionary Profile(string profileName) { - Mod.Log.LogInfo("Assigning Profile Name..."); - return Config.GetOrCreate(profileName, new Dictionary()); + return Config.GetOrCreate(profileName, () => new Dictionary()); } protected Dictionary Vehicle(string profileName, string vehicleName) { - Mod.Log.LogInfo("Assigning Vehicle Name..."); - return Profile(profileName).GetOrCreate(vehicleName, new Dictionary()); + return Profile(profileName).GetOrCreate(vehicleName, () => new Dictionary()); } protected CarColors GetCarColors(string profileName, string vehicleName) @@ -50,7 +48,7 @@ protected CarColors GetCarColors(string profileName, string vehicleName) protected Color GetColor(Dictionary vehicle, string category, Color defaultColor) { //Mod.Log.LogWarning(e); - Dictionary color = vehicle.GetOrCreate(category, new Dictionary()); + Dictionary color = vehicle.GetOrCreate(category, () => new Dictionary()); float r = color.GetOrCreate("r", defaultColor.r); float g = color.GetOrCreate("g", defaultColor.g); diff --git a/Distance.CustomCar/Section.cs b/Distance.CustomCar/Section.cs index 383ebb6..4d4d7e3 100644 --- a/Distance.CustomCar/Section.cs +++ b/Distance.CustomCar/Section.cs @@ -79,6 +79,18 @@ public T GetOrCreate(string key, T defaultValue) return GetItem(key); } + public T GetOrCreate(string key, Func factory) where T : class + { + if (!ContainsKey(key)) + { + T value = factory(); + this[key] = value; + ValueChanged?.Invoke(this, new SettingsChangedEventArgs(key, null, this[key])); + return value; + } + return (T)this[key]; + } + public bool ContainsKey(string key) { try From 1b468d237aaf17f663106d429eceebc56a0b90c5 Mon Sep 17 00:00:00 2001 From: "hector.munoz" Date: Wed, 8 Jul 2026 11:47:28 -0600 Subject: [PATCH 3/5] Strip CarCache, add PrefabIndex + dynamic batch size --- Distance.CustomCar/Data/Car/CarBuilder.cs | 116 ++++----- Distance.CustomCar/Data/Car/CarCache.cs | 248 ------------------- Distance.CustomCar/Data/Car/PrefabIndex.cs | 139 +++++++++++ Distance.CustomCar/Distance.CustomCar.csproj | 2 +- 4 files changed, 194 insertions(+), 311 deletions(-) delete mode 100644 Distance.CustomCar/Data/Car/CarCache.cs create mode 100644 Distance.CustomCar/Data/Car/PrefabIndex.cs diff --git a/Distance.CustomCar/Data/Car/CarBuilder.cs b/Distance.CustomCar/Data/Car/CarBuilder.cs index fdb890a..95e7fc9 100644 --- a/Distance.CustomCar/Data/Car/CarBuilder.cs +++ b/Distance.CustomCar/Data/Car/CarBuilder.cs @@ -16,10 +16,7 @@ public void CreateCars(CarInfos infos) { infos_ = infos; - CarCache cache = new CarCache(); - cache.Load(); - - Dictionary cars = LoadAssetsBundles(cache); + Dictionary cars = LoadAssetsBundles(); List carsInfos = new List(); @@ -43,14 +40,6 @@ public void CreateCars(CarInfos infos) } } - if (cars.Count > 0) - { - int cachedCount = cache.Files.Count; - Mod.Log.LogInfo($"Car cache: {cachedCount} files tracked, {cars.Count} prefab(s) loaded"); - } - - cache.Save(); - RegisterCars(carsInfos); } @@ -127,7 +116,7 @@ private void RegisterCars(List carsInfos) } } - private Dictionary LoadAssetsBundles(CarCache cache) + private Dictionary LoadAssetsBundles() { Dictionary assetsList = new Dictionary(); DirectoryInfo globalCarsDirectory = new DirectoryInfo(Path.Combine(Resource.personalDistanceDirPath_, "CustomCars")); @@ -147,7 +136,9 @@ private Dictionary LoadAssetsBundles(CarCache cache) } } - Dictionary currentHashes = new Dictionary(); + PrefabIndex prefabIndex = new PrefabIndex(); + prefabIndex.Load(); + DirectoryInfo profileDirectory = new DirectoryInfo(Directory.GetParent(Path.GetDirectoryName(System.Reflection.Assembly.GetCallingAssembly().Location)).ToString()); List rawFiles = new List(); @@ -157,27 +148,10 @@ private Dictionary LoadAssetsBundles(CarCache cache) rawFiles.Add(f); } - List validFiles = new List(); - List validSignatures = new List(); - int skippedInvalid = 0; - foreach (FileInfo file in rawFiles) - { - string sig = CarCache.GetFileSignature(file.FullName); - currentHashes[file.FullName] = sig; - if (cache.IsFileInvalid(file.FullName, sig)) - { - skippedInvalid++; - } - else - { - validFiles.Add(file); - validSignatures.Add(sig); - } - } - if (skippedInvalid > 0) - Mod.Log.LogInfo($"Skipped {skippedInvalid} previously invalid file(s)"); + List validFiles = rawFiles; + HashSet seenPaths = new HashSet(); - int batchSize = 4; + int batchSize = Math.Max(2, Environment.ProcessorCount); for (int batchStart = 0; batchStart < validFiles.Count; batchStart += batchSize) { int batchEnd = Math.Min(batchStart + batchSize, validFiles.Count); @@ -190,10 +164,9 @@ private Dictionary LoadAssetsBundles(CarCache cache) { int index = i; FileInfo file = validFiles[batchStart + i]; - string sig = validSignatures[batchStart + i]; ThreadPool.QueueUserWorkItem(_ => { - batchResults[index] = LoadBundle(file, sig); + batchResults[index] = LoadBundle(file); if (Interlocked.Increment(ref loaded) == batchCount) batchDone.Set(); }); @@ -208,7 +181,6 @@ private Dictionary LoadAssetsBundles(CarCache cache) batchResults[i] = new BundleLoadResult { FilePath = validFiles[batchStart + i].FullName, - Signature = validSignatures[batchStart + i], Error = new TimeoutException("Bundle loading timed out after 30 seconds — the file may be corrupted") }; } @@ -219,7 +191,7 @@ private Dictionary LoadAssetsBundles(CarCache cache) foreach (BundleLoadResult result in batchResults) { string filePath = result.FilePath; - string signature = result.Signature; + seenPaths.Add(filePath); if (result.Bundle == null) { @@ -230,26 +202,27 @@ private Dictionary LoadAssetsBundles(CarCache cache) Mod.Instance.Errors.Add(result.Error); Mod.Log.LogError(result.Error); } - cache.MarkFileInvalid(filePath, signature); continue; } AssetBundle bundle = result.Bundle; - bool isCached = cache.Files.ContainsKey(filePath) && cache.Files[filePath].Hash == signature; string[] prefabNames; - if (isCached) + if (prefabIndex.IsUpToDate(filePath)) { - prefabNames = cache.GetPrefabNames(filePath); - if (prefabNames.Length == 0) + prefabNames = prefabIndex.GetPrefabNames(filePath); + if (prefabNames == null || prefabNames.Length == 0) prefabNames = ScanBundleForPrefabs(bundle); } else { prefabNames = ScanBundleForPrefabs(bundle); - Mod.Log.LogInfo($"New/changed file: {Path.GetFileName(filePath)} ({prefabNames.Length} prefab(s))"); + if (prefabNames.Length > 0) + Mod.Log.LogInfo($"Scanned: {Path.GetFileName(filePath)} ({prefabNames.Length} prefab(s))"); } + prefabIndex.SetPrefabNames(filePath, prefabNames); + int foundPrefabCount = 0; foreach (string assetName in prefabNames) { @@ -268,59 +241,78 @@ private Dictionary LoadAssetsBundles(CarCache cache) Mod.Log.LogError($"Can't find a prefab in the asset bundle: {filePath}"); } - cache.UpdateFile(filePath, signature, prefabNames); bundle.Unload(false); } } - foreach (string cachedPath in new List(cache.Files.Keys)) + prefabIndex.RemoveStaleEntries(seenPaths); + prefabIndex.Save(); + return assetsList; + } + + private static bool HasValidBundleSignature(string filePath) + { + try { - if (!currentHashes.ContainsKey(cachedPath)) - cache.RemoveFile(cachedPath); - } + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + if (stream.Length < 8) + return false; + + byte[] header = new byte[8]; + if (stream.Read(header, 0, 8) != 8) + return false; - foreach (string invalidPath in new List(cache.InvalidFiles.Keys)) + if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && + header[4] == 'y' && header[5] == 'F' && header[6] == 'S') + return true; + + if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && + header[4] == 'y' && header[5] == 'R' && header[6] == 'a' && header[7] == 'w') + return true; + + if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && + header[4] == 'y' && header[5] == 'W' && header[6] == 'e' && header[7] == 'b') + return true; + + return false; + } + } + catch { - if (!currentHashes.ContainsKey(invalidPath)) - cache.InvalidFiles.Remove(invalidPath); + return false; } - - cache.CombinedHash = CarCache.ComputeCombinedHash(currentHashes); - return assetsList; } - private static BundleLoadResult LoadBundle(FileInfo file, string signature) + private static BundleLoadResult LoadBundle(FileInfo file) { try { - if (!CarCache.HasValidBundleSignature(file.FullName)) + if (!HasValidBundleSignature(file.FullName)) return new BundleLoadResult { FilePath = file.FullName, - Signature = signature, Error = new InvalidDataException("File is not a valid Unity AssetBundle (missing UnityFS/UnityRaw/UnityWeb header)") }; Assets assets = Assets.FromUnsafePath(file.FullName); if (assets == null) - return new BundleLoadResult { FilePath = file.FullName, Signature = signature }; + return new BundleLoadResult { FilePath = file.FullName }; return new BundleLoadResult { FilePath = file.FullName, - Signature = signature, Bundle = assets.Bundle as AssetBundle }; } catch (Exception ex) { - return new BundleLoadResult { FilePath = file.FullName, Signature = signature, Error = ex }; + return new BundleLoadResult { FilePath = file.FullName, Error = ex }; } } private struct BundleLoadResult { public string FilePath; - public string Signature; public AssetBundle Bundle; public Exception Error; } diff --git a/Distance.CustomCar/Data/Car/CarCache.cs b/Distance.CustomCar/Data/Car/CarCache.cs deleted file mode 100644 index 3d19fd7..0000000 --- a/Distance.CustomCar/Data/Car/CarCache.cs +++ /dev/null @@ -1,248 +0,0 @@ -using JsonFx.Json; -using JsonFx.Serialization; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Distance.CustomCar.Data.Car -{ - public class CarCache - { - private static readonly string CachePath; - - static CarCache() - { - string modDir = Path.GetDirectoryName(typeof(CarCache).Assembly.Location); - string cacheDir = Path.Combine(modDir, "Settings"); - CachePath = Path.Combine(cacheDir, "car_cache.json"); - } - - public int Version { get; set; } = 1; - public string CombinedHash { get; set; } = string.Empty; - public Dictionary Files { get; set; } = new Dictionary(); - public Dictionary InvalidFiles { get; set; } = new Dictionary(); - - public static string GetFileSignature(string filePath) - { - if (!File.Exists(filePath)) - return string.Empty; - - var info = new FileInfo(filePath); - return $"{info.LastWriteTimeUtc.Ticks}|{info.Length}"; - } - - public static string ComputeCombinedHash(Dictionary fileSignatures) - { - var ordered = fileSignatures.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase); - var combined = new StringBuilder(); - foreach (var kv in ordered) - { - combined.Append(kv.Key); - combined.Append('='); - combined.Append(kv.Value); - combined.Append('|'); - } - return combined.ToString(); - } - - public static bool HasValidBundleSignature(string filePath) - { - try - { - using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - if (stream.Length < 8) - return false; - - byte[] header = new byte[8]; - if (stream.Read(header, 0, 8) != 8) - return false; - - if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && - header[4] == 'y' && header[5] == 'F' && header[6] == 'S') - return true; - - if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && - header[4] == 'y' && header[5] == 'R' && header[6] == 'a' && header[7] == 'w') - return true; - - if (header[0] == 'U' && header[1] == 'n' && header[2] == 'i' && header[3] == 't' && - header[4] == 'y' && header[5] == 'W' && header[6] == 'e' && header[7] == 'b') - return true; - - return false; - } - } - catch - { - return false; - } - } - - public bool IsFileInvalid(string filePath, string currentSignature) - { - if (string.IsNullOrEmpty(currentSignature)) - return true; - - if (!InvalidFiles.TryGetValue(filePath, out string cachedSignature)) - return false; - - return cachedSignature == currentSignature; - } - - public void MarkFileInvalid(string filePath, string signature) - { - InvalidFiles[filePath] = signature; - } - - public void Load() - { - try - { - if (!File.Exists(CachePath)) - { - Mod.Log.LogInfo($"No car cache file at {CachePath}"); - return; - } - - string json = File.ReadAllText(CachePath); - var reader = new JsonReader(); - var data = reader.Read>(json); - - if (data == null || data.Count == 0) - return; - - if (data.TryGetValue("Version", out object versionVal) && versionVal != null) - Version = Convert.ToInt32(versionVal); - - if (data.TryGetValue("CombinedHash", out object combinedHashVal) && combinedHashVal != null) - CombinedHash = combinedHashVal.ToString(); - - if (data.TryGetValue("Files", out object filesVal) && filesVal is Dictionary filesDict) - { - foreach (var kv in filesDict) - { - if (kv.Value is Dictionary entryDict) - { - var entry = new CarCacheEntry(); - if (entryDict.TryGetValue("Hash", out object hashVal) && hashVal != null) - entry.Hash = hashVal.ToString(); - if (entryDict.TryGetValue("PrefabNames", out object prefabsVal) && prefabsVal is List prefabList) - { - entry.PrefabNames = prefabList.ConvertAll(x => x?.ToString() ?? string.Empty).ToArray(); - } - if (entryDict.TryGetValue("LastModified", out object modVal) && modVal != null) - { - DateTime parsed; - DateTime.TryParse(modVal.ToString(), out parsed); - entry.LastModified = parsed; - } - Files[kv.Key] = entry; - } - } - } - - if (data.TryGetValue("InvalidFiles", out object invalidVal) && invalidVal is Dictionary invalidDict) - { - foreach (var kv in invalidDict) - { - if (kv.Value != null) - InvalidFiles[kv.Key] = kv.Value.ToString(); - } - } - } - catch (Exception ex) - { - Mod.Log.LogWarning($"Failed to load car cache: {ex.Message}"); - Files.Clear(); - InvalidFiles.Clear(); - } - } - - public void Save() - { - try - { - var filesDict = new Dictionary(); - foreach (var kv in Files) - { - var entryDict = new Dictionary - { - ["Hash"] = kv.Value.Hash, - ["PrefabNames"] = new List(kv.Value.PrefabNames ?? new string[0]), - ["LastModified"] = kv.Value.LastModified.ToString("O") - }; - filesDict[kv.Key] = entryDict; - } - - var invalidDict = new Dictionary(); - foreach (var kv in InvalidFiles) - invalidDict[kv.Key] = kv.Value; - - var data = new Dictionary - { - ["Version"] = Version, - ["CombinedHash"] = CombinedHash, - ["Files"] = filesDict, - ["InvalidFiles"] = invalidDict - }; - - string cacheDir = Path.GetDirectoryName(CachePath); - if (!Directory.Exists(cacheDir)) - Directory.CreateDirectory(cacheDir); - - var writer = new JsonWriter(new DataWriterSettings { PrettyPrint = true }); - string json = writer.Write(data); - File.WriteAllText(CachePath, json); - } - catch (Exception ex) - { - Mod.Log.LogWarning($"Failed to save car cache: {ex.Message}"); - } - } - - public bool HasFileChanged(string filePath, out string currentSignature) - { - currentSignature = GetFileSignature(filePath); - - if (string.IsNullOrEmpty(currentSignature)) - return true; - - if (!Files.TryGetValue(filePath, out CarCacheEntry entry)) - return true; - - return entry.Hash != currentSignature; - } - - public void UpdateFile(string filePath, string signature, string[] prefabNames) - { - Files[filePath] = new CarCacheEntry - { - Hash = signature, - PrefabNames = prefabNames ?? new string[0], - LastModified = DateTime.UtcNow - }; - } - - public void RemoveFile(string filePath) - { - Files.Remove(filePath); - } - - public string[] GetPrefabNames(string filePath) - { - if (Files.TryGetValue(filePath, out CarCacheEntry entry) && entry.PrefabNames != null) - return entry.PrefabNames; - return new string[0]; - } - } - - public class CarCacheEntry - { - public string Hash { get; set; } = string.Empty; - public string[] PrefabNames { get; set; } = new string[0]; - public DateTime LastModified { get; set; } - } -} diff --git a/Distance.CustomCar/Data/Car/PrefabIndex.cs b/Distance.CustomCar/Data/Car/PrefabIndex.cs new file mode 100644 index 0000000..680c655 --- /dev/null +++ b/Distance.CustomCar/Data/Car/PrefabIndex.cs @@ -0,0 +1,139 @@ +using JsonFx.Json; +using JsonFx.Serialization; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Distance.CustomCar.Data.Car +{ + public class PrefabIndex + { + private static readonly string IndexPath; + + static PrefabIndex() + { + string modDir = Path.GetDirectoryName(typeof(PrefabIndex).Assembly.Location); + IndexPath = Path.Combine(modDir, Path.Combine("Settings", "prefab_index.json")); + } + + public Dictionary Files { get; set; } = new Dictionary(); + + public string[] GetPrefabNames(string filePath) + { + if (Files.TryGetValue(filePath, out PrefabIndexEntry entry) && entry.PrefabNames != null) + return entry.PrefabNames; + return null; + } + + public void SetPrefabNames(string filePath, string[] prefabNames) + { + Files[filePath] = new PrefabIndexEntry + { + LastWriteTime = GetLastWriteSafe(filePath), + PrefabNames = prefabNames ?? new string[0] + }; + } + + public bool IsUpToDate(string filePath) + { + if (!Files.TryGetValue(filePath, out PrefabIndexEntry entry)) + return false; + + return entry.LastWriteTime == GetLastWriteSafe(filePath); + } + + public void RemoveStaleEntries(HashSet validPaths) + { + List toRemove = new List(); + foreach (string path in Files.Keys) + { + if (!validPaths.Contains(path)) + toRemove.Add(path); + } + foreach (string path in toRemove) + Files.Remove(path); + } + + private static long GetLastWriteSafe(string path) + { + try + { + return File.GetLastWriteTimeUtc(path).Ticks; + } + catch + { + return 0; + } + } + + public void Load() + { + try + { + if (!File.Exists(IndexPath)) + return; + + string json = File.ReadAllText(IndexPath); + var reader = new JsonReader(); + var data = reader.Read>(json); + if (data == null) + return; + + Files.Clear(); + foreach (var kv in data) + { + if (kv.Value is Dictionary entry) + { + var prefabIndexEntry = new PrefabIndexEntry(); + if (entry.TryGetValue("LastWriteTime", out object lwt)) + prefabIndexEntry.LastWriteTime = Convert.ToInt64(lwt); + if (entry.TryGetValue("PrefabNames", out object names) && names is List nameList) + { + prefabIndexEntry.PrefabNames = nameList.ConvertAll(x => x?.ToString() ?? string.Empty).ToArray(); + } + Files[kv.Key] = prefabIndexEntry; + } + } + } + catch (Exception ex) + { + Mod.Log.LogWarning($"Failed to load prefab index: {ex.Message}"); + Files.Clear(); + } + } + + public void Save() + { + try + { + var data = new Dictionary(); + foreach (var kv in Files) + { + var entry = new Dictionary + { + ["LastWriteTime"] = kv.Value.LastWriteTime, + ["PrefabNames"] = new List(kv.Value.PrefabNames ?? new string[0]) + }; + data[kv.Key] = entry; + } + + string dir = Path.GetDirectoryName(IndexPath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + var writer = new JsonWriter(new DataWriterSettings { PrettyPrint = true }); + File.WriteAllText(IndexPath, writer.Write(data)); + } + catch (Exception ex) + { + Mod.Log.LogWarning($"Failed to save prefab index: {ex.Message}"); + } + } + } + + public class PrefabIndexEntry + { + public long LastWriteTime { get; set; } + public string[] PrefabNames { get; set; } = new string[0]; + } +} diff --git a/Distance.CustomCar/Distance.CustomCar.csproj b/Distance.CustomCar/Distance.CustomCar.csproj index bfd7096..0ccf188 100644 --- a/Distance.CustomCar/Distance.CustomCar.csproj +++ b/Distance.CustomCar/Distance.CustomCar.csproj @@ -61,8 +61,8 @@ - + From e841253a194f367a3633c1f6e254093f80ff5c1c Mon Sep 17 00:00:00 2001 From: "hector.munoz" Date: Wed, 8 Jul 2026 11:57:50 -0600 Subject: [PATCH 4/5] Bump min batch size from 2 to 4 --- Distance.CustomCar/Data/Car/CarBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Distance.CustomCar/Data/Car/CarBuilder.cs b/Distance.CustomCar/Data/Car/CarBuilder.cs index 95e7fc9..6cd2599 100644 --- a/Distance.CustomCar/Data/Car/CarBuilder.cs +++ b/Distance.CustomCar/Data/Car/CarBuilder.cs @@ -151,7 +151,7 @@ private Dictionary LoadAssetsBundles() List validFiles = rawFiles; HashSet seenPaths = new HashSet(); - int batchSize = Math.Max(2, Environment.ProcessorCount); + int batchSize = Math.Max(4, Environment.ProcessorCount); for (int batchStart = 0; batchStart < validFiles.Count; batchStart += batchSize) { int batchEnd = Math.Min(batchStart + batchSize, validFiles.Count); From 6d8afa6d603bf2906e0703eb062662abdd2e64cc Mon Sep 17 00:00:00 2001 From: "hector.munoz" Date: Wed, 8 Jul 2026 12:08:54 -0600 Subject: [PATCH 5/5] Pre-filter non-bundles, clean up debug logs --- Distance.CustomCar/Data/Car/CarBuilder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Distance.CustomCar/Data/Car/CarBuilder.cs b/Distance.CustomCar/Data/Car/CarBuilder.cs index 6cd2599..297356c 100644 --- a/Distance.CustomCar/Data/Car/CarBuilder.cs +++ b/Distance.CustomCar/Data/Car/CarBuilder.cs @@ -144,7 +144,7 @@ private Dictionary LoadAssetsBundles() List rawFiles = new List(); foreach (FileInfo f in profileDirectory.GetFiles("*", SearchOption.AllDirectories).Concat(globalCarsDirectory.GetFiles("*", SearchOption.AllDirectories)).OrderBy(x => x.Name)) { - if (f.Extension == "") + if (f.Extension == "" && HasValidBundleSignature(f.FullName)) rawFiles.Add(f); } @@ -152,9 +152,11 @@ private Dictionary LoadAssetsBundles() HashSet seenPaths = new HashSet(); int batchSize = Math.Max(4, Environment.ProcessorCount); + int totalBatches = (validFiles.Count + batchSize - 1) / batchSize; for (int batchStart = 0; batchStart < validFiles.Count; batchStart += batchSize) { int batchEnd = Math.Min(batchStart + batchSize, validFiles.Count); + Mod.Log.LogInfo($"Batch {batchStart / batchSize + 1}/{totalBatches} ({batchEnd - batchStart} files)"); BundleLoadResult[] batchResults = new BundleLoadResult[batchEnd - batchStart]; int batchCount = batchResults.Length; int loaded = 0; @@ -245,6 +247,7 @@ private Dictionary LoadAssetsBundles() } } + Mod.Log.LogInfo($"{assetsList.Count} prefab(s) loaded from {seenPaths.Count} file(s)"); prefabIndex.RemoveStaleEntries(seenPaths); prefabIndex.Save(); return assetsList;