Skip to content

Commit 60df184

Browse files
committed
chore(release): merge dev into main for v0.0.62
2 parents ed2f1e3 + 8c32dce commit 60df184

55 files changed

Lines changed: 3202 additions & 428 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System.Collections.Concurrent;
2+
3+
namespace STS2RitsuLib.Audio
4+
{
5+
/// <summary>
6+
/// Coordinates adaptive room/combat/victory music playback in response to game lifecycle transitions.
7+
/// </summary>
8+
public sealed class AudioAdaptiveMusicDirector : IDisposable
9+
{
10+
private readonly ConcurrentDictionary<AudioAdaptiveMusicHandle, AudioAdaptiveMusicPlan> _active = new();
11+
private readonly IDisposable _combatEndedSubscription;
12+
private readonly IDisposable _combatStartingSubscription;
13+
private readonly IDisposable _combatVictorySubscription;
14+
private readonly IDisposable _roomEnteredSubscription;
15+
private readonly IDisposable _runEndedSubscription;
16+
private readonly IDisposable _runLoadedSubscription;
17+
private readonly IDisposable _runStartedSubscription;
18+
19+
private AudioAdaptiveMusicDirector()
20+
{
21+
_runStartedSubscription = RitsuLibFramework.SubscribeLifecycle<RunStartedEvent>(_ => RefreshRoomState());
22+
_runLoadedSubscription = RitsuLibFramework.SubscribeLifecycle<RunLoadedEvent>(_ => RefreshRoomState());
23+
_roomEnteredSubscription = RitsuLibFramework.SubscribeLifecycle<RoomEnteredEvent>(_ => RefreshRoomState());
24+
_combatStartingSubscription =
25+
RitsuLibFramework.SubscribeLifecycle<CombatStartingEvent>(_ => SwitchCombatState());
26+
_combatVictorySubscription =
27+
RitsuLibFramework.SubscribeLifecycle<CombatVictoryEvent>(_ => SwitchVictoryState());
28+
_combatEndedSubscription =
29+
RitsuLibFramework.SubscribeLifecycle<CombatEndedEvent>(_ => RestoreAfterCombat());
30+
_runEndedSubscription = RitsuLibFramework.SubscribeLifecycle<RunEndedEvent>(_ => ClearAll(false));
31+
}
32+
33+
/// <summary>
34+
/// Shared singleton director.
35+
/// </summary>
36+
public static AudioAdaptiveMusicDirector Shared { get; } = new();
37+
38+
/// <summary>
39+
/// Disposes framework lifecycle subscriptions owned by this director.
40+
/// </summary>
41+
public void Dispose()
42+
{
43+
_runStartedSubscription.Dispose();
44+
_runLoadedSubscription.Dispose();
45+
_roomEnteredSubscription.Dispose();
46+
_combatStartingSubscription.Dispose();
47+
_combatVictorySubscription.Dispose();
48+
_combatEndedSubscription.Dispose();
49+
_runEndedSubscription.Dispose();
50+
}
51+
52+
/// <summary>
53+
/// Starts following the supplied adaptive music plan and returns a handle for later shutdown.
54+
/// </summary>
55+
public AudioAdaptiveMusicHandle Attach(AudioAdaptiveMusicPlan plan)
56+
{
57+
var handle = new AudioAdaptiveMusicHandle(plan);
58+
_active[handle] = plan;
59+
RefreshRoomState(handle, plan);
60+
return handle;
61+
}
62+
63+
/// <summary>
64+
/// Removes a previously attached adaptive music handle from lifecycle tracking.
65+
/// </summary>
66+
public void Detach(AudioAdaptiveMusicHandle handle)
67+
{
68+
_active.TryRemove(handle, out _);
69+
}
70+
71+
private void RefreshRoomState()
72+
{
73+
foreach (var pair in _active)
74+
RefreshRoomState(pair.Key, pair.Value);
75+
}
76+
77+
private static void RefreshRoomState(AudioAdaptiveMusicHandle handle, AudioAdaptiveMusicPlan plan)
78+
{
79+
if (plan.RoomSource is null)
80+
{
81+
if (plan.RefreshVanillaRoomStateOnRoomEnter)
82+
AudioVanillaBridge.RefreshTrackAndAmbience();
83+
return;
84+
}
85+
86+
var music = GameFmod.Playback.PlayMusic(plan.RoomSource, plan.RoomOptions);
87+
handle.SwitchTo(music);
88+
}
89+
90+
private void SwitchCombatState()
91+
{
92+
foreach (var pair in _active)
93+
{
94+
if (pair.Value.CombatSource is null)
95+
continue;
96+
97+
var music = GameFmod.Playback.PlayMusic(pair.Value.CombatSource, pair.Value.CombatOptions);
98+
pair.Key.SwitchTo(music);
99+
}
100+
}
101+
102+
private void SwitchVictoryState()
103+
{
104+
foreach (var pair in _active)
105+
{
106+
if (pair.Value.VictorySource is null)
107+
continue;
108+
109+
var music = GameFmod.Playback.PlayMusic(pair.Value.VictorySource, pair.Value.VictoryOptions);
110+
pair.Key.SwitchTo(music);
111+
}
112+
}
113+
114+
private void RestoreAfterCombat()
115+
{
116+
foreach (var pair in _active)
117+
{
118+
if (pair.Value.RestoreVanillaMusicOnCombatEnd)
119+
{
120+
pair.Key.Stop();
121+
continue;
122+
}
123+
124+
RefreshRoomState(pair.Key, pair.Value);
125+
}
126+
}
127+
128+
private void ClearAll(bool restoreVanillaMusic)
129+
{
130+
foreach (var handle in _active.Keys)
131+
handle.Stop(restoreVanillaMusic);
132+
133+
_active.Clear();
134+
}
135+
}
136+
}

Audio/AudioAdaptiveMusicHandle.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
namespace STS2RitsuLib.Audio
2+
{
3+
/// <summary>
4+
/// Represents an active adaptive music binding that can switch tracks and restore vanilla state when stopped.
5+
/// </summary>
6+
public sealed class AudioAdaptiveMusicHandle : IDisposable
7+
{
8+
private readonly AudioAdaptiveMusicPlan _plan;
9+
private AudioMusicHandle? _current;
10+
private bool _disposed;
11+
12+
internal AudioAdaptiveMusicHandle(AudioAdaptiveMusicPlan plan)
13+
{
14+
_plan = plan;
15+
}
16+
17+
/// <summary>
18+
/// Stops adaptive playback and unregisters this handle from the shared director.
19+
/// </summary>
20+
public void Dispose()
21+
{
22+
if (_disposed)
23+
return;
24+
25+
_disposed = true;
26+
Stop();
27+
AudioAdaptiveMusicDirector.Shared.Detach(this);
28+
}
29+
30+
internal void SwitchTo(AudioMusicHandle? handle)
31+
{
32+
_current?.Dispose();
33+
_current = handle;
34+
}
35+
36+
internal void RefreshVolume(float volume)
37+
{
38+
_current?.TrySetVolume(volume);
39+
}
40+
41+
/// <summary>
42+
/// Stops the current adaptive override and optionally restores vanilla run music.
43+
/// </summary>
44+
public void Stop(bool restoreVanillaMusic = true)
45+
{
46+
if (_disposed)
47+
return;
48+
49+
_current?.Dispose();
50+
_current = null;
51+
52+
if (restoreVanillaMusic && _plan.RestoreVanillaMusicOnStop)
53+
AudioVanillaBridge.RefreshRunMusic();
54+
}
55+
}
56+
}

Audio/AudioAdaptiveMusicPlan.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace STS2RitsuLib.Audio
2+
{
3+
/// <summary>
4+
/// Declares room/combat/victory music sources that should follow the game's lifecycle transitions.
5+
/// </summary>
6+
public sealed class AudioAdaptiveMusicPlan
7+
{
8+
/// <summary>
9+
/// Music source to use while the player is in a room outside combat.
10+
/// </summary>
11+
public AudioSource? RoomSource { get; init; }
12+
13+
/// <summary>
14+
/// Music source to use while combat is active.
15+
/// </summary>
16+
public AudioSource? CombatSource { get; init; }
17+
18+
/// <summary>
19+
/// Music source to use after combat victory, when provided.
20+
/// </summary>
21+
public AudioSource? VictorySource { get; init; }
22+
23+
/// <summary>
24+
/// Restores vanilla run music when the adaptive handle is stopped.
25+
/// </summary>
26+
public bool RestoreVanillaMusicOnStop { get; init; } = true;
27+
28+
/// <summary>
29+
/// Restores vanilla run music after combat ends instead of returning to the room override.
30+
/// </summary>
31+
public bool RestoreVanillaMusicOnCombatEnd { get; init; } = true;
32+
33+
/// <summary>
34+
/// Refreshes vanilla room track and ambience when entering a room with no room override.
35+
/// </summary>
36+
public bool RefreshVanillaRoomStateOnRoomEnter { get; init; } = true;
37+
38+
/// <summary>
39+
/// Playback options applied when starting room music.
40+
/// </summary>
41+
public AudioPlaybackOptions RoomOptions { get; init; } = new() { Scope = AudioLifecycleScope.Room };
42+
43+
/// <summary>
44+
/// Playback options applied when starting combat music.
45+
/// </summary>
46+
public AudioPlaybackOptions CombatOptions { get; init; } = new() { Scope = AudioLifecycleScope.Combat };
47+
48+
/// <summary>
49+
/// Playback options applied when starting victory music.
50+
/// </summary>
51+
public AudioPlaybackOptions VictoryOptions { get; init; } = new() { Scope = AudioLifecycleScope.Combat };
52+
}
53+
}

Audio/AudioAdaptivePlans.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace STS2RitsuLib.Audio
2+
{
3+
/// <summary>
4+
/// Convenience builders for common adaptive music override patterns.
5+
/// </summary>
6+
public static class AudioAdaptivePlans
7+
{
8+
/// <summary>
9+
/// Builds a plan that overrides combat music and optionally room and victory transitions.
10+
/// </summary>
11+
public static AudioAdaptiveMusicPlan CombatOverride(
12+
AudioSource combatSource,
13+
AudioSource? roomSource = null,
14+
AudioSource? victorySource = null,
15+
AudioPlaybackOptions? combatOptions = null,
16+
AudioPlaybackOptions? roomOptions = null,
17+
AudioPlaybackOptions? victoryOptions = null)
18+
{
19+
return new()
20+
{
21+
RoomSource = roomSource,
22+
CombatSource = combatSource,
23+
VictorySource = victorySource,
24+
RoomOptions = roomOptions ?? new AudioPlaybackOptions { Scope = AudioLifecycleScope.Room },
25+
CombatOptions = combatOptions ?? new AudioPlaybackOptions { Scope = AudioLifecycleScope.Combat },
26+
VictoryOptions = victoryOptions ?? new AudioPlaybackOptions { Scope = AudioLifecycleScope.Combat },
27+
};
28+
}
29+
30+
/// <summary>
31+
/// Builds a plan that supplies room and combat overrides for the full run without restoring vanilla music after
32+
/// combat.
33+
/// </summary>
34+
public static AudioAdaptiveMusicPlan FullRunOverride(
35+
AudioSource roomSource,
36+
AudioSource combatSource,
37+
AudioSource? victorySource = null)
38+
{
39+
return new()
40+
{
41+
RoomSource = roomSource,
42+
CombatSource = combatSource,
43+
VictorySource = victorySource,
44+
RestoreVanillaMusicOnCombatEnd = false,
45+
RoomOptions = new() { Scope = AudioLifecycleScope.Room },
46+
CombatOptions = new() { Scope = AudioLifecycleScope.Combat },
47+
VictoryOptions = new() { Scope = AudioLifecycleScope.Combat },
48+
};
49+
}
50+
}
51+
}

Audio/AudioChannelMode.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace STS2RitsuLib.Audio
2+
{
3+
/// <summary>
4+
/// How a named channel should behave when new playback arrives.
5+
/// </summary>
6+
public enum AudioChannelMode
7+
{
8+
/// <summary>
9+
/// Keep the existing playback and ignore the new request.
10+
/// </summary>
11+
KeepExisting = 0,
12+
13+
/// <summary>
14+
/// Stop the existing playback and replace it with the new one.
15+
/// </summary>
16+
ReplaceExisting = 1,
17+
}
18+
}

0 commit comments

Comments
 (0)