Files
test/Assets/PerfectWorld/Scripts/Sound/SFXManager.cs
T
2026-04-14 17:50:19 +07:00

165 lines
6.6 KiB
C#

using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using BrewMonster.Scripts;
namespace BrewMonster.Scripts
{
/// <summary>
/// Translates numeric SFX IDs (from the original C++ sound.txt table) into
/// Addressable audio paths, then loads and plays them via AddressableManager.
///
/// C++ equivalent: CECGameRun::m_SoundStringTable + GetOrLoadSoundFromTable()
/// </summary>
public class SFXManager : MonoSingleton<SFXManager>
{
private readonly Dictionary<int, string> _soundTable = new();
private const string SoundTableResourcePath = "sound"; // Assets/Resources/sound.txt
protected override void Initialize()
{
base.Initialize();
LoadSoundTable();
}
// ────────────────────────────────────────────────────────────────────
// Startup: parse sound.txt
// ────────────────────────────────────────────────────────────────────
private void LoadSoundTable()
{
var textAsset = Resources.Load<TextAsset>(SoundTableResourcePath);
if (textAsset == null)
{
BMLogger.LogError("SFXManager: sound.txt not found in Resources/");
return;
}
var lines = textAsset.text.Split('\n');
foreach (var line in lines)
{
var trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed)) continue;
var tabIndex = trimmed.IndexOf('\t');
if (tabIndex < 0) continue;
var idStr = trimmed.Substring(0, tabIndex).Trim();
var rawPath = trimmed.Substring(tabIndex + 1).Trim().Trim('"');
if (!int.TryParse(idStr, out int id)) continue;
if (string.IsNullOrEmpty(rawPath)) continue; // empty entry — intentional silence
_soundTable[id] = NormalizePath(rawPath);
}
BMLogger.Log($"SFXManager: Loaded {_soundTable.Count} SFX entries from sound.txt");
}
/// <summary>
/// Convert Windows backslash path from sound.txt into a forward-slash
/// lowercase Addressables address.
/// e.g. "SFX\Character\FootStep\LandWalkMaleA.wav"
/// → "sfx/character/footstep/landwalkmalea.wav"
///
/// Adjust this method if your Addressables catalog uses a different format.
/// </summary>
private static string NormalizePath(string raw)
{
return raw.Replace('\\', '/').ToLowerInvariant();
}
// ────────────────────────────────────────────────────────────────────
// Public API
// ────────────────────────────────────────────────────────────────────
private const float SkillSfxVolume = 1f;
/// <summary>
/// Play a skill SFX by Addressable address at a world position.
/// Uses the cache fast-path when the clip is already loaded; otherwise waits
/// for AddressableManager initialization (no per-frame lambda polling) before
/// loading and playing the clip.
/// </summary>
public async UniTaskVoid PlaySkillSfxAtPointAsync(string address, Vector3 worldPos)
{
if (string.IsNullOrEmpty(address)) return;
var mgr = AddressableManager.Instance;
if (mgr == null) return;
if (mgr.TryGetCachedAudioClip(address, out var clip) && clip != null)
{
AudioSource.PlayClipAtPoint(clip, worldPos, SkillSfxVolume);
return;
}
await mgr.WaitUntilInitializedAsync();
var loaded = await mgr.LoadAudioClipAsync(address);
if (loaded != null)
AudioSource.PlayClipAtPoint(loaded, Vector3.zero, SkillSfxVolume);
}
/// <summary>
/// Resolve a numeric SFX ID to its normalized Addressable path.
/// Returns null if the ID is not in the table or maps to an empty entry.
/// </summary>
public string GetSFXPath(int id)
{
return _soundTable.TryGetValue(id, out var path) ? path : null;
}
/// <summary>
/// Load (if not already cached) and play a sound by its numeric ID.
/// Maps directly to CECHostPlayer::PlayMoveSound / SFX_INFO::Start logic.
///
/// <paramref name="source"/> controls spatialization — pass a 2D source for
/// movement sounds or a 3D world-space source for positional SFX.
///
/// No-op if the ID is unmapped, maps to an empty entry, or the clip fails
/// to load.
/// </summary>
public async UniTaskVoid PlaySFXByIdAsync(int id, AudioSource source)
{
if (source == null) return;
var path = GetSFXPath(id);
if (string.IsNullOrEmpty(path)) return;
// Fast path: clip already in Addressables cache
if (AddressableManager.Instance.TryGetCachedAudioClip(path, out var cached))
{
source.PlayOneShot(cached);
return;
}
// Slow path: load from Addressables bundle
var clip = await AddressableManager.Instance.LoadAudioClipAsync(path).ToUniTask();
if (clip != null)
source.PlayOneShot(clip);
}
/// <summary>
/// Awaitable variant — use when you need to know when playback has been
/// dispatched (e.g. chaining movement sound updates).
/// </summary>
public async UniTask PlaySFXByIdAsyncAwaitable(int id, AudioSource source)
{
if (source == null) return;
var path = GetSFXPath(id);
if (string.IsNullOrEmpty(path)) return;
if (AddressableManager.Instance.TryGetCachedAudioClip(path, out var cached))
{
source.PlayOneShot(cached);
return;
}
var clip = await AddressableManager.Instance.LoadAudioClipAsync(path).ToUniTask();
if (clip != null)
source.PlayOneShot(clip);
}
}
}