using 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 System.Text; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Core.Logging.Interpolation; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using HarmonyLib; using Il2CppInterop.Runtime.Injection; using Microsoft.CodeAnalysis; using NAudio.Wave; using NVorbis; using P1.Sound; using P1.System.Sound; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("PataponMusicMenuManualMood")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("PataponMusicMenuManualMood")] [assembly: AssemblyTitle("PataponMusicMenuManualMood")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace PataponMusicMenuManualMood { internal sealed class RuntimeSetting { private T _value; private readonly ConfigEntry? _entry; public T Value { get { return (_entry != null) ? _entry.Value : _value; } set { _value = value; if (_entry != null) { _entry.Value = value; } } } public RuntimeSetting(T value) { _value = value; } public RuntimeSetting(ConfigEntry entry) { _entry = entry; _value = entry.Value; } } [BepInPlugin("juras.patapon.musicmenu.manualmood", "Patapon Music Menu Manual Mood Prototype", "0.4.6")] public sealed class Plugin : BasePlugin { public const string PluginGuid = "juras.patapon.musicmenu.manualmood"; public const string PluginName = "Patapon Music Menu Manual Mood Prototype"; public const string PluginVersion = "0.4.6"; internal static ManualLogSource L = null; internal static Harmony H = null; private static Plugin? _instance; internal static RuntimeSetting Enable = null; internal static RuntimeSetting Trace = null; internal static RuntimeSetting P1StartSessionOnStartPlayGame = null; internal static RuntimeSetting P1StartSessionOnHelperRhythmPlayMidi = null; internal static RuntimeSetting P1StartSessionOnBattleAddMidi = null; internal static RuntimeSetting P1MuteHelperCategoryFromDirectorPlayMidi = null; internal static RuntimeSetting P1MuteBgmCategoryFromDirectorPlayMidi = null; internal static RuntimeSetting P1MuteUnknownMidi0CategoriesFromDirectorPlayMidi = null; internal static RuntimeSetting P1OnlyMuteMidiIndex0 = null; internal static RuntimeSetting P1MuteAddMidiResults = null; internal static RuntimeSetting P1MuteAddMidiBgmCategory = null; internal static RuntimeSetting P1MuteAddMidiRhythmCategory = null; internal static RuntimeSetting P1MuteAddMidiOnlyWhenBattleOrMissionFilename = null; internal static RuntimeSetting P1ReapplyMuteOnRhythmUpdate = null; internal static RuntimeSetting P1MaxAddMidiTraceLogs = null; internal static RuntimeSetting P1ZeroCriPlayDirectRhythmMissionKaesi = null; internal static RuntimeSetting P1ZeroCriPlayDirectAllRhythm = null; internal static RuntimeSetting P1TraceCriPlayDirectRhythm = null; internal static RuntimeSetting P1MaxCriPlayDirectTraceLogs = null; internal static RuntimeSetting ThemeEnable = null; internal static RuntimeSetting ThemeDirectory = null; internal static RuntimeSetting ThemeUseNumberedThemeFolders = null; internal static RuntimeSetting ThemeLibraryRootDirectory = null; internal static RuntimeSetting ThemeSelectedThemeNumber = null; internal static RuntimeSetting ThemePartsFolderSuffix = null; internal static RuntimeSetting ThemeVoicesFolderSuffix = null; internal static RuntimeSetting ThemeVolume = null; internal static RuntimeSetting ThemeMasterVolumeScale = null; internal static RuntimeSetting ThemeTrace = null; internal static RuntimeSetting ThemeAutoStartMood0OnBattleStart = null; internal static RuntimeSetting ThemeInitialDelaySeconds = null; internal static RuntimeSetting ThemeScheduleLookAheadSeconds = null; internal static RuntimeSetting ThemeLoadAudioAtPluginStart = null; internal static RuntimeSetting ThemePartsPerLoadFrame = null; internal static RuntimeSetting ThemeTraceBeforeEachPartLoad = null; internal static RuntimeSetting ThemeUseDecodeTimeout = null; internal static RuntimeSetting ThemeDecodeTimeoutSeconds = null; internal static RuntimeSetting ThemeSkipPartNames = null; internal static RuntimeSetting ThemeUseFixedPartWindow = null; internal static RuntimeSetting ThemeAnchorFixedWindowClock = null; internal static RuntimeSetting ThemeTraceFixedWindowClock = null; internal static RuntimeSetting ThemeResyncOnP1RhythmBoundary = null; internal static RuntimeSetting ThemeTraceRhythmBoundaryResync = null; internal static RuntimeSetting ThemeFrameStallCompensation = null; internal static RuntimeSetting ThemeFrameStallThresholdSeconds = null; internal static RuntimeSetting ThemeFrameStallMaxCompensationSeconds = null; internal static RuntimeSetting ThemeTraceFrameStallCompensation = null; internal static RuntimeSetting ThemeFixedPartWindowSeconds = null; internal static RuntimeSetting ThemeFixedWindowPhaseOffsetSeconds = null; internal static RuntimeSetting ThemeUseSeparateHelperPhaseOffset = null; internal static RuntimeSetting ThemeHelperPhaseOffsetSeconds = null; internal static RuntimeSetting ThemeKeepHelperLoopRunningMuted = null; internal static RuntimeSetting ThemeHelperStartsAfterIntroWindow = null; internal static RuntimeSetting ThemeAutoMoodFromP1Rhythm = null; internal static RuntimeSetting ThemeAutoMoodImmediateSwitch = null; internal static RuntimeSetting ThemeHighMoodMapsTo = null; internal static RuntimeSetting ThemeTraceAutoMood = null; internal static RuntimeSetting ThemeMoodLossSwitchToHelper = null; internal static RuntimeSetting ThemeDetectMoodLossFromRhythmUpdate = null; internal static RuntimeSetting ThemeTraceRhythmUpdateMoodLoss = null; internal static RuntimeSetting ThemeMaxRhythmUpdateMoodLossTraceLogs = null; internal static RuntimeSetting ThemeStopOnFailedPlayGame = null; internal static RuntimeSetting ThemeStopOnStopBattleBgm = null; internal static RuntimeSetting ResponseEnable = null; internal static RuntimeSetting ResponseMuteOriginalRhythmVoice = null; internal static RuntimeSetting ResponsePlayCustomResponses = null; internal static RuntimeSetting ResponsePlayFirstLoadedAsFallback = null; internal static RuntimeSetting ResponseSuppressUnmappedFallback = null; internal static RuntimeSetting ResponseTestCueName = null; internal static RuntimeSetting ResponseVolume = null; internal static RuntimeSetting ResponseMasterVolumeScale = null; internal static RuntimeSetting ExternalAudioGameVolumeFactor = null; private static CriSoundManager? _cachedCriSoundManager; private static float _cachedGameMasterVolume = 1f; private static double _lastGameMasterVolumeSampleTime = -9999.0; internal static RuntimeSetting ResponseDebounceSeconds = null; internal static RuntimeSetting ResponseDelaySeconds = null; internal static RuntimeSetting ResponseTraceOriginalRhythmVoice = null; internal static RuntimeSetting ResponseMaxTraceLogs = null; internal static RuntimeSetting ResponseMiscVoicesDirectory = null; internal static RuntimeSetting ResponseMiscVoiceNumberPrefix = null; internal static RuntimeSetting ResponseUseCommandDetection = null; internal static RuntimeSetting ResponseCommandMaxAgeSeconds = null; internal static RuntimeSetting ResponseTraceCommandDetection = null; internal static RuntimeSetting ResponseMaxCommandTraceLogs = null; internal static RuntimeSetting ResponseCommandSummaryMaxChars = null; internal static RuntimeSetting ResponseP1EvadeMapsTo = null; internal static RuntimeSetting PerformanceReduceLogSpam = null; internal static RuntimeSetting PerformanceForceGameplayQuietMode = null; internal static bool Enabled => Enable.Value != 0; internal static bool TraceEnabled => Enabled && Trace.Value != 0; internal static bool StartP1SessionOnStartPlayGame => Enabled && P1StartSessionOnStartPlayGame.Value != 0; internal static bool StartP1SessionOnHelperRhythmPlayMidi => Enabled && P1StartSessionOnHelperRhythmPlayMidi.Value != 0; internal static bool StartP1SessionOnBattleAddMidi => Enabled && P1StartSessionOnBattleAddMidi.Value != 0; internal static bool MuteP1HelperCategoryFromDirectorPlayMidi => Enabled && P1MuteHelperCategoryFromDirectorPlayMidi.Value != 0; internal static bool MuteP1BgmCategoryFromDirectorPlayMidi => Enabled && P1MuteBgmCategoryFromDirectorPlayMidi.Value != 0; internal static bool MuteP1UnknownMidi0CategoriesFromDirectorPlayMidi => Enabled && P1MuteUnknownMidi0CategoriesFromDirectorPlayMidi.Value != 0; internal static bool OnlyMuteMidiIndex0 => P1OnlyMuteMidiIndex0.Value != 0; internal static bool MuteP1AddMidiResults => Enabled && P1MuteAddMidiResults.Value != 0; internal static bool MuteP1AddMidiBgmCategory => Enabled && P1MuteAddMidiBgmCategory.Value != 0; internal static bool MuteP1AddMidiRhythmCategory => Enabled && P1MuteAddMidiRhythmCategory.Value != 0; internal static bool MuteP1AddMidiOnlyWhenBattleOrMissionFilename => Enabled && P1MuteAddMidiOnlyWhenBattleOrMissionFilename.Value != 0; internal static bool ReapplyP1MuteOnRhythmUpdate => Enabled && P1ReapplyMuteOnRhythmUpdate.Value != 0; internal static int MaxP1AddMidiTraceLogs => Math.Max(0, P1MaxAddMidiTraceLogs.Value); internal static bool ZeroP1CriPlayDirectRhythmMissionKaesi => Enabled && P1ZeroCriPlayDirectRhythmMissionKaesi.Value != 0; internal static bool ZeroP1CriPlayDirectAllRhythm => Enabled && P1ZeroCriPlayDirectAllRhythm.Value != 0; internal static bool TraceP1CriPlayDirectRhythm => TraceEnabled && P1TraceCriPlayDirectRhythm.Value != 0; internal static int MaxP1CriPlayDirectTraceLogs => Math.Max(0, P1MaxCriPlayDirectTraceLogs.Value); internal static bool ExternalThemeEnabled => Enabled && ThemeEnable.Value != 0; internal static bool ExternalThemeTraceEnabled => ExternalThemeEnabled && ThemeTrace.Value != 0 && !ReduceLogSpam; internal static bool ExternalThemeAutoStartMood0 => ExternalThemeEnabled && ThemeAutoStartMood0OnBattleStart.Value != 0; internal static string ExternalThemeDirectory => ThemePathResolver.ResolvePartsDirectory(); internal static string ExternalThemeLegacyDirectory => ThemeDirectory.Value ?? string.Empty; internal static bool ExternalThemeUseNumberedThemeFolders => ExternalThemeEnabled && ThemeUseNumberedThemeFolders.Value != 0; internal static string ExternalThemeLibraryRootDirectory => ThemeLibraryRootDirectory.Value ?? string.Empty; internal static int ExternalThemeSelectedThemeNumber => Math.Max(0, ThemeSelectedThemeNumber.Value); internal static string ExternalThemePartsFolderSuffix => ThemePartsFolderSuffix.Value ?? "Parts"; internal static string ExternalThemeVoicesFolderSuffix => ThemeVoicesFolderSuffix.Value ?? "Voices"; internal static string ExternalThemeVoicesDirectory => ThemePathResolver.ResolveVoicesDirectory(); internal static float ExternalGameScaledVolume => GetCurrentGameMasterVolume() * Math.Max(0f, ExternalAudioGameVolumeFactor.Value); internal static float ExternalThemeVolume => ExternalGameScaledVolume * Math.Max(0f, ThemeVolume.Value) * Math.Max(0f, ThemeMasterVolumeScale.Value); internal static double ExternalThemeInitialDelaySeconds => Math.Max(0.01, ThemeInitialDelaySeconds.Value); internal static double ExternalThemeScheduleLookAheadSeconds => Math.Max(0.02, ThemeScheduleLookAheadSeconds.Value); internal static bool ExternalThemeLoadAudioAtPluginStart => ThemeLoadAudioAtPluginStart.Value != 0; internal static int ExternalThemePartsPerLoadFrame => Math.Clamp(ThemePartsPerLoadFrame.Value, 1, 28); internal static bool ExternalThemeTraceBeforeEachPartLoad => ThemeTraceBeforeEachPartLoad.Value != 0; internal static bool ExternalThemeUseDecodeTimeout => ExternalThemeEnabled && ThemeUseDecodeTimeout.Value != 0; internal static double ExternalThemeDecodeTimeoutSeconds => Math.Clamp(ThemeDecodeTimeoutSeconds.Value, 0.25, 30.0); internal static string ExternalThemeSkipPartNames => ThemeSkipPartNames.Value ?? string.Empty; internal static bool ExternalThemeUseFixedPartWindow => ExternalThemeEnabled && ThemeUseFixedPartWindow.Value != 0; internal static bool ExternalThemeAnchorFixedWindowClock => ExternalThemeUseFixedPartWindow && ThemeAnchorFixedWindowClock.Value != 0; internal static bool ExternalThemeTraceFixedWindowClock => ExternalThemeUseFixedPartWindow && ThemeTraceFixedWindowClock.Value != 0 && !ReduceLogSpam; internal static bool ExternalThemeResyncOnP1RhythmBoundary => ExternalThemeUseFixedPartWindow && ThemeResyncOnP1RhythmBoundary.Value != 0; internal static bool ExternalThemeTraceRhythmBoundaryResync => ExternalThemeUseFixedPartWindow && ThemeTraceRhythmBoundaryResync.Value != 0 && !ReduceLogSpam; internal static bool ExternalThemeFrameStallCompensation => ExternalThemeUseFixedPartWindow && ThemeFrameStallCompensation.Value != 0; internal static double ExternalThemeFrameStallThresholdSeconds => Math.Clamp(ThemeFrameStallThresholdSeconds.Value, 0.03, 2.0); internal static double ExternalThemeFrameStallMaxCompensationSeconds => Math.Clamp(ThemeFrameStallMaxCompensationSeconds.Value, 0.0, 5.0); internal static bool ExternalThemeTraceFrameStallCompensation => ExternalThemeUseFixedPartWindow && ThemeTraceFrameStallCompensation.Value != 0 && !ReduceLogSpam; internal static double ExternalThemeFixedPartWindowSeconds => Math.Max(0.25, ThemeFixedPartWindowSeconds.Value); internal static double ExternalThemeFixedWindowPhaseOffsetSeconds => Math.Max(0.0, ThemeFixedWindowPhaseOffsetSeconds.Value); internal static bool ExternalThemeUseSeparateHelperPhaseOffset => ExternalThemeEnabled && ThemeUseSeparateHelperPhaseOffset.Value != 0; internal static double ExternalThemeHelperPhaseOffsetSeconds => Math.Max(0.0, ThemeHelperPhaseOffsetSeconds.Value); internal static bool ExternalThemeKeepHelperLoopRunningMuted => ExternalThemeEnabled && ThemeKeepHelperLoopRunningMuted.Value != 0; internal static bool ExternalThemeHelperStartsAfterIntroWindow => ExternalThemeEnabled && ThemeHelperStartsAfterIntroWindow.Value != 0; internal static bool ExternalThemeAutoMoodFromP1Rhythm => ExternalThemeEnabled && ThemeAutoMoodFromP1Rhythm.Value != 0; internal static bool ExternalThemeAutoMoodImmediateSwitch => ThemeAutoMoodImmediateSwitch.Value != 0; internal static int ExternalThemeHighMoodMapsTo => Math.Clamp(ThemeHighMoodMapsTo.Value, 0, 3); internal static bool ExternalThemeTraceAutoMood => ExternalThemeEnabled && ThemeTraceAutoMood.Value != 0 && !ReduceLogSpam; internal static bool ExternalThemeMoodLossSwitchToHelper => ExternalThemeEnabled && ThemeMoodLossSwitchToHelper.Value != 0; internal static bool ExternalThemeDetectMoodLossFromRhythmUpdate => ExternalThemeEnabled && ThemeDetectMoodLossFromRhythmUpdate.Value != 0; internal static bool ExternalThemeTraceRhythmUpdateMoodLoss => ExternalThemeEnabled && ThemeTraceRhythmUpdateMoodLoss.Value != 0 && !ReduceLogSpam; internal static int ExternalThemeMaxRhythmUpdateMoodLossTraceLogs => Math.Max(0, ThemeMaxRhythmUpdateMoodLossTraceLogs.Value); internal static bool ExternalThemeStopOnFailedPlayGame => ExternalThemeEnabled && ThemeStopOnFailedPlayGame.Value != 0; internal static bool ExternalThemeStopOnStopBattleBgm => ExternalThemeEnabled && ThemeStopOnStopBattleBgm.Value != 0; internal static bool ExternalResponsesEnabled => Enabled && ResponseEnable.Value != 0; internal static bool MuteOriginalRhythmVoiceResponses => ExternalResponsesEnabled && ResponseMuteOriginalRhythmVoice.Value != 0; internal static bool PlayCustomResponses => ExternalResponsesEnabled && ResponsePlayCustomResponses.Value != 0; internal static bool ResponsePlayFirstLoadedAsFallbackEnabled => ExternalResponsesEnabled && ResponsePlayFirstLoadedAsFallback.Value != 0; internal static bool ExternalResponseSuppressUnmappedFallback => ExternalResponsesEnabled && ResponseSuppressUnmappedFallback.Value != 0; internal static string ResponseTestCueNameText => ResponseTestCueName.Value ?? string.Empty; internal static float ExternalResponseVolume => ExternalGameScaledVolume * Math.Max(0f, ResponseVolume.Value) * Math.Max(0f, ResponseMasterVolumeScale.Value); internal static double ExternalResponseDebounceSeconds => Math.Max(0.0, ResponseDebounceSeconds.Value); internal static double ExternalResponseDelaySeconds => Math.Clamp(ResponseDelaySeconds.Value, 0.0, 2.0); internal static bool TraceOriginalRhythmVoiceResponses => ExternalResponsesEnabled && ResponseTraceOriginalRhythmVoice.Value != 0 && !ReduceLogSpam; internal static int MaxResponseTraceLogs => Math.Max(0, ResponseMaxTraceLogs.Value); internal static string ExternalResponseMiscVoicesDirectoryText => ResponseMiscVoicesDirectory.Value ?? string.Empty; internal static string ExternalResponseMiscVoicesDirectory => ThemePathResolver.ResolveMiscVoicesDirectory(); internal static string ExternalResponseMiscVoiceNumberPrefix => string.IsNullOrWhiteSpace(ResponseMiscVoiceNumberPrefix.Value) ? "00" : ResponseMiscVoiceNumberPrefix.Value.Trim(); internal static bool ExternalResponseCommandDetectionEnabled => ExternalResponsesEnabled && ResponseUseCommandDetection.Value != 0; internal static double ExternalResponseCommandMaxAgeSeconds => Math.Clamp(ResponseCommandMaxAgeSeconds.Value, 0.1, 30.0); internal static bool ExternalResponseTraceCommandDetection => ExternalResponsesEnabled && ResponseTraceCommandDetection.Value != 0 && !ReduceLogSpam; internal static int ExternalResponseMaxCommandTraceLogs => Math.Max(0, ResponseMaxCommandTraceLogs.Value); internal static int ExternalResponseCommandSummaryMaxChars => Math.Clamp(ResponseCommandSummaryMaxChars.Value, 1000, 20000); internal static string ExternalResponseP1EvadeCommandMapsTo { get { string text = (ResponseP1EvadeMapsTo.Value ?? "Defend").Trim(); return text.Equals("Retreat", StringComparison.OrdinalIgnoreCase) ? "Retreat" : "Defend"; } } internal static bool ReduceLogSpam => PerformanceReduceLogSpam.Value != 0 || PerformanceForceGameplayQuietMode.Value != 0; public override void Load() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Expected O, but got Unknown L = ((BasePlugin)this).Log; _instance = this; try { BindConfig(); ManualLogSource l = L; bool flag = default(bool); BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(8, 2, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendFormatted("Patapon Music Menu Manual Mood Prototype"); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted("0.4.6"); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" loaded"); } l.LogInfo(val); LogConfig(); ExternalThemeSequencer.Initialize(); H = new Harmony("juras.patapon.musicmenu.manualmood"); H.PatchAll(Assembly.GetExecutingAssembly()); LogPatchedMethods(); } catch (Exception ex) { L.LogError((object)"Plugin load failed:"); L.LogError((object)ex); } } private void BindConfig() { string text = Path.Combine(Paths.PluginPath, "PataponMusicMenu", "Themes"); string value = Path.Combine(text, "Default"); ThemeLibraryRootDirectory = BindVisible("ExternalTheme.ThemeSelection", "ThemeLibraryRootDirectory", text, "Root folder containing numbered theme folders, e.g. Themes/36ThemeName/36Parts and Themes/36ThemeName/36Voices."); ThemeSelectedThemeNumber = BindVisible("ExternalTheme.ThemeSelection", "SelectedThemeNumber", 36, "Theme number to load. Numbers below 10 are zero-padded, so 2 means 02ThemeName/02Parts/02Voices."); ExternalAudioGameVolumeFactor = BindVisible("ExternalAudio", "GameVolumeFactor", 0.1f, "External audio multiplier applied to the current in-game master volume. Example: game volume 0.5 and factor 0.1 => external volume 0.05."); ThemeVolume = BindVisible("ExternalTheme", "Volume", 1f, "Extra multiplier for external theme music after the game volume factor."); ResponseVolume = BindVisible("ExternalResponses", "Volume", 1f, "Extra multiplier for custom response voices after the game volume factor."); ThemeMasterVolumeScale = Fixed(1f); ResponseMasterVolumeScale = Fixed(1f); Enable = Fixed(1); Trace = Fixed(0); P1StartSessionOnStartPlayGame = Fixed(0); P1StartSessionOnHelperRhythmPlayMidi = Fixed(1); P1StartSessionOnBattleAddMidi = Fixed(1); P1MuteHelperCategoryFromDirectorPlayMidi = Fixed(1); P1MuteBgmCategoryFromDirectorPlayMidi = Fixed(1); P1MuteUnknownMidi0CategoriesFromDirectorPlayMidi = Fixed(0); P1OnlyMuteMidiIndex0 = Fixed(1); P1MuteAddMidiResults = Fixed(1); P1MuteAddMidiBgmCategory = Fixed(1); P1MuteAddMidiRhythmCategory = Fixed(1); P1MuteAddMidiOnlyWhenBattleOrMissionFilename = Fixed(1); P1ReapplyMuteOnRhythmUpdate = Fixed(0); P1MaxAddMidiTraceLogs = Fixed(0); P1ZeroCriPlayDirectRhythmMissionKaesi = Fixed(1); P1ZeroCriPlayDirectAllRhythm = Fixed(0); P1TraceCriPlayDirectRhythm = Fixed(0); P1MaxCriPlayDirectTraceLogs = Fixed(0); ThemeEnable = Fixed(1); ThemeDirectory = Fixed(value); ThemeUseNumberedThemeFolders = Fixed(1); ThemePartsFolderSuffix = Fixed("Parts"); ThemeVoicesFolderSuffix = Fixed("Voices"); ThemeTrace = Fixed(0); ThemeAutoStartMood0OnBattleStart = Fixed(1); ThemeInitialDelaySeconds = Fixed(0.1); ThemeScheduleLookAheadSeconds = Fixed(0.2); ThemeLoadAudioAtPluginStart = Fixed(0); ThemePartsPerLoadFrame = Fixed(1); ThemeTraceBeforeEachPartLoad = Fixed(0); ThemeUseDecodeTimeout = Fixed(1); ThemeDecodeTimeoutSeconds = Fixed(2.5); ThemeSkipPartNames = Fixed(""); ThemeUseFixedPartWindow = Fixed(1); ThemeAnchorFixedWindowClock = Fixed(1); ThemeTraceFixedWindowClock = Fixed(0); ThemeResyncOnP1RhythmBoundary = Fixed(1); ThemeTraceRhythmBoundaryResync = Fixed(0); ThemeFrameStallCompensation = Fixed(1); ThemeFrameStallThresholdSeconds = Fixed(0.12); ThemeFrameStallMaxCompensationSeconds = Fixed(0.5); ThemeTraceFrameStallCompensation = Fixed(0); ThemeFixedPartWindowSeconds = Fixed(4.0); ThemeFixedWindowPhaseOffsetSeconds = Fixed(0.05); ThemeUseSeparateHelperPhaseOffset = Fixed(1); ThemeHelperPhaseOffsetSeconds = Fixed(0.4); ThemeKeepHelperLoopRunningMuted = Fixed(1); ThemeHelperStartsAfterIntroWindow = Fixed(1); ThemeAutoMoodFromP1Rhythm = Fixed(1); ThemeAutoMoodImmediateSwitch = Fixed(1); ThemeHighMoodMapsTo = Fixed(3); ThemeTraceAutoMood = Fixed(0); ThemeMoodLossSwitchToHelper = Fixed(1); ThemeDetectMoodLossFromRhythmUpdate = Fixed(1); ThemeTraceRhythmUpdateMoodLoss = Fixed(0); ThemeMaxRhythmUpdateMoodLossTraceLogs = Fixed(0); ThemeStopOnFailedPlayGame = Fixed(1); ThemeStopOnStopBattleBgm = Fixed(1); ResponseEnable = Fixed(1); ResponseMuteOriginalRhythmVoice = Fixed(1); ResponsePlayCustomResponses = Fixed(1); ResponsePlayFirstLoadedAsFallback = Fixed(1); ResponseSuppressUnmappedFallback = Fixed(1); ResponseTestCueName = Fixed(""); ResponseDebounceSeconds = Fixed(0.12); ResponseDelaySeconds = Fixed(0.05); ResponseTraceOriginalRhythmVoice = Fixed(0); ResponseMaxTraceLogs = Fixed(0); ResponseMiscVoicesDirectory = Fixed(""); ResponseMiscVoiceNumberPrefix = Fixed("00"); ResponseUseCommandDetection = Fixed(1); ResponseCommandMaxAgeSeconds = Fixed(6.0); ResponseTraceCommandDetection = Fixed(0); ResponseMaxCommandTraceLogs = Fixed(0); ResponseCommandSummaryMaxChars = Fixed(6000); ResponseP1EvadeMapsTo = Fixed("Retreat"); PerformanceReduceLogSpam = Fixed(1); PerformanceForceGameplayQuietMode = Fixed(1); PruneOldConfigEntries(); } private void PruneOldConfigEntries() { try { object obj = ((object)((BasePlugin)this).Config).GetType().GetProperty("OrphanedEntries")?.GetValue(((BasePlugin)this).Config); if (obj is IDictionary dictionary && dictionary.Count > 0) { dictionary.Clear(); ((BasePlugin)this).Config.Save(); } } catch { } } private RuntimeSetting BindVisible(string section, string key, T defaultValue, string description) { return new RuntimeSetting(((BasePlugin)this).Config.Bind(section, key, defaultValue, description)); } private static RuntimeSetting Fixed(T value) { return new RuntimeSetting(value); } private static void LogConfig() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Expected O, but got Unknown ManualLogSource l = L; bool flag = default(bool); BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(27, 1, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("ThemeLibraryRootDirectory: "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ThemeLibraryRootDirectory.Value); } l.LogInfo(val); ManualLogSource l2 = L; val = new BepInExInfoLogInterpolatedStringHandler(21, 1, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("SelectedThemeNumber: "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ThemeSelectedThemeNumber.Value); } l2.LogInfo(val); ManualLogSource l3 = L; val = new BepInExInfoLogInterpolatedStringHandler(32, 1, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("ExternalAudio.GameVolumeFactor: "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ExternalAudioGameVolumeFactor.Value); } l3.LogInfo(val); ManualLogSource l4 = L; val = new BepInExInfoLogInterpolatedStringHandler(22, 1, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("ExternalTheme.Volume: "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ThemeVolume.Value); } l4.LogInfo(val); ManualLogSource l5 = L; val = new BepInExInfoLogInterpolatedStringHandler(26, 1, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("ExternalResponses.Volume: "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ResponseVolume.Value); } l5.LogInfo(val); } private static void LogPatchedMethods() { //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown if (H == null) { return; } int num = 0; foreach (MethodBase patchedMethod in H.GetPatchedMethods()) { T("Patched method: " + patchedMethod.DeclaringType?.FullName + "." + patchedMethod.Name); num++; } ManualLogSource l = L; bool flag = default(bool); BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(27, 2, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("Harmony patches owned by "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted("juras.patapon.musicmenu.manualmood"); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(": "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(num); } l.LogInfo(val); } internal static void SetSelectedThemeNumber(int number) { number = Math.Clamp(number, 1, 99); ThemeSelectedThemeNumber.Value = number; try { Plugin? instance = _instance; if (instance != null) { ((BasePlugin)instance).Config.Save(); } } catch { } } private static float GetCurrentGameMasterVolume() { double num = 0.0; try { num = Time.realtimeSinceStartup; } catch { } if (num > 0.0 && num - _lastGameMasterVolumeSampleTime < 0.25) { return _cachedGameMasterVolume; } _lastGameMasterVolumeSampleTime = num; try { CriSoundManager val = _cachedCriSoundManager; if ((Object)(object)val == (Object)null) { val = Object.FindObjectOfType(); if ((Object)(object)val != (Object)null) { _cachedCriSoundManager = val; } } if ((Object)(object)val != (Object)null) { float masterVolume = val.MasterVolume; if (!float.IsNaN(masterVolume) && !float.IsInfinity(masterVolume)) { _cachedGameMasterVolume = Math.Clamp(masterVolume, 0f, 1f); } } } catch { } return _cachedGameMasterVolume; } internal static void T(string message) { if (TraceEnabled) { L.LogInfo((object)message); } } internal static void ThemeT(string message) { if (ExternalThemeTraceEnabled) { L.LogInfo((object)message); } } internal static void W(string message) { L.LogWarning((object)message); } } internal static class P1OriginalThemeMuteState { public const uint MuteMask = uint.MaxValue; private static readonly List CapturedPlayers = new List(); private static BgmPlayer? OriginalHelperPlayer; private static bool _originalHelperCaptured; private static bool _originalHelperHasPlayed; private static double _lastOriginalHelperPlayingTime = -999.0; private static bool _lastOriginalHelperPollWasPlaying; private const double OriginalHelperInactiveGraceSeconds = 0.35; public static bool BattleActive { get; private set; } public static bool OriginalHelperAllowsExternalAudio { get { if (!BattleActive) { return false; } if (!_originalHelperCaptured || OriginalHelperPlayer == null) { return true; } if (ObserveOriginalHelperPlayback("helper gate poll")) { return true; } if (!_originalHelperHasPlayed) { return true; } double num = Time.realtimeSinceStartup; return num - _lastOriginalHelperPlayingTime <= 0.35; } } public static void StartBattle(string source) { if (BattleActive) { Plugin.T("[P1 THEME MUTE] start ignored because already active from " + source); ReapplyMute(source + " already-active start"); } else { BattleActive = true; Plugin.T("[P1 THEME MUTE] mute session active from " + source); ExternalThemeSequencer.OnBattleStart(source); } } public static void Reset(string source) { if (CapturedPlayers.Count > 0) { foreach (BgmPlayer capturedPlayer in CapturedPlayers) { try { if (capturedPlayer != null) { capturedPlayer.setMute(0u); } } catch (Exception ex) { Plugin.W("[P1 THEME MUTE] unmute failed during reset from " + source + ": " + ex.Message); } } } CapturedPlayers.Clear(); OriginalHelperPlayer = null; _originalHelperCaptured = false; _originalHelperHasPlayed = false; _lastOriginalHelperPlayingTime = -999.0; _lastOriginalHelperPollWasPlaying = false; BattleActive = false; Plugin.T("[P1 THEME MUTE] reset from " + source); ExternalThemeSequencer.OnBattleEnd(source); } public static void CaptureAndMute(BgmPlayer? player, string slot, string source) { if (!BattleActive) { return; } if (player == null) { Plugin.W($"[P1 THEME MUTE] wanted to mute {slot} from {source}, but player is null"); return; } try { if (!CapturedPlayers.Contains(player)) { CapturedPlayers.Add(player); } player.setMute(uint.MaxValue); Plugin.T($"[P1 THEME MUTE] MUTED {slot} from {source}; " + DescribePlayer(player)); } catch (Exception value) { Plugin.W($"[P1 THEME MUTE] failed to mute {slot} from {source}: {value}"); } } public static void CaptureAndMuteHelper(BgmPlayer? player, string slot, string source) { if (player != null) { OriginalHelperPlayer = player; _originalHelperCaptured = true; ObserveOriginalHelperPlayback(source + " helper capture"); ExternalThemeSequencer.OnOriginalHelperPlaybackAnchor(source + " helper capture"); } CaptureAndMute(player, slot, source); } private static bool ObserveOriginalHelperPlayback(string source) { try { BgmPlayer originalHelperPlayer = OriginalHelperPlayer; if (originalHelperPlayer == null) { return false; } bool flag = originalHelperPlayer.isPlay(); if (flag) { _originalHelperHasPlayed = true; _lastOriginalHelperPlayingTime = Time.realtimeSinceStartup; if (!_lastOriginalHelperPollWasPlaying) { ExternalThemeSequencer.OnOriginalHelperPlaybackAnchor(source + " helper resumed"); } } _lastOriginalHelperPollWasPlaying = flag; return flag; } catch (Exception ex) { Plugin.T("[P1 THEME MUTE] helper playback poll failed from " + source + ": " + ex.Message); return true; } } public static void CaptureAndMuteObject(object? obj, string slot, string source) { if (!BattleActive || obj == null) { return; } BgmPlayer val = (BgmPlayer)((obj is BgmPlayer) ? obj : null); if (val != null) { CaptureAndMute(val, slot, source); return; } try { Type type = obj.GetType(); string text = type.FullName ?? type.Name; if (text.IndexOf("Sound", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Player", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Bgm", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Rhythm", StringComparison.OrdinalIgnoreCase) >= 0) { MethodInfo methodInfo = FindSetMuteMethod(type); if (!(methodInfo == null)) { object obj2 = ((methodInfo.GetParameters()[0].ParameterType == typeof(int)) ? ((object)(-1)) : ((object)uint.MaxValue)); methodInfo.Invoke(obj, new object[1] { obj2 }); Plugin.T($"[P1 THEME MUTE] MUTED generic {slot} from {source}; objectType={text}, method={methodInfo.Name}"); } } } catch (Exception ex) { Plugin.W($"[P1 THEME MUTE] generic mute failed for {slot} from {source}: {ex.Message}"); } } private static MethodInfo? FindSetMuteMethod(Type t) { try { MethodInfo[] methods = t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, "setMute", StringComparison.Ordinal)) { continue; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 1) { Type parameterType = parameters[0].ParameterType; if (parameterType == typeof(uint) || parameterType == typeof(int)) { return methodInfo; } } } } catch { } return null; } public static void ReapplyMute(string source) { if (!BattleActive || CapturedPlayers.Count == 0) { return; } foreach (BgmPlayer capturedPlayer in CapturedPlayers) { try { if (capturedPlayer != null) { capturedPlayer.setMute(uint.MaxValue); } } catch (Exception ex) { Plugin.W("[P1 THEME MUTE] reapply failed from " + source + ": " + ex.Message); } } } public static string DescribePlayer(BgmPlayer? player) { if (player == null) { return "player=null"; } try { return $"playId={player.playId_}, rid={player.getRID()}, vol={player.getVolumeValue()}, targetVol={player.getTargetVolume()}, isPlay={player.isPlay()}"; } catch (Exception ex) { return "player-info-failed=" + ex.Message; } } } [HarmonyPatch(typeof(Director), "startPlayGame")] internal static class P1ThemeMuteStartPlayGamePatch { private static void Prefix(Director __instance) { if (Plugin.StartP1SessionOnStartPlayGame) { P1OriginalThemeMuteState.StartBattle("Director.startPlayGame"); } } } [HarmonyPatch(typeof(Director), "exitPlayGame")] internal static class P1ThemeMuteExitPlayGamePatch { private static void Prefix() { if (Plugin.Enabled) { P1OriginalThemeMuteState.Reset("Director.exitPlayGame"); } } } [HarmonyPatch(typeof(Director), "playMidi")] internal static class P1ThemeMutePlayMidiPatch { private static void Postfix(Director __instance, int midiIndex, MixingCategory cate, float fadeTime) { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Invalid comparison between Unknown and I4 //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Invalid comparison between Unknown and I4 //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_0226: Unknown result type (might be due to invalid IL or missing references) //IL_02a8: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.Enabled) { return; } try { string value = P1OriginalThemeMuteState.DescribePlayer(__instance.curPlayer_); Plugin.T($"[P1 THEME MUTE] playMidi observed: battleActive={P1OriginalThemeMuteState.BattleActive}, midiIndex={midiIndex}, cate={cate}, fadeTime={fadeTime}, {value}"); bool flag = (int)cate == 4; bool flag2 = (int)cate == 3; if (Plugin.OnlyMuteMidiIndex0 && midiIndex != 0) { return; } if (!P1OriginalThemeMuteState.BattleActive && Plugin.StartP1SessionOnHelperRhythmPlayMidi && midiIndex == 0 && flag) { P1OriginalThemeMuteState.StartBattle($"Director.playMidi({midiIndex}, {cate}) helper/Rhythm auto-start"); } if (P1OriginalThemeMuteState.BattleActive) { if (flag && Plugin.MuteP1HelperCategoryFromDirectorPlayMidi) { P1OriginalThemeMuteState.CaptureAndMuteHelper(__instance.curPlayer_, "Director.playMidi helper/Rhythm category", $"Director.playMidi({midiIndex}, {cate})"); } else if (flag2 && Plugin.MuteP1BgmCategoryFromDirectorPlayMidi) { P1OriginalThemeMuteState.CaptureAndMute(__instance.curPlayer_, "Director.playMidi mood/BGM category", $"Director.playMidi({midiIndex}, {cate})"); } else if (midiIndex == 0 && !flag && !flag2 && Plugin.MuteP1UnknownMidi0CategoriesFromDirectorPlayMidi) { P1OriginalThemeMuteState.CaptureAndMute(__instance.curPlayer_, "Director.playMidi unknown midiIndex 0 category", $"Director.playMidi({midiIndex}, {cate})"); } } } catch (Exception value2) { Plugin.W($"[P1 THEME MUTE] Director.playMidi postfix failed: {value2}"); } } } [HarmonyPatch(typeof(Rhythm), "update")] internal static class P1ThemeMuteRhythmUpdatePatch { private static void Postfix(Rhythm __instance) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) if (Plugin.ReapplyP1MuteOnRhythmUpdate) { P1OriginalThemeMuteState.ReapplyMute("Rhythm.update"); } if (!Plugin.ExternalThemeDetectMoodLossFromRhythmUpdate || !P1OriginalThemeMuteState.BattleActive) { return; } try { string lastMoodText; try { object obj; if (__instance == null) { obj = null; } else { Mood lastMood_ = __instance.lastMood_; obj = ((object)(Mood)(ref lastMood_)).ToString(); } if (obj == null) { obj = "rhythm=null"; } lastMoodText = (string)obj; } catch (Exception ex) { lastMoodText = "lastMood-error=" + ex.Message; } uint playCount; try { playCount = ((__instance != null) ? __instance.playCount_ : 0u); } catch { playCount = 0u; } uint playSeIndex; try { playSeIndex = ((__instance != null) ? __instance.playSEIndex_ : uint.MaxValue); } catch { playSeIndex = uint.MaxValue; } bool isPlay; try { isPlay = __instance != null && __instance.isPlay_; } catch { isPlay = false; } ExternalThemeSequencer.OnP1RhythmUpdateObserved(lastMoodText, playCount, playSeIndex, isPlay, "P1.Sound.Rhythm.update"); } catch (Exception ex2) { Plugin.W("[EXT THEME AUTO] Rhythm.update observer failed: " + ex2.Message); } } } [HarmonyPatch] [HarmonyPriority(0)] internal static class P1BgmPlayersAddMidiMutePatch { private sealed class AddMidiInfo { public MixingCategory? Category; public string BgmTypeText = ""; public string FilenameText = ""; public string ArgsText = ""; public string MidiIdText = ""; public string SubGameTypeText = ""; public bool IsBattleType => BgmTypeText.IndexOf("Battle", StringComparison.OrdinalIgnoreCase) >= 0; public bool IsMissionFilename { get { string text = FilenameText.Trim().ToLowerInvariant(); return text.StartsWith("missionid_") || text.Contains("/missionid_"); } } public bool IsSubGameMidiCandidate { get { if (MidiIdText.IndexOf("SubGame", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (ArgsText.IndexOf("MidiId_SubGame", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } return SubGameTypeText.IndexOf("SubGameType_", StringComparison.OrdinalIgnoreCase) >= 0 && SubGameTypeText.IndexOf("SubGameType_None", StringComparison.OrdinalIgnoreCase) < 0; } } public bool HasBattleMetadata => !string.IsNullOrWhiteSpace(BgmTypeText) || !string.IsNullOrWhiteSpace(FilenameText); public bool IsNonBattleBgmWithMetadata => (int)Category.GetValueOrDefault() == 3 && HasBattleMetadata && !IsBattleType && !IsMissionFilename; public static AddMidiInfo From(MethodBase method, object[] args) { //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: 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) AddMidiInfo addMidiInfo = new AddMidiInfo(); ParameterInfo[] parameters = method.GetParameters(); List list = new List(); for (int i = 0; i < args.Length; i++) { object obj = args[i]; string text = ((i < parameters.Length) ? (parameters[i].Name ?? $"arg{i}") : $"arg{i}"); string text2 = ((i < parameters.Length) ? (parameters[i].ParameterType.FullName ?? parameters[i].ParameterType.Name) : (obj?.GetType().FullName ?? "unknown")); string text3 = SafeToString(obj); list.Add(text + "=" + text3); if (obj is MixingCategory value) { addMidiInfo.Category = value; } if (text2.IndexOf("BgmType", StringComparison.OrdinalIgnoreCase) >= 0) { addMidiInfo.BgmTypeText = text3; } if (text2.IndexOf("MidiId", StringComparison.OrdinalIgnoreCase) >= 0) { addMidiInfo.MidiIdText = text3; } if (text2.IndexOf("SubGameType", StringComparison.OrdinalIgnoreCase) >= 0) { addMidiInfo.SubGameTypeText = text3; } if (obj is string filenameText) { addMidiInfo.FilenameText = filenameText; } } addMidiInfo.ArgsText = string.Join(", ", list); return addMidiInfo; } public override string ToString() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(63, 6); defaultInterpolatedStringHandler.AppendLiteral("cate="); object value; if (!Category.HasValue) { value = ""; } else { MixingCategory value2 = Category.Value; value = ((object)(MixingCategory)(ref value2)).ToString(); } defaultInterpolatedStringHandler.AppendFormatted((string?)value); defaultInterpolatedStringHandler.AppendLiteral(", "); defaultInterpolatedStringHandler.AppendLiteral("type='"); defaultInterpolatedStringHandler.AppendFormatted(Escape(BgmTypeText)); defaultInterpolatedStringHandler.AppendLiteral("', midiId='"); defaultInterpolatedStringHandler.AppendFormatted(Escape(MidiIdText)); defaultInterpolatedStringHandler.AppendLiteral("', "); defaultInterpolatedStringHandler.AppendLiteral("subGameType='"); defaultInterpolatedStringHandler.AppendFormatted(Escape(SubGameTypeText)); defaultInterpolatedStringHandler.AppendLiteral("', filename='"); defaultInterpolatedStringHandler.AppendFormatted(Escape(FilenameText)); defaultInterpolatedStringHandler.AppendLiteral("', "); defaultInterpolatedStringHandler.AppendLiteral("args=["); defaultInterpolatedStringHandler.AppendFormatted(ArgsText); defaultInterpolatedStringHandler.AppendLiteral("]"); return defaultInterpolatedStringHandler.ToStringAndClear(); } private static string SafeToString(object? value) { if (value == null) { return "null"; } try { return value.ToString() ?? ""; } catch { return ""; } } private static string Escape(string value) { return (value ?? "").Replace("\r", "\\r").Replace("\n", "\\n"); } } [CompilerGenerated] private sealed class d__1 : IEnumerable, IEnumerable, IEnumerator, IEnumerator, IDisposable { private int <>1__state; private MethodBase <>2__current; private int <>l__initialThreadId; private Type 5__1; private MethodInfo[] <>s__2; private int <>s__3; private MethodInfo 5__4; MethodBase IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__1(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; <>s__2 = null; 5__4 = null; <>1__state = -2; } private bool MoveNext() { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } <>1__state = -1; goto IL_0096; } <>1__state = -1; 5__1 = typeof(BgmPlayers); <>s__2 = 5__1.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); <>s__3 = 0; goto IL_00ac; IL_0096: 5__4 = null; <>s__3++; goto IL_00ac; IL_00ac: if (<>s__3 < <>s__2.Length) { 5__4 = <>s__2[<>s__3]; if (5__4.Name == "addMidi") { <>2__current = 5__4; <>1__state = 1; return true; } goto IL_0096; } <>s__2 = null; return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; return this; } return new d__1(0); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } } private static int _traceCount; [IteratorStateMachine(typeof(d__1))] private static IEnumerable TargetMethods() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__1(-2); } private static void Postfix(MethodBase __originalMethod, object? __result, object[] __args) { //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Invalid comparison between Unknown and I4 if (!Plugin.Enabled) { return; } try { AddMidiInfo addMidiInfo = AddMidiInfo.From(__originalMethod, __args); BgmPlayer val = (BgmPlayer)((__result is BgmPlayer) ? __result : null); TraceAddMidi(__originalMethod, addMidiInfo, val, __result); if (addMidiInfo.IsSubGameMidiCandidate) { if (P1OriginalThemeMuteState.BattleActive) { P1OriginalThemeMuteState.Reset($"BgmPlayers.addMidi subgame MIDI; {addMidiInfo}"); } return; } if (P1OriginalThemeMuteState.BattleActive && addMidiInfo.IsNonBattleBgmWithMetadata) { P1OriginalThemeMuteState.Reset($"BgmPlayers.addMidi non-battle BGM; {addMidiInfo}"); return; } bool flag = ShouldMute(addMidiInfo); if (!P1OriginalThemeMuteState.BattleActive && Plugin.StartP1SessionOnBattleAddMidi && flag && ShouldStartBattleFromAddMidi(addMidiInfo)) { P1OriginalThemeMuteState.StartBattle($"BgmPlayers.addMidi battle marker before Director.playMidi; {addMidiInfo}"); } if (Plugin.MuteP1AddMidiResults && P1OriginalThemeMuteState.BattleActive && flag) { if (val == null) { Plugin.W($"[P1 THEME MUTE] addMidi candidate matched but result is not P1.System.Sound.BgmPlayer; resultType={__result?.GetType().FullName ?? "null"}; {addMidiInfo}"); } else if ((int)addMidiInfo.Category.GetValueOrDefault() == 4) { P1OriginalThemeMuteState.CaptureAndMuteHelper(val, "BgmPlayers.addMidi helper/Rhythm result", $"{DescribeMethod(__originalMethod)}; {addMidiInfo}"); } else { P1OriginalThemeMuteState.CaptureAndMute(val, "BgmPlayers.addMidi result", $"{DescribeMethod(__originalMethod)}; {addMidiInfo}"); } } } catch (Exception value) { Plugin.W($"[P1 THEME MUTE] BgmPlayers.addMidi postfix failed: {value}"); } } private static bool ShouldStartBattleFromAddMidi(AddMidiInfo info) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Invalid comparison between Unknown and I4 if (info.IsSubGameMidiCandidate) { return false; } if (info.IsMissionFilename) { return true; } return (int)info.Category.GetValueOrDefault() == 4 && info.IsBattleType; } private static bool ShouldMute(AddMidiInfo info) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Invalid comparison between Unknown and I4 //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Invalid comparison between Unknown and I4 if (info.IsSubGameMidiCandidate) { return false; } if (((int)info.Category.GetValueOrDefault() != 3 || !Plugin.MuteP1AddMidiBgmCategory) && ((int)info.Category.GetValueOrDefault() != 4 || !Plugin.MuteP1AddMidiRhythmCategory)) { return false; } if (!Plugin.MuteP1AddMidiOnlyWhenBattleOrMissionFilename) { return true; } if ((int)info.Category.GetValueOrDefault() == 3) { return info.IsMissionFilename; } return info.IsBattleType || info.IsMissionFilename; } private static void TraceAddMidi(MethodBase method, AddMidiInfo info, BgmPlayer? player, object? rawResult) { if (Plugin.TraceEnabled && _traceCount < Plugin.MaxP1AddMidiTraceLogs) { _traceCount++; string value = ((player != null) ? P1OriginalThemeMuteState.DescribePlayer(player) : ("resultType=" + (rawResult?.GetType().FullName ?? "null"))); Plugin.T($"[P1 THEME MUTE] addMidi observed: battleActive={P1OriginalThemeMuteState.BattleActive}, method={DescribeMethod(method)}, {info}, {value}"); } } private static string DescribeMethod(MethodBase method) { try { ParameterInfo[] parameters = method.GetParameters(); return method.DeclaringType?.FullName + "." + method.Name + "(" + string.Join(", ", parameters.Select((ParameterInfo p) => p.ParameterType.Name + " " + p.Name)) + ")"; } catch { return method.DeclaringType?.FullName + "." + method.Name; } } } [HarmonyPatch] [HarmonyPriority(800)] internal static class P1CriPlayDirectMutePatch { private static int _traceCount; private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(CriSoundManager), "playDirect", new Type[12] { typeof(SoundGameFileType), typeof(MixingCategory), typeof(string), typeof(string), typeof(uint), typeof(int), typeof(int), typeof(int), typeof(float), typeof(float), typeof(float), typeof(bool) }, (Type[])null); } private static void Prefix(SoundGameFileType fileType, MixingCategory category, string acbFilePath, string key, uint incrementGlobalCounter, int prog, int rgndCueId, int waveRefIndex, float pitch, float pan, ref float volume, bool loop) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Invalid comparison between Unknown and I4 //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.Enabled) { return; } try { if (!P1OriginalThemeMuteState.BattleActive) { return; } string text = acbFilePath ?? string.Empty; string text2 = key ?? string.Empty; float num = volume; if (IsRhythmVoiceCategory(category)) { ExternalResponseSequencer.OnOriginalRhythmVoice(((object)(SoundGameFileType)(ref fileType)).ToString(), ((object)(MixingCategory)(ref category)).ToString(), text, text2, incrementGlobalCounter, prog, rgndCueId, waveRefIndex, pitch, pan, num, loop); if (Plugin.MuteOriginalRhythmVoiceResponses) { volume = 0f; } } else if ((int)category == 4) { string text3 = text + "|" + text2; bool flag = text3.IndexOf("missionid_", StringComparison.OrdinalIgnoreCase) >= 0 && text3.IndexOf("kaesi", StringComparison.OrdinalIgnoreCase) >= 0; bool flag2 = Plugin.ZeroP1CriPlayDirectAllRhythm || (Plugin.ZeroP1CriPlayDirectRhythmMissionKaesi && flag); if (flag2) { volume = 0f; } if (Plugin.TraceP1CriPlayDirectRhythm && _traceCount < Plugin.MaxP1CriPlayDirectTraceLogs) { _traceCount++; Plugin.T($"[P1 CRI PLAYDIRECT MUTE] category={category}, fileType={fileType}, key={text2}, path={text}, prog={prog}, rgndCueId={rgndCueId}, waveRefIndex={waveRefIndex}, pitch={pitch}, pan={pan}, loop={loop}, looksLikeMissionKaesi={flag}, zeroed={flag2}, volume {num} -> {volume}"); } } } catch (Exception ex) { Plugin.W("[P1 CRI PLAYDIRECT MUTE] prefix failed: " + ex.Message); } } private static bool IsRhythmVoiceCategory(MixingCategory category) { try { return ((object)(MixingCategory)(ref category)).ToString().IndexOf("RhythmVoice", StringComparison.OrdinalIgnoreCase) >= 0; } catch { return false; } } } [HarmonyPatch(typeof(Rhythm), "playRhythm")] internal static class P1RhythmAutoMoodPatch { private static void Postfix(Rhythm __instance, PlayRequest rhythmPlayRequest, bool __result) { if (!__result || !Plugin.ExternalThemeAutoMoodFromP1Rhythm || !P1OriginalThemeMuteState.BattleActive) { return; } try { string text = SafeRequestMoodText(rhythmPlayRequest); string text2 = SafeLastMoodText(__instance); uint num = SafePlaySeIndex(__instance); uint value = SafePlayCount(__instance); bool value2 = SafeIsPlay(__instance); ExternalResponseSequencer.OnP1RhythmCommandObserved(rhythmPlayRequest, $"P1 Rhythm.playRhythm requestMood={text}, lastMood={text2}, playCount={value}, playSEIndex={num}"); if (!TryMapMood(text, text2, num, out int mood, out string reason)) { if (Plugin.ExternalThemeTraceAutoMood) { Plugin.ThemeT($"[EXT THEME AUTO] unable to map P1 rhythm mood; requestMood={text}, lastMood={text2}, playCount={value}, playSEIndex={num}, isPlay={value2}"); } return; } if (Plugin.ExternalThemeTraceAutoMood) { Plugin.ThemeT($"[EXT THEME AUTO] detected P1 rhythm mood={mood}; reason={reason}; requestMood={text}, lastMood={text2}, playCount={value}, playSEIndex={num}, isPlay={value2}"); } ExternalThemeSequencer.OnP1RhythmMoodDetected(mood, $"P1 Rhythm.playRhythm requestMood={text}, lastMood={text2}, playCount={value}, playSEIndex={num}"); } catch (Exception ex) { Plugin.W("[EXT THEME AUTO] P1 Rhythm.playRhythm mood detection failed: " + ex.Message); } } private static bool TryMapMood(string requestMoodText, string lastMoodText, uint playSeIndex, out int mood, out string reason) { string text = ((requestMoodText ?? string.Empty) + " " + (lastMoodText ?? string.Empty)).ToLowerInvariant(); if (text.Contains("high") || text.Contains("fever")) { mood = 3; reason = "P1 rhythm enum Mood_High/Fever -> external mood 3"; return true; } if (text.Contains("mid")) { mood = 2; reason = "P1 rhythm enum Mood_Mid -> external mood 2"; return true; } if (text.Contains("low")) { mood = 1; reason = "P1 rhythm enum Mood_Low -> external mood 1"; return true; } if (text.Contains("none") || text.Contains("begin") || text.Contains("zero") || text.Contains("default")) { mood = 0; reason = "P1 rhythm enum looks like reset/begin -> external mood 0"; return true; } if (playSeIndex >= 720907 && playSeIndex < 720999) { mood = 3; reason = "fallback high/fever playSEIndex >= 720907 -> external mood 3"; return true; } if (playSeIndex >= 720902 && playSeIndex < 720907) { mood = 2; reason = "fallback mid playSEIndex 720902..720906 -> external mood 2"; return true; } if (playSeIndex >= 720897 && playSeIndex < 720902) { mood = 1; reason = "fallback low playSEIndex 720897..720901 -> external mood 1"; return true; } mood = 0; reason = "unmapped"; return false; } private static string SafeRequestMoodText(PlayRequest? request) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) try { object obj; if (request == null) { obj = null; } else { Mood rhythmMood = request.rhythmMood; obj = ((object)(Mood)(ref rhythmMood)).ToString(); } if (obj == null) { obj = "request=null"; } return (string)obj; } catch (Exception ex) { return "requestMood-error=" + ex.Message; } } private static string SafeLastMoodText(Rhythm? rhythm) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) try { object obj; if (rhythm == null) { obj = null; } else { Mood lastMood_ = rhythm.lastMood_; obj = ((object)(Mood)(ref lastMood_)).ToString(); } if (obj == null) { obj = "rhythm=null"; } return (string)obj; } catch (Exception ex) { return "lastMood-error=" + ex.Message; } } private static uint SafePlaySeIndex(Rhythm? rhythm) { try { return (rhythm != null) ? rhythm.playSEIndex_ : uint.MaxValue; } catch { return uint.MaxValue; } } private static uint SafePlayCount(Rhythm? rhythm) { try { return (rhythm != null) ? rhythm.playCount_ : 0u; } catch { return 0u; } } private static bool SafeIsPlay(Rhythm? rhythm) { try { return rhythm != null && rhythm.isPlay_; } catch { return false; } } } [HarmonyPatch] internal static class P1CommandEventResponsePatch { [CompilerGenerated] private sealed class d__0 : IEnumerable, IEnumerable, IEnumerator, IEnumerator, IDisposable { private int <>1__state; private MethodBase <>2__current; private int <>l__initialThreadId; private MethodInfo 5__1; private MethodInfo 5__2; MethodBase IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__0(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; 5__2 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; 5__1 = AccessTools.DeclaredMethod(typeof(Director), "onFixCommand", (Type[])null, (Type[])null); if (5__1 != null) { <>2__current = 5__1; <>1__state = 1; return true; } goto IL_0076; case 1: <>1__state = -1; goto IL_0076; case 2: { <>1__state = -1; break; } IL_0076: 5__2 = AccessTools.DeclaredMethod(typeof(Director), "onExecCommand", (Type[])null, (Type[])null); if (5__2 != null) { <>2__current = 5__2; <>1__state = 2; return true; } break; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; return this; } return new d__0(0); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } } [IteratorStateMachine(typeof(d__0))] private static IEnumerable TargetMethods() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__0(-2); } private static void Prefix(object __0, MethodBase __originalMethod) { if (!Plugin.ExternalResponsesEnabled || !Plugin.ExternalResponseCommandDetectionEnabled || !P1OriginalThemeMuteState.BattleActive) { return; } try { ExternalResponseSequencer.OnP1CommandEventObserved(__0, "P1.Director." + __originalMethod.Name); } catch (Exception ex) { Plugin.W("[EXT RESP CMD] Director command hook failed: " + ex.Message); } } } [HarmonyPatch(typeof(Director), "failedPlayGame")] internal static class P1ExternalThemeStopOnFailedPlayGamePatch { private static void Prefix() { if (!Plugin.ExternalThemeStopOnFailedPlayGame) { return; } try { P1OriginalThemeMuteState.Reset("Director.failedPlayGame"); } catch (Exception ex) { Plugin.W("[EXT THEME] failedPlayGame stop hook failed: " + ex.Message); } } } [HarmonyPatch(typeof(Director), "stopBattleBgm")] internal static class P1ExternalThemeStopOnStopBattleBgmPatch { private static void Prefix() { if (!Plugin.ExternalThemeStopOnStopBattleBgm) { return; } try { P1OriginalThemeMuteState.Reset("Director.stopBattleBgm"); } catch (Exception ex) { Plugin.W("[EXT THEME] stopBattleBgm stop hook failed: " + ex.Message); } } } [HarmonyPatch(typeof(Director), "moodDown")] internal static class P1ExternalThemeMoodDownToHelperPatch { private static void Postfix() { try { ExternalThemeSequencer.OnP1MoodLossToHelperDetected("Director.moodDown"); } catch (Exception ex) { Plugin.W("[EXT THEME AUTO] moodDown helper hook failed: " + ex.Message); } } } [HarmonyPatch(typeof(Rhythm), "clearPlayCount")] internal static class P1ExternalThemeClearPlayCountToHelperPatch { private static void Postfix() { try { ExternalThemeSequencer.OnP1MoodLossToHelperDetected("Rhythm.clearPlayCount"); } catch (Exception ex) { Plugin.W("[EXT THEME AUTO] clearPlayCount helper hook failed: " + ex.Message); } } } [HarmonyPatch(typeof(Rhythm), "resetPlayCount")] internal static class P1ExternalThemeResetPlayCountToHelperPatch { private static void Postfix() { try { ExternalThemeSequencer.OnP1MoodLossToHelperDetected("Rhythm.resetPlayCount"); } catch (Exception ex) { Plugin.W("[EXT THEME AUTO] resetPlayCount helper hook failed: " + ex.Message); } } } internal static class ExternalThemeSequencer { private static readonly string[] Mood0Loop = new string[1] { "Begin-02" }; private static readonly string[] Mood1Loop = new string[4] { "Combo-01", "Combo-02", "Combo-03", "Combo-04" }; private static readonly string[] Mood2Loop = new string[4] { "Combo-06", "Combo-07", "Combo-08", "Combo-09" }; private static readonly string[] Mood3Loop = (from i in Enumerable.Range(1, 16) select $"Fever-{i:00}").ToArray(); private static readonly Dictionary Parts = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly HashSet MissingPartWarnings = new HashSet(StringComparer.OrdinalIgnoreCase); private static GameObject? _root; private static readonly ManagedNaudioPlayer MainPlayer = new ManagedNaudioPlayer(); private static readonly ManagedNaudioPlayer HelperPlayer = new ManagedNaudioPlayer(); private static bool _initialized; private static bool _loadAttempted; private static bool _loadComplete; private static bool _loadedAny; private static bool _loadRequested; private static int _loadDelayFrames; private static string _loadRequestReason = string.Empty; private static bool _loggedOggExceptionDetails; private static bool _incrementalLoadActive; private static string[] _loadPartQueue = Array.Empty(); private static int _loadPartIndex; private static string _loadDirectory = string.Empty; private static Dictionary _loadWavByNormalizedStem = new Dictionary(StringComparer.OrdinalIgnoreCase); private static Dictionary _loadOggByNormalizedStem = new Dictionary(StringComparer.OrdinalIgnoreCase); private static bool _battleActive; private static bool _originalHelperGateOpen = true; private static bool _hasLastUpdateTime; private static double _lastUpdateTime; private static double _lastP1RhythmBoundaryResyncTime; private static double _lastOriginalHelperAnchorResyncTime; private static double _lastP1RhythmBoundaryTime; private static double _lastMainWindowStartTime; private static double _helperParallelAnchorTime; private static double _pendingInitialOriginalHelperAnchorTime; private static bool _hasLastRhythmUpdateState; private static uint _lastRhythmUpdatePlayCount; private static bool _lastRhythmUpdateIsPlay; private static string _lastRhythmUpdateMoodText = string.Empty; private static uint _lastRhythmUpdatePlaySeIndex = uint.MaxValue; private static bool _rhythmUpdateLossAlreadyHandled; private static int _rhythmUpdateLossTraceCount; private static bool _mainPlaying; private static bool _mainWaitingForStartDelay; private static double _mainStartDelayEndTime; private static bool _mainStopAfterMood0Intro; private static double _mainNextFixedBoundaryTime; private static bool _helperRunning; private static bool _helperWaitingForStartDelay; private static double _helperStartDelayEndTime; private static bool _helperAudible; private static string _helperCurrentPart = string.Empty; private static string _helperScheduleSource = string.Empty; private static double _helperNextFixedBoundaryTime; private static bool _startWhenLoaded; private static int _startWhenLoadedMood; private static bool _startWhenLoadedIncludeMissionIntro; private static string _startWhenLoadedSource = string.Empty; private static int _currentMood; private static int _pendingMood; private static bool _pendingIncludeMissionIntro; private static bool _hasPendingMood; private static bool _beginIntroPlayedThisBattle; private static string[] _loopParts = Array.Empty(); private static int _loopIndex; private static readonly Queue OneShotQueue = new Queue(); private static string _currentPart = string.Empty; public static int CurrentMood => _currentMood; public static string CurrentMainPart => _currentPart; public static bool BattleActive => _battleActive; public static void Initialize() { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown if (_initialized) { return; } _initialized = true; try { ClassInjector.RegisterTypeInIl2Cpp(); } catch (Exception ex) { Plugin.W("[EXT THEME] ClassInjector registration warning: " + ex.Message); } try { _root = new GameObject("PataponMusicMenu_ManualMoodSequencer"); Object.DontDestroyOnLoad((Object)(object)_root); ((Object)_root).hideFlags = (HideFlags)61; _root.AddComponent(); if (Plugin.ExternalThemeLoadAudioAtPluginStart) { RequestLoad("plugin start", 8); } ExternalResponseSequencer.Initialize(); Plugin.ThemeT("[EXT THEME] sequencer initialized using managed NAudio playback backend"); } catch (Exception value) { Plugin.W($"[EXT THEME] Initialize failed: {value}"); } } public static bool SelectThemeFromMenu(int themeNumber, string source) { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Expected O, but got Unknown if (!Plugin.ExternalThemeEnabled) { return false; } if (_battleActive) { Plugin.W("[THEME MENU] theme selection is disabled during an active battle; finish/leave the mission first."); return false; } themeNumber = Math.Clamp(themeNumber, 1, 99); Plugin.SetSelectedThemeNumber(themeNumber); ThemePathResolver.ClearCache(); UnloadLoadedAudio("theme menu selection (" + source + ")"); RequestLoad($"theme menu selected {themeNumber:00}", 0); ManualLogSource l = Plugin.L; bool flag = default(bool); BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(24, 2, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[THEME MENU] selected "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(themeNumber, "00"); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(": "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ThemePathResolver.SelectedThemeTitle); } l.LogInfo(val); return true; } public static void OnBattleStart(string source) { if (Plugin.ExternalThemeEnabled) { _battleActive = true; _originalHelperGateOpen = true; _hasLastUpdateTime = false; _lastP1RhythmBoundaryResyncTime = 0.0; _lastOriginalHelperAnchorResyncTime = 0.0; _lastP1RhythmBoundaryTime = 0.0; _lastMainWindowStartTime = 0.0; _helperParallelAnchorTime = 0.0; _pendingInitialOriginalHelperAnchorTime = 0.0; ResetRhythmUpdateLossState(); ExternalResponseSequencer.OnBattleStart(source); RequestLoad("battle start", 1); if (Plugin.ExternalThemeAutoStartMood0) { Plugin.ThemeT("[EXT THEME] battle start from " + source + "; auto-start mood 0"); SetMood(0, immediate: true, "battle start from " + source, !_beginIntroPlayedThisBattle); } } } public static void OnBattleEnd(string source) { if (_battleActive || _mainPlaying || _helperRunning || _helperWaitingForStartDelay) { Plugin.ThemeT("[EXT THEME] battle end from " + source + "; stopping external theme"); _battleActive = false; _originalHelperGateOpen = true; _hasLastUpdateTime = false; _lastP1RhythmBoundaryResyncTime = 0.0; _lastOriginalHelperAnchorResyncTime = 0.0; _lastP1RhythmBoundaryTime = 0.0; _lastMainWindowStartTime = 0.0; _helperParallelAnchorTime = 0.0; _pendingInitialOriginalHelperAnchorTime = 0.0; ResetRhythmUpdateLossState(); ExternalResponseSequencer.OnBattleEnd(source); _beginIntroPlayedThisBattle = false; Stop("battle end"); UnloadLoadedAudio("battle end from " + source); } } public static void Update() { if (!_initialized || !Plugin.ExternalThemeEnabled) { return; } try { ProcessDeferredLoad(); ProcessIncrementalLoad(); double now = NowSeconds(); ApplyFrameStallCompensation(now); UpdateOriginalHelperGate(); RefreshRuntimeVolumes(); UpdateHelper(now); UpdateMain(now); ExternalResponseSequencer.Update(now); } catch (Exception ex) { Plugin.W("[EXT THEME] Update failed: " + ex.Message); } } private static void RefreshRuntimeVolumes() { try { MainPlayer.SetVolume(GateThemeVolume(Plugin.ExternalThemeVolume)); HelperPlayer.SetVolume(_helperAudible ? GateThemeVolume(Plugin.ExternalThemeVolume) : 0f); ExternalResponseSequencer.RefreshRuntimeVolume(); } catch { } } private static void UpdateOriginalHelperGate() { bool flag = !_battleActive || P1OriginalThemeMuteState.OriginalHelperAllowsExternalAudio; if (_originalHelperGateOpen == flag) { return; } _originalHelperGateOpen = flag; if (!flag) { MainPlayer.SetVolume(0f); HelperPlayer.SetVolume(0f); if (_battleActive && (_currentMood > 0 || _mainPlaying || _mainWaitingForStartDelay || _hasPendingMood)) { SwitchToHelperNow("original helper gate closed"); } } else { AlignHelperWindowToBestKnownAnchor("original helper gate reopened", _currentMood == 0); MainPlayer.SetVolume(Plugin.ExternalThemeVolume); HelperPlayer.SetVolume(_helperAudible ? Plugin.ExternalThemeVolume : 0f); } ExternalResponseSequencer.SetExternalAudioGate(flag); Plugin.ThemeT("[EXT THEME HELPER GATE] original helper " + (flag ? "playing/resumed" : "not playing") + "; external theme audio " + (flag ? "unmuted" : "muted")); } private static float GateThemeVolume(float volume) { return _originalHelperGateOpen ? volume : 0f; } public static void OnOriginalHelperPlaybackAnchor(string source) { if (!Plugin.ExternalThemeEnabled || !_battleActive) { return; } double num = NowSeconds(); if (_currentMood == 0 && _lastMainWindowStartTime <= 0.0001 && !_hasPendingMood && !_beginIntroPlayedThisBattle) { if (!_loadedAny) { _pendingInitialOriginalHelperAnchorTime = num; return; } _pendingInitialOriginalHelperAnchorTime = 0.0; StartMissionIntroAtOriginalHelperAnchor(num, source + " initial original helper anchor"); } else if (_currentMood == 0 && !_mainPlaying && !_mainWaitingForStartDelay && !_hasPendingMood) { AlignHelperWindowToBestKnownAnchor(source, makeAudible: true); } } private static void ResyncHelperWindowToOriginalHelperAnchor(string source, bool useHelperPhaseDelay) { double desiredFirstStartTime = NowSeconds() + (useHelperPhaseDelay ? GetPhaseOffsetForMood(0) : 0.0); AlignHelperWindowToAbsoluteStart(desiredFirstStartTime, source, makeAudible: true, force: true); } private static bool StartMissionIntroAtOriginalHelperAnchor(double anchorTime, string source) { if (!Plugin.ExternalThemeEnabled || !_battleActive) { return false; } if (!Parts.TryGetValue("Begin-01", out AudioPart value) || value == null) { if (!Parts.TryGetValue("Begin-02", out value) || value == null) { WarnMissingPart("Begin-01"); _pendingInitialOriginalHelperAnchorTime = anchorTime; return false; } WarnMissingPart("Begin-01"); Plugin.ThemeT("[EXT THEME] Begin-01 missing at original-helper anchor; using Begin-02 once as mission intro fallback"); } double num = NowSeconds(); double num2 = GetEffectivePartWindowSeconds(value); if (num2 <= 0.0001) { num2 = Plugin.ExternalThemeFixedPartWindowSeconds; } double num3 = ((anchorTime > 0.0001) ? anchorTime : num); if (num - num3 > Math.Min(0.5, num2 * 0.25)) { num3 = num; } double num4; for (num4 = num3 + num2; num4 <= num + 0.02; num4 = num3 + num2) { num3 += num2; } StopMainPlayerOnly(); StopHelperPlayerOnly(); _helperRunning = false; _helperWaitingForStartDelay = false; _helperCurrentPart = string.Empty; _currentMood = 0; _pendingMood = 0; _hasPendingMood = false; _pendingIncludeMissionIntro = false; _loopParts = Array.Empty(); _loopIndex = 0; OneShotQueue.Clear(); _mainStopAfterMood0Intro = true; _beginIntroPlayedThisBattle = true; _mainPlaying = true; _mainWaitingForStartDelay = false; _mainNextFixedBoundaryTime = num3; _lastMainWindowStartTime = 0.0; _lastP1RhythmBoundaryResyncTime = 0.0; double num5 = Math.Max(0.0, num - num3); if (num5 >= num2) { num5 = 0.0; } MainPlayer.PlayUntil(value, GateThemeVolume(Plugin.ExternalThemeVolume), num, num4, num5); _currentPart = (value.Name.Equals("Begin-02", StringComparison.OrdinalIgnoreCase) ? "Begin-02" : "Begin-01"); ScheduleHelperWindowAt(num4, source + " -> Begin-02 after Begin-01", audible: true); Plugin.ThemeT($"[EXT THEME] mission intro anchored to original helper: playing {_currentPart}; phase={num5:F4}s, endIn={num4 - num:F4}s, source={source}"); return true; } private static void ScheduleHelperWindowAt(double startTime, string source, bool audible) { if (!Plugin.ExternalThemeKeepHelperLoopRunningMuted || !Plugin.ExternalThemeEnabled || !_battleActive) { return; } if (!Parts.ContainsKey("Begin-02")) { WarnMissingPart("Begin-02"); return; } double num = NowSeconds(); _helperAudible = audible; _helperScheduleSource = source; _helperParallelAnchorTime = startTime; _helperNextFixedBoundaryTime = startTime; _lastOriginalHelperAnchorResyncTime = 0.0; HelperPlayer.Stop(); _helperRunning = false; _helperCurrentPart = string.Empty; if (num + 0.0001 < startTime) { _helperWaitingForStartDelay = true; _helperStartDelayEndTime = startTime; HelperPlayer.SetVolume(0f); Plugin.ThemeT($"[EXT THEME HELPER] scheduled Begin-02 after mission intro from {source}; startIn={startTime - num:F4}s, audible={audible}"); } else { _helperWaitingForStartDelay = false; AlignHelperWindowToParallelClock(source, audible, force: true); } } private static bool AlignHelperWindowToBestKnownAnchor(string source, bool makeAudible) { if (!Plugin.ExternalThemeKeepHelperLoopRunningMuted || !Plugin.ExternalThemeEnabled || !_battleActive) { return false; } EnsureHelperParallelAnchorFromBestKnownWindow(source); return AlignHelperWindowToParallelClock(source, makeAudible, force: true); } private static void SyncMutedHelperToMainWindowStart(double mainWindowStartTime, string source) { if (_battleActive && !_helperAudible) { SetHelperParallelAnchorFromMainWindowStart(mainWindowStartTime, source); AlignHelperWindowToParallelClock(source, makeAudible: false, force: true); } } private static bool AlignHelperWindowToAbsoluteStart(double desiredFirstStartTime, string source, bool makeAudible, bool force) { if (desiredFirstStartTime > 0.0001) { _helperParallelAnchorTime = desiredFirstStartTime; } return AlignHelperWindowToParallelClock(source, makeAudible, force); } private static void EnsureHelperParallelAnchorFromBestKnownWindow(string source) { if (!(_helperParallelAnchorTime > 0.0001)) { if (_lastMainWindowStartTime > 0.0001) { SetHelperParallelAnchorFromMainWindowStart(_lastMainWindowStartTime, source); } else if (_lastP1RhythmBoundaryTime > 0.0001) { _helperParallelAnchorTime = _lastP1RhythmBoundaryTime + GetPhaseOffsetForMood(Math.Max(1, _currentMood)); } else { _helperParallelAnchorTime = NowSeconds() + GetPhaseOffsetForMood(0); } } } private static void SetHelperParallelAnchorFromMainWindowStart(double mainWindowStartTime, string source) { if (!(mainWindowStartTime <= 0.0001)) { _helperParallelAnchorTime = mainWindowStartTime; } } private static bool AlignHelperWindowToParallelClock(string source, bool makeAudible, bool force) { if (!Plugin.ExternalThemeKeepHelperLoopRunningMuted || !Plugin.ExternalThemeEnabled || !_battleActive) { return false; } if (!Parts.TryGetValue("Begin-02", out AudioPart value) || value == null) { return false; } double num = NowSeconds(); if (!force && _lastOriginalHelperAnchorResyncTime > 0.0001 && num - _lastOriginalHelperAnchorResyncTime < 0.35) { return false; } EnsureHelperParallelAnchorFromBestKnownWindow(source); double num2 = GetEffectivePartWindowSeconds(value); if (num2 <= 0.0001) { num2 = Plugin.ExternalThemeFixedPartWindowSeconds; } double num3 = _helperParallelAnchorTime; if (num3 <= 0.0001) { num3 = (_helperParallelAnchorTime = num); } if (num >= num3) { double num4 = Math.Floor((num - num3) / num2); if (num4 > 0.0) { num3 += num4 * num2; } } double num5; for (num5 = num3 + num2; num5 <= num + 0.02; num5 = num3 + num2) { num3 += num2; } _lastOriginalHelperAnchorResyncTime = num; _helperAudible = makeAudible; _helperScheduleSource = source; _helperNextFixedBoundaryTime = num5; if (num + 0.0001 < num3) { HelperPlayer.Stop(); _helperRunning = false; _helperWaitingForStartDelay = true; _helperStartDelayEndTime = num3; _helperCurrentPart = string.Empty; Plugin.ThemeT($"[EXT THEME HELPER] parallel Begin-02 from {source}; scheduledStartIn={num3 - num:F4}s, audible={_helperAudible}"); return true; } if (!force && _helperRunning && !_helperWaitingForStartDelay && Math.Abs(_helperNextFixedBoundaryTime - num5) < 0.03) { HelperPlayer.SetVolume(_helperAudible ? GateThemeVolume(Plugin.ExternalThemeVolume) : 0f); return true; } double num6 = Math.Max(0.0, num - num3); if (num6 >= num2) { num6 = 0.0; } float volume = (_helperAudible ? GateThemeVolume(Plugin.ExternalThemeVolume) : 0f); HelperPlayer.PlayUntil(value, volume, num, num5, num6); _helperRunning = true; _helperWaitingForStartDelay = false; _helperCurrentPart = "Begin-02"; Plugin.ThemeT($"[EXT THEME HELPER] parallel Begin-02 from {source}; phase={num6:F4}s, endIn={num5 - num:F4}s, audible={_helperAudible}"); return true; } private static double GetHelperRelativeToMainOffsetSeconds() { return 0.0; } private static void ApplyFrameStallCompensation(double now) { if (!_hasLastUpdateTime) { _hasLastUpdateTime = true; _lastUpdateTime = now; return; } double num = now - _lastUpdateTime; _lastUpdateTime = now; if (!Plugin.ExternalThemeFrameStallCompensation || !_battleActive) { return; } double externalThemeFrameStallThresholdSeconds = Plugin.ExternalThemeFrameStallThresholdSeconds; if (num <= externalThemeFrameStallThresholdSeconds) { return; } double num2 = Math.Min(num, Plugin.ExternalThemeFrameStallMaxCompensationSeconds); if (num2 <= 0.0001) { return; } bool flag = false; if (_mainWaitingForStartDelay) { _mainStartDelayEndTime += num2; if (_mainNextFixedBoundaryTime > 0.0001) { _mainNextFixedBoundaryTime += num2; } flag = true; } else if (_mainPlaying) { flag |= MainPlayer.DelayExpectedEnd(num2); if (_mainNextFixedBoundaryTime > 0.0001) { _mainNextFixedBoundaryTime += num2; } } if (flag && Plugin.ExternalThemeTraceFrameStallCompensation) { Plugin.ThemeT($"[EXT THEME CLOCK] frame stall compensation: updateDelta={num:F4}s, threshold={externalThemeFrameStallThresholdSeconds:F4}s, appliedDelay={num2:F4}s, currentMood={_currentMood}, mainPart={_currentPart}, helperAudible={_helperAudible}"); } } private static void UpdateMain(double now) { if (!_mainPlaying) { return; } MainPlayer.SetVolume(GateThemeVolume(Plugin.ExternalThemeVolume)); if (_mainWaitingForStartDelay) { if (now >= _mainStartDelayEndTime) { _mainWaitingForStartDelay = false; ArmMainFixedBoundary(_mainStartDelayEndTime); PlayNextMainPart("initial delay elapsed"); } } else if (MainPlayer.HasReachedEnd(now)) { PlayNextMainPart("previous part ended (" + _currentPart + ")"); } } private static void UpdateHelper(double now) { if (!_battleActive || !Plugin.ExternalThemeKeepHelperLoopRunningMuted) { return; } if (_helperWaitingForStartDelay) { if (now >= _helperStartDelayEndTime) { _helperWaitingForStartDelay = false; ArmHelperFixedBoundary(_helperStartDelayEndTime); PlayHelperWindow("helper start delay elapsed (" + _helperScheduleSource + ")"); } } else if (_helperRunning) { HelperPlayer.SetVolume(_helperAudible ? GateThemeVolume(Plugin.ExternalThemeVolume) : 0f); if (HelperPlayer.HasReachedEnd(now)) { PlayHelperWindow("previous helper window ended (" + _helperCurrentPart + ")"); } } } public static void OnP1RhythmMoodDetected(int detectedMood, string source) { if (!Plugin.ExternalThemeAutoMoodFromP1Rhythm || !Plugin.ExternalThemeEnabled || !_battleActive) { return; } detectedMood = Math.Clamp(detectedMood, 0, 3); _lastP1RhythmBoundaryTime = NowSeconds(); RequestLoad($"auto mood {detectedMood} from {source}", 1); if (!_loadedAny) { if (!_loadComplete) { _startWhenLoaded = true; _startWhenLoadedMood = detectedMood; _startWhenLoadedIncludeMissionIntro = false; _startWhenLoadedSource = source; Plugin.ThemeT($"[EXT THEME AUTO] queued detected mood {detectedMood} from {source}; waiting for OGG/WAV loading to finish"); } } else if (!_mainPlaying && detectedMood > 0) { SwitchMoodAtGameBoundary(detectedMood, source, includeMissionIntro: false); } else if (!_hasPendingMood && detectedMood == _currentMood) { ResyncCurrentMainWindowToP1RhythmBoundary(detectedMood, source); } else if (Plugin.ExternalThemeAutoMoodImmediateSwitch) { SwitchMoodAtGameBoundary(detectedMood, source, includeMissionIntro: false); } else { SetMood(detectedMood, immediate: false, source); } } public static void OnP1RhythmUpdateObserved(string lastMoodText, uint playCount, uint playSeIndex, bool isPlay, string source) { if (!Plugin.ExternalThemeDetectMoodLossFromRhythmUpdate || !Plugin.ExternalThemeEnabled || !_battleActive) { return; } try { if (!_hasLastRhythmUpdateState) { _hasLastRhythmUpdateState = true; _lastRhythmUpdateMoodText = lastMoodText ?? string.Empty; _lastRhythmUpdatePlayCount = playCount; _lastRhythmUpdatePlaySeIndex = playSeIndex; _lastRhythmUpdateIsPlay = isPlay; return; } bool flag = _currentMood > 0 || _mainPlaying; bool flag2 = _lastRhythmUpdatePlayCount != 0 || _lastRhythmUpdateIsPlay; bool flag3 = playCount == 0 && !isPlay; if (Plugin.ExternalThemeTraceRhythmUpdateMoodLoss && _rhythmUpdateLossTraceCount < Plugin.ExternalThemeMaxRhythmUpdateMoodLossTraceLogs) { _rhythmUpdateLossTraceCount++; Plugin.ThemeT($"[EXT THEME AUTO] Rhythm.update poll: mainMoodActive={flag}, currentMood={_currentMood}, lastMood={_lastRhythmUpdateMoodText}->{lastMoodText}, playCount={_lastRhythmUpdatePlayCount}->{playCount}, playSEIndex={_lastRhythmUpdatePlaySeIndex}->{playSeIndex}, isPlay={_lastRhythmUpdateIsPlay}->{isPlay}, lostCandidate={flag3 && flag2}"); } if (!_rhythmUpdateLossAlreadyHandled && flag && flag2 && flag3) { _rhythmUpdateLossAlreadyHandled = true; Plugin.ThemeT($"[EXT THEME AUTO] Rhythm.update mood loss detected from {source}; lastMood={_lastRhythmUpdateMoodText}->{lastMoodText}, playCount={_lastRhythmUpdatePlayCount}->0, playSEIndex={_lastRhythmUpdatePlaySeIndex}->{playSeIndex}, isPlay={_lastRhythmUpdateIsPlay}->{isPlay}; switching to helper immediately"); SwitchToHelperNow("Rhythm.update loss detector (" + source + ")"); } else if (playCount != 0 || isPlay) { _rhythmUpdateLossAlreadyHandled = false; } _lastRhythmUpdateMoodText = lastMoodText ?? string.Empty; _lastRhythmUpdatePlayCount = playCount; _lastRhythmUpdatePlaySeIndex = playSeIndex; _lastRhythmUpdateIsPlay = isPlay; } catch (Exception ex) { Plugin.W("[EXT THEME AUTO] Rhythm.update mood-loss detector failed: " + ex.Message); } } public static void OnP1MoodLossToHelperDetected(string source) { if (!Plugin.ExternalThemeMoodLossSwitchToHelper || !Plugin.ExternalThemeEnabled || !_battleActive) { return; } RequestLoad("mood loss helper switch from " + source, 1); if (!_loadedAny) { if (!_loadComplete) { _startWhenLoaded = true; _startWhenLoadedMood = 0; _startWhenLoadedIncludeMissionIntro = false; _startWhenLoadedSource = source; Plugin.ThemeT("[EXT THEME AUTO] queued mood-loss helper switch from " + source + "; waiting for OGG/WAV loading to finish"); } } else { SwitchToHelperNow("mood loss hook " + source); } } private static void ResyncCurrentMainWindowToP1RhythmBoundary(int detectedMood, string source) { if (!Plugin.ExternalThemeResyncOnP1RhythmBoundary || !_battleActive || !_mainPlaying || _mainWaitingForStartDelay || detectedMood <= 0 || detectedMood != _currentMood) { return; } double num = NowSeconds(); if (_lastP1RhythmBoundaryResyncTime > 0.0001 && num - _lastP1RhythmBoundaryResyncTime < 0.75) { return; } _lastP1RhythmBoundaryResyncTime = num; double phaseOffsetForMood = GetPhaseOffsetForMood(detectedMood); double num2 = num + phaseOffsetForMood; if (MainPlayer.AdjustExpectedEnd(num2)) { _mainNextFixedBoundaryTime = num2; double mainWindowStartTime = (_lastMainWindowStartTime = num2 - Plugin.ExternalThemeFixedPartWindowSeconds); if (_currentMood > 0 && !_helperAudible) { SetHelperParallelAnchorFromMainWindowStart(mainWindowStartTime, "rhythm-boundary resync " + source); AlignHelperWindowToParallelClock("rhythm-boundary resync " + source, makeAudible: false, force: true); } if (Plugin.ExternalThemeTraceRhythmBoundaryResync || Plugin.ExternalThemeTraceFixedWindowClock) { Plugin.ThemeT($"[EXT THEME CLOCK] rhythm-boundary resync: mood={detectedMood}, currentPart={_currentPart}, source={source}, nextPartStartIn={phaseOffsetForMood:F4}s, secondsUntilEnd={MainPlayer.SecondsUntilExpectedEnd(num):F4}s"); } } } private static void SwitchMoodAtGameBoundary(int mood, string source, bool includeMissionIntro) { mood = Math.Clamp(mood, 0, 3); if (mood == 0) { SwitchToHelperNow("game rhythm boundary (" + source + ")"); return; } SetHelperAudible(audible: false, $"switching to mood {mood} from {source}"); StopMainPlayerOnly(); ApplyMood(mood); _mainPlaying = true; double phaseOffsetForMood = GetPhaseOffsetForMood(mood); if (phaseOffsetForMood > 0.0001) { _mainWaitingForStartDelay = true; _mainStartDelayEndTime = NowSeconds() + phaseOffsetForMood; _mainNextFixedBoundaryTime = _mainStartDelayEndTime; SetHelperParallelAnchorFromMainWindowStart(_mainStartDelayEndTime, "game rhythm boundary " + source); AlignHelperWindowToParallelClock("game rhythm boundary " + source, makeAudible: false, force: true); Plugin.ThemeT($"[EXT THEME AUTO] switching to mood {mood} at game rhythm boundary from {source}; delayed by phase offset {phaseOffsetForMood:F3}s"); } else { _mainWaitingForStartDelay = false; ArmMainFixedBoundary(NowSeconds()); Plugin.ThemeT($"[EXT THEME AUTO] switching immediately to mood {mood} at game rhythm boundary from {source}"); PlayNextMainPart("game rhythm boundary (" + source + ")"); } } private static void SwitchToHelperNow(string source) { _currentMood = 0; _rhythmUpdateLossAlreadyHandled = true; _pendingMood = 0; _hasPendingMood = false; _mainStopAfterMood0Intro = false; _mainNextFixedBoundaryTime = 0.0; _helperNextFixedBoundaryTime = 0.0; _lastP1RhythmBoundaryResyncTime = 0.0; _lastOriginalHelperAnchorResyncTime = 0.0; OneShotQueue.Clear(); _loopParts = Array.Empty(); StopMainPlayerOnly(); if (!AlignHelperWindowToBestKnownAnchor(source, makeAudible: true)) { EnsureHelperLoopScheduled(includeMissionIntroWindow: false, source, allowImmediateStartIfMissing: true); if (!_helperRunning && Parts.ContainsKey("Begin-02")) { _helperWaitingForStartDelay = false; PlayHelperWindow("helper forced immediate start (" + source + ")"); } SetHelperAudible(audible: true, source); } Plugin.ThemeT("[EXT THEME AUTO] switched to helper immediately from " + source + "; helper aligned to main window when available, otherwise mission-start helper phase"); } private static void SetMood(int mood, bool immediate, string source, bool includeMissionIntro = false) { if (!Plugin.ExternalThemeEnabled) { return; } mood = Math.Clamp(mood, 0, 3); RequestLoad($"mood {mood} request from {source}", 1); if (!_loadedAny) { if (!_loadComplete) { _startWhenLoaded = true; _startWhenLoadedMood = mood; _startWhenLoadedIncludeMissionIntro = includeMissionIntro; _startWhenLoadedSource = source; Plugin.ThemeT($"[EXT THEME] queued mood {mood} from {source}; waiting for OGG/WAV loading to finish"); } else { Plugin.W("[EXT THEME] cannot start: no OGG/WAV files loaded"); } } else if (mood == 0 && !includeMissionIntro) { if (immediate || !_mainPlaying) { SwitchToHelperNow(source); return; } _pendingMood = 0; _pendingIncludeMissionIntro = false; _hasPendingMood = true; Plugin.ThemeT("[EXT THEME] pending helper mood 0 from " + source + "; will unmute helper on next main part boundary"); } else if (immediate || !_mainPlaying) { StopMainPlayerOnly(); ApplyMood(mood, includeMissionIntro); _mainPlaying = true; _mainWaitingForStartDelay = true; if (mood == 0) { EnsureHelperLoopScheduled(includeMissionIntro, source, allowImmediateStartIfMissing: false); SetHelperAudible(audible: true, source); } else { SetHelperAudible(audible: false, source); } double phaseOffsetForMood = GetPhaseOffsetForMood(mood); double num = Plugin.ExternalThemeInitialDelaySeconds + phaseOffsetForMood; _mainStartDelayEndTime = NowSeconds() + num; _mainNextFixedBoundaryTime = _mainStartDelayEndTime; if (mood > 0) { SetHelperParallelAnchorFromMainWindowStart(_mainStartDelayEndTime, $"scheduled main mood {mood} from {source}"); AlignHelperWindowToParallelClock($"scheduled main mood {mood} from {source}", makeAudible: false, force: true); } Plugin.ThemeT($"[EXT THEME] mood {mood} armed from {source}; starting after {num:F3}s delay (initial={Plugin.ExternalThemeInitialDelaySeconds:F3}s, phaseOffset={phaseOffsetForMood:F3}s)"); } else { _pendingMood = mood; _pendingIncludeMissionIntro = includeMissionIntro; _hasPendingMood = true; Plugin.ThemeT($"[EXT THEME] pending mood {mood} from {source}; will switch on next main part boundary"); } } private static void ApplyMood(int mood, bool includeMissionIntro = false) { _currentMood = mood; if (mood > 0) { _rhythmUpdateLossAlreadyHandled = false; } _pendingMood = mood; _hasPendingMood = false; _loopIndex = 0; _mainStopAfterMood0Intro = false; _mainNextFixedBoundaryTime = 0.0; _helperNextFixedBoundaryTime = 0.0; _lastP1RhythmBoundaryResyncTime = 0.0; _lastP1RhythmBoundaryTime = 0.0; _lastMainWindowStartTime = 0.0; OneShotQueue.Clear(); switch (mood) { case 0: if (includeMissionIntro && !_beginIntroPlayedThisBattle) { OneShotQueue.Enqueue("Begin-01"); _beginIntroPlayedThisBattle = true; _mainStopAfterMood0Intro = true; } _loopParts = Array.Empty(); break; case 1: _loopParts = Mood1Loop; break; case 2: OneShotQueue.Enqueue("Combo-05"); _loopParts = Mood2Loop; break; case 3: OneShotQueue.Enqueue("Combo-10"); _loopParts = Mood3Loop; break; default: _loopParts = Array.Empty(); break; } Plugin.ThemeT($"[EXT THEME] applied main mood {mood}; loop=[{string.Join(", ", _loopParts)}], oneShots={OneShotQueue.Count}, helperAudible={_helperAudible}"); } private static void PlayNextMainPart(string source) { if (!_mainPlaying) { return; } if (_hasPendingMood) { if (_pendingMood == 0 && !_pendingIncludeMissionIntro) { SwitchToHelperNow("pending mood 0 at main boundary; source=" + source); return; } if (_pendingMood > 0) { SetHelperAudible(audible: false, $"pending mood {_pendingMood} applied at main boundary"); } ApplyMood(_pendingMood, _pendingIncludeMissionIntro); } int num = 0; while (num < 64) { string text = GetNextMainPartName(); if (text == null) { if (_currentMood == 0 || _mainStopAfterMood0Intro) { StopMainPlayerOnly(); _mainStopAfterMood0Intro = false; Plugin.ThemeT("[EXT THEME] main mood 0 intro finished from " + source + "; helper loop remains active/audible"); } else { Stop("no sequence parts available"); } return; } if (!Parts.TryGetValue(text, out AudioPart value) || value == null) { if (!text.Equals("Begin-01", StringComparison.OrdinalIgnoreCase) || !Parts.TryGetValue("Begin-02", out AudioPart value2) || value2 == null) { WarnMissingPart(text); num++; continue; } WarnMissingPart(text); Plugin.ThemeT("[EXT THEME] Begin-01 missing; using Begin-02 once as a temporary intro fallback"); value = value2; text = "Begin-02"; } double num2 = NowSeconds(); double effectivePartWindowSeconds = GetEffectivePartWindowSeconds(value); double andAdvanceMainExpectedEndTime = GetAndAdvanceMainExpectedEndTime(num2, effectivePartWindowSeconds); double mainWindowStartTime = (_lastMainWindowStartTime = andAdvanceMainExpectedEndTime - effectivePartWindowSeconds); MainPlayer.PlayUntil(value, GateThemeVolume(Plugin.ExternalThemeVolume), num2, andAdvanceMainExpectedEndTime); _currentPart = text; if (_currentMood > 0) { SyncMutedHelperToMainWindowStart(mainWindowStartTime, $"main window {text} ({source})"); } string value3 = (Plugin.ExternalThemeUseFixedPartWindow ? $", fixedWindow={effectivePartWindowSeconds:F4}s" : string.Empty); string value4 = (Plugin.ExternalThemeAnchorFixedWindowClock ? $", anchoredEndIn={andAdvanceMainExpectedEndTime - num2:F4}s" : string.Empty); if (Plugin.ExternalThemeTraceFixedWindowClock && Plugin.ExternalThemeAnchorFixedWindowClock) { Plugin.ThemeT($"[EXT THEME CLOCK] main part={text}, now={num2:F4}, expectedEnd={andAdvanceMainExpectedEndTime:F4}, startsLateBy={Math.Max(0.0, num2 - (andAdvanceMainExpectedEndTime - effectivePartWindowSeconds)):F4}s"); } Plugin.ThemeT($"[EXT THEME] playing {text} mood={_currentMood}, duration={value.DurationSeconds:F4}s{value3}{value4}, channels={value.Channels}, hz={value.SampleRate}, source={source}"); return; } Stop("could not find any loaded main part after 64 attempts"); } private static void EnsureHelperLoopScheduled(bool includeMissionIntroWindow, string source, bool allowImmediateStartIfMissing) { if (!Plugin.ExternalThemeKeepHelperLoopRunningMuted || _helperRunning || _helperWaitingForStartDelay) { return; } if (!Parts.ContainsKey("Begin-02")) { WarnMissingPart("Begin-02"); return; } double num = Plugin.ExternalThemeInitialDelaySeconds + GetPhaseOffsetForMood(0); if (includeMissionIntroWindow && Plugin.ExternalThemeHelperStartsAfterIntroWindow) { num += Plugin.ExternalThemeFixedPartWindowSeconds; } if (allowImmediateStartIfMissing) { num = 0.0; } double num2 = NowSeconds(); _helperScheduleSource = source; _helperParallelAnchorTime = num2 + num; if (num > 0.0001) { _helperWaitingForStartDelay = true; _helperStartDelayEndTime = _helperParallelAnchorTime; _helperNextFixedBoundaryTime = _helperParallelAnchorTime; Plugin.ThemeT($"[EXT THEME HELPER] scheduled parallel Begin-02 loop from {source}; startDelay={num:F3}s, audible={_helperAudible}"); } else { PlayHelperWindow("helper immediate start (" + source + ")"); } } private static void PlayHelperWindow(string source) { if (Plugin.ExternalThemeKeepHelperLoopRunningMuted) { EnsureHelperParallelAnchorFromBestKnownWindow(source); AlignHelperWindowToParallelClock(source, _helperAudible, force: true); } } private static void SetHelperAudible(bool audible, string source) { if (Plugin.ExternalThemeKeepHelperLoopRunningMuted) { if (_helperAudible == audible) { HelperPlayer.SetVolume(audible ? GateThemeVolume(Plugin.ExternalThemeVolume) : 0f); return; } _helperAudible = audible; HelperPlayer.SetVolume(audible ? GateThemeVolume(Plugin.ExternalThemeVolume) : 0f); Plugin.ThemeT("[EXT THEME HELPER] helper " + (audible ? "unmuted" : "muted") + " from " + source); } } private static void ArmMainFixedBoundary(double intendedStartTime) { if (Plugin.ExternalThemeAnchorFixedWindowClock) { _mainNextFixedBoundaryTime = intendedStartTime; } else { _mainNextFixedBoundaryTime = 0.0; } } private static void ArmHelperFixedBoundary(double intendedStartTime) { if (Plugin.ExternalThemeAnchorFixedWindowClock) { _helperNextFixedBoundaryTime = intendedStartTime; } else { _helperNextFixedBoundaryTime = 0.0; } } private static double GetAndAdvanceMainExpectedEndTime(double now, double windowSeconds) { return GetAndAdvanceExpectedEndTime(ref _mainNextFixedBoundaryTime, now, windowSeconds); } private static double GetAndAdvanceHelperExpectedEndTime(double now, double windowSeconds) { return GetAndAdvanceExpectedEndTime(ref _helperNextFixedBoundaryTime, now, windowSeconds); } private static double GetAndAdvanceExpectedEndTime(ref double nextBoundaryTime, double now, double windowSeconds) { if (!Plugin.ExternalThemeAnchorFixedWindowClock || windowSeconds <= 0.0001) { return now + Math.Max(0.01, windowSeconds); } if (nextBoundaryTime <= 0.0001) { nextBoundaryTime = now; } double num; for (num = nextBoundaryTime + windowSeconds; num <= now + 0.02; num = nextBoundaryTime + windowSeconds) { nextBoundaryTime = num; } nextBoundaryTime = num; return num; } private static double GetPhaseOffsetForMood(int mood) { double externalThemeFixedWindowPhaseOffsetSeconds = Plugin.ExternalThemeFixedWindowPhaseOffsetSeconds; if (mood == 0 && Plugin.ExternalThemeUseSeparateHelperPhaseOffset) { return externalThemeFixedWindowPhaseOffsetSeconds + Plugin.ExternalThemeHelperPhaseOffsetSeconds; } return externalThemeFixedWindowPhaseOffsetSeconds; } private static double GetEffectivePartWindowSeconds(AudioPart audioPart) { if (Plugin.ExternalThemeUseFixedPartWindow) { return Plugin.ExternalThemeFixedPartWindowSeconds; } return audioPart.DurationSeconds; } private static string? GetNextMainPartName() { if (OneShotQueue.Count > 0) { return OneShotQueue.Dequeue(); } if (_loopParts.Length == 0) { return null; } string result = _loopParts[_loopIndex % _loopParts.Length]; _loopIndex++; return result; } private static void UnloadLoadedAudio(string source) { Stop("unload: " + source); Parts.Clear(); MissingPartWarnings.Clear(); _loadAttempted = false; _loadComplete = false; _loadedAny = false; _loadRequested = false; _incrementalLoadActive = false; _loadPartQueue = Array.Empty(); _loadPartIndex = 0; _loadDirectory = string.Empty; _loadWavByNormalizedStem = new Dictionary(StringComparer.OrdinalIgnoreCase); _loadOggByNormalizedStem = new Dictionary(StringComparer.OrdinalIgnoreCase); _loggedOggExceptionDetails = false; _startWhenLoaded = false; _startWhenLoadedMood = 0; _startWhenLoadedIncludeMissionIntro = false; _startWhenLoadedSource = string.Empty; ExternalResponseSequencer.Unload(source); Plugin.ThemeT("[EXT THEME] unloaded selected theme audio from " + source); } private static void Stop(string source) { StopMainPlayerOnly(); StopHelperPlayerOnly(); _mainPlaying = false; _mainWaitingForStartDelay = false; _helperRunning = false; _helperWaitingForStartDelay = false; _helperAudible = false; _hasPendingMood = false; _startWhenLoaded = false; _mainStopAfterMood0Intro = false; _mainNextFixedBoundaryTime = 0.0; _helperNextFixedBoundaryTime = 0.0; _lastP1RhythmBoundaryResyncTime = 0.0; _lastP1RhythmBoundaryTime = 0.0; _lastMainWindowStartTime = 0.0; _helperParallelAnchorTime = 0.0; OneShotQueue.Clear(); _currentPart = string.Empty; _helperCurrentPart = string.Empty; ResetRhythmUpdateLossState(); Plugin.ThemeT("[EXT THEME] stopped from " + source); } private static void ResetRhythmUpdateLossState() { _hasLastRhythmUpdateState = false; _lastRhythmUpdatePlayCount = 0u; _lastRhythmUpdateIsPlay = false; _lastRhythmUpdateMoodText = string.Empty; _lastRhythmUpdatePlaySeIndex = uint.MaxValue; _rhythmUpdateLossAlreadyHandled = false; _rhythmUpdateLossTraceCount = 0; } private static void StopMainPlayerOnly() { try { MainPlayer.Stop(); } catch { } _mainPlaying = false; _mainWaitingForStartDelay = false; _currentPart = string.Empty; _mainNextFixedBoundaryTime = 0.0; } private static void StopHelperPlayerOnly() { try { HelperPlayer.Stop(); } catch { } _helperNextFixedBoundaryTime = 0.0; } private static void RequestLoad(string reason, int delayFrames) { if (!_loadAttempted && !_loadComplete) { int num = Math.Max(0, delayFrames); if (!_loadRequested || num < _loadDelayFrames) { _loadDelayFrames = num; } _loadRequested = true; _loadRequestReason = reason; Plugin.ThemeT($"[EXT THEME] load requested from {reason}; delayFrames={_loadDelayFrames}"); } } private static void ProcessDeferredLoad() { if (_loadRequested && !_loadAttempted && !_loadComplete) { if (_loadDelayFrames > 0) { _loadDelayFrames--; return; } string loadRequestReason = _loadRequestReason; _loadRequested = false; Plugin.ThemeT("[EXT THEME] starting deferred OGG/WAV load from " + loadRequestReason); EnsureLoaded(); } } private static void EnsureLoaded() { BeginIncrementalLoad(); } private static void BeginIncrementalLoad() { if (_loadAttempted) { return; } _loadAttempted = true; _loadRequested = false; _loadComplete = false; _loadedAny = false; _incrementalLoadActive = false; _loadPartQueue = Array.Empty(); _loadPartIndex = 0; _loadDirectory = string.Empty; _loadWavByNormalizedStem = new Dictionary(StringComparer.OrdinalIgnoreCase); _loadOggByNormalizedStem = new Dictionary(StringComparer.OrdinalIgnoreCase); Parts.Clear(); MissingPartWarnings.Clear(); string externalThemeDirectory = Plugin.ExternalThemeDirectory; try { if (string.IsNullOrWhiteSpace(externalThemeDirectory)) { _loadComplete = true; Plugin.W("[EXT THEME] ThemeDirectory is empty"); return; } if (!Directory.Exists(externalThemeDirectory)) { Directory.CreateDirectory(externalThemeDirectory); _loadComplete = true; Plugin.W("[EXT THEME] theme directory did not exist, created it: " + externalThemeDirectory); Plugin.W("[EXT THEME] add OGG or WAV files there, then restart the game or reload the plugin"); return; } _loadDirectory = externalThemeDirectory; _loadWavByNormalizedStem = (from p in Directory.EnumerateFiles(externalThemeDirectory, "*.wav", SearchOption.TopDirectoryOnly) group p by NormalizePartName(Path.GetFileNameWithoutExtension(p))).ToDictionary, string, string>((IGrouping g) => g.Key, (IGrouping g) => g.First(), StringComparer.OrdinalIgnoreCase); _loadOggByNormalizedStem = (from p in Directory.EnumerateFiles(externalThemeDirectory, "*.ogg", SearchOption.TopDirectoryOnly) group p by NormalizePartName(Path.GetFileNameWithoutExtension(p))).ToDictionary, string, string>((IGrouping g) => g.Key, (IGrouping g) => g.First(), StringComparer.OrdinalIgnoreCase); _loadPartQueue = RequiredPartNames(); _loadPartIndex = 0; _incrementalLoadActive = true; Plugin.ThemeT($"[EXT THEME] incremental preload prepared: {_loadPartQueue.Length} required parts, oggFiles={_loadOggByNormalizedStem.Count}, wavFiles={_loadWavByNormalizedStem.Count}, dir={externalThemeDirectory}"); } catch (Exception value) { _loadComplete = true; _incrementalLoadActive = false; Plugin.W($"[EXT THEME] BeginIncrementalLoad failed: {value}"); } } private static void ProcessIncrementalLoad() { if (!_incrementalLoadActive || _loadComplete) { return; } int externalThemePartsPerLoadFrame = Plugin.ExternalThemePartsPerLoadFrame; for (int i = 0; i < externalThemePartsPerLoadFrame; i++) { if (_loadPartIndex >= _loadPartQueue.Length) { break; } string text = _loadPartQueue[_loadPartIndex++]; if (ShouldSkipPart(text)) { Plugin.W("[EXT THEME] skipping " + text + " because it is listed in ExternalTheme.Loading.SkipPartNames"); continue; } string[] array = BuildFileStemAliases(text); string text2 = FindFirstByAliases(_loadOggByNormalizedStem, array); if (!string.IsNullOrWhiteSpace(text2)) { if (Plugin.ExternalThemeTraceBeforeEachPartLoad) { Plugin.ThemeT($"[EXT THEME] about to load OGG {text} from {Path.GetFileName(text2)} ({_loadPartIndex}/{_loadPartQueue.Length})"); } TryLoadOgg(text, text2); continue; } string text3 = FindFirstByAliases(_loadWavByNormalizedStem, array); if (!string.IsNullOrWhiteSpace(text3)) { if (Plugin.ExternalThemeTraceBeforeEachPartLoad) { Plugin.ThemeT($"[EXT THEME] about to load WAV {text} from {Path.GetFileName(text3)} ({_loadPartIndex}/{_loadPartQueue.Length})"); } TryLoadWav(text, text3); } else { Plugin.W($"[EXT THEME] missing OGG/WAV for {text} in {_loadDirectory}; tried aliases=[{string.Join(", ", array)}]"); } } if (_loadPartIndex >= _loadPartQueue.Length) { _incrementalLoadActive = false; FinishLoadingIfDone(); } } private static void TryLoadWav(string partName, string path) { try { AudioPart audioPart = WavAudioClipLoader.LoadWav(path, partName); Parts[partName] = audioPart; Plugin.ThemeT($"[EXT THEME] loaded WAV {partName} from {Path.GetFileName(path)}; frames={audioPart.FrameCount}, channels={audioPart.Channels}, hz={audioPart.SampleRate}, length={audioPart.DurationSeconds:F4}s"); } catch (Exception ex) { Plugin.W($"[EXT THEME] failed to load WAV {partName} from {path}: {ex.Message}"); } } private static void TryLoadOgg(string partName, string path) { try { FileInfo fileInfo = new FileInfo(path); if (Plugin.ExternalThemeTraceBeforeEachPartLoad) { Plugin.ThemeT($"[EXT THEME] decoding OGG {partName}: bytes={fileInfo.Length}, timeout={(Plugin.ExternalThemeUseDecodeTimeout ? (Plugin.ExternalThemeDecodeTimeoutSeconds.ToString("F2") + "s") : "off")}"); } if (!AudioDecodeRunner.TryLoadOgg(path, partName, Plugin.ExternalThemeDecodeTimeoutSeconds, out AudioPart part, out string error) || part == null) { string value = FirstLine(error); Plugin.W($"[EXT THEME] failed to load OGG {partName} from {path}: {value}"); if (!_loggedOggExceptionDetails) { _loggedOggExceptionDetails = true; Plugin.W("[EXT THEME] first OGG exception/details: " + error); } } else { Parts[partName] = part; Plugin.ThemeT($"[EXT THEME] loaded OGG {partName} from {Path.GetFileName(path)}; frames={part.FrameCount}, channels={part.Channels}, hz={part.SampleRate}, length={part.DurationSeconds:F4}s"); } } catch (Exception ex) { Plugin.W($"[EXT THEME] failed to load OGG {partName} from {path}: {ex.Message}"); if (!_loggedOggExceptionDetails) { _loggedOggExceptionDetails = true; Plugin.W($"[EXT THEME] first OGG exception details: {ex}"); } } } private static string FirstLine(string text) { if (string.IsNullOrWhiteSpace(text)) { return "unknown error"; } string text2 = text.Replace("\r", ""); int num = text2.IndexOf('\n'); return (num >= 0) ? text2.Substring(0, num) : text2; } private static bool ShouldSkipPart(string partName) { string externalThemeSkipPartNames = Plugin.ExternalThemeSkipPartNames; if (string.IsNullOrWhiteSpace(externalThemeSkipPartNames)) { return false; } string b = NormalizePartName(partName); char[] separator = new char[6] { ',', ';', '|', '\n', '\r', '\t' }; string[] array = externalThemeSkipPartNames.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (string text in array) { if (!string.IsNullOrWhiteSpace(text) && string.Equals(NormalizePartName(text), b, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static void FinishLoadingIfDone() { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown if (!_loadComplete) { _loadedAny = Parts.Count > 0; _loadComplete = true; ManualLogSource l = Plugin.L; bool flag = default(bool); BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(40, 3, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[EXT THEME] loaded "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(Parts.Count); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("/"); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(RequiredPartNames().Length); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" OGG/WAV parts from "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(Plugin.ExternalThemeDirectory); } l.LogInfo(val); if (_pendingInitialOriginalHelperAnchorTime > 0.0001 && _battleActive && _currentMood == 0 && _lastMainWindowStartTime <= 0.0001 && _loadedAny && !_beginIntroPlayedThisBattle) { double pendingInitialOriginalHelperAnchorTime = _pendingInitialOriginalHelperAnchorTime; _pendingInitialOriginalHelperAnchorTime = 0.0; _startWhenLoaded = false; _startWhenLoadedIncludeMissionIntro = false; StartMissionIntroAtOriginalHelperAnchor(pendingInitialOriginalHelperAnchorTime, "deferred initial original helper anchor after load"); } if (_startWhenLoaded && _battleActive && !_mainPlaying && _loadedAny) { int startWhenLoadedMood = _startWhenLoadedMood; bool startWhenLoadedIncludeMissionIntro = _startWhenLoadedIncludeMissionIntro; string startWhenLoadedSource = _startWhenLoadedSource; _startWhenLoaded = false; _startWhenLoadedIncludeMissionIntro = false; SetMood(startWhenLoadedMood, immediate: true, startWhenLoadedSource + " after load", startWhenLoadedIncludeMissionIntro); } } } private static void WarnMissingPart(string part) { if (MissingPartWarnings.Add(part)) { Plugin.W("[EXT THEME] sequence wants " + part + ", but it is not loaded"); } } private static string? FindFirstByAliases(Dictionary pathsByNormalizedStem, string[] aliases) { foreach (string name in aliases) { string key = NormalizePartName(name); if (pathsByNormalizedStem.TryGetValue(key, out string value)) { return value; } } return null; } private static string[] BuildFileStemAliases(string canonicalPartName) { string selectedThemeNumberPrefix = ThemePathResolver.SelectedThemeNumberPrefix; return new string[4] { canonicalPartName, selectedThemeNumberPrefix + canonicalPartName, selectedThemeNumberPrefix + canonicalPartName.Replace("-", string.Empty), canonicalPartName.Replace("-", string.Empty) }; } private static string[] RequiredPartNames() { List list = new List(); list.Add("Begin-01"); list.AddRange(Mood0Loop); for (int i = 1; i <= 10; i++) { list.Add($"Combo-{i:00}"); } list.AddRange(Mood3Loop); return list.ToArray(); } private static string NormalizePartName(string name) { return ThemePathResolver.NormalizePartOrCueName(name); } private static double NowSeconds() { return Time.realtimeSinceStartup; } } internal static class ExternalResponseSequencer { private sealed class PendingResponse { public string Command = string.Empty; public string SourceKey = string.Empty; public string SourcePath = string.Empty; public int Prog; public int RgndCueId; public int WaveRefIndex; public double PlayAt; } private sealed class CommandParamSnapshot { public string AName = string.Empty; public int? Id; public int? Command; public int? CommandType; public int? UseType; public string KeyPattern = string.Empty; } [CompilerGenerated] private sealed class d__63 : IEnumerable, IEnumerable, IEnumerator, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private int <>l__initialThreadId; private string responseIndex; public string <>3__responseIndex; string IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__63(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; if (responseIndex.Equals("02", StringComparison.OrdinalIgnoreCase)) { <>2__current = "03"; <>1__state = 1; return true; } if (responseIndex.Equals("03", StringComparison.OrdinalIgnoreCase)) { <>2__current = "02"; <>1__state = 3; return true; } <>2__current = "02"; <>1__state = 5; return true; case 1: <>1__state = -1; <>2__current = "01"; <>1__state = 2; return true; case 2: <>1__state = -1; return false; case 3: <>1__state = -1; <>2__current = "01"; <>1__state = 4; return true; case 4: <>1__state = -1; return false; case 5: <>1__state = -1; <>2__current = "03"; <>1__state = 6; return true; case 6: <>1__state = -1; return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { d__63 d__; if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; d__ = this; } else { d__ = new d__63(0); } d__.responseIndex = <>3__responseIndex; return d__; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } } [CompilerGenerated] private sealed class d__67 : IEnumerable, IEnumerable, IEnumerator, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private int <>l__initialThreadId; private string candidate; public string <>3__candidate; private string 5__1; string IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__67(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; 5__1 = NormalizeVoiceKey(candidate); if (!string.IsNullOrWhiteSpace(5__1)) { <>2__current = 5__1; <>1__state = 1; return true; } break; case 1: <>1__state = -1; break; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { d__67 d__; if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; d__ = this; } else { d__ = new d__67(0); } d__.candidate = <>3__candidate; return d__; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } } private static readonly Dictionary Voices = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly List VoiceOrder = new List(); private static readonly HashSet MissingCueWarnings = new HashSet(StringComparer.OrdinalIgnoreCase); private static readonly ManagedNaudioPlayer ResponsePlayer = new ManagedNaudioPlayer(); private static readonly List PendingResponses = new List(); private static readonly HashSet ThemeUniqueCommands = new HashSet(StringComparer.OrdinalIgnoreCase) { "Attack", "Defend", "March" }; private static bool _initialized; private static bool _loadAttempted; private static bool _loadComplete; private static bool _battleActive; private static bool _externalAudioGateOpen = true; private static double _lastResponseTriggerTime = -999.0; private static int _traceCount; private static int _commandTraceCount; private static string _lastDetectedCommand = string.Empty; private static string _lastDetectedCommandSource = string.Empty; private static double _lastDetectedCommandTime = -999.0; private static double _lastFixCommandTime = -999.0; private static bool _feverEntryUniversalPlayedThisFever; private static int _feverCommandResponseCounter; private static string _lastResponseSelectionNote = string.Empty; public static void Initialize() { if (!_initialized) { _initialized = true; } } public static void Unload(string source) { PendingResponses.Clear(); try { ResponsePlayer.Stop(); } catch { } Voices.Clear(); VoiceOrder.Clear(); MissingCueWarnings.Clear(); _loadAttempted = false; _loadComplete = false; _lastResponseTriggerTime = -999.0; _traceCount = 0; _commandTraceCount = 0; _lastDetectedCommand = string.Empty; _lastDetectedCommandSource = string.Empty; _lastDetectedCommandTime = -999.0; _lastFixCommandTime = -999.0; _feverEntryUniversalPlayedThisFever = false; _feverCommandResponseCounter = 0; _lastResponseSelectionNote = string.Empty; Plugin.ThemeT("[EXT RESP] unloaded response voices from " + source); } public static void OnBattleStart(string source) { _battleActive = true; _externalAudioGateOpen = true; _lastResponseTriggerTime = -999.0; _traceCount = 0; _commandTraceCount = 0; _lastDetectedCommand = string.Empty; _lastDetectedCommandSource = string.Empty; _lastDetectedCommandTime = -999.0; _lastFixCommandTime = -999.0; _feverEntryUniversalPlayedThisFever = false; _feverCommandResponseCounter = 0; _lastResponseSelectionNote = string.Empty; PendingResponses.Clear(); EnsureLoaded(); Plugin.ThemeT($"[EXT RESP] battle start from {source}; voicesLoaded={Voices.Count}; lastCommand cleared"); } public static void OnBattleEnd(string source) { _battleActive = false; _externalAudioGateOpen = true; _lastDetectedCommand = string.Empty; _lastDetectedCommandSource = string.Empty; _lastDetectedCommandTime = -999.0; _lastFixCommandTime = -999.0; _feverEntryUniversalPlayedThisFever = false; _feverCommandResponseCounter = 0; _lastResponseSelectionNote = string.Empty; PendingResponses.Clear(); try { ResponsePlayer.Stop(); } catch { } Plugin.ThemeT("[EXT RESP] battle end from " + source + "; response player stopped"); } public static void SetExternalAudioGate(bool open) { if (_externalAudioGateOpen == open) { return; } _externalAudioGateOpen = open; if (!open) { PendingResponses.Clear(); try { ResponsePlayer.SetVolume(0f); return; } catch { return; } } try { ResponsePlayer.SetVolume(Plugin.ExternalResponseVolume); } catch { } } public static void RefreshRuntimeVolume() { try { ResponsePlayer.SetVolume(_externalAudioGateOpen ? Plugin.ExternalResponseVolume : 0f); } catch { } } public static void Update(double now) { if (!_externalAudioGateOpen) { PendingResponses.Clear(); } else { if (PendingResponses.Count == 0) { return; } for (int num = PendingResponses.Count - 1; num >= 0; num--) { PendingResponse pendingResponse = PendingResponses[num]; if (!(now + 0.0001 < pendingResponse.PlayAt)) { PendingResponses.RemoveAt(num); PlayResponseNow(pendingResponse, now); } } } } public static void EnsureLoaded() { if (_loadAttempted || _loadComplete || !Plugin.ExternalResponsesEnabled) { return; } _loadAttempted = true; Voices.Clear(); VoiceOrder.Clear(); MissingCueWarnings.Clear(); try { string externalThemeVoicesDirectory = Plugin.ExternalThemeVoicesDirectory; string externalResponseMiscVoicesDirectory = Plugin.ExternalResponseMiscVoicesDirectory; int value = LoadVoiceDirectory(externalThemeVoicesDirectory, "selected theme"); int value2 = 0; int value3 = 0; if (!string.IsNullOrWhiteSpace(externalResponseMiscVoicesDirectory) && !SameDirectory(externalThemeVoicesDirectory, externalResponseMiscVoicesDirectory)) { value2 = LoadVoiceDirectory(externalResponseMiscVoicesDirectory, "00Misc voices fallback"); } string text = ResolveMiscOtherDirectory(externalResponseMiscVoicesDirectory); if (!string.IsNullOrWhiteSpace(text) && !SameDirectory(externalThemeVoicesDirectory, text) && !SameDirectory(externalResponseMiscVoicesDirectory, text)) { value3 = LoadVoiceDirectory(text, "00Misc other fallback"); } _loadComplete = true; Plugin.ThemeT($"[EXT RESP] loaded {Voices.Count} unique OGG/WAV voice keys; selectedFiles={value}, miscFiles={value2}, miscOtherFiles={value3}, selectedDir='{externalThemeVoicesDirectory}', miscDir='{externalResponseMiscVoicesDirectory}', miscOtherDir='{text}'"); } catch (Exception value4) { _loadComplete = true; Plugin.W($"[EXT RESP] EnsureLoaded failed: {value4}"); } } private static string ResolveMiscOtherDirectory(string miscVoicesDir) { try { if (!string.IsNullOrWhiteSpace(miscVoicesDir)) { DirectoryInfo parent = Directory.GetParent(miscVoicesDir); if (parent != null) { return Path.Combine(parent.FullName, "00Other"); } } } catch { } string text = Plugin.ExternalThemeLibraryRootDirectory; if (string.IsNullOrWhiteSpace(text)) { text = Path.GetDirectoryName(Plugin.ExternalThemeLegacyDirectory) ?? string.Empty; } return Path.Combine(text, "00Misc", "00Other"); } private static int LoadVoiceDirectory(string dir, string label) { if (string.IsNullOrWhiteSpace(dir)) { Plugin.W("[EXT RESP] " + label + " voices directory is empty"); return 0; } if (!Directory.Exists(dir)) { Plugin.W("[EXT RESP] " + label + " voices directory does not exist yet: " + dir); return 0; } int num = 0; foreach (string item in Directory.EnumerateFiles(dir, "*.ogg", SearchOption.TopDirectoryOnly).OrderBy((string p) => p, StringComparer.OrdinalIgnoreCase)) { if (TryLoadVoice(item, isOgg: true, label)) { num++; } } foreach (string item2 in Directory.EnumerateFiles(dir, "*.wav", SearchOption.TopDirectoryOnly).OrderBy((string p) => p, StringComparer.OrdinalIgnoreCase)) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(item2); if (!HasVoiceStem(fileNameWithoutExtension) && TryLoadVoice(item2, isOgg: false, label)) { num++; } } return num; } public static void OnP1CommandEventObserved(object? commandEvent, string source) { if (!Plugin.ExternalResponsesEnabled || !Plugin.ExternalResponseCommandDetectionEnabled || !_battleActive) { return; } try { ObserveCommandFromObject(commandEvent, source); } catch (Exception ex) { Plugin.W("[EXT RESP CMD] command event detector failed from " + source + ": " + ex.Message); } } public static void OnP1RhythmCommandObserved(object? rhythmPlayRequest, string source) { if (!Plugin.ExternalResponsesEnabled || !Plugin.ExternalResponseCommandDetectionEnabled || !_battleActive) { return; } try { ObserveCommandFromObject(rhythmPlayRequest, source); } catch (Exception ex) { Plugin.W("[EXT RESP CMD] rhythm request detector failed from " + source + ": " + ex.Message); } } private static string TryInferStrictCommandFromKnownObjectShape(object? obj, out string debug) { debug = "strict=no-object"; if (obj == null) { return string.Empty; } object commandArray = TryGetMemberValue(obj, "command_") ?? TryGetMemberValue(obj, "beatCommand") ?? TryGetMemberValue(obj, "beatCommand_"); string debug2; string text = TryInferStrictCommandFromArray(commandArray, out debug2); if (!string.IsNullOrWhiteSpace(debug2)) { debug = debug2; } if (!string.IsNullOrWhiteSpace(text)) { return text; } CommandParamSnapshot snap = SnapshotCommandParam(obj); string result = MapStrictSnapshot(snap); debug = "strict=direct " + FormatSnapshot(snap); return result; } private static string TryInferStrictCommandFromArray(object? commandArray, out string debug) { debug = string.Empty; if (commandArray == null) { debug = "strict=array-null"; return string.Empty; } int num = TryGetCount(commandArray); if (num <= 0) { debug = "strict=array-empty"; return string.Empty; } List list = new List(); string text = string.Empty; for (int i = 0; i < Math.Min(num, 8); i++) { object param = TryGetIndexedElement(commandArray, i); CommandParamSnapshot snap = SnapshotCommandParam(param); string text2 = MapStrictSnapshot(snap); if (string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2)) { text = text2; } list.Add($"{i}:{FormatSnapshot(snap)}=>{(string.IsNullOrWhiteSpace(text2) ? "?" : text2)}"); } debug = $"strict=array count={num}; elems=[{string.Join(" | ", list)}]"; return text; } private static CommandParamSnapshot SnapshotCommandParam(object? param) { CommandParamSnapshot commandParamSnapshot = new CommandParamSnapshot(); if (param == null) { return commandParamSnapshot; } object value = TryGetMemberValue(param, "aName") ?? TryGetMemberValue(param, "aName_") ?? TryGetMemberValue(param, "name") ?? TryGetMemberValue(param, "name_"); commandParamSnapshot.AName = SafeValueText(value); if (commandParamSnapshot.AName == "" || commandParamSnapshot.AName == "") { commandParamSnapshot.AName = string.Empty; } commandParamSnapshot.Id = TryGetIntMember(param, "id") ?? TryGetIntMember(param, "id_"); commandParamSnapshot.Command = TryGetIntMember(param, "command") ?? TryGetIntMember(param, "command_"); commandParamSnapshot.CommandType = TryGetIntMember(param, "commandType") ?? TryGetIntMember(param, "commandType_"); commandParamSnapshot.UseType = TryGetIntMember(param, "useType") ?? TryGetIntMember(param, "useType_"); object arr = TryGetMemberValue(param, "key") ?? TryGetMemberValue(param, "key_"); commandParamSnapshot.KeyPattern = TryReadSmallIntArray(arr); return commandParamSnapshot; } private static string MapStrictSnapshot(CommandParamSnapshot snap) { string text = MapCommandKeyPattern(snap.KeyPattern); if (!string.IsNullOrWhiteSpace(text)) { return text; } string text2 = MapCommandName(snap.AName); if (!string.IsNullOrWhiteSpace(text2)) { return text2; } int? num = snap.CommandType ?? snap.Command ?? (snap.Id.HasValue ? new int?(snap.Id.Value + 1) : null); if (!num.HasValue) { return string.Empty; } int value = num.Value; if (1 == 0) { } string result = value switch { 1 => "March", 2 => "Attack", 3 => "Attack", 4 => Plugin.ExternalResponseP1EvadeCommandMapsTo, 5 => "Charge", 6 => "Jump", _ => string.Empty, }; if (1 == 0) { } return result; } private static string MapCommandKeyPattern(string? keyPattern) { if (string.IsNullOrWhiteSpace(keyPattern)) { return string.Empty; } string text = keyPattern.Replace(" ", string.Empty).Trim(); if (1 == 0) { } string result = text switch { "3,0,3,0,3,0,1,-1" => "March", "3,0,3,0,3,0,1" => "March", "1,0,1,0,3,0,1,-1" => "Attack", "1,0,1,0,3,0,1" => "Attack", "4,0,4,0,3,0,1,-1" => "Defend", "4,0,4,0,3,0,1" => "Defend", "1,0,1,0,4,0,4,-1" => "Charge", "1,0,1,0,4,0,4" => "Charge", "1,0,3,0,1,0,3,-1" => "Retreat", "1,0,3,0,1,0,3" => "Retreat", "2,0,2,0,4,0,4,-1" => "Jump", "2,0,2,0,4,0,4" => "Jump", "1,0,1,0,2,0,4,-1" => "Party", "1,0,1,0,2,0,4" => "Party", _ => string.Empty, }; if (1 == 0) { } return result; } private static string MapCommandName(string? name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } string haystack = name.Replace(" ", string.Empty).Replace("\u3000", string.Empty); if (ContainsAny(haystack, "防御", "防衛", "ガード", "守備", "守る")) { return "Defend"; } if (ContainsAny(haystack, "回避", "退却", "後退", "撤退", "リトリート")) { return Plugin.ExternalResponseP1EvadeCommandMapsTo; } if (ContainsAny(haystack, "前進", "行進")) { return "March"; } if (ContainsAny(haystack, "チャージ")) { return "Charge"; } if (ContainsAny(haystack, "ジャンプ")) { return "Jump"; } if (ContainsAny(haystack, "パーティ", "パーティー")) { return "Party"; } if (ContainsAny(haystack, "近距離攻撃", "遠距離攻撃", "攻撃")) { return "Attack"; } string haystack2 = ThemePathResolver.NormalizePartOrCueName(name); if (ContainsAny(haystack2, "defend", "defence", "defense", "guard")) { return "Defend"; } if (ContainsAny(haystack2, "retreat")) { return "Retreat"; } if (ContainsAny(haystack2, "march")) { return "March"; } if (ContainsAny(haystack2, "charge")) { return "Charge"; } if (ContainsAny(haystack2, "jump")) { return "Jump"; } if (ContainsAny(haystack2, "party")) { return "Party"; } if (ContainsAny(haystack2, "attack", "atack", "attak")) { return "Attack"; } return string.Empty; } private static string FormatSnapshot(CommandParamSnapshot snap) { return $"aName='{snap.AName}', id={Fmt(snap.Id)}, command={Fmt(snap.Command)}, commandType={Fmt(snap.CommandType)}, useType={Fmt(snap.UseType)}, key=[{snap.KeyPattern}]"; } private static string Fmt(int? value) { return value.HasValue ? value.Value.ToString() : "?"; } private static string TryReadSmallIntArray(object? arr) { if (arr == null) { return string.Empty; } int num = TryGetCount(arr); if (num <= 0 || num > 32) { return string.Empty; } List list = new List(); for (int i = 0; i < num && i < 16; i++) { object obj = TryGetIndexedElement(arr, i); if (obj == null) { list.Add("null"); continue; } try { list.Add(Convert.ToInt32(obj).ToString()); } catch { list.Add(SafeValueText(obj)); } } if (num > 16) { list.Add("..."); } return string.Join(",", list); } private static string TryInferCommandFromKnownObjectShape(object? obj) { try { if (obj == null) { return string.Empty; } object commandArray = TryGetMemberValue(obj, "command_") ?? TryGetMemberValue(obj, "command") ?? TryGetMemberValue(obj, "beatCommand") ?? TryGetMemberValue(obj, "beatCommand_"); string text = TryInferCommandFromCommandArray(commandArray); if (!string.IsNullOrWhiteSpace(text)) { return text; } string text2 = TryInferCommandFromCommandParam(obj); if (!string.IsNullOrWhiteSpace(text2)) { return text2; } } catch { } return string.Empty; } private static string TryInferCommandFromCommandArray(object? commandArray) { if (commandArray == null) { return string.Empty; } int num = TryGetCount(commandArray); if (num <= 0) { return string.Empty; } List list = new List(); for (int i = 0; i < Math.Min(num, 8); i++) { object obj = TryGetIndexedElement(commandArray, i); if (obj != null) { string text = TryInferCommandFromCommandParam(obj); if (!string.IsNullOrWhiteSpace(text)) { return text; } string text2 = BuildObjectSummary(obj, 2, 1200); list.Add(text2); string text3 = InferCommandFromText(text2); if (!string.IsNullOrWhiteSpace(text3)) { return text3; } } } string text4 = string.Join(" | ", list); string text5 = TryInferCommandFromNumericPattern(text4); if (!string.IsNullOrWhiteSpace(text5)) { return text5; } return string.Empty; } private static string TryInferCommandFromCommandParam(object? param) { if (param == null) { return string.Empty; } string[] array = new string[6] { "aName", "aName_", "name", "name_", "label", "label_" }; foreach (string name in array) { object value = TryGetMemberValue(param, name); string text = InferCommandFromText(SafeValueText(value)); if (!string.IsNullOrWhiteSpace(text)) { return text; } } string text2 = TryInferCommandFromCommandNumbers(param); if (!string.IsNullOrWhiteSpace(text2)) { return text2; } string text3 = SafeValueText(param); string text4 = InferCommandFromText(text3); if (!string.IsNullOrWhiteSpace(text4)) { return text4; } string[] array2 = new string[22] { "command", "command_", "commandType", "commandType_", "type", "type_", "kind", "kind_", "aName", "aName_", "name", "name_", "label", "label_", "percussion", "percussion_", "key", "key_", "rhythmCommand", "rhythmCommand_", "beatCommand", "beatCommand_" }; string[] array3 = array2; foreach (string name2 in array3) { object value2 = TryGetMemberValue(param, name2); string text5 = InferCommandFromText(SafeValueText(value2)); if (!string.IsNullOrWhiteSpace(text5)) { return text5; } } Type type = param.GetType(); PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.GetIndexParameters().Length == 0 && propertyInfo.CanRead) { object value3; try { value3 = propertyInfo.GetValue(param); } catch { continue; } string text6 = InferCommandFromText(SafeValueText(value3)); if (!string.IsNullOrWhiteSpace(text6)) { return text6; } } } FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); foreach (FieldInfo fieldInfo in fields) { object value4; try { value4 = fieldInfo.GetValue(param); } catch { continue; } string text7 = InferCommandFromText(SafeValueText(value4)); if (!string.IsNullOrWhiteSpace(text7)) { return text7; } } return string.Empty; } private static string TryInferCommandFromCommandNumbers(object param) { int? num = TryGetIntMember(param, "commandType") ?? TryGetIntMember(param, "commandType_"); int? num2 = TryGetIntMember(param, "command") ?? TryGetIntMember(param, "command_"); int? num3 = TryGetIntMember(param, "id") ?? TryGetIntMember(param, "id_"); int num4 = num ?? num2 ?? (num3.HasValue ? (num3.Value + 1) : (-1)); if (1 == 0) { } string result = num4 switch { 1 => "March", 2 => "Attack", 3 => "Attack", 4 => Plugin.ExternalResponseP1EvadeCommandMapsTo, 5 => "Charge", 6 => "Jump", _ => string.Empty, }; if (1 == 0) { } return result; } private static int? TryGetIntMember(object obj, string name) { object obj2 = TryGetMemberValue(obj, name); if (obj2 == null) { return null; } try { return Convert.ToInt32(obj2); } catch { return null; } } private static object? TryGetMemberValue(object obj, string name) { try { Type type = obj.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public); if (property != null && property.CanRead && property.GetIndexParameters().Length == 0) { return property.GetValue(obj); } FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public); if (field != null) { return field.GetValue(obj); } MethodInfo method = type.GetMethod("get_" + name, BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null); if (method != null) { return method.Invoke(obj, null); } } catch { } return null; } private static int TryGetCount(object obj) { string[] array = new string[2] { "Length", "Count" }; foreach (string name in array) { object obj2 = TryGetMemberValue(obj, name); try { if (obj2 != null) { return Convert.ToInt32(obj2); } } catch { } } if (obj is ICollection collection) { return collection.Count; } return 0; } private static object? TryGetIndexedElement(object obj, int index) { try { Type type = obj.GetType(); PropertyInfo propertyInfo = type.GetProperties(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault((PropertyInfo p) => p.GetIndexParameters().Length == 1 && p.GetIndexParameters()[0].ParameterType == typeof(int)); if (propertyInfo != null) { return propertyInfo.GetValue(obj, new object[1] { index }); } MethodInfo methodInfo = type.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault((MethodInfo m) => (m.Name == "get_Item" || m.Name == "get_ItemImpl" || m.Name == "get_Item_Injected") && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(int)); if (methodInfo != null) { return methodInfo.Invoke(obj, new object[1] { index }); } } catch { } try { if (obj is IList list && index >= 0 && index < list.Count) { return list[index]; } } catch { } return null; } private static string TryInferCommandFromNumericPattern(string text) { return string.Empty; } private static void ObserveCommandFromObject(object? obj, string source) { //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Expected O, but got Unknown string debug; string text = TryInferStrictCommandFromKnownObjectShape(obj, out debug); double num = Time.realtimeSinceStartup; bool flag = source.Contains("onFixCommand", StringComparison.OrdinalIgnoreCase); bool flag2 = source.Contains("onExecCommand", StringComparison.OrdinalIgnoreCase); bool flag3 = source.Contains("Rhythm.playRhythm", StringComparison.OrdinalIgnoreCase); if (!string.IsNullOrWhiteSpace(text) && (flag || string.IsNullOrWhiteSpace(_lastDetectedCommand) || num - _lastDetectedCommandTime > 1.0 || (!flag3 && num - _lastFixCommandTime > 0.35))) { _lastDetectedCommand = text; _lastDetectedCommandSource = source; _lastDetectedCommandTime = num; if (flag) { _lastFixCommandTime = num; } } if (Plugin.ExternalResponseTraceCommandDetection && _commandTraceCount < Plugin.ExternalResponseMaxCommandTraceLogs) { _commandTraceCount++; ManualLogSource l = Plugin.L; bool flag4 = default(bool); BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(65, 5, ref flag4); if (flag4) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[EXT RESP EXACT] observed="); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(string.IsNullOrWhiteSpace(text) ? "" : text); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("; "); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("source="); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(source); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("; storedLast="); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(_lastDetectedCommand); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("; storedSource="); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(_lastDetectedCommandSource); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("; "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(debug); } l.LogInfo(val); } } public static void OnOriginalRhythmVoice(string fileType, string category, string acbFilePath, string key, uint incrementGlobalCounter, int prog, int rgndCueId, int waveRefIndex, float pitch, float pan, float originalVolume, bool loop) { if (!Plugin.ExternalResponsesEnabled || !_battleActive || !_externalAudioGateOpen) { return; } string text = acbFilePath ?? string.Empty; string text2 = key ?? string.Empty; string text3 = ResolveCommandForResponse(text2, text); if (Plugin.TraceOriginalRhythmVoiceResponses && _traceCount < Plugin.MaxResponseTraceLogs) { _traceCount++; Plugin.ThemeT($"[EXT RESP] original RhythmVoice: fileType={fileType}, category={category}, key={text2}, path={text}, prog={prog}, rgndCueId={rgndCueId}, waveRefIndex={waveRefIndex}, pitch={pitch}, pan={pan}, volume={originalVolume}, loop={loop}, extMood={ExternalThemeSequencer.CurrentMood}, currentPart={ExternalThemeSequencer.CurrentMainPart}, command={(string.IsNullOrWhiteSpace(text3) ? "" : text3)}, lastCommand={_lastDetectedCommand}, lastCommandSource={_lastDetectedCommandSource}"); } if (!Plugin.PlayCustomResponses) { return; } double num = Time.realtimeSinceStartup; if (num - _lastResponseTriggerTime < Plugin.ExternalResponseDebounceSeconds) { return; } EnsureLoaded(); if (Voices.Count != 0) { _lastResponseTriggerTime = num; PendingResponse pendingResponse = new PendingResponse { Command = (string.IsNullOrWhiteSpace(text3) ? string.Empty : text3), SourceKey = text2, SourcePath = text, Prog = prog, RgndCueId = rgndCueId, WaveRefIndex = waveRefIndex, PlayAt = num + Plugin.ExternalResponseDelaySeconds }; if (Plugin.ExternalResponseDelaySeconds <= 0.0001) { PlayResponseNow(pendingResponse, num); } else { PendingResponses.Add(pendingResponse); } } } private static void PlayResponseNow(PendingResponse pending, double now) { if (!_battleActive || !_externalAudioGateOpen) { return; } string text = pending.Command ?? string.Empty; int currentMood = ExternalThemeSequencer.CurrentMood; string text2 = ExternalThemeSequencer.CurrentMainPart ?? string.Empty; if (currentMood < 3) { _feverEntryUniversalPlayedThisFever = false; _feverCommandResponseCounter = 0; } string text3 = DetermineResponseIndex(currentMood, text2); _lastResponseSelectionNote = string.Empty; AudioPart audioPart = SelectResponsePart(text, pending.SourceKey, pending.SourcePath, pending.Prog, pending.RgndCueId, pending.WaveRefIndex, currentMood, text2); if (audioPart != null) { ResponsePlayer.Play(audioPart, _externalAudioGateOpen ? Plugin.ExternalResponseVolume : 0f, now, audioPart.DurationSeconds); if (currentMood >= 3 && !text3.Equals("Fever", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(text)) { _feverCommandResponseCounter++; } } } private static AudioPart? SelectResponsePart(string command, string key, string path, int prog, int rgndCueId, int waveRefIndex, int mood, string currentPart) { string responseTestCueNameText = Plugin.ResponseTestCueNameText; if (!string.IsNullOrWhiteSpace(responseTestCueNameText) && TryGetVoiceByCandidate(responseTestCueNameText, out AudioPart part)) { return part; } string text = DetermineResponseIndex(mood, currentPart); if (ShouldPlayFeverEntryUniversalResponse(mood, currentPart)) { string[] array = BuildFeverEntryUniversalCandidates(); foreach (string text2 in array) { if (TryGetVoiceByCandidate(text2, out AudioPart part2)) { _feverEntryUniversalPlayedThisFever = true; _lastResponseSelectionNote = "universal fever entry candidate=" + text2; return part2; } } _feverEntryUniversalPlayedThisFever = true; _lastResponseSelectionNote = "universal fever entry missing"; string item = "universal-fever-entry"; if (MissingCueWarnings.Add(item)) { Plugin.W("[EXT RESP] Combo-10 fever-entry response expected 00Misc/00Other/Fever.wav but no matching Fever voice was loaded."); } return null; } if (!string.IsNullOrWhiteSpace(command)) { string[] array2 = BuildCommandResponseCandidates(command, text, mood); string[] array3 = array2; foreach (string text3 in array3) { if (TryGetVoiceByCandidate(text3, out AudioPart part3)) { _lastResponseSelectionNote = $"command candidate={text3}; candidates=[{string.Join(", ", array2)}]"; return part3; } } string item2 = command + "|" + text; if (MissingCueWarnings.Add(item2)) { Plugin.W($"[EXT RESP] no custom response matched command={command}, responseIndex={text}, candidates=[{string.Join(", ", array2)}]"); } _lastResponseSelectionNote = "command missing; candidates=[" + string.Join(", ", array2) + "]"; return null; } string[] array4 = BuildLegacyResponseCandidates(text, key, path, prog, rgndCueId, waveRefIndex); string[] array5 = array4; foreach (string text4 in array5) { if (TryGetVoiceByCandidate(text4, out AudioPart part4)) { _lastResponseSelectionNote = "legacy candidate=" + text4; return part4; } } if (string.IsNullOrWhiteSpace(command) && Plugin.ExternalResponseSuppressUnmappedFallback) { string item3 = "unmapped|" + text; if (MissingCueWarnings.Add(item3)) { Plugin.W("[EXT RESP] response command is unmapped; suppressing first-loaded fallback so it does not spam Attack-01. Enable SuppressUnmappedFallback=0 only for old test behavior."); } _lastResponseSelectionNote = "unmapped suppressed"; return null; } if (Plugin.ResponsePlayFirstLoadedAsFallbackEnabled && VoiceOrder.Count > 0) { _lastResponseSelectionNote = "first-loaded fallback key=" + VoiceOrder[0]; return Voices[VoiceOrder[0]]; } string item4 = string.Join("|", array4.Take(8)); if (MissingCueWarnings.Add(item4)) { Plugin.W("[EXT RESP] no custom response matched legacy candidates=[" + string.Join(", ", array4) + "]. Command detection may need more trace data."); } _lastResponseSelectionNote = "legacy missing; candidates=[" + string.Join(", ", array4) + "]"; return null; } private static string ResolveCommandForResponse(string key, string path) { double num = Time.realtimeSinceStartup; if (Plugin.ExternalResponseCommandDetectionEnabled && !string.IsNullOrWhiteSpace(_lastDetectedCommand) && num - _lastDetectedCommandTime <= Plugin.ExternalResponseCommandMaxAgeSeconds) { return _lastDetectedCommand; } return InferCommandFromText((key ?? string.Empty) + " " + (path ?? string.Empty)); } private static string DetermineResponseIndex(int mood, string? partOverride = null) { if (mood < 3) { return "01"; } if (ShouldPlayFeverEntryUniversalResponse(mood, partOverride)) { return "Fever"; } return (_feverCommandResponseCounter % 2 == 0) ? "02" : "03"; } private static bool ShouldPlayFeverEntryUniversalResponse(int mood, string? partOverride = null) { return !_feverEntryUniversalPlayedThisFever && IsFeverEntryUniversalPart(mood, partOverride); } private static bool IsFeverEntryUniversalPart(int mood, string? partOverride = null) { if (mood < 3) { return false; } string text = partOverride ?? ExternalThemeSequencer.CurrentMainPart ?? string.Empty; if (string.IsNullOrWhiteSpace(text)) { return true; } return text.StartsWith("Combo-10", StringComparison.OrdinalIgnoreCase); } private static string[] BuildFeverEntryUniversalCandidates() { string externalResponseMiscVoiceNumberPrefix = Plugin.ExternalResponseMiscVoiceNumberPrefix; return new string[7] { "Fever", "Fever.wav", externalResponseMiscVoiceNumberPrefix + "Fever", externalResponseMiscVoiceNumberPrefix + "OtherFever", "OtherFever", "Fever-01", externalResponseMiscVoiceNumberPrefix + "Fever-01" }; } private static string[] BuildCommandResponseCandidates(string command, string responseIndex, int mood) { string selectedThemeNumberPrefix = ThemePathResolver.SelectedThemeNumberPrefix; string text = CanonicalizeCommand(command); string externalResponseMiscVoiceNumberPrefix = Plugin.ExternalResponseMiscVoiceNumberPrefix; List list = new List(); bool flag = ThemeUniqueCommands.Contains(text); bool flag2 = flag && mood >= 2; if (flag2) { AddCommandNameCandidates(list, selectedThemeNumberPrefix + text, responseIndex); AddCommandNameCandidates(list, text, responseIndex); if (mood >= 3) { foreach (string item in AlternateResponseIndices(responseIndex)) { AddCommandNameCandidates(list, selectedThemeNumberPrefix + text, item); AddCommandNameCandidates(list, text, item); } } } string index = (flag ? "01" : ((mood >= 3) ? responseIndex : "01")); AddCommandNameCandidates(list, externalResponseMiscVoiceNumberPrefix + text, index); AddCommandNameCandidates(list, text, index); if (!flag && mood >= 3 && responseIndex != "01") { AddCommandNameCandidates(list, externalResponseMiscVoiceNumberPrefix + text, "01"); AddCommandNameCandidates(list, text, "01"); } if (text.Equals("Attack", StringComparison.OrdinalIgnoreCase)) { if (flag2) { AddCommandNameCandidates(list, selectedThemeNumberPrefix + "Atack", responseIndex); AddCommandNameCandidates(list, "Atack", responseIndex); if (mood >= 3) { foreach (string item2 in AlternateResponseIndices(responseIndex)) { AddCommandNameCandidates(list, selectedThemeNumberPrefix + "Atack", item2); AddCommandNameCandidates(list, "Atack", item2); } } } AddCommandNameCandidates(list, externalResponseMiscVoiceNumberPrefix + "Atack", index); AddCommandNameCandidates(list, "Atack", index); } return list.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } [IteratorStateMachine(typeof(d__63))] private static IEnumerable AlternateResponseIndices(string responseIndex) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__63(-2) { <>3__responseIndex = responseIndex }; } private static void AddCommandNameCandidates(List candidates, string prefix, string index) { if (!string.IsNullOrWhiteSpace(prefix) && !string.IsNullOrWhiteSpace(index)) { candidates.Add(prefix + "-" + index); candidates.Add(prefix + index); candidates.Add(prefix + "_" + index); } } private static string[] BuildLegacyResponseCandidates(string responseIndex, string key, string path, int prog, int rgndCueId, int waveRefIndex) { string selectedThemeNumberPrefix = ThemePathResolver.SelectedThemeNumberPrefix; string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(key ?? string.Empty); string fileNameWithoutExtension2 = Path.GetFileNameWithoutExtension(path ?? string.Empty); string text = key ?? string.Empty; return new string[19] { text, fileNameWithoutExtension, fileNameWithoutExtension2, responseIndex, "Response-" + responseIndex, "Voice-" + responseIndex, "Kaesi-" + responseIndex, "Kaeshi-" + responseIndex, selectedThemeNumberPrefix + responseIndex, selectedThemeNumberPrefix + "Response-" + responseIndex, selectedThemeNumberPrefix + "Voice-" + responseIndex, selectedThemeNumberPrefix + "Kaesi-" + responseIndex, selectedThemeNumberPrefix + "Kaeshi-" + responseIndex, $"prog-{prog}", $"cue-{rgndCueId}", $"wave-{waveRefIndex}", $"{selectedThemeNumberPrefix}prog-{prog}", $"{selectedThemeNumberPrefix}cue-{rgndCueId}", $"{selectedThemeNumberPrefix}wave-{waveRefIndex}" }; } private static bool TryGetVoiceByCandidate(string? candidate, out AudioPart? part) { part = null; if (string.IsNullOrWhiteSpace(candidate)) { return false; } foreach (string item in CandidateKeyVariants(candidate)) { if (Voices.TryGetValue(item, out part)) { return true; } } return false; } [IteratorStateMachine(typeof(d__67))] private static IEnumerable CandidateKeyVariants(string candidate) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__67(-2) { <>3__candidate = candidate }; } private static bool TryLoadVoice(string path, bool isOgg, string label) { try { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); string partName = fileNameWithoutExtension; AudioPart audioPart; if (isOgg) { if (!AudioDecodeRunner.TryLoadOgg(path, partName, Plugin.ExternalThemeDecodeTimeoutSeconds, out AudioPart part, out string error) || part == null) { throw new InvalidDataException(FirstLine(error)); } audioPart = part; } else { audioPart = WavAudioClipLoader.LoadWav(path, partName); } AddVoiceKey(fileNameWithoutExtension, audioPart); Plugin.ThemeT($"[EXT RESP] loaded {(isOgg ? "OGG" : "WAV")} voice {fileNameWithoutExtension} from {Path.GetFileName(path)} ({label}); frames={audioPart.FrameCount}, channels={audioPart.Channels}, hz={audioPart.SampleRate}, length={audioPart.DurationSeconds:F4}s"); return true; } catch (Exception ex) { Plugin.W($"[EXT RESP] failed to load {(isOgg ? "OGG" : "WAV")} voice from {path}: {ex.Message}"); return false; } } private static string FirstLine(string text) { if (string.IsNullOrWhiteSpace(text)) { return "unknown error"; } string text2 = text.Replace("\r", ""); int num = text2.IndexOf('\n'); return (num >= 0) ? text2.Substring(0, num) : text2; } private static bool HasVoiceStem(string stem) { foreach (string item in CandidateKeyVariants(stem)) { if (Voices.ContainsKey(item)) { return true; } } return false; } private static void AddVoiceKey(string stem, AudioPart part) { string text = NormalizeVoiceKey(stem); if (!string.IsNullOrWhiteSpace(text) && !Voices.ContainsKey(text)) { Voices[text] = part; VoiceOrder.Add(text); } } private static string NormalizeVoiceKey(string text) { return ThemePathResolver.NormalizePartOrCueName(text); } private static string CanonicalizeCommand(string command) { string text = InferCommandFromText(command); return string.IsNullOrWhiteSpace(text) ? command.Trim() : text; } private static string InferCommandFromText(string? text) { if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } string text2 = text.Replace("\r", " ").Replace("\n", " ").Trim(); string haystack = text2.Replace(" ", string.Empty).Replace("\u3000", string.Empty); if (ContainsAny(haystack, "近距離攻撃", "遠距離攻撃", "攻撃")) { return "Attack"; } if (ContainsAny(haystack, "回避")) { return Plugin.ExternalResponseP1EvadeCommandMapsTo; } if (ContainsAny(haystack, "防御", "防衛", "ガード", "守備", "守る")) { return "Defend"; } if (ContainsAny(haystack, "退却", "後退", "撤退", "リトリート")) { return "Retreat"; } if (ContainsAny(haystack, "前進", "行進")) { return "March"; } if (ContainsAny(haystack, "チャージ")) { return "Charge"; } if (ContainsAny(haystack, "ジャンプ")) { return "Jump"; } if (ContainsAny(haystack, "パーティ", "パーティー")) { return "Party"; } if (ContainsAny(haystack, "退却", "後退", "撤退", "リトリート")) { return "Retreat"; } string text3 = ThemePathResolver.NormalizePartOrCueName(text); string haystack2 = text3.Replace("totaldisableattacktime", string.Empty, StringComparison.OrdinalIgnoreCase).Replace("disableattacktime", string.Empty, StringComparison.OrdinalIgnoreCase).Replace("attacktime", string.Empty, StringComparison.OrdinalIgnoreCase); if (ContainsAny(haystack2, "attack", "atack", "attak")) { return "Attack"; } if (ContainsAny(haystack2, "defend", "defence", "defense", "guard")) { return "Defend"; } if (ContainsAny(haystack2, "march", "marching")) { return "March"; } if (ContainsAny(haystack2, "charge")) { return "Charge"; } if (ContainsAny(haystack2, "jump")) { return "Jump"; } if (ContainsAny(haystack2, "party")) { return "Party"; } if (ContainsAny(haystack2, "retreat")) { return "Retreat"; } return string.Empty; } private static bool ContainsAny(string haystack, params string[] needles) { foreach (string value in needles) { if (haystack.Contains(value, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static string BuildObjectSummary(object? obj, int maxDepth, int maxChars) { try { StringBuilder stringBuilder = new StringBuilder(); HashSet seen = new HashSet(); AppendObjectSummary(stringBuilder, obj, 0, maxDepth, seen, maxChars); string text = stringBuilder.ToString().Replace("\r", " ").Replace("\n", " "); return (text.Length > maxChars) ? (text.Substring(0, maxChars) + "...") : text; } catch (Exception ex) { return "summary-error=" + ex.Message; } } private static void AppendObjectSummary(StringBuilder sb, object? obj, int depth, int maxDepth, HashSet seen, int maxChars) { if (sb.Length >= maxChars) { return; } if (obj == null) { sb.Append(""); return; } Type type = obj.GetType(); sb.Append(type.FullName).Append('{'); try { sb.Append("ToString=").Append(SafeValueText(obj)); } catch { } if (depth >= maxDepth || IsSimpleType(type)) { sb.Append('}'); return; } AppendIndexedElementsIfAny(sb, obj, depth, maxDepth, seen, maxChars); foreach (PropertyInfo item in type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Take(24)) { if (sb.Length >= maxChars) { break; } if (item.GetIndexParameters().Length == 0 && item.CanRead) { object value; try { value = item.GetValue(obj); } catch { continue; } sb.Append("; ").Append(item.Name).Append('='); AppendValueOrNested(sb, value, depth, maxDepth, seen, maxChars); } } foreach (FieldInfo item2 in type.GetFields(BindingFlags.Instance | BindingFlags.Public).Take(24)) { if (sb.Length >= maxChars) { break; } object value2; try { value2 = item2.GetValue(obj); } catch { continue; } sb.Append("; ").Append(item2.Name).Append('='); AppendValueOrNested(sb, value2, depth, maxDepth, seen, maxChars); } sb.Append('}'); } private static void AppendIndexedElementsIfAny(StringBuilder sb, object obj, int depth, int maxDepth, HashSet seen, int maxChars) { int num = TryGetCount(obj); if (num <= 0 || num > 64) { return; } Type type = obj.GetType(); string text = type.FullName ?? string.Empty; if (!text.Contains("Array", StringComparison.OrdinalIgnoreCase) && !text.Contains("List", StringComparison.OrdinalIgnoreCase) && !typeof(IEnumerable).IsAssignableFrom(type)) { return; } sb.Append("; elements=["); int num2 = Math.Min(num, 6); for (int i = 0; i < num2; i++) { if (sb.Length >= maxChars) { break; } if (i > 0) { sb.Append(" | "); } sb.Append(i).Append(':'); object value = TryGetIndexedElement(obj, i); AppendValueOrNested(sb, value, depth + 1, maxDepth, seen, maxChars); } if (num > num2) { sb.Append(" | ..."); } sb.Append(']'); } private static void AppendValueOrNested(StringBuilder sb, object? value, int depth, int maxDepth, HashSet seen, int maxChars) { if (value == null) { sb.Append(""); return; } Type type = value.GetType(); if (IsSimpleType(type)) { sb.Append(SafeValueText(value)); } else if (depth + 1 > maxDepth) { sb.Append(SafeValueText(value)); } else { AppendObjectSummary(sb, value, depth + 1, maxDepth, seen, maxChars); } } private static bool IsSimpleType(Type t) { return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal); } private static string SafeValueText(object? value) { if (value == null) { return ""; } try { string text = value.ToString(); if (string.IsNullOrWhiteSpace(text)) { return ""; } text = text.Replace("\r", " ").Replace("\n", " "); return (text.Length > 180) ? (text.Substring(0, 180) + "...") : text; } catch (Exception ex) { return "value-error=" + ex.Message; } } private static bool SameDirectory(string a, string b) { try { if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) { return false; } string a2 = Path.GetFullPath(a).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string b2 = Path.GetFullPath(b).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); return string.Equals(a2, b2, StringComparison.OrdinalIgnoreCase); } catch { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); } } } internal static class ThemePathResolver { private readonly struct ThemeFolderResolution { public string? ThemeRootFolder { get; } public string? SelectedSubFolder { get; } public string ThemeTitle { get; } public ThemeFolderResolution(string? themeRootFolder, string? selectedSubFolder, string themeTitle) { ThemeRootFolder = themeRootFolder; SelectedSubFolder = selectedSubFolder; ThemeTitle = themeTitle; } } private static string? _cachedThemeRootDirectory; private static string? _cachedPartsDirectory; private static string? _cachedVoicesDirectory; private static string? _cachedThemeTitle; public static string SelectedThemeTitle { get { if (!string.IsNullOrWhiteSpace(_cachedThemeTitle)) { return _cachedThemeTitle; } _cachedThemeTitle = ResolveThemeTitle(Plugin.ExternalThemeLibraryRootDirectory, Plugin.ExternalThemeSelectedThemeNumber); return _cachedThemeTitle; } } public static string SelectedThemeNumberPrefix => FormatThemeNumber(Plugin.ExternalThemeSelectedThemeNumber); public static void ClearCache() { _cachedThemeRootDirectory = null; _cachedPartsDirectory = null; _cachedVoicesDirectory = null; _cachedThemeTitle = null; } public static string ResolvePartsDirectory() { if (!Plugin.ExternalThemeUseNumberedThemeFolders) { return Plugin.ExternalThemeLegacyDirectory; } if (!string.IsNullOrWhiteSpace(_cachedPartsDirectory)) { return _cachedPartsDirectory; } string externalThemeLibraryRootDirectory = Plugin.ExternalThemeLibraryRootDirectory; int externalThemeSelectedThemeNumber = Plugin.ExternalThemeSelectedThemeNumber; string externalThemePartsFolderSuffix = Plugin.ExternalThemePartsFolderSuffix; ThemeFolderResolution themeFolderResolution = ResolveNumberedSubFolder(externalThemeLibraryRootDirectory, externalThemeSelectedThemeNumber, externalThemePartsFolderSuffix, "parts"); if (!string.IsNullOrWhiteSpace(themeFolderResolution.SelectedSubFolder)) { _cachedThemeRootDirectory = themeFolderResolution.ThemeRootFolder; _cachedPartsDirectory = themeFolderResolution.SelectedSubFolder; _cachedThemeTitle = themeFolderResolution.ThemeTitle; Plugin.ThemeT($"[EXT THEME] selected theme {externalThemeSelectedThemeNumber}: themeRoot='{_cachedThemeRootDirectory}', parts='{_cachedPartsDirectory}', title='{_cachedThemeTitle}'"); return _cachedPartsDirectory; } string text = FormatThemeNumber(externalThemeSelectedThemeNumber); Plugin.W($"[EXT THEME] could not resolve numbered parts folder for theme {externalThemeSelectedThemeNumber} under '{externalThemeLibraryRootDirectory}' with suffix '{externalThemePartsFolderSuffix}'. Expected preferred layout: {Path.Combine(externalThemeLibraryRootDirectory, text + "", text + externalThemePartsFolderSuffix)}. Falling back to ThemeDirectory: {Plugin.ExternalThemeLegacyDirectory}"); _cachedPartsDirectory = Plugin.ExternalThemeLegacyDirectory; _cachedThemeTitle = ResolveThemeTitle(externalThemeLibraryRootDirectory, externalThemeSelectedThemeNumber); return _cachedPartsDirectory; } public static string ResolveVoicesDirectory() { if (!Plugin.ExternalThemeUseNumberedThemeFolders) { return Path.Combine(Plugin.ExternalThemeLegacyDirectory, "Voices"); } if (!string.IsNullOrWhiteSpace(_cachedVoicesDirectory)) { return _cachedVoicesDirectory; } string externalThemeLibraryRootDirectory = Plugin.ExternalThemeLibraryRootDirectory; int externalThemeSelectedThemeNumber = Plugin.ExternalThemeSelectedThemeNumber; string externalThemeVoicesFolderSuffix = Plugin.ExternalThemeVoicesFolderSuffix; ThemeFolderResolution themeFolderResolution = ResolveNumberedSubFolder(externalThemeLibraryRootDirectory, externalThemeSelectedThemeNumber, externalThemeVoicesFolderSuffix, "voices"); if (!string.IsNullOrWhiteSpace(themeFolderResolution.SelectedSubFolder)) { _cachedThemeRootDirectory = themeFolderResolution.ThemeRootFolder; _cachedVoicesDirectory = themeFolderResolution.SelectedSubFolder; if (string.IsNullOrWhiteSpace(_cachedThemeTitle)) { _cachedThemeTitle = themeFolderResolution.ThemeTitle; } Plugin.ThemeT($"[EXT RESP] selected theme {externalThemeSelectedThemeNumber}: themeRoot='{_cachedThemeRootDirectory}', voices='{_cachedVoicesDirectory}', title='{_cachedThemeTitle}'"); return _cachedVoicesDirectory; } string cachedVoicesDirectory = ExpectedNestedSubFolder(externalThemeLibraryRootDirectory, externalThemeSelectedThemeNumber, externalThemeVoicesFolderSuffix); _cachedVoicesDirectory = cachedVoicesDirectory; Plugin.W($"[EXT RESP] could not resolve numbered voices folder for theme {externalThemeSelectedThemeNumber} under '{externalThemeLibraryRootDirectory}' with suffix '{externalThemeVoicesFolderSuffix}'. Expected preferred layout near: {_cachedVoicesDirectory}"); return _cachedVoicesDirectory; } public static string FormatThemeNumber(int number) { number = Math.Max(0, number); return (number < 100) ? number.ToString("00") : number.ToString(); } public static string NormalizePartOrCueName(string name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } Span span = stackalloc char[Math.Min(name.Length, 256)]; int num = 0; foreach (char c in name) { if (num >= span.Length) { break; } if (char.IsLetterOrDigit(c)) { span[num++] = char.ToLowerInvariant(c); } } return new string(span.Slice(0, num)); } public static string StripSelectedNumberPrefix(string name) { string selectedThemeNumberPrefix = SelectedThemeNumberPrefix; if (!string.IsNullOrWhiteSpace(name) && name.StartsWith(selectedThemeNumberPrefix, StringComparison.OrdinalIgnoreCase)) { return name.Substring(selectedThemeNumberPrefix.Length); } return name; } public static string ResolveMiscVoicesDirectory() { string externalResponseMiscVoicesDirectoryText = Plugin.ExternalResponseMiscVoicesDirectoryText; if (!string.IsNullOrWhiteSpace(externalResponseMiscVoicesDirectoryText)) { return externalResponseMiscVoicesDirectoryText; } string text = Plugin.ExternalThemeLibraryRootDirectory; if (string.IsNullOrWhiteSpace(text)) { text = Path.GetDirectoryName(Plugin.ExternalThemeLegacyDirectory) ?? string.Empty; } return Path.Combine(text, "00Misc", "00Voices"); } public static string StripLeadingNumberPrefix(string name) { if (string.IsNullOrWhiteSpace(name)) { return name; } int i; for (i = 0; i < name.Length && char.IsDigit(name[i]); i++) { } return (i > 0 && i < name.Length) ? name.Substring(i) : name; } private static ThemeFolderResolution ResolveNumberedSubFolder(string root, int number, string suffix, string label) { string suffix2 = suffix; try { if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) { return new ThemeFolderResolution(null, null, string.Empty); } string prefix = FormatThemeNumber(number); string exactSubName = prefix + (suffix2 ?? string.Empty); DirectoryInfo directoryInfo = new DirectoryInfo(root); DirectoryInfo directoryInfo2 = FindThemeRootFolder(directoryInfo, prefix); if (directoryInfo2 != null) { DirectoryInfo directoryInfo3 = directoryInfo2.EnumerateDirectories("*", SearchOption.TopDirectoryOnly).FirstOrDefault((DirectoryInfo d) => string.Equals(d.Name, exactSubName, StringComparison.OrdinalIgnoreCase)); if (directoryInfo3 != null) { return new ThemeFolderResolution(directoryInfo2.FullName, directoryInfo3.FullName, TitleFromThemeRoot(directoryInfo2.Name, prefix)); } if (!string.IsNullOrWhiteSpace(suffix2)) { DirectoryInfo directoryInfo4 = (from d in directoryInfo2.EnumerateDirectories("*", SearchOption.TopDirectoryOnly) where d.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) select d).FirstOrDefault((DirectoryInfo d) => d.Name.EndsWith(suffix2, StringComparison.OrdinalIgnoreCase)); if (directoryInfo4 != null) { return new ThemeFolderResolution(directoryInfo2.FullName, directoryInfo4.FullName, TitleFromThemeRoot(directoryInfo2.Name, prefix)); } } Plugin.W($"[EXT THEME] selected theme folder '{directoryInfo2.FullName}' has no {label} subfolder named '{exactSubName}'."); } DirectoryInfo directoryInfo5 = directoryInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly).FirstOrDefault((DirectoryInfo d) => string.Equals(d.Name, exactSubName, StringComparison.OrdinalIgnoreCase)); if (directoryInfo5 != null) { return new ThemeFolderResolution(root, directoryInfo5.FullName, ResolveThemeTitle(root, number)); } if (!string.IsNullOrWhiteSpace(suffix2)) { DirectoryInfo directoryInfo6 = (from d in directoryInfo.EnumerateDirectories(prefix + "*", SearchOption.TopDirectoryOnly) where d.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) select d).FirstOrDefault((DirectoryInfo d) => d.Name.EndsWith(suffix2, StringComparison.OrdinalIgnoreCase)); if (directoryInfo6 != null) { return new ThemeFolderResolution(root, directoryInfo6.FullName, ResolveThemeTitle(root, number)); } } return new ThemeFolderResolution(directoryInfo2?.FullName, null, (directoryInfo2 == null) ? string.Empty : TitleFromThemeRoot(directoryInfo2.Name, prefix)); } catch (Exception ex) { Plugin.W("[EXT THEME] numbered " + label + " folder scan failed: " + ex.Message); return new ThemeFolderResolution(null, null, string.Empty); } } private static DirectoryInfo? FindThemeRootFolder(DirectoryInfo rootInfo, string prefix) { string prefix2 = prefix; string partsName = prefix2 + Plugin.ExternalThemePartsFolderSuffix; string voicesName = prefix2 + Plugin.ExternalThemeVoicesFolderSuffix; return (from d in rootInfo.EnumerateDirectories(prefix2 + "*", SearchOption.TopDirectoryOnly) where d.Name.StartsWith(prefix2, StringComparison.OrdinalIgnoreCase) where !string.Equals(d.Name, partsName, StringComparison.OrdinalIgnoreCase) where !string.Equals(d.Name, voicesName, StringComparison.OrdinalIgnoreCase) where !d.Name.EndsWith(Plugin.ExternalThemePartsFolderSuffix, StringComparison.OrdinalIgnoreCase) where !d.Name.EndsWith(Plugin.ExternalThemeVoicesFolderSuffix, StringComparison.OrdinalIgnoreCase) select d).OrderBy((DirectoryInfo d) => d.Name, StringComparer.OrdinalIgnoreCase).FirstOrDefault(); } private static string ResolveThemeTitle(string root, int number) { try { if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) { return string.Empty; } string prefix = FormatThemeNumber(number); DirectoryInfo directoryInfo = FindThemeRootFolder(new DirectoryInfo(root), prefix); if (directoryInfo == null) { return string.Empty; } return TitleFromThemeRoot(directoryInfo.Name, prefix); } catch { return string.Empty; } } private static string TitleFromThemeRoot(string folderName, string prefix) { return (folderName.Length > prefix.Length) ? folderName.Substring(prefix.Length) : folderName; } private static string ExpectedNestedSubFolder(string root, int number, string suffix) { try { if (!string.IsNullOrWhiteSpace(root) && Directory.Exists(root)) { string text = FormatThemeNumber(number); DirectoryInfo directoryInfo = FindThemeRootFolder(new DirectoryInfo(root), text); if (directoryInfo != null) { return Path.Combine(directoryInfo.FullName, text + suffix); } } } catch { } string text2 = FormatThemeNumber(number); return Path.Combine(root ?? string.Empty, text2 + "", text2 + suffix); } } internal sealed class ThemeSequencerBehaviour : MonoBehaviour { public ThemeSequencerBehaviour(IntPtr ptr) : base(ptr) { } public void Update() { ThemeMenuOverlay.Update(); ExternalThemeSequencer.Update(); } public void OnGUI() { ThemeMenuOverlay.OnGUI(); } } internal static class ThemeMenuOverlay { private readonly struct ThemeMenuItem { public int Number { get; } public string Title { get; } public string FolderName { get; } public ThemeMenuItem(int number, string title, string folderName) { Number = number; Title = title; FolderName = folderName; } } private static readonly List Items = new List(); private static bool _open; private static bool _needsScan = true; private static int _selectedIndex; private static int _scrollIndex; private static string _message = string.Empty; private static double _messageUntil; private const int VisibleRows = 12; private const int RowHeight = 28; public static bool IsOpen => _open; public static void Update() { try { if (ReadKeyDown((KeyCode)287)) { _open = !_open; if (_open) { OpenMenu(); } } else { if (!_open) { return; } if (ReadKeyDown((KeyCode)27)) { _open = false; return; } if (ReadKeyDown((KeyCode)286)) { Rescan(); } if (Items.Count != 0) { if (ReadKeyDown((KeyCode)273)) { MoveSelection(-1); } else if (ReadKeyDown((KeyCode)274)) { MoveSelection(1); } else if (ReadKeyDown((KeyCode)276)) { MoveSelection(-10); } else if (ReadKeyDown((KeyCode)275)) { MoveSelection(10); } else if (ReadKeyDown((KeyCode)280)) { MoveSelection(-12); } else if (ReadKeyDown((KeyCode)281)) { MoveSelection(12); } else if (ReadKeyDown((KeyCode)278)) { SetSelection(0); } else if (ReadKeyDown((KeyCode)279)) { SetSelection(Items.Count - 1); } else if (ReadKeyDown((KeyCode)13) || ReadKeyDown((KeyCode)271)) { SelectCurrent(); } } } } catch (Exception ex) { Plugin.W("[THEME MENU] update failed: " + ex.Message); _open = false; } } public static void OnGUI() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_0273: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Unknown result type (might be due to invalid IL or missing references) if (!_open) { return; } try { float num = 670f; float num2 = 456f; Rect val = default(Rect); ((Rect)(ref val))..ctor(60f, 60f, num, num2); GUI.Box(val, "Patapon Custom Theme Menu"); float num3 = ((Rect)(ref val)).x + 18f; float num4 = ((Rect)(ref val)).y + 30f; GUI.Label(new Rect(num3, num4, num - 36f, 22f), "F6: close/open Up/Down: choose Left/Right: +/-10 Enter: select F5: rescan Esc: close"); num4 += 24f; GUI.Label(new Rect(num3, num4, num - 36f, 22f), "Themes root: " + Plugin.ExternalThemeLibraryRootDirectory); num4 += 26f; if (Items.Count == 0) { GUI.Label(new Rect(num3, num4, num - 36f, 80f), "No selectable 01-99 theme folders found. 00Misc is ignored and should remain as the support folder."); DrawMessage(num3, ((Rect)(ref val)).yMax - 28f, num - 36f); return; } int num5 = Math.Min(Items.Count, _scrollIndex + 12); Rect val2 = default(Rect); for (int i = _scrollIndex; i < num5; i++) { ThemeMenuItem themeMenuItem = Items[i]; bool flag = i == _selectedIndex; bool flag2 = themeMenuItem.Number == Plugin.ExternalThemeSelectedThemeNumber; string value = (flag ? "> " : " "); string value2 = (flag2 ? " [current]" : string.Empty); string text = $"{value}{themeMenuItem.Number:00} {themeMenuItem.Title}{value2}"; ((Rect)(ref val2))..ctor(num3, num4 + (float)((i - _scrollIndex) * 28), num - 36f, 25f); if (GUI.Button(val2, text)) { _selectedIndex = i; SelectCurrent(); break; } } int value3 = _scrollIndex / 12 + 1; int value4 = Math.Max(1, (Items.Count + 12 - 1) / 12); GUI.Label(new Rect(num3, ((Rect)(ref val)).yMax - 54f, num - 36f, 22f), $"Page {value3}/{value4} Total themes: {Items.Count}"); DrawMessage(num3, ((Rect)(ref val)).yMax - 28f, num - 36f); } catch (Exception ex) { Plugin.W("[THEME MENU] OnGUI failed: " + ex.Message); _open = false; } } private static bool ReadKeyDown(KeyCode key) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return Input.GetKeyDown(key); } private static void DrawMessage(float x, float y, float width) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrWhiteSpace(_message)) { double num = Time.realtimeSinceStartup; if (_messageUntil > 0.0 && num > _messageUntil) { _message = string.Empty; _messageUntil = 0.0; } else { GUI.Label(new Rect(x, y, width, 22f), _message); } } } private static void OpenMenu() { if (_needsScan || Items.Count == 0) { Rescan(); } else { SelectCurrentThemeInList(); } } private static void Rescan() { Items.Clear(); _needsScan = false; string externalThemeLibraryRootDirectory = Plugin.ExternalThemeLibraryRootDirectory; try { if (string.IsNullOrWhiteSpace(externalThemeLibraryRootDirectory) || !Directory.Exists(externalThemeLibraryRootDirectory)) { SetMessage("Theme root not found: " + externalThemeLibraryRootDirectory, 4.0); return; } foreach (DirectoryInfo item in new DirectoryInfo(externalThemeLibraryRootDirectory).EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) { if (TryParseThemeFolder(item.Name, out int number, out string title)) { Items.Add(new ThemeMenuItem(number, title, item.Name)); } } Items.Sort((ThemeMenuItem a, ThemeMenuItem b) => (a.Number != b.Number) ? a.Number.CompareTo(b.Number) : string.Compare(a.FolderName, b.FolderName, StringComparison.OrdinalIgnoreCase)); SelectCurrentThemeInList(); SetMessage((Items.Count == 0) ? "No selectable theme folders found." : $"Found {Items.Count} selectable themes.", 2.0); } catch (Exception ex) { SetMessage("Theme scan failed: " + ex.Message, 5.0); Plugin.W($"[THEME MENU] scan failed: {ex}"); } } private static bool TryParseThemeFolder(string folderName, out int number, out string title) { number = 0; title = string.Empty; if (string.IsNullOrWhiteSpace(folderName) || folderName.Length < 3) { return false; } if (!char.IsDigit(folderName[0]) || !char.IsDigit(folderName[1])) { return false; } if (!int.TryParse(folderName.Substring(0, 2), out number)) { return false; } if (number <= 0 || number > 99) { return false; } title = folderName.Substring(2).Trim(); if (string.IsNullOrWhiteSpace(title)) { return false; } if (title.Equals(Plugin.ExternalThemePartsFolderSuffix, StringComparison.OrdinalIgnoreCase) || title.Equals(Plugin.ExternalThemeVoicesFolderSuffix, StringComparison.OrdinalIgnoreCase)) { return false; } return true; } private static void SelectCurrentThemeInList() { int current = Plugin.ExternalThemeSelectedThemeNumber; int num = Items.FindIndex((ThemeMenuItem i) => i.Number == current); SetSelection((num >= 0) ? num : 0); } private static void MoveSelection(int delta) { SetSelection(_selectedIndex + delta); } private static void SetSelection(int index) { if (Items.Count == 0) { _selectedIndex = 0; _scrollIndex = 0; return; } _selectedIndex = Math.Clamp(index, 0, Items.Count - 1); if (_selectedIndex < _scrollIndex) { _scrollIndex = _selectedIndex; } else if (_selectedIndex >= _scrollIndex + 12) { _scrollIndex = _selectedIndex - 12 + 1; } _scrollIndex = Math.Clamp(_scrollIndex, 0, Math.Max(0, Items.Count - 12)); } private static void SelectCurrent() { if (Items.Count != 0) { ThemeMenuItem themeMenuItem = Items[_selectedIndex]; if (ExternalThemeSequencer.BattleActive) { SetMessage("Cannot change theme during an active battle. Select before starting a mission.", 4.0); } else if (ExternalThemeSequencer.SelectThemeFromMenu(themeMenuItem.Number, "overlay menu")) { SetMessage($"Selected {themeMenuItem.Number:00}: {themeMenuItem.Title}", 2.0); _open = false; } else { SetMessage($"Could not select {themeMenuItem.Number:00}: {themeMenuItem.Title}", 4.0); } } } private static void SetMessage(string message, double seconds) { _message = message; _messageUntil = (double)Time.realtimeSinceStartup + Math.Max(0.5, seconds); } } internal static class WavAudioClipLoader { public static AudioPart LoadWav(string path, string partName) { byte[] array = File.ReadAllBytes(path); if (array.Length < 44) { throw new InvalidDataException("File is too small to be a WAV file."); } if (ReadAscii(array, 0, 4) != "RIFF" || ReadAscii(array, 8, 4) != "WAVE") { throw new InvalidDataException("Expected RIFF/WAVE header."); } int num = 12; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = 0; int num6 = -1; int num7 = 0; while (num + 8 <= array.Length) { string text = ReadAscii(array, num, 4); int num8 = ReadInt32LE(array, num + 4); int num9 = num + 8; if (num8 < 0 || num9 + num8 > array.Length) { throw new InvalidDataException("Invalid WAV chunk '" + text + "'."); } if (text == "fmt ") { if (num8 < 16) { throw new InvalidDataException("Invalid fmt chunk."); } num2 = ReadInt16LE(array, num9); num3 = ReadInt16LE(array, num9 + 2); num4 = ReadInt32LE(array, num9 + 4); num5 = ReadInt16LE(array, num9 + 14); } else if (text == "data") { num6 = num9; num7 = num8; } num = num9 + num8; if (((uint)num & (true ? 1u : 0u)) != 0) { num++; } } if (num3 <= 0 || num4 <= 0 || num5 <= 0) { throw new InvalidDataException("Missing or invalid fmt chunk."); } if (num6 < 0 || num7 <= 0) { throw new InvalidDataException("Missing data chunk."); } int num10 = num5 / 8; if (num10 <= 0) { throw new InvalidDataException($"Unsupported bit depth: {num5}"); } int num11 = num7 / num10; int frameCount = num11 / num3; float[] array2 = new float[num11]; bool isPcm = num2 == 1; bool isFloat = num2 == 3; for (int i = 0; i < num11; i++) { int p = num6 + i * num10; array2[i] = DecodeSample(array, p, num5, isPcm, isFloat); } return new AudioPart(partName, array2, frameCount, num3, num4); } private static float DecodeSample(byte[] b, int p, int bits, bool isPcm, bool isFloat) { if (isFloat && bits == 32) { return BitConverter.ToSingle(b, p); } if (!isPcm) { throw new InvalidDataException("Unsupported WAV format. Only PCM and 32-bit float WAV are supported."); } if (1 == 0) { } float result = bits switch { 8 => (float)(b[p] - 128) / 128f, 16 => (float)ReadInt16LE(b, p) / 32768f, 24 => (float)ReadInt24LE(b, p) / 8388608f, 32 => (float)ReadInt32LE(b, p) / 2.1474836E+09f, _ => throw new InvalidDataException($"Unsupported PCM bit depth: {bits}"), }; if (1 == 0) { } return result; } private static string ReadAscii(byte[] b, int offset, int count) { return Encoding.ASCII.GetString(b, offset, count); } private static short ReadInt16LE(byte[] b, int p) { return (short)(b[p] | (b[p + 1] << 8)); } private static int ReadInt24LE(byte[] b, int p) { int num = b[p] | (b[p + 1] << 8) | (b[p + 2] << 16); if (((uint)num & 0x800000u) != 0) { num |= -16777216; } return num; } private static int ReadInt32LE(byte[] b, int p) { return b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24); } } internal static class OggAudioClipLoader { public static AudioPart LoadOgg(string path, string partName) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown using FileStream fileStream = File.OpenRead(path); VorbisReader val = new VorbisReader((Stream)fileStream, false); try { int channels = val.Channels; int sampleRate = val.SampleRate; if (channels <= 0) { throw new InvalidDataException($"Invalid OGG channel count: {channels}"); } if (sampleRate <= 0) { throw new InvalidDataException($"Invalid OGG sample rate: {sampleRate}"); } List list = new List(EstimateCapacity(val, channels)); float[] array = new float[Math.Max(4096, sampleRate * channels / 4)]; while (true) { int num = val.ReadSamples(array, 0, array.Length); if (num <= 0) { break; } for (int i = 0; i < num; i++) { list.Add(array[i]); } } if (list.Count == 0) { throw new InvalidDataException("NVorbis decoded zero samples."); } int num2 = list.Count / channels; int num3 = num2 * channels; if (num3 <= 0) { throw new InvalidDataException("Decoded OGG did not contain a full sample frame."); } float[] array2 = new float[num3]; for (int j = 0; j < num3; j++) { array2[j] = list[j]; } return new AudioPart(partName, array2, num2, channels, sampleRate); } finally { ((IDisposable)val)?.Dispose(); } } private static int EstimateCapacity(VorbisReader reader, int channels) { try { long totalSamples = reader.TotalSamples; long num = totalSamples * Math.Max(1, channels); if (num > 0 && num < int.MaxValue) { return (int)num; } } catch { } return 262144; } } internal static class AudioDecodeRunner { public static bool TryLoadOgg(string path, string partName, double timeoutSeconds, out AudioPart? part, out string error) { string path2 = path; string partName2 = partName; part = null; error = string.Empty; if (!Plugin.ExternalThemeUseDecodeTimeout) { try { part = OggAudioClipLoader.LoadOgg(path2, partName2); return true; } catch (Exception ex) { error = ex.ToString(); return false; } } Task task; try { task = Task.Run(() => OggAudioClipLoader.LoadOgg(path2, partName2)); } catch (Exception ex2) { error = "Could not start OGG decoder task: " + ex2; return false; } try { if (!task.Wait(TimeSpan.FromSeconds(timeoutSeconds))) { ObserveFaultLater(task); error = $"OGG decode timed out after {timeoutSeconds:F2}s. The worker thread may still finish later, but this part was skipped so the game can continue."; return false; } part = task.Result; return true; } catch (AggregateException ex3) { Exception baseException = ex3.GetBaseException(); error = baseException.ToString(); return false; } catch (Exception ex4) { error = ex4.ToString(); return false; } } private static void ObserveFaultLater(Task task) { try { task.ContinueWith(delegate(Task t) { _ = t.Exception; }, TaskContinuationOptions.OnlyOnFaulted); } catch { } } } internal sealed class AudioPart { public string Name { get; } public float[] Samples { get; } public int FrameCount { get; } public int Channels { get; } public int SampleRate { get; } public double DurationSeconds { get; } public AudioPart(string name, float[] interleavedSamples, int frameCount, int channels, int sampleRate) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Audio part name is empty.", "name"); } if (interleavedSamples == null || interleavedSamples.Length == 0) { throw new ArgumentException("Audio part sample buffer is empty.", "interleavedSamples"); } if (frameCount <= 0) { throw new ArgumentOutOfRangeException("frameCount"); } if (channels <= 0) { throw new ArgumentOutOfRangeException("channels"); } if (sampleRate <= 0) { throw new ArgumentOutOfRangeException("sampleRate"); } if (interleavedSamples.Length < frameCount * channels) { throw new ArgumentException("Audio part sample buffer is shorter than frameCount * channels.", "interleavedSamples"); } Name = name; Samples = interleavedSamples; FrameCount = frameCount; Channels = channels; SampleRate = sampleRate; DurationSeconds = (double)frameCount / (double)sampleRate; } } internal sealed class ManagedNaudioPlayer : IDisposable { private sealed class FloatPcm16WaveProvider : IWaveProvider { private readonly float[] _samples; private int _position; public WaveFormat WaveFormat { get; } public float Volume { get; set; } = 1f; public bool Finished => _position >= _samples.Length; public FloatPcm16WaveProvider(float[] samples, int sampleRate, int channels, double startOffsetSeconds = 0.0) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown _samples = samples ?? throw new ArgumentNullException("samples"); WaveFormat = new WaveFormat(sampleRate, 16, channels); if (startOffsetSeconds > 0.0001 && sampleRate > 0 && channels > 0) { long num = (long)Math.Round(startOffsetSeconds * (double)sampleRate) * channels; if (num > 0) { _position = (int)Math.Min(_samples.Length, num); } } } public int Read(byte[] buffer, int offset, int count) { int num = 0; int num2 = count / 2; float volume = Volume; while (num < count && num2 > 0) { short num4; if (_position < _samples.Length) { float num3 = _samples[_position++] * volume; if (num3 > 1f) { num3 = 1f; } else if (num3 < -1f) { num3 = -1f; } num4 = (short)Math.Round(num3 * 32767f); } else { num4 = 0; } buffer[offset + num] = (byte)((uint)num4 & 0xFFu); buffer[offset + num + 1] = (byte)((uint)(num4 >> 8) & 0xFFu); num += 2; num2--; } if (num < count) { buffer[offset + num] = 0; num++; } return num; } } private readonly object _lock = new object(); private WaveOutEvent? _output; private FloatPcm16WaveProvider? _provider; private double _startedAtUnityTime; private double _expectedEndUnityTime; private string _currentPart = string.Empty; private bool _disposed; public bool IsPlaying { get { lock (_lock) { return _output != null && _provider != null && !_provider.Finished; } } } public bool HasReachedEnd(double now) { lock (_lock) { if (_output == null || _provider == null) { return true; } if (_expectedEndUnityTime > 0.0) { return now >= _expectedEndUnityTime; } return _provider.Finished; } } public bool AdjustExpectedEnd(double expectedEndUnityTime) { lock (_lock) { if (_output == null || _provider == null) { return false; } _expectedEndUnityTime = Math.Max(_startedAtUnityTime + 0.01, expectedEndUnityTime); return true; } } public bool DelayExpectedEnd(double delaySeconds) { if (delaySeconds <= 0.0001) { return false; } lock (_lock) { if (_output == null || _provider == null || _expectedEndUnityTime <= 0.0) { return false; } _expectedEndUnityTime += delaySeconds; return true; } } public double SecondsUntilExpectedEnd(double now) { lock (_lock) { if (_output == null || _provider == null || _expectedEndUnityTime <= 0.0) { return 0.0; } return _expectedEndUnityTime - now; } } public void Play(AudioPart part, float volume, double now, double expectedDurationSeconds) { PlayUntil(part, volume, now, now + Math.Max(0.01, expectedDurationSeconds)); } public void PlayUntil(AudioPart part, float volume, double now, double expectedEndUnityTime) { PlayUntil(part, volume, now, expectedEndUnityTime, 0.0); } public void PlayUntil(AudioPart part, float volume, double now, double expectedEndUnityTime, double startOffsetSeconds) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Expected O, but got Unknown lock (_lock) { ThrowIfDisposed(); StopLocked(); _provider = new FloatPcm16WaveProvider(part.Samples, part.SampleRate, part.Channels, startOffsetSeconds) { Volume = Math.Max(0f, volume) }; _output = new WaveOutEvent { DesiredLatency = 60, NumberOfBuffers = 3 }; _output.Init((IWaveProvider)(object)_provider); _output.Play(); _currentPart = part.Name; _startedAtUnityTime = now; _expectedEndUnityTime = Math.Max(now + 0.01, expectedEndUnityTime); } } public void SetVolume(float volume) { lock (_lock) { if (_provider != null) { _provider.Volume = Math.Max(0f, volume); } } } public void Stop() { lock (_lock) { StopLocked(); } } private void StopLocked() { try { WaveOutEvent? output = _output; if (output != null) { output.Stop(); } } catch { } try { WaveOutEvent? output2 = _output; if (output2 != null) { output2.Dispose(); } } catch { } _output = null; _provider = null; _currentPart = string.Empty; _startedAtUnityTime = 0.0; _expectedEndUnityTime = 0.0; } private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException("ManagedNaudioPlayer"); } } public void Dispose() { lock (_lock) { if (!_disposed) { StopLocked(); _disposed = true; } } } } }