Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of SZtyrmod v1.0.36
BepInEx\plugins\SZtyrmod\SZtyrmod.dll
Decompiled a week agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SZtyrmod")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+6968621772e9047c42c05f3702f721ce77bfb213")] [assembly: AssemblyProduct("SZtyrmod")] [assembly: AssemblyTitle("SZtyrmod")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RepoMonsterAudio { [BepInPlugin("pl.local.sztyrmod", "SZtyrmod", "1.0.36")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "pl.local.sztyrmod"; public const string PluginName = "SZtyrmod"; public const string PluginVersion = "1.0.36"; private static ConfigEntry<bool>? EnabledConfig; private static ConfigEntry<bool>? DebugConfig; private static ConfigEntry<string>? RulesFileConfig; private static ConfigEntry<bool>? FixSoundApiConfig; private Harmony? _harmony; private static int _scanCount; private static int _lastEnemyCount = -1; internal static ManualLogSource Log { get; private set; } = null; internal static bool DebugLogging => DebugConfig?.Value ?? false; internal static bool Enabled => EnabledConfig?.Value ?? false; internal static string PluginDirectory { get; private set; } = string.Empty; private void Awake() { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; PluginDirectory = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location) ?? Paths.PluginPath; EnabledConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable configured monster-sound replacements."); RulesFileConfig = ((BaseUnityPlugin)this).Config.Bind<string>("General", "RulesFile", "sounds/replacements.json", "Path to the replacement rules file, relative to RepoMonsterAudio.dll."); DebugConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "DebugLogging", false, "Write a rate-limited report of discovered monster clips and replacements."); FixSoundApiConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Compatibility", "FixSoundApiRepoNullSource", true, "Guard the known null-source prefix in loaforcsSoundAPI_REPO 1.0.1 when it is installed."); _harmony = new Harmony("pl.local.sztyrmod"); try { _harmony.PatchAll(); Log.LogInfo((object)"Harmony patches applied."); LogPatchStatus(); } catch (Exception arg) { Log.LogError((object)$"Harmony patch setup failed: {arg}"); } Log.LogInfo((object)"BepInEx runtime initialized; starting enemy scan and rule loading."); SceneManager.sceneLoaded += OnSceneLoaded; TryInstallSoundApiCompatibility(_harmony); AudioCatalogLoader.Begin(); ScanEnemiesOnce(); } private static void LogPatchStatus() { MethodInfo method = AccessTools.Method(typeof(EnemyParent), "Setup", (Type[])null, (Type[])null); MethodInfo method2 = AccessTools.Method(typeof(Sound), "Play", new Type[5] { typeof(Vector3), typeof(float), typeof(float), typeof(float), typeof(float) }, (Type[])null); MethodInfo method3 = AccessTools.Method(typeof(Sound), "PlayLoop", new Type[5] { typeof(bool), typeof(float), typeof(float), typeof(float), typeof(float) }, (Type[])null); Log.LogInfo((object)("Patch verification: " + $"EnemyParent.Setup={IsPatchedByUs(method)}, " + $"Sound.Play={IsPatchedByUs(method2)}, " + $"Sound.PlayLoop={IsPatchedByUs(method3)}; " + $"debug={DebugLogging}.")); } private static bool IsPatchedByUs(MethodBase? method) { Patches val = ((method == null) ? null : Harmony.GetPatchInfo(method)); if (val != null) { return val.Owners.Contains("pl.local.sztyrmod"); } return false; } internal static void ScanEnemiesOnce() { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) try { HashSet<EnemyParent> hashSet = new HashSet<EnemyParent>(); EnemyDirector instance = EnemyDirector.instance; if ((Object)(object)instance != (Object)null) { foreach (EnemyParent item in instance.enemiesSpawned) { if ((Object)(object)item != (Object)null) { hashSet.Add(item); } } } EnemyParent[] array = Resources.FindObjectsOfTypeAll<EnemyParent>(); foreach (EnemyParent val in array) { if ((Object)(object)val != (Object)null) { Scene scene = ((Component)val).gameObject.scene; if (((Scene)(ref scene)).IsValid()) { hashSet.Add(val); } } } int num = 0; foreach (EnemyParent item2 in hashSet) { num += EnemySoundRegistry.Register(item2); } EnemySoundRegistry.RemoveDestroyed(); _scanCount++; if (DebugLogging && (num > 0 || hashSet.Count != _lastEnemyCount || _scanCount % 5 == 0)) { Log.LogInfo((object)($"Enemy scan: roots={hashSet.Count}, director={(Object)(object)instance != (Object)null}, " + $"registered={num}, tracked sounds={EnemySoundRegistry.SoundCount}, " + $"sources={EnemySoundRegistry.SourceCount}.")); } _lastEnemyCount = hashSet.Count; } catch (Exception arg) { Log.LogError((object)$"Enemy scanner failed: {arg}"); } } internal static IEnumerator LoadRules() { return AudioCatalog.Reload(RulesFileConfig?.Value ?? "sounds/replacements.json"); } internal static void TryInstallSoundApiCompatibility(Harmony harmony) { //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Expected O, but got Unknown ConfigEntry<bool>? fixSoundApiConfig = FixSoundApiConfig; if (fixSoundApiConfig == null || !fixSoundApiConfig.Value || !Chainloader.PluginInfos.TryGetValue("me.loaforc.soundapi.repo", out var value) || value.Metadata.Version.ToString() != "1.0.1") { return; } try { MethodInfo methodInfo = AccessTools.Method(AccessTools.TypeByName("loaforcsSoundAPI.REPO.Patches.SoundPatch"), "ResetAudioInfoFetched", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(Sound), "PlayLoop", new Type[5] { typeof(bool), typeof(float), typeof(float), typeof(float), typeof(float) }, (Type[])null); if (methodInfo == null || methodInfo2 == null) { Log.LogWarning((object)"SoundAPI REPO is present, but its loop-reset patch was not found."); return; } harmony.Unpatch((MethodBase)methodInfo2, (HarmonyPatchType)1, "me.loaforc.soundapi.repo"); harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(SoundApiCompatibility), "SafeResetAudioInfoFetched", (Type[])null) { priority = 800 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Installed null-safe compatibility guard for SoundAPI REPO loop handling."); } catch (Exception ex) { Log.LogWarning((object)("Could not install SoundAPI REPO compatibility guard: " + ex.Message)); } } private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; Log.LogInfo((object)"BepInEx plugin component was destroyed; static Harmony patches remain active."); } private static void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) Log.LogInfo((object)$"Scene loaded: '{((Scene)(ref scene)).name}' ({mode})."); } } internal sealed class AudioCatalogLoader : MonoBehaviour { private static AudioCatalogLoader? _instance; private bool _started; internal static void Begin() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown if (!((Object)(object)_instance != (Object)null)) { GameObject val = new GameObject("SZtyrmod Audio Loader"); Object.DontDestroyOnLoad((Object)val); _instance = val.AddComponent<AudioCatalogLoader>(); _instance.StartLoading(); } } private void Start() { StartLoading(); } private void StartLoading() { if (!_started) { _started = true; Plugin.Log.LogInfo((object)"Persistent audio loader started."); ((MonoBehaviour)this).StartCoroutine(Plugin.LoadRules()); } } private void OnDestroy() { if ((Object)(object)_instance == (Object)(object)this) { _instance = null; } } } [Serializable] public sealed class ReplacementFile { public string path = string.Empty; public int weight = 1; } [Serializable] public sealed class ReplacementRuleDefinition { public string enemy = "*"; public string[] clips = Array.Empty<string>(); public ReplacementFile[] sounds = Array.Empty<ReplacementFile>(); public float volumeMultiplier = 1f; public float pitchMultiplier = 1f; } [Serializable] public sealed class ReplacementRuleFile { public ReplacementRuleDefinition[] rules = Array.Empty<ReplacementRuleDefinition>(); } internal sealed class LoadedReplacementRule { internal string EnemyPattern = "*"; internal string[] ClipPatterns = Array.Empty<string>(); internal List<LoadedReplacementFile> Sounds = new List<LoadedReplacementFile>(); internal float VolumeMultiplier = 1f; internal float PitchMultiplier = 1f; internal bool Matches(string enemyName, string clipName) { if (Wildcard.Match(enemyName, EnemyPattern)) { return ClipPatterns.Any((string pattern) => Wildcard.Match(clipName, pattern)); } return false; } } internal sealed class LoadedReplacementFile { internal AudioClip Clip; internal int Weight; } internal static class AudioCatalog { private static readonly List<LoadedReplacementRule> Rules = new List<LoadedReplacementRule>(); private static readonly List<AudioClip> OwnedClips = new List<AudioClip>(); private static readonly List<AudioClip> RandomMonsterClips = new List<AudioClip>(); private static readonly Dictionary<EnemyParent, AudioClip> AssignedMonsterClips = new Dictionary<EnemyParent, AudioClip>(); private static readonly Random Random = new Random(); private static bool _ready; internal static bool TryPick(string enemyName, AudioClip original, out AudioClip replacement, out float volumeMultiplier, out float pitchMultiplier) { replacement = null; volumeMultiplier = 1f; pitchMultiplier = 1f; if (!Plugin.Enabled || !_ready || (Object)(object)original == (Object)null) { return false; } foreach (LoadedReplacementRule rule in Rules) { if (!rule.Matches(enemyName, ((Object)original).name) || rule.Sounds.Count == 0) { continue; } int maxValue = rule.Sounds.Sum((LoadedReplacementFile sound) => Math.Max(1, sound.Weight)); int num = Random.Next(maxValue); foreach (LoadedReplacementFile sound in rule.Sounds) { num -= Math.Max(1, sound.Weight); if (num < 0) { replacement = sound.Clip; volumeMultiplier = rule.VolumeMultiplier; pitchMultiplier = rule.PitchMultiplier; EnemyAudioDiagnostics.Report(enemyName, original, replacement); return true; } } } return false; } internal static bool TryPickForMonster(EnemyParent? owner, string enemyName, AudioClip original, out AudioClip replacement, out float volumeMultiplier, out float pitchMultiplier) { replacement = null; volumeMultiplier = 1f; pitchMultiplier = 1f; if (!Plugin.Enabled || !_ready || (Object)(object)original == (Object)null) { return false; } if (TryPick(enemyName, original, out replacement, out volumeMultiplier, out pitchMultiplier)) { return true; } if ((Object)(object)owner != (Object)null && RandomMonsterClips.Count > 0) { if (!AssignedMonsterClips.TryGetValue(owner, out replacement) || (Object)(object)replacement == (Object)null) { replacement = RandomMonsterClips[Random.Next(RandomMonsterClips.Count)]; AssignedMonsterClips[owner] = replacement; if (Plugin.DebugLogging) { Plugin.Log.LogInfo((object)("Assigned '" + ((Object)replacement).name + "' to monster '" + owner.enemyName + "' for this round.")); } } return true; } return false; } internal static IEnumerator Reload(string configuredPath) { Dispose(); string rulesPath = TryGetPathInsidePlugin(configuredPath); if (rulesPath == null) { Plugin.Log.LogError((object)("RulesFile must stay inside the mod folder: " + configuredPath)); yield break; } IEnumerator randomLoader = LoadRandomMonsterFolder(); while (randomLoader.MoveNext()) { yield return randomLoader.Current; } Plugin.Log.LogInfo((object)("Loading replacement rules from: " + rulesPath)); if (!File.Exists(rulesPath)) { Plugin.Log.LogWarning((object)("Replacement rules were not found: " + rulesPath + ". Random-folder mode is still available.")); _ready = true; yield break; } ReplacementRuleFile replacementRuleFile; try { replacementRuleFile = JsonConvert.DeserializeObject<ReplacementRuleFile>(File.ReadAllText(rulesPath)); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not parse replacement rules: " + ex.Message)); yield break; } if (replacementRuleFile?.rules == null || replacementRuleFile.rules.Length == 0) { Plugin.Log.LogWarning((object)"Replacement rules file contains no rules."); _ready = true; yield break; } Dictionary<string, AudioClip> clipCache = new Dictionary<string, AudioClip>(StringComparer.OrdinalIgnoreCase); int skippedRules = 0; ReplacementRuleDefinition[] rules = replacementRuleFile.rules; foreach (ReplacementRuleDefinition replacementRuleDefinition in rules) { if (replacementRuleDefinition == null || replacementRuleDefinition.clips == null || replacementRuleDefinition.clips.Length == 0 || replacementRuleDefinition.sounds == null || replacementRuleDefinition.sounds.Length == 0) { skippedRules++; continue; } LoadedReplacementRule rule = new LoadedReplacementRule { EnemyPattern = (string.IsNullOrWhiteSpace(replacementRuleDefinition.enemy) ? "*" : replacementRuleDefinition.enemy), ClipPatterns = replacementRuleDefinition.clips.Where((string pattern) => !string.IsNullOrWhiteSpace(pattern)).ToArray(), VolumeMultiplier = Mathf.Max(0f, replacementRuleDefinition.volumeMultiplier), PitchMultiplier = Mathf.Max(0.01f, replacementRuleDefinition.pitchMultiplier) }; ReplacementFile[] sounds = replacementRuleDefinition.sounds; foreach (ReplacementFile definitionFile in sounds) { if (definitionFile == null || string.IsNullOrWhiteSpace(definitionFile.path)) { continue; } string audioPath = TryGetPathInsidePlugin(Path.Combine("sounds", definitionFile.path)); if (audioPath == null || !File.Exists(audioPath)) { audioPath = TryGetPathInsidePlugin(definitionFile.path); } if (audioPath == null || !File.Exists(audioPath)) { Plugin.Log.LogWarning((object)("Replacement audio was not found: " + definitionFile.path)); continue; } if (!clipCache.TryGetValue(audioPath, out AudioClip value)) { AudioType? audioType = GetAudioType(audioPath); if (!audioType.HasValue) { Plugin.Log.LogWarning((object)("Unsupported replacement format: " + definitionFile.path)); continue; } UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(new Uri(audioPath).AbsoluteUri, audioType.Value); try { yield return request.SendWebRequest(); if ((int)request.result != 1) { Plugin.Log.LogWarning((object)("Could not load replacement audio '" + definitionFile.path + "': " + request.error)); continue; } value = DownloadHandlerAudioClip.GetContent(request); ((Object)value).name = "RepoMonsterAudio_" + Path.GetFileNameWithoutExtension(audioPath); clipCache.Add(audioPath, value); OwnedClips.Add(value); } finally { ((IDisposable)request)?.Dispose(); } } rule.Sounds.Add(new LoadedReplacementFile { Clip = value, Weight = Math.Max(1, definitionFile.weight) }); } if (rule.ClipPatterns.Length == 0 || rule.Sounds.Count == 0) { skippedRules++; } else { Rules.Add(rule); } } _ready = true; Plugin.Log.LogInfo((object)($"Loaded {RandomMonsterClips.Count} random monster clip(s), {Rules.Count} replacement rule(s), " + $"{OwnedClips.Count} total audio clip(s); skipped {skippedRules} invalid rule(s).")); } private static IEnumerator LoadRandomMonsterFolder() { string text = TryGetPathInsidePlugin(Path.Combine("sounds", "random")); if (text == null || !Directory.Exists(text)) { Plugin.Log.LogInfo((object)"Random monster audio folder was not found; using configured rules only."); yield break; } string[] audioPaths = (from path in Directory.GetFiles(text, "*.*", SearchOption.AllDirectories) where GetAudioType(path).HasValue select path).OrderBy<string, string>((string path) => path, StringComparer.OrdinalIgnoreCase).ToArray(); Plugin.Log.LogInfo((object)$"Found {audioPaths.Length} random monster audio file(s); loading them now."); string[] array = audioPaths; foreach (string audioPath in array) { AudioType? audioType = GetAudioType(audioPath); UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(new Uri(audioPath).AbsoluteUri, audioType.Value); try { request.timeout = 10; yield return request.SendWebRequest(); if ((int)request.result != 1) { Plugin.Log.LogWarning((object)("Could not load random monster audio '" + Path.GetFileName(audioPath) + "': " + request.error)); continue; } AudioClip content = DownloadHandlerAudioClip.GetContent(request); ((Object)content).name = "RepoMonsterAudio_Random_" + Path.GetFileNameWithoutExtension(audioPath); RandomMonsterClips.Add(content); OwnedClips.Add(content); _ready = true; if (Plugin.DebugLogging && RandomMonsterClips.Count % 25 == 0) { Plugin.Log.LogInfo((object)$"Loaded {RandomMonsterClips.Count}/{audioPaths.Length} random monster audio file(s)."); } } finally { ((IDisposable)request)?.Dispose(); } } Plugin.Log.LogInfo((object)$"Random monster audio loading finished: {RandomMonsterClips.Count}/{audioPaths.Length} file(s) ready."); } internal static void Dispose() { _ready = false; Rules.Clear(); RandomMonsterClips.Clear(); AssignedMonsterClips.Clear(); foreach (AudioClip ownedClip in OwnedClips) { if ((Object)(object)ownedClip != (Object)null) { Object.Destroy((Object)(object)ownedClip); } } OwnedClips.Clear(); } private static string? TryGetPathInsidePlugin(string relativePath) { try { string fullPath = Path.GetFullPath(Plugin.PluginDirectory); string fullPath2 = Path.GetFullPath(Path.Combine(fullPath, relativePath)); return fullPath2.StartsWith(fullPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ? fullPath2 : null; } catch { return null; } } private static AudioType? GetAudioType(string path) { return Path.GetExtension(path).ToLowerInvariant() switch { ".ogg" => (AudioType)14, ".wav" => (AudioType)20, ".mp3" => (AudioType)13, _ => null, }; } } internal static class Wildcard { internal static bool Match(string value, string pattern) { value = value ?? string.Empty; pattern = (string.IsNullOrWhiteSpace(pattern) ? "*" : pattern); int num = 0; int i = 0; int num2 = -1; int num3 = 0; while (num < value.Length) { if (i < pattern.Length && (pattern[i] == '?' || char.ToUpperInvariant(pattern[i]) == char.ToUpperInvariant(value[num]))) { num++; i++; continue; } if (i < pattern.Length && pattern[i] == '*') { num2 = i++; num3 = num; continue; } if (num2 >= 0) { i = num2 + 1; num = ++num3; continue; } return false; } for (; i < pattern.Length && pattern[i] == '*'; i++) { } return i == pattern.Length; } } internal static class EnemySoundRegistry { private static readonly Dictionary<Sound, EnemyParent> Sounds = new Dictionary<Sound, EnemyParent>(); private static readonly Dictionary<AudioSource, EnemyParent> Sources = new Dictionary<AudioSource, EnemyParent>(); internal static int SoundCount => Sounds.Count; internal static int SourceCount => Sources.Count; internal static int Register(EnemyParent enemyParent) { int num = Sounds.Count + Sources.Count; AudioSource[] componentsInChildren = ((Component)enemyParent).GetComponentsInChildren<AudioSource>(true); foreach (AudioSource key in componentsInChildren) { Sources[key] = enemyParent; } MonoBehaviour[] componentsInChildren2 = ((Component)enemyParent).GetComponentsInChildren<MonoBehaviour>(true); for (int i = 0; i < componentsInChildren2.Length; i++) { RegisterSoundFields(componentsInChildren2[i], enemyParent); } return Sounds.Count + Sources.Count - num; } internal static bool TryGetOwner(Sound sound, out EnemyParent owner) { if (Sounds.TryGetValue(sound, out EnemyParent value) && (Object)(object)value != (Object)null) { owner = value; return true; } owner = null; return false; } internal static bool TryGetOwner(AudioSource source, out EnemyParent owner) { if (Sources.TryGetValue(source, out EnemyParent value) && (Object)(object)value != (Object)null) { owner = value; return true; } owner = ((Component)source).GetComponentInParent<EnemyParent>(true); if ((Object)(object)owner != (Object)null) { Sources[source] = owner; return true; } return false; } internal static void RemoveDestroyed() { Sound[] array = Sounds.Keys.Where((Sound sound) => sound == null || !Object.op_Implicit((Object)(object)Sounds[sound])).ToArray(); foreach (Sound key in array) { Sounds.Remove(key); } AudioSource[] array2 = Sources.Keys.Where((AudioSource source) => (Object)(object)source == (Object)null || !Object.op_Implicit((Object)(object)Sources[source])).ToArray(); foreach (AudioSource key2 in array2) { Sources.Remove(key2); } } private static void RegisterSoundFields(MonoBehaviour component, EnemyParent owner) { Type type = ((object)component).GetType(); while (type != null) { FieldInfo[] fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { try { object value = fieldInfo.GetValue(component); Sound val = (Sound)((value is Sound) ? value : null); if (val != null) { RegisterSound(val, owner); } else { if (!(value is IEnumerable<Sound> enumerable)) { continue; } foreach (Sound item in enumerable) { if (item != null) { RegisterSound(item, owner); } } continue; } } catch (Exception ex) { if (Plugin.DebugLogging) { Plugin.Log.LogWarning((object)("Could not inspect " + ((object)component).GetType().Name + "." + fieldInfo.Name + ": " + ex.Message)); } } } type = type.BaseType; } } private static void RegisterSound(Sound sound, EnemyParent owner) { Sounds[sound] = owner; if ((Object)(object)sound.Source != (Object)null) { Sources[sound.Source] = owner; } } } [HarmonyPatch(typeof(EnemyParent), "Awake")] internal static class EnemyRegistrationPatch { [HarmonyPostfix] private static void RegisterEnemySounds(EnemyParent __instance) { int num = EnemySoundRegistry.Register(__instance); if (Plugin.DebugLogging) { Plugin.Log.LogInfo((object)$"EnemyParent.Awake: '{__instance.enemyName}', registered={num}."); } } } [HarmonyPatch(typeof(EnemyParent), "Setup")] internal static class EnemySetupRegistrationPatch { [HarmonyPostfix] private static void RegisterEnemyAfterSetupStarted(EnemyParent __instance) { int num = EnemySoundRegistry.Register(__instance); if (Plugin.DebugLogging) { Plugin.Log.LogInfo((object)($"EnemyParent.Setup: '{__instance.enemyName}', registered={num}, " + $"tracked sounds={EnemySoundRegistry.SoundCount}, " + $"sources={EnemySoundRegistry.SourceCount}.")); } } } [HarmonyPatch(typeof(Enemy), "Start")] internal static class EnemyInstanceRegistrationPatch { [HarmonyPostfix] private static void RegisterEnemyInstance(Enemy __instance) { EnemyParent val = __instance.EnemyParent ?? ((Component)__instance).GetComponentInParent<EnemyParent>(true); if ((Object)(object)val == (Object)null) { if (Plugin.DebugLogging) { Plugin.Log.LogWarning((object)("Enemy.Start: '" + ((Object)__instance).name + "' has no EnemyParent.")); } return; } int num = EnemySoundRegistry.Register(val); if (Plugin.DebugLogging) { Plugin.Log.LogInfo((object)($"Enemy.Start: '{val.enemyName}', registered={num}, " + $"tracked sounds={EnemySoundRegistry.SoundCount}.")); } } } internal struct SoundOverrideState { internal bool Applied; internal AudioClip[]? Sounds; internal float Volume; internal float Pitch; } internal static class EnemySoundCallContext { internal static string? FindEnemyCaller() { StackFrame[] array = new StackTrace(3, fNeedFileInfo: false).GetFrames() ?? Array.Empty<StackFrame>(); for (int i = 0; i < array.Length; i++) { Type type = array[i].GetMethod()?.DeclaringType; if (type?.Assembly == typeof(Enemy).Assembly && type.Name.StartsWith("Enemy", StringComparison.Ordinal)) { return type.Name; } } return null; } } internal static class EnemySoundIdentity { internal static bool TryGetName(Sound sound, out string enemyName) { if (EnemySoundRegistry.TryGetOwner(sound, out EnemyParent owner)) { enemyName = owner.enemyName; return true; } string text = EnemySoundCallContext.FindEnemyCaller(); if (text != null) { enemyName = text; return true; } AudioClip[] array = sound.Sounds ?? Array.Empty<AudioClip>(); for (int i = 0; i < array.Length; i++) { if (TryGetName(array[i], out enemyName)) { return true; } } enemyName = string.Empty; return false; } internal static bool TryGetName(AudioSource source, AudioClip clip, out string enemyName) { if (EnemySoundRegistry.TryGetOwner(source, out EnemyParent owner)) { enemyName = owner.enemyName; return true; } return TryGetName(clip, out enemyName); } private static bool TryGetName(AudioClip? clip, out string enemyName) { if ((Object)(object)clip != (Object)null && ((Object)clip).name.StartsWith("enemy ", StringComparison.OrdinalIgnoreCase)) { string text = ((Object)clip).name.Substring("enemy ".Length); int num = text.IndexOf(' '); enemyName = ((num > 0) ? text.Substring(0, num) : text); return !string.IsNullOrWhiteSpace(enemyName); } enemyName = string.Empty; return false; } } [HarmonyPatch(typeof(Sound))] internal static class EnemySoundPatches { private struct LoopParameterState { internal float Volume; internal float Pitch; } private static readonly Dictionary<Sound, LoopParameterState> LoopParameters = new Dictionary<Sound, LoopParameterState>(); [HarmonyPrefix] [HarmonyPatch("Play", new Type[] { typeof(Vector3), typeof(float), typeof(float), typeof(float), typeof(float) })] private static void ReplacePlaySound(Sound __instance, out SoundOverrideState __state) { Apply(__instance, isLoop: false, out __state); } [HarmonyPostfix] [HarmonyPatch("Play", new Type[] { typeof(Vector3), typeof(float), typeof(float), typeof(float), typeof(float) })] private static void RestorePlaySound(Sound __instance, SoundOverrideState __state) { Restore(__instance, __state, isLoop: false); } [HarmonyFinalizer] [HarmonyPatch("Play", new Type[] { typeof(Vector3), typeof(float), typeof(float), typeof(float), typeof(float) })] private static Exception? RestorePlaySoundOnException(Sound __instance, SoundOverrideState __state, Exception? __exception) { Restore(__instance, __state, isLoop: false); return __exception; } [HarmonyPrefix] [HarmonyPatch("PlayLoop", new Type[] { typeof(bool), typeof(float), typeof(float), typeof(float), typeof(float) })] private static void ReplaceLoopSound(Sound __instance, out SoundOverrideState __state) { Apply(__instance, isLoop: true, out __state); } [HarmonyPostfix] [HarmonyPatch("PlayLoop", new Type[] { typeof(bool), typeof(float), typeof(float), typeof(float), typeof(float) })] private static void RestoreLoopSound(Sound __instance, SoundOverrideState __state) { Restore(__instance, __state, isLoop: true); } [HarmonyFinalizer] [HarmonyPatch("PlayLoop", new Type[] { typeof(bool), typeof(float), typeof(float), typeof(float), typeof(float) })] private static Exception? RestoreLoopSoundOnException(Sound __instance, SoundOverrideState __state, Exception? __exception) { Restore(__instance, __state, isLoop: true); return __exception; } private static void Apply(Sound sound, bool isLoop, out SoundOverrideState state) { state = default(SoundOverrideState); EnemyAudioDiagnostics.ReportSoundObserved(sound, isLoop); if (sound.Sounds == null || !EnemySoundIdentity.TryGetName(sound, out string enemyName)) { return; } AudioClip[] sounds = sound.Sounds; AudioClip[] array = (AudioClip[])(object)new AudioClip[sounds.Length]; bool flag = false; float num = 1f; float num2 = 1f; for (int i = 0; i < sounds.Length; i++) { AudioClip val = (array[i] = sounds[i]); EnemyParent owner2; EnemyParent owner = (EnemySoundRegistry.TryGetOwner(sound, out owner2) ? owner2 : null); if ((Object)(object)val != (Object)null && AudioCatalog.TryPickForMonster(owner, enemyName, val, out AudioClip replacement, out float volumeMultiplier, out float pitchMultiplier)) { array[i] = replacement; num = volumeMultiplier; num2 = pitchMultiplier; flag = true; } } if (!flag) { if (isLoop && LoopParameters.TryGetValue(sound, out var value)) { sound.Volume = value.Volume; sound.Pitch = value.Pitch; LoopParameters.Remove(sound); } return; } state = new SoundOverrideState { Applied = true, Sounds = sounds, Volume = sound.Volume, Pitch = sound.Pitch }; sound.Sounds = array; if (isLoop) { if (!LoopParameters.TryGetValue(sound, out var value2)) { value2 = new LoopParameterState { Volume = sound.Volume, Pitch = sound.Pitch }; LoopParameters.Add(sound, value2); } sound.Volume = value2.Volume * num; sound.Pitch = value2.Pitch * num2; if ((Object)(object)sound.Source != (Object)null) { SoundFieldAccess.ResetAudioInfoFetched(sound); } } else { sound.Volume *= num; sound.Pitch *= num2; } } private static void Restore(Sound sound, SoundOverrideState state, bool isLoop) { if (state.Applied) { sound.Sounds = state.Sounds; if (!isLoop) { sound.Volume = state.Volume; sound.Pitch = state.Pitch; } } } } [HarmonyPatch(typeof(AudioSource))] internal static class EnemyAudioSourcePatches { [HarmonyPrefix] [HarmonyPatch("PlayOneShot", new Type[] { typeof(AudioClip) })] private static void ReplaceOneShotWithoutVolume(AudioSource __instance, ref AudioClip clip) { EnemyAudioDiagnostics.ReportOneShotObserved(__instance, clip); EnemyParent owner2; EnemyParent owner = (EnemySoundRegistry.TryGetOwner(__instance, out owner2) ? owner2 : null); if ((Object)(object)clip != (Object)null && EnemySoundIdentity.TryGetName(__instance, clip, out string enemyName) && AudioCatalog.TryPickForMonster(owner, enemyName, clip, out AudioClip replacement, out float _, out float _)) { clip = replacement; } } [HarmonyPrefix] [HarmonyPatch("PlayOneShot", new Type[] { typeof(AudioClip), typeof(float) })] private static void ReplaceOneShot(AudioSource __instance, ref AudioClip clip, ref float volumeScale) { EnemyAudioDiagnostics.ReportOneShotObserved(__instance, clip); EnemyParent owner2; EnemyParent owner = (EnemySoundRegistry.TryGetOwner(__instance, out owner2) ? owner2 : null); if ((Object)(object)clip != (Object)null && EnemySoundIdentity.TryGetName(__instance, clip, out string enemyName) && AudioCatalog.TryPickForMonster(owner, enemyName, clip, out AudioClip replacement, out float volumeMultiplier, out float _)) { clip = replacement; volumeScale *= volumeMultiplier; } } } internal static class SoundApiCompatibility { internal static void SafeResetAudioInfoFetched(Sound __instance) { if (__instance != null && (Object)(object)__instance.Source != (Object)null && (Object)(object)__instance.Source.clip == (Object)null) { SoundFieldAccess.ResetAudioInfoFetched(__instance); } } } internal static class SoundFieldAccess { private static readonly FieldInfo? AudioInfoFetched = AccessTools.Field(typeof(Sound), "AudioInfoFetched"); internal static void ResetAudioInfoFetched(Sound sound) { AudioInfoFetched?.SetValue(sound, false); } } internal static class EnemyAudioDiagnostics { private static readonly HashSet<string> Seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<Sound> ObservedSounds = new HashSet<Sound>(); private static readonly HashSet<string> ObservedOneShots = new HashSet<string>(StringComparer.Ordinal); internal static void ReportSoundObserved(Sound sound, bool isLoop) { if (Plugin.DebugLogging && ObservedSounds.Add(sound)) { string text = ((sound.Sounds == null) ? "none" : string.Join(", ", from clip in sound.Sounds where (Object)(object)clip != (Object)null select ((Object)clip).name)); string text2 = EnemySoundCallContext.FindEnemyCaller() ?? "none"; Plugin.Log.LogInfo((object)("Observed Sound." + (isLoop ? "PlayLoop" : "Play") + ": clips=[" + text + "], enemy caller=" + text2 + ".")); } } internal static void ReportOneShotObserved(AudioSource source, AudioClip? clip) { if (Plugin.DebugLogging && !((Object)(object)clip == (Object)null)) { string item = ((Object)source).name + ":" + ((Object)clip).name; if (ObservedOneShots.Add(item)) { Plugin.Log.LogInfo((object)("Observed AudioSource.PlayOneShot: source='" + ((Object)source).name + "', clip='" + ((Object)clip).name + "'.")); } } } internal static void Report(string enemyName, AudioClip original, AudioClip replacement) { if (Plugin.DebugLogging) { string item = enemyName + ":" + ((Object)original).name + ":" + ((Object)replacement).name; if (Seen.Add(item)) { Plugin.Log.LogInfo((object)("Replacing '" + ((Object)original).name + "' for enemy '" + enemyName + "' with '" + ((Object)replacement).name + "'.")); } } } } }