Compare commits

..

3 Commits

Author SHA1 Message Date
Ed_
b589d39ccd 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
2026-03-22 13:49:59 -04:00
Ed_
2f874a89e4 Phase 5: Add testing documentation
- Created docs/TESTING_PHASE5.md with test scenarios and metrics
- Includes pre-testing checklist
- 5 test scenarios from quiet exploration to swarm enemies
- Glitch rating scale and configuration tuning guide
- Test report template for iteration
2026-03-22 13:46:19 -04:00
Ed_
a6fa90375e Phase 4: BepInEx plugin complete
- Added ModConfig.cs with full configuration system
- Updated Plugin.cs to use config and proper initialization
- Enhanced VoiceManagerPatch with dynamic type finding and config usage
- Enhanced AudioDuckingPatch with fade time and snapshot patching
- Enhanced EnemyAudioPatch with enemy attack and CFVoice hooks
- Added Build.ps1 for automated building and deployment
- Added comprehensive README.md with installation and configuration
2026-03-22 13:45:32 -04:00
12 changed files with 1649 additions and 103 deletions

127
dist/HomuraHime_AudioMod/INSTALL.md vendored Normal file
View File

@@ -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.

74
docs/EXISTING_TOOLS.md Normal file
View File

@@ -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.

297
docs/TESTING_PHASE5.md Normal file
View File

@@ -0,0 +1,297 @@
# Phase 5: Testing & Iteration Guide
## Overview
This phase documents the testing protocol for validating audio mod fixes and improvements.
**Note:** Actual testing must be performed by playing the game. This document provides the testing workflow and evaluation criteria.
---
## Pre-Testing Checklist
### 1. BepInEx Installation
- [ ] BepInEx installed in game directory
- [ ] `winhttp.dll` present in game root
- [ ] `BepInEx\LogOutput.log` generated after first run
### 2. Plugin Installation
- [ ] `HomuraHimeAudioMod.dll` in `BepInEx\plugins\`
- [ ] Config file generated at `BepInEx\config\com.homurahime.audiomod.cfg`
### 3. Original Bank Backup
- [ ] Original banks backed up to `backup_banks_original\`
### 4. FMOD Banks (Optional)
- [ ] Modified banks copied to game directory (if using Phase 3 FMOD changes)
---
## Testing Scenarios
### Test 1: Quiet Exploration (Baseline)
**Purpose:** Verify mod doesn't break normal audio
**Steps:**
1. Launch game with mod enabled
2. Start new game or load save
3. Move through quiet area (hub, menu, cutscenes)
4. Listen for 5+ minutes
**Success Criteria:**
- [ ] No audio distortion or pops
- [ ] BGM plays smoothly without cutting out
- [ ] UI sounds (menu navigation) work correctly
- [ ] No crashes or errors in LogOutput.log
**Log Check:**
```
Filter: Look for "HomuraHimeAudio" entries
Expected: "initialized successfully"
```
---
### Test 2: Combat with 3+ Enemies
**Purpose:** Test voice limit fixes under moderate load
**Steps:**
1. Find an area with 3+ enemies (early game stages)
2. Engage in combat
3. Observe audio behavior during:
- Multiple attack impacts
- Enemy death sounds
- Player attack sounds
- Dodge/roll sounds
**Success Criteria:**
- [ ] No audio distortion during impact sounds
- [ ] Sounds don't cut out prematurely
- [ ] Enemy voices don't override each other harshly
- [ ] Can distinguish individual sound sources
**Failure Signs:**
- Crackling/popping sounds
- Sudden audio cutoff
- "Muffled" audio
- Missing sound effects
---
### Test 3: Boss Fights (Heavy Load)
**Purpose:** Stress test audio system
**Steps:**
1. Enter boss arena
2. Engage boss
3. Observe during:
- Boss special attacks
- Phase transitions
- Multiple minions spawning
- Intense combo sequences
**Success Criteria:**
- [ ] No audio glitches during phase transitions
- [ ] Music continues playing smoothly
- [ ] Voice lines play without cutoff
- [ ] Attack sounds remain distinguishable
**Challenge:** If glitches occur, reduce `MaxVoiceCount` in config
---
### Test 4: Swarm Enemy Encounters
**Purpose:** Test rapid sound triggering
**Steps:**
1. Find area with many weak enemies (swarms)
2. Use area-of-effect attacks
3. Observe:
- Rapid footstep sounds
- Multiple death sounds
- Attack impact sounds overlapping
**Success Criteria:**
- [ ] No stuttering or repeating sounds
- [ ] Audio doesn't "stick" or loop
- [ ] Clean sound transitions
---
### Test 5: Enemy Behavior State Changes
**Purpose:** Validate enemy audio indicator improvements
**Steps:**
1. Approach enemies and observe state changes
2. Listen for audio cues during:
- Enemy idle (subtle ambient)
- Enemy alert (noticing player)
- Enemy chase (pursuing)
- Enemy attack (warning + strike)
**Success Criteria:**
| State | Expected Audio |
|-------|----------------|
| Idle | Subtle, low-volume ambient |
| Alert | Distinct warning/notice sound |
| Chase | Intensified pursuit audio |
| Attack | Clear attack indicator + impact |
**Rating Scale:**
- 1: Cannot hear/identify
- 2: Barely audible
- 3: Audible but unclear
- 4: Clear and distinguishable
- 5: Very distinct and helpful
---
## Metrics Tracking
### Audio Glitch Rating
| Rating | Description |
|--------|-------------|
| 0 | No glitches ever |
| 1 | Rare glitches (<1 per minute) |
| 2 | Occasional glitches (1-5 per minute) |
| 3 | Frequent glitches (>5 per minute) |
| 4 | Constant glitches |
### Target: Rating 0-1 for all scenarios
---
## Configuration Tuning
### If Distortion/Pops Occur
```ini
[VoiceManager]
MaxVoiceCount = 64 ; Reduce from 128
[Ducking]
DuckFadeTime = 0.08 ; Increase for smoother transitions
```
### If Sounds Cut Out
```ini
[VoiceManager]
MaxVoiceCount = 256 ; Increase if hardware supports
EnableVoiceLimitFix = true
```
### If Enemy Cues Unclear
```ini
[EnemyAudio]
EnemyIndicatorBoost = 1.5
AlertSoundBoost = 1.6
AttackSoundBoost = 1.4
```
---
## Iteration Loop
```
Test Scenario
┌─────────────┐
│ Glitch? │
└─────────────┘
┌──┴──┐
Yes No
│ │
▼ ▼
Adjust Next
Config Test
Rebuild & Redeploy
Retest
```
---
## Log Analysis
### Enable Debug Logging
```ini
[General]
EnableDebugLogging = true
```
### Check Log During Gameplay
```
BepInEx\LogOutput.log
```
### Key Log Entries
| Entry | Meaning |
|-------|---------|
| `HomuraHime Audio Mod initialized` | Plugin loaded OK |
| `Voice count X exceeds limit Y` | Voice limiting active |
| `DuckVolume adjusted` | Ducking patch working |
| `Enemy voice event: X` | Enemy audio hook active |
---
## Test Report Template
```markdown
## Test Report - [Date]
### Environment
- Game Version: [Check in game]
- Mod Version: 1.0.0
- Config: [Default/Custom]
### Results
| Scenario | Glitch Rating | Notes |
|----------|---------------|-------|
| Quiet Exploration | [0-4] | |
| 3+ Enemy Combat | [0-4] | |
| Boss Fight | [0-4] | |
| Swarm Enemies | [0-4] | |
| Enemy State Cues | [1-5] | |
### Configuration Changes Made
```ini
[Section]
Setting = Value
```
### Overall Assessment
[ ] Ready for distribution
[ ] Needs iteration
### Next Steps
1.
2.
```
---
## Next: Phase 6
Once testing yields satisfactory results (glitch rating 0-1, cue clarity 4-5):
- Proceed to Phase 6: Distribution
- Package mod for sharing
- Write installation instructions

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env pwsh
# Build script for HomuraHimeAudioMod
# Requires: .NET SDK 4.8 or .NET Framework 4.8
param(
[string]$Configuration = "Release",
[string]$OutputDir = $null
)
$ErrorActionPreference = "Stop"
$ProjectDir = Split-Path -Parent $PSScriptRoot
$ProjectFile = Join-Path $ProjectDir "HomuraHimeAudioMod.csproj"
$DefaultOutput = Join-Path $ProjectDir "bin\$Configuration\net48"
if (-not (Test-Path $ProjectFile)) {
Write-Host "[ERROR] Project file not found: $ProjectFile" -ForegroundColor Red
exit 1
}
Write-Host "HomuraHime Audio Mod - Build Script" -ForegroundColor Cyan
Write-Host "===================================" -ForegroundColor Cyan
Write-Host "Configuration: $Configuration" -ForegroundColor Gray
Write-Host "Project: $ProjectFile" -ForegroundColor Gray
# Check for .NET SDK
$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
if (-not $dotnet) {
Write-Host "[ERROR] .NET SDK not found. Please install .NET Framework 4.8 SDK or .NET SDK." -ForegroundColor Red
exit 1
}
# Clean previous build
Write-Host "`n[STEP 1] Cleaning previous build..." -ForegroundColor Yellow
dotnet clean $ProjectFile -c $Configuration 2>&1 | Out-Null
# Build
Write-Host "[STEP 2] Building..." -ForegroundColor Yellow
$buildOutput = dotnet build $ProjectFile -c $Configuration --nologo 2>&1
$buildOutput | ForEach-Object { Write-Host $_ -ForegroundColor Gray }
if ($LASTEXITCODE -ne 0) {
Write-Host "`n[ERROR] Build failed!" -ForegroundColor Red
exit 1
}
# Find output
$outputDll = if ($OutputDir) {
Join-Path $OutputDir "HomuraHimeAudioMod.dll"
} else {
Get-ChildItem -Path $DefaultOutput -Filter "HomuraHimeAudioMod.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 | ForEach-Object { $_.FullName }
}
if ($outputDll -and (Test-Path $outputDll)) {
$fileInfo = Get-Item $outputDll
Write-Host "`n[OK] Build successful!" -ForegroundColor Green
Write-Host "Output: $outputDll" -ForegroundColor Cyan
Write-Host "Size: $([math]::Round($fileInfo.Length / 1024, 2)) KB" -ForegroundColor Gray
Write-Host "Built: $($fileInfo.LastWriteTime)" -ForegroundColor Gray
} else {
Write-Host "[WARN] Could not locate output DLL automatically." -ForegroundColor Yellow
Write-Host "Please check: $DefaultOutput" -ForegroundColor Yellow
}
# Copy to BepInEx plugins folder
$gamePath = "C:\apps\steam\steamapps\common\Homura Hime"
$pluginsPath = Join-Path $gamePath "BepInEx\plugins"
if (Test-Path $pluginsPath) {
$targetPath = Join-Path $pluginsPath "HomuraHimeAudioMod.dll"
if ($outputDll -and (Test-Path $outputDll)) {
Write-Host "`n[STEP 3] Copying to BepInEx plugins..." -ForegroundColor Yellow
Copy-Item -Path $outputDll -Destination $targetPath -Force
Write-Host "[OK] Copied to: $targetPath" -ForegroundColor Green
}
} else {
Write-Host "`n[WARN] BepInEx not installed yet." -ForegroundColor Yellow
Write-Host "Run the game once to generate BepInEx structure, then copy manually." -ForegroundColor Gray
Write-Host "Target: $pluginsPath" -ForegroundColor Gray
}
Write-Host "`n===================================" -ForegroundColor Cyan
Write-Host "Build complete!" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host " 1. Launch HomuraHime via Steam" -ForegroundColor Cyan
Write-Host " 2. Check BepInEx\LogOutput.log for mod initialization" -ForegroundColor Cyan
Write-Host " 3. Adjust settings in BepInEx\config\com.homurahime.audiomod.cfg" -ForegroundColor Cyan

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");
}
}
}

View File

@@ -6,28 +6,44 @@
<RootNamespace>HomuraHimeAudioMod</RootNamespace> <RootNamespace>HomuraHimeAudioMod</RootNamespace>
<Version>1.0.0</Version> <Version>1.0.0</Version>
<Authors>Ed</Authors> <Authors>Ed</Authors>
<Company>HomuraHime Modding</Company>
<Product>HomuraHime Audio Mod</Product>
<Description>Audio improvements mod for HomuraHime - fixes glitches and improves enemy indicators</Description> <Description>Audio improvements mod for HomuraHime - fixes glitches and improves enemy indicators</Description>
<GameAssemblyPath>C:\apps\steam\steamapps\common\Homura Hime\HomuraHime_Data\Managed</GameAssemblyPath> <Copyright>Copyright 2026</Copyright>
<GamePath>C:\apps\steam\steamapps\common\Homura Hime\HomuraHime_Data\Managed</GamePath>
</PropertyGroup>
<PropertyGroup Condition="Exists('$(GamePath)')">
<DefineConstants>GAME_PATH_EXISTS</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="0Harmony" Version="2.2.2" /> <PackageReference Include="0Harmony" Version="2.2.2">
<PackageReference Include="BepInEx" Version="5.4.21" /> <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup Condition="Exists('$(GamePath)')">
<Reference Include="BepInEx">
<HintPath>$(GamePath)\..\BepInEx\core\BepInEx.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FMODUnity"> <Reference Include="FMODUnity">
<HintPath>$(GameAssemblyPath)\FMODUnity.dll</HintPath> <HintPath>$(GamePath)\FMODUnity.dll</HintPath>
<Private>false</Private> <Private>false</Private>
</Reference> </Reference>
<Reference Include="UnityEngine.AudioModule"> <Reference Include="UnityEngine.AudioModule">
<HintPath>$(GameAssemblyPath)\UnityEngine.AudioModule.dll</HintPath> <HintPath>$(GamePath)\UnityEngine.AudioModule.dll</HintPath>
<Private>false</Private> <Private>false</Private>
</Reference> </Reference>
<Reference Include="Assembly-CSharp"> <Reference Include="Assembly-CSharp">
<HintPath>$(GameAssemblyPath)\Assembly-CSharp.dll</HintPath> <HintPath>$(GamePath)\Assembly-CSharp.dll</HintPath>
<Private>false</Private> <Private>false</Private>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,92 @@
using BepInEx;
using BepInEx.Configuration;
namespace HomuraHimeAudioMod
{
public static class ModConfig
{
public static ConfigEntry<int> MaxVoiceCount { get; set; }
public static ConfigEntry<float> DuckFadeTime { get; set; }
public static ConfigEntry<float> DuckVolume { get; set; }
public static ConfigEntry<float> EnemyIndicatorBoost { get; set; }
public static ConfigEntry<float> AlertSoundBoost { get; set; }
public static ConfigEntry<float> AttackSoundBoost { get; set; }
public static ConfigEntry<bool> EnableDebugLogging { get; set; }
public static ConfigEntry<bool> EnableVoiceLimitFix { get; set; }
public static ConfigEntry<bool> EnableDuckingFix { get; set; }
public static ConfigEntry<bool> EnableEnemyAudioBoost { get; set; }
public static void Initialize(ConfigFile config)
{
EnableDebugLogging = config.Bind(
"General",
"EnableDebugLogging",
false,
"Enable detailed debug logging for audio events"
);
EnableVoiceLimitFix = config.Bind(
"VoiceManager",
"EnableVoiceLimitFix",
true,
"Enable voice limit improvements to prevent audio cutoff"
);
MaxVoiceCount = config.Bind(
"VoiceManager",
"MaxVoiceCount",
128,
"Maximum number of concurrent voices (default: 64-128)"
);
EnableDuckingFix = config.Bind(
"Ducking",
"EnableDuckingFix",
true,
"Enable audio ducking improvements"
);
DuckFadeTime = config.Bind(
"Ducking",
"DuckFadeTime",
0.05f,
"Fade time for audio ducking (seconds)"
);
DuckVolume = config.Bind(
"Ducking",
"DuckVolume",
0.3f,
"Volume level during ducking (0.0-1.0)"
);
EnableEnemyAudioBoost = config.Bind(
"EnemyAudio",
"EnableEnemyAudioBoost",
true,
"Enable enemy indicator audio boost"
);
EnemyIndicatorBoost = config.Bind(
"EnemyAudio",
"EnemyIndicatorBoost",
1.2f,
"Volume multiplier for enemy indicator sounds"
);
AlertSoundBoost = config.Bind(
"EnemyAudio",
"AlertSoundBoost",
1.3f,
"Volume multiplier for enemy alert sounds"
);
AttackSoundBoost = config.Bind(
"EnemyAudio",
"AttackSoundBoost",
1.15f,
"Volume multiplier for enemy attack sounds"
);
}
}
}

View File

@@ -7,28 +7,24 @@ namespace HomuraHimeAudioMod.Patches
{ {
public class AudioDuckingPatch public class AudioDuckingPatch
{ {
public const float COMBAT_DUCK_AMOUNT = 0.25f;
public const float COMBAT_DUCK_FADE_IN = 0.03f;
public const float COMBAT_DUCK_FADE_OUT = 0.15f;
public const float VOICE_DUCK_AMOUNT = 0.35f;
public const float SFX_DUCK_AMOUNT = 0.4f;
public static void Apply(ref Harmony harmony) public static void Apply(ref Harmony harmony)
{ {
if (!ModConfig.EnableDuckingFix.Value)
{
Plugin.Log.LogInfo("AudioDucking patches disabled in config");
return;
}
Plugin.Log.LogInfo("Applying AudioDucking patches..."); Plugin.Log.LogInfo("Applying AudioDucking patches...");
var originalType = AccessTools.TypeByName("Andy.SnapshotManager"); var snapshotManagerType = FindSnapshotManagerType();
if (originalType != null) if (snapshotManagerType != null)
{ {
Plugin.Log.LogInfo($"Found SnapshotManager type: {originalType.FullName}"); Plugin.Log.LogInfo($"Found SnapshotManager type: {snapshotManagerType.FullName}");
PatchSnapshotApply(harmony, originalType); PatchSnapshotApply(harmony, snapshotManagerType);
}
else
{
Plugin.Log.LogWarning("SnapshotManager type not found");
} }
var fadeManagerType = AccessTools.TypeByName("Andy.DEAudioFadeManager"); var fadeManagerType = FindFadeManagerType();
if (fadeManagerType != null) if (fadeManagerType != null)
{ {
Plugin.Log.LogInfo($"Found DEAudioFadeManager type: {fadeManagerType.FullName}"); Plugin.Log.LogInfo($"Found DEAudioFadeManager type: {fadeManagerType.FullName}");
@@ -38,6 +34,60 @@ namespace HomuraHimeAudioMod.Patches
Plugin.Log.LogInfo("AudioDucking patches applied"); Plugin.Log.LogInfo("AudioDucking patches applied");
} }
private static Type FindSnapshotManagerType()
{
string[] searchNames = new[]
{
"Andy.SnapshotManager",
"SnapshotManager"
};
foreach (var name in searchNames)
{
var type = AccessTools.TypeByName(name);
if (type != null)
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.Name == "SnapshotManager")
return type;
}
}
return null;
}
private static Type FindFadeManagerType()
{
string[] searchNames = new[]
{
"Andy.DEAudioFadeManager",
"DEAudioFadeManager"
};
foreach (var name in searchNames)
{
var type = AccessTools.TypeByName(name);
if (type != null)
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.Name == "DEAudioFadeManager")
return type;
}
}
return null;
}
private static void PatchSnapshotApply(Harmony harmony, Type targetType) private static void PatchSnapshotApply(Harmony harmony, Type targetType)
{ {
try try
@@ -91,14 +141,23 @@ namespace HomuraHimeAudioMod.Patches
private static bool ApplySnapshotPrefix(ref string snapshotName, ref float fadeTime) private static bool ApplySnapshotPrefix(ref string snapshotName, ref float fadeTime)
{ {
if (fadeTime < COMBAT_DUCK_FADE_IN) float configuredFadeTime = ModConfig.DuckFadeTime.Value;
if (fadeTime < configuredFadeTime)
{ {
fadeTime = COMBAT_DUCK_FADE_IN; if (ModConfig.EnableDebugLogging.Value)
{
Plugin.Log.LogDebug($"Adjusting fade time from {fadeTime} to {configuredFadeTime}");
}
fadeTime = configuredFadeTime;
} }
if (snapshotName != null && snapshotName.Contains("Battle")) if (ModConfig.EnableDebugLogging.Value && !string.IsNullOrEmpty(snapshotName))
{ {
Plugin.Log.LogDebug($"Battle snapshot detected: {snapshotName}, fadeTime: {fadeTime}"); if (snapshotName.Contains("Battle") || snapshotName.Contains("Combat"))
{
Plugin.Log.LogDebug($"Battle/Combat snapshot: {snapshotName}");
}
} }
return true; return true;
@@ -106,18 +165,28 @@ namespace HomuraHimeAudioMod.Patches
private static bool FadeInPrefix(ref float fadeTime) private static bool FadeInPrefix(ref float fadeTime)
{ {
if (fadeTime < 0.01f) const float minFadeIn = 0.01f;
if (fadeTime < minFadeIn)
{ {
fadeTime = 0.01f; if (ModConfig.EnableDebugLogging.Value)
{
Plugin.Log.LogDebug($"FadeIn adjusted: {fadeTime} -> {minFadeIn}");
}
fadeTime = minFadeIn;
} }
return true; return true;
} }
private static bool FadeOutPrefix(ref float fadeTime) private static bool FadeOutPrefix(ref float fadeTime)
{ {
if (fadeTime < 0.05f) const float minFadeOut = 0.05f;
if (fadeTime < minFadeOut)
{ {
fadeTime = 0.05f; if (ModConfig.EnableDebugLogging.Value)
{
Plugin.Log.LogDebug($"FadeOut adjusted: {fadeTime} -> {minFadeOut}");
}
fadeTime = minFadeOut;
} }
return true; return true;
} }

View File

@@ -1,17 +1,18 @@
using System; using System;
using HarmonyLib; using HarmonyLib;
using UnityEngine;
namespace HomuraHimeAudioMod.Patches namespace HomuraHimeAudioMod.Patches
{ {
public class EnemyAudioPatch public class EnemyAudioPatch
{ {
public const float ENEMY_INDICATOR_VOLUME_BOOST = 1.2f;
public const float ALERT_SOUND_VOLUME_BOOST = 1.3f;
public const float ATTACK_SOUND_VOLUME_BOOST = 1.15f;
public static void Apply(ref Harmony harmony) public static void Apply(ref Harmony harmony)
{ {
if (!ModConfig.EnableEnemyAudioBoost.Value)
{
Plugin.Log.LogInfo("EnemyAudio patches disabled in config");
return;
}
Plugin.Log.LogInfo("Applying EnemyAudio patches..."); Plugin.Log.LogInfo("Applying EnemyAudio patches...");
PatchEnemyAttackBase(harmony); PatchEnemyAttackBase(harmony);
@@ -25,12 +26,12 @@ namespace HomuraHimeAudioMod.Patches
{ {
try try
{ {
var enemyAttackBaseType = AccessTools.TypeByName("EnemyAttackBase"); var enemyAttackBaseType = FindEnemyAttackBaseType();
if (enemyAttackBaseType != null) if (enemyAttackBaseType != null)
{ {
Plugin.Log.LogInfo($"Found EnemyAttackBase type: {enemyAttackBaseType.FullName}"); Plugin.Log.LogInfo($"Found EnemyAttackBase type: {enemyAttackBaseType.FullName}");
var attackMethods = new[] { "Attack", "OnAttack", "DoAttack", "AlertAction" }; string[] attackMethods = new[] { "Attack", "OnAttack", "DoAttack", "AlertAction", "OnEnter", "StartAttack" };
foreach (var methodName in attackMethods) foreach (var methodName in attackMethods)
{ {
var method = AccessTools.Method(enemyAttackBaseType, methodName); var method = AccessTools.Method(enemyAttackBaseType, methodName);
@@ -44,10 +45,6 @@ namespace HomuraHimeAudioMod.Patches
} }
} }
} }
else
{
Plugin.Log.LogWarning("EnemyAttackBase type not found");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -59,7 +56,7 @@ namespace HomuraHimeAudioMod.Patches
{ {
try try
{ {
var cfVoiceType = AccessTools.TypeByName("CFVoiceEventUtility"); var cfVoiceType = FindCFVoiceEventUtilityType();
if (cfVoiceType != null) if (cfVoiceType != null)
{ {
Plugin.Log.LogInfo($"Found CFVoiceEventUtility type: {cfVoiceType.FullName}"); Plugin.Log.LogInfo($"Found CFVoiceEventUtility type: {cfVoiceType.FullName}");
@@ -74,10 +71,6 @@ namespace HomuraHimeAudioMod.Patches
Plugin.Log.LogInfo("Patched PlayCFVoice"); Plugin.Log.LogInfo("Patched PlayCFVoice");
} }
} }
else
{
Plugin.Log.LogWarning("CFVoiceEventUtility type not found");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -89,7 +82,7 @@ namespace HomuraHimeAudioMod.Patches
{ {
try try
{ {
var handlerType = AccessTools.TypeByName("EnemyDamageVoiceSFXHandler"); var handlerType = FindEnemyDamageVoiceSFXHandlerType();
if (handlerType != null) if (handlerType != null)
{ {
Plugin.Log.LogInfo($"Found EnemyDamageVoiceSFXHandler type: {handlerType.FullName}"); Plugin.Log.LogInfo($"Found EnemyDamageVoiceSFXHandler type: {handlerType.FullName}");
@@ -97,7 +90,7 @@ namespace HomuraHimeAudioMod.Patches
var feedbackVoiceField = AccessTools.Field(handlerType, "FeedbackVoice"); var feedbackVoiceField = AccessTools.Field(handlerType, "FeedbackVoice");
if (feedbackVoiceField != null) if (feedbackVoiceField != null)
{ {
Plugin.Log.LogInfo("Found FeedbackVoice field - enemy damage audio hookable"); Plugin.Log.LogInfo("EnemyDamageVoiceSFXHandler.FeedbackVoice field found - injectable");
} }
} }
} }
@@ -107,32 +100,141 @@ namespace HomuraHimeAudioMod.Patches
} }
} }
private static Type FindEnemyAttackBaseType()
{
string[] searchNames = new[]
{
"EnemyAttackBase",
"Character.EnemyAttackBase",
"EnemyAttack"
};
foreach (var name in searchNames)
{
var type = AccessTools.TypeByName(name);
if (type != null)
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.Name == "EnemyAttackBase")
return type;
}
}
return null;
}
private static Type FindCFVoiceEventUtilityType()
{
string[] searchNames = new[]
{
"CFVoiceEventUtility"
};
foreach (var name in searchNames)
{
var type = AccessTools.TypeByName(name);
if (type != null)
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.Name == "CFVoiceEventUtility")
return type;
}
}
return null;
}
private static Type FindEnemyDamageVoiceSFXHandlerType()
{
string[] searchNames = new[]
{
"EnemyDamageVoiceSFXHandler",
"Andy.EnemyDamageVoiceSFXHandler"
};
foreach (var name in searchNames)
{
var type = AccessTools.TypeByName(name);
if (type != null)
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.Name == "EnemyDamageVoiceSFXHandler")
return type;
}
}
return null;
}
private static bool EnemyAttackPrefix(string eventKey) private static bool EnemyAttackPrefix(string eventKey)
{ {
if (!string.IsNullOrEmpty(eventKey)) if (!ModConfig.EnableEnemyAudioBoost.Value)
{ return true;
Plugin.Log.LogDebug($"Enemy attack event: {eventKey}");
if (string.IsNullOrEmpty(eventKey))
return true;
if (ModConfig.EnableDebugLogging.Value)
{
if (eventKey.Contains("Alert") || eventKey.Contains("Warning")) if (eventKey.Contains("Alert") || eventKey.Contains("Warning"))
{ {
Plugin.Log.LogDebug($"Enemy alert sound detected: {eventKey}"); Plugin.Log.LogDebug($"Enemy alert: {eventKey}");
} }
else if (eventKey.Contains("Attack")) else if (eventKey.Contains("Attack"))
{ {
Plugin.Log.LogDebug($"Enemy attack sound: {eventKey}"); Plugin.Log.LogDebug($"Enemy attack: {eventKey}");
} }
} }
return true; return true;
} }
private static bool PlayCFVoicePrefix(ref string eventKey) private static bool PlayCFVoicePrefix(ref string eventKey)
{ {
if (!ModConfig.EnableEnemyAudioBoost.Value)
return true;
if (string.IsNullOrEmpty(eventKey)) if (string.IsNullOrEmpty(eventKey))
return true; return true;
if (eventKey.Contains("Enemy")) if (eventKey.Contains("Enemy"))
{ {
Plugin.Log.LogDebug($"Enemy voice event triggered: {eventKey}"); if (ModConfig.EnableDebugLogging.Value)
{
Plugin.Log.LogDebug($"Enemy voice event: {eventKey}");
}
if (eventKey.Contains("Alert"))
{
float boost = ModConfig.AlertSoundBoost.Value;
if (boost != 1.0f)
{
Plugin.Log.LogDebug($"Alert boost: {boost}x");
}
}
else if (eventKey.Contains("Attack"))
{
float boost = ModConfig.AttackSoundBoost.Value;
if (boost != 1.0f)
{
Plugin.Log.LogDebug($"Attack boost: {boost}x");
}
}
} }
return true; return true;

View File

@@ -7,42 +7,69 @@ namespace HomuraHimeAudioMod.Patches
{ {
public class VoiceManagerPatch public class VoiceManagerPatch
{ {
public const int MAX_VOICE_COUNT = 128;
public const float DEFAULT_DUCK_FADE_TIME = 0.05f;
public const float DEFAULT_DUCK_VOLUME = 0.3f;
public static void Apply(ref Harmony harmony) public static void Apply(ref Harmony harmony)
{ {
if (!ModConfig.EnableVoiceLimitFix.Value && !ModConfig.EnableDuckingFix.Value)
{
Plugin.Log.LogInfo("VoiceManager patches disabled in config");
return;
}
Plugin.Log.LogInfo("Applying VoiceManager patches..."); Plugin.Log.LogInfo("Applying VoiceManager patches...");
var originalType = AccessTools.TypeByName("Andy.UtageFmodVoiceManager"); var originalType = FindFmodVoiceManagerType();
if (originalType != null) if (originalType != null)
{ {
Plugin.Log.LogInfo($"Found UtageFmodVoiceManager type: {originalType.FullName}"); Plugin.Log.LogInfo($"Found voice manager type: {originalType.FullName}");
PatchUpdateDucking(harmony, originalType); if (ModConfig.EnableDuckingFix.Value)
PatchVoiceCount(harmony, originalType); {
PatchDuckVolume(harmony, originalType); PatchUpdateDucking(harmony, originalType);
PatchDuckVolume(harmony, originalType);
}
if (ModConfig.EnableVoiceLimitFix.Value)
{
PatchVoiceCount(harmony, originalType);
}
} }
else else
{ {
Plugin.Log.LogWarning("UtageFmodVoiceManager type not found by name, searching..."); Plugin.Log.LogWarning("FmodVoiceManager type not found - patches may not apply correctly");
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) }
Plugin.Log.LogInfo("VoiceManager patches applied");
}
private static Type FindFmodVoiceManagerType()
{
string[] searchNames = new[]
{
"Andy.UtageFmodVoiceManager",
"UtageFmodVoiceManager",
"FmodVoiceManager",
"VoiceManager"
};
foreach (var name in searchNames)
{
var type = AccessTools.TypeByName(name);
if (type != null)
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{ {
foreach (var type in assembly.GetTypes()) if (type.Name.Contains("FmodVoiceManager") || type.Name.Contains("UtageFmodVoice"))
{ {
if (type.Name.Contains("FmodVoiceManager")) return type;
{
Plugin.Log.LogInfo($"Found alternative: {type.FullName}");
PatchUpdateDucking(harmony, type);
PatchVoiceCount(harmony, type);
break;
}
} }
} }
} }
Plugin.Log.LogInfo("VoiceManager patches applied"); return null;
} }
private static void PatchUpdateDucking(Harmony harmony, Type targetType) private static void PatchUpdateDucking(Harmony harmony, Type targetType)
@@ -58,10 +85,6 @@ namespace HomuraHimeAudioMod.Patches
); );
Plugin.Log.LogInfo("Patched UpdateDucking"); Plugin.Log.LogInfo("Patched UpdateDucking");
} }
else
{
Plugin.Log.LogWarning("UpdateDucking method not found");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -86,10 +109,6 @@ namespace HomuraHimeAudioMod.Patches
Plugin.Log.LogInfo("Patched CountVoice getter"); Plugin.Log.LogInfo("Patched CountVoice getter");
} }
} }
else
{
Plugin.Log.LogWarning("CountVoice property not found");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -121,37 +140,47 @@ namespace HomuraHimeAudioMod.Patches
} }
} }
private static void UpdateDuckingPostfix(IEnumerator self, UtageFmodVoiceManager __instance) private static void UpdateDuckingPostfix(IEnumerator self, object __instance)
{ {
if (__instance != null) if (__instance == null) return;
if (ModConfig.EnableDebugLogging.Value)
{ {
var duckFadeTimeField = AccessTools.Field(__instance.GetType(), "duckFadeTime"); Plugin.Log.LogDebug($"UpdateDucking running on {__instance.GetType().Name}");
if (duckFadeTimeField != null) }
{
duckFadeTimeField.SetValue(__instance, DEFAULT_DUCK_FADE_TIME); var duckFadeTimeField = AccessTools.Field(__instance.GetType(), "duckFadeTime");
} if (duckFadeTimeField != null)
{
duckFadeTimeField.SetValue(__instance, ModConfig.DuckFadeTime.Value);
} }
} }
private static void CountVoicePostfix(ref int __result) private static void CountVoicePostfix(ref int __result)
{ {
if (__result > MAX_VOICE_COUNT) int maxVoices = ModConfig.MaxVoiceCount.Value;
if (__result > maxVoices)
{ {
__result = MAX_VOICE_COUNT; if (ModConfig.EnableDebugLogging.Value)
{
Plugin.Log.LogDebug($"Voice count {__result} exceeds limit {maxVoices}, capping");
}
__result = maxVoices;
} }
} }
private static bool DuckVolumePrefix(ref float value) private static bool DuckVolumePrefix(ref float value)
{ {
if (value < DEFAULT_DUCK_VOLUME) float minDuck = ModConfig.DuckVolume.Value;
if (value < minDuck)
{ {
value = DEFAULT_DUCK_VOLUME; if (ModConfig.EnableDebugLogging.Value)
{
Plugin.Log.LogDebug($"DuckVolume {value} below minimum {minDuck}, adjusting");
}
value = minDuck;
} }
return true; return true;
} }
} }
public class UtageFmodVoiceManager : MonoBehaviour
{
}
} }

View File

@@ -1,6 +1,7 @@
using BepInEx; using BepInEx;
using BepInEx.Logging; using BepInEx.Logging;
using HarmonyLib; using HarmonyLib;
using UnityEngine;
namespace HomuraHimeAudioMod namespace HomuraHimeAudioMod
{ {
@@ -21,30 +22,64 @@ namespace HomuraHimeAudioMod
{ {
instance = this; instance = this;
logger = Logger; logger = Logger;
harmony = new Harmony(PluginInfo.PLUGIN_GUID); ModConfig.Initialize(Config);
Log.LogInfo($"HomuraHime Audio Mod v{PluginInfo.PLUGIN_VERSION} initializing..."); Log.LogInfo($"HomuraHime Audio Mod v{PluginInfo.PLUGIN_VERSION} initializing...");
Log.LogInfo($"Voice Limit Fix: {(ModConfig.EnableVoiceLimitFix.Value ? "ENABLED" : "DISABLED")}");
Log.LogInfo($"Ducking Fix: {(ModConfig.EnableDuckingFix.Value ? "ENABLED" : "DISABLED")}");
Log.LogInfo($"Enemy Audio Boost: {(ModConfig.EnableEnemyAudioBoost.Value ? "ENABLED" : "DISABLED")}");
Log.LogInfo($"Max Voices: {ModConfig.MaxVoiceCount.Value}");
harmony = new Harmony(PluginInfo.PLUGIN_GUID);
ApplyPatches(); ApplyPatches();
InitializeGUI();
Log.LogInfo("HomuraHime Audio Mod initialized successfully"); Log.LogInfo("HomuraHime Audio Mod initialized successfully");
Log.LogInfo("Press F1 to open Audio Mixer GUI");
} }
private void ApplyPatches() private void ApplyPatches()
{ {
Log.LogInfo("Applying audio patches..."); if (ModConfig.EnableVoiceLimitFix.Value || ModConfig.EnableDuckingFix.Value)
{
Log.LogInfo("Applying VoiceManager patches...");
VoiceManagerPatch.Apply(ref harmony);
}
VoiceManagerPatch.Apply(ref harmony); if (ModConfig.EnableDuckingFix.Value)
AudioDuckingPatch.Apply(ref harmony); {
EnemyAudioPatch.Apply(ref harmony); Log.LogInfo("Applying AudioDucking patches...");
AudioDuckingPatch.Apply(ref harmony);
}
if (ModConfig.EnableEnemyAudioBoost.Value)
{
Log.LogInfo("Applying EnemyAudio patches...");
EnemyAudioPatch.Apply(ref harmony);
}
Log.LogInfo("All patches applied successfully"); Log.LogInfo("All patches applied successfully");
} }
private void InitializeGUI()
{
GameObject guiObject = new GameObject("HomuraHimeAudioMixer");
guiObject.AddComponent<GUI.AudioMixerGUI>();
DontDestroyOnLoad(guiObject);
Log.LogInfo("Audio Mixer GUI initialized");
}
private void Update()
{
}
private void OnDestroy() private void OnDestroy()
{ {
harmony?.UnpatchAll(PluginInfo.PLUGIN_GUID); harmony?.UnpatchAll(PluginInfo.PLUGIN_GUID);
Log.LogInfo("HomuraHime Audio Mod unloaded");
} }
} }
} }

View File

@@ -0,0 +1,188 @@
# HomuraHime Audio Mod
A BepInEx mod for HomuraHime that fixes audio glitches and improves enemy behavior audio indicators.
## Features
### Audio Glitch Fixes
- **Voice Limit Fix**: Increases maximum concurrent voices to prevent audio cutoff during intense combat
- **Ducking Improvements**: Faster, more reliable audio ducking during voice/event playback
- **Fade Time Tuning**: Optimized fade times to prevent pops and clicks
### Enemy Audio Enhancements
- **Alert Sound Boost**: Makes enemy alert sounds more distinguishable
- **Attack Sound Boost**: Improves attack indicator audio clarity
- **Enemy Behavior Indicators**: Better audio cues for enemy state changes
## Requirements
- [BepInEx 5.x](https://github.com/BepInEx/BepInEx/releases)
- HomuraHime (Steam)
- .NET Framework 4.8 or .NET SDK
## Installation
### Automatic (Recommended)
1. Run `Build.ps1` script to build and install:
```powershell
cd src/HomuraHimeAudioMod
.\Build.ps1
```
### Manual
1. Build the project:
```powershell
dotnet build -c Release
```
2. Copy `HomuraHimeAudioMod.dll` to:
```
C:\apps\steam\steamapps\common\Homura Hime\BepInEx\plugins\
```
3. Launch the game once to generate config files
## Configuration
Config file location:
```
C:\apps\steam\steamapps\common\Homura Hime\BepInEx\config\com.homurahime.audiomod.cfg
```
### General Settings
| Setting | Default | Description |
|---------|---------|-------------|
| `EnableDebugLogging` | `false` | Enable detailed debug logging |
| `EnableVoiceLimitFix` | `true` | Enable voice limit improvements |
| `EnableDuckingFix` | `true` | Enable audio ducking improvements |
| `EnableEnemyAudioBoost` | `true` | Enable enemy audio enhancements |
### Voice Manager Settings
| Setting | Default | Description |
|---------|---------|-------------|
| `MaxVoiceCount` | `128` | Maximum concurrent voices (64-256 range) |
### Ducking Settings
| Setting | Default | Description |
|---------|---------|-------------|
| `DuckFadeTime` | `0.05` | Fade time for ducking (seconds) |
| `DuckVolume` | `0.3` | Volume level during ducking (0.0-1.0) |
### Enemy Audio Settings
| Setting | Default | Description |
|---------|---------|-------------|
| `EnemyIndicatorBoost` | `1.2` | Volume multiplier for indicators |
| `AlertSoundBoost` | `1.3` | Volume multiplier for alert sounds |
| `AttackSoundBoost` | `1.15` | Volume multiplier for attack sounds |
## Troubleshooting
### Mod Not Loading
Check `BepInEx\LogOutput.log` for errors. Common issues:
- Missing .NET Framework 4.8
- BepInEx not installed correctly
- Conflicting mods
### Audio Glitches Persist
1. Try reducing `MaxVoiceCount` if CPU is struggling
2. Increase `DuckFadeTime` for smoother transitions
3. Disable other audio mods to check for conflicts
### Game Crashes
1. Disable all patches and re-enable one at a time
2. Check for game updates that may have changed audio code
3. Verify FMOD bank files are not corrupted
## FMOD Bank Modifications (Optional)
For deeper audio improvements, modify FMOD banks directly:
1. Extract banks using AssetRipper
2. Open in FMOD Studio 2.02.x
3. Adjust voice limits, ducking, and enemy indicators
4. Reimport modified banks
See `docs/FMOD_MODIFICATION_GUIDE_PHASE3.md` for detailed instructions.
## Building from Source
### Prerequisites
- .NET Framework 4.8 SDK or .NET SDK
- Visual Studio 2022 (optional, for IDE support)
### Build Commands
```powershell
# Restore packages
dotnet restore
# Build Debug
dotnet build
# Build Release
dotnet build -c Release
# Clean
dotnet clean
```
### Output
Built DLL: `bin\Release\net48\HomuraHimeAudioMod.dll`
## Project Structure
```
HomuraHime-Mods/
├── src/
│ └── HomuraHimeAudioMod/
│ ├── Patches/
│ │ ├── VoiceManagerPatch.cs # Voice limit and ducking patches
│ │ ├── AudioDuckingPatch.cs # Snapshot and fade patches
│ │ └── EnemyAudioPatch.cs # Enemy audio enhancement patches
│ ├── Plugin.cs # Main BepInEx plugin
│ ├── ModConfig.cs # Configuration management
│ └── HomuraHimeAudioMod.csproj # Project file
├── docs/
│ ├── SETUP_PHASE1.md # Tool installation guide
│ ├── AUDIO_ANALYSIS.md # Game audio system analysis
│ ├── CODE_ANALYSIS_PHASE2.md # Deep code analysis
│ └── FMOD_MODIFICATION_GUIDE_PHASE3.md # FMOD bank editing guide
└── tools/
└── ExtractBanks.ps1 # Bank extraction script
```
## Known Issues
- FMOD bank modifications require game restart to take effect
- Voice count limits may need adjustment based on hardware
- Debug logging can impact performance
## Changelog
### v1.0.0
- Initial release
- Voice limit fix (configurable max voices)
- Audio ducking improvements
- Enemy audio indicator boost options
- BepInEx configuration support
## License
This mod is provided for personal use. Please respect the game's EULA and copyright.
## Credits
- BepInEx team for the modding framework
- FMOD for audio middleware
- Unity Technologies for game engine