Phase 6 + GUI: Distribution and in-game audio mixer

Added:
- GUI/AudioMixerGUI.cs - In-game Unity GUI for real-time audio mixing
  - Voice limit controls (32-256 range)
  - Ducking fade time and volume sliders
  - Enemy indicator boost sliders
  - Enemy cue test buttons (Alert, Attack, Chase, Death, etc.)
  - Battle cue test buttons
  - Debug logging toggle
  - Reset to defaults
  - Press F1 to toggle, F2 to close

Updated:
- Plugin.cs - Initializes AudioMixerGUI on startup
- HomuraHimeAudioMod.csproj - Removed failed ImGui deps, using Unity native GUI

Added:
- docs/EXISTING_TOOLS.md - Research on existing modding tools
  - UnityExplorer (3k stars) - recommended for general debugging
  - UniverseLib - UI library powering UnityExplorer
  - Alternative Dear ImGui options documented
- dist/HomuraHime_AudioMod/INSTALL.md - Distribution package README

Recommendation: Use BOTH UnityExplorer (F5) for general debugging + our AudioMixerGUI (F1) for audio-specific controls
This commit is contained in:
2026-03-22 13:49:59 -04:00
parent 2f874a89e4
commit b589d39ccd
5 changed files with 646 additions and 9 deletions

View File

@@ -0,0 +1,428 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace HomuraHimeAudioMod.GUI
{
public class AudioMixerGUI : MonoBehaviour
{
private static AudioMixerGUI instance;
public static AudioMixerGUI Instance => instance;
private bool isVisible = false;
private Vector2 scrollPosition;
private float margin = 10f;
private float labelWidth = 140f;
private float sliderWidth = 200f;
private float buttonHeight = 25f;
private float lineHeight = 30f;
private float windowWidth = 380f;
private float windowHeight = 520f;
private string[] enemyCueNames = new string[]
{
"Enemy_Idle",
"Enemy_Alert",
"Enemy_Attack",
"Enemy_Chase",
"Enemy_Death",
"Enemy_Damage_Taken",
"Enemy_Special"
};
private string[] battleCueNames = new string[]
{
"Battle_Start",
"Battle_Victory",
"Player_Attack",
"Player_Dodge",
"Player_Block",
"Player_Critical"
};
private Rect windowRect
{
get => new Rect(margin, margin, windowWidth, windowHeight);
}
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
Plugin.Log.LogInfo("AudioMixerGUI initialized");
}
else
{
Destroy(gameObject);
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.F1))
{
isVisible = !isVisible;
Plugin.Log.LogDebug($"Audio Mixer GUI: {(isVisible ? "VISIBLE" : "HIDDEN")}");
}
if (Input.GetKeyDown(KeyCode.F2))
{
isVisible = false;
}
}
private void OnGUI()
{
if (!isVisible) return;
GUI.skin = CreateCustomSkin();
windowRect.x = Mathf.Clamp(windowRect.x, 0, Screen.width - windowWidth - 10);
windowRect.y = Mathf.Clamp(windowRect.y, 0, Screen.height - windowHeight - 10);
windowRect = GUI.Window(0, windowRect, DrawMainWindow, "HomuraHime Audio Mixer (F1: Toggle | F2: Close)");
}
private GUISkin CreateCustomSkin()
{
GUISkin skin = GUI.skin;
GUIStyle headerStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 14,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter
};
GUIStyle sectionStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 12,
fontStyle = FontStyle.Bold,
normal = { textColor = new Color(0.9f, 0.7f, 0.3f) }
};
GUIStyle buttonStyle = new GUIStyle(GUI.skin.button)
{
fontSize = 11,
fontStyle = FontStyle.Normal
};
return skin;
}
private void DrawMainWindow(int windowID)
{
GUI.DragWindow(new Rect(0, 0, windowWidth - 20, 25));
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
GUILayout.Space(margin);
DrawHeader();
GUILayout.Space(margin);
DrawVoiceControls();
GUILayout.Space(10);
DrawDuckingControls();
GUILayout.Space(10);
DrawEnemyAudioControls();
GUILayout.Space(10);
DrawEnemyCueTestButtons();
GUILayout.Space(10);
DrawBattleCueTestButtons();
GUILayout.Space(10);
DrawDebugControls();
GUILayout.Space(margin);
GUILayout.EndScrollView();
GUILayout.BeginHorizontal();
GUILayout.Label($"v1.0.0 | F1: Toggle | F2: Close", GUILayout.Height(20));
GUILayout.EndHorizontal();
}
private void DrawHeader()
{
GUILayout.BeginHorizontal();
GUILayout.Label("=== HOMURAHIME AUDIO MIXER ===", GUILayout.Height(25));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label($"Voice Count: {GetCurrentVoiceCount()} | Max: {ModConfig.MaxVoiceCount.Value}", GUILayout.Height(20));
GUILayout.EndHorizontal();
}
private void DrawVoiceControls()
{
GUILayout.BeginVertical("box");
GUILayout.Label("VOICE MANAGER", GUILayout.Height(20));
GUILayout.BeginHorizontal();
GUILayout.Label("Max Voices:", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.MaxVoiceCount.Value = (int)GUILayout.HorizontalSlider(
ModConfig.MaxVoiceCount.Value, 32, 256,
GUILayout.Width(sliderWidth), GUILayout.Height(lineHeight));
GUILayout.Label($"{ModConfig.MaxVoiceCount.Value}", GUILayout.Width(40));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label($"Voice Limit Fix: ", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.EnableVoiceLimitFix.Value = GUILayout.Toggle(
ModConfig.EnableVoiceLimitFix.Value, "Enabled");
GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
private void DrawDuckingControls()
{
GUILayout.BeginVertical("box");
GUILayout.Label("AUDIO DUCKING", GUILayout.Height(20));
GUILayout.BeginHorizontal();
GUILayout.Label("Duck Fade Time:", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.DuckFadeTime.Value = GUILayout.HorizontalSlider(
ModConfig.DuckFadeTime.Value, 0.01f, 0.3f,
GUILayout.Width(sliderWidth), GUILayout.Height(lineHeight));
GUILayout.Label($"{ModConfig.DuckFadeTime.Value:F3}s", GUILayout.Width(50));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("Duck Volume:", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.DuckVolume.Value = GUILayout.HorizontalSlider(
ModConfig.DuckVolume.Value, 0.1f, 0.8f,
GUILayout.Width(sliderWidth), GUILayout.Height(lineHeight));
GUILayout.Label($"{ModConfig.DuckVolume.Value:F2}", GUILayout.Width(50));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label($"Ducking Fix: ", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.EnableDuckingFix.Value = GUILayout.Toggle(
ModConfig.EnableDuckingFix.Value, "Enabled");
GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
private void DrawEnemyAudioControls()
{
GUILayout.BeginVertical("box");
GUILayout.Label("ENEMY AUDIO INDICATORS", GUILayout.Height(20));
GUILayout.BeginHorizontal();
GUILayout.Label("Indicator Boost:", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.EnemyIndicatorBoost.Value = GUILayout.HorizontalSlider(
ModConfig.EnemyIndicatorBoost.Value, 0.5f, 2.0f,
GUILayout.Width(sliderWidth), GUILayout.Height(lineHeight));
GUILayout.Label($"{ModConfig.EnemyIndicatorBoost.Value:F2}x", GUILayout.Width(50));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("Alert Boost:", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.AlertSoundBoost.Value = GUILayout.HorizontalSlider(
ModConfig.AlertSoundBoost.Value, 0.5f, 2.0f,
GUILayout.Width(sliderWidth), GUILayout.Height(lineHeight));
GUILayout.Label($"{ModConfig.AlertSoundBoost.Value:F2}x", GUILayout.Width(50));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("Attack Boost:", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.AttackSoundBoost.Value = GUILayout.HorizontalSlider(
ModConfig.AttackSoundBoost.Value, 0.5f, 2.0f,
GUILayout.Width(sliderWidth), GUILayout.Height(lineHeight));
GUILayout.Label($"{ModConfig.AttackSoundBoost.Value:F2}x", GUILayout.Width(50));
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label($"Enemy Audio: ", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.EnableEnemyAudioBoost.Value = GUILayout.Toggle(
ModConfig.EnableEnemyAudioBoost.Value, "Enabled");
GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
private void DrawEnemyCueTestButtons()
{
GUILayout.BeginVertical("box");
GUILayout.Label("TEST ENEMY CUES (Debug)", GUILayout.Height(20));
GUILayout.BeginHorizontal();
for (int i = 0; i < enemyCueNames.Length; i++)
{
string cueName = enemyCueNames[i];
if (GUILayout.Button(cueName, GUILayout.Height(buttonHeight)))
{
TestEnemyCue(cueName);
}
if ((i + 1) % 2 == 0) GUILayout.EndHorizontal();
if ((i + 1) % 2 != 0 && i < enemyCueNames.Length - 1) GUILayout.BeginHorizontal();
}
if (enemyCueNames.Length % 2 != 0) GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
private void DrawBattleCueTestButtons()
{
GUILayout.BeginVertical("box");
GUILayout.Label("TEST BATTLE CUES (Debug)", GUILayout.Height(20));
GUILayout.BeginHorizontal();
for (int i = 0; i < battleCueNames.Length; i++)
{
string cueName = battleCueNames[i];
if (GUILayout.Button(cueName, GUILayout.Height(buttonHeight)))
{
TestBattleCue(cueName);
}
if ((i + 1) % 2 == 0) GUILayout.EndHorizontal();
if ((i + 1) % 2 != 0 && i < battleCueNames.Length - 1) GUILayout.BeginHorizontal();
}
if (battleCueNames.Length % 2 != 0) GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
private void DrawDebugControls()
{
GUILayout.BeginVertical("box");
GUILayout.Label("DEBUG OPTIONS", GUILayout.Height(20));
GUILayout.BeginHorizontal();
GUILayout.Label($"Debug Logging: ", GUILayout.Width(labelWidth), GUILayout.Height(lineHeight));
ModConfig.EnableDebugLogging.Value = GUILayout.Toggle(
ModConfig.EnableDebugLogging.Value, "Enabled");
GUILayout.EndHorizontal();
if (GUILayout.Button("Log Current Config", GUILayout.Height(buttonHeight)))
{
LogCurrentConfig();
}
if (GUILayout.Button("Reset to Defaults", GUILayout.Height(buttonHeight)))
{
ResetToDefaults();
}
GUILayout.EndVertical();
}
private int GetCurrentVoiceCount()
{
try
{
var voiceManagerType = FindTypeByName("FmodVoiceManager");
if (voiceManagerType != null)
{
var instance = AccessTools.Property(voiceManagerType, "Instance")?.GetValue(null);
if (instance != null)
{
var countProp = AccessTools.Property(voiceManagerType, "CountVoice");
if (countProp != null)
{
return (int)countProp.GetValue(instance);
}
}
}
}
catch { }
return -1;
}
private Type FindTypeByName(string name)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.Name.Contains(name))
return type;
}
}
return null;
}
private void TestEnemyCue(string cueName)
{
Plugin.Log.LogDebug($"[GUI] Testing enemy cue: {cueName}");
try
{
var cfVoiceType = FindTypeByName("CFVoiceEventUtility");
if (cfVoiceType != null)
{
var playMethod = AccessTools.Method(cfVoiceType, "PlayCFVoice");
if (playMethod != null)
{
playMethod.Invoke(null, new object[] { cueName });
Plugin.Log.LogDebug($"[GUI] Triggered enemy cue: {cueName}");
}
}
}
catch (Exception ex)
{
Plugin.Log.LogWarning($"[GUI] Could not trigger enemy cue: {ex.Message}");
}
}
private void TestBattleCue(string cueName)
{
Plugin.Log.LogDebug($"[GUI] Testing battle cue: {cueName}");
try
{
var cfVoiceType = FindTypeByName("CFVoiceEventUtility");
if (cfVoiceType != null)
{
var playMethod = AccessTools.Method(cfVoiceType, "PlayCFVoice");
if (playMethod != null)
{
playMethod.Invoke(null, new object[] { cueName });
Plugin.Log.LogDebug($"[GUI] Triggered battle cue: {cueName}");
}
}
}
catch (Exception ex)
{
Plugin.Log.LogWarning($"[GUI] Could not trigger battle cue: {ex.Message}");
}
}
private void LogCurrentConfig()
{
Plugin.Log.LogInfo("=== CURRENT AUDIO MOD CONFIG ===");
Plugin.Log.LogInfo($"MaxVoiceCount: {ModConfig.MaxVoiceCount.Value}");
Plugin.Log.LogInfo($"DuckFadeTime: {ModConfig.DuckFadeTime.Value}");
Plugin.Log.LogInfo($"DuckVolume: {ModConfig.DuckVolume.Value}");
Plugin.Log.LogInfo($"EnemyIndicatorBoost: {ModConfig.EnemyIndicatorBoost.Value}");
Plugin.Log.LogInfo($"AlertSoundBoost: {ModConfig.AlertSoundBoost.Value}");
Plugin.Log.LogInfo($"AttackSoundBoost: {ModConfig.AttackSoundBoost.Value}");
Plugin.Log.LogInfo($"EnableVoiceLimitFix: {ModConfig.EnableVoiceLimitFix.Value}");
Plugin.Log.LogInfo($"EnableDuckingFix: {ModConfig.EnableDuckingFix.Value}");
Plugin.Log.LogInfo($"EnableEnemyAudioBoost: {ModConfig.EnableEnemyAudioBoost.Value}");
Plugin.Log.LogInfo($"EnableDebugLogging: {ModConfig.EnableDebugLogging.Value}");
Plugin.Log.LogInfo("===============================");
}
private void ResetToDefaults()
{
ModConfig.MaxVoiceCount.Value = 128;
ModConfig.DuckFadeTime.Value = 0.05f;
ModConfig.DuckVolume.Value = 0.3f;
ModConfig.EnemyIndicatorBoost.Value = 1.2f;
ModConfig.AlertSoundBoost.Value = 1.3f;
ModConfig.AttackSoundBoost.Value = 1.15f;
ModConfig.EnableVoiceLimitFix.Value = true;
ModConfig.EnableDuckingFix.Value = true;
ModConfig.EnableEnemyAudioBoost.Value = true;
ModConfig.EnableDebugLogging.Value = false;
Plugin.Log.LogInfo("[GUI] Config reset to defaults");
}
}
}