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 MobByLevel = new Dictionary(); 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), "(?:\"(?[^\"]+)\"|(?\\d+))\\s*:\\s*\"(?[^\"]+)\"", 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 EnableCustomSounds; public static ConfigEntry AudioSourceHierarchyFilter; public static ConfigEntry LogReplacements; public static ConfigEntry LogUnknownVanillaClipNamesOnce; public static ConfigEntry WriteDiscoveredClipsHierarchyJson; public static ConfigEntry DiscoveredClipsHierarchyFileName; public static ConfigEntry DiscoveredClipsOutputPath; public static ConfigEntry EnableSpawnOverrides; public static ConfigEntry SpawnOverridesFileName; public static ConfigEntry LogSpawnOverrides; public static ConfigEntry DebugLogHookEntrypoints; public static ConfigEntry DebugLogHookEntrypointsPerMethod; public static ConfigEntry DebugSuppressMenuSpam; public static ConfigEntry LogWeightedSoundPicks; public static void Bind(ConfigFile config) { EnableCustomSounds = config.Bind("General", "EnableCustomSounds", true, "Active les remplacements de sons personnalises."); AudioSourceHierarchyFilter = config.Bind("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("General", "LogReplacements", false, "Active les logs de remplacement audio."); LogUnknownVanillaClipNamesOnce = config.Bind("General", "LogUnknownVanillaClipNamesOnce", false, "Log chaque nom de clip vanilla non mappe une seule fois (utile pour completer replacements.json)."); WriteDiscoveredClipsHierarchyJson = config.Bind("General", "WriteDiscoveredClipsHierarchyJson", true, "Ecrit les nouveaux noms de clips rencontres dans un fichier JSON hierarchique (sans doublons)."); DiscoveredClipsHierarchyFileName = config.Bind("General", "DiscoveredClipsHierarchyFileName", "discovered_clips_hierarchy.json", "Nom du fichier JSON hierarchique qui stocke les clips decouverts."); DiscoveredClipsOutputPath = config.Bind("General", "DiscoveredClipsOutputPath", "", "Chemin absolu du dossier ou ecrire le JSON des clips decouverts (ex: .../votre-clone/CustomSounds). Vide = CustomSounds du plugin."); EnableSpawnOverrides = config.Bind("Spawning", "EnableSpawnOverrides", false, "Active le remplacement du mob principal via un mapping niveau -> mob (fichier JSON dans SpawnConfig)."); SpawnOverridesFileName = config.Bind("Spawning", "SpawnOverridesFileName", "level_enemy_overrides.json", "Nom du fichier JSON contenant le mapping niveau -> identifiant de mob."); LogSpawnOverrides = config.Bind("Spawning", "LogSpawnOverrides", true, "Log les decisions de remplacement des mobs principaux (selection, fallback, erreurs)."); DebugLogHookEntrypoints = config.Bind("Debug", "DebugLogHookEntrypoints", false, "Log les entrees des hooks audio (meme si source/clip est null)."); DebugLogHookEntrypointsPerMethod = config.Bind("Debug", "DebugLogHookEntrypointsPerMethod", 500, "Nombre max de logs d'entree par hook/methode pour eviter le spam (<= 0 = illimite)."); DebugSuppressMenuSpam = config.Bind("Debug", "DebugSuppressMenuSpam", true, "Masque le bruit des logs audio menu/UI (ex: menu hover, PlayHelper) pour faciliter le debug en partie."); LogWeightedSoundPicks = config.Bind("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(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) && 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(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 Variants = new List(); 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 ParseRootObject() { SkipWs(); Expect('{'); List list = new List(); 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 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 Parse(string json) { if (string.IsNullOrWhiteSpace(json)) { return new List(); } return new Parser(json.Trim()).ParseRootObject(); } } internal enum SoundReplacementResolve { NoMatch, KeepVanilla, Replace } internal static class SoundBank { private sealed class ClipTreeNode { public readonly Dictionary Children = new Dictionary(); public readonly List Clips = new List(); } 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 ClipsByVanillaName = new Dictionary(); private static readonly List> ContainsRules = new List>(); private static readonly HashSet ManagedClips = new HashSet(); private static readonly HashSet UnknownLoggedOnce = new HashSet(); private static readonly HashSet DiscoveredClipNames = new HashSet(); private static ManualLogSource _logger; private static string _customSoundsRootPath; private static string _discoveredOutputRootPath; private static int _resolveCacheFrame = -1; private static readonly Dictionary _resolveCacheByClipId = new Dictionary(); private static readonly HashSet _resolveCacheDebugOncePerClipThisFrame = new HashSet(); private static ConditionalWeakTable _audioSourceToVanillaClipName = new ConditionalWeakTable(); private static readonly Dictionary VanillaClipRefByExactName = new Dictionary(StringComparer.Ordinal); private static ConditionalWeakTable _managedRerollStickyBySource = new ConditionalWeakTable(); 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 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(); _managedRerollStickyBySource = new ConditionalWeakTable(); 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 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 dictionary = new Dictionary(StringComparer.Ordinal); foreach (ReplacementsJsonParser.Entry item in list) { dictionary[item.MappingKey] = item; } foreach (KeyValuePair 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 list = new List(); List list2 = new List(); List list3 = new List(); 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(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*\"(?(?:[^\"\\\\]|\\\\.)*)\"", 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*\\[(?[^\\]]*)\\]", RegexOptions.IgnoreCase)) { foreach (Match item3 in Regex.Matches(item2.Groups["arr"].Value, "\"(?(?:[^\"\\\\]|\\\\.)*)\"")) { 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 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 GetSmartTokens(string clipName) { List list = new List(); 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 list = new List(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 DebugEntryCounts = new Dictionary(); [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 : ""); string text2 = (((Object)(object)clip != (Object)null) ? ((Object)clip).name : ""); 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 DebugEntryCounts = new Dictionary(); private static IEnumerable TargetMethods() { Type type = AccessTools.TypeByName("Sound"); if (type == null) { return Enumerable.Empty(); } 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() ?? val.Field("Source").GetValue(); 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(); 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 : ""); string text3 = (((Object)(object)source != (Object)null && (Object)(object)source.clip != (Object)null) ? ((Object)source.clip).name : ""); 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 DebugEntryCounts = new Dictionary(); private static IEnumerable 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 : ""); string text2 = (((Object)(object)clip != (Object)null) ? ((Object)clip).name : ""); Plugin.Log.LogInfo((object)("[HookEntry] " + hookName + " src='" + text + "' clip='" + text2 + "'")); } } } } [HarmonyPatch] internal static class CustomSoundAudioSourceSetClipPatch { private static bool _loggedRuntimeError; private static readonly Dictionary DebugEntryCounts = new Dictionary(); 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 : ""); string text2 = (((Object)(object)value != (Object)null) ? ((Object)value).name : ""); 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 LoggedLevelNoRule = new HashSet(); private static readonly HashSet LoggedFallbacks = new HashSet(); private static readonly HashSet AppliedOverrideLevels = new HashSet(); 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 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 CollectEnemySetups(object enemyDirector, Type expectedSetupType) { List list = new List(); List 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 list = new List(); Type type = setup.GetType(); list.Add(type.Name); string value = Traverse.Create(setup).Field("name").GetValue(); 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 value3 = obj.Field("resourcePath").GetValue(); if (!string.IsNullOrWhiteSpace(value2)) { list.Add(value2); } if (!string.IsNullOrWhiteSpace(value3)) { list.Add(value3); } } } } return string.Join(" ", list); } } }