diff --git a/dist/HomuraHime_AudioMod/INSTALL.md b/dist/HomuraHime_AudioMod/INSTALL.md new file mode 100644 index 0000000..a269eb5 --- /dev/null +++ b/dist/HomuraHime_AudioMod/INSTALL.md @@ -0,0 +1,127 @@ +# HomuraHime Audio Mod - Installation Guide + +## Quick Install + +### Prerequisites +- HomuraHime on Steam +- BepInEx 5.x + +### Steps + +1. **Backup Original Files** (optional but recommended) + ``` + Copy HomuraHime_Data\StreamingAssets\FMOD\Desktop\*.bank to backup folder + ``` + +2. **Install BepInEx** (if not already installed) + - Download BepInEx 5.x from: https://github.com/BepInEx/BepInEx/releases + - Extract to game folder: `C:\apps\steam\steamapps\common\Homura Hime\` + - Launch game once, then close + +3. **Copy Mod Files** + ``` + Copy BepInEx\plugins\HomuraHimeAudioMod.dll + To: C:\apps\steam\steamapps\common\Homura Hime\BepInEx\plugins\ + ``` + +4. **Launch Game** + ``` + Start HomuraHime via Steam + Check BepInEx\LogOutput.log for: "HomuraHime Audio Mod initialized successfully" + ``` + +--- + +## Mod Components + +### Required (BepInEx Plugin) +- `BepInEx/plugins/HomuraHimeAudioMod.dll` - Main mod DLL + +### Optional (FMOD Banks) +- `FMOD_banks/Desktop/*.bank` - Modified audio banks (if included) + +### Documentation +- `docs/` - Full documentation +- `README.md` - This file + +--- + +## Configuration + +After first launch, configure in: +``` +C:\apps\steam\steamapps\common\Homura Hime\BepInEx\config\com.homurahime.audiomod.cfg +``` + +### Recommended Settings + +**For Low-End Systems:** +```ini +[VoiceManager] +MaxVoiceCount = 64 + +[Ducking] +DuckFadeTime = 0.1 +``` + +**For High-End Systems:** +```ini +[VoiceManager] +MaxVoiceCount = 256 + +[EnemyAudio] +EnemyIndicatorBoost = 1.5 +AlertSoundBoost = 1.6 +AttackSoundBoost = 1.4 +``` + +--- + +## Troubleshooting + +### Mod Not Loading +1. Check `BepInEx\LogOutput.log` +2. Ensure DLL is in `BepInEx\plugins\` (not a subfolder) +3. Verify .NET Framework 4.8 is installed + +### Audio Glitches Persist +1. Reduce `MaxVoiceCount` to 64 +2. Increase `DuckFadeTime` to 0.1 +3. Disable FMOD banks modification if using + +### Game Crashes +1. Disable mod temporarily +2. Test with default settings +3. Check for conflicting mods + +--- + +## Uninstall + +1. Delete `BepInEx\plugins\HomuraHimeAudioMod.dll` +2. Restore original FMOD banks from backup (if modified) +3. Delete config: `BepInEx\config\com.homurahime.audiomod.cfg` + +--- + +## Version History + +### v1.0.0 +- Initial release +- Voice limit fix (configurable 64-256) +- Audio ducking improvements +- Enemy audio indicator boost + +--- + +## Credits + +- Mod by: [Your Name] +- Framework: BepInEx +- Audio Engine: FMOD Studio + +--- + +## License + +For personal use only. Please respect game copyright. diff --git a/docs/EXISTING_TOOLS.md b/docs/EXISTING_TOOLS.md new file mode 100644 index 0000000..128f1a9 --- /dev/null +++ b/docs/EXISTING_TOOLS.md @@ -0,0 +1,74 @@ +# Existing Modding Tools for HomuraHime + +## Recommended Existing Tools + +### 1. UnityExplorer (3k stars) +**Purpose:** In-game UI for exploring, debugging, and modifying Unity games + +**Features:** +- Object Explorer - browse scenes, find audio objects +- Inspector - view/edit component values in real-time +- Hook Manager - create Harmony patches at runtime (no rebuild needed) +- C# Console - execute code live +- Mouse Inspect - click objects to inspect them + +**Download:** +- BepInEx 5.x Mono: https://github.com/sinai-dev/UnityExplorer/releases/download/UnityExplorer.BepInEx5.Mono.zip + +**Installation:** +``` +1. Download UnityExplorer.BepInEx5.Mono.zip +2. Extract to: C:\apps\steam\steamapps\common\Homura Hime\BepInEx\plugins\ +3. Launch game - press F5 to open UnityExplorer menu +``` + +### 2. UniverseLib (129 stars) +**Purpose:** UI library for Unity mod plugins + +**NuGet:** +- Mono: `rainbowblood.UniverseLib.Mono` +- IL2CPP: `rainbowblood.UniverseLib.IL2CPP` + +Note: UniverseLib is included with UnityExplorer, no separate install needed. + +--- + +## Why Keep Our Custom AudioMixerGUI? + +While UnityExplorer is excellent for general debugging, our **AudioMixerGUI** provides: + +| Feature | UnityExplorer | Our AudioMixerGUI | +|---------|---------------|-------------------| +| Audio-specific sliders | No | Yes | +| One-click enemy cue testing | No | Yes | +| Battle cue testing | No | Yes | +| Voice count monitoring | Manual | Automatic | +| Audio presets | No | Yes | + +**Recommendation:** Use **BOTH** +- UnityExplorer for general debugging and object browsing +- Our AudioMixerGUI for focused audio parameter tweaking and cue testing + +--- + +## Alternative: Dear ImGui (Future Option) + +If you prefer Dear ImGui-style UI, consider: + +1. **MelonImGui** - https://github.com/plusno69/MelonImGui +2. **ui_imgui** - https://github.com/nk-mermaid/ui_imgui + +These require additional dependencies but provide modern ImGui-style UI. + +**Trade-off:** More work to integrate, but looks more professional. + +--- + +## Recommended Setup + +1. **Install UnityExplorer** (for general modding/debugging) +2. **Keep our AudioMixerGUI** (for audio-specific controls) +3. **Press F1** to toggle our audio mixer +4. **Press F5** to toggle UnityExplorer + +This gives you the best of both worlds - professional debugging tools + dedicated audio mixer. diff --git a/src/HomuraHimeAudioMod/GUI/AudioMixerGUI.cs b/src/HomuraHimeAudioMod/GUI/AudioMixerGUI.cs new file mode 100644 index 0000000..814a856 --- /dev/null +++ b/src/HomuraHimeAudioMod/GUI/AudioMixerGUI.cs @@ -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"); + } + } +} diff --git a/src/HomuraHimeAudioMod/HomuraHimeAudioMod.csproj b/src/HomuraHimeAudioMod/HomuraHimeAudioMod.csproj index 6d8112e..c2a06fe 100644 --- a/src/HomuraHimeAudioMod/HomuraHimeAudioMod.csproj +++ b/src/HomuraHimeAudioMod/HomuraHimeAudioMod.csproj @@ -18,12 +18,16 @@ - - - + + runtime + + + $(GamePath)\..\BepInEx\core\BepInEx.dll + false + $(GamePath)\FMODUnity.dll false diff --git a/src/HomuraHimeAudioMod/Plugin.cs b/src/HomuraHimeAudioMod/Plugin.cs index 2592435..521b914 100644 --- a/src/HomuraHimeAudioMod/Plugin.cs +++ b/src/HomuraHimeAudioMod/Plugin.cs @@ -1,6 +1,7 @@ using BepInEx; using BepInEx.Logging; using HarmonyLib; +using UnityEngine; namespace HomuraHimeAudioMod { @@ -34,7 +35,10 @@ namespace HomuraHimeAudioMod ApplyPatches(); + InitializeGUI(); + Log.LogInfo("HomuraHime Audio Mod initialized successfully"); + Log.LogInfo("Press F1 to open Audio Mixer GUI"); } private void ApplyPatches() @@ -60,15 +64,15 @@ namespace HomuraHimeAudioMod Log.LogInfo("All patches applied successfully"); } - private void Update() + private void InitializeGUI() { - if (ModConfig.EnableDebugLogging.Value) - { - DebugLogUpdate(); - } + GameObject guiObject = new GameObject("HomuraHimeAudioMixer"); + guiObject.AddComponent(); + DontDestroyOnLoad(guiObject); + Log.LogInfo("Audio Mixer GUI initialized"); } - private void DebugLogUpdate() + private void Update() { }