Merge campaign-hover-ui-round: fix #417 — silence world audio at the logout reset
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
0228876a8d
8 changed files with 104 additions and 2 deletions
|
|
@ -24,6 +24,32 @@ What does NOT go here:
|
||||||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||||
|
|
||||||
|
## #417 — World ambience keeps playing (and re-firing) on the character-select screen after the in-world logoff
|
||||||
|
|
||||||
|
**Status:** ✅ FIXED 2026-08-17 (logout-audio round; fix + tests in the same
|
||||||
|
commit as this entry). **Symptom:** log out to character select — the old
|
||||||
|
world's ambient noise continues. **Root cause:** the character-session
|
||||||
|
reset manifest had NO audio step at all. Retail's logoff destroys the
|
||||||
|
world's sound sources WITH the world, so character select is silent; our
|
||||||
|
OpenAL world pool and the ambient scheduler are process-lifetime, so after
|
||||||
|
the reset the playing voices (including the continuous ambient beds) ran
|
||||||
|
on, and `AmbientSoundController.Tick` kept RE-FIRING deadlines against the
|
||||||
|
stale listener (the region stays installed and nothing suspends the
|
||||||
|
controller — `Suspend`/`StopAll` had zero callers outside the class; the
|
||||||
|
existing `WorldGenerationQuiescence` suspend only cycles around generation
|
||||||
|
REPLACES, i.e. teleports, never the logout reset). **Fix:** a new
|
||||||
|
`WorldAudioSessionGate` (engine `SuspendWorldAudio` — stops all sixteen
|
||||||
|
world-pool voices and gates new plays — plus ambient `StopAll` — drops
|
||||||
|
every deadline; the soundscape rebuilds on the next objcell observation
|
||||||
|
exactly as a cell change always did) wired as the reset manifest's new
|
||||||
|
"world audio" step, with the pool reopened at the entered-world edge via
|
||||||
|
the new `LiveSessionEnteredWorldBindings.ResumeWorldAudio` binding
|
||||||
|
(default-null, headless-safe), invoked FIRST in `ApplyEnteredWorld` so no
|
||||||
|
entered-world callback can emit into a closed pool. Covers logout,
|
||||||
|
reconnect, and full-stop uniformly (all run the same manifest). UI-pool
|
||||||
|
sounds (interface bank, portal cues) are untouched by design — retail's
|
||||||
|
logoff plays its cue through the same interface path.
|
||||||
|
|
||||||
## #416 — Character-select roster hover highlight never clears (sweeping the roster leaves every row highlighted)
|
## #416 — Character-select roster hover highlight never clears (sweeping the roster leaves every row highlighted)
|
||||||
|
|
||||||
**Status:** ✅ FIXED 2026-08-17 (same round as #414; fix + tests in the same
|
**Status:** ✅ FIXED 2026-08-17 (same round as #414; fix + tests in the same
|
||||||
|
|
|
||||||
39
src/AcDream.App/Audio/WorldAudioSessionGate.cs
Normal file
39
src/AcDream.App/Audio/WorldAudioSessionGate.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
namespace AcDream.App.Audio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logout-audio round (2026-08-17): the character-session reset's world-audio
|
||||||
|
/// teardown. Retail's logoff destroys every world sound source WITH the world
|
||||||
|
/// (<c>CPlayerSystem::ExecuteLogOff @ 0x0055D780</c> world teardown — the
|
||||||
|
/// DirectSound buffers die with their owners), so the character-select screen
|
||||||
|
/// is silent. acdream's OpenAL voices and the ambient scheduler are
|
||||||
|
/// process-lifetime, so the session reset must stop them explicitly:
|
||||||
|
/// <see cref="SuspendForSessionReset"/> stops all sixteen world-pool voices
|
||||||
|
/// (the continuous ambient beds included) and drops every ambient deadline so
|
||||||
|
/// nothing re-fires against the stale listener at character select. The pool
|
||||||
|
/// re-opens at the next entered-world edge
|
||||||
|
/// (<c>LiveSessionEnteredWorldBindings.ResumeWorldAudio</c>); the ambient
|
||||||
|
/// soundscape needs no explicit resume — the next
|
||||||
|
/// <c>AmbientSoundController.ObserveListener</c> objcell change rebuilds it,
|
||||||
|
/// exactly as a cell change always does.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldAudioSessionGate
|
||||||
|
{
|
||||||
|
private readonly OpenAlAudioEngine _engine;
|
||||||
|
private readonly AmbientSoundController? _ambient;
|
||||||
|
|
||||||
|
public WorldAudioSessionGate(
|
||||||
|
OpenAlAudioEngine engine,
|
||||||
|
AmbientSoundController? ambient)
|
||||||
|
{
|
||||||
|
_engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
||||||
|
_ambient = ambient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SuspendForSessionReset()
|
||||||
|
{
|
||||||
|
_engine.SuspendWorldAudio();
|
||||||
|
_ambient?.StopAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeForWorldEntry() => _engine.ResumeWorldAudio();
|
||||||
|
}
|
||||||
|
|
@ -1118,6 +1118,11 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.SelectionInteractions),
|
live.SelectionInteractions),
|
||||||
new LiveSessionWorldRuntime(
|
new LiveSessionWorldRuntime(
|
||||||
content.Dats,
|
content.Dats,
|
||||||
|
content.Audio?.Engine is { } sessionAudioEngine
|
||||||
|
? new AcDream.App.Audio.WorldAudioSessionGate(
|
||||||
|
sessionAudioEngine,
|
||||||
|
content.Audio.Ambient)
|
||||||
|
: null,
|
||||||
live.WorldState,
|
live.WorldState,
|
||||||
live.LiveEntities,
|
live.LiveEntities,
|
||||||
sessionEvents,
|
sessionEvents,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ internal sealed class LiveSessionResetBindings
|
||||||
public required Action MouseCapture { get; init; }
|
public required Action MouseCapture { get; init; }
|
||||||
public required Action PlayerPresentation { get; init; }
|
public required Action PlayerPresentation { get; init; }
|
||||||
public required Action TeleportPresentation { get; init; }
|
public required Action TeleportPresentation { get; init; }
|
||||||
|
public required Action WorldAudio { get; init; }
|
||||||
public required Action SessionDialogs { get; init; }
|
public required Action SessionDialogs { get; init; }
|
||||||
public required Action SettingsCharacterContext { get; init; }
|
public required Action SettingsCharacterContext { get; init; }
|
||||||
public required Action EquippedChildren { get; init; }
|
public required Action EquippedChildren { get; init; }
|
||||||
|
|
@ -48,6 +49,14 @@ internal static class LiveSessionResetManifest
|
||||||
new("mouse capture", bindings.MouseCapture),
|
new("mouse capture", bindings.MouseCapture),
|
||||||
new("player presentation", bindings.PlayerPresentation),
|
new("player presentation", bindings.PlayerPresentation),
|
||||||
new("teleport presentation", bindings.TeleportPresentation),
|
new("teleport presentation", bindings.TeleportPresentation),
|
||||||
|
// Logout-audio round (2026-08-17): retail's logoff destroys the
|
||||||
|
// world's sound sources with the world; ours must stop the
|
||||||
|
// sixteen world-pool voices (continuous ambient beds included)
|
||||||
|
// and drop the ambient deadlines, or the character-select screen
|
||||||
|
// keeps playing — and re-firing — the old world's ambience. The
|
||||||
|
// pool reopens at the next entered-world edge
|
||||||
|
// (LiveSessionEnteredWorldBindings.ResumeWorldAudio).
|
||||||
|
new("world audio", bindings.WorldAudio),
|
||||||
new("session dialogs", bindings.SessionDialogs),
|
new("session dialogs", bindings.SessionDialogs),
|
||||||
new("settings character context", bindings.SettingsCharacterContext),
|
new("settings character context", bindings.SettingsCharacterContext),
|
||||||
// Attachment projections own GL-backed registrations and must leave
|
// Attachment projections own GL-backed registrations and must leave
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ internal sealed record LiveSessionInteractionRuntime(
|
||||||
|
|
||||||
internal sealed record LiveSessionWorldRuntime(
|
internal sealed record LiveSessionWorldRuntime(
|
||||||
IDatReaderWriter Dats,
|
IDatReaderWriter Dats,
|
||||||
|
// Logout-audio round (2026-08-17): null only when audio is disabled
|
||||||
|
// (ACDREAM_NO_AUDIO / init failure) — the reset step and entered-world
|
||||||
|
// resume both no-op then.
|
||||||
|
Audio.WorldAudioSessionGate? WorldAudio,
|
||||||
GpuWorldState WorldState,
|
GpuWorldState WorldState,
|
||||||
LiveEntityRuntime LiveEntities,
|
LiveEntityRuntime LiveEntities,
|
||||||
LiveEntitySessionController EntitySession,
|
LiveEntitySessionController EntitySession,
|
||||||
|
|
@ -210,7 +214,11 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
},
|
},
|
||||||
SyncToolbar: () => _ui.RetailUi?.SyncToolbarWindowButtons(),
|
SyncToolbar: () => _ui.RetailUi?.SyncToolbarWindowButtons(),
|
||||||
LoadCharacterSettings: _interaction.Settings.LoadCharacterContext,
|
LoadCharacterSettings: _interaction.Settings.LoadCharacterContext,
|
||||||
ArmPlayerModeAutoEntry: _interaction.PlayerModeAutoEntry.Arm),
|
ArmPlayerModeAutoEntry: _interaction.PlayerModeAutoEntry.Arm,
|
||||||
|
// Logout-audio round (2026-08-17): reopen the world-audio
|
||||||
|
// pool the session reset closed (see the reset manifest's
|
||||||
|
// "world audio" step).
|
||||||
|
ResumeWorldAudio: () => _world.WorldAudio?.ResumeForWorldEntry()),
|
||||||
Connecting: (host, port, user) =>
|
Connecting: (host, port, user) =>
|
||||||
_domain.Communication.Chat.OnSystemMessage(
|
_domain.Communication.Chat.OnSystemMessage(
|
||||||
$"connecting to {host}:{port} as {user}",
|
$"connecting to {host}:{port} as {user}",
|
||||||
|
|
@ -251,6 +259,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
PlayerPresentation = ResetPlayerPresentation,
|
PlayerPresentation = ResetPlayerPresentation,
|
||||||
TeleportPresentation =
|
TeleportPresentation =
|
||||||
_world.Teleport.ResetGenerationPresentation,
|
_world.Teleport.ResetGenerationPresentation,
|
||||||
|
WorldAudio = () => _world.WorldAudio?.SuspendForSessionReset(),
|
||||||
SessionDialogs = () => _ui.RetailUi?.ResetSessionTransientUi(),
|
SessionDialogs = () => _ui.RetailUi?.ResetSessionTransientUi(),
|
||||||
SettingsCharacterContext =
|
SettingsCharacterContext =
|
||||||
_interaction.Settings.RestoreDefaultCharacterContext,
|
_interaction.Settings.RestoreDefaultCharacterContext,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,13 @@ public sealed record LiveSessionEnteredWorldBindings(
|
||||||
Action RestoreLayout,
|
Action RestoreLayout,
|
||||||
Action SyncToolbar,
|
Action SyncToolbar,
|
||||||
Action<string> LoadCharacterSettings,
|
Action<string> LoadCharacterSettings,
|
||||||
Action ArmPlayerModeAutoEntry);
|
Action ArmPlayerModeAutoEntry,
|
||||||
|
/// <summary>Logout-audio round (2026-08-17): re-enables world-pool audio
|
||||||
|
/// after the session reset's suspend (retail's logoff tears the world's
|
||||||
|
/// sound sources down with the world; ours must re-open the pool at the
|
||||||
|
/// next world entry). Default no-op preserves headless and existing
|
||||||
|
/// construction sites.</summary>
|
||||||
|
Action? ResumeWorldAudio = null);
|
||||||
|
|
||||||
public sealed record LiveSessionHostBindings(
|
public sealed record LiveSessionHostBindings(
|
||||||
LiveSessionRoutingFactories Routing,
|
LiveSessionRoutingFactories Routing,
|
||||||
|
|
@ -269,6 +275,11 @@ public sealed class LiveSessionHost
|
||||||
private void ApplyEnteredWorld(LiveSessionCharacterSelection selection)
|
private void ApplyEnteredWorld(LiveSessionCharacterSelection selection)
|
||||||
{
|
{
|
||||||
string name = selection.CharacterName;
|
string name = selection.CharacterName;
|
||||||
|
// FIRST: the world-audio pool must be open before any entered-world
|
||||||
|
// callback can emit a sound (retail never closes its pool across a
|
||||||
|
// logoff — the sources die with the world — so the reopened pool is
|
||||||
|
// the earliest faithful moment).
|
||||||
|
_enteredWorld.ResumeWorldAudio?.Invoke();
|
||||||
_enteredWorld.SetActiveCharacter(name);
|
_enteredWorld.SetActiveCharacter(name);
|
||||||
_enteredWorld.RestoreLayout();
|
_enteredWorld.RestoreLayout();
|
||||||
_enteredWorld.SyncToolbar();
|
_enteredWorld.SyncToolbar();
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ public sealed class LiveSessionResetPlanTests
|
||||||
MouseCapture = Stage("mouse capture"),
|
MouseCapture = Stage("mouse capture"),
|
||||||
PlayerPresentation = Stage("player presentation"),
|
PlayerPresentation = Stage("player presentation"),
|
||||||
TeleportPresentation = Stage("teleport presentation"),
|
TeleportPresentation = Stage("teleport presentation"),
|
||||||
|
WorldAudio = Stage("world audio"),
|
||||||
SessionDialogs = Stage("session dialogs"),
|
SessionDialogs = Stage("session dialogs"),
|
||||||
SettingsCharacterContext = Stage("settings character context"),
|
SettingsCharacterContext = Stage("settings character context"),
|
||||||
EquippedChildren = Stage("equipped children"),
|
EquippedChildren = Stage("equipped children"),
|
||||||
|
|
@ -295,6 +296,7 @@ public sealed class LiveSessionResetPlanTests
|
||||||
"mouse capture",
|
"mouse capture",
|
||||||
"player presentation",
|
"player presentation",
|
||||||
"teleport presentation",
|
"teleport presentation",
|
||||||
|
"world audio",
|
||||||
"session dialogs",
|
"session dialogs",
|
||||||
"settings character context",
|
"settings character context",
|
||||||
"equipped children",
|
"equipped children",
|
||||||
|
|
|
||||||
|
|
@ -1019,6 +1019,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
MouseCapture = noop,
|
MouseCapture = noop,
|
||||||
PlayerPresentation = noop,
|
PlayerPresentation = noop,
|
||||||
TeleportPresentation = noop,
|
TeleportPresentation = noop,
|
||||||
|
WorldAudio = noop,
|
||||||
SessionDialogs = noop,
|
SessionDialogs = noop,
|
||||||
SettingsCharacterContext = noop,
|
SettingsCharacterContext = noop,
|
||||||
EquippedChildren = noop,
|
EquippedChildren = noop,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue