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.0
patchers/FastAssetBundleLoader.dll
Decompiled 3 weeks agousing System; using System.Buffers.Binary; using System.Collections.Concurrent; 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 FastAssetBundleLoader.Models; using HarmonyLib; using Microsoft.CodeAnalysis; using Mono.Cecil; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyDescription("Valheim asset bundle cache patcher that rewrites slow bundles into reusable cached bundles.")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+9fd34dd7c3e61fc42e57ed0b6af4bb1192aa0b2a")] [assembly: AssemblyProduct("FastAssetBundleLoader")] [assembly: AssemblyTitle("FastAssetBundleLoader")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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 { [HarmonyPatch] internal static class RuntimeHooks { private static bool s_AssetBundleHooksInstalled; internal static ManualLogSource Logger { get; private set; } internal static AssetBundleCacheManager CacheManager { get; private set; } internal static MetadataStore MetadataStore { get; private set; } internal static bool IsAssetBundleCacheEnabled { get; private set; } [HarmonyPatch(typeof(Chainloader), "Initialize")] [HarmonyPostfix] private static void ChainloaderInitialized() { Logger = Logger.CreateLogSource("FastAssetBundleLoader"); try { AsyncHelper.InitializeMainThreadContext(); PatcherSettings.Initialize(); } catch (Exception arg) { Logger.LogError((object)$"Failed to initialize patcher.{Environment.NewLine}{arg}"); return; } InitializeAssetBundleCache(); } private static void InitializeAssetBundleCache() { if (!PatcherSettings.AssetBundleCacheEnabled) { Logger.LogInfo((object)"Asset bundle cache is disabled via config."); return; } try { string cachePath = PatcherSettings.CachePath; Directory.CreateDirectory(cachePath); MetadataStore = new MetadataStore(Path.Combine(cachePath, "metadata.tsv"), cachePath, PatcherSettings.CacheRetentionDays); CacheManager = new AssetBundleCacheManager(cachePath, MetadataStore, PatcherSettings.MinimumFreeDiskSpaceGb); InstallAssetBundleHooks(); IsAssetBundleCacheEnabled = true; Logger.LogInfo((object)("Asset bundle cache ready at \"" + cachePath + "\".")); } catch (Exception arg) { Logger.LogError((object)$"Failed to initialize asset bundle cache.{Environment.NewLine}{arg}"); } } private static void InstallAssetBundleHooks() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Expected O, but got Unknown //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Expected O, but got Unknown //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01b1: Expected O, but got Unknown //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Expected O, but got Unknown if (!s_AssetBundleHooksInstalled) { 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); s_AssetBundleHooksInstalled = true; } } private static void RedirectLoadFromFileWithOffset(ref string path, ulong offset) { if (offset == 0) { RedirectLoadFromFile(ref path); } } private static void RedirectLoadFromFile(ref string path) { if (!IsAssetBundleCacheEnabled || 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, ref AssetBundle? __result) { if (TryResolveCachedPath(stream, out string cachedPath)) { __result = AssetBundle.LoadFromFile(cachedPath); return false; } return true; } private static bool RedirectLoadFromStreamAsync(Stream stream, ref AssetBundleCreateRequest? __result) { if (TryResolveCachedPath(stream, out string cachedPath)) { __result = AssetBundle.LoadFromFileAsync(cachedPath); return false; } return true; } private static bool RedirectLoadFromMemory(byte[] binary, ref AssetBundle? __result) { if (TryResolveCachedPath(binary, out string cachedPath)) { __result = AssetBundle.LoadFromFile(cachedPath); return false; } return true; } private static bool RedirectLoadFromMemoryAsync(byte[] binary, ref AssetBundleCreateRequest? __result) { if (TryResolveCachedPath(binary, out string cachedPath)) { __result = AssetBundle.LoadFromFileAsync(cachedPath); return false; } return true; } private static bool TryResolveCachedPath(Stream stream, out string cachedPath) { cachedPath = string.Empty; if (!IsAssetBundleCacheEnabled || !stream.CanSeek) { return false; } long position = stream.Position; try { return CacheManager.TryUseCachedBundle(stream, out cachedPath); } catch (Exception arg) { Logger.LogError((object)$"Failed to process asset bundle stream.{Environment.NewLine}{arg}"); return false; } finally { if (stream.CanSeek) { stream.Position = position; } } } private static bool TryResolveCachedPath(byte[] binary, out string cachedPath) { cachedPath = string.Empty; if (!IsAssetBundleCacheEnabled) { return false; } try { return CacheManager.TryUseCachedBundle(binary, out cachedPath); } catch (Exception arg) { Logger.LogError((object)$"Failed to process asset bundle bytes.{Environment.NewLine}{arg}"); return false; } } } 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 _) { } } } namespace FastAssetBundleLoader.Models { internal sealed class AssetBundleMetadata { public string OriginalHash { get; set; } = string.Empty; public string? CachedBundleFileName { get; set; } public bool ShouldSkipCaching { get; set; } public DateTime LastAccessTimeUtc { get; set; } } } 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_RunnerLock = new object(); private readonly ConcurrentDictionary<string, byte> m_InFlightHashes = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, byte> m_ReportedSkippedHashes = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentQueue<PendingBundle> m_PendingBundles = new ConcurrentQueue<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.IsAlreadyFastToLoad(stream)) { return false; } string originalHash = HashingHelper.ComputeHash(stream); return TryUseKnownHash(stream, originalHash, out cachedPath); } public bool TryUseCachedBundle(byte[] binary, [NotNullWhen(true)] out string cachedPath) { cachedPath = string.Empty; using MemoryStream stream = new MemoryStream(binary, writable: false); if (BundleHelper.IsAlreadyFastToLoad(stream)) { return false; } string originalHash = HashingHelper.ComputeHash(stream); return TryUseKnownHash(binary, originalHash, 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: { if (m_InFlightHashes.ContainsKey(originalHash)) { return false; } if (stream is FileStream fileStream) { QueueBundleForCaching(fileStream.Name, originalHash, deleteSourceAfterCaching: false); return false; } string 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); } QueueBundleForCaching(text, originalHash, deleteSourceAfterCaching: true); return false; } } } private bool TryUseKnownHash(byte[] binary, string originalHash, [NotNullWhen(true)] out string cachedPath) { cachedPath = string.Empty; string cachedPath2; switch (TryResolveExistingCache(originalHash, out cachedPath2)) { case CacheLookupResult.Cached: cachedPath = cachedPath2; return true; default: if (!m_InFlightHashes.ContainsKey(originalHash)) { string text = Path.Combine(m_TempPath, $"{Guid.NewGuid():N}.assetbundle"); using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1048576, FileOptions.SequentialScan)) { fileStream.Write(binary, 0, binary.Length); } QueueBundleForCaching(text, originalHash, deleteSourceAfterCaching: true); return false; } goto case CacheLookupResult.Skip; case CacheLookupResult.Skip: return false; } } private CacheLookupResult TryResolveExistingCache(string originalHash, [NotNullWhen(true)] out string? cachedPath) { cachedPath = null; if (!m_MetadataStore.TryGet(originalHash, out AssetBundleMetadata metadata)) { return CacheLookupResult.Miss; } if (metadata.ShouldSkipCaching) { if (m_ReportedSkippedHashes.TryAdd(originalHash, 0)) { RuntimeHooks.Logger.LogDebug((object)("Skipping cache creation for previously marked asset bundle hash \"" + originalHash + "\".")); } m_MetadataStore.Touch(metadata); return CacheLookupResult.Skip; } if (string.IsNullOrWhiteSpace(metadata.CachedBundleFileName)) { m_MetadataStore.Delete(originalHash); return CacheLookupResult.Miss; } string text = Path.Combine(m_CachePath, metadata.CachedBundleFileName); if (!File.Exists(text)) { RuntimeHooks.Logger.LogWarning((object)("Cached bundle is missing: \"" + text + "\"")); m_MetadataStore.Delete(originalHash); return CacheLookupResult.Miss; } RuntimeHooks.Logger.LogDebug((object)("Using cached asset bundle \"" + metadata.CachedBundleFileName + "\" for hash \"" + originalHash + "\".")); m_MetadataStore.Touch(metadata); cachedPath = text; return CacheLookupResult.Cached; } private void QueueBundleForCaching(string sourcePath, string originalHash, bool deleteSourceAfterCaching) { if (!DriveHelper.HasDriveSpaceOnPath(m_CachePath, m_MinimumFreeDiskSpaceGb)) { RuntimeHooks.Logger.LogWarning((object)$"Skipping cache creation because free space is below {m_MinimumFreeDiskSpaceGb} GB."); } else if (m_InFlightHashes.TryAdd(originalHash, 0)) { RuntimeHooks.Logger.LogInfo((object)("Queueing asset bundle cache creation for \"" + Path.GetFileName(sourcePath) + "\" (hash \"" + originalHash + "\").")); m_PendingBundles.Enqueue(new PendingBundle(sourcePath, originalHash, deleteSourceAfterCaching)); StartQueueProcessor(); } } private void StartQueueProcessor() { if (m_IsProcessingQueue) { return; } lock (m_RunnerLock) { if (m_IsProcessingQueue) { return; } m_IsProcessingQueue = true; } AsyncHelper.Schedule(ProcessQueueAsync); } private async Task ProcessQueueAsync() { while (true) { if (m_PendingBundles.TryDequeue(out var bundle)) { await CacheBundleAsync(bundle); continue; } lock (m_RunnerLock) { if (m_PendingBundles.IsEmpty) { m_IsProcessingQueue = false; break; } } } } private async Task CacheBundleAsync(PendingBundle bundle) { string outputFileName = bundle.OriginalHash + ".assetbundle"; string outputPath = Path.Combine(m_CachePath, outputFileName); AssetBundleMetadata metadata = new AssetBundleMetadata { OriginalHash = bundle.OriginalHash, LastAccessTimeUtc = DateTime.UtcNow }; Exception exception; try { await FileHelper.WaitUntilReadableAsync(bundle.SourcePath, 5); 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); return; } string cachedHash = HashingHelper.ComputeHash(outputPath); if (string.Equals(cachedHash, 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.")); metadata.ShouldSkipCaching = true; m_MetadataStore.Save(metadata); FileHelper.TryDeleteFile(outputPath, out exception); } else { metadata.CachedBundleFileName = outputFileName; m_MetadataStore.Save(metadata); RuntimeHooks.Logger.LogInfo((object)("Cached asset bundle \"" + Path.GetFileName(bundle.SourcePath) + "\" (hash \"" + bundle.OriginalHash + "\") as \"" + outputFileName + "\".")); } } catch (Exception ex2) { exception = ex2; Exception ex = exception; RuntimeHooks.Logger.LogError((object)$"Failed to cache asset bundle \"{bundle.SourcePath}\".{Environment.NewLine}{ex}"); FileHelper.TryDeleteFile(outputPath, out exception); } finally { m_InFlightHashes.TryRemove(bundle.OriginalHash, out var _); if (bundle.DeleteSourceAfterCaching) { FileHelper.TryDeleteFile(bundle.SourcePath, out exception); } } } private void DeleteTemporaryFiles() { Exception exception; foreach (string item in Directory.EnumerateFiles(m_CachePath, "*.tmp", SearchOption.TopDirectoryOnly)) { FileHelper.TryDeleteFile(item, out exception); } foreach (string item2 in Directory.EnumerateFiles(m_TempPath, "*.assetbundle", SearchOption.TopDirectoryOnly)) { FileHelper.TryDeleteFile(item2, out exception); } } } internal sealed class MetadataStore { 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 Dictionary<string, AssetBundleMetadata> m_Metadata = new Dictionary<string, AssetBundleMetadata>(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(delegate { Flush(); }, null, -1, -1); Load(); CleanupStaleEntries(); AppDomain.CurrentDomain.ProcessExit += OnProcessExit; } public bool TryGet(string originalHash, out AssetBundleMetadata metadata) { lock (m_Lock) { return m_Metadata.TryGetValue(originalHash, out metadata); } } public void Save(AssetBundleMetadata metadata) { lock (m_Lock) { m_Metadata[metadata.OriginalHash] = metadata; MarkDirtyLocked(); } } public void Touch(AssetBundleMetadata metadata) { lock (m_Lock) { metadata.LastAccessTimeUtc = DateTime.UtcNow; 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; } string[] array = File.ReadAllLines(m_MetadataFilePath); foreach (string text in array) { if (!string.IsNullOrWhiteSpace(text)) { if (!TryParseLine(text, out AssetBundleMetadata metadata)) { RuntimeHooks.Logger.LogWarning((object)("Ignoring invalid metadata line: " + text)); } else { m_Metadata[metadata.OriginalHash] = metadata; } } } } private void CleanupStaleEntries() { bool flag = false; lock (m_Lock) { if (m_RetentionDays > 0) { DateTime cutoff = DateTime.UtcNow.AddDays(-m_RetentionDays); AssetBundleMetadata[] array = m_Metadata.Values.Where((AssetBundleMetadata m) => m.LastAccessTimeUtc < cutoff).ToArray(); foreach (AssetBundleMetadata assetBundleMetadata in array) { m_Metadata.Remove(assetBundleMetadata.OriginalHash); flag = true; if (!string.IsNullOrWhiteSpace(assetBundleMetadata.CachedBundleFileName)) { string path = Path.Combine(m_CachePath, assetBundleMetadata.CachedBundleFileName); DeleteCacheFile(path, "Deleting expired cache " + assetBundleMetadata.CachedBundleFileName); } } } HashSet<string> hashSet = new HashSet<string>(from m in m_Metadata.Values where !string.IsNullOrWhiteSpace(m.CachedBundleFileName) select m.CachedBundleFileName, StringComparer.OrdinalIgnoreCase); foreach (string item in Directory.EnumerateFiles(m_CachePath, "*.assetbundle", SearchOption.TopDirectoryOnly)) { string fileName = Path.GetFileName(item); if (!hashSet.Contains(fileName)) { flag = true; DeleteCacheFile(item, "Deleting untracked cache " + fileName); } } if (flag) { WriteMetadataFileLocked(); m_IsDirty = false; } } } private void MarkDirtyLocked() { m_IsDirty = true; m_FlushTimer.Change(60000, -1); } private void FlushDirtyLocked() { if (m_IsDirty) { WriteMetadataFileLocked(); m_IsDirty = false; } } private void WriteMetadataFileLocked() { string[] contents = m_Metadata.Values.OrderBy<AssetBundleMetadata, string>((AssetBundleMetadata m) => m.OriginalHash, StringComparer.OrdinalIgnoreCase).Select(SerializeLine).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(AssetBundleMetadata metadata) { string text = Convert.ToBase64String(Encoding.UTF8.GetBytes(metadata.CachedBundleFileName ?? string.Empty)); return string.Join("\t", metadata.OriginalHash, metadata.LastAccessTimeUtc.Ticks.ToString(), metadata.ShouldSkipCaching ? "1" : "0", text); } private static bool TryParseLine(string line, out AssetBundleMetadata metadata) { metadata = null; string[] array = line.Split('\t'); if ((array.Length != 4 && array.Length != 5) || string.IsNullOrWhiteSpace(array[0])) { return false; } if (!long.TryParse(array[1], out var result)) { return false; } string @string; try { @string = Encoding.UTF8.GetString(Convert.FromBase64String(array[^1])); } catch (FormatException) { return false; } metadata = new AssetBundleMetadata { OriginalHash = array[0], LastAccessTimeUtc = new DateTime(result, DateTimeKind.Utc), ShouldSkipCaching = (array[2] == "1"), CachedBundleFileName = (string.IsNullOrWhiteSpace(@string) ? null : @string) }; 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() { SynchronizationContext current = SynchronizationContext.Current; if (current == null) { throw new InvalidOperationException("Unity SynchronizationContext is not available."); } s_MainThreadContext = current; s_MainThreadId = Thread.CurrentThread.ManagedThreadId; } public static void Schedule(Func<Task> work) { Func<Task> work2 = work; Task.Run(async delegate { try { await work2(); } catch (Exception ex2) { Exception ex = ex2; RuntimeHooks.Logger.LogError((object)ex); } }); } public static SwitchToMainThreadAwaiter SwitchToMainThread() { return default(SwitchToMainThreadAwaiter); } public static SwitchToThreadPoolAwaiter SwitchToThreadPool() { return default(SwitchToThreadPoolAwaiter); } } internal static class AsyncOperationHelper { internal struct AsyncOperationAwaiter : ICriticalNotifyCompletion, INotifyCompletion { private AsyncOperation? m_AsyncOperation; private Action? m_Continuation; public bool IsCompleted => m_AsyncOperation != null && m_AsyncOperation.isDone; 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) { m_Continuation = continuation; m_AsyncOperation.completed += OnCompleted; } private void OnCompleted(AsyncOperation _) { m_Continuation?.Invoke(); } } public static AsyncOperationAwaiter WaitCompletionAsync<T>(this T asyncOperation) where T : AsyncOperation { return new AsyncOperationAwaiter((AsyncOperation)(object)asyncOperation); } } internal static class BundleHelper { public static bool IsAlreadyFastToLoad(Stream stream) { try { stream.Seek(0L, SeekOrigin.Begin); SkipCString(stream); stream.Position += 4L; SkipCString(stream); SkipCString(stream); stream.Position += 16L; Span<byte> span = stackalloc byte[4]; if (stream.Read(span) != span.Length) { return false; } int num = BinaryPrimitives.ReadInt32BigEndian(span); int num2 = num & 0x3F; return (num2 == 0 || num2 == 2) ? true : false; } catch { return false; } } private static void SkipCString(Stream stream) { while (stream.ReadByte() > 0) { } } } 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; } DriveInfo driveInfo = new DriveInfo(pathRoot); return driveInfo.TotalFreeSpace >= 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 { if (File.Exists(path)) { 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"; } return Path.IsPathRooted(text) ? text : Path.Combine(Paths.CachePath, text); } } 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_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown if (!s_Initialized) { s_Initialized = true; ConfigFile val = new ConfigFile(Path.Combine(Paths.ConfigPath, "sighsorry.fast_asset_bundle_loader.cfg"), true); s_AssetBundleCacheEnabled = val.Bind<bool>("General", "Enabled", true, "Enable cached LZMA-to-LZ4 asset bundle redirects."); 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."); } } } }