From 11aa7a2b576d4d203c8970797ad54b15f504cfb6 Mon Sep 17 00:00:00 2001 From: kenca Date: Fri, 20 Mar 2026 12:15:45 -0400 Subject: [PATCH 1/4] Added ModAudioHub --- Abstracts/CustomCharacterModel.cs | 7 + Utils/ModAudioHub.cs | 496 ++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 Utils/ModAudioHub.cs diff --git a/Abstracts/CustomCharacterModel.cs b/Abstracts/CustomCharacterModel.cs index 641184b..7ab87a9 100644 --- a/Abstracts/CustomCharacterModel.cs +++ b/Abstracts/CustomCharacterModel.cs @@ -49,6 +49,13 @@ public CustomCharacterModel() public virtual string? CustomCastSfx => null; public virtual string? CustomDeathSfx => null; + /// + /// Godot resource root for this mod's packaged audio (e.g. res://mods/my_character). + /// resolves audio/sfx/... and audio/bgm/... under this path. + /// Override in your character model, or call directly. Return null to opt out. + /// + public virtual string? ModAudioPath => null; + //Defaults public override int StartingGold => 99; public override float AttackAnimDelay => 0.15f; diff --git a/Utils/ModAudioHub.cs b/Utils/ModAudioHub.cs new file mode 100644 index 0000000..66bdcfa --- /dev/null +++ b/Utils/ModAudioHub.cs @@ -0,0 +1,496 @@ +using System.Collections.Generic; +using System.Reflection; +using Godot; +using MegaCrit.Sts2.Core.Entities.Creatures; +using MegaCrit.Sts2.Core.Nodes; +using MegaCrit.Sts2.Core.Nodes.Rooms; +using MegaCrit.Sts2.Core.Random; +using MegaCrit.Sts2.Core.Saves; +using BaseLib.Abstracts; + +namespace BaseLib.Utils; + +/// +/// Central audio helper for mods depending on BaseLib. Resolves streams under each +/// character's (mod res root). +/// +public static class ModAudioHub +{ + private static readonly Dictionary CachedStreams = new(); + private static readonly Dictionary CharacterIdToRoot = new(); + private static bool _discovered; + + private static AudioStreamPlayer? _musicPlayer; + private static string? _currentMusicPath; + private static float _currentVolumeOffset; + private static Tween? _fadeTween; + private static AudioStreamPlayer? _outgoingPlayer; + private static Tween? _outgoingFadeTween; + private static AudioStreamPlayer? _ambiencePlayer; + private static string? _currentAmbiencePath; + private static Tween? _ambienceFadeTween; + + private const float MusicVolumeOffset = -6f; + private const float AmbienceVolumeOffset = -6f; + private const float SfxVolumeOffset = -3f; + + + public static void Register(string characterId, string modAudioResRoot) + { + if (string.IsNullOrWhiteSpace(characterId) || string.IsNullOrWhiteSpace(modAudioResRoot)) + return; + CharacterIdToRoot[characterId] = modAudioResRoot.TrimEnd('/'); + } + + /// + /// Scans loaded assemblies for concrete types, + /// instantiates them, and registers non-empty . + /// + public static void DiscoverPlaceholderCharacters() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.IsDynamic) + continue; + + IEnumerable types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + types = e.Types.Where(t => t != null)!; + } + + foreach (var type in types) + { + if (type is null || type.IsAbstract || !typeof(PlaceholderCharacterModel).IsAssignableFrom(type)) + continue; + if (type.Assembly == typeof(PlaceholderCharacterModel).Assembly) + continue; + + CustomCharacterModel? instance; + try + { + instance = Activator.CreateInstance(type) as CustomCharacterModel; + } + catch + { + continue; + } + + if (instance == null) + continue; + + var root = instance.ModAudioPath; + if (string.IsNullOrWhiteSpace(root)) + continue; + + var id = instance is PlaceholderCharacterModel ph && !string.IsNullOrWhiteSpace(ph.PlaceholderID) + ? ph.PlaceholderID + : type.Name; + + Register(id, root); + } + } + } + + private static void EnsureDiscovered() + { + if (_discovered) + return; + _discovered = true; + DiscoverPlaceholderCharacters(); + } + + private static bool TryResolveRoot(string characterId, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) + { + EnsureDiscovered(); + return CharacterIdToRoot.TryGetValue(characterId, out root); + } + + private static bool TryResolveRoot(CustomCharacterModel? model, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) + { + root = model?.ModAudioPath?.TrimEnd('/'); + if (!string.IsNullOrEmpty(root)) + return true; + + EnsureDiscovered(); + if (model is PlaceholderCharacterModel ph && !string.IsNullOrWhiteSpace(ph.PlaceholderID) && + CharacterIdToRoot.TryGetValue(ph.PlaceholderID, out root)) + return true; + + return false; + } + + public static void Play(string characterId, string folder, string soundName, float volume = 0f, float pitchVariation = 0f, + float basePitch = 1f) + { + if (!TryResolveRoot(characterId, out var modRoot)) + return; + PlayFromRoot(modRoot, folder, soundName, volume, pitchVariation, basePitch); + } + + public static void Play(CustomCharacterModel model, string folder, string soundName, float volume = 0f, + float pitchVariation = 0f, float basePitch = 1f) + { + if (!TryResolveRoot(model, out var modRoot)) + return; + PlayFromRoot(modRoot, folder, soundName, volume, pitchVariation, basePitch); + } + + private static void PlayFromRoot(string modRoot, string folder, string soundName, float volume, float pitchVariation, + float basePitch) + { + var stream = GetOrLoadStream(modRoot, folder, soundName); + if (stream == null) + return; + + var player = new AudioStreamPlayer + { + Stream = stream, + VolumeDb = volume + SfxVolumeOffset, + Bus = "SFX", + PitchScale = pitchVariation > 0f + ? basePitch + (float)Rng.Chaotic.NextDouble() * 2f * pitchVariation - pitchVariation + : basePitch + }; + + var combatRoom = NCombatRoom.Instance; + if (combatRoom != null) + { + combatRoom.AddChild(player); + player.Play(); + player.Finished += () => player.QueueFree(); + } + } + + /// Legacy-style call: creature parameter ignored (same as per-mod ModAudio). + public static void Play(Creature creature, string characterId, string folder, string soundName, float volume = 0f) => + Play(characterId, folder, soundName, volume); + + public static void PlaySfx(string characterId, string soundName, float volume = 0f, float pitchVariation = 0f) => + Play(characterId, "", soundName, volume, pitchVariation); + + public static void PlayGlobalSfx(string characterId, string soundName, float volume = 0f) + { + if (!TryResolveRoot(characterId, out var modRoot)) + return; + PlayGlobalSfxFromRoot(modRoot, soundName, volume); + } + + public static void PlayGlobalSfx(CustomCharacterModel model, string soundName, float volume = 0f) + { + if (!TryResolveRoot(model, out var modRoot)) + return; + PlayGlobalSfxFromRoot(modRoot, soundName, volume); + } + + private static void PlayGlobalSfxFromRoot(string modRoot, string soundName, float volume) + { + var stream = GetOrLoadStream(modRoot, "", soundName); + if (stream == null) + return; + + var player = new AudioStreamPlayer + { + Stream = stream, + VolumeDb = volume + SfxVolumeOffset, + Bus = "SFX" + }; + + var tree = Engine.GetMainLoop() as SceneTree; + var root = tree?.Root; + if (root == null) + return; + + root.AddChild(player); + player.Play(); + player.Finished += () => player.QueueFree(); + } + + private static AudioStream? GetOrLoadStream(string modRoot, string folder, string soundName) + { + var key = $"{modRoot}|{(string.IsNullOrEmpty(folder) ? soundName : $"{folder}/{soundName}")}"; + if (CachedStreams.TryGetValue(key, out var cached)) + return cached; + + var path = string.IsNullOrEmpty(folder) + ? $"{modRoot}/audio/sfx/{soundName}.ogg" + : $"{modRoot}/audio/sfx/{folder}/{soundName}.ogg"; + var stream = GD.Load(path); + if (stream != null) + CachedStreams[key] = stream; + + return stream; + } + + public static void PlayMusic(string characterId, string[] musicOptions, float volumeDbOffset = 0f) + { + if (musicOptions == null || musicOptions.Length == 0) + return; + if (!TryResolveRoot(characterId, out var modRoot)) + return; + + var musicName = musicOptions[GD.RandRange(0, musicOptions.Length - 1)]; + var path = $"{modRoot}/audio/bgm/{musicName}.ogg"; + + if (_currentMusicPath == path && _musicPlayer?.Playing == true) + return; + + StopMusic(); + + var stream = GD.Load(path); + if (stream == null) + return; + + if (stream is AudioStreamOggVorbis ogg) + ogg.Loop = true; + + _musicPlayer = new AudioStreamPlayer + { + Stream = stream, + Bus = "Master" + }; + + _currentVolumeOffset = volumeDbOffset; + var bgmVolume = SaveManager.Instance.SettingsSave.VolumeBgm; + _musicPlayer.VolumeDb = Mathf.LinearToDb(Mathf.Pow(bgmVolume, 2f)) + _currentVolumeOffset + MusicVolumeOffset; + + var runNode = NRun.Instance; + if (runNode != null) + { + runNode.AddChild(_musicPlayer); + _musicPlayer.Play(); + _currentMusicPath = path; + } + } + + public static void SetMusicVolume(float volume) + { + if (_musicPlayer != null && GodotObject.IsInstanceValid(_musicPlayer)) + _musicPlayer.VolumeDb = Mathf.LinearToDb(Mathf.Pow(volume, 2f)) + _currentVolumeOffset + MusicVolumeOffset; + } + + public static void FadeIn(string characterId, string[] musicOptions, float duration = 1.0f, float volumeDbOffset = 0f) + { + if (musicOptions == null || musicOptions.Length == 0) + return; + if (!TryResolveRoot(characterId, out var modRoot)) + return; + + var musicName = musicOptions[GD.RandRange(0, musicOptions.Length - 1)]; + var path = $"{modRoot}/audio/bgm/{musicName}.ogg"; + + if (_currentMusicPath == path && _musicPlayer?.Playing == true) + return; + + if (_musicPlayer != null && GodotObject.IsInstanceValid(_musicPlayer)) + { + _outgoingFadeTween?.Kill(); + _outgoingPlayer?.QueueFree(); + + _outgoingPlayer = _musicPlayer; + _outgoingFadeTween = _outgoingPlayer.CreateTween(); + _outgoingFadeTween.TweenProperty(_outgoingPlayer, "volume_db", -80f, duration) + .SetTrans(Tween.TransitionType.Sine) + .SetEase(Tween.EaseType.In); + _outgoingFadeTween.TweenCallback(Callable.From(() => + { + _outgoingPlayer?.QueueFree(); + _outgoingPlayer = null; + })); + } + + _fadeTween?.Kill(); + _musicPlayer = null; + _currentMusicPath = null; + + var stream = GD.Load(path); + if (stream == null) + return; + + if (stream is AudioStreamOggVorbis ogg) + ogg.Loop = true; + + _musicPlayer = new AudioStreamPlayer + { + Stream = stream, + Bus = "Master", + VolumeDb = -80f + }; + + _currentVolumeOffset = volumeDbOffset; + + var runNode = NRun.Instance; + if (runNode != null) + { + runNode.AddChild(_musicPlayer); + _musicPlayer.Play(); + _currentMusicPath = path; + + var targetDb = Mathf.LinearToDb(Mathf.Pow(SaveManager.Instance.SettingsSave.VolumeBgm, 2f)) + + _currentVolumeOffset + MusicVolumeOffset; + + _fadeTween = _musicPlayer.CreateTween(); + _fadeTween.TweenProperty(_musicPlayer, "volume_db", targetDb, duration) + .SetTrans(Tween.TransitionType.Sine) + .SetEase(Tween.EaseType.Out); + } + } + + public static void FadeOut(float duration = 1.0f) + { + if (_musicPlayer == null || !GodotObject.IsInstanceValid(_musicPlayer)) + return; + + _fadeTween?.Kill(); + _fadeTween = _musicPlayer.CreateTween(); + _fadeTween.TweenProperty(_musicPlayer, "volume_db", -80f, duration) + .SetTrans(Tween.TransitionType.Sine) + .SetEase(Tween.EaseType.In); + _fadeTween.TweenCallback(Callable.From(StopMusicImmediate)); + } + + private static void StopMusicImmediate() + { + _fadeTween?.Kill(); + _fadeTween = null; + _outgoingFadeTween?.Kill(); + _outgoingFadeTween = null; + + if (_musicPlayer != null && GodotObject.IsInstanceValid(_musicPlayer)) + { + _musicPlayer.Stop(); + _musicPlayer.QueueFree(); + } + _musicPlayer = null; + _currentMusicPath = null; + + if (_outgoingPlayer != null && GodotObject.IsInstanceValid(_outgoingPlayer)) + { + _outgoingPlayer.Stop(); + _outgoingPlayer.QueueFree(); + } + _outgoingPlayer = null; + } + + public static void StopMusic() => StopMusicImmediate(); + + public static bool IsPlayingLegacyMusic() => _musicPlayer?.Playing == true; + + public static void PlayAmbience(string characterId, string ambienceName, float volumeDbOffset = 0f) + { + if (!TryResolveRoot(characterId, out var modRoot)) + return; + + var path = $"{modRoot}/audio/bgm/{ambienceName}.ogg"; + + if (_currentAmbiencePath == path && _ambiencePlayer?.Playing == true) + return; + + StopAmbience(); + + var stream = GD.Load(path); + if (stream == null) + return; + + if (stream is AudioStreamOggVorbis ogg) + ogg.Loop = true; + + _ambiencePlayer = new AudioStreamPlayer + { + Stream = stream, + Bus = "Master" + }; + + var ambienceVolume = SaveManager.Instance.SettingsSave.VolumeAmbience; + _ambiencePlayer.VolumeDb = Mathf.LinearToDb(Mathf.Pow(ambienceVolume, 2f)) + volumeDbOffset + AmbienceVolumeOffset; + + var runNode = NRun.Instance; + if (runNode != null) + { + runNode.AddChild(_ambiencePlayer); + _ambiencePlayer.Play(); + _currentAmbiencePath = path; + } + } + + public static void FadeInAmbience(string characterId, string ambienceName, float duration = 1.0f, + float volumeDbOffset = 0f) + { + if (!TryResolveRoot(characterId, out var modRoot)) + return; + + var path = $"{modRoot}/audio/bgm/{ambienceName}.ogg"; + + if (_currentAmbiencePath == path && _ambiencePlayer?.Playing == true) + return; + + StopAmbience(); + + var stream = GD.Load(path); + if (stream == null) + return; + + if (stream is AudioStreamOggVorbis ogg) + ogg.Loop = true; + + _ambiencePlayer = new AudioStreamPlayer + { + Stream = stream, + Bus = "Master", + VolumeDb = -80f + }; + + var runNode = NRun.Instance; + if (runNode != null) + { + runNode.AddChild(_ambiencePlayer); + _ambiencePlayer.Play(); + _currentAmbiencePath = path; + + var targetDb = Mathf.LinearToDb(Mathf.Pow(SaveManager.Instance.SettingsSave.VolumeAmbience, 2f)) + + volumeDbOffset + AmbienceVolumeOffset; + + _ambienceFadeTween = _ambiencePlayer.CreateTween(); + _ambienceFadeTween.TweenProperty(_ambiencePlayer, "volume_db", targetDb, duration) + .SetTrans(Tween.TransitionType.Sine) + .SetEase(Tween.EaseType.Out); + } + } + + public static void FadeOutAmbience(float duration = 1.0f) + { + if (_ambiencePlayer == null || !GodotObject.IsInstanceValid(_ambiencePlayer)) + return; + + _ambienceFadeTween?.Kill(); + _ambienceFadeTween = _ambiencePlayer.CreateTween(); + _ambienceFadeTween.TweenProperty(_ambiencePlayer, "volume_db", -80f, duration) + .SetTrans(Tween.TransitionType.Sine) + .SetEase(Tween.EaseType.In); + _ambienceFadeTween.TweenCallback(Callable.From(StopAmbience)); + } + + public static void StopAmbience() + { + _ambienceFadeTween?.Kill(); + _ambienceFadeTween = null; + + if (_ambiencePlayer != null && GodotObject.IsInstanceValid(_ambiencePlayer)) + { + _ambiencePlayer.Stop(); + _ambiencePlayer.QueueFree(); + } + _ambiencePlayer = null; + _currentAmbiencePath = null; + } + + public static void SetAmbienceVolume(float volume) + { + if (_ambiencePlayer != null && GodotObject.IsInstanceValid(_ambiencePlayer)) + _ambiencePlayer.VolumeDb = Mathf.LinearToDb(Mathf.Pow(volume, 2f)) + AmbienceVolumeOffset; + } +} From cc6338ab32403215de917a58384fe02fded3a2c0 Mon Sep 17 00:00:00 2001 From: kenca Date: Fri, 20 Mar 2026 12:19:23 -0400 Subject: [PATCH 2/4] Changed Naming to match the Custom***Path that the rest of BaseLib uses --- Abstracts/CustomCharacterModel.cs | 2 +- Utils/ModAudioHub.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Abstracts/CustomCharacterModel.cs b/Abstracts/CustomCharacterModel.cs index 7ab87a9..6782ad3 100644 --- a/Abstracts/CustomCharacterModel.cs +++ b/Abstracts/CustomCharacterModel.cs @@ -54,7 +54,7 @@ public CustomCharacterModel() /// resolves audio/sfx/... and audio/bgm/... under this path. /// Override in your character model, or call directly. Return null to opt out. /// - public virtual string? ModAudioPath => null; + public virtual string? CustomAudioPath => null; //Defaults public override int StartingGold => 99; diff --git a/Utils/ModAudioHub.cs b/Utils/ModAudioHub.cs index 66bdcfa..9ca6f8b 100644 --- a/Utils/ModAudioHub.cs +++ b/Utils/ModAudioHub.cs @@ -12,7 +12,7 @@ namespace BaseLib.Utils; /// /// Central audio helper for mods depending on BaseLib. Resolves streams under each -/// character's (mod res root). +/// character's (mod res root). /// public static class ModAudioHub { @@ -44,7 +44,7 @@ public static void Register(string characterId, string modAudioResRoot) /// /// Scans loaded assemblies for concrete types, - /// instantiates them, and registers non-empty . + /// instantiates them, and registers non-empty . /// public static void DiscoverPlaceholderCharacters() { @@ -83,7 +83,7 @@ public static void DiscoverPlaceholderCharacters() if (instance == null) continue; - var root = instance.ModAudioPath; + var root = instance.CustomAudioPath; if (string.IsNullOrWhiteSpace(root)) continue; @@ -112,7 +112,7 @@ private static bool TryResolveRoot(string characterId, [System.Diagnostics.CodeA private static bool TryResolveRoot(CustomCharacterModel? model, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) { - root = model?.ModAudioPath?.TrimEnd('/'); + root = model?.CustomAudioPath?.TrimEnd('/'); if (!string.IsNullOrEmpty(root)) return true; From 8855948766fa78f0522a8f1c51913ed95b7a7da7 Mon Sep 17 00:00:00 2001 From: kenca Date: Fri, 20 Mar 2026 12:51:08 -0400 Subject: [PATCH 3/4] Made Audio process Easier to use --- Abstracts/CustomCharacterModel.cs | 7 +- Utils/{ModAudioHub.cs => ModAudio.cs} | 168 +++++++++++++++++++++++--- 2 files changed, 153 insertions(+), 22 deletions(-) rename Utils/{ModAudioHub.cs => ModAudio.cs} (69%) diff --git a/Abstracts/CustomCharacterModel.cs b/Abstracts/CustomCharacterModel.cs index 6782ad3..eae1c6e 100644 --- a/Abstracts/CustomCharacterModel.cs +++ b/Abstracts/CustomCharacterModel.cs @@ -19,6 +19,7 @@ public abstract class CustomCharacterModel : CharacterModel, ICustomModel public CustomCharacterModel() { ModelDbCustomCharacters.Register(this); + ModAudio.NotifyCharacterConstructed(this); } /// @@ -50,9 +51,9 @@ public CustomCharacterModel() public virtual string? CustomDeathSfx => null; /// - /// Godot resource root for this mod's packaged audio (e.g. res://mods/my_character). - /// resolves audio/sfx/... and audio/bgm/... under this path. - /// Override in your character model, or call directly. Return null to opt out. + /// Godot path to this mod's audio folder — the folder that directly contains sfx and bgm + /// (e.g. res://ThePaladin/audio). Then use , , etc. + /// Optional: overrides; for id-based calls. /// public virtual string? CustomAudioPath => null; diff --git a/Utils/ModAudioHub.cs b/Utils/ModAudio.cs similarity index 69% rename from Utils/ModAudioHub.cs rename to Utils/ModAudio.cs index 9ca6f8b..7642eb1 100644 --- a/Utils/ModAudioHub.cs +++ b/Utils/ModAudio.cs @@ -1,24 +1,28 @@ using System.Collections.Generic; using System.Reflection; +using BaseLib.Abstracts; using Godot; using MegaCrit.Sts2.Core.Entities.Creatures; using MegaCrit.Sts2.Core.Nodes; using MegaCrit.Sts2.Core.Nodes.Rooms; using MegaCrit.Sts2.Core.Random; using MegaCrit.Sts2.Core.Saves; -using BaseLib.Abstracts; namespace BaseLib.Utils; /// -/// Central audio helper for mods depending on BaseLib. Resolves streams under each -/// character's (mod res root). +/// OGG audio: set to your Godot audio folder (the one that +/// contains sfx and bgm), then e.g. ModAudio.PlaySfx("clip", vol). +/// Layout: {audioRoot}/sfx/... and {audioRoot}/bgm/.... /// -public static class ModAudioHub +public static class ModAudio { private static readonly Dictionary CachedStreams = new(); private static readonly Dictionary CharacterIdToRoot = new(); + private static readonly HashSet DistinctCustomAudioRoots = new(StringComparer.Ordinal); + private static string? _implicitRootFromCharacters; private static bool _discovered; + private static string? _defaultAudioRoot; private static AudioStreamPlayer? _musicPlayer; private static string? _currentMusicPath; @@ -34,7 +38,27 @@ public static class ModAudioHub private const float AmbienceVolumeOffset = -6f; private const float SfxVolumeOffset = -3f; + /// Optional override: same as — path to the audio folder (with sfx / bgm inside). + public static void SetRoot(string? resRoot) + { + _defaultAudioRoot = string.IsNullOrWhiteSpace(resRoot) ? null : resRoot.TrimEnd('/'); + } + + /// Effective root: if set, else inferred from character(s). + public static string? DefaultAudioRoot => _defaultAudioRoot ?? _implicitRootFromCharacters; + + internal static void NotifyCharacterConstructed(CustomCharacterModel model) + { + var p = model.CustomAudioPath?.TrimEnd('/'); + if (string.IsNullOrEmpty(p)) + return; + DistinctCustomAudioRoots.Add(p); + _implicitRootFromCharacters = DistinctCustomAudioRoots.Count == 1 ? p : null; + } + /// Manual id → audio root mapping for ModAudio.Play(characterId, ...) overloads. + /// Character id string (e.g. placeholder id). + /// Path to the audio folder (contains sfx and bgm). public static void Register(string characterId, string modAudioResRoot) { if (string.IsNullOrWhiteSpace(characterId) || string.IsNullOrWhiteSpace(modAudioResRoot)) @@ -43,8 +67,8 @@ public static void Register(string characterId, string modAudioResRoot) } /// - /// Scans loaded assemblies for concrete types, - /// instantiates them, and registers non-empty . + /// Scans assemblies for concrete types and registers their + /// (for id-based overloads). /// public static void DiscoverPlaceholderCharacters() { @@ -104,13 +128,15 @@ private static void EnsureDiscovered() DiscoverPlaceholderCharacters(); } - private static bool TryResolveRoot(string characterId, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) + private static bool TryResolveRoot(string characterId, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) { EnsureDiscovered(); return CharacterIdToRoot.TryGetValue(characterId, out root); } - private static bool TryResolveRoot(CustomCharacterModel? model, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) + private static bool TryResolveRoot(CustomCharacterModel? model, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) { root = model?.CustomAudioPath?.TrimEnd('/'); if (!string.IsNullOrEmpty(root)) @@ -124,8 +150,38 @@ private static bool TryResolveRoot(CustomCharacterModel? model, [System.Diagnost return false; } - public static void Play(string characterId, string folder, string soundName, float volume = 0f, float pitchVariation = 0f, - float basePitch = 1f) + private static bool TryResolveDefaultRoot([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? root) + { + root = _defaultAudioRoot; + if (!string.IsNullOrEmpty(root)) + return true; + + root = _implicitRootFromCharacters; + if (!string.IsNullOrEmpty(root)) + return true; + + EnsureDiscovered(); + root = _implicitRootFromCharacters; + if (!string.IsNullOrEmpty(root)) + return true; + + if (CharacterIdToRoot.Count == 0) + return false; + + string? uniform = null; + foreach (var v in CharacterIdToRoot.Values) + { + uniform ??= v; + if (uniform != v) + return false; + } + + root = uniform!; + return true; + } + + public static void Play(string characterId, string folder, string soundName, float volume = 0f, + float pitchVariation = 0f, float basePitch = 1f) { if (!TryResolveRoot(characterId, out var modRoot)) return; @@ -140,6 +196,15 @@ public static void Play(CustomCharacterModel model, string folder, string soundN PlayFromRoot(modRoot, folder, soundName, volume, pitchVariation, basePitch); } + /// Combat SFX using or your character's . + public static void Play(string folder, string soundName, float volume = 0f, float pitchVariation = 0f, + float basePitch = 1f) + { + if (!TryResolveDefaultRoot(out var modRoot)) + return; + PlayFromRoot(modRoot, folder, soundName, volume, pitchVariation, basePitch); + } + private static void PlayFromRoot(string modRoot, string folder, string soundName, float volume, float pitchVariation, float basePitch) { @@ -166,13 +231,21 @@ private static void PlayFromRoot(string modRoot, string folder, string soundName } } - /// Legacy-style call: creature parameter ignored (same as per-mod ModAudio). + /// Creature + character id (legacy); creature ignored. public static void Play(Creature creature, string characterId, string folder, string soundName, float volume = 0f) => Play(characterId, folder, soundName, volume); + /// Creature ignored; uses resolved mod audio root. + public static void Play(Creature creature, string folder, string soundName, float volume = 0f) => + Play(folder, soundName, volume); + public static void PlaySfx(string characterId, string soundName, float volume = 0f, float pitchVariation = 0f) => Play(characterId, "", soundName, volume, pitchVariation); + /// {audioRoot}/sfx/{soundName}.ogg — see / . + public static void PlaySfx(string soundName, float volume = 0f, float pitchVariation = 0f) => + Play("", soundName, volume, pitchVariation); + public static void PlayGlobalSfx(string characterId, string soundName, float volume = 0f) { if (!TryResolveRoot(characterId, out var modRoot)) @@ -187,6 +260,13 @@ public static void PlayGlobalSfx(CustomCharacterModel model, string soundName, f PlayGlobalSfxFromRoot(modRoot, soundName, volume); } + public static void PlayGlobalSfx(string soundName, float volume = 0f) + { + if (!TryResolveDefaultRoot(out var modRoot)) + return; + PlayGlobalSfxFromRoot(modRoot, soundName, volume); + } + private static void PlayGlobalSfxFromRoot(string modRoot, string soundName, float volume) { var stream = GetOrLoadStream(modRoot, "", soundName); @@ -217,8 +297,8 @@ private static void PlayGlobalSfxFromRoot(string modRoot, string soundName, floa return cached; var path = string.IsNullOrEmpty(folder) - ? $"{modRoot}/audio/sfx/{soundName}.ogg" - : $"{modRoot}/audio/sfx/{folder}/{soundName}.ogg"; + ? $"{modRoot}/sfx/{soundName}.ogg" + : $"{modRoot}/sfx/{folder}/{soundName}.ogg"; var stream = GD.Load(path); if (stream != null) CachedStreams[key] = stream; @@ -232,9 +312,22 @@ public static void PlayMusic(string characterId, string[] musicOptions, float vo return; if (!TryResolveRoot(characterId, out var modRoot)) return; + PlayMusicAtRoot(modRoot, musicOptions, volumeDbOffset); + } + + public static void PlayMusic(string[] musicOptions, float volumeDbOffset = 0f) + { + if (musicOptions == null || musicOptions.Length == 0) + return; + if (!TryResolveDefaultRoot(out var modRoot)) + return; + PlayMusicAtRoot(modRoot, musicOptions, volumeDbOffset); + } + private static void PlayMusicAtRoot(string modRoot, string[] musicOptions, float volumeDbOffset) + { var musicName = musicOptions[GD.RandRange(0, musicOptions.Length - 1)]; - var path = $"{modRoot}/audio/bgm/{musicName}.ogg"; + var path = $"{modRoot}/bgm/{musicName}.ogg"; if (_currentMusicPath == path && _musicPlayer?.Playing == true) return; @@ -273,15 +366,29 @@ public static void SetMusicVolume(float volume) _musicPlayer.VolumeDb = Mathf.LinearToDb(Mathf.Pow(volume, 2f)) + _currentVolumeOffset + MusicVolumeOffset; } - public static void FadeIn(string characterId, string[] musicOptions, float duration = 1.0f, float volumeDbOffset = 0f) + public static void FadeIn(string characterId, string[] musicOptions, float duration = 1.0f, + float volumeDbOffset = 0f) { if (musicOptions == null || musicOptions.Length == 0) return; if (!TryResolveRoot(characterId, out var modRoot)) return; + FadeInAtRoot(modRoot, musicOptions, duration, volumeDbOffset); + } + public static void FadeIn(string[] musicOptions, float duration = 1.0f, float volumeDbOffset = 0f) + { + if (musicOptions == null || musicOptions.Length == 0) + return; + if (!TryResolveDefaultRoot(out var modRoot)) + return; + FadeInAtRoot(modRoot, musicOptions, duration, volumeDbOffset); + } + + private static void FadeInAtRoot(string modRoot, string[] musicOptions, float duration, float volumeDbOffset) + { var musicName = musicOptions[GD.RandRange(0, musicOptions.Length - 1)]; - var path = $"{modRoot}/audio/bgm/{musicName}.ogg"; + var path = $"{modRoot}/bgm/{musicName}.ogg"; if (_currentMusicPath == path && _musicPlayer?.Playing == true) return; @@ -384,8 +491,19 @@ public static void PlayAmbience(string characterId, string ambienceName, float v { if (!TryResolveRoot(characterId, out var modRoot)) return; + PlayAmbienceAtRoot(modRoot, ambienceName, volumeDbOffset); + } - var path = $"{modRoot}/audio/bgm/{ambienceName}.ogg"; + public static void PlayAmbience(string ambienceName, float volumeDbOffset = 0f) + { + if (!TryResolveDefaultRoot(out var modRoot)) + return; + PlayAmbienceAtRoot(modRoot, ambienceName, volumeDbOffset); + } + + private static void PlayAmbienceAtRoot(string modRoot, string ambienceName, float volumeDbOffset) + { + var path = $"{modRoot}/bgm/{ambienceName}.ogg"; if (_currentAmbiencePath == path && _ambiencePlayer?.Playing == true) return; @@ -406,7 +524,8 @@ public static void PlayAmbience(string characterId, string ambienceName, float v }; var ambienceVolume = SaveManager.Instance.SettingsSave.VolumeAmbience; - _ambiencePlayer.VolumeDb = Mathf.LinearToDb(Mathf.Pow(ambienceVolume, 2f)) + volumeDbOffset + AmbienceVolumeOffset; + _ambiencePlayer.VolumeDb = + Mathf.LinearToDb(Mathf.Pow(ambienceVolume, 2f)) + volumeDbOffset + AmbienceVolumeOffset; var runNode = NRun.Instance; if (runNode != null) @@ -422,8 +541,19 @@ public static void FadeInAmbience(string characterId, string ambienceName, float { if (!TryResolveRoot(characterId, out var modRoot)) return; + FadeInAmbienceAtRoot(modRoot, ambienceName, duration, volumeDbOffset); + } + + public static void FadeInAmbience(string ambienceName, float duration = 1.0f, float volumeDbOffset = 0f) + { + if (!TryResolveDefaultRoot(out var modRoot)) + return; + FadeInAmbienceAtRoot(modRoot, ambienceName, duration, volumeDbOffset); + } - var path = $"{modRoot}/audio/bgm/{ambienceName}.ogg"; + private static void FadeInAmbienceAtRoot(string modRoot, string ambienceName, float duration, float volumeDbOffset) + { + var path = $"{modRoot}/bgm/{ambienceName}.ogg"; if (_currentAmbiencePath == path && _ambiencePlayer?.Playing == true) return; From 4cd36c82951b4935d396bc5eb075dc1038527c1c Mon Sep 17 00:00:00 2001 From: kenca Date: Fri, 20 Mar 2026 12:56:14 -0400 Subject: [PATCH 4/4] Added in CustomAudio --- Abstracts/CustomCharacterModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Abstracts/CustomCharacterModel.cs b/Abstracts/CustomCharacterModel.cs index eae1c6e..f63359d 100644 --- a/Abstracts/CustomCharacterModel.cs +++ b/Abstracts/CustomCharacterModel.cs @@ -52,7 +52,7 @@ public CustomCharacterModel() /// /// Godot path to this mod's audio folder — the folder that directly contains sfx and bgm - /// (e.g. res://ThePaladin/audio). Then use , , etc. + /// (e.g. res://TheCharacter/audio). Then use , , etc. /// Optional: overrides; for id-based calls. /// public virtual string? CustomAudioPath => null;