feat(audio): Campaign A slice A4 — the interface sound bus

Retail's UI sound bank was absent, so three families of cue were silent:
the portal enter/exit stingers, the AdminEnvirons dungeon atmosphere
(chanting, drums, whispers, thunder — what players remember as dungeon
'music'), and every other interface slot.

The bank's DID is not a literal anywhere in retail: GetUISoundTable
@0x00563FB0 asks GetByEnum for enum slot 7, and DBCache::GetDIDFromEnum
@0x00413940 resolves it through two EnumIDMap hops off the portal dat
header's master map. UiSoundTableResolver walks that chain the way
RetailCursorResolver already walks it for cursors. Against the shipped
dats it resolves to 0x2000004B, and that table holds exactly the 32 UI_*
slots (UI_EnterPortal 0x6A .. UI_Thunder6 0x8A) — content that confirms
the walk independently of the decode. UiSoundTableResolutionTests pins
the walk, the DID, and the content, and skips when dats are absent.

Two corrections to the research along the way. The lane-5 note recorded
GetByEnum's arguments transposed: the 0x22 it called a fileType is the
CACHE type (CLOCache(cache, CSoundTable::Allocator, 0x22)) and the real
second-hop key is 0x10000003; walking it the other way finds nothing. And
its claim that the interface volume pref applies is wrong — GetAttenuation
with ambient=0 multiplies by the EFFECT knob, so retail's
interface_sound_volume stays the dead knob lane 1 byte-decoded it to be.

EnvironSoundCueMap is an explicit 21-case table read straight out of
Handle_Admin__Environs @0x0055DE20, not arithmetic: codes 0x65..0x72 sit
0x11 below their SoundType, but 0x73/0x74 have no case, so 0x75 lands on
UI_Squeal (0x84) where an offset gives 0x86, and the switch ends at 0x7B
with no 0x7C case. Verified case-by-case against the decomp rather than
from the lane note, whose tail table was ambiguous.

Cues are attached where retail plays them: the teleport-animation
boundary for the portal pair, and the AdminEnvirons handler for the
stingers. PlaySoundFromCenter's pan-0 / distance-0 shape is what
PlayUiWave already implements after A2.

Retires TS-54. Narrows AP-115 to its notice-presentation residual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-08 22:22:09 +02:00
parent 8bc458fb88
commit 6eaa490bb3
12 changed files with 586 additions and 13 deletions

View file

@ -307,7 +307,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |
| ~~AP-114~~ | **RETIRED 2026-07-14 (protection-effect corrective gate)** — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail `Always2D` classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | `src/AcDream.App/Rendering/ParticleRenderer.cs`; `RetailParticleGeometryClassifier.cs`; `particle_mesh.vert/.frag` | — | — | `CPhysicsPart::Draw @ 0x0050D7A0`; `CPhysicsPart::Always2D @ 0x0050D8A0`; `ParticleEmitter::SetInfo @ 0x0051CE90`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` |
| AP-115 | The DAT-authored portal-space viewport, animation `SoundTweakedHook`, and centered repeating `"In Portal Space - Please Wait..."` display string are live, but the separate `ClientUISystem` enter/exit sound enums are not yet presented. **Scope note (2026-08-06):** this row covers the cue's PRESENTATION only. Its five-second arming threshold is a separate, unregistered divergence now filed as AP-150 — retail emits the notice unconditionally per tunnel rotation segment (0.6-1.8 s) and has no such threshold. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/Streaming/LocalPlayerTeleportController.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs` | acdream has no ClientUISystem sound-table-enum resolver yet; inventing direct wave IDs would be less faithful. The notice uses the retained fullscreen UI rather than chat and remains tied to the portal presentation lifetime. | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, animation-authored sound, and centered wait notice, but lacks retail's short UI enter/exit cue sounds. | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
| AP-115 | **NARROWED 2026-08-08 (Campaign A slice A4) — the sound half is landed; only the notice's presentation remains.** The enter/exit cues now play: `LocalPlayerTeleportPresentation.EnterTunnel`/`ExitTunnel` fire `UI_EnterPortal`/`UI_ExitPortal` through the resolved interface sound bank, which is where retail plays them (`0x004D638E` / `0x004D7405`, inside the teleport-animation boundary rather than the tunnel renderer). The DAT-authored portal-space viewport, animation `SoundTweakedHook`, and centered repeating `"In Portal Space - Please Wait..."` display string are live. **Scope note (2026-08-06):** this row covers the cue's PRESENTATION only. Its five-second arming threshold is a separate, unregistered divergence now filed as AP-150 — retail emits the notice unconditionally per tunnel rotation segment (0.6-1.8 s) and has no such threshold. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/Streaming/LocalPlayerTeleportController.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs` | acdream has no ClientUISystem sound-table-enum resolver yet; inventing direct wave IDs would be less faithful. The notice uses the retained fullscreen UI rather than chat and remains tied to the portal presentation lifetime. | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, animation-authored sound, centered wait notice, and (as of A4) retail's short UI enter/exit cue sounds. The residual is that the notice uses the retained fullscreen UI rather than chat, and its five-second arming is AP-150. | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
| AP-116 | Default `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; the `Retail` option restores exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
| AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
| AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative `WieldObject`, a mode that settled without a blocker transition clears immediately; local ACE's observed pre-wield transition plus `ready -> NonCombat`, or post-wield `NonCombat -> ready -> NonCombat`, causes one normal `ChangeCombatMode` request from the trailing notice. Explicit user combat input cancels settlement. Retail's client does not need this extra request against the retail server. | `src/AcDream.App/UI/AutoWieldController.cs`; production binding in `GameWindow.cs` | Local ACE queues a trailing NonCombat callback during primary-weapon replacement and rejects an earlier request while the shuffle is busy; responding to the authoritative notice that completes that exact sequence orders the ordinary request after it without suppressing any server state | A non-ACE server that emits a different intermediate sequence can retain the settlement until a later explicit combat request, replacement, or logout clears it; peace-mode equips send none | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0`; ACE `Player_Inventory.TryShuffleStance` / `TryDequipObjectWithNetworking` |
@ -368,7 +368,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| TS-51 | Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each ordinary object's ParticleManager then ScriptManager inside every admitted `UpdateObjectInternal` quantum; `animate_static_object` instead advances that static owner's ScriptManager then ParticleManager and only then `process_hooks`, using its whole admitted elapsed interval. acdream's shared tail is Particle → Script after static hook capture. | `src/AcDream.App/Update/LiveObjectFrameController.cs` (`LiveObjectFrameController` + `LiveEffectFrameController` shared `_particles.Tick` / `_scripts.Tick` tail); `src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs` | The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and ordinary manager order faithful without pretending the shared tails have per-owner timing or static-tail order. Splitting ownership safely requires a later effect-lifetime slice. | A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static hooks can route before their script/particle managers and static default scripts/particles use render elapsed in Particle → Script order rather than `animate_static_object` elapsed/discard and Script → Particle → hooks timing. | `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum/order |
| TS-52 | The terrain shader applies retail-authored base/overlay/road `TerrainTex.TexTiling` but omits the separate Environment Detail Textures pass and its viewer-distance fade (**#226**). | `src/AcDream.App/Rendering/TerrainAtlas.cs`; `src/AcDream.App/Rendering/TerrainModernRenderer.cs`; `src/AcDream.App/Rendering/Shaders/terrain_modern.frag` | `bb5acab9` fixed the user-visible stretched/blurry regression by porting the distinct base-tiling contract. An earlier experimental detail array darkened the whole ground because its source/neutral blend contract was wrong, so it was correctly reverted rather than guessed into production. | With retail's Environment Detail Textures preference enabled, close terrain lacks the extra high-frequency modulation/fade even though authored base texture scale is correct. | `LScape::GenerateDetailSurfaces` / `SetDetailTexturing @ 0x00506B40`; `ACRender::landPolyDraw @ 0x006B6450..0x006B6525`; issue #226 |
| TS-53 | acdream advances retained UI time on the draw seam and local teleport/UI-camera presentation after its SmartBox-shaped object → inbound network → CommandInterpreter barrier. Retail `Client::UseTime` calls `UIElementManager::UseTime` first, whose global time message reaches `gmSmartBoxUI::UseTime`, and publishes player-camera work from the physics/player callback rather than one post-network camera tail. Slices 67 preserve the accepted host order as ownership-only extractions. | `src/AcDream.App/Update/UpdateFrameOrchestrator.cs` (post-live-frame teleport/camera phases); `src/AcDream.App/Rendering/PrivatePresentationRenderer.cs` (`RetainedGameplayUiFrame.Render`); `docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`; `docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md` | Current retained UI, portal transit, reveal, camera, and connected movement traces are accepted; changing cross-subsystem host order while extracting ownership would combine a behavior change with the structural cutover. | Retained UI, teleport, and camera presentation can observe same-frame object/inbound/player state one host update earlier or later than retail at transition boundaries; a future exact host-order port must prove UI, input, reveal, and camera consequences together. | `Client::UseTime @ 0x00411C40`; `UIElementManager::UseTime`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; `CPhysics::UseTime @ 0x00509950`; retire only with a focused host-order port and connected portal/camera comparison |
| TS-54 | AdminEnvirons sound values `0x65..0x7B` are diagnosed by retail enum name but do not play audio. Retail checks that the local player physics object and UI sound table exist, then calls `SoundManager::PlaySoundFromCenter(Sound_UI_*, table)` for Roar through Thunder6. | `src/AcDream.App/World/WorldEnvironmentController.cs` (`ApplyAdminEnvirons`) | The current audio owner has no typed retail UI-sound-table binding; logging preserves the inbound evidence without inventing wave DIDs or routing the sounds through positional world audio. | Server-authored ambience/thunder packets are silent in acdream while retail plays the centered UI sound. | `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20` (`0x0055E07F..0x0055E2C7`); `SoundManager::PlaySoundFromCenter @ 0x00550950` |
| ~~TS-54~~ | **RETIRED 2026-08-08 (Campaign A slice A4).** The AdminEnvirons stingers now play. `UiSoundController.PlayEnvironCue` maps the change type through `EnvironSoundCueMap` — an EXPLICIT table read case-by-case out of `CPlayerSystem::Handle_Admin__Environs` @ `0x0055DE20` (`0x0055E0C6..0x0055E2C7`), not an offset: codes `0x65..0x72` sit 0x11 below their SoundType but `0x73`/`0x74` have no case at all, so `0x75` lands on `UI_Squeal` (0x84) where arithmetic would give 0x86, and the switch ends at `0x7B`/`UI_Thunder6` with no `0x7C` case. All 21 cases are pinned by conformance tests. The bank itself is no longer a blocker either: the UI sound table's DID is resolved by walking the dats' EnumIDMap chain (`UiSoundTableResolver`, master → slot-7 map → `0x2000004B`), which is how retail finds it — `GetUISoundTable` holds no literal. | retired | — | — | `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20`; `SoundManager::PlaySoundFromCenter @ 0x00550950`; `ClientUISystem::GetUISoundTable @ 0x00563FB0`; `docs/research/2026-08-08-audio-retail-music-absence.md` §5 |
| TS-55 | AdminEnvirons fog values remain a color-only `WeatherSystem.Override` approximation. Retail values 1..5 install authored ambient color/level plus fog color/max; value 6 also forces transition/min/max and blanks radar; Clear restores all override fields and radar; `0x270F` installs a separate authored override. | `src/AcDream.App/World/WorldEnvironmentController.cs` (`ApplyAdminEnvirons`); `src/AcDream.Core/World/WeatherState.cs` (`EnvironOverrideColor`) | Preserves the already accepted enum bridge while Slice 8 moves ownership; porting the complete environment/radar presentation is a separate behavior change requiring focused visual gates. | Forced-fog hue, density, scene ambient, and radar blanking differ from retail; `0x270F` is ignored. | `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20` (`0x0055DE2B..0x0055E344`) |
| TS-57 | No outbound `RejectRetransmit`: a server NAK for an id no longer in the sent-packet cache is dropped silently (counted in `TransportStats.UncachedNakIds`); retail answers `RejectRetransmit @ FlowQueue` so the server abandons the id immediately | `src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs` (`OnRetransmitRequest`) | ACE parses `RejectRetransmit` and no-ops it (NetworkSession.cs — no handler), and the standalone unsequenced form would trip ACE's watermark hole (campaign doc §3 row 3: any cleartext non-ack packet with a live sequence advances the watermark and skips a real packet forever) | Against a server that DOES honor RejectRetransmit, an uncached NAKed id keeps being re-requested until that server's own NAK give-up logic fires — never against ACE, which forgets the id when its next cumulative ack passes it | `RecipientData::ProcessNaks @ 0x00547010`; ACE NetworkSession.cs:299-304 (server-side emit), no client-consume handler |
| TS-58 | No outbound TimeSync/EchoRequest keepalive (retail sends both every 6 half-second intervals, ~3 s). The 2.0 s cumulative AckSequence is the sole idle keepalive; it refreshes ACE's 60 s timeout, which is the only server-side consumer. | `src/AcDream.Core.Net/Transport/TransportClock.cs`; `src/AcDream.Core.Net/Transport/AckNakScheduler.cs` | Standalone unsequenced TimeSync/Echo packets trip ACE's exactly-AckSequence watermark rule (NetworkSession.cs:474-476) and are only ACE-safe piggybacked, which needs retail's CoalesceData (AP-125). The ack keepalive covers the timeout; no transport RTT sample is lost that LinkStatus' app-level ping does not already provide. | No transport-level RTT/latency sample; a future server gating on TimeSync cadence would see silence; ACE's speedhack echo checks never engage. | `ClientFlowQueue::IncrementLocalInterval @ 0x00547F10`; ACE `NetworkSession.cs:474-476`, `Session.cs:101-102` |