Decompiled source of Fast AssetBundle Loader v1.0.3

patchers/FastAssetBundleLoader.dll

Decompiled 3 weeks ago
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<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);
		}
	}
}