using 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 TargetDLLs { get; } = Array.Empty(); 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 m_InFlightHashes = new HashSet(StringComparer.OrdinalIgnoreCase); private readonly Queue m_PendingBundles = new Queue(); 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(); 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 m_Metadata = new Dictionary(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[] array = m_Metadata.Where>((KeyValuePair pair) => pair.Value.LastAccessTimeUtc < cutoff).ToArray(); for (int num2 = 0; num2 < array.Length; num2++) { KeyValuePair 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 hashSet = new HashSet(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, string>((KeyValuePair 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 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(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 span = stackalloc byte[4]; if (!ReadExactly(stream, span) || BinaryPrimitives.ReadInt32BigEndian(span) < 6 || !SkipCString(stream) || !SkipCString(stream)) { return false; } Span 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 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("--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 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 s_AssetBundleCacheEnabled; private static ConfigEntry s_CacheDirectory; private static ConfigEntry s_MinimumFreeDiskSpaceGb; private static ConfigEntry 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("General", "Enabled", true, "Enable reusable LZ4 cache redirects for UnityFS asset bundles."); s_CacheDirectory = val.Bind("General", "CacheDirectory", "ValheimFasterLoadAssetBundles", "Relative path under BepInEx/cache or an absolute path."); s_MinimumFreeDiskSpaceGb = val.Bind("Cache", "MinimumFreeDiskSpaceGb", 10, "Skip creating new cache entries when the drive has less free space than this."); s_CacheRetentionDays = val.Bind("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); } } }