Decompiled source of Fast AssetBundle Loader v1.0.0

patchers/FastAssetBundleLoader.dll

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