Decompiled source of NobleMod v1.0.2

NobleMod.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;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("Raisery")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("NobleMod")]
[assembly: AssemblyTitle("NobleMod")]
[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.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace NobleMod
{
	internal static class ClipFileLoader
	{
		public static AudioClip LoadFromPath(string path)
		{
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Invalid comparison between Unknown and I4
			if (Path.GetExtension(path)?.ToLowerInvariant() != ".ogg")
			{
				return null;
			}
			UnityWebRequest audioClip = UnityWebRequestMultimedia.GetAudioClip(new Uri(path), (AudioType)14);
			try
			{
				UnityWebRequestAsyncOperation val = audioClip.SendWebRequest();
				while (!((AsyncOperation)val).isDone)
				{
				}
				if ((int)audioClip.result != 1)
				{
					return null;
				}
				return DownloadHandlerAudioClip.GetContent(audioClip);
			}
			finally
			{
				((IDisposable)audioClip)?.Dispose();
			}
		}
	}
	internal static class LevelEnemyOverrideBank
	{
		private static readonly Dictionary<int, string> MobByLevel = new Dictionary<int, string>();

		private static ManualLogSource _logger;

		private static string _spawnConfigRootPath;

		public static void Initialize(ManualLogSource logger)
		{
			_logger = logger;
			Reload();
		}

		public static bool TryGetMobKey(int levelNumber, out string mobKey)
		{
			if (MobByLevel.TryGetValue(levelNumber, out mobKey) && !string.IsNullOrWhiteSpace(mobKey))
			{
				return true;
			}
			mobKey = null;
			return false;
		}

		private static void Reload()
		{
			MobByLevel.Clear();
			_spawnConfigRootPath = Path.Combine(Plugin.AssemblyDirectory, "SpawnConfig");
			Directory.CreateDirectory(_spawnConfigRootPath);
			string text = ModConfig.SpawnOverridesFileName.Value;
			if (string.IsNullOrWhiteSpace(text))
			{
				text = "level_enemy_overrides.json";
			}
			string text2 = Path.Combine(_spawnConfigRootPath, text);
			if (!File.Exists(text2))
			{
				WriteDefaultTemplate(text2);
				_logger.LogWarning((object)("Missing spawn override file: " + text2 + ". Template created."));
				return;
			}
			try
			{
				foreach (Match item in Regex.Matches(File.ReadAllText(text2, Encoding.UTF8), "(?:\"(?<key1>[^\"]+)\"|(?<key2>\\d+))\\s*:\\s*\"(?<value>[^\"]+)\"", RegexOptions.IgnoreCase))
				{
					string text3 = (item.Groups["key1"].Success ? item.Groups["key1"].Value : item.Groups["key2"].Value);
					string value = item.Groups["value"].Value.Trim();
					if (!string.IsNullOrWhiteSpace(text3) && !string.IsNullOrWhiteSpace(value) && TryParseLevelKey(text3, out var levelNumber) && levelNumber > 0)
					{
						MobByLevel[levelNumber] = NormalizeMobKey(value);
					}
				}
				if (ModConfig.LogSpawnOverrides.Value)
				{
					_logger.LogInfo((object)$"Loaded {MobByLevel.Count} level mob override(s) from '{text2}'.");
				}
			}
			catch (Exception ex)
			{
				_logger.LogError((object)("Failed to parse spawn override file '" + text2 + "': " + ex.Message));
			}
		}

		private static bool TryParseLevelKey(string rawKey, out int levelNumber)
		{
			levelNumber = 0;
			if (string.IsNullOrWhiteSpace(rawKey))
			{
				return false;
			}
			Match match = Regex.Match(rawKey, "\\d+");
			if (!match.Success)
			{
				return false;
			}
			return int.TryParse(match.Value, out levelNumber);
		}

		private static string NormalizeMobKey(string value)
		{
			return value.Trim().ToLowerInvariant();
		}

		private static void WriteDefaultTemplate(string outputPath)
		{
			File.WriteAllText(outputPath, "{\n  \"1\": \"huntsman\",\n  \"2\": \"headman\"\n}\n", Encoding.UTF8);
		}
	}
	internal static class ModConfig
	{
		public static ConfigEntry<bool> EnableCustomSounds;

		public static ConfigEntry<string> AudioSourceHierarchyFilter;

		public static ConfigEntry<bool> LogReplacements;

		public static ConfigEntry<bool> LogUnknownVanillaClipNamesOnce;

		public static ConfigEntry<bool> WriteDiscoveredClipsHierarchyJson;

		public static ConfigEntry<string> DiscoveredClipsHierarchyFileName;

		public static ConfigEntry<string> DiscoveredClipsOutputPath;

		public static ConfigEntry<bool> EnableSpawnOverrides;

		public static ConfigEntry<string> SpawnOverridesFileName;

		public static ConfigEntry<bool> LogSpawnOverrides;

		public static ConfigEntry<bool> DebugLogHookEntrypoints;

		public static ConfigEntry<int> DebugLogHookEntrypointsPerMethod;

		public static ConfigEntry<bool> DebugSuppressMenuSpam;

		public static ConfigEntry<bool> LogWeightedSoundPicks;

		public static void Bind(ConfigFile config)
		{
			EnableCustomSounds = config.Bind<bool>("General", "EnableCustomSounds", true, "Active les remplacements de sons personnalises.");
			AudioSourceHierarchyFilter = config.Bind<string>("General", "AudioSourceHierarchyFilter", "", "Limite remplacements / decouverte aux AudioSource dont la hierarchie (noms des transforms) contient cette sous-chaine. Vide = tout le jeu (generique).");
			LogReplacements = config.Bind<bool>("General", "LogReplacements", false, "Active les logs de remplacement audio.");
			LogUnknownVanillaClipNamesOnce = config.Bind<bool>("General", "LogUnknownVanillaClipNamesOnce", false, "Log chaque nom de clip vanilla non mappe une seule fois (utile pour completer replacements.json).");
			WriteDiscoveredClipsHierarchyJson = config.Bind<bool>("General", "WriteDiscoveredClipsHierarchyJson", true, "Ecrit les nouveaux noms de clips rencontres dans un fichier JSON hierarchique (sans doublons).");
			DiscoveredClipsHierarchyFileName = config.Bind<string>("General", "DiscoveredClipsHierarchyFileName", "discovered_clips_hierarchy.json", "Nom du fichier JSON hierarchique qui stocke les clips decouverts.");
			DiscoveredClipsOutputPath = config.Bind<string>("General", "DiscoveredClipsOutputPath", "", "Chemin absolu du dossier ou ecrire le JSON des clips decouverts (ex: .../votre-clone/CustomSounds). Vide = CustomSounds du plugin.");
			EnableSpawnOverrides = config.Bind<bool>("Spawning", "EnableSpawnOverrides", false, "Active le remplacement du mob principal via un mapping niveau -> mob (fichier JSON dans SpawnConfig).");
			SpawnOverridesFileName = config.Bind<string>("Spawning", "SpawnOverridesFileName", "level_enemy_overrides.json", "Nom du fichier JSON contenant le mapping niveau -> identifiant de mob.");
			LogSpawnOverrides = config.Bind<bool>("Spawning", "LogSpawnOverrides", true, "Log les decisions de remplacement des mobs principaux (selection, fallback, erreurs).");
			DebugLogHookEntrypoints = config.Bind<bool>("Debug", "DebugLogHookEntrypoints", false, "Log les entrees des hooks audio (meme si source/clip est null).");
			DebugLogHookEntrypointsPerMethod = config.Bind<int>("Debug", "DebugLogHookEntrypointsPerMethod", 500, "Nombre max de logs d'entree par hook/methode pour eviter le spam (<= 0 = illimite).");
			DebugSuppressMenuSpam = config.Bind<bool>("Debug", "DebugSuppressMenuSpam", true, "Masque le bruit des logs audio menu/UI (ex: menu hover, PlayHelper) pour faciliter le debug en partie.");
			LogWeightedSoundPicks = config.Bind<bool>("Debug", "LogWeightedSoundPicks", false, "Log chaque tirage pondere (Random.value, creneaux cumules, resultat). Utile pour comprendre vanilla % vs customs. Les reutilisations meme frame (cache) ou boucle (sticky) sont en LogDebug.");
		}
	}
	internal static class NobleModMenuIntegration
	{
		private static Assembly _menuLib;

		private static Type _menuApiType;

		private static MethodInfo _miCreateButton;

		private static MethodInfo _miCreateToggle;

		private static MethodInfo _miCreatePopup;

		private static Type _presetSideType;

		private static object _popup;

		private static object _toggleSounds;

		private static object _toggleLogWeightedPicks;

		private static object _toggleSpawn;

		private static bool _methodsResolved;

		private static void ClearNoblePopupCache()
		{
			_popup = null;
			_toggleSounds = null;
			_toggleLogWeightedPicks = null;
			_toggleSpawn = null;
		}

		private static bool IsUnityObjAlive(object u)
		{
			Object val = (Object)((u is Object) ? u : null);
			if (val != null)
			{
				return val != (Object)null;
			}
			return false;
		}

		private static object GetRepoPopupMenuPage(object repoPopupPage)
		{
			return repoPopupPage?.GetType().GetField("menuPage", BindingFlags.Instance | BindingFlags.Public)?.GetValue(repoPopupPage);
		}

		internal static void TryRegister(ManualLogSource log)
		{
			try
			{
				_menuLib = Array.Find(AppDomain.CurrentDomain.GetAssemblies(), (Assembly a) => string.Equals(a.GetName().Name, "MenuLib", StringComparison.Ordinal));
				if (_menuLib == null)
				{
					log.LogWarning((object)"[NobleMod] MenuLib introuvable. Installez la dependance 'MenuLib' (Thunderstore, auteur nickklmao). Le bouton NobleMod dans Parametres ne sera pas ajoute.");
					return;
				}
				_menuApiType = _menuLib.GetType("MenuLib.MenuAPI", throwOnError: false);
				if (_menuApiType == null)
				{
					log.LogError((object)"[NobleMod] Type MenuLib.MenuAPI introuvable.");
					return;
				}
				MethodInfo method = _menuApiType.GetMethod("AddElementToSettingsMenu", BindingFlags.Static | BindingFlags.Public);
				if (method == null)
				{
					log.LogError((object)"[NobleMod] MenuAPI.AddElementToSettingsMenu introuvable.");
					return;
				}
				Type parameterType = method.GetParameters()[0].ParameterType;
				MethodInfo method2 = typeof(NobleModMenuIntegration).GetMethod("OnSettingsMenuBuilt", BindingFlags.Static | BindingFlags.NonPublic);
				Delegate @delegate = Delegate.CreateDelegate(parameterType, method2);
				method.Invoke(null, new object[1] { @delegate });
				log.LogInfo((object)"[NobleMod] MenuLib: bouton ajoute au menu Parametres du jeu.");
			}
			catch (Exception arg)
			{
				log.LogError((object)$"[NobleMod] Integration MenuLib: {arg}");
			}
		}

		private static void OnSettingsMenuBuilt(Transform parent)
		{
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				ResolveMenuApiMethods();
				Transform val = FindSettingsLeftNavControlsRow(parent);
				if ((Object)(object)val != (Object)null && (Object)(object)val.parent != (Object)null)
				{
					Transform parent2 = val.parent;
					object? obj = _miCreateButton.Invoke(null, new object[4]
					{
						"NOBLEMOD",
						new Action(OpenNobleModPopup),
						parent2,
						Vector2.zero
					});
					PlaceNobleButtonUnderControlsNav((Component)((obj is Component) ? obj : null), val);
					return;
				}
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogWarning((object)"[NobleMod] Entree CONTROLS (liste gauche) introuvable; placement repli sur le scroll.");
				}
				Transform val2 = FindMenuScrollScroller(parent) ?? parent;
				Vector2 val3 = ComputeAppendLocalPosition(val2);
				object? obj2 = _miCreateButton.Invoke(null, new object[4]
				{
					"NobleMod — reglages du mod",
					new Action(OpenNobleModPopup),
					val2,
					val3
				});
				object? obj3 = ((obj2 is Component) ? obj2 : null);
				if (obj3 != null)
				{
					((Component)obj3).transform.SetAsLastSibling();
				}
			}
			catch (Exception arg)
			{
				ManualLogSource log2 = Plugin.Log;
				if (log2 != null)
				{
					log2.LogError((object)$"[NobleMod] OnSettingsMenuBuilt: {arg}");
				}
			}
		}

		private static Transform FindSettingsLeftNavControlsRow(Transform pageRoot)
		{
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			Transform result = null;
			float num = float.MaxValue;
			Transform[] componentsInChildren = ((Component)pageRoot).GetComponentsInChildren<Transform>(true);
			foreach (Transform val in componentsInChildren)
			{
				Component val2 = TryGetTmpTextComponent(val);
				if ((Object)(object)val2 == (Object)null)
				{
					continue;
				}
				string tmpText = GetTmpText(val2);
				if (string.IsNullOrEmpty(tmpText))
				{
					continue;
				}
				string text = tmpText.Trim();
				if (!text.Equals("CONTROLS", StringComparison.OrdinalIgnoreCase) && !text.Equals("CONTRÔLES", StringComparison.OrdinalIgnoreCase))
				{
					continue;
				}
				Transform parent = val.parent;
				if (!((Object)(object)parent == (Object)null))
				{
					float x = parent.position.x;
					if (x < num)
					{
						num = x;
						result = parent;
					}
				}
			}
			return result;
		}

		private static Component TryGetTmpTextComponent(Transform tr)
		{
			Component component = ((Component)tr).GetComponent("TMPro.TextMeshProUGUI");
			if ((Object)(object)component != (Object)null)
			{
				return component;
			}
			return ((Component)tr).GetComponent("TextMeshProUGUI");
		}

		private static string GetTmpText(Component tmp)
		{
			return ((object)tmp).GetType().GetProperty("text")?.GetValue(tmp) as string;
		}

		private static void PlaceNobleButtonUnderControlsNav(Component btnComponent, Transform controlsRow)
		{
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_0101: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_011a: Unknown result type (might be due to invalid IL or missing references)
			//IL_012f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_0145: Unknown result type (might be due to invalid IL or missing references)
			//IL_0168: Unknown result type (might be due to invalid IL or missing references)
			//IL_017f: Unknown result type (might be due to invalid IL or missing references)
			//IL_018d: 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)
			Transform obj = ((btnComponent != null) ? btnComponent.transform : null);
			RectTransform val = (RectTransform)(object)((obj is RectTransform) ? obj : null);
			RectTransform val2 = (RectTransform)(object)((controlsRow is RectTransform) ? controlsRow : null);
			Transform val3 = ((controlsRow != null) ? controlsRow.parent : null);
			if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null || (Object)(object)val3 == (Object)null)
			{
				return;
			}
			int siblingIndex = controlsRow.GetSiblingIndex();
			RectTransform val4 = null;
			if (siblingIndex + 1 < val3.childCount)
			{
				Transform child = val3.GetChild(siblingIndex + 1);
				val4 = (RectTransform)(object)((child is RectTransform) ? child : null);
			}
			((Transform)val).SetSiblingIndex(siblingIndex + 1);
			val.anchorMin = val2.anchorMin;
			val.anchorMax = val2.anchorMax;
			val.pivot = val2.pivot;
			val.sizeDelta = val2.sizeDelta;
			float num;
			if (siblingIndex > 0)
			{
				Transform child2 = val3.GetChild(siblingIndex - 1);
				RectTransform val5 = (RectTransform)(object)((child2 is RectTransform) ? child2 : null);
				if (val5 != null)
				{
					num = ((Transform)val2).localPosition.y - ((Transform)val5).localPosition.y;
					goto IL_012d;
				}
			}
			num = ((!((Object)(object)val4 != (Object)null)) ? (0f - (((val2.sizeDelta.y > 8f) ? val2.sizeDelta.y : 40f) + 6f)) : ((((Transform)val4).localPosition.y - ((Transform)val2).localPosition.y) * 0.5f));
			goto IL_012d;
			IL_012d:
			((Transform)val).localPosition = ((Transform)val2).localPosition + new Vector3(0f, num, 0f);
			if ((Object)(object)val4 != (Object)null && Mathf.Abs(num) > 0.01f)
			{
				float num2 = ((Transform)val).localPosition.y + num;
				RectTransform val6 = val4;
				((Transform)val6).localPosition = new Vector3(((Transform)val6).localPosition.x, num2, ((Transform)val6).localPosition.z);
			}
		}

		private static Vector2 ComputeAppendLocalPosition(Transform content)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)content == (Object)null)
			{
				return new Vector2(250f, -40f);
			}
			float num = float.MaxValue;
			bool flag = false;
			for (int i = 0; i < content.childCount; i++)
			{
				Transform child = content.GetChild(i);
				RectTransform val = (RectTransform)(object)((child is RectTransform) ? child : null);
				if (val != null && !IsChromeScrollChild(child))
				{
					float localBottomY = GetLocalBottomY(val, content);
					if (localBottomY < num)
					{
						num = localBottomY;
						flag = true;
					}
				}
			}
			float num2 = (flag ? (num - 22f) : (-40f));
			return new Vector2(250f, num2);
		}

		private static bool IsChromeScrollChild(Transform child)
		{
			string text = ((Object)child).name ?? string.Empty;
			if (text.IndexOf("scroll", StringComparison.OrdinalIgnoreCase) >= 0 && text.IndexOf("bar", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				return true;
			}
			return false;
		}

		private static float GetLocalBottomY(RectTransform rt, Transform contentParent)
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			Vector3[] array = (Vector3[])(object)new Vector3[4];
			rt.GetWorldCorners(array);
			float num = float.MaxValue;
			for (int i = 0; i < 4; i++)
			{
				Vector3 val = contentParent.InverseTransformPoint(array[i]);
				if (val.y < num)
				{
					num = val.y;
				}
			}
			return num;
		}

		private static void ResolveMenuApiMethods()
		{
			if (_methodsResolved)
			{
				return;
			}
			MethodInfo[] methods = _menuApiType.GetMethods(BindingFlags.Static | BindingFlags.Public);
			foreach (MethodInfo methodInfo in methods)
			{
				if (methodInfo.Name == "CreateREPOButton")
				{
					ParameterInfo[] parameters = methodInfo.GetParameters();
					if (parameters.Length == 4 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == typeof(Action) && parameters[2].ParameterType == typeof(Transform) && parameters[3].ParameterType == typeof(Vector2))
					{
						_miCreateButton = methodInfo;
					}
				}
				else if (methodInfo.Name == "CreateREPOToggle")
				{
					ParameterInfo[] parameters2 = methodInfo.GetParameters();
					if (parameters2.Length == 7 && parameters2[0].ParameterType == typeof(string) && parameters2[1].ParameterType == typeof(Action<bool>) && parameters2[2].ParameterType == typeof(Transform) && parameters2[3].ParameterType == typeof(Vector2) && parameters2[4].ParameterType == typeof(string) && parameters2[5].ParameterType == typeof(string) && parameters2[6].ParameterType == typeof(bool))
					{
						_miCreateToggle = methodInfo;
					}
				}
				else if (methodInfo.Name == "CreateREPOPopupPage")
				{
					ParameterInfo[] parameters3 = methodInfo.GetParameters();
					if (parameters3.Length >= 5 && parameters3[0].ParameterType == typeof(string) && parameters3[1].ParameterType.IsEnum && parameters3[2].ParameterType == typeof(bool) && parameters3[3].ParameterType == typeof(bool) && parameters3[4].ParameterType == typeof(float))
					{
						_miCreatePopup = methodInfo;
						_presetSideType = parameters3[1].ParameterType;
					}
				}
			}
			if (_miCreateButton == null || _miCreateToggle == null || _miCreatePopup == null || _presetSideType == null)
			{
				throw new InvalidOperationException("Signatures MenuAPI inattendues (mettre a jour NobleModMenuIntegration).");
			}
			_methodsResolved = true;
		}

		private static Transform FindMenuScrollScroller(Transform pageRoot)
		{
			MonoBehaviour[] componentsInChildren = ((Component)pageRoot).GetComponentsInChildren<MonoBehaviour>(true);
			foreach (MonoBehaviour val in componentsInChildren)
			{
				if (!((Object)(object)val == (Object)null) && !(((object)val).GetType().Name != "MenuScrollBox"))
				{
					object? obj = ((object)val).GetType().GetField("scroller", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(val);
					Transform val2 = (Transform)((obj is Transform) ? obj : null);
					if (val2 != null)
					{
						return val2;
					}
				}
			}
			return null;
		}

		private static void OpenNobleModPopup()
		{
			try
			{
				EnsureNoblePopup();
				if (!IsUnityObjAlive(_popup))
				{
					ManualLogSource log = Plugin.Log;
					if (log != null)
					{
						log.LogError((object)"[NobleMod] Popup NobleMod introuvable apres EnsureNoblePopup.");
					}
					return;
				}
				MethodInfo methodInfo = (_toggleSounds?.GetType())?.GetMethod("SetState", new Type[2]
				{
					typeof(bool),
					typeof(bool)
				});
				if (IsUnityObjAlive(_toggleSounds))
				{
					methodInfo?.Invoke(_toggleSounds, new object[2]
					{
						ModConfig.EnableCustomSounds.Value,
						false
					});
				}
				if (IsUnityObjAlive(_toggleLogWeightedPicks))
				{
					methodInfo?.Invoke(_toggleLogWeightedPicks, new object[2]
					{
						ModConfig.LogWeightedSoundPicks.Value,
						false
					});
				}
				if (IsUnityObjAlive(_toggleSpawn))
				{
					methodInfo?.Invoke(_toggleSpawn, new object[2]
					{
						ModConfig.EnableSpawnOverrides.Value,
						false
					});
				}
				_popup.GetType().GetMethod("OpenPage", new Type[1] { typeof(bool) })?.Invoke(_popup, new object[1] { false });
			}
			catch (Exception arg)
			{
				ManualLogSource log2 = Plugin.Log;
				if (log2 != null)
				{
					log2.LogError((object)$"[NobleMod] OpenNobleModPopup: {arg}");
				}
			}
		}

		private static void EnsureNoblePopup()
		{
			//IL_0186: Unknown result type (might be due to invalid IL or missing references)
			//IL_021a: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ae: Unknown result type (might be due to invalid IL or missing references)
			//IL_0342: Unknown result type (might be due to invalid IL or missing references)
			if (!IsUnityObjAlive(_popup) || !IsUnityObjAlive(GetRepoPopupMenuPage(_popup)))
			{
				ClearNoblePopupCache();
				ResolveMenuApiMethods();
				object obj = Enum.Parse(_presetSideType, "Left");
				_popup = _miCreatePopup.Invoke(null, new object[5] { "NobleMod", obj, true, true, 0f });
				Type type = _popup.GetType();
				object obj2 = type.GetField("menuScrollBox", BindingFlags.Instance | BindingFlags.Public)?.GetValue(_popup);
				if (obj2 == null)
				{
					throw new InvalidOperationException("REPOPopupPage.menuScrollBox null (Awake pas encore execute ?).");
				}
				object? obj3 = obj2.GetType().GetField("scroller", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj2);
				Transform val = (Transform)((obj3 is Transform) ? obj3 : null);
				if ((Object)(object)val == (Object)null)
				{
					throw new InvalidOperationException("MenuScrollBox.scroller null.");
				}
				MethodInfo method = type.GetMethod("AddElementToScrollView", BindingFlags.Instance | BindingFlags.Public, null, new Type[4]
				{
					typeof(RectTransform),
					typeof(Vector2),
					typeof(float),
					typeof(float)
				}, null);
				if (method == null)
				{
					throw new InvalidOperationException("REPOPopupPage.AddElementToScrollView(RectTransform, ...) introuvable.");
				}
				RegisterPopupScrollElement(ctrl: (Component)(_toggleSounds = (object)/*isinst with value type is only supported in some contexts*/), popupPage: _popup, addElementToScrollView: method, topPad: 14f, bottomPad: 0f);
				RegisterPopupScrollElement(ctrl: (Component)(_toggleLogWeightedPicks = (object)/*isinst with value type is only supported in some contexts*/), popupPage: _popup, addElementToScrollView: method, topPad: 0f, bottomPad: 0f);
				RegisterPopupScrollElement(ctrl: (Component)(_toggleSpawn = (object)/*isinst with value type is only supported in some contexts*/), popupPage: _popup, addElementToScrollView: method, topPad: 0f, bottomPad: 0f);
				object? obj4 = _miCreateButton.Invoke(null, new object[4]
				{
					"Fermer",
					(Action)delegate
					{
						_popup.GetType().GetMethod("ClosePage", new Type[1] { typeof(bool) })?.Invoke(_popup, new object[1] { false });
					},
					val,
					Vector2.zero
				});
				Component ctrl4 = (Component)((obj4 is Component) ? obj4 : null);
				RegisterPopupScrollElement(_popup, method, ctrl4, 0f, 20f);
				object obj5 = type.GetField("scrollView", BindingFlags.Instance | BindingFlags.Public)?.GetValue(_popup);
				(obj5?.GetType().GetMethod("UpdateElements", BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null))?.Invoke(obj5, null);
				(obj5?.GetType().GetMethod("SetScrollPosition", BindingFlags.Instance | BindingFlags.Public, null, new Type[1] { typeof(float) }, null))?.Invoke(obj5, new object[1] { 0f });
			}
		}

		private static void RegisterPopupScrollElement(object popupPage, MethodInfo addElementToScrollView, Component ctrl, float topPad, float bottomPad)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)ctrl == (Object)null) && !(addElementToScrollView == null))
			{
				Transform transform = ctrl.transform;
				RectTransform val = (RectTransform)(object)((transform is RectTransform) ? transform : null);
				if (!((Object)(object)val == (Object)null))
				{
					addElementToScrollView.Invoke(popupPage, new object[4]
					{
						val,
						Vector2.zero,
						topPad,
						bottomPad
					});
				}
			}
		}

		private static void SaveCfg()
		{
			try
			{
				Plugin instance = Plugin.Instance;
				if (instance != null)
				{
					((BaseUnityPlugin)instance).Config.Save();
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogWarning((object)("[NobleMod] Sauvegarde config: " + ex.Message));
				}
			}
		}
	}
	[BepInPlugin("raisery.noblemod", "NobleMod", "1.0.2")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		internal static Plugin Instance;

		internal static Harmony Harmony;

		internal static ManualLogSource Log;

		internal static string AssemblyDirectory { get; private set; } = "";


		private void Awake()
		{
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Expected O, but got Unknown
			AssemblyDirectory = Path.GetDirectoryName(typeof(Plugin).Assembly.Location) ?? "";
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			ModConfig.Bind(((BaseUnityPlugin)this).Config);
			SoundBank.Initialize(((BaseUnityPlugin)this).Logger);
			LevelEnemyOverrideBank.Initialize(((BaseUnityPlugin)this).Logger);
			Harmony = new Harmony("raisery.noblemod");
			Harmony.PatchAll();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"NobleMod v1.0.2 loaded.");
			NobleModMenuIntegration.TryRegister(((BaseUnityPlugin)this).Logger);
		}

		private void OnDestroy()
		{
		}
	}
	internal static class PluginInfo
	{
		public const string Guid = "raisery.noblemod";

		public const string Name = "NobleMod";

		public const string Version = "1.0.2";
	}
	internal static class ReplacementsJsonParser
	{
		internal sealed class Entry
		{
			public readonly string MappingKey;

			public readonly List<Variant> Variants = new List<Variant>();

			public Entry(string mappingKey)
			{
				MappingKey = mappingKey;
			}
		}

		internal struct Variant
		{
			public string File;

			public float WeightPercent;

			public bool IsVanilla;
		}

		private sealed class Parser
		{
			private readonly string _s;

			private int _i;

			private bool Eof => _i >= _s.Length;

			public Parser(string s)
			{
				_s = s;
				_i = 0;
			}

			public List<Entry> ParseRootObject()
			{
				SkipWs();
				Expect('{');
				List<Entry> list = new List<Entry>();
				SkipWs();
				while (!Eof && Peek() != '}')
				{
					string text = ReadString();
					SkipWs();
					Expect(':');
					SkipWs();
					Entry entry = new Entry(text);
					if (Peek() == '[')
					{
						ReadVariantArray(entry.Variants);
					}
					else
					{
						if (Peek() != '"')
						{
							throw new FormatException("Valeur attendue pour la cle '" + text + "' : chaine ou tableau.");
						}
						entry.Variants.Add(new Variant
						{
							File = ReadString(),
							WeightPercent = 100f,
							IsVanilla = false
						});
					}
					list.Add(entry);
					SkipWs();
					if (Peek() == ',')
					{
						_i++;
						SkipWs();
					}
					else if (Peek() != '}')
					{
						throw new FormatException("',' ou '}' attendu apres une entree.");
					}
				}
				Expect('}');
				SkipWs();
				if (!Eof)
				{
					throw new FormatException("Caracteres inattendus apres la racine JSON.");
				}
				return list;
			}

			private void ReadVariantArray(List<Variant> target)
			{
				Expect('[');
				SkipWs();
				while (!Eof && Peek() != ']')
				{
					Expect('{');
					SkipWs();
					string text = null;
					float? num = null;
					bool? flag = null;
					while (!Eof && Peek() != '}')
					{
						string a = ReadString();
						SkipWs();
						Expect(':');
						SkipWs();
						if (string.Equals(a, "file", StringComparison.OrdinalIgnoreCase))
						{
							if (Peek() != '"')
							{
								throw new FormatException("\"file\" doit etre une chaine.");
							}
							text = ReadString();
						}
						else if (string.Equals(a, "weight", StringComparison.OrdinalIgnoreCase))
						{
							num = ReadNumber();
						}
						else if (string.Equals(a, "vanilla", StringComparison.OrdinalIgnoreCase))
						{
							flag = ReadJsonBool();
						}
						else
						{
							SkipValue();
						}
						SkipWs();
						if (Peek() == ',')
						{
							_i++;
							SkipWs();
						}
						else if (Peek() != '}')
						{
							throw new FormatException("Propriete d'objet : ',' ou '}' attendu.");
						}
					}
					Expect('}');
					bool flag2 = flag == true;
					if (!flag2 && string.IsNullOrWhiteSpace(text))
					{
						throw new FormatException("Variante sans \"file\" (ou utilisez \"vanilla\": true).");
					}
					if (!num.HasValue)
					{
						throw new FormatException(flag2 ? "Variante vanilla sans \"weight\"." : ("Variante sans \"weight\" pour '" + text + "'."));
					}
					target.Add(new Variant
					{
						File = (flag2 ? null : text.Trim()),
						WeightPercent = num.Value,
						IsVanilla = flag2
					});
					SkipWs();
					if (Peek() == ',')
					{
						_i++;
						SkipWs();
					}
					else if (Peek() != ']')
					{
						throw new FormatException("',' ou ']' attendu dans le tableau de variantes.");
					}
				}
				Expect(']');
			}

			private void SkipValue()
			{
				if (Peek() == '"')
				{
					ReadString();
					return;
				}
				if (Peek() == '[')
				{
					int num = 0;
					while (!Eof)
					{
						switch (_s[_i++])
						{
						case '[':
							num++;
							break;
						case ']':
							num--;
							if (num == 0)
							{
								return;
							}
							break;
						}
					}
					return;
				}
				if (Peek() == '{')
				{
					int num2 = 0;
					while (!Eof)
					{
						switch (_s[_i++])
						{
						case '{':
							num2++;
							break;
						case '}':
							num2--;
							if (num2 == 0)
							{
								return;
							}
							break;
						}
					}
					return;
				}
				SkipWs();
				if (!Eof && _i + 4 <= _s.Length && string.CompareOrdinal(_s, _i, "true", 0, 4) == 0)
				{
					_i += 4;
					return;
				}
				if (!Eof && _i + 5 <= _s.Length && string.CompareOrdinal(_s, _i, "false", 0, 5) == 0)
				{
					_i += 5;
					return;
				}
				if (!Eof && _i + 4 <= _s.Length && string.CompareOrdinal(_s, _i, "null", 0, 4) == 0)
				{
					_i += 4;
					return;
				}
				while (!Eof && !char.IsWhiteSpace(_s[_i]) && _s[_i] != ',' && _s[_i] != '}' && _s[_i] != ']')
				{
					_i++;
				}
			}

			private bool ReadJsonBool()
			{
				SkipWs();
				if (_i + 4 <= _s.Length && string.CompareOrdinal(_s, _i, "true", 0, 4) == 0)
				{
					_i += 4;
					return true;
				}
				if (_i + 5 <= _s.Length && string.CompareOrdinal(_s, _i, "false", 0, 5) == 0)
				{
					_i += 5;
					return false;
				}
				throw new FormatException("Litteral JSON true ou false attendu.");
			}

			private float ReadNumber()
			{
				SkipWs();
				int i = _i;
				if (_i < _s.Length && (_s[_i] == '-' || _s[_i] == '+'))
				{
					_i++;
				}
				while (_i < _s.Length && char.IsDigit(_s[_i]))
				{
					_i++;
				}
				if (_i < _s.Length && _s[_i] == '.')
				{
					_i++;
					while (_i < _s.Length && char.IsDigit(_s[_i]))
					{
						_i++;
					}
				}
				string text = _s.Substring(i, _i - i);
				if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
				{
					throw new FormatException("Nombre invalide : '" + text + "'");
				}
				return result;
			}

			private string ReadString()
			{
				Expect('"');
				StringBuilder stringBuilder = new StringBuilder();
				while (!Eof && _s[_i] != '"')
				{
					if (_s[_i] == '\\')
					{
						_i++;
						if (Eof)
						{
							throw new FormatException("Chaine non terminee (echappement).");
						}
						char c = _s[_i++];
						switch (c)
						{
						case '"':
							stringBuilder.Append('"');
							break;
						case '\\':
							stringBuilder.Append('\\');
							break;
						case '/':
							stringBuilder.Append('/');
							break;
						case 'b':
							stringBuilder.Append('\b');
							break;
						case 'f':
							stringBuilder.Append('\f');
							break;
						case 'n':
							stringBuilder.Append('\n');
							break;
						case 'r':
							stringBuilder.Append('\r');
							break;
						case 't':
							stringBuilder.Append('\t');
							break;
						default:
							stringBuilder.Append(c);
							break;
						}
					}
					else
					{
						stringBuilder.Append(_s[_i++]);
					}
				}
				Expect('"');
				return stringBuilder.ToString();
			}

			private void SkipWs()
			{
				while (!Eof && char.IsWhiteSpace(_s[_i]))
				{
					_i++;
				}
			}

			private char Peek()
			{
				if (!Eof)
				{
					return _s[_i];
				}
				return '\0';
			}

			private void Expect(char c)
			{
				if (Eof || _s[_i] != c)
				{
					throw new FormatException($"Caractere '{c}' attendu, position {_i}.");
				}
				_i++;
			}
		}

		internal static List<Entry> Parse(string json)
		{
			if (string.IsNullOrWhiteSpace(json))
			{
				return new List<Entry>();
			}
			return new Parser(json.Trim()).ParseRootObject();
		}
	}
	internal enum SoundReplacementResolve
	{
		NoMatch,
		KeepVanilla,
		Replace
	}
	internal static class SoundBank
	{
		private sealed class ClipTreeNode
		{
			public readonly Dictionary<string, ClipTreeNode> Children = new Dictionary<string, ClipTreeNode>();

			public readonly List<string> Clips = new List<string>();
		}

		private readonly struct PickerSlot
		{
			public readonly bool IsVanilla;

			public readonly AudioClip Clip;

			public PickerSlot(bool isVanilla, AudioClip clip)
			{
				IsVanilla = isVanilla;
				Clip = clip;
			}
		}

		private sealed class WeightedClipPicker
		{
			private readonly PickerSlot[] _slots;

			private readonly float[] _cumulative;

			public WeightedClipPicker(PickerSlot[] slots, float[] weightsNormalized)
			{
				_slots = slots;
				_cumulative = new float[slots.Length];
				float num = 0f;
				for (int i = 0; i < weightsNormalized.Length; i++)
				{
					num += weightsNormalized[i];
					_cumulative[i] = num;
				}
			}

			public ReplacementPick Pick(string vanillaClipNameHeard, string ruleLabel)
			{
				if (!TryPickCore(out var pick, out var r, out var slotIndex))
				{
					return default(ReplacementPick);
				}
				if (ModConfig.LogWeightedSoundPicks.Value && _logger != null)
				{
					LogPick(vanillaClipNameHeard, ruleLabel, pick, r, slotIndex);
				}
				return pick;
			}

			private bool TryPickCore(out ReplacementPick pick, out float r, out int slotIndex)
			{
				pick = default(ReplacementPick);
				r = 0f;
				slotIndex = 0;
				if (_slots == null || _slots.Length == 0)
				{
					return false;
				}
				if (_slots.Length == 1)
				{
					slotIndex = 0;
					r = float.NaN;
					PickerSlot pickerSlot = _slots[0];
					pick = (pickerSlot.IsVanilla ? new ReplacementPick(keepVanilla: true, null) : new ReplacementPick(keepVanilla: false, pickerSlot.Clip));
					return true;
				}
				r = Random.value;
				for (int i = 0; i < _cumulative.Length; i++)
				{
					if (r < _cumulative[i])
					{
						slotIndex = i;
						PickerSlot pickerSlot2 = _slots[i];
						pick = (pickerSlot2.IsVanilla ? new ReplacementPick(keepVanilla: true, null) : new ReplacementPick(keepVanilla: false, pickerSlot2.Clip));
						return true;
					}
				}
				slotIndex = _slots.Length - 1;
				PickerSlot pickerSlot3 = _slots[_slots.Length - 1];
				pick = (pickerSlot3.IsVanilla ? new ReplacementPick(keepVanilla: true, null) : new ReplacementPick(keepVanilla: false, pickerSlot3.Clip));
				return true;
			}

			private void LogPick(string vanillaClipNameHeard, string ruleLabel, ReplacementPick pick, float r, int slotIndex)
			{
				string text = (pick.KeepVanilla ? "vanilla" : (((Object)(object)pick.Clip != (Object)null) ? ((Object)pick.Clip).name : "(null)"));
				if (_slots.Length == 1)
				{
					_logger.LogInfo((object)("[Tirage] vanilla lu='" + vanillaClipNameHeard + "' regle=" + ruleLabel + " | 1 seul creneau → " + text));
					return;
				}
				StringBuilder stringBuilder = new StringBuilder(256);
				stringBuilder.Append("[Tirage] vanilla lu='").Append(vanillaClipNameHeard).Append("' regle=")
					.Append(ruleLabel);
				stringBuilder.Append(" | r=").Append(r.ToString("F4")).Append(" | creneaux cumules: ");
				float num = 0f;
				for (int i = 0; i < _cumulative.Length; i++)
				{
					float num2 = _cumulative[i];
					PickerSlot pickerSlot = _slots[i];
					string value = (pickerSlot.IsVanilla ? "vanilla" : (((Object)(object)pickerSlot.Clip != (Object)null) ? ((Object)pickerSlot.Clip).name : "?"));
					if (i > 0)
					{
						stringBuilder.Append("; ");
					}
					stringBuilder.Append('[').Append(num.ToString("F3")).Append(',')
						.Append(num2.ToString("F3"))
						.Append(")=")
						.Append(value);
					num = num2;
				}
				stringBuilder.Append(" | choix=slot#").Append(slotIndex).Append(" → ")
					.Append(text);
				_logger.LogInfo((object)stringBuilder.ToString());
			}
		}

		private readonly struct ReplacementPick
		{
			public readonly bool KeepVanilla;

			public readonly AudioClip Clip;

			public ReplacementPick(bool keepVanilla, AudioClip clip)
			{
				KeepVanilla = keepVanilla;
				Clip = clip;
			}
		}

		private struct CachedResolveEntry
		{
			public SoundReplacementResolve Kind;

			public AudioClip ReplacementClip;
		}

		private sealed class ManagedRerollSticky
		{
			public AudioClip ChosenClip;
		}

		private static readonly Dictionary<string, WeightedClipPicker> ClipsByVanillaName = new Dictionary<string, WeightedClipPicker>();

		private static readonly List<KeyValuePair<string, WeightedClipPicker>> ContainsRules = new List<KeyValuePair<string, WeightedClipPicker>>();

		private static readonly HashSet<AudioClip> ManagedClips = new HashSet<AudioClip>();

		private static readonly HashSet<string> UnknownLoggedOnce = new HashSet<string>();

		private static readonly HashSet<string> DiscoveredClipNames = new HashSet<string>();

		private static ManualLogSource _logger;

		private static string _customSoundsRootPath;

		private static string _discoveredOutputRootPath;

		private static int _resolveCacheFrame = -1;

		private static readonly Dictionary<int, CachedResolveEntry> _resolveCacheByClipId = new Dictionary<int, CachedResolveEntry>();

		private static readonly HashSet<int> _resolveCacheDebugOncePerClipThisFrame = new HashSet<int>();

		private static ConditionalWeakTable<AudioSource, string> _audioSourceToVanillaClipName = new ConditionalWeakTable<AudioSource, string>();

		private static readonly Dictionary<string, AudioClip> VanillaClipRefByExactName = new Dictionary<string, AudioClip>(StringComparer.Ordinal);

		private static ConditionalWeakTable<AudioSource, ManagedRerollSticky> _managedRerollStickyBySource = new ConditionalWeakTable<AudioSource, ManagedRerollSticky>();

		public static bool IsAudioSourceInFilterScope(AudioSource source)
		{
			if ((Object)(object)source == (Object)null)
			{
				return false;
			}
			string value = ModConfig.AudioSourceHierarchyFilter.Value;
			if (string.IsNullOrWhiteSpace(value))
			{
				return true;
			}
			Transform val = ((Component)source).transform;
			while ((Object)(object)val != (Object)null)
			{
				if (((Object)val).name.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					return true;
				}
				val = val.parent;
			}
			return false;
		}

		public static void RememberVanillaPlaybackContext(AudioSource source, AudioClip vanillaClip)
		{
			if (!((Object)(object)source == (Object)null) && !((Object)(object)vanillaClip == (Object)null) && !IsManagedClip(vanillaClip))
			{
				string name = ((Object)vanillaClip).name;
				if (!string.IsNullOrEmpty(name))
				{
					_audioSourceToVanillaClipName.Remove(source);
					_audioSourceToVanillaClipName.Add(source, name);
					VanillaClipRefByExactName[name] = vanillaClip;
					_managedRerollStickyBySource.Remove(source);
				}
			}
		}

		public static bool TryRerollManagedClipOnSource(AudioSource source, ref AudioClip clip)
		{
			if ((Object)(object)source == (Object)null || (Object)(object)clip == (Object)null || !IsManagedClip(clip))
			{
				return false;
			}
			if (!IsAudioSourceInFilterScope(source))
			{
				return false;
			}
			if (!_audioSourceToVanillaClipName.TryGetValue(source, out var value) || string.IsNullOrEmpty(value))
			{
				return false;
			}
			if (!VanillaClipRefByExactName.TryGetValue(value, out var value2) || (Object)(object)value2 == (Object)null)
			{
				return false;
			}
			if (_managedRerollStickyBySource.TryGetValue(source, out var value3) && (Object)(object)value3.ChosenClip != (Object)null)
			{
				if (ModConfig.LogWeightedSoundPicks.Value && _logger != null)
				{
					_logger.LogDebug((object)$"[Tirage] sticky AudioSource id={((Object)source).GetInstanceID()} ref vanilla='{value}' → garde '{((Object)value3.ChosenClip).name}' (pas de nouveau Random jusqu'a un clip vanilla sur cette source)");
				}
				clip = value3.ChosenClip;
				return true;
			}
			AudioClip replacementClip;
			SoundReplacementResolve soundReplacementResolve = TryResolveReplacement(value, clip, out replacementClip);
			AudioClip val;
			if (soundReplacementResolve == SoundReplacementResolve.KeepVanilla || soundReplacementResolve == SoundReplacementResolve.NoMatch)
			{
				val = value2;
			}
			else
			{
				if (soundReplacementResolve != SoundReplacementResolve.Replace || !((Object)(object)replacementClip != (Object)null))
				{
					return false;
				}
				val = replacementClip;
			}
			clip = val;
			_managedRerollStickyBySource.Remove(source);
			_managedRerollStickyBySource.Add(source, new ManagedRerollSticky
			{
				ChosenClip = val
			});
			return true;
		}

		public static void Initialize(ManualLogSource logger)
		{
			_logger = logger;
			Reload();
		}

		public static bool IsManagedClip(AudioClip clip)
		{
			if ((Object)(object)clip != (Object)null)
			{
				return ManagedClips.Contains(clip);
			}
			return false;
		}

		public static SoundReplacementResolve TryResolveReplacement(string vanillaClipName, AudioClip vanillaClipAsset, out AudioClip replacementClip)
		{
			replacementClip = null;
			if (string.IsNullOrWhiteSpace(vanillaClipName))
			{
				return SoundReplacementResolve.NoMatch;
			}
			if ((Object)(object)vanillaClipAsset != (Object)null)
			{
				int frameCount = Time.frameCount;
				if (_resolveCacheFrame != frameCount)
				{
					_resolveCacheByClipId.Clear();
					_resolveCacheDebugOncePerClipThisFrame.Clear();
					_resolveCacheFrame = frameCount;
				}
				int instanceID = ((Object)vanillaClipAsset).GetInstanceID();
				if (_resolveCacheByClipId.TryGetValue(instanceID, out var value))
				{
					if (ModConfig.LogWeightedSoundPicks.Value && _logger != null && _resolveCacheDebugOncePerClipThisFrame.Add(instanceID))
					{
						string arg = value.Kind switch
						{
							SoundReplacementResolve.KeepVanilla => "KeepVanilla", 
							SoundReplacementResolve.Replace => "Replace → " + (((Object)(object)value.ReplacementClip != (Object)null) ? ((Object)value.ReplacementClip).name : "?"), 
							_ => "NoMatch", 
						};
						_logger.LogDebug((object)$"[Tirage] cache meme frame (instance clip vanilla id={instanceID}) → reutilise {arg} (plusieurs hooks ont appele TryResolveReplacement sans nouveau tirage)");
					}
					replacementClip = value.ReplacementClip;
					return value.Kind;
				}
				SoundReplacementResolve soundReplacementResolve = TryResolveReplacementCore(vanillaClipName, out replacementClip);
				_resolveCacheByClipId[instanceID] = new CachedResolveEntry
				{
					Kind = soundReplacementResolve,
					ReplacementClip = replacementClip
				};
				return soundReplacementResolve;
			}
			return TryResolveReplacementCore(vanillaClipName, out replacementClip);
		}

		private static SoundReplacementResolve TryResolveReplacementCore(string vanillaClipName, out AudioClip replacementClip)
		{
			replacementClip = null;
			if (ClipsByVanillaName.TryGetValue(vanillaClipName, out var value))
			{
				return ResolvePick(value.Pick(vanillaClipName, "exact:" + vanillaClipName), out replacementClip);
			}
			for (int i = 0; i < ContainsRules.Count; i++)
			{
				KeyValuePair<string, WeightedClipPicker> keyValuePair = ContainsRules[i];
				if (vanillaClipName.IndexOf(keyValuePair.Key, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					return ResolvePick(keyValuePair.Value.Pick(vanillaClipName, "contains:" + keyValuePair.Key), out replacementClip);
				}
			}
			return SoundReplacementResolve.NoMatch;
		}

		private static SoundReplacementResolve ResolvePick(ReplacementPick pick, out AudioClip replacementClip)
		{
			replacementClip = null;
			if (pick.KeepVanilla)
			{
				return SoundReplacementResolve.KeepVanilla;
			}
			replacementClip = pick.Clip;
			if (!((Object)(object)replacementClip != (Object)null))
			{
				return SoundReplacementResolve.NoMatch;
			}
			return SoundReplacementResolve.Replace;
		}

		public static bool ShouldLogUnknownClip(string vanillaClipName)
		{
			if (string.IsNullOrWhiteSpace(vanillaClipName))
			{
				return false;
			}
			return UnknownLoggedOnce.Add(vanillaClipName);
		}

		private static void Reload()
		{
			ClipsByVanillaName.Clear();
			ContainsRules.Clear();
			ManagedClips.Clear();
			UnknownLoggedOnce.Clear();
			DiscoveredClipNames.Clear();
			_resolveCacheFrame = -1;
			_resolveCacheByClipId.Clear();
			_resolveCacheDebugOncePerClipThisFrame.Clear();
			VanillaClipRefByExactName.Clear();
			_audioSourceToVanillaClipName = new ConditionalWeakTable<AudioSource, string>();
			_managedRerollStickyBySource = new ConditionalWeakTable<AudioSource, ManagedRerollSticky>();
			string text = (_customSoundsRootPath = Path.Combine(Plugin.AssemblyDirectory, "CustomSounds"));
			string text2 = ModConfig.DiscoveredClipsOutputPath.Value?.Trim();
			_discoveredOutputRootPath = (string.IsNullOrWhiteSpace(text2) ? text : text2);
			Directory.CreateDirectory(text);
			Directory.CreateDirectory(_discoveredOutputRootPath);
			LoadExistingDiscoveredClips();
			string text3 = Path.Combine(text, "replacements.json");
			if (!File.Exists(text3))
			{
				WriteDefaultMappingTemplate(text3);
				_logger.LogWarning((object)("Missing mapping file: " + text3 + ". Template created."));
				return;
			}
			List<ReplacementsJsonParser.Entry> list;
			try
			{
				list = ReplacementsJsonParser.Parse(File.ReadAllText(text3, Encoding.UTF8));
			}
			catch (Exception ex)
			{
				_logger.LogError((object)("replacements.json invalide : " + ex.Message));
				return;
			}
			if (list == null || list.Count == 0)
			{
				_logger.LogWarning((object)("No valid mapping entry found in " + text3));
				return;
			}
			Dictionary<string, ReplacementsJsonParser.Entry> dictionary = new Dictionary<string, ReplacementsJsonParser.Entry>(StringComparer.Ordinal);
			foreach (ReplacementsJsonParser.Entry item in list)
			{
				dictionary[item.MappingKey] = item;
			}
			foreach (KeyValuePair<string, ReplacementsJsonParser.Entry> item2 in dictionary)
			{
				Register(text, item2.Value);
			}
		}

		public static void TrackDiscoveredClip(string vanillaClipName)
		{
			if (string.IsNullOrWhiteSpace(vanillaClipName) || !DiscoveredClipNames.Add(vanillaClipName) || !ModConfig.WriteDiscoveredClipsHierarchyJson.Value)
			{
				return;
			}
			try
			{
				if (!string.IsNullOrWhiteSpace(_discoveredOutputRootPath))
				{
					Directory.CreateDirectory(_discoveredOutputRootPath);
					string text = ModConfig.DiscoveredClipsHierarchyFileName.Value;
					if (string.IsNullOrWhiteSpace(text))
					{
						text = "discovered_clips_hierarchy.json";
					}
					SaveDiscoveredHierarchyJson(Path.Combine(_discoveredOutputRootPath, text));
				}
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("Failed to write discovered clip '" + vanillaClipName + "': " + ex.Message));
			}
		}

		private static void Register(string root, ReplacementsJsonParser.Entry entry)
		{
			string mappingKey = entry.MappingKey;
			string text = "exact";
			string text2 = mappingKey;
			if (mappingKey.StartsWith("contains:", StringComparison.OrdinalIgnoreCase))
			{
				text = "contains";
				text2 = mappingKey.Substring("contains:".Length).Trim();
			}
			else if (mappingKey.StartsWith("exact:", StringComparison.OrdinalIgnoreCase))
			{
				text = "exact";
				text2 = mappingKey.Substring("exact:".Length).Trim();
			}
			if (string.IsNullOrWhiteSpace(text2))
			{
				_logger.LogWarning((object)("Ignoring empty mapping key: '" + mappingKey + "'"));
				return;
			}
			if (entry.Variants == null || entry.Variants.Count == 0)
			{
				_logger.LogWarning((object)("No variants for mapping key: '" + mappingKey + "'"));
				return;
			}
			List<float> list = new List<float>();
			List<PickerSlot> list2 = new List<PickerSlot>();
			List<string> list3 = new List<string>();
			foreach (ReplacementsJsonParser.Variant variant in entry.Variants)
			{
				if (variant.WeightPercent < 0f)
				{
					_logger.LogWarning((object)("Negative weight ignored under '" + mappingKey + "'"));
				}
				else if (variant.IsVanilla)
				{
					list.Add(variant.WeightPercent);
					list2.Add(new PickerSlot(isVanilla: true, null));
					list3.Add($"vanilla ({variant.WeightPercent}%)");
				}
				else
				{
					if (string.IsNullOrWhiteSpace(variant.File))
					{
						continue;
					}
					string text3 = Path.Combine(root, variant.File.Trim());
					if (!File.Exists(text3))
					{
						_logger.LogWarning((object)("Missing sound file: " + text3 + " (cle '" + mappingKey + "')"));
						continue;
					}
					AudioClip val = ClipFileLoader.LoadFromPath(text3);
					if ((Object)(object)val == (Object)null)
					{
						_logger.LogWarning((object)("Failed to load clip: " + text3));
						continue;
					}
					((Object)val).name = "noble_" + variant.File;
					list.Add(variant.WeightPercent);
					list2.Add(new PickerSlot(isVanilla: false, val));
					ManagedClips.Add(val);
					list3.Add($"{variant.File} ({variant.WeightPercent}%)");
				}
			}
			if (list2.Count == 0)
			{
				_logger.LogWarning((object)("No loadable variants for '" + mappingKey + "'"));
				return;
			}
			float num = list.Sum();
			float[] array = new float[list2.Count];
			if (num <= 0f)
			{
				_logger.LogWarning((object)("Sum of weights is 0 for '" + mappingKey + "', using equal shares."));
				float num2 = 1f / (float)list2.Count;
				for (int i = 0; i < list2.Count; i++)
				{
					array[i] = num2;
				}
			}
			else
			{
				if (Math.Abs(num - 100f) > 0.01f)
				{
					_logger.LogInfo((object)$"Weights for '{mappingKey}' sum to {num:0.##}% (not 100); normalizing.");
				}
				float num3 = 1f / num;
				for (int j = 0; j < list2.Count; j++)
				{
					array[j] = list[j] * num3;
				}
			}
			WeightedClipPicker value = new WeightedClipPicker(list2.ToArray(), array);
			if (text == "contains")
			{
				ContainsRules.Add(new KeyValuePair<string, WeightedClipPicker>(text2, value));
			}
			else
			{
				ClipsByVanillaName[text2] = value;
			}
			string text4 = string.Join(", ", list3);
			_logger.LogInfo((object)("Loaded replacement [" + text + "] '" + text2 + "' <- " + text4));
		}

		private static void WriteDefaultMappingTemplate(string mappingFilePath)
		{
			File.WriteAllText(mappingFilePath, "{\r\n  \"vanilla_clip_name_exact\": \"mon_son.ogg\",\r\n  \"contains:footstep\": [\r\n    { \"file\": \"pas_a.ogg\", \"weight\": 50 },\r\n    { \"vanilla\": true, \"weight\": 25 },\r\n    { \"file\": \"pas_b.ogg\", \"weight\": 25 }\r\n  ]\r\n}", Encoding.UTF8);
		}

		private static void LoadExistingDiscoveredClips()
		{
			try
			{
				if (ModConfig.WriteDiscoveredClipsHierarchyJson.Value)
				{
					string text = ModConfig.DiscoveredClipsHierarchyFileName.Value;
					if (string.IsNullOrWhiteSpace(text))
					{
						text = "discovered_clips_hierarchy.json";
					}
					string path = Path.Combine(_discoveredOutputRootPath, text);
					if (File.Exists(path))
					{
						LoadDiscoveredNamesFromHierarchyJson(File.ReadAllText(path, Encoding.UTF8));
					}
				}
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("Failed to load discovered clips file: " + ex.Message));
			}
		}

		private static void LoadDiscoveredNamesFromHierarchyJson(string json)
		{
			foreach (Match item in Regex.Matches(json, "\"name\"\\s*:\\s*\"(?<v>(?:[^\"\\\\]|\\\\.)*)\"", RegexOptions.IgnoreCase))
			{
				string[] array = UnescapeJsonString(item.Groups["v"].Value).Split(new string[1] { " | " }, StringSplitOptions.RemoveEmptyEntries);
				for (int i = 0; i < array.Length; i++)
				{
					string text = array[i].Trim();
					if (!string.IsNullOrWhiteSpace(text))
					{
						DiscoveredClipNames.Add(text);
					}
				}
			}
			if (json.IndexOf("\"__clips\"", StringComparison.OrdinalIgnoreCase) < 0)
			{
				return;
			}
			foreach (Match item2 in Regex.Matches(json, "\"__clips\"\\s*:\\s*\\[(?<arr>[^\\]]*)\\]", RegexOptions.IgnoreCase))
			{
				foreach (Match item3 in Regex.Matches(item2.Groups["arr"].Value, "\"(?<v>(?:[^\"\\\\]|\\\\.)*)\""))
				{
					string text2 = UnescapeJsonString(item3.Groups["v"].Value).Trim();
					if (!string.IsNullOrWhiteSpace(text2))
					{
						DiscoveredClipNames.Add(text2);
					}
				}
			}
		}

		private static string JsonEscapeString(string s)
		{
			return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
		}

		private static string UnescapeJsonString(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return s;
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length);
			for (int i = 0; i < s.Length; i++)
			{
				if (s[i] != '\\' || i + 1 >= s.Length)
				{
					stringBuilder.Append(s[i]);
					continue;
				}
				i++;
				switch (s[i])
				{
				case '"':
					stringBuilder.Append('"');
					break;
				case '\\':
					stringBuilder.Append('\\');
					break;
				case '/':
					stringBuilder.Append('/');
					break;
				case 'b':
					stringBuilder.Append('\b');
					break;
				case 'f':
					stringBuilder.Append('\f');
					break;
				case 'n':
					stringBuilder.Append('\n');
					break;
				case 'r':
					stringBuilder.Append('\r');
					break;
				case 't':
					stringBuilder.Append('\t');
					break;
				default:
					stringBuilder.Append(s[i]);
					break;
				}
			}
			return stringBuilder.ToString();
		}

		private static void SaveDiscoveredHierarchyJson(string outputPath)
		{
			ClipTreeNode clipTreeNode = new ClipTreeNode();
			foreach (string discoveredClipName in DiscoveredClipNames)
			{
				List<string> smartTokens = GetSmartTokens(discoveredClipName);
				if (smartTokens.Count == 0)
				{
					continue;
				}
				ClipTreeNode clipTreeNode2 = clipTreeNode;
				for (int i = 0; i < smartTokens.Count; i++)
				{
					string key = smartTokens[i];
					if (!clipTreeNode2.Children.TryGetValue(key, out var value))
					{
						value = new ClipTreeNode();
						clipTreeNode2.Children[key] = value;
					}
					clipTreeNode2 = value;
				}
				if (!clipTreeNode2.Clips.Contains(discoveredClipName))
				{
					clipTreeNode2.Clips.Add(discoveredClipName);
				}
			}
			StringBuilder stringBuilder = new StringBuilder();
			WriteNodeJson(stringBuilder, clipTreeNode, 0);
			File.WriteAllText(outputPath, stringBuilder.ToString(), Encoding.UTF8);
		}

		private static List<string> GetSmartTokens(string clipName)
		{
			List<string> list = new List<string>();
			foreach (Match item in Regex.Matches(clipName.ToLowerInvariant(), "[a-z0-9]+"))
			{
				string text = item.Value.Trim();
				if (!string.IsNullOrWhiteSpace(text))
				{
					list.Add(text);
				}
			}
			return list;
		}

		private static void WriteNodeJson(StringBuilder sb, ClipTreeNode node, int indent)
		{
			string value = new string(' ', indent * 2);
			string value2 = new string(' ', (indent + 1) * 2);
			sb.AppendLine("{");
			List<string> list = new List<string>(node.Children.Keys);
			list.Sort(StringComparer.Ordinal);
			bool flag = true;
			for (int i = 0; i < list.Count; i++)
			{
				if (!flag)
				{
					sb.AppendLine(",");
				}
				flag = false;
				string text = list[i];
				sb.Append(value2).Append('"').Append(text)
					.Append("\": ");
				WriteNodeJson(sb, node.Children[text], indent + 1);
			}
			if (node.Clips.Count > 0)
			{
				if (!flag)
				{
					sb.AppendLine(",");
				}
				flag = false;
				node.Clips.Sort(StringComparer.Ordinal);
				string s = ((node.Clips.Count == 1) ? node.Clips[0] : string.Join(" | ", node.Clips));
				sb.Append(value2).Append("\"name\": \"").Append(JsonEscapeString(s))
					.Append('"');
			}
			if (!flag)
			{
				sb.AppendLine();
			}
			sb.Append(value).Append("}");
		}
	}
}
namespace NobleMod.Patches
{
	[HarmonyPatch(typeof(AudioSource), "PlayOneShot", new Type[] { typeof(AudioClip) })]
	internal static class CustomSoundPlayOneShotPatch
	{
		private static bool _loggedRuntimeError;

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

		[HarmonyPrefix]
		private static void BeforePlayOneShot(AudioSource __instance, ref AudioClip clip)
		{
			LogHookEntry("AudioSource.PlayOneShot(AudioClip)", __instance, clip);
			SafeTryReplace(__instance, ref clip);
		}

		internal static void TryReplace(AudioSource source, ref AudioClip clip)
		{
			if (!ModConfig.EnableCustomSounds.Value || (Object)(object)source == (Object)null || (Object)(object)clip == (Object)null)
			{
				return;
			}
			if (SoundBank.IsManagedClip(clip))
			{
				SoundBank.TryRerollManagedClipOnSource(source, ref clip);
			}
			else
			{
				if (!IsAudioSourceInScope(source))
				{
					return;
				}
				SoundBank.RememberVanillaPlaybackContext(source, clip);
				AudioClip replacementClip;
				switch (SoundBank.TryResolveReplacement(((Object)clip).name, clip, out replacementClip))
				{
				case SoundReplacementResolve.NoMatch:
					SoundBank.TrackDiscoveredClip(((Object)clip).name);
					if (ModConfig.LogUnknownVanillaClipNamesOnce.Value && SoundBank.ShouldLogUnknownClip(((Object)clip).name))
					{
						Plugin.Log.LogInfo((object)("No mapping for vanilla clip: '" + ((Object)clip).name + "'"));
					}
					return;
				case SoundReplacementResolve.KeepVanilla:
					return;
				}
				if (ModConfig.LogReplacements.Value)
				{
					Plugin.Log.LogInfo((object)("Replace '" + ((Object)clip).name + "' -> '" + ((Object)replacementClip).name + "'"));
				}
				clip = replacementClip;
			}
		}

		internal static void SafeTryReplace(AudioSource source, ref AudioClip clip)
		{
			try
			{
				TryReplace(source, ref clip);
			}
			catch (Exception arg)
			{
				if (!_loggedRuntimeError)
				{
					_loggedRuntimeError = true;
					Plugin.Log.LogError((object)$"Runtime error in audio replacement patch: {arg}");
				}
			}
		}

		internal static bool IsAudioSourceInScope(AudioSource source)
		{
			return SoundBank.IsAudioSourceInFilterScope(source);
		}

		internal static void LogHookEntry(string hookName, AudioSource source, AudioClip clip)
		{
			if (ModConfig.DebugLogHookEntrypoints.Value && !ShouldSuppressDebugEntry(hookName, source, clip))
			{
				int num = Mathf.Max(0, ModConfig.DebugLogHookEntrypointsPerMethod.Value);
				if (!DebugEntryCounts.TryGetValue(hookName, out var value))
				{
					value = 0;
				}
				if (num <= 0 || value < num)
				{
					DebugEntryCounts[hookName] = value + 1;
					string text = (((Object)(object)source != (Object)null) ? ((Object)source).name : "<null-source>");
					string text2 = (((Object)(object)clip != (Object)null) ? ((Object)clip).name : "<null-clip>");
					Plugin.Log.LogInfo((object)("[HookEntry] " + hookName + " src='" + text + "' clip='" + text2 + "'"));
				}
			}
		}

		internal static bool ShouldSuppressDebugEntry(string hookName, AudioSource source, AudioClip clip)
		{
			if (!ModConfig.DebugSuppressMenuSpam.Value)
			{
				return false;
			}
			string text = (((Object)(object)source != (Object)null) ? ((Object)source).name : string.Empty);
			string text2 = (((Object)(object)clip != (Object)null) ? ((Object)clip).name : string.Empty);
			string text3 = (hookName + " " + text + " " + text2).ToLowerInvariant();
			if (text3.Contains("menu") || text3.Contains("ui "))
			{
				return true;
			}
			if (hookName.IndexOf("PlayHelper", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				return true;
			}
			return false;
		}
	}
	[HarmonyPatch(typeof(AudioSource), "PlayOneShot", new Type[]
	{
		typeof(AudioClip),
		typeof(float)
	})]
	internal static class CustomSoundPlayOneShotWithVolumePatch
	{
		[HarmonyPrefix]
		private static void BeforePlayOneShot(AudioSource __instance, ref AudioClip clip, float volumeScale)
		{
			CustomSoundPlayOneShotPatch.LogHookEntry("AudioSource.PlayOneShot(AudioClip,float)", __instance, clip);
			CustomSoundPlayOneShotPatch.SafeTryReplace(__instance, ref clip);
		}
	}
	[HarmonyPatch]
	internal static class CustomSoundAudioSourcePlayPatch
	{
		private static MethodBase TargetMethod()
		{
			return AccessTools.Method(typeof(AudioSource), "Play", Type.EmptyTypes, (Type[])null);
		}

		[HarmonyPrefix]
		private static void BeforePlay(AudioSource __instance)
		{
			CustomSoundPlayOneShotPatch.LogHookEntry("AudioSource.Play()", __instance, ((Object)(object)__instance != (Object)null) ? __instance.clip : null);
			if (!((Object)(object)__instance == (Object)null) && !((Object)(object)__instance.clip == (Object)null))
			{
				AudioClip clip = __instance.clip;
				CustomSoundPlayOneShotPatch.SafeTryReplace(__instance, ref clip);
				__instance.clip = clip;
			}
		}
	}
	[HarmonyPatch]
	internal static class CustomSoundGameSoundPatch
	{
		private static bool _loggedRuntimeError;

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

		private static IEnumerable<MethodBase> TargetMethods()
		{
			Type type = AccessTools.TypeByName("Sound");
			if (type == null)
			{
				return Enumerable.Empty<MethodBase>();
			}
			return from m in AccessTools.GetDeclaredMethods(type)
				where m.Name == "Play" || m.Name == "PlayLoop" || m.Name == "PlayOneShot"
				select m;
		}

		[HarmonyPrefix]
		private static void BeforeSoundMethod(object __instance, MethodBase __originalMethod)
		{
			try
			{
				if (!ModConfig.EnableCustomSounds.Value || __instance == null)
				{
					return;
				}
				Traverse val = Traverse.Create(__instance);
				AudioSource val2 = val.Property("Source", (object[])null).GetValue<AudioSource>() ?? val.Field("Source").GetValue<AudioSource>();
				LogHookEntry(__originalMethod.Name, val2);
				if ((Object)(object)val2 == (Object)null || (Object)(object)val2.clip == (Object)null)
				{
					return;
				}
				if (SoundBank.IsManagedClip(val2.clip))
				{
					AudioClip clip = val2.clip;
					if (CustomSoundPlayOneShotPatch.IsAudioSourceInScope(val2) && SoundBank.TryRerollManagedClipOnSource(val2, ref clip))
					{
						val2.clip = clip;
						if (SoundBank.IsManagedClip(clip))
						{
							val.Field("LoopClip").SetValue((object)clip);
						}
						else
						{
							SyncSoundLoopClipIfStaleCustom(val, clip);
						}
					}
				}
				else
				{
					if (!CustomSoundPlayOneShotPatch.IsAudioSourceInScope(val2))
					{
						return;
					}
					SoundBank.RememberVanillaPlaybackContext(val2, val2.clip);
					AudioClip clip2 = val2.clip;
					AudioClip replacementClip;
					switch (SoundBank.TryResolveReplacement(((Object)clip2).name, clip2, out replacementClip))
					{
					case SoundReplacementResolve.NoMatch:
						SoundBank.TrackDiscoveredClip(((Object)clip2).name);
						if (ModConfig.LogUnknownVanillaClipNamesOnce.Value && SoundBank.ShouldLogUnknownClip(((Object)clip2).name))
						{
							Plugin.Log.LogInfo((object)("[Sound." + __originalMethod.Name + "] No mapping for vanilla clip: '" + ((Object)clip2).name + "'"));
						}
						SyncSoundLoopClipIfStaleCustom(val, clip2);
						return;
					case SoundReplacementResolve.KeepVanilla:
						SyncSoundLoopClipIfStaleCustom(val, clip2);
						return;
					}
					if (ModConfig.LogReplacements.Value)
					{
						Plugin.Log.LogInfo((object)("[Sound." + __originalMethod.Name + "] Replace '" + ((Object)clip2).name + "' -> '" + ((Object)replacementClip).name + "'"));
					}
					val2.clip = replacementClip;
					val.Field("LoopClip").SetValue((object)replacementClip);
				}
			}
			catch (Exception arg)
			{
				if (!_loggedRuntimeError)
				{
					_loggedRuntimeError = true;
					Plugin.Log.LogError((object)$"Runtime error in Sound patch: {arg}");
				}
			}
		}

		private static void SyncSoundLoopClipIfStaleCustom(Traverse tr, AudioClip vanillaClip)
		{
			if ((Object)(object)vanillaClip == (Object)null)
			{
				return;
			}
			try
			{
				AudioClip value = tr.Field("LoopClip").GetValue<AudioClip>();
				if ((Object)(object)value == (Object)null || SoundBank.IsManagedClip(value))
				{
					tr.Field("LoopClip").SetValue((object)vanillaClip);
				}
			}
			catch
			{
			}
		}

		private static void LogHookEntry(string methodName, AudioSource source)
		{
			if (!ModConfig.DebugLogHookEntrypoints.Value)
			{
				return;
			}
			string text = "Sound." + methodName;
			int num = Mathf.Max(0, ModConfig.DebugLogHookEntrypointsPerMethod.Value);
			if (!DebugEntryCounts.TryGetValue(text, out var value))
			{
				value = 0;
			}
			if (num <= 0 || value < num)
			{
				DebugEntryCounts[text] = value + 1;
				string text2 = (((Object)(object)source != (Object)null) ? ((Object)source).name : "<null-source>");
				string text3 = (((Object)(object)source != (Object)null && (Object)(object)source.clip != (Object)null) ? ((Object)source.clip).name : "<null-clip>");
				if (!CustomSoundPlayOneShotPatch.ShouldSuppressDebugEntry(text, source, ((Object)(object)source != (Object)null) ? source.clip : null))
				{
					Plugin.Log.LogInfo((object)("[HookEntry] " + text + " src='" + text2 + "' clip='" + text3 + "'"));
				}
			}
		}
	}
	[HarmonyPatch]
	internal static class CustomSoundAudioSourcePlayAnyPatch
	{
		private static bool _loggedRuntimeError;

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

		private static IEnumerable<MethodBase> TargetMethods()
		{
			return from m in AccessTools.GetDeclaredMethods(typeof(AudioSource))
				where m.Name.StartsWith("Play", StringComparison.Ordinal)
				where !IsHandledByDedicatedAudioSourcePlayPatch(m)
				select m;
		}

		private static bool IsHandledByDedicatedAudioSourcePlayPatch(MethodInfo m)
		{
			if (m.Name == "Play" && m.GetParameters().Length == 0)
			{
				return true;
			}
			if (m.Name == "PlayOneShot")
			{
				ParameterInfo[] parameters = m.GetParameters();
				if (parameters.Length == 1 && parameters[0].ParameterType == typeof(AudioClip))
				{
					return true;
				}
				if (parameters.Length == 2 && parameters[0].ParameterType == typeof(AudioClip) && parameters[1].ParameterType == typeof(float))
				{
					return true;
				}
			}
			return false;
		}

		[HarmonyPrefix]
		private static void BeforeAnyPlay(MethodBase __originalMethod, AudioSource __instance, object[] __args)
		{
			try
			{
				if (!ModConfig.EnableCustomSounds.Value)
				{
					return;
				}
				AudioClip val = null;
				if (__args != null)
				{
					foreach (object obj in __args)
					{
						AudioClip val2 = (AudioClip)((obj is AudioClip) ? obj : null);
						if (val2 != null)
						{
							val = val2;
							break;
						}
					}
				}
				AudioClip val3 = (((Object)(object)__instance != (Object)null) ? __instance.clip : null);
				AudioClip val4 = val ?? val3;
				LogHookEntry("AudioSource." + __originalMethod.Name, __instance, val4);
				if ((Object)(object)__instance == (Object)null || (Object)(object)val4 == (Object)null)
				{
					return;
				}
				if (SoundBank.IsManagedClip(val4))
				{
					AudioClip clip = val4;
					if (!CustomSoundPlayOneShotPatch.IsAudioSourceInScope(__instance) || !SoundBank.TryRerollManagedClipOnSource(__instance, ref clip))
					{
						return;
					}
					if ((Object)(object)val != (Object)null && __args != null)
					{
						for (int j = 0; j < __args.Length; j++)
						{
							if (__args[j] is AudioClip)
							{
								__args[j] = clip;
								break;
							}
						}
					}
					else
					{
						__instance.clip = clip;
					}
				}
				else
				{
					if (!CustomSoundPlayOneShotPatch.IsAudioSourceInScope(__instance))
					{
						return;
					}
					SoundBank.RememberVanillaPlaybackContext(__instance, val4);
					AudioClip replacementClip;
					switch (SoundBank.TryResolveReplacement(((Object)val4).name, val4, out replacementClip))
					{
					case SoundReplacementResolve.NoMatch:
						SoundBank.TrackDiscoveredClip(((Object)val4).name);
						if (ModConfig.LogUnknownVanillaClipNamesOnce.Value && SoundBank.ShouldLogUnknownClip(((Object)val4).name))
						{
							Plugin.Log.LogInfo((object)("[AudioSource." + __originalMethod.Name + "] No mapping for vanilla clip: '" + ((Object)val4).name + "'"));
						}
						return;
					case SoundReplacementResolve.KeepVanilla:
						return;
					}
					if (ModConfig.LogReplacements.Value)
					{
						Plugin.Log.LogInfo((object)("[AudioSource." + __originalMethod.Name + "] Replace '" + ((Object)val4).name + "' -> '" + ((Object)replacementClip).name + "'"));
					}
					if ((Object)(object)val != (Object)null && __args != null)
					{
						for (int k = 0; k < __args.Length; k++)
						{
							if (__args[k] is AudioClip)
							{
								__args[k] = replacementClip;
								break;
							}
						}
					}
					else
					{
						__instance.clip = replacementClip;
					}
				}
			}
			catch (Exception arg)
			{
				if (!_loggedRuntimeError)
				{
					_loggedRuntimeError = true;
					Plugin.Log.LogError((object)$"Runtime error in AudioSource Play* patch: {arg}");
				}
			}
		}

		private static void LogHookEntry(string hookName, AudioSource source, AudioClip clip)
		{
			if (ModConfig.DebugLogHookEntrypoints.Value && !CustomSoundPlayOneShotPatch.ShouldSuppressDebugEntry(hookName, source, clip))
			{
				int num = Mathf.Max(0, ModConfig.DebugLogHookEntrypointsPerMethod.Value);
				if (!DebugEntryCounts.TryGetValue(hookName, out var value))
				{
					value = 0;
				}
				if (num <= 0 || value < num)
				{
					DebugEntryCounts[hookName] = value + 1;
					string text = (((Object)(object)source != (Object)null) ? ((Object)source).name : "<null-source>");
					string text2 = (((Object)(object)clip != (Object)null) ? ((Object)clip).name : "<null-clip>");
					Plugin.Log.LogInfo((object)("[HookEntry] " + hookName + " src='" + text + "' clip='" + text2 + "'"));
				}
			}
		}
	}
	[HarmonyPatch]
	internal static class CustomSoundAudioSourceSetClipPatch
	{
		private static bool _loggedRuntimeError;

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

		private static MethodBase TargetMethod()
		{
			return AccessTools.PropertySetter(typeof(AudioSource), "clip");
		}

		[HarmonyPrefix]
		private static void BeforeSetClip(AudioSource __instance, ref AudioClip value)
		{
			try
			{
				if (!ModConfig.EnableCustomSounds.Value)
				{
					return;
				}
				if (ModConfig.DebugLogHookEntrypoints.Value && !CustomSoundPlayOneShotPatch.ShouldSuppressDebugEntry("AudioSource.set_clip", __instance, value))
				{
					int num = Mathf.Max(0, ModConfig.DebugLogHookEntrypointsPerMethod.Value);
					if (!DebugEntryCounts.TryGetValue("AudioSource.set_clip", out var value2))
					{
						value2 = 0;
					}
					if (num <= 0 || value2 < num)
					{
						DebugEntryCounts["AudioSource.set_clip"] = value2 + 1;
						string text = (((Object)(object)__instance != (Object)null) ? ((Object)__instance).name : "<null-source>");
						string text2 = (((Object)(object)value != (Object)null) ? ((Object)value).name : "<null-clip>");
						Plugin.Log.LogInfo((object)("[HookEntry] AudioSource.set_clip src='" + text + "' clip='" + text2 + "'"));
					}
				}
				if ((Object)(object)__instance == (Object)null || (Object)(object)value == (Object)null)
				{
					return;
				}
				if (SoundBank.IsManagedClip(value))
				{
					AudioClip clip = value;
					if (SoundBank.TryRerollManagedClipOnSource(__instance, ref clip))
					{
						value = clip;
					}
				}
				else
				{
					if (!CustomSoundPlayOneShotPatch.IsAudioSourceInScope(__instance))
					{
						return;
					}
					SoundBank.RememberVanillaPlaybackContext(__instance, value);
					AudioClip replacementClip;
					switch (SoundBank.TryResolveReplacement(((Object)value).name, value, out replacementClip))
					{
					case SoundReplacementResolve.NoMatch:
						SoundBank.TrackDiscoveredClip(((Object)value).name);
						if (ModConfig.LogUnknownVanillaClipNamesOnce.Value && SoundBank.ShouldLogUnknownClip(((Object)value).name))
						{
							Plugin.Log.LogInfo((object)("[AudioSource.set_clip] No mapping for vanilla clip: '" + ((Object)value).name + "'"));
						}
						return;
					case SoundReplacementResolve.KeepVanilla:
						return;
					}
					if (ModConfig.LogReplacements.Value)
					{
						Plugin.Log.LogInfo((object)("[AudioSource.set_clip] Replace '" + ((Object)value).name + "' -> '" + ((Object)replacementClip).name + "'"));
					}
					value = replacementClip;
				}
			}
			catch (Exception arg)
			{
				if (!_loggedRuntimeError)
				{
					_loggedRuntimeError = true;
					Plugin.Log.LogError((object)$"Runtime error in AudioSource.set_clip patch: {arg}");
				}
			}
		}
	}
	[HarmonyPatch]
	internal static class LevelEnemyOverridePatches
	{
		private static readonly HashSet<int> LoggedLevelNoRule = new HashSet<int>();

		private static readonly HashSet<string> LoggedFallbacks = new HashSet<string>();

		private static readonly HashSet<int> AppliedOverrideLevels = new HashSet<int>();

		private static MethodBase TargetMethod()
		{
			Type type = AccessTools.TypeByName("EnemyDirector");
			if (type == null)
			{
				return null;
			}
			return AccessTools.Method(type, "GetEnemy", (Type[])null, (Type[])null);
		}

		[HarmonyPostfix]
		private static void AfterGetEnemy(MethodBase __originalMethod, object __instance, ref object __result)
		{
			if (__instance == null)
			{
				return;
			}
			try
			{
				if (!ModConfig.EnableSpawnOverrides.Value || __result == null || !LooksLikeEnemySetupResult(__result))
				{
					return;
				}
				int num = TryGetCurrentLevelNumber();
				if (num <= 0 || AppliedOverrideLevels.Contains(num))
				{
					return;
				}
				if (!LevelEnemyOverrideBank.TryGetMobKey(num, out var mobKey))
				{
					if (ModConfig.LogSpawnOverrides.Value && LoggedLevelNoRule.Add(num))
					{
						Plugin.Log.LogInfo((object)$"[SpawnOverride] Aucun override pour niveau {num}. Spawn vanilla conserve.");
					}
					return;
				}
				object obj = FindBestSetupMatch(__instance, mobKey, __result.GetType());
				if (obj == null)
				{
					if (ModConfig.LogSpawnOverrides.Value)
					{
						string item = $"{num}:{mobKey}";
						if (LoggedFallbacks.Add(item))
						{
							Plugin.Log.LogWarning((object)$"[SpawnOverride] Niveau {num}: mob '{mobKey}' introuvable, fallback vanilla conserve.");
						}
					}
				}
				else
				{
					__result = obj;
					AppliedOverrideLevels.Add(num);
					if (ModConfig.LogSpawnOverrides.Value)
					{
						Plugin.Log.LogInfo((object)$"[SpawnOverride] Niveau {num}: mob principal force vers '{mobKey}' (applique une seule fois).");
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[SpawnOverride] Erreur runtime: " + ex.Message));
			}
		}

		private static bool LooksLikeEnemySetupResult(object result)
		{
			string name = result.GetType().Name;
			if (name.IndexOf("Enemy", StringComparison.OrdinalIgnoreCase) >= 0 && name.IndexOf("Setup", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				return true;
			}
			try
			{
				return AccessTools.Field(result.GetType(), "spawnObjects") != null;
			}
			catch
			{
				return false;
			}
		}

		private static int TryGetCurrentLevelNumber()
		{
			try
			{
				Type type = AccessTools.TypeByName("RunManager");
				if (type == null)
				{
					return 0;
				}
				object obj = AccessTools.Field(type, "instance")?.GetValue(null);
				if (obj == null)
				{
					return 0;
				}
				object value = Traverse.Create(obj).Field("levelsCompleted").GetValue();
				if (value == null)
				{
					return 0;
				}
				return Convert.ToInt32(value) + 1;
			}
			catch
			{
				return 0;
			}
		}

		private static object FindBestSetupMatch(object enemyDirector, string desiredMobKey, Type expectedSetupType)
		{
			List<object> list = CollectEnemySetups(enemyDirector, expectedSetupType);
			if (list.Count == 0)
			{
				return null;
			}
			string token = desiredMobKey.Trim().ToLowerInvariant();
			foreach (object item in list)
			{
				if (SetupContainsToken(item, token))
				{
					return item;
				}
			}
			foreach (object item2 in list)
			{
				if (SetupMatchesAlias(item2, token))
				{
					return item2;
				}
			}
			return null;
		}

		private static List<object> CollectEnemySetups(object enemyDirector, Type expectedSetupType)
		{
			List<object> list = new List<object>();
			List<FieldInfo> declaredFields = AccessTools.GetDeclaredFields(enemyDirector.GetType());
			for (int i = 0; i < declaredFields.Count; i++)
			{
				FieldInfo fieldInfo = declaredFields[i];
				object value;
				try
				{
					value = fieldInfo.GetValue(enemyDirector);
				}
				catch
				{
					continue;
				}
				if (value is IEnumerable enumerable && !(value is string))
				{
					foreach (object item in enumerable)
					{
						if (item != null && expectedSetupType.IsInstanceOfType(item) && !list.Contains(item))
						{
							list.Add(item);
						}
					}
				}
				else if (value != null && expectedSetupType.IsInstanceOfType(value) && !list.Contains(value))
				{
					list.Add(value);
				}
			}
			return list;
		}

		private static bool SetupContainsToken(object setup, string token)
		{
			return BuildSearchBlob(setup).IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0;
		}

		private static bool SetupMatchesAlias(object setup, string token)
		{
			string text = BuildSearchBlob(setup).ToLowerInvariant();
			switch (token)
			{
			case "huntsman":
			case "hunter":
				return text.Contains("hunter");
			case "headman":
				return text.Contains("headman");
			default:
				return false;
			}
		}

		private static string BuildSearchBlob(object setup)
		{
			List<string> list = new List<string>();
			Type type = setup.GetType();
			list.Add(type.Name);
			string value = Traverse.Create(setup).Field("name").GetValue<string>();
			if (!string.IsNullOrWhiteSpace(value))
			{
				list.Add(value);
			}
			if (Traverse.Create(setup).Field("spawnObjects").GetValue() is IEnumerable enumerable)
			{
				foreach (object item in enumerable)
				{
					if (item != null)
					{
						Traverse obj = Traverse.Create(item);
						string value2 = obj.Field("prefabName").GetValue<string>();
						string value3 = obj.Field("resourcePath").GetValue<string>();
						if (!string.IsNullOrWhiteSpace(value2))
						{
							list.Add(value2);
						}
						if (!string.IsNullOrWhiteSpace(value3))
						{
							list.Add(value3);
						}
					}
				}
			}
			return string.Join(" ", list);
		}
	}
}