Decompiled source of DunderKlumpar v1.0.1

BepInEx/plugins/DunderKlumpar/DunderKlumpar.dll

Decompiled 4 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;

[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("DunderKlumpar")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyInformationalVersion("1.0.1")]
[assembly: AssemblyProduct("DunderKlumpar")]
[assembly: AssemblyTitle("DunderKlumpar")]
[assembly: AssemblyVersion("1.0.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 DunderKlumpar
{
	[HarmonyPatch]
	internal static class PaintingPatches
	{
		private const string PaintingItemName = "Painting";

		private static int _appliedForSeed = int.MinValue;

		[HarmonyPostfix]
		[HarmonyPatch(typeof(GrabbableObject), "SetScrapValue")]
		private static void OnScrapValueSet(GrabbableObject __instance)
		{
			try
			{
				if ((Object)(object)__instance == (Object)null || (Object)(object)__instance.itemProperties == (Object)null || __instance.itemProperties.itemName != "Painting" || _appliedForSeed == RoundInfo.Seed)
				{
					return;
				}
				string[] array = Plugin.AssetFiles("paintings");
				if (array.Length == 0)
				{
					return;
				}
				string path = array[new Random(RoundInfo.Seed + 977).Next(array.Length)];
				Texture2D mainTexture = Plugin.LoadTexture(path);
				Material[] materialVariants = __instance.itemProperties.materialVariants;
				if (materialVariants == null || materialVariants.Length == 0)
				{
					return;
				}
				Material[] array2 = materialVariants;
				foreach (Material val in array2)
				{
					if ((Object)(object)val != (Object)null)
					{
						val.mainTexture = (Texture)(object)mainTexture;
					}
				}
				_appliedForSeed = RoundInfo.Seed;
				Plugin.Log.LogInfo((object)("Painting ← " + Path.GetFileName(path)));
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Could not swap the painting: {arg}");
			}
		}
	}
	internal class PlayOnAwakeRelay : MonoBehaviour
	{
		internal AudioSource Source;

		private bool _played;

		private void OnEnable()
		{
			_played = false;
			TryPlay();
		}

		private void OnDisable()
		{
			if ((Object)(object)Source != (Object)null && Source.isPlaying)
			{
				Source.Stop();
			}
			_played = false;
		}

		private void Update()
		{
			TryPlay();
		}

		private void TryPlay()
		{
			if ((Object)(object)Source == (Object)null || (Object)(object)Source.clip == (Object)null)
			{
				_played = false;
			}
			else if (!Source.isPlaying)
			{
				if (!((Behaviour)Source).isActiveAndEnabled)
				{
					_played = false;
				}
				else if (!_played)
				{
					Source.Play();
					_played = true;
				}
			}
		}
	}
	internal static class PlayOnAwakePatcher
	{
		private const float RescanSeconds = 5f;

		internal static void Start(MonoBehaviour host)
		{
			SceneManager.sceneLoaded += delegate
			{
				Convert("scene load");
			};
			host.StartCoroutine(Rescan());
			Convert("startup");
		}

		private static IEnumerator Rescan()
		{
			WaitForSeconds wait = new WaitForSeconds(5f);
			while (true)
			{
				yield return wait;
				Convert(null);
			}
		}

		private static void Convert(string reason)
		{
			int num = 0;
			AudioSource[] array = Object.FindObjectsOfType<AudioSource>(true);
			foreach (AudioSource val in array)
			{
				if (!((Object)(object)val == (Object)null) && val.playOnAwake)
				{
					val.playOnAwake = false;
					(((Component)val).gameObject.GetComponent<PlayOnAwakeRelay>() ?? ((Component)val).gameObject.AddComponent<PlayOnAwakeRelay>()).Source = val;
					num++;
				}
			}
			if (num > 0 && reason != null)
			{
				Plugin.Log.LogInfo((object)$"[Sound] Took over {num} auto-playing sounds ({reason}).");
			}
			else if (num > 0)
			{
				Plugin.Log.LogInfo((object)$"[Sound] Took over {num} new auto-playing sounds.");
			}
		}
	}
	[BepInPlugin("nu.dunderpatrullen.dunderklumpar", "DunderKlumpar", "1.0.1")]
	public class Plugin : BaseUnityPlugin
	{
		internal class Host : MonoBehaviour
		{
		}

		public const string PluginGuid = "nu.dunderpatrullen.dunderklumpar";

		public const string PluginName = "DunderKlumpar";

		public const string PluginVersion = "1.0.1";

		internal static ManualLogSource Log;

		internal static string PluginDir;

		private void Awake()
		{
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Expected O, but got Unknown
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Expected O, but got Unknown
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			PluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			Settings.Init(((BaseUnityPlugin)this).Config);
			SoundCatalog.Init();
			Harmony val = new Harmony("nu.dunderpatrullen.dunderklumpar");
			val.PatchAll(Assembly.GetExecutingAssembly());
			SoundPatcher.Apply(val);
			GameObject val2 = new GameObject("DunderKlumparHost");
			Object.DontDestroyOnLoad((Object)val2);
			((Object)val2).hideFlags = (HideFlags)61;
			PlayOnAwakePatcher.Start((MonoBehaviour)(object)val2.AddComponent<Host>());
			val2.AddComponent<DecorWatcher>();
			try
			{
				SoundReplacement.LoadAll();
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"Could not load sounds: {arg}");
			}
			Log.LogInfo((object)("DunderKlumpar v1.0.1 loaded! Asset folder: " + PluginDir));
		}

		internal static Texture2D LoadTexture(string path)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Expected O, but got Unknown
			//IL_0015: Expected O, but got Unknown
			Texture2D val = new Texture2D(2, 2);
			ImageConversion.LoadImage(val, File.ReadAllBytes(path));
			return val;
		}

		internal static string[] AssetFiles(string subfolder)
		{
			string path = Path.Combine(PluginDir, subfolder);
			if (!Directory.Exists(path))
			{
				return new string[0];
			}
			return Directory.GetFiles(path, "*.png");
		}
	}
	internal static class RoundInfo
	{
		internal static int Seed
		{
			get
			{
				StartOfRound instance = StartOfRound.Instance;
				if (!((Object)(object)instance == (Object)null))
				{
					return instance.randomMapSeed;
				}
				return 0;
			}
		}
	}
	[HarmonyPatch]
	internal static class SaveRepair
	{
		private const string StoredKey = "UnlockedShipObjects";

		private const string BackupSuffix = ".dunderklumpar-backup";

		[HarmonyPrefix]
		[HarmonyPatch(typeof(StartOfRound), "LoadUnlockables")]
		private static void RepairBeforeLoad(StartOfRound __instance)
		{
			try
			{
				Repair(__instance);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[Save] Could not check the save file: {arg}");
			}
		}

		private static void Repair(StartOfRound round)
		{
			if ((Object)(object)round == (Object)null || (Object)(object)round.unlockablesList == (Object)null || round.unlockablesList.unlockables == null)
			{
				return;
			}
			GameNetworkManager instance = GameNetworkManager.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			string currentSaveFileName = instance.currentSaveFileName;
			if (string.IsNullOrEmpty(currentSaveFileName) || !ES3.KeyExists("UnlockedShipObjects", currentSaveFileName))
			{
				return;
			}
			int[] array = ES3.Load<int[]>("UnlockedShipObjects", currentSaveFileName);
			if (array == null || array.Length == 0)
			{
				return;
			}
			int count = round.unlockablesList.unlockables.Count;
			int[] array2 = array.Where((int i) => i >= 0 && i < count).ToArray();
			if (array2.Length != array.Length)
			{
				string[] array3 = (from i in array
					where i < 0 || i >= count
					select i.ToString()).ToArray();
				Backup(currentSaveFileName);
				ES3.Save<int[]>("UnlockedShipObjects", array2, currentSaveFileName);
				Plugin.Log.LogWarning((object)($"[Save] '{currentSaveFileName}' pointed at {array3.Length} ship object(s) that do not exist " + string.Format("(index {0}; the list holds {1}). Left behind by a mod ", string.Join(", ", array3), count) + "that is no longer installed. Removed them - otherwise the game aborts loading and the suit rack stays completely empty."));
			}
		}

		private static void Backup(string saveName)
		{
			try
			{
				string text = Path.Combine(Application.persistentDataPath, saveName);
				string text2 = text + ".dunderklumpar-backup";
				if (File.Exists(text) && !File.Exists(text2))
				{
					File.Copy(text, text2);
					Plugin.Log.LogInfo((object)("[Save] Original copied to " + Path.GetFileName(text2) + " before editing."));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Save] Could not write a backup: " + ex.Message));
			}
		}
	}
	internal static class Settings
	{
		internal static ConfigEntry<bool> LogSounds;

		internal static ConfigEntry<bool> ExportOriginalSounds;

		internal static ConfigEntry<bool> DumpAllSounds;

		internal static ConfigEntry<bool> ExportSuitTemplate;

		internal static void Init(ConfigFile cfg)
		{
			LogSounds = cfg.Bind<bool>("Sounds", "LogSoundNames", true, "Log the name of every sound the game plays, and collect them in 'discovered_sounds.txt'. Use those names as filenames in sounds/ to replace a sound.");
			ExportOriginalSounds = cfg.Bind<bool>("Sounds", "ExportOriginalSounds", false, "Save the game's original sounds as .wav in 'original_sounds/' the first time each one plays. Useful for hearing what you are replacing and how long it is. Turn off when done - it uses disk space.");
			DumpAllSounds = cfg.Bind<bool>("Sounds", "DumpAllSounds", false, "Dump ALL of the game's sounds at once into 'original_sounds/' when a round starts, plus a searchable list in 'all_sounds.html'. Do this ONCE to get the whole library to work from, then turn it off again. Takes a few seconds and some disk space.");
			ExportSuitTemplate = cfg.Bind<bool>("Suits", "ExportSuitTemplate", true, "Write the game's own suit texture to suits/_TEMPLATE.png (only if it is missing), so you have the correct layout to paint over in Photoshop. Files starting with '_' are never loaded as suits.");
		}
	}
	[HarmonyPatch]
	internal static class ShipDecorPatches
	{
		private const string PlanePath = "HangarShip/Plane.001";

		private static Random _rng = new Random(0);

		private static int _appliedSeed = int.MinValue;

		[HarmonyPostfix]
		[HarmonyPatch(typeof(StartOfRound), "Start")]
		private static void OnShipStart()
		{
			ApplyDecor("StartOfRound.Start", verbose: true);
			SoundDump.DumpIfEnabled();
		}

		[HarmonyPostfix]
		[HarmonyPatch(typeof(RoundManager), "GenerateNewLevelClientRpc")]
		private static void OnNewLevel()
		{
			ApplyDecor("RoundManager.GenerateNewLevelClientRpc", verbose: true);
		}

		internal static void EnsureApplied()
		{
			if (RoundInfo.Seed != _appliedSeed)
			{
				ApplyDecor("seed arrived", verbose: false);
			}
		}

		private static void ApplyDecor(string source, bool verbose)
		{
			try
			{
				int seed = RoundInfo.Seed;
				_rng = new Random(seed);
				GameObject val = GameObject.Find("HangarShip/Plane.001");
				if ((Object)(object)val == (Object)null)
				{
					if (verbose)
					{
						Plugin.Log.LogWarning((object)("[" + source + "] Could not find 'HangarShip/Plane.001' - skipping posters."));
					}
					return;
				}
				MeshRenderer component = val.GetComponent<MeshRenderer>();
				if ((Object)(object)component == (Object)null)
				{
					if (verbose)
					{
						Plugin.Log.LogWarning((object)("[" + source + "] 'HangarShip/Plane.001' has no MeshRenderer."));
					}
				}
				else
				{
					Material[] materials = ((Renderer)component).materials;
					SwapMaterialTexture(materials, 0, "posters");
					SwapMaterialTexture(materials, 1, "tips");
					((Renderer)component).materials = materials;
					_appliedSeed = seed;
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[{source}] Error swapping posters: {arg}");
			}
		}

		private static void SwapMaterialTexture(Material[] mats, int index, string subfolder)
		{
			if (index < mats.Length)
			{
				string[] array = Plugin.AssetFiles(subfolder);
				if (array.Length == 0)
				{
					Plugin.Log.LogWarning((object)$"No images in '{subfolder}/' - keeping original for material {index}.");
					return;
				}
				string path = array[_rng.Next(array.Length)];
				mats[index].mainTexture = (Texture)(object)Plugin.LoadTexture(path);
				Plugin.Log.LogInfo((object)$"Material {index} ({subfolder}) ← {Path.GetFileName(path)}");
			}
		}
	}
	internal class DecorWatcher : MonoBehaviour
	{
		private const float IntervalSeconds = 2f;

		private float _next;

		private void Update()
		{
			if (!(Time.unscaledTime < _next))
			{
				_next = Time.unscaledTime + 2f;
				ShipDecorPatches.EnsureApplied();
			}
		}
	}
	internal static class SoundCatalog
	{
		private static readonly HashSet<string> Seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		private static string _catalogPath;

		private static string _exportDir;

		internal static void Init()
		{
			_catalogPath = Path.Combine(Plugin.PluginDir, "discovered_sounds.txt");
			_exportDir = Path.Combine(Plugin.PluginDir, "original_sounds");
		}

		internal static void Note(AudioClip clip)
		{
			if (!((Object)(object)clip == (Object)null) && !string.IsNullOrEmpty(((Object)clip).name) && Seen.Add(((Object)clip).name))
			{
				if (Settings.LogSounds != null && Settings.LogSounds.Value)
				{
					Plugin.Log.LogInfo((object)("[SoundScan] " + ((Object)clip).name));
					TryAppendCatalog(clip);
				}
				if (Settings.ExportOriginalSounds != null && Settings.ExportOriginalSounds.Value)
				{
					TryExport(clip);
				}
			}
		}

		private static void TryAppendCatalog(AudioClip clip)
		{
			try
			{
				string text = (SoundReplacement.Variants.ContainsKey(((Object)clip).name) ? "  [REPLACED]" : "");
				File.AppendAllText(_catalogPath, $"{((Object)clip).name}{new string(' ', Math.Max(1, 40 - ((Object)clip).name.Length))}{clip.length:F1}s{text}{Environment.NewLine}", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[SoundScan] Could not write catalogue: " + ex.Message));
			}
		}

		private static void TryExport(AudioClip clip)
		{
			try
			{
				Directory.CreateDirectory(_exportDir);
				string path = Path.Combine(_exportDir, SafeName(((Object)clip).name) + ".wav");
				if (!File.Exists(path))
				{
					float[] array = new float[clip.samples * clip.channels];
					if (!clip.GetData(array, 0))
					{
						Plugin.Log.LogWarning((object)("[Export] Could not read audio data for " + ((Object)clip).name + " (streamed clip?)."));
						return;
					}
					File.WriteAllBytes(path, ToWav(array, clip.channels, clip.frequency));
					Plugin.Log.LogInfo((object)("[Export] Saved original: original_sounds/" + Path.GetFileName(path)));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Export] Failed for " + ((Object)clip).name + ": " + ex.Message));
			}
		}

		private static string SafeName(string name)
		{
			char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
			foreach (char oldChar in invalidFileNameChars)
			{
				name = name.Replace(oldChar, '_');
			}
			return name;
		}

		private static byte[] ToWav(float[] samples, int channels, int frequency)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
			int num = samples.Length * 2;
			binaryWriter.Write(Encoding.ASCII.GetBytes("RIFF"));
			binaryWriter.Write(36 + num);
			binaryWriter.Write(Encoding.ASCII.GetBytes("WAVE"));
			binaryWriter.Write(Encoding.ASCII.GetBytes("fmt "));
			binaryWriter.Write(16);
			binaryWriter.Write((short)1);
			binaryWriter.Write((short)channels);
			binaryWriter.Write(frequency);
			binaryWriter.Write(frequency * channels * 2);
			binaryWriter.Write((short)(channels * 2));
			binaryWriter.Write((short)16);
			binaryWriter.Write(Encoding.ASCII.GetBytes("data"));
			binaryWriter.Write(num);
			foreach (float num2 in samples)
			{
				binaryWriter.Write((short)(Mathf.Clamp(num2, -1f, 1f) * 32767f));
			}
			binaryWriter.Flush();
			return memoryStream.ToArray();
		}
	}
	internal static class SoundDump
	{
		private static bool _done;

		internal static void DumpIfEnabled()
		{
			if (_done || Settings.DumpAllSounds == null || !Settings.DumpAllSounds.Value)
			{
				return;
			}
			_done = true;
			try
			{
				Dump();
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[Dump] Failed: {arg}");
			}
		}

		private static void Dump()
		{
			string text = Path.Combine(Plugin.PluginDir, "original_sounds");
			Directory.CreateDirectory(text);
			HashSet<AudioClip> ours = new HashSet<AudioClip>(from v in SoundReplacement.Variants.Values.SelectMany((List<SoundVariant> v) => v)
				select v.Clip);
			List<AudioClip> list = (from g in (from c in Resources.FindObjectsOfTypeAll<AudioClip>()
					where (Object)(object)c != (Object)null && !string.IsNullOrEmpty(((Object)c).name) && !ours.Contains(c)
					select c).GroupBy<AudioClip, string>((AudioClip c) => ((Object)c).name, StringComparer.OrdinalIgnoreCase)
				select g.First()).OrderBy<AudioClip, string>((AudioClip c) => ((Object)c).name, StringComparer.OrdinalIgnoreCase).ToList();
			Plugin.Log.LogInfo((object)$"[Dump] Found {list.Count} sounds in the game. Exporting to original_sounds/ ...");
			List<(string, float, bool)> list2 = new List<(string, float, bool)>();
			int num = 0;
			int num2 = 0;
			foreach (AudioClip item in list)
			{
				bool flag = TryExportWav(item, text);
				if (flag)
				{
					num++;
				}
				else
				{
					num2++;
				}
				list2.Add((((Object)item).name, item.length, flag));
			}
			WriteHtmlIndex(list2);
			Plugin.Log.LogInfo((object)($"[Dump] Done: {num} sounds saved, {num2} could not be read. " + "See all_sounds.html for the full list."));
		}

		private static bool TryExportWav(AudioClip clip, string dir)
		{
			try
			{
				string path = Path.Combine(dir, SafeName(((Object)clip).name) + ".wav");
				if (File.Exists(path))
				{
					return true;
				}
				float[] array = new float[clip.samples * clip.channels];
				if (array.Length == 0 || !clip.GetData(array, 0))
				{
					return false;
				}
				File.WriteAllBytes(path, WavEncoder.Encode(array, clip.channels, clip.frequency));
				return true;
			}
			catch
			{
				return false;
			}
		}

		internal static string SafeName(string name)
		{
			char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
			foreach (char oldChar in invalidFileNameChars)
			{
				name = name.Replace(oldChar, '_');
			}
			return name;
		}

		private static void WriteHtmlIndex(List<(string Name, float Length, bool Exported)> rows)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("<!doctype html><html lang=\"en\"><meta charset=\"utf-8\">\n<title>DunderKlumpar - all sounds in the game</title>\n<style>\n body{font:15px/1.5 system-ui,sans-serif;margin:2rem;background:#14161a;color:#e6e6e6}\n h1{font-size:1.4rem;margin:0 0 .25rem} p{color:#9aa0a6;margin:.25rem 0 1rem}\n input{width:100%;padding:.7rem .9rem;font-size:1rem;border-radius:8px;border:1px solid #333;\n       background:#1c1f24;color:#eee;margin-bottom:1rem}\n table{border-collapse:collapse;width:100%} th,td{text-align:left;padding:.45rem .6rem;border-bottom:1px solid #262a30}\n th{position:sticky;top:0;background:#14161a;color:#9aa0a6;font-weight:600}\n code{background:#1c1f24;padding:.15rem .4rem;border-radius:4px;color:#7fd1b9}\n .name{font-family:ui-monospace,Consolas,monospace;color:#7fd1b9}\n .no{color:#e0794f} .yes{color:#6bbf59} .rep{color:#f0c674;font-weight:600}\n tr:hover{background:#1a1d22}\n</style>\n<h1>All sounds in Lethal Company</h1>\n<p>Name your audio file <b>exactly</b> like the Sound name column (e.g. <code>StartGameLever.ogg</code>)\nand put it in <code>sounds/</code>. Add <code>-ver01</code>, <code>-ver02</code>&hellip; to have the game\npick randomly between several versions. Saved originals are the .wav files in <code>original_sounds/</code>.</p>\n<input id=\"q\" placeholder=\"Search sounds... (e.g. scream, door, lever)\" autofocus>\n<table><thead><tr><th>Sound name</th><th>Length</th><th>Saved as .wav</th><th></th></tr></thead><tbody id=\"t\">\n");
			foreach (var row in rows)
			{
				string text = (SoundReplacement.Variants.ContainsKey(row.Name) ? "<span class=\"rep\">REPLACED</span>" : "");
				string text2 = (row.Exported ? "<span class=\"yes\">yes</span>" : "<span class=\"no\">no (streamed)</span>");
				stringBuilder.Append("<tr><td class=\"name\">" + Escape(row.Name) + "</td><td>" + row.Length.ToString("F1", CultureInfo.InvariantCulture) + "s</td><td>" + text2 + "</td><td>" + text + "</td></tr>\n");
			}
			stringBuilder.Append("</tbody></table>\n<script>\n const q=document.getElementById('q'),rows=[...document.querySelectorAll('#t tr')];\n q.addEventListener('input',()=>{const v=q.value.toLowerCase();\n   rows.forEach(r=>r.style.display=r.textContent.toLowerCase().includes(v)?'':'none')});\n</script></html>");
			string text3 = Path.Combine(Plugin.PluginDir, "all_sounds.html");
			File.WriteAllText(text3, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			Plugin.Log.LogInfo((object)("[Dump] Wrote list: " + text3));
		}

		private static string Escape(string s)
		{
			return s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
		}
	}
	internal static class WavEncoder
	{
		internal static byte[] Encode(float[] samples, int channels, int frequency)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
			int num = samples.Length * 2;
			binaryWriter.Write(Encoding.ASCII.GetBytes("RIFF"));
			binaryWriter.Write(36 + num);
			binaryWriter.Write(Encoding.ASCII.GetBytes("WAVE"));
			binaryWriter.Write(Encoding.ASCII.GetBytes("fmt "));
			binaryWriter.Write(16);
			binaryWriter.Write((short)1);
			binaryWriter.Write((short)channels);
			binaryWriter.Write(frequency);
			binaryWriter.Write(frequency * channels * 2);
			binaryWriter.Write((short)(channels * 2));
			binaryWriter.Write((short)16);
			binaryWriter.Write(Encoding.ASCII.GetBytes("data"));
			binaryWriter.Write(num);
			foreach (float num2 in samples)
			{
				binaryWriter.Write((short)(Mathf.Clamp(num2, -1f, 1f) * 32767f));
			}
			binaryWriter.Flush();
			return memoryStream.ToArray();
		}
	}
	internal class SoundVariant
	{
		internal AudioClip Clip;

		internal string File;
	}
	internal static class SoundReplacement
	{
		internal static readonly Dictionary<string, List<SoundVariant>> Variants = new Dictionary<string, List<SoundVariant>>(StringComparer.OrdinalIgnoreCase);

		private static readonly Regex VariantSuffix = new Regex("-ver\\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private static readonly Dictionary<string, int> Plays = new Dictionary<string, int>();

		private static int _playsSeed = int.MinValue;

		internal static void LoadAll()
		{
			//IL_008d: Unknown result type (might be due to invalid IL or missing references)
			string text = Path.Combine(Plugin.PluginDir, "sounds");
			if (!Directory.Exists(text))
			{
				Plugin.Log.LogWarning((object)("[Sound] No sounds/ folder (" + text + ") - skipping sound loading."));
				return;
			}
			string[] files = Directory.GetFiles(text);
			Plugin.Log.LogInfo((object)$"[Sound] Found {files.Length} files in sounds/.");
			string[] array = files;
			foreach (string text2 in array)
			{
				AudioType? val = DetectAudioType(text2);
				if (!val.HasValue)
				{
					continue;
				}
				string text3 = ParseName(Path.GetFileNameWithoutExtension(text2));
				AudioClip val2 = LoadFromDisk(text2, val.Value);
				if (!((Object)(object)val2 == (Object)null))
				{
					((Object)val2).name = text3;
					if (!Variants.TryGetValue(text3, out var value))
					{
						value = (Variants[text3] = new List<SoundVariant>());
					}
					value.Add(new SoundVariant
					{
						Clip = val2,
						File = Path.GetFileName(text2)
					});
					Plugin.Log.LogInfo((object)$"[Sound] {text3} ← {Path.GetFileName(text2)} ({val2.length:F1}s)");
				}
			}
			int num = Variants.Count((KeyValuePair<string, List<SoundVariant>> kv) => kv.Value.Count > 1);
			Plugin.Log.LogInfo((object)($"[Sound] Ready: {Variants.Count} sounds can be replaced" + ((num > 0) ? $", {num} of which pick randomly between versions." : ".")));
		}

		private static string ParseName(string fileName)
		{
			Match match = VariantSuffix.Match(fileName);
			if (!match.Success)
			{
				return fileName;
			}
			return fileName.Substring(0, match.Index);
		}

		internal static AudioClip Pick(string clipName)
		{
			if (clipName == null || !Variants.TryGetValue(clipName, out var value) || value.Count == 0)
			{
				return null;
			}
			if (value.Count == 1)
			{
				return value[0].Clip;
			}
			int seed = RoundInfo.Seed;
			if (seed != _playsSeed)
			{
				Plays.Clear();
				_playsSeed = seed;
			}
			Plays.TryGetValue(clipName, out var value2);
			Plays[clipName] = value2 + 1;
			int[] array = ShuffledOrder(clipName, seed, value2 / value.Count, value.Count);
			return value[array[value2 % value.Count]].Clip;
		}

		private static int[] ShuffledOrder(string clipName, int seed, int cycle, int count)
		{
			if (count == 2)
			{
				return Shuffle(clipName, seed, 0, count);
			}
			int[] array = Shuffle(clipName, seed, cycle, count);
			int[] array2 = Shuffle(clipName, seed, cycle + 1, count);
			if (array[count - 1] == array2[0])
			{
				int num = array[count - 1];
				array[count - 1] = array[count - 2];
				array[count - 2] = num;
			}
			return array;
		}

		private static int[] Shuffle(string clipName, int seed, int cycle, int count)
		{
			int[] array = new int[count];
			for (int i = 0; i < count; i++)
			{
				array[i] = i;
			}
			uint num = StableHash(clipName) ^ (uint)seed ^ (uint)(cycle * -1640531535);
			if (num == 0)
			{
				num = 2463534242u;
			}
			for (int num2 = count - 1; num2 > 0; num2--)
			{
				num = NextRandom(num);
				int num3 = (int)(num % (uint)(num2 + 1));
				int num4 = array[num2];
				array[num2] = array[num3];
				array[num3] = num4;
			}
			return array;
		}

		private static uint NextRandom(uint state)
		{
			state ^= state << 13;
			state ^= state >> 17;
			state ^= state << 5;
			if (state != 0)
			{
				return state;
			}
			return 2463534242u;
		}

		private static uint StableHash(string text)
		{
			uint num = 2166136261u;
			foreach (char c in text)
			{
				num ^= char.ToLowerInvariant(c);
				num *= 16777619;
			}
			return num;
		}

		private static AudioType? DetectAudioType(string file)
		{
			return Path.GetExtension(file).ToLowerInvariant() switch
			{
				".ogg" => (AudioType)14, 
				".wav" => (AudioType)20, 
				".mp3" => (AudioType)13, 
				_ => null, 
			};
		}

		private static AudioClip LoadFromDisk(string path, AudioType type)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Invalid comparison between Unknown and I4
			UnityWebRequest audioClip = UnityWebRequestMultimedia.GetAudioClip(path, type);
			try
			{
				audioClip.SendWebRequest();
				try
				{
					while (!audioClip.isDone)
					{
					}
					if ((int)audioClip.result != 1)
					{
						Plugin.Log.LogError((object)("[Sound] Could not load " + Path.GetFileName(path) + ": " + audioClip.error));
						return null;
					}
					return DownloadHandlerAudioClip.GetContent(audioClip);
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)("[Sound] Error loading " + Path.GetFileName(path) + ": " + ex.Message));
					return null;
				}
			}
			finally
			{
				((IDisposable)audioClip)?.Dispose();
			}
		}
	}
	internal static class SoundPatcher
	{
		internal static void Apply(Harmony harmony)
		{
			Type typeFromHandle = typeof(AudioSource);
			Type typeFromHandle2 = typeof(SoundPatcher);
			Hook(harmony, AccessTools.Method(typeFromHandle, "Play", new Type[0], (Type[])null), typeFromHandle2, "OnSourcePlay");
			Hook(harmony, AccessTools.Method(typeFromHandle, "Play", new Type[1] { typeof(ulong) }, (Type[])null), typeFromHandle2, "OnSourcePlay");
			Hook(harmony, AccessTools.Method(typeFromHandle, "PlayDelayed", new Type[1] { typeof(float) }, (Type[])null), typeFromHandle2, "OnSourcePlay");
			Hook(harmony, AccessTools.Method(typeFromHandle, "PlayScheduled", new Type[1] { typeof(double) }, (Type[])null), typeFromHandle2, "OnSourcePlay");
			Hook(harmony, AccessTools.Method(typeFromHandle, "PlayOneShotHelper", new Type[3]
			{
				typeof(AudioSource),
				typeof(AudioClip),
				typeof(float)
			}, (Type[])null), typeFromHandle2, "OnPlayOneShot");
			Hook(harmony, AccessTools.Method(typeFromHandle, "PlayClipAtPoint", new Type[3]
			{
				typeof(AudioClip),
				typeof(Vector3),
				typeof(float)
			}, (Type[])null), typeFromHandle2, "OnClipAtPoint");
		}

		private static void Hook(Harmony harmony, MethodBase target, Type owner, string prefix)
		{
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Expected O, but got Unknown
			if (target == null)
			{
				Plugin.Log.LogWarning((object)("[Sound] Could not find AudioSource method for " + prefix + " - skipped."));
				return;
			}
			try
			{
				harmony.Patch(target, new HarmonyMethod(AccessTools.Method(owner, prefix, (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Sound] Could not patch " + target.Name + ": " + ex.Message));
			}
		}

		private static void OnSourcePlay(AudioSource __instance)
		{
			if (!((Object)(object)__instance == (Object)null) && !((Object)(object)__instance.clip == (Object)null))
			{
				SoundCatalog.Note(__instance.clip);
				AudioClip val = SoundReplacement.Pick(((Object)__instance.clip).name);
				if ((Object)(object)val != (Object)null)
				{
					__instance.clip = val;
				}
			}
		}

		private static void OnPlayOneShot(AudioSource source, ref AudioClip clip)
		{
			if (!((Object)(object)clip == (Object)null))
			{
				SoundCatalog.Note(clip);
				AudioClip val = SoundReplacement.Pick(((Object)clip).name);
				if ((Object)(object)val != (Object)null)
				{
					clip = val;
				}
			}
		}

		private static void OnClipAtPoint(ref AudioClip clip)
		{
			if (!((Object)(object)clip == (Object)null))
			{
				SoundCatalog.Note(clip);
				AudioClip val = SoundReplacement.Pick(((Object)clip).name);
				if ((Object)(object)val != (Object)null)
				{
					clip = val;
				}
			}
		}
	}
	[HarmonyPatch(typeof(StartOfRound))]
	internal static class SuitLoader
	{
		private static bool _added;

		[HarmonyPatch("Start")]
		[HarmonyPrefix]
		private static void AddSuits(ref StartOfRound __instance)
		{
			if (_added)
			{
				return;
			}
			try
			{
				Add(__instance);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[Suits] Failed: {arg}");
			}
		}

		private static void Add(StartOfRound round)
		{
			//IL_018b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0190: Unknown result type (might be due to invalid IL or missing references)
			//IL_0197: Unknown result type (might be due to invalid IL or missing references)
			//IL_019c: Unknown result type (might be due to invalid IL or missing references)
			string text = Path.Combine(Plugin.PluginDir, "suits");
			if (!Directory.Exists(text))
			{
				Plugin.Log.LogInfo((object)"[Suits] No suits/ folder - skipping.");
				_added = true;
				return;
			}
			List<string> list = (from p in Directory.GetFiles(text, "*.png")
				where !Path.GetFileName(p).StartsWith("_")
				select p).OrderBy<string, string>((string p) => Path.GetFileName(p), StringComparer.OrdinalIgnoreCase).ToList();
			if (list.Count == 0)
			{
				_added = true;
				return;
			}
			UnlockableItem val = ((IEnumerable<UnlockableItem>)round.unlockablesList.unlockables).FirstOrDefault((Func<UnlockableItem, bool>)((UnlockableItem u) => (Object)(object)u.suitMaterial != (Object)null && u.alreadyUnlocked));
			if (val == null)
			{
				Plugin.Log.LogWarning((object)"[Suits] Could not find the game's default suit - skipping.");
				return;
			}
			if (Settings.ExportSuitTemplate != null && Settings.ExportSuitTemplate.Value)
			{
				ExportTemplate(val.suitMaterial, text);
			}
			int num = 0;
			foreach (string item in list)
			{
				try
				{
					string text2 = DisplayName(Path.GetFileNameWithoutExtension(item));
					UnlockableItem val2 = JsonUtility.FromJson<UnlockableItem>(JsonUtility.ToJson((object)val));
					Material val3 = Object.Instantiate<Material>(val2.suitMaterial);
					val3.mainTexture = (Texture)(object)LoadTexture(item);
					val3.SetFloat("_NormalScale", 0f);
					ApplyAdvanced(val3, text, text2);
					val2.suitMaterial = val3;
					val2.unlockableName = text2;
					val2.alreadyUnlocked = true;
					val2.hasBeenMoved = false;
					val2.placedPosition = Vector3.zero;
					val2.placedRotation = Vector3.zero;
					round.unlockablesList.unlockables.Add(val2);
					num++;
					Plugin.Log.LogInfo((object)("[Suits] Added suit: " + text2));
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("[Suits] Could not add " + Path.GetFileName(item) + ": " + ex.Message));
				}
			}
			_added = true;
			Plugin.Log.LogInfo((object)$"[Suits] Ready: {num} suits added.");
		}

		private static string DisplayName(string fileName)
		{
			int num = fileName.IndexOf('-');
			if (num <= 0 || num == fileName.Length - 1)
			{
				return fileName;
			}
			return fileName.Substring(num + 1).Trim();
		}

		private static void ExportTemplate(Material source, string suitsDir)
		{
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Expected O, but got Unknown
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			string path = Path.Combine(suitsDir, "_TEMPLATE.png");
			if (File.Exists(path))
			{
				return;
			}
			Texture mainTexture = source.mainTexture;
			if ((Object)(object)mainTexture == (Object)null)
			{
				return;
			}
			RenderTexture temporary = RenderTexture.GetTemporary(mainTexture.width, mainTexture.height, 0, (RenderTextureFormat)7, (RenderTextureReadWrite)2);
			RenderTexture active = RenderTexture.active;
			try
			{
				Graphics.Blit(mainTexture, temporary);
				RenderTexture.active = temporary;
				Texture2D val = new Texture2D(mainTexture.width, mainTexture.height, (TextureFormat)4, false);
				val.ReadPixels(new Rect(0f, 0f, (float)((Texture)temporary).width, (float)((Texture)temporary).height), 0, 0);
				val.Apply();
				File.WriteAllBytes(path, ImageConversion.EncodeToPNG(val));
				Plugin.Log.LogInfo((object)$"[Suits] Wrote template: suits/_TEMPLATE.png ({mainTexture.width}x{mainTexture.height})");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Suits] Could not write template: " + ex.Message));
			}
			finally
			{
				RenderTexture.active = active;
				RenderTexture.ReleaseTemporary(temporary);
			}
		}

		private static Texture2D LoadTexture(string path)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Expected O, but got Unknown
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			Texture2D val = new Texture2D(2, 2);
			ImageConversion.LoadImage(val, File.ReadAllBytes(path));
			val.Apply(true, true);
			return val;
		}

		private static void ApplyAdvanced(Material material, string suitsDir, string suitName)
		{
			//IL_017d: Unknown result type (might be due to invalid IL or missing references)
			string path = Path.Combine(suitsDir, "advanced", suitName + ".json");
			if (!File.Exists(path))
			{
				return;
			}
			string[] array = File.ReadAllLines(path);
			for (int i = 0; i < array.Length; i++)
			{
				string[] array2 = array[i].Trim().Split(':');
				if (array2.Length != 2)
				{
					continue;
				}
				string text = array2[0].Trim('"', ' ', ',');
				string text2 = array2[1].Trim('"', ' ', ',');
				if (text.Length == 0 || text2.Length == 0)
				{
					continue;
				}
				try
				{
					float result;
					Vector4 v;
					if (text2.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
					{
						string path2 = Path.Combine(suitsDir, "advanced", text2);
						if (File.Exists(path2))
						{
							material.SetTexture(text, (Texture)(object)LoadTexture(path2));
						}
						else
						{
							Plugin.Log.LogWarning((object)("[Suits] " + suitName + ": missing texture " + text2));
						}
					}
					else if (text2 == "KEYWORD")
					{
						material.EnableKeyword(text);
					}
					else if (text2 == "DISABLEKEYWORD")
					{
						material.DisableKeyword(text);
					}
					else if (text == "SHADER")
					{
						Shader val = Shader.Find(text2);
						if ((Object)(object)val != (Object)null)
						{
							material.shader = val;
						}
					}
					else if (float.TryParse(text2, NumberStyles.Float, CultureInfo.InvariantCulture, out result))
					{
						material.SetFloat(text, result);
					}
					else if (TryParseVector(text2, out v))
					{
						material.SetVector(text, v);
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("[Suits] " + suitName + ": could not apply '" + text + "': " + ex.Message));
				}
			}
		}

		private static bool TryParseVector(string s, out Vector4 v)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			v = Vector4.zero;
			string[] array = s.Split(',');
			if (array.Length < 2 || array.Length > 4)
			{
				return false;
			}
			float[] array2 = new float[4];
			for (int i = 0; i < array.Length; i++)
			{
				if (!float.TryParse(array[i].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out array2[i]))
				{
					return false;
				}
			}
			v = new Vector4(array2[0], array2[1], array2[2], array2[3]);
			return true;
		}

		[HarmonyPatch("PositionSuitsOnRack")]
		[HarmonyPrefix]
		private static bool PositionSuitsOnRack(ref StartOfRound __instance)
		{
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0098: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				List<UnlockableSuit> list = (from s in Object.FindObjectsOfType<UnlockableSuit>()
					orderby s.syncedSuitID.Value
					select s).ToList();
				float num = 0.18f;
				if (list.Count > 13)
				{
					num /= (float)Math.Min(list.Count, 20) / 12f;
				}
				for (int num2 = 0; num2 < list.Count; num2++)
				{
					AutoParentToShip component = ((Component)list[num2]).gameObject.GetComponent<AutoParentToShip>();
					if (!((Object)(object)component == (Object)null))
					{
						component.overrideOffset = true;
						component.positionOffset = new Vector3(-2.45f, 2.75f, -8.41f) + __instance.rightmostSuitPosition.forward * num * (float)num2;
						component.rotationOffset = new Vector3(0f, 90f, 0f);
					}
				}
				return false;
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[Suits] Rack layout failed: {arg}");
				return true;
			}
		}
	}
}