Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Fast AssetBundle Loader v1.0.3
patchers/FastAssetBundleLoader.dll
Decompiled 3 weeks agousing System; using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using FastAssetBundleLoader.Configuration; using FastAssetBundleLoader.Helpers; using FastAssetBundleLoader.Managers; using HarmonyLib; using Microsoft.CodeAnalysis; using Mono.Cecil; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Valheim asset bundle cache patcher that rewrites slow bundles into reusable cached bundles.")] [assembly: AssemblyFileVersion("1.0.3.0")] [assembly: AssemblyInformationalVersion("1.0.3+dfdb360c9f4fb1b2d6af3a130228a77f8742d737")] [assembly: AssemblyProduct("FastAssetBundleLoader")] [assembly: AssemblyTitle("FastAssetBundleLoader")] [assembly: AssemblyVersion("1.0.3.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace FastAssetBundleLoader { public static class FastAssetBundleLoaderPatcher { internal static Harmony Harmony { get; } = new Harmony("FastAssetBundleLoader"); public static IEnumerable<string> TargetDLLs { get; } = Array.Empty<string>(); public static void Finish() { Harmony.PatchAll(typeof(FastAssetBundleLoaderPatcher).Assembly); } public static void Patch(AssemblyDefinition _) { } } [HarmonyPatch] internal static class RuntimeHooks { private static int s_InitializationStarted; [ThreadStatic] private static bool s_IsLoadingCachedBundle; internal static ManualLogSource Logger { get; private set; } internal static AssetBundleCacheManager CacheManager { get; private set; } [HarmonyPatch(typeof(Chainloader), "Initialize")] [HarmonyPostfix] private static void ChainloaderInitialized() { if (Interlocked.Exchange(ref s_InitializationStarted, 1) != 0) { return; } try { Logger = Logger.CreateLogSource("FastAssetBundleLoader"); PatcherSettings.Initialize(); if (!PatcherSettings.AssetBundleCacheEnabled) { Logger.LogInfo((object)"Asset bundle cache is disabled via config."); return; } AsyncHelper.InitializeMainThreadContext(); string cachePath = PatcherSettings.CachePath; Directory.CreateDirectory(cachePath); MetadataStore metadataStore = new MetadataStore(Path.Combine(cachePath, "metadata.tsv"), cachePath, PatcherSettings.CacheRetentionDays); CacheManager = new AssetBundleCacheManager(cachePath, metadataStore, PatcherSettings.MinimumFreeDiskSpaceGb); InstallAssetBundleHooks(); Logger.LogInfo((object)("Asset bundle cache ready at \"" + cachePath + "\".")); } catch (Exception arg) { try { ManualLogSource logger = Logger; if (logger != null) { logger.LogError((object)$"Failed to initialize asset bundle cache.{Environment.NewLine}{arg}"); } } catch { } } } private static void InstallAssetBundleHooks() { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Expected O, but got Unknown //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Expected O, but got Unknown //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Expected O, but got Unknown //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Expected O, but got Unknown //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Expected O, but got Unknown Type typeFromHandle = typeof(RuntimeHooks); Harmony harmony = FastAssetBundleLoaderPatcher.Harmony; BindingFlags all = AccessTools.all; Type typeFromHandle2 = typeof(AssetBundle); HarmonyMethod val = new HarmonyMethod(typeFromHandle.GetMethod("RedirectLoadFromFile", all)); HarmonyMethod val2 = new HarmonyMethod(typeFromHandle.GetMethod("RedirectLoadFromFileWithOffset", all)); string[] array = new string[2] { "LoadFromFile", "LoadFromFileAsync" }; foreach (string text in array) { harmony.Patch((MethodBase)AccessTools.Method(typeFromHandle2, text, new Type[1] { typeof(string) }, (Type[])null), val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeFromHandle2, text, new Type[2] { typeof(string), typeof(uint) }, (Type[])null), val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeFromHandle2, text, new Type[3] { typeof(string), typeof(uint), typeof(ulong) }, (Type[])null), val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } harmony.Patch((MethodBase)AccessTools.Method(typeFromHandle2, "LoadFromStreamInternal", (Type[])null, (Type[])null), new HarmonyMethod(typeFromHandle.GetMethod("RedirectLoadFromStream", all)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeFromHandle2, "LoadFromStreamAsyncInternal", (Type[])null, (Type[])null), new HarmonyMethod(typeFromHandle.GetMethod("RedirectLoadFromStreamAsync", all)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeFromHandle2, "LoadFromMemory_Internal", (Type[])null, (Type[])null), new HarmonyMethod(typeFromHandle.GetMethod("RedirectLoadFromMemory", all)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeFromHandle2, "LoadFromMemoryAsync_Internal", (Type[])null, (Type[])null), new HarmonyMethod(typeFromHandle.GetMethod("RedirectLoadFromMemoryAsync", all)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void RedirectLoadFromFileWithOffset(ref string path, ulong offset) { if (offset == 0L) { RedirectLoadFromFile(ref path); } } private static void RedirectLoadFromFile(ref string path) { if (s_IsLoadingCachedBundle || string.IsNullOrWhiteSpace(path)) { return; } try { using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1048576, FileOptions.SequentialScan); if (TryResolveCachedPath(stream, out string cachedPath)) { path = cachedPath; } } catch (Exception arg) { Logger.LogError((object)$"Failed to inspect asset bundle \"{path}\".{Environment.NewLine}{arg}"); } } private static bool RedirectLoadFromStream(Stream stream, uint crc, ref AssetBundle? __result) { if (crc != 0) { return true; } if (TryResolveCachedPath(stream, out string cachedPath)) { try { AssetBundle val = LoadCachedBundle(cachedPath); if ((Object)(object)val != (Object)null) { __result = val; return false; } } catch (Exception arg) { Logger.LogError((object)$"Failed to load cached asset bundle \"{cachedPath}\".{Environment.NewLine}{arg}"); } } return true; } private static bool RedirectLoadFromStreamAsync(Stream stream, uint crc, ref AssetBundleCreateRequest? __result) { if (crc != 0) { return true; } if (TryResolveCachedPath(stream, out string cachedPath)) { try { AssetBundleCreateRequest val = LoadCachedBundleAsync(cachedPath); if (val != null) { __result = val; return false; } } catch (Exception arg) { Logger.LogError((object)$"Failed to load cached asset bundle \"{cachedPath}\" asynchronously.{Environment.NewLine}{arg}"); } } return true; } private static bool RedirectLoadFromMemory(byte[] binary, uint crc, ref AssetBundle? __result) { if (crc != 0) { return true; } if (TryResolveCachedPath(binary, out string cachedPath)) { try { AssetBundle val = LoadCachedBundle(cachedPath); if ((Object)(object)val != (Object)null) { __result = val; return false; } } catch (Exception arg) { Logger.LogError((object)$"Failed to load cached asset bundle \"{cachedPath}\".{Environment.NewLine}{arg}"); } } return true; } private static bool RedirectLoadFromMemoryAsync(byte[] binary, uint crc, ref AssetBundleCreateRequest? __result) { if (crc != 0) { return true; } if (TryResolveCachedPath(binary, out string cachedPath)) { try { AssetBundleCreateRequest val = LoadCachedBundleAsync(cachedPath); if (val != null) { __result = val; return false; } } catch (Exception arg) { Logger.LogError((object)$"Failed to load cached asset bundle \"{cachedPath}\" asynchronously.{Environment.NewLine}{arg}"); } } return true; } private static AssetBundle? LoadCachedBundle(string cachedPath) { s_IsLoadingCachedBundle = true; try { return AssetBundle.LoadFromFile(cachedPath); } finally { s_IsLoadingCachedBundle = false; } } private static AssetBundleCreateRequest? LoadCachedBundleAsync(string cachedPath) { s_IsLoadingCachedBundle = true; try { return AssetBundle.LoadFromFileAsync(cachedPath); } finally { s_IsLoadingCachedBundle = false; } } private static bool TryResolveCachedPath(Stream stream, out string cachedPath) { cachedPath = string.Empty; long position; try { if (!stream.CanSeek) { return false; } position = stream.Position; if (position != 0L) { return false; } } catch (Exception arg) { Logger.LogError((object)$"Failed to inspect asset bundle stream position.{Environment.NewLine}{arg}"); return false; } bool result; try { result = CacheManager.TryUseCachedBundle(stream, out cachedPath); } catch (Exception arg2) { Logger.LogError((object)$"Failed to process asset bundle stream.{Environment.NewLine}{arg2}"); result = false; } try { stream.Position = position; return result; } catch (Exception arg3) { Logger.LogError((object)$"Failed to restore asset bundle stream position.{Environment.NewLine}{arg3}"); return result; } } private static bool TryResolveCachedPath(byte[] binary, out string cachedPath) { cachedPath = string.Empty; try { return CacheManager.TryUseCachedBundle(binary, out cachedPath); } catch (Exception arg) { Logger.LogError((object)$"Failed to process asset bundle bytes.{Environment.NewLine}{arg}"); return false; } } } } namespace FastAssetBundleLoader.Managers { internal sealed class AssetBundleCacheManager { internal enum CacheLookupResult { Miss, Cached, Skip } private readonly struct PendingBundle { public string SourcePath { get; } public string OriginalHash { get; } public bool DeleteSourceAfterCaching { get; } public PendingBundle(string sourcePath, string originalHash, bool deleteSourceAfterCaching) { SourcePath = sourcePath; OriginalHash = originalHash; DeleteSourceAfterCaching = deleteSourceAfterCaching; } } private readonly string m_CachePath; private readonly MetadataStore m_MetadataStore; private readonly int m_MinimumFreeDiskSpaceGb; private readonly string m_TempPath; private readonly object m_QueueLock = new object(); private readonly HashSet<string> m_InFlightHashes = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private readonly Queue<PendingBundle> m_PendingBundles = new Queue<PendingBundle>(); private bool m_IsProcessingQueue; public AssetBundleCacheManager(string cachePath, MetadataStore metadataStore, int minimumFreeDiskSpaceGb) { m_CachePath = cachePath; m_MetadataStore = metadataStore; m_MinimumFreeDiskSpaceGb = minimumFreeDiskSpaceGb; m_TempPath = Path.Combine(cachePath, "temp"); Directory.CreateDirectory(m_CachePath); Directory.CreateDirectory(m_TempPath); DeleteTemporaryFiles(); } public bool TryUseCachedBundle(Stream stream, [NotNullWhen(true)] out string cachedPath) { cachedPath = string.Empty; if (!BundleHelper.IsRecompressibleUnityFs(stream)) { return false; } string originalHash = HashingHelper.ComputeHash(stream); return TryUseKnownHash(stream, originalHash, out cachedPath); } public bool TryUseCachedBundle(byte[] binary, [NotNullWhen(true)] out string cachedPath) { using MemoryStream stream = new MemoryStream(binary, writable: false); return TryUseCachedBundle(stream, out cachedPath); } private bool TryUseKnownHash(Stream stream, string originalHash, [NotNullWhen(true)] out string cachedPath) { cachedPath = string.Empty; string cachedPath2; switch (TryResolveExistingCache(originalHash, out cachedPath2)) { case CacheLookupResult.Cached: cachedPath = cachedPath2; return true; case CacheLookupResult.Skip: return false; default: { lock (m_QueueLock) { if (!m_InFlightHashes.Add(originalHash)) { return false; } } string text = null; bool flag = false; try { if (!DriveHelper.HasDriveSpaceOnPath(m_CachePath, m_MinimumFreeDiskSpaceGb)) { RuntimeHooks.Logger.LogWarning((object)$"Skipping cache creation because free space is below {m_MinimumFreeDiskSpaceGb} GB."); return false; } PendingBundle bundle; if (stream is FileStream fileStream) { bundle = new PendingBundle(fileStream.Name, originalHash, deleteSourceAfterCaching: false); } else { text = Path.Combine(m_TempPath, $"{Guid.NewGuid():N}.assetbundle"); using (FileStream destination = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1048576, FileOptions.SequentialScan)) { stream.Seek(0L, SeekOrigin.Begin); stream.CopyTo(destination, 1048576); } bundle = new PendingBundle(text, originalHash, deleteSourceAfterCaching: true); } EnqueueReservedBundle(bundle); flag = true; return false; } finally { if (!flag) { lock (m_QueueLock) { m_InFlightHashes.Remove(originalHash); } if (text != null) { FileHelper.TryDeleteFile(text, out Exception _); } } } } } } private CacheLookupResult TryResolveExistingCache(string originalHash, [NotNullWhen(true)] out string? cachedPath) { cachedPath = null; if (!m_MetadataStore.TryGetAndTouch(originalHash, out var shouldSkipCaching)) { return CacheLookupResult.Miss; } if (shouldSkipCaching) { return CacheLookupResult.Skip; } string text = originalHash + ".assetbundle"; string text2 = Path.Combine(m_CachePath, text); if (!File.Exists(text2)) { RuntimeHooks.Logger.LogWarning((object)("Cached bundle is missing: \"" + text2 + "\"")); m_MetadataStore.Delete(originalHash); return CacheLookupResult.Miss; } bool flag; using (FileStream stream = new FileStream(text2, FileMode.Open, FileAccess.Read, FileShare.Read)) { flag = BundleHelper.IsRecompressibleUnityFs(stream); } if (!flag) { RuntimeHooks.Logger.LogWarning((object)("Cached bundle is invalid: \"" + text2 + "\"")); m_MetadataStore.Delete(originalHash); FileHelper.TryDeleteFile(text2, out Exception _); return CacheLookupResult.Miss; } RuntimeHooks.Logger.LogDebug((object)("Using cached asset bundle \"" + text + "\" for hash \"" + originalHash + "\".")); cachedPath = text2; return CacheLookupResult.Cached; } private void EnqueueReservedBundle(PendingBundle bundle) { RuntimeHooks.Logger.LogInfo((object)("Queueing asset bundle cache creation for \"" + Path.GetFileName(bundle.SourcePath) + "\" (hash \"" + bundle.OriginalHash + "\").")); lock (m_QueueLock) { m_PendingBundles.Enqueue(bundle); if (m_IsProcessingQueue) { return; } m_IsProcessingQueue = true; try { AsyncHelper.Schedule(ProcessQueueAsync); } catch { m_IsProcessingQueue = false; m_PendingBundles.Dequeue(); throw; } } } private async Task ProcessQueueAsync() { while (true) { PendingBundle bundle; lock (m_QueueLock) { if (m_PendingBundles.Count == 0) { m_IsProcessingQueue = false; break; } bundle = m_PendingBundles.Dequeue(); } await CacheBundleAsync(bundle); } } private async Task CacheBundleAsync(PendingBundle bundle) { string outputFileName = bundle.OriginalHash + ".assetbundle"; string outputPath = Path.Combine(m_CachePath, outputFileName); Exception exception; try { await FileHelper.WaitUntilReadableAsync(bundle.SourcePath, 5); if (AreSamePath(bundle.SourcePath, outputPath)) { RuntimeHooks.Logger.LogWarning((object)("Skipping cache creation because source and output paths are identical: \"" + outputPath + "\"")); return; } if (!DriveHelper.HasDriveSpaceOnPath(m_CachePath, m_MinimumFreeDiskSpaceGb)) { RuntimeHooks.Logger.LogWarning((object)$"Skipping cache creation because free space is below {m_MinimumFreeDiskSpaceGb} GB."); return; } FileHelper.TryDeleteFile(outputPath, out exception); await AsyncHelper.SwitchToMainThread(); AssetBundleRecompressOperation recompressOperation = AssetBundle.RecompressAssetBundleAsync(bundle.SourcePath, outputPath, BuildCompression.LZ4Runtime, 0u, (ThreadPriority)2); await recompressOperation.WaitCompletionAsync<AssetBundleRecompressOperation>(); AssetBundleLoadResult result = recompressOperation.result; bool success = recompressOperation.success; string details = recompressOperation.humanReadableResult; await AsyncHelper.SwitchToThreadPool(); if (!success || (int)result != 0 || !File.Exists(outputPath)) { RuntimeHooks.Logger.LogWarning((object)$"Failed to cache asset bundle \"{bundle.SourcePath}\". Result: {result}, {details}"); FileHelper.TryDeleteFile(outputPath, out exception); } else if (string.Equals(HashingHelper.ComputeHash(outputPath), bundle.OriginalHash, StringComparison.OrdinalIgnoreCase)) { RuntimeHooks.Logger.LogInfo((object)("Asset bundle \"" + Path.GetFileName(bundle.SourcePath) + "\" (hash \"" + bundle.OriginalHash + "\") did not benefit from recompression. Marking it to skip future caching.")); m_MetadataStore.SaveSkipped(bundle.OriginalHash); FileHelper.TryDeleteFile(outputPath, out exception); } else { m_MetadataStore.SaveCached(bundle.OriginalHash); RuntimeHooks.Logger.LogInfo((object)("Cached asset bundle \"" + Path.GetFileName(bundle.SourcePath) + "\" (hash \"" + bundle.OriginalHash + "\") as \"" + outputFileName + "\".")); } } catch (Exception arg) { RuntimeHooks.Logger.LogError((object)$"Failed to cache asset bundle \"{bundle.SourcePath}\".{Environment.NewLine}{arg}"); FileHelper.TryDeleteFile(outputPath, out exception); } finally { lock (m_QueueLock) { m_InFlightHashes.Remove(bundle.OriginalHash); } if (bundle.DeleteSourceAfterCaching) { FileHelper.TryDeleteFile(bundle.SourcePath, out exception); } } } private void DeleteTemporaryFiles() { foreach (string item in Directory.EnumerateFiles(m_TempPath, "*.assetbundle", SearchOption.TopDirectoryOnly)) { FileHelper.TryDeleteFile(item, out Exception _); } } private static bool AreSamePath(string firstPath, string secondPath) { StringComparison comparisonType = ((Path.DirectorySeparatorChar == '\\' || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); return string.Equals(Path.GetFullPath(firstPath), Path.GetFullPath(secondPath), comparisonType); } } internal sealed class MetadataStore { private sealed class MetadataEntry { public bool ShouldSkipCaching { get; set; } public DateTime LastAccessTimeUtc { get; set; } } private const string CachedBundleFileExtension = ".assetbundle"; private const int FlushDelayMs = 60000; private readonly string m_CachePath; private readonly Timer m_FlushTimer; private readonly string m_MetadataFilePath; private readonly int m_RetentionDays; private readonly object m_Lock = new object(); private readonly Dictionary<string, MetadataEntry> m_Metadata = new Dictionary<string, MetadataEntry>(StringComparer.OrdinalIgnoreCase); private bool m_IsDirty; public MetadataStore(string metadataFilePath, string cachePath, int retentionDays) { m_MetadataFilePath = metadataFilePath; m_CachePath = cachePath; m_RetentionDays = retentionDays; m_FlushTimer = new Timer(OnFlushTimer, null, -1, -1); Load(); CleanupStaleEntries(); AppDomain.CurrentDomain.ProcessExit += OnProcessExit; } public bool TryGetAndTouch(string originalHash, out bool shouldSkipCaching) { lock (m_Lock) { if (!m_Metadata.TryGetValue(originalHash, out MetadataEntry value)) { shouldSkipCaching = false; return false; } value.LastAccessTimeUtc = DateTime.UtcNow; shouldSkipCaching = value.ShouldSkipCaching; MarkDirtyLocked(); return true; } } public void SaveCached(string originalHash) { Save(originalHash, shouldSkipCaching: false); } public void SaveSkipped(string originalHash) { Save(originalHash, shouldSkipCaching: true); } private void Save(string originalHash, bool shouldSkipCaching) { lock (m_Lock) { m_Metadata[originalHash] = new MetadataEntry { LastAccessTimeUtc = DateTime.UtcNow, ShouldSkipCaching = shouldSkipCaching }; MarkDirtyLocked(); } } public void Delete(string originalHash) { lock (m_Lock) { if (m_Metadata.Remove(originalHash)) { MarkDirtyLocked(); } } } public void Flush() { lock (m_Lock) { FlushDirtyLocked(); } } private void Load() { if (!File.Exists(m_MetadataFilePath)) { return; } bool flag = false; string[] array = File.ReadAllLines(m_MetadataFilePath); foreach (string text in array) { string originalHash; MetadataEntry metadata; if (string.IsNullOrWhiteSpace(text)) { flag = true; } else if (!TryParseLine(text, out originalHash, out metadata)) { RuntimeHooks.Logger.LogWarning((object)("Ignoring invalid metadata line: " + text)); flag = true; } else { m_Metadata[originalHash] = metadata; } } if (!flag) { return; } lock (m_Lock) { MarkDirtyLocked(); } } private void CleanupStaleEntries() { bool flag = false; lock (m_Lock) { if (m_RetentionDays > 0) { DateTime utcNow = DateTime.UtcNow; long num = utcNow.Ticks / 864000000000L; DateTime cutoff = ((m_RetentionDays > num) ? DateTime.MinValue : new DateTime(utcNow.Ticks - m_RetentionDays * 864000000000L, DateTimeKind.Utc)); KeyValuePair<string, MetadataEntry>[] array = m_Metadata.Where<KeyValuePair<string, MetadataEntry>>((KeyValuePair<string, MetadataEntry> pair) => pair.Value.LastAccessTimeUtc < cutoff).ToArray(); for (int num2 = 0; num2 < array.Length; num2++) { KeyValuePair<string, MetadataEntry> keyValuePair = array[num2]; m_Metadata.Remove(keyValuePair.Key); flag = true; if (!keyValuePair.Value.ShouldSkipCaching) { string cachedBundleFileName = GetCachedBundleFileName(keyValuePair.Key); DeleteCacheFile(Path.Combine(m_CachePath, cachedBundleFileName), "Deleting expired cache " + cachedBundleFileName); } } } HashSet<string> hashSet = new HashSet<string>(from pair in m_Metadata where !pair.Value.ShouldSkipCaching select GetCachedBundleFileName(pair.Key), StringComparer.OrdinalIgnoreCase); foreach (string item in Directory.EnumerateFiles(m_CachePath, "*.assetbundle", SearchOption.TopDirectoryOnly)) { string fileName = Path.GetFileName(item); if (IsOwnedCachedBundleFileName(fileName) && !hashSet.Contains(fileName)) { flag = true; DeleteCacheFile(item, "Deleting untracked cache " + fileName); } } if (flag) { WriteMetadataFileLocked(); m_IsDirty = false; } } } private void MarkDirtyLocked() { if (!m_IsDirty) { m_IsDirty = true; m_FlushTimer.Change(60000, -1); } } private void OnFlushTimer(object? state) { try { Flush(); } catch (Exception arg) { lock (m_Lock) { if (m_IsDirty) { m_FlushTimer.Change(60000, -1); } } RuntimeHooks.Logger.LogWarning((object)$"Failed to flush asset bundle metadata. Retrying in {60} seconds.{Environment.NewLine}{arg}"); } } private void FlushDirtyLocked() { if (m_IsDirty) { WriteMetadataFileLocked(); m_IsDirty = false; } } private void WriteMetadataFileLocked() { string[] contents = (from pair in m_Metadata.OrderBy<KeyValuePair<string, MetadataEntry>, string>((KeyValuePair<string, MetadataEntry> pair) => pair.Key, StringComparer.OrdinalIgnoreCase) select SerializeLine(pair.Key, pair.Value)).ToArray(); string text = m_MetadataFilePath + ".tmp"; string directoryName = Path.GetDirectoryName(m_MetadataFilePath); if (!string.IsNullOrWhiteSpace(directoryName)) { Directory.CreateDirectory(directoryName); } FileHelper.TryDeleteFile(text, out Exception _); File.WriteAllLines(text, contents, Encoding.UTF8); if (File.Exists(m_MetadataFilePath)) { File.Replace(text, m_MetadataFilePath, null); } else { File.Move(text, m_MetadataFilePath); } } private void OnProcessExit(object? sender, EventArgs e) { try { Flush(); } catch { } } private static string SerializeLine(string originalHash, MetadataEntry metadata) { string s = (metadata.ShouldSkipCaching ? string.Empty : GetCachedBundleFileName(originalHash)); string text = Convert.ToBase64String(Encoding.UTF8.GetBytes(s)); return string.Join("\t", originalHash, metadata.LastAccessTimeUtc.Ticks.ToString(), metadata.ShouldSkipCaching ? "1" : "0", text); } private static bool TryParseLine(string line, out string originalHash, out MetadataEntry metadata) { originalHash = string.Empty; metadata = null; string[] array = line.Split('\t'); if ((array.Length != 4 && array.Length != 5) || !IsValidHash(array[0])) { return false; } if (!long.TryParse(array[1], out var result) || result < DateTime.MinValue.Ticks || result > DateTime.MaxValue.Ticks) { return false; } if (array[2] != "0" && array[2] != "1") { return false; } bool flag = array[2] == "1"; string text; try { text = Encoding.UTF8.GetString(Convert.FromBase64String(array[^1])); } catch (FormatException) { return false; } if (flag) { if (text.Length != 0) { return false; } } else if (!IsExpectedCachedBundleFileName(array[0], text)) { return false; } originalHash = array[0]; metadata = new MetadataEntry { LastAccessTimeUtc = new DateTime(result, DateTimeKind.Utc), ShouldSkipCaching = flag }; return true; } private static bool IsExpectedCachedBundleFileName(string hash, string? fileName) { if (IsValidHash(hash)) { return string.Equals(fileName, GetCachedBundleFileName(hash), StringComparison.Ordinal); } return false; } private static string GetCachedBundleFileName(string hash) { return hash + ".assetbundle"; } private static bool IsOwnedCachedBundleFileName(string fileName) { if (fileName.Length == 32 + ".assetbundle".Length && fileName.EndsWith(".assetbundle", StringComparison.Ordinal)) { return IsValidHash(fileName.Substring(0, 32)); } return false; } private static bool IsValidHash(string? hash) { if (hash == null || hash.Length != 32) { return false; } foreach (char c in hash) { if ((c < '0' || c > '9') && (c < 'A' || c > 'F') && (c < 'a' || c > 'f')) { return false; } } return true; } private static void DeleteCacheFile(string path, string logMessage) { RuntimeHooks.Logger.LogInfo((object)logMessage); if (!FileHelper.TryDeleteFile(path, out Exception exception)) { RuntimeHooks.Logger.LogWarning((object)$"Failed to delete cache file \"{path}\".{Environment.NewLine}{exception}"); } } } } namespace FastAssetBundleLoader.Helpers { internal static class AsyncHelper { [StructLayout(LayoutKind.Sequential, Size = 1)] internal readonly struct SwitchToMainThreadAwaiter : ICriticalNotifyCompletion, INotifyCompletion { private static readonly SendOrPostCallback Callback = delegate(object state) { ((Action)state)(); }; public bool IsCompleted => Thread.CurrentThread.ManagedThreadId == s_MainThreadId; public SwitchToMainThreadAwaiter GetAwaiter() { return this; } public void GetResult() { } public void OnCompleted(Action continuation) { UnsafeOnCompleted(continuation); } public void UnsafeOnCompleted(Action continuation) { s_MainThreadContext.Post(Callback, continuation); } } [StructLayout(LayoutKind.Sequential, Size = 1)] internal readonly struct SwitchToThreadPoolAwaiter : ICriticalNotifyCompletion, INotifyCompletion { private static readonly WaitCallback Callback = delegate(object state) { ((Action)state)(); }; public bool IsCompleted => false; public SwitchToThreadPoolAwaiter GetAwaiter() { return this; } public void GetResult() { } public void OnCompleted(Action continuation) { UnsafeOnCompleted(continuation); } public void UnsafeOnCompleted(Action continuation) { ThreadPool.UnsafeQueueUserWorkItem(Callback, continuation); } } private static SynchronizationContext s_MainThreadContext = null; private static int s_MainThreadId = -1; public static void InitializeMainThreadContext() { s_MainThreadContext = SynchronizationContext.Current ?? throw new InvalidOperationException("Unity SynchronizationContext is not available."); s_MainThreadId = Thread.CurrentThread.ManagedThreadId; } public static void Schedule(Func<Task> work) { Task.Run(async delegate { try { await work(); } catch (Exception ex) { RuntimeHooks.Logger.LogError((object)ex); } }); } public static SwitchToMainThreadAwaiter SwitchToMainThread() { return default(SwitchToMainThreadAwaiter); } public static SwitchToThreadPoolAwaiter SwitchToThreadPool() { return default(SwitchToThreadPoolAwaiter); } } internal static class AsyncOperationHelper { internal sealed class AsyncOperationAwaiter : ICriticalNotifyCompletion, INotifyCompletion { private AsyncOperation? m_AsyncOperation; private Action? m_Continuation; public bool IsCompleted { get { if (m_AsyncOperation != null) { return m_AsyncOperation.isDone; } return true; } } public AsyncOperationAwaiter(AsyncOperation asyncOperation) { m_AsyncOperation = asyncOperation; m_Continuation = null; } public AsyncOperationAwaiter GetAwaiter() { return this; } public void GetResult() { if (m_AsyncOperation != null) { m_AsyncOperation.completed -= OnCompleted; m_AsyncOperation = null; } m_Continuation = null; } public void OnCompleted(Action continuation) { UnsafeOnCompleted(continuation); } public void UnsafeOnCompleted(Action continuation) { if (continuation == null) { throw new ArgumentNullException("continuation"); } m_Continuation = continuation; (m_AsyncOperation ?? throw new InvalidOperationException("The operation has already completed.")).completed += OnCompleted; } private void OnCompleted(AsyncOperation asyncOperation) { asyncOperation.completed -= OnCompleted; m_AsyncOperation = null; Action? continuation = m_Continuation; m_Continuation = null; continuation?.Invoke(); } } public static AsyncOperationAwaiter WaitCompletionAsync<T>(this T asyncOperation) where T : AsyncOperation { return new AsyncOperationAwaiter((AsyncOperation)(object)asyncOperation); } } internal static class BundleHelper { private const int MinimumUnityFsFormatVersion = 6; private const int MaxCStringLength = 256; public static bool IsRecompressibleUnityFs(Stream stream) { try { stream.Seek(0L, SeekOrigin.Begin); if (!ReadSignature(stream)) { return false; } Span<byte> span = stackalloc byte[4]; if (!ReadExactly(stream, span) || BinaryPrimitives.ReadInt32BigEndian(span) < 6 || !SkipCString(stream) || !SkipCString(stream)) { return false; } Span<byte> span2 = stackalloc byte[20]; if (!ReadExactly(stream, span2)) { return false; } long num = BinaryPrimitives.ReadInt64BigEndian(span2); int num2 = BinaryPrimitives.ReadInt32BigEndian(span2.Slice(8)); int num3 = BinaryPrimitives.ReadInt32BigEndian(span2.Slice(12)); if (num < stream.Position || num2 <= 0 || num3 <= 0 || (stream.CanSeek && num > stream.Length)) { return false; } return true; } catch { return false; } } private static bool ReadSignature(Stream stream) { string text = "UnityFS"; foreach (char c in text) { if (stream.ReadByte() != c) { return false; } } return stream.ReadByte() == 0; } private static bool SkipCString(Stream stream) { for (int i = 0; i < 256; i++) { int num = stream.ReadByte(); if (num < 0) { return false; } if (num == 0) { return true; } } return false; } private static bool ReadExactly(Stream stream, Span<byte> buffer) { int num; for (int i = 0; i < buffer.Length; i += num) { num = stream.Read(buffer.Slice(i)); if (num <= 0) { return false; } } return true; } } internal static class DriveHelper { private static readonly bool IgnoreDriveSpaceChecks = Environment.GetCommandLineArgs().Contains<string>("--ignore-space-check", StringComparer.OrdinalIgnoreCase); public static bool HasDriveSpaceOnPath(string path, long requiredGigabytes) { if (requiredGigabytes <= 0 || IgnoreDriveSpaceChecks) { return true; } string pathRoot = Path.GetPathRoot(Path.GetFullPath(path)); if (string.IsNullOrWhiteSpace(pathRoot)) { return true; } return new DriveInfo(pathRoot).AvailableFreeSpace >= requiredGigabytes * 1073741824; } } internal static class FileHelper { public const long Gigabyte = 1073741824L; public const int StreamBufferSize = 1048576; public static bool TryDeleteFile(string path, [NotNullWhen(false)] out Exception? exception) { try { File.Delete(path); exception = null; return true; } catch (Exception ex) { exception = ex; return false; } } public static async Task WaitUntilReadableAsync(string path, int maxAttempts) { for (int attempt = 0; attempt < maxAttempts; attempt++) { try { using (new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { break; } } catch (IOException) when (attempt + 1 < maxAttempts) { await Task.Delay(500); } } } } internal static class HashingHelper { public static string ComputeHash(string path) { using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1048576, FileOptions.SequentialScan); return ComputeHash(stream); } public static string ComputeHash(Stream stream) { stream.Seek(0L, SeekOrigin.Begin); using MD5 mD = MD5.Create(); byte[] array = mD.ComputeHash(stream); Span<char> span = stackalloc char[array.Length * 2]; for (int i = 0; i < array.Length; i++) { byte b = array[i]; span[i * 2] = GetHexCharacter(b >> 4); span[i * 2 + 1] = GetHexCharacter(b & 0xF); } return span.ToString(); } private static char GetHexCharacter(int value) { return (char)((value < 10) ? (48 + value) : (65 + value - 10)); } } } namespace FastAssetBundleLoader.Configuration { internal static class PatcherSettings { private const string ConfigFileName = "sighsorry.fast_asset_bundle_loader.cfg"; private static bool s_Initialized; private static ConfigEntry<bool> s_AssetBundleCacheEnabled; private static ConfigEntry<string> s_CacheDirectory; private static ConfigEntry<int> s_MinimumFreeDiskSpaceGb; private static ConfigEntry<int> s_CacheRetentionDays; public static bool AssetBundleCacheEnabled => s_AssetBundleCacheEnabled.Value; public static string CachePath { get { string text = (s_CacheDirectory.Value ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(text)) { text = "ValheimFasterLoadAssetBundles"; } if (Path.IsPathRooted(text)) { return Path.GetFullPath(text); } string fullPath = Path.GetFullPath(Paths.CachePath); string fullPath2 = Path.GetFullPath(Path.Combine(fullPath, text)); if (!IsSameOrChildPath(fullPath, fullPath2)) { throw new InvalidOperationException("Relative cache path must stay under BepInEx/cache: " + text); } return fullPath2; } } public static int MinimumFreeDiskSpaceGb => Math.Max(0, s_MinimumFreeDiskSpaceGb.Value); public static int CacheRetentionDays => Math.Max(0, s_CacheRetentionDays.Value); public static void Initialize() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) if (!s_Initialized) { ConfigFile val = new ConfigFile(Path.Combine(Paths.ConfigPath, "sighsorry.fast_asset_bundle_loader.cfg"), true); s_AssetBundleCacheEnabled = val.Bind<bool>("General", "Enabled", true, "Enable reusable LZ4 cache redirects for UnityFS asset bundles."); s_CacheDirectory = val.Bind<string>("General", "CacheDirectory", "ValheimFasterLoadAssetBundles", "Relative path under BepInEx/cache or an absolute path."); s_MinimumFreeDiskSpaceGb = val.Bind<int>("Cache", "MinimumFreeDiskSpaceGb", 10, "Skip creating new cache entries when the drive has less free space than this."); s_CacheRetentionDays = val.Bind<int>("Cache", "RetentionDays", 0, "Delete cached bundles that have not been reused for this many days. Set to 0 to disable cleanup."); s_Initialized = true; } } private static bool IsSameOrChildPath(string parentPath, string candidatePath) { StringComparison comparisonType = ((Path.DirectorySeparatorChar == '\\' || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); if (string.Equals(parentPath, candidatePath, comparisonType)) { return true; } string value = parentPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; return candidatePath.StartsWith(value, comparisonType); } } }