diff --git a/README.md b/README.md index 616035ea..b7e9b5fa 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ $env:ACDREAM_TEST_HOST = "127.0.0.1" $env:ACDREAM_TEST_PORT = "9000" $env:ACDREAM_TEST_USER = "testaccount" $env:ACDREAM_TEST_PASS = "testpassword" +$env:ACDREAM_RETAIL_UI = "1" dotnet run --project src\AcDream.App\AcDream.App.csproj -c Release ``` @@ -207,7 +208,7 @@ built-in policies are `idle`, `lifecycle-smoke`, `observer-movement`, and | `ACDREAM_LIVE=1` | Enable connected mode | | `ACDREAM_TEST_HOST` / `ACDREAM_TEST_PORT` | ACE endpoint | | `ACDREAM_TEST_USER` / `ACDREAM_TEST_PASS` | Graphical-client credentials | -| `ACDREAM_RETAIL_UI=0` | Disable the retained retail gameplay UI for diagnostics; it is enabled by default | +| `ACDREAM_RETAIL_UI=1` | Enable the retained retail gameplay UI | | `ACDREAM_DEVTOOLS=1` | Enable ImGui developer tools | | `ACDREAM_NO_AUDIO=1` | Suppress OpenAL initialization | | `ACDREAM_UNCAPPED_RENDER=1` | Disable normal frame pacing for diagnostics | diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 0922068d..756713a2 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,156 +24,10 @@ What does NOT go here: - 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. -## 2026-08-28 open-entry validity audit - -The 88 non-final/active-status headings were checked against current source, -tests, recorded evidence, superseding work, and a clean Release build. The -audit flags 35 as confirmed current, 33 as ghosts/not current client issues, -and 20 as needing a focused current-binary verification. It deliberately -changes no issue status and fixes no product code. See -[`docs/research/2026-08-28-open-issue-validity-audit.md`](research/2026-08-28-open-issue-validity-audit.md). - -The exact 57-ID owner-closed batch was audited separately under the same -read-only rule: 3 have unresolved technical evidence but were explicitly -confirmed closed by the owner, 11 need a focused live gate, and 43 are safe to -remain closed. See -[`docs/research/2026-08-28-owner-closed-issue-validity-audit.md`](research/2026-08-28-owner-closed-issue-validity-audit.md). - -## #456 — Occluded distant buildings/creatures show through structures at raised cameras - -**Status:** OPEN — mechanism established 2026-08-29; needs its own slice. -**Severity:** MEDIUM (visual correctness at pulled-out cameras; the long-standing -"camera outside shows buildings behind terrain" residual, now with a mechanism) -**Component:** pview outdoor shell drawing / dynamics distance admission - -**Symptom (owner, 2026-08-29, Sanctuary cathedral terrace `0xF4180104` -[31.1, 55.7, 169.8]):** zoom the chase camera out and a building in the next -landblock (the `0xF518` shrine, model `0x01001777` at world (47173, 4741, 130), -317 m away) plus purple creatures appear across the lake where structure/ridge -should hide them; zoom in and they vanish. Verified with the punched-cell probe: -zoomed in (root = EnvCell `0xF4180104`, `visible=1`) the shrine never enters the -frame; zoomed out (root = landcell `0xF4180003`) the per-building flood admits -`0xF5180100`/`0xF5180106` at 316.8 m into the punch/shell pipeline. DAT terrain -profile from the true eye: the sight-line is blocked by a z=160 knoll by only -−2.8 m (roof) / −10.3 m (base), and a ~9 m camera raise makes the roofline -geometrically clear — but the screenshots show the WHOLE building, so an -occluder that should cover its lower half is not being drawn. - -**Established mechanism candidates (both retail-cited):** -1. Retail `RenderDeviceD3D::DrawBuilding @0x0059F2A0` draws a building's whole - exterior unconditionally in the block walk (`CPhysicsPart::Draw(parts, 0)`); - only the interior flood is portal-gated. Our shell pass draws only - flood-admitted cells' shells, so intervening cathedral pieces whose cells the - flood did not reach leave a hole the shrine shows through (the owner's - original "wall appears transparent though its geometry is present" read). -2. Retail degrades/skips distant objects (`DrawBuilding`'s - `gfxobj[deg_level] != 0` gate; `GfxObjDegradeInfo::get_degrade` off CYpt in - `CPhysicsPart::UpdateViewerDistance @0x0050E030`) and never draws creatures - at 300 m; we draw dynamics and admit look-in structures at unlimited range. - -**Prescribed next step:** RenderDoc pixel history on an artifact pixel (which -draw owns it, what should have covered it), then port the missing admission -gates. Full evidence chain in -`C:/Users/erikn/.codex/worktrees/16ee/acdream/docs/research/2026-08-29-cathedral-seam-progress.md`. - -## #455 — Clicking an equipped item on the paperdoll does not dequip it - -**Status:** OPEN — needs retail investigation BEFORE any implementation. -**Severity:** MEDIUM (core inventory interaction missing) -**Component:** paperdoll viewport interaction / equipped-item picking - -**Symptom (owner, 2026-08-29):** clicking an equipped item on the inventory -paperdoll does nothing. The owner expects the click to dequip / pick up the -item; the exact retail gesture (single click pickup-to-cursor, click-drag, -or double-click) is NOT yet established and must come from the named decomp, -not a guess. - -**Retail investigation owed:** how `gmPaperDollUI` maps a doll-viewport -click to the equipped object — the hit test against the private -CreatureMode render and the handler that starts the pickup (class anchor -already known: `gmPaperDollUI::RedressCreature @ 0x004A3BC0`; the -click/drag handler functions are unmapped). Compare with the shipped -world-side equipped-child picking (M4 slice 4, user-accepted 2026-07-29), -which may share the pick primitive. - -**acdream side today:** `UiViewport` supports `Clicked`/`ClickedAt` -(viewport-local pixel coords), but the paperdoll wires no equipped-item hit -path at all — the click falls through, so "does nothing" is currently -by construction. - ---- - -## #454 — Timered quest-item pickup leaves the icon barred in the backpack - -**Status:** OPEN -**Severity:** LOW-MEDIUM (icon state presentation) -**Component:** inventory icon overlay state / pickup + quest-timer response -handling - -**Symptom (owner, 2026-08-29, with screenshot):** pick up a boss quest item -that carries a completion timer — ACE replies -`You may complete this quest again in 19h 59m 59s.` The item lands in the -backpack with the BARRED (unusable) icon overlay and stays barred; second -occurrence observed by the owner. Expected: the icon is clear immediately -once the item sits in the pack — the timer belongs in chat, not on the -icon. - -**Mechanism (unestablished — candidates only, verify before fixing):** -(a) the quest-timer/use-refusal response stamps an unusable icon state that -is never cleared when the pickup transaction completes; (b) the icon -overlay state machine misses a refresh on container placement. Retail -oracle to establish: how retail presents a freshly picked-up timered quest -item's icon (expected no bar). - -**Repro:** any boss quest item with a reuse timer on the local ACE server; -pick it up, watch the backpack icon. - ---- - -## #453 — Rain and thunder audio disappears while the Rainy sky remains active - -**Status:** DONE — USER-ACCEPTED 2026-08-28 -**Severity:** MEDIUM (weather presentation loses all authored audio) -**Component:** sky default-script playback / audio hook positioning - -**Symptom (owner):** rain remains visible, but its repeating rain sound and -the thunder noises are missing. - -**Root cause:** the installed Rainy carriers are correctly active and their -PES chains contain the expected `SoundTweaked` hooks: wave `0x0A00038B` -repeats every 2.8 seconds for rain, while `0x33000453` schedules the authored -thunder waves. `SkyPesFrameController` refreshed the carrier's visual pose at -the camera every frame but only set `PhysicsScriptRunner`'s sound-dispatch -anchor when the script was first created. After login, teleport, or ordinary -movement displaced the camera, later hooks played at that stale position and -fell outside retail's audible radius. - -**Fix:** refresh the script-owner anchor from the same current camera pose on -every active-carrier update. The script remains persistent—there is no PES -restart or cadence change—but each later sound hook now dispatches from the -viewer-centered sky cell like retail. A regression test starts a persistent -weather carrier, moves the camera 400+ metres before its delayed sound hook, -and pins the hook to the new camera position. - -**Automated gate:** installed-DAT PES audit confirms the exact rain/thunder -waves and authored cadence; the focused sky lifecycle suite passes 8/8. The -canonical Release gate passes 16,322/16,322 across 14 test assemblies with a -0-warning, 0-error build. - -**Acceptance:** during a Rainy group, the repeating rain bed remains audible -after login, movement, and portal travel, and the lightning carrier's authored -thunder sequence is audible without restarting the weather slot. - -**Owner listening gate 2026-08-28:** PASSED — “Ok good working.” #453 is -closed. - ---- - ## #452 — GLFW can dereference another acdream process's private window pointer after cross-process activation -**Status:** CLOSED 2026-08-28 — exact root fixed; the 100-switch/30-minute -dual-client stress passed 2026-08-27, and both retained `app-release24` -sessions subsequently exited through the graceful character-logoff path. +**Status:** IN-PROGRESS — exact root fixed; 100-switch/30-minute dual-client +stress passed 2026-08-27. Graceful exit of both sessions remains before closure. **Component:** graphical host / GLFW Win32 event pump / multi-process stability. **Severity:** HIGH for multi-account play; one of the two sessions is lost without an orderly disconnect. @@ -240,12 +94,8 @@ Evidence: - Windows Application Error events at 2026-08-27 15:01:39, 15:03:35 and 15:08:58 (same module, exception and offset). -**Closure evidence:** `i451-guard-primary2/stdout.log` records -`logout-confirmed`, return to character select, and orderly plugin teardown; -`i451-guard-secondary/stdout.log` records `graceful logout requested`, -`graceful logout confirmed`, and orderly plugin teardown. Neither process -remains alive. The activation, sustained in-world, and terminal lifecycle -portions of the regression gate all passed. +**Next:** close only after both `app-release24` sessions exit gracefully; the +activation and sustained in-world portions of the regression gate have passed. ## #451 — Sanctuary cathedral portal seam leaks exterior world and particles **Status:** DONE — OWNER-ACCEPTED 2026-08-27 ("Looks good! Gate pass now!"). @@ -522,31 +372,7 @@ authoritative 8→8 reconciliation. Owner check is in ## #443 — Examination/paperdoll private viewport: doll appears only after a delay on first open (was: "renders nothing") -**Status:** FIXED 2026-08-29 — root cause found; owner visual gate pending. - -**2026-08-29 root cause (the "only visible in portal space" recurrence):** -the classic `WbDrawDispatcher.Draw` path appended its transforms into the -SHARED world transform frame with a non-zero base instance, but the default -mesh shaders index every parallel per-instance array (clip slots, light -sets, indoor, OPACITY, selection lighting, detail category) zero-based — -only the packed world submission's shader convention subtracts the -shared-arena prefix. The doll therefore drew all of its instances with -per-instance opacity 0 into a cleared target whenever a world transform -frame was active: counted draws, blank pixels, deterministic. Portal space -worked because the arena is inactive there (ring path, base 0). The -private viewports are the only production consumers of the classic path, -which is why nothing else ever showed the defect. Fix: -`WbDrawDispatcher.NextClassicDrawIsPrivatePass` — private passes always -take the ring transform path (self-contained render state). The same round -also restored per-flight-slot private targets (the f6fe0f2a single-slot -revert relied on cross-command-buffer ordering Vulkan does not guarantee), -moved paperdoll resource preparation into the frame's resource phase ahead -of world composite-budget consumption, made the presenter redress on every -dirty edge, and stopped transient zero handles from erasing a completed -image (session reset is the one explicit clear). Verified live: doll -visible in the normal world, through portal space, and after arrival. - -**Previous status:** FIXED / CONNECTED LIVE RE-GATE PASSED 2026-08-26 — awaiting owner +**Status:** FIXED / CONNECTED LIVE RE-GATE PASSED 2026-08-26 — awaiting owner acceptance. Reopened after the owner again observed a missing paperdoll that appeared only after waiting. Previously marked FIXED / OWNER-ACCEPTED 2026-08-25. The recurrence exposed two remaining gaps: palette/clothing texture @@ -681,7 +507,7 @@ deleted in `ddbd7e40` per the probe-dies rule; no diagnostic flag remains. ## #442 — Flake: DirectionalShadowCasterFrameTests.WarmDenseChangedFrames_AllocateZeroAndReadNoSceneRecords fails intermittently under full parallel suite load -**Status:** FIXED 2026-08-27 by `d123c4b6`; ledger reconciled 2026-08-28. +**Status:** OPEN. **Component:** rendering tests / zero-allocation pins. **Filed:** 2026-08-25 (surfaced by the Campaign AS AS4 Opus review's full-solution run; unrelated to AS4 — the slice touches no rendering code). @@ -697,16 +523,9 @@ up: the established `Lane=Timing` quarantine per `docs/release-gate.md` (do NOT chase individually), or a warmed re-measure loop like other zero-alloc pins use. Do not weaken the assertion itself without measuring. -**Resolution:** the test now uses the repository's shared -`ZeroAllocationProbe.AssertAllocatesNothing` with a 64-frame measurement -batch after its dense-path warmup. The zero-allocation assertion itself is -unchanged; fixed-cost tiering noise no longer masquerades as per-frame -allocation. - ## #441 — Death return to lifestone: stuck in portal once; arrived once with world not ready (missing doors/portals) -**Status:** CLOSED 2026-08-28 — owner-directed closure after the probed -baseline remained healthy and the symptom did not recur. +**Status:** OPEN (intermittent — did not reproduce under probes 2026-08-24). **Component:** reveal generation / death teleport / entity hydration Owner observed twice (before probes were armed), then could NOT reproduce in a @@ -769,14 +588,7 @@ broadcast (`InfoRegion::OnQualityChanged @ 0x004F0EB0` → ## #439 — Flake candidate: LossySession_FivePercentSeeded_ZeroMessageLoss_Headroom256 fails under full parallel suite load -**Status:** FIXED 2026-08-28 — the end-to-end test now runs in the repository's -serialized `Lane=Timing`, because it deliberately includes the real background -receive owner and its result depends on OS scheduling under parallel load. A -20-run isolated stress pass plus the focused transport suite pass. The -investigation also captured a stale queued NAK overtaken by a newer cumulative -ACK; production now suppresses that already-acknowledged encrypted resend, with -a deterministic regression test. The ordinary equal-watermark NAK still -resends, preserving real loss recovery. +**Status:** OPEN (observation filed; do NOT chase individually per docs/release-gate.md) **Severity:** LOW (test-infra) **Filed:** 2026-08-24 (one occurrence during Campaign CA CA2's full-suite run) **Component:** Core.Net.Tests / transport lossy decorator @@ -1132,12 +944,7 @@ delete the claim or make it true. Decide which before touching either. ## #433 — Stale entities from OTHER landblocks visible ("hanging in the air") after portal travel or /ls near Holtburg -**Status:** FIXED 2026-08-28. The retry pump now copies pending receipts into -a retained scratch list instead of allocating `Values.ToArray()` on every -non-empty tick. Scratch is indexed by synchronous call depth so observer -re-entry retains the original snapshot semantics. The warmed one-receipt -fixture fell from 424 B/retry to the event stream's existing 72 B publication -floor; allocation-regression and re-entrant behavior tests pin both results. +**Status:** OPEN **Severity:** MEDIUM (visibly wrong world state after ordinary travel) **Filed:** 2026-08-24 (owner report) **Component:** entity lifecycle / landblock retirement / reveal generation @@ -1580,9 +1387,7 @@ transient parallel-load flake observed once, known-flake class). ## #432 — Sustained ~6.3 MB/frame + ~20 ms/frame while Holtburg town center is in view -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ROOT-CAUSED 2026-08-24 — NOT a production bug; a +**Status:** ROOT-CAUSED 2026-08-24 — NOT a production bug; a measurement-harness mode. Fix verified in a hermetic gate, awaiting commit approval. **Reclassified:** the mode is neither town-specific nor view-triggered — it is active in ANY diagnostics-instrumented run, @@ -1696,23 +1501,11 @@ removed; that theory died with `ca4bae77`.) ## #428 — Sky lightning slot fired 3 CreateParticle hooks with no live pose after a TimeSync day-group flip -**Status:** FIXED 2026-08-28 +**Status:** OPEN **Severity:** LOW (bounded diagnostic spam; no crash, no leak) **Filed:** 2026-08-23 (launch11.err, first production run of the sky default-script port) **Component:** sky / vfx / script lifecycle -**Resolution.** The captured diagnostic could mean either a missing root or a -missing indexed part; here it was the latter, not a hook that survived owner -teardown. Installed-DAT inspection proves the scripted weather Setups, -including lightning carrier `0x02000BA6`, each have one part with an identity -default frame, while `SkyPesFrameController` published a synthetic root with -an empty part array. The legitimate `CreateParticle part=0` therefore failed -even while the carrier was live. Sky carriers now publish that exact identity -part-0 pose. The regression drives a real part-0 particle hook, proves emitter -creation has no diagnostic, flips to a null day group, and proves the stopped -script cannot fire afterward. The existing invalid-index contract remains -strict; this is not a global fallback from arbitrary missing parts to root. - **Symptom:** three identical stderr lines during the startup TimeSync window: `vfx: No live effect pose for owner 0xF8000007, part 0; emitter 0x320002C2 was not created.` Owner `0xF8000007` = post-scene sky slot index 7 — the Rainy lightning carrier @@ -1776,9 +1569,7 @@ edge lands ~96 % fogged. ## #426 — Every solid-colour (untextured) polygon on every object client-wide was invisible: mesh extraction misread NO_POS_UVS as "no positive face" -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED 2026-08-23 (found on the Holtburg windmill axle, GfxObj +**Status:** ✅ FIXED 2026-08-23 (found on the Holtburg windmill axle, GfxObj 0x010010CE, 30.4N 28.2E). **Component:** content extraction (`MeshExtractor`/`GfxObjMesh`) + draw-time classification (`WbDrawDispatcher`) @@ -1884,9 +1675,7 @@ try again. ## #424 — Client crashed on alt-tab out of exclusive fullscreen: zero-area frame reached `RenderPackActivationExtent.Validate` -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED 2026-08-23 (same session it was found — the owner's VM6/VM3 gate launch). +**Status:** ✅ FIXED 2026-08-23 (same session it was found — the owner's VM6/VM3 gate launch). **Component:** rendering / frame orchestration (host minimised-window guard) **Symptom:** exclusive fullscreen 2560×1440, High pack, alt-tab to the desktop → @@ -1994,9 +1783,7 @@ Reference: Campaign AR review F7, ## #420 — Client crashes on the character-select screen (`UiButton.OnDraw` null media-state key) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED 2026-08-19, root cause proven by a reverting test. +**Status:** ✅ FIXED 2026-08-19, root cause proven by a reverting test. **Symptom:** every launcher-started play session died seconds after login. The user's own session evidence (`%LOCALAPPDATA%\acdream\cache\launcher\ sessions\*/`) shows the exact shape three times in a row on 2026-08-19: @@ -2036,9 +1823,7 @@ died there. Distinct from the LU5 UX work. ## #419 — Portal-tunnel rim polygon visible (FOV-coupled) + ring flash at exit (camera dolly vs retail's view-plane animation) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED / OWNER-ACCEPTED 2026-08-25 — the centered disk, 16:9 +**Status:** ✅ FIXED / OWNER-ACCEPTED 2026-08-25 — the centered disk, 16:9 faceted outer rim, and destination lower-viewport hole are gone in the live owner-pak gate. **Symptom:** the tunnel's low-poly mouth shows as a faceted polygon @@ -2203,9 +1988,7 @@ owner-pak live capture, and the owner accepted the final in-game transition on ## #418 — Login world load takes ~27 s: publication advances at a flat 32 blocks/s -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** IN-PROGRESS 2026-08-17 — producer half landed (this commit's +**Status:** IN-PROGRESS 2026-08-17 — producer half landed (this commit's striped `LandblockStreamer` worker pool); the pacer measurably remains on the consumer side. **Symptom:** login holds the portal tunnel ~27 s while the 25×25 window (625 landblocks) drips in at exactly 32 blocks/s @@ -2383,9 +2166,7 @@ adding a per-frame diagnostic tax. ## #417 — World ambience keeps playing (and re-firing) on the character-select screen after the in-world logoff -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED 2026-08-17 (logout-audio round; fix + tests in the same +**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 @@ -2411,9 +2192,7 @@ logoff plays its cue through the same interface path. ## #416 — Character-select roster hover highlight never clears (sweeping the roster leaves every row highlighted) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous 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 commit as this entry). **Symptom:** hovering a roster row highlights it, but moving off leaves the highlight on — sweep all rows and every one stays lit. **Root cause chain (decomp-grounded):** the roster row template @@ -2446,9 +2225,7 @@ commit/cascade/#408 guard), live-DAT spin pin updated to the commit truth. ## #415 — UI-probe `wait world-*` verbs are dead without `ACDREAM_AUTOMATION_ARTIFACT_DIR` (unbound deferred automation wrapper) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED 2026-08-17 (same round as #414/#416; fix in the same +**Status:** ✅ FIXED 2026-08-17 (same round as #414/#416; fix in the same commit as this entry's flip). Filed as "reads the reset snapshot" — that diagnosis was WRONG: the completed reveal KEEPS `WorldViewportObserved` (only the next `BeginRevealCore` clears it). The actual cause: @@ -2467,9 +2244,7 @@ failing with a generic timeout. Test apparatus only — no player impact. ## #414 — Mouse cursor disappears at character select after the in-world logoff (teardown fly-mode fallback raw-captures the cursor) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED 2026-08-17 (entry/exit presentation follow-up; fix + +**Status:** ✅ FIXED 2026-08-17 (entry/exit presentation follow-up; fix + regression tests in the same commit as this entry). **Symptom:** press the indicator bar's X, confirm Yes, land on character select — the OS cursor is gone (and captured). Second Enter still works; the cursor returns once a @@ -2494,13 +2269,12 @@ contract. ## #413 — House tab shows no content (owned-house display, Display* line builders unported) -**Status:** CLOSED 2026-08-28. The complete owned-house builder chain is -ported from the PDB-paired retail binary: purchase/rent payments, bought and -maintenance times, outdoor location, paid/unpaid warning text, purchase-wait -text, and all three authored font-palette states. The previously parsed but -unrouted 0x0227/0x0228 rent-update notices now refresh the same canonical -Runtime owner in both graphical and headless hosts. Synthetic wire/state/UI -coverage passes, and the canonical Release gate passes 16,315/16,315. +**Status:** NARROWED 2026-08-17 (House-tab ownership-text closer session); +item 2's not-expired branch closed same-day at the night-round review fix +round (F8); **the houseless case CORRECTED same-day at the morning gate +round (user finding 2 — see the correction note inside item 2)**. Items 1 +and 2 below are DONE; item 3 (owned-house-only builder content) remains +OPEN and is the entire remaining scope. **What's shipped (this session, on top of Batch C's mount + parser groundwork).** @@ -2581,20 +2355,26 @@ groundwork).** already use (a plain `(layoutId, elementId) -> UiElement` resolve+build, nothing map-specific about it despite the binding's name). -**Final item 3 resolution (2026-08-28):** +**What remains open — item 3, the entire surviving scope:** 3. **The owned-house-only `Display*` line content** (`DisplayBuyPayment`'s OWNED branch — its houseless branch shipped at the 2026-08-17 morning gate correction above — plus `DisplayRentPayment`, `DisplayBuyTime`, `DisplayRentTimes`, `DisplayLocation`, `DisplayWarningText`, all called - from `gmHouseUI::DisplayHouseData @0x004a3380`). Direct x86 disassembly - recovered every hidden literal and arithmetic branch. The Runtime owner - now retains a defensive `HouseData` snapshot, composes payment lists and - pluralization exactly, uses 30-day landscape and 90-day apartment periods, - emits the retail Y-then-X location string, and exposes palette indices - 0/1/2. Rent-time updates clear paid counts; rent-payment updates replace - the list; both rebuild the panel. Apartments omit the location exactly - like retail. + from `gmHouseUI::DisplayHouseData @0x004a3380`). Each is dozens-to-a-few-hundred + lines of heavily FPU/string-mangled BN pseudo-C (PStringBase sprintf + chains, `HousePaymentList` iteration, `IsPaidInFull`/ + `ConstructRentWarningMessage`-style formatting) — genuinely sized as its + own session, and only exercisable once a test character actually owns a + house (not true of `+Acdream` today; `RuntimeHouseState.ApplyHouseData` + is wired and tested against a synthetic `GameEvents.HouseData`, but has + never been exercised against a real ACE-owned house). `DisplayLocation` + is the exception: its own logic is clean (`GetHouseLocation` → + `LandDefs::gid_to_lcoord` → the SAME `(v-0x400)*0.1+0.5` transform the + Map tab already ports via `RadarCoordinates`) but its output STRING + format is BN-mangled the same way the Map tab's coordinate readout was — + reuse whatever resolution that gets if/when #413's map coordinate format + string is independently recovered. **Reference:** `docs/research/2026-08-17-map-house-recon.md` (the recon); `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (this session's owner, @@ -2604,7 +2384,8 @@ AD-107 (the HouseQuery-on-tab-open trigger adaptation); `src/AcDream.Core.Net/Messages/GameEvents.cs` (House parsers), `src/AcDream.Core.Net/GameEventWiring.cs` (delegate holes). -**Acceptance.** The House tab, on a fresh `+Acdream` +**Acceptance test — CLOSED for the houseless case, LIVE-VERIFIED, still the +target for the owned-house case.** The House tab, on a fresh `+Acdream` connect with no owned house, shows "You may buy another house immediately." after the tab is opened (client sends `HouseQuery`, ACE replies `HouseStatus`, `RuntimeHouseState.ApplyHouseStatus` fires, @@ -2623,11 +2404,8 @@ house immediately." and a structural UI dump confirming the House page (`0x100001F7`), its ListBox (`0x100001E6`), and its ONE rendered row (`0x100001E7`, the authored template) all visible and correctly placed. Both launches ended with an ACE-confirmed graceful logout -(`[session] graceful logout confirmed`). The owned-house path is closed by -exact synthetic snapshots covering every type-dependent branch, both rent- -update wire events, and the real authored palette-index seam. No owned house -exists on the available `+Acdream` test character; a future live visual -spot-check is welcome but is not carrying an unimplemented path. +(`[session] graceful logout confirmed`). Still owed: the owned-house case +once item 3 lands. ## #412 — Options panel Config tab content escapes the window frame (footer mid-panel, rows drawing below the window's bottom edge) @@ -2829,13 +2607,7 @@ centered where retail left-aligns; point-fixed in same point-fix). When this issue's client-wide default sweep runs, fix H and V together. -**Status:** FIXED 2026-08-28. The shared importer now maps both justification -axes through retail's exact raw table (1=center, 3/5=far edge, all other -values=near edge), defaults unauthored text to Left/Top, and preserves -explicit Center during inheritance by testing raw property presence rather -than using Center as an “unset” sentinel. Focused UI tests pass 250/250, the -automated App lane passes 6506/6506, and the installed-DAT selected corpus -passes 63/63. +**Status:** OPEN **Severity:** MEDIUM (silently mispositions every DAT-imported `UiText` that relies on the unauthored default, or that authors a raw vertical- justification value other than 1 — currently invisible unless two @@ -2916,9 +2688,7 @@ horizontal `HJustify` mapping while in this code, since it shares the ## #409 — Client-wide UI tooltip system is unshipped (GF-16, deferred out of Campaign CC gate round 1) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** CODE-COMPLETE 2026-08-16; review-fix round F1-F11, the LIVE-FAILURE round, and the hover-feedback completion round (item-cell tooltips + world-object hover tooltip) all landed same day. The live-failure round's own fix is LIVE-VERIFIED (Options -> Character tab tooltip observed on a real connected client, screenshot evidence); the hover-feedback completion round's three items are automated-gate-verified (unit + live-DAT) but the user's connected gate for THOSE items specifically is still owed — see that round's own "Live-verify all three" note. +**Status:** CODE-COMPLETE 2026-08-16; review-fix round F1-F11, the LIVE-FAILURE round, and the hover-feedback completion round (item-cell tooltips + world-object hover tooltip) all landed same day. The live-failure round's own fix is LIVE-VERIFIED (Options -> Character tab tooltip observed on a real connected client, screenshot evidence); the hover-feedback completion round's three items are automated-gate-verified (unit + live-DAT) but the user's connected gate for THOSE items specifically is still owed — see that round's own "Live-verify all three" note. **Severity:** LOW-MEDIUM (cosmetic/discoverability — no gameplay impact, but retail shows a tooltip on hover for authored elements client-wide and acdream showed none before this fix) **2026-08-16 re-derivation + port.** Full re-derivation from @@ -3307,9 +3077,9 @@ mistake #306 already named for a different subsystem; (6) its own connected visual gate — hovering a representative sample across multiple screens (chargen, main game UI, chat, Options) side-by-side with retail. -## #408 — FIXED: General importer-wide honor of dat property 0x3B (Invisible) +## #408 — General importer-wide honor of dat property 0x3B (Invisible) is unshipped (1,083 elements client-wide) -**Status:** FIXED 2026-08-28 +**Status:** OPEN **Severity:** LOW-MEDIUM (cosmetic — extra/leaked elements render where retail hides them; no gameplay/wire impact) Found while fixing GF-13 (Campaign CC gate round 1, Batch A, 2026-08-16): @@ -3366,23 +3136,6 @@ post-children state reapply (measured: 10 combat-layout elements went un-hit-testable, breaking the spell-favorite drag tests, before the scoping was added). -**Resolution (2026-08-28):** the shared importer now applies -`Invisible=true` as the initial `Visible=false` state for every built -widget, while preserving `AuthoredInvisible` for diagnostics. The old -chargen-only visibility walk and the media-child carve-out's duplicate -write are gone. `UiDatElement`, `UiText`, and `UiButton` also apply the -committed state's own `0x3B` value, including DirectState fallback, matching -the same widget-agnostic `UIElement::OnSetAttribute` switch retail uses. -Controllers can still explicitly show an authored-hidden window later, as -retail does; the combat pointer fixtures now model that real mount edge. - -The permanent installed-DAT sweep enumerates the current resolved corpus as -**1,019 authored-invisible elements across 38 layouts** (the earlier 1,083 -count was from the prior installed DAT snapshot). Of those, 990 survive -behavioral-widget child consumption as widgets and **all 990 start hidden**. -Landmarks cover chargen, combat, and chat. The complete automated App lane -passes **6,511/6,511** with the importer-wide behavior enabled. - ## #407 — Windowed resolution offering starves on RDP/virtual displays (video-mode gating) **Status:** DONE (`e601a496`, 2026-08-16 — same gate round, user-directed immediate fix) @@ -3509,19 +3262,22 @@ launch: `started → connected → characterList`, graceful close. ## #404 — ChargenSkillScoreResolver duplicates ChargenTableReader's own SkillTable read -**Status:** FIXED 2026-08-28 +**Status:** OPEN (post-CC cleanup follow-up) **Severity:** LOW **Filed:** 2026-08-16 (Campaign CC CC5 re-review residual round, nit 3) **Component:** `src/AcDream.App/Composition/InteractionRetainedUiComposition.cs` (`ChargenSkillScoreResolver` construction, `:670-672`), `src/AcDream.Content/CharGen/ChargenTableReader.cs` (`:41`, `:61`) -`ChargenSkillScoreResolver`'s constructor took its OWN independent read of +`ChargenSkillScoreResolver`'s constructor takes its OWN independent read of the global SkillTable (portal.dat `0x0E000004`) at composition time (`InteractionRetainedUiComposition.cs:670-672`, `d.Dats.Get(0x0E000004u)`), beside `ChargenTableReader`'s own already-established read of the SAME table -(`ChargenTableReader.cs:41` names the id, `:61` reads it). Two independent reads of +(`ChargenTableReader.cs:41` names the id, `:61` reads it) — which discards +the DAT's `SkillFormula` field entirely (`ChargenTableReader.Project` only +projects `TrainedCost`/`SpecializedCost` per skill into +`ChargenSkillCost`, never `SkillBase.Formula`). Two independent reads of the same DAT file are harmless today (both are read-only, one-shot, under the DAT lock) but are a duplicate-source-of-truth smell: if the two readers ever diverge (a caching change, a future write path), nothing enforces they @@ -3543,25 +3299,9 @@ replacement) takes `ChargenOptions`/a projected formula table instead of a raw `SkillTable`; existing `RetailSkillFormulaTests`/`ChargenTableReaderInstalledDatTests` coverage still passes. -**Resolution (2026-08-28):** `ChargenTableReader` had already gained the -required `ChargenSkillDetail` projection during the CC gate closeout, so the -remaining follow-up was narrower than this original report. The retained-UI -composition now constructs `ChargenSkillScoreResolver` directly from -`Runtime.CharacterCreation.Options`; the resolver consumes the projected -formula/attribute ids and no longer imports or reads a raw `SkillTable`. -`RetailSkillFormula` exposes the same unsigned retail arithmetic for the -projected formula shape, without allocating a DAT object. Focused resolver -tests pass 24/24, synthetic chargen projection tests pass 16/16, and the -installed-DAT `ChargenTableReaderInstalledDatTests` pass 9/9. - ## #403 — Consolidate RetailAnimationCyclePlayback into LiveEntityAnimationPresenter's legacy branch -**Status:** FIXED 2026-08-28. `LiveEntityAnimationPresenter` now delegates -legacy frame advance/wrap and no-sequence part interpolation to the shared -`RetailAnimationCyclePlayback` primitive. Presenter-level regression coverage -pins inclusive-span wrap plus lerp output, including the former negative-rate -edge semantics. Core playback tests pass 11/11 and live-presenter tests pass -14/14. +**Status:** OPEN (post-CC consolidation follow-up) **Severity:** LOW **Filed:** 2026-08-15 (Campaign CC slice CC6b-PRE review fix round, F5) **Component:** `src/AcDream.Core/Physics/RetailAnimationCyclePlayback.cs`, @@ -3594,18 +3334,13 @@ change to any currently-animated NPC. ## #402 — Flaky test: Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate -**Status:** FIXED 2026-08-18 by `dfc841b7`; ledger reconciled 2026-08-28. +**Status:** OPEN (flake, not a regression) **Severity:** LOW (test-infra noise; no known production defect) **Filed:** 2026-08-15 (Campaign CC slice CC4 review fix round, R2 — noticed while running the full App.Tests suite repeatedly for the F1/R1 FixedCanvasSize arbiter gate) **Component:** `tests/AcDream.App.Tests/Streaming/LandblockBuildFactoryTests.cs` -The fix replaced the timing-fragile task observation with a named dedicated -thread, explicit start/block/join bounds, captured worker exceptions, and a -`finally`-protected gate release. It still proves the supplied monitor is the -serialization owner; focused revalidation passes. - `Build_UsesTheSuppliedSharedReaderGate` fails intermittently in full-suite runs (observed roughly 2 of 5 runs) but passes reliably when run in isolation (`--filter FullyQualifiedName~Build_UsesTheSuppliedSharedReaderGate`). @@ -3629,12 +3364,7 @@ intermittently failing on this test. ## #401 — RetailUi should default ON (opt-out), not per-path forced -**Status:** FIXED 2026-08-28. `RuntimeOptions.Parse` now enables the retained -retail UI by default for every launch path; only the literal value -`ACDREAM_RETAIL_UI=0` opts out. `FromSessionConfig` no longer force-overrides -the parsed value. README and the authoritative launch-options reference now -describe the default-on contract, and parse/session-config regression tests -pin both the default and explicit opt-out. +**Status:** OPEN (product-default decision) **Severity:** MEDIUM (recurrence risk) **Filed:** 2026-08-15 (Campaign LA gate-round-2 batch review, F2) **Component:** `src/AcDream.App/RuntimeOptions.cs` @@ -3660,8 +3390,7 @@ opted out; the forcing is gone; docs updated. ## #400 — Character select: Credits button is ghosted; retail opens gmCreditsUI -**Status:** FIXED 2026-08-28 — the authored credits screen and retail scroll/ -return state machine are mounted from the installed DATs. +**Status:** OPEN (post-LA polish) **Severity:** LOW **Filed:** 2026-08-15 (Campaign LA gate round 2, char-select findings batch) **Component:** `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` @@ -3669,25 +3398,14 @@ return state machine are mounted from the installed DATs. Retail's character-management screen routes the Credits button (`0x100003A3`, listbox-base offset 6 in `gmCharacterManagementUI::ListenToElementMessage @0x004ed5a0`) to -`QueueUIMode(0x10000005)` → `gmCreditsUI` (`Register @0x004e7500`) — a -scrolling credits screen. +`QueueUIMode(0x10000005)` → `gmCreditsUI` (`Register @0x0047a69e`) — a +scrolling credits screen. acdream ghosts the button (visible, disabled, +no invented action — the same treatment as Create Character). Porting +`gmCreditsUI` is its own small screen (authored layout, scroll behavior, +return-to-select) and is deliberately out of Campaign LA's scope. -**Fix:** `CreditsUiController` ports -`gmCreditsUI @0x004e6e70..0x004e79e0`: selected roots `0x10000413` and -`0x10000410` from layout enum `0x10000004`, the localized -`ID_Credits1..N` glyph block, cyclic property-`0x10000005` picture strip, -shared pixel scroll, retail duration formula, completion/input action, Please -Wait dialog, and return to character management. Character management now -enables button `0x100003A3` and suppresses its retained presentation while the -credits mode owns the same authored 800x600 canvas. The installed EoR DAT gate -pins layout `0x21000003`, 2,345 credit strings, all seven picture DIDs, the -20-second section value, and the real Type-12 text child. The focused -controller/character tests pass 25/25 (including installed DAT), and the -standard App Release lane passes 6,519/6,519. - -**Acceptance:** automated behavior and installed-DAT acceptance pass. A visual -look is welcome but no longer hides a missing implementation or leaves the -button ghosted. +**Acceptance:** Credits opens the ported retail credits screen and +returns to character select; button re-enabled. ## #399 — Launcher: no test ever constructs MainWindow, so code-behind defects reach the user gate @@ -3790,9 +3508,9 @@ ever fails, this sink needs the status-stream's credential scanning. ## #397 — Windows: LauncherProcessSupervisor.Stop has no reliable graceful-stop signal for a no-window console host -**Status:** CLOSED 2026-08-28 — the isolated process-group implementation, -real Windows fixtures, and connected Windows supervisor acceptance gate all -pass. +**Status:** IN-PROGRESS — the isolated process-group implementation and real +Windows fixtures are complete; the LA11 connected acceptance row remains +required before closure. **Severity:** MODERATE (a hard-killed `AcDream.Headless` leaves the ACE account session stuck for several minutes — a documented project landmine; see CLAUDE.md "Logout-before-reconnect") @@ -3820,26 +3538,17 @@ and a sibling process group that remains running until it receives its own targeted break. Safe-handle cleanup, early-failure termination, and the Linux SIGINT gate remain covered by the Launcher.Core suite. -**Connected closure evidence (2026-08-28):** the Release headless host entered -the world as `+Acdream`, was stopped through the real Windows -`LauncherProcessSupervisor.Stop` path, received ACE's graceful-logout -confirmation, and emitted `disconnected` plus `exited{code:0,reason:graceful}`. -After the headless host's documented 2.5-second ACE account-release quiescence, -a fresh separately supervised process received the character list, entered the -same character, and repeated the same graceful code-0 shutdown. This is the -intended immediate-reconnect contract; it avoids the former ~3-minute stale -session without pretending ACE releases its account index synchronously with -the wire confirmation. Retained evidence: -`artifacts/live-gates/issue-397-20260828/quiesced-first-status.jsonl` and -`quiesced-second-status.jsonl`. +**Acceptance for closing this issue:** automated process-group and targeted- +signal coverage is complete. Keep the issue IN-PROGRESS until the LA11 live +connected row proves `AcDream.Headless` exits gracefully and ACE clears the +session immediately (not after the ~3-minute stale-session window) when +stopped through `LauncherProcessSupervisor.Stop` on Windows, matching the +Linux SIGINT behavior. ## #396 — Configure Keyboard: no capture-instruction dialog on a mapping-button click -**Status:** CLOSED 2026-08-28 — the capture instruction dialog is mounted and -the mapping-button path was live-verified crash-free; the old visual re-check -label does not leave a known defect open. - -The follow-up crash (`2a81e813`: the wait root's retail class +**Status:** ROOT-CAUSED + FIXED — pending the user's visual re-gate of the +dialog itself. The follow-up crash (`2a81e813`: the wait root's retail class type 0x19 was unmapped in DatWidgetFactory, so the first mapping-button click threw out of OnClick and killed the client) was live-verified fixed 2026-08-14 — the user exercised the mapping-button path, no crash. @@ -3862,9 +3571,7 @@ cancel would race it). ## #395 — Configure Keyboard: key captions show raw enum spellings, not retail's localized key names -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ROOT-CAUSED + FIXED (this commit) — pending the user's re-gate. +**Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's re-gate. Filed 2026-08-14 at the OP8 re-gate (user report: acdream shows "Shift+ShiftLeft" where retail shows "SKIFT" on their Swedish layout). `DescribeChord` printed Silk enum spellings; retail's @@ -3882,9 +3589,7 @@ DirectInput-vs-GetKeyNameText adaptation and the non-Windows fallback). ## #394 — Configure Keyboard: row captions render in the debug bitmap font, not the authored 18px serif -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ROOT-CAUSED + FIXED (this commit) — pending the user's re-gate. +**Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's re-gate. Filed 2026-08-14 at the OP8 re-gate (user side-by-side screenshot: acdream's "Move Forward" label vs retail's serif). The controller-synthesized row caption (`BuildActionRow`'s composed `UiText`) never set `DatFont`, so it @@ -3900,19 +3605,39 @@ and applied to the caption `UiText`. Probe evidence: `KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings` (env-gated, kept). -## #393 — Texture detail options / high-resolution DAT handling +## #393 — Texture detail options: retail's "High Resolution Textures" toggle + Landscape/Environment TextureDetail mip-skip -**Status:** VOID/CLOSED — 2026-08-28 by owner direction. The useful behavior -is already implemented: acdream loads and uses `client_highres.dat` through -both the live DAT path and the baked `acdream.pak` path. The filed claim that -retail exposes a separate "High Resolution Textures" toggle was a research -misread: `ID_Option_HighResChange` is a change-notice dialog associated with -the old Landscape/Environment Texture Detail controls, while -`CLCache::LoadHighResDat @0x004FA250` is product-capability loading, not that -checkbox. The remaining retail behavior deliberately discards the largest -source texture levels to lower quality. The project owner explicitly does not -want that legacy degradation added. No renderer change remains from the -abandoned implementation attempt. +**Status:** OPEN — filed 2026-08-14 from the highres-texture verification +(post-M4 nice-to-have; perf/nostalgia option, no gameplay impact). +acdream today loads `client_highres.dat` unconditionally and always picks +`Textures[0]` — retail's MAXIMUM texture detail, verified end-to-end (live +path AND the baked `acdream.pak`; the 2026-08-14 investigation's evidence +chain). Retail additionally offers two knobs acdream has no equivalent for: + +1. **"High Resolution Textures" toggle** (change-notice string + `ID_Option_HighResChange`; `CLCache::LoadHighResDat @0x004FA250` only + runs when armed). Off = `client_highres.dat` never loads; lookups use + the portal-resident versions. acdream shape: a Config-tab option that + skips the highres fallback in `DatCollectionAdapter.TryGet` (portal + class: `_portal || _highRes`, `DatCollectionAdapter.cs:90`) and + `MeshExtractor`'s explicit highres fallbacks (`MeshExtractor.cs:399,767` + — which currently THROW on a miss; the off-path must degrade the way + retail does, NOT throw). **Research prerequisite:** what retail falls + back to for surfaces whose ONLY copy lives in highres. +2. **Texture detail levels** (`Render_LandscapeTextureDetail` / + `Render_EnvironmentTextureDetail` UIPreferences; the pick + `@0x0044C3C8`): the enum is an INDEX into the texture's source-level + (mip) chain — detail 0 keeps every level, detail N drops the N largest + (`i_1 = m_num - esi_1` keeps levels from index N; + `RenderTexture::ShouldDropHighDetail` can force it under memory + pressure). acdream shape: skip/downsample the top N levels at Vulkan + texture upload — works for live uploads and pak payloads alike (the + downsample is at upload, not bake; NO re-bake needed). + +Also carried from the same investigation: a one-off enumeration proving +portal/highres id sets are disjoint (the "portal wins on overlap" +`TryResolvePreferred` corner — no overlap evidence exists, a ~20-line +tool run closes it). ## #392 — A refused/failed fullscreen enter leaves `fullscreen: true` persisted against a windowed client @@ -4118,42 +3843,30 @@ should shrink AND the image should re-render crisp at the new pixel count (UI elements occupy proportionally more of the window, retail-style), not scale down blurrily; same for a window-edge drag. -## #386 — Vendor category dropdown: authored ListBox is edge-docked — retail sizes the popup to content +## #386 — Vendor category dropdown: authored ListBox is edge-docked — retail would size the popup to content, our shipped 6-row window may diverge -**Status:** DONE 2026-08-28. The named-retail path resolves the earlier -screenshot ambiguity: `VendorItemsUI::OpenVendor @0x004c16d0` inserts only -categories actually present; `UIElement_ListBox::UpdateLayout @0x0046e460` -sums those row heights; `UIElement_Scrollable::ResizeScrollableArea -@0x00474730` broadcasts message `0x32`; and -`UIElement_Menu::ListenToElementMessage` answers it with -`RecalculatePopupSize @0x0046caf0`. Because ListBox -`0x21000043/0x10000350` is docked on all four edges, the popup shrinks or -grows to the complete category list, uncapped. The old registration-timing -hypothesis was false: `MakePopup` registers the popup message route during -menu initialization, before a vendor can be opened. - -Installed-DAT inspection also found effective scrollbar property -`0x79=true` on sibling `0x10000351`. That is retail's -`HideWhenDisabled`: once size-to-content makes viewport equal content, the -scrollbar disappears completely rather than leaving the striped disabled -track seen in the report. `VendorUiController` now enables both -`UiMenu.PopupSizeToContent` and -`UiMenu.PopupScrollbarHideWhenDisabled`; drawing and pointer dispatch share -the hidden-presentation projection. Focused menu/vendor coverage pins short -content, content beyond the old six-row extent, hidden disabled chrome/input, -and the still-visible overflow case. AD-88 is retired. - -**Same-day visual re-gate correction:** the first implementation hid the -scrollbar sprites and input but `UiMenu.InteriorW` still unconditionally -added the authored sibling's 16-pixel width, leaving the empty black -placeholder reported in the second screenshot. Retail owns a real sibling -widget and `UIElement_Scrollbar::UpdateLayout @0x004710d0` applies -`SetVisible(false)` when Disabled + HideDisabled are both set; acdream -procedurally flattens that sibling into `UiMenu`, so the equivalent visible -result requires its effective width to become zero too. The shared -presentation predicate now controls drawing, input, and popup width. Tests -pin 110 px outer width for the hidden short-list case (100 px row + two 5 px -bevels) and 126 px when a real 16 px scrollbar is visible. +**Status:** OPEN — filed 2026-08-13 while fixing #385. The #385 probe +(`OptionsPanelLiveMountProbeTests.ProbeMenuPopupSizingAndTextStyle`, +menuprobe3) measured the vendor category popup's authored ListBox +(`0x21000043/0x10000350`) as edge-docked on all four sides (L=T=R=B=1) — +the exact authored condition that arms retail's +`UIElement_Menu::RecalculatePopupSize @0x0046caf0` size-to-content path +(popup grows/shrinks to the summed row heights, uncapped). Our vendor +dropdown ships G5's fixed 6-row scrollable window instead, which the G5 +retail screenshot ("~one-column-with-scrollbar look") appeared to support +and the vendor connected gate user-passed. The two pieces of evidence +conflict: the decomp mechanism says an 18-category popup should open +full-height (~324 px) with an inert stretched scrollbar strip; the G5 +screenshot was read as a 6-row scroll window. Next step is a retail +side-by-side of the vendor category dropdown specifically (open the +category menu at a vendor with many categories). If retail shows the +full-height popup, flip `UiMenu.PopupSizeToContent = true` in +`VendorUiController` (one line — the mechanism shipped with #385) and +retire the divergence; if retail truly shows a 6-row window, document WHY +the docked ListBox does not trigger RecalculatePopupSize there (a message +routing difference is plausible: the vendor popup's items are inserted +BEFORE `RegisterForElementMessages`, so the 0x32 broadcast may never reach +the menu). Register row AD-88 (unclear) tracks it. ## #385 — Options-panel dropdowns: gold left-aligned text + fixed 6-row popup (retail: white, centered, size-to-content) @@ -4178,11 +3891,8 @@ is #376/#377) — no change. ## #384 — FA6 allegiance-swear bot gate: ACE returns no response to 0x001D swear (no confirmation/0x0020/error) -**Status:** CLOSED AS EXTERNAL ACE BLOCKER 2026-08-28 — the client sends the -swear action; the missing response is server behavior. - -Filed 2026-08-12 at Campaign FA slice FA6. The two-bot headless -fellowship/allegiance connected gate +**Status:** OPEN — filed 2026-08-12 at Campaign FA slice FA6. The two-bot +headless fellowship/allegiance connected gate (`src/AcDream.Headless/Policies/HeadlessBotPolicy.cs`, `FellowshipAllegianceLeaderBotPolicy`/`FellowshipAllegianceRecruitBotPolicy`) ran live against local ACE (`127.0.0.1:9000`, `testaccount`/`+Acdream` as @@ -4254,10 +3964,7 @@ the flag is off. ## #383 — Installed-DAT vs committed-fixture drift: regeneration produces large diffs in existing UI fixtures -**Status:** CLOSED AS FIXTURE MAINTENANCE 2026-08-28 — installed-DAT provenance -drift is an environment/fixture concern, not a client product defect. - -Filed 2026-08-12 at Campaign FA slice FA3. Running the +**Status:** OPEN — filed 2026-08-12 at Campaign FA slice FA3. Running the env-gated fixture generator (`ACDREAM_REGENERATE_UI_FIXTURES=1`) on this machine to dump the NEW social-panel fixture also silently rewrote `keyboard_config_21000009.json` and `options_2100002B.json` with LARGE @@ -4523,11 +4230,7 @@ cap and open a real bordered, scrollable popup on click.** ## #377 — Startup CRASH (0xC0000005 in Glfw.GetVideoMode) when settings.json has `fullscreen: true` -**Status:** CLOSED 2026-08-28 — superseded by #388's state-aware native mode -switch and topology guard. The current binary already completed three -consecutive persisted-fullscreen cold starts, and the later physical-display -gate completed the exact mode-switch path with graceful desktop restoration. -Historical status: NOT REPRODUCIBLE on current code (2026-08-13, display +**Status:** OPEN, NOT REPRODUCIBLE on current code (2026-08-13, display block slice 4 attempt). Three consecutive `fullscreen: true` launches on the exact current binary (post-#387/#389/#391) all reached in-world cleanly at the 2560x1440 desktop mode with the swapchain following @@ -4832,16 +4535,7 @@ symptom. ## #360 — @allegiance/@house management dispatchers only port their simple subcommands -**Status:** CLOSED 2026-08-28. The complete retail command grammar now runs -through one presentation-independent dispatcher shared by the graphical and -headless hosts. Every allegiance, house, and standalone `@motd` branch has its -named-retail argument/refusal behavior and its byte-verified ACE GameAction -packet; unknown nested commands remain client-local. Exact grammar, packet, -sequence, router-lifetime, and both-host regression matrices pass. The -canonical Release gate passes **16,309/16,309** tests with zero failures. -Register row TS-68 is retired. - -**Historical filing:** filed 2026-08-09, Campaign CH slice CH4; corrected +**Status:** OPEN — filed 2026-08-09, Campaign CH slice CH4; corrected 2026-08-09 at the CH4 REJECT-review (Blocker 1). Retail's `@allegiance`/`@all` and `@house`/`@hou` are 12- and 15-subcommand local command dispatchers (`ClientCommunicationSystem::DoAllegiance @ @@ -4895,19 +4589,16 @@ slice CH4). ## #361 — @day / @log / @render pure-local commands recognized in help only, not executed -**Status:** CLOSED 2026-08-28. All three commands now execute locally. -`@log` owns a reconnect-safe chat-log lifecycle. `@day` toggles the canonical -`PersistentAtDay` character option, reproduces retail's two exact replies, -and samples noon landscape lighting without freezing the live sky/fog clock. -`@render radius` accepts retail's 5–25 range and updates the same persisted -landscape-radius preference used by the Config panel; that panel now carries -retail's real `{3,5,8,11,15,25}` payloads. `@render fov` accepts retail's -10–160 range and updates the persisted field of view. Usage, range errors, -`atoi`-style numeric-prefix parsing, ignored extra arguments, and silent -unknown options match the named retail handlers. The graphical command, -render-frame, settings, Config-panel, and router regressions pass; standard -App tests pass 6,542/6,542, UI Abstractions pass 884/884, and the complete -Release solution builds with zero warnings/errors. Register row: TS-69. +**Status:** OPEN — filed 2026-08-09, Campaign CH slice CH4. Three +retail-registered pure-local verbs are not yet wired to real behavior: +`@day` (daylight override — needs a sky/time-of-day hook the renderer +doesn't expose), `@log` (chat-to-file logging — deferred to avoid an +unaudited file-handle lifecycle across session reconnects; see AP/TS-69 +for the reasoning), and `@render` (retail's `SmartBox::HandleRenderOption` +— acdream has no equivalent render-option surface). All three are +recognized by `/help ` (`RetailCommandHelpTable`) with retail's own +extracted help text, but fall through to server passthrough on execution. +Register row: TS-69. **Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH, slice CH4). @@ -5159,10 +4850,7 @@ default case against a real DAT-loaded landblock). ## #370 — Headless jump-probe: the released jump never registers as airborne (proven NOT a threading artifact) -**Status:** CLOSED 2026-08-28 — the current published headless-player path was -reproduced in-process through the real headless command source and frame host: -charged `SetIntent` release becomes airborne with positive vertical velocity. -The historical timeout is stale after the later movement/physics cutovers. +**Status:** OPEN — filed 2026-08-10 during #368's fix verification. **Symptom:** with #368 fixed (one dedicated update thread, thread migration provably gone — the new affinity test pins it), the `jump-probe` @@ -5196,11 +4884,8 @@ the timeout fires seconds after `releasing jump (fire)`. ## #369 — Unconfirmed whether retail's floating chat windows share the main window's currently-selected talk-focus channel -**Status:** CLOSED AS RESEARCH 2026-08-28 — this records an unanswered retail -question, not an observed client mismatch. - -Filed 2026-08-10, Campaign CH slice CH6b (register row AP-188). The floating -chat window LayoutDesc (`0x2100005B`) authors no +**Status:** OPEN — filed 2026-08-10, Campaign CH slice CH6b (register row +AP-188). The floating chat window LayoutDesc (`0x2100005B`) authors no talk-focus menu (`docs/research/2026-08-09-chat-retail-window-shell.md` §2.2 — only the main window's `0x2100006F` has one, element `0x10000014`), so acdream's `FloatingChatWindowController` hardcodes every floaty window's @@ -5230,12 +4915,9 @@ controllers read instead of the main window's private field. ## #366 — Chat window's new-unseen-text indicator (0x1000048C) imports but is never independently wired -**Status:** CLOSED 2026-08-28 — `ChatWindowController` binds the indicator, -tracks unseen text, advances its authored attention sequence, and clears it on -click; focused regression coverage is present. - -Campaign CC gate round 1 Batch C Commit 2 completed the build half. -`LayoutImporter.BuildWidget` gained a `UiText`/`UiField` +**Status:** OPEN, NARROWED 2026-08-16 at Campaign CC gate round 1 Batch C +Commit 2 — the BUILD half of this issue's own "fix shape" recommendation is +now DONE. `LayoutImporter.BuildWidget` gained a `UiText`/`UiField` media-bearing-child carve-out (mirroring `UiMeter`'s own text-overlay carve-out, EXACTLY the shape this issue proposed) as part of a chargen description-box fix; the client-wide blast-radius sweep that fix's own @@ -5243,8 +4925,10 @@ tests run (`LayoutImporterMediaBearingChildSweepTests.MediaBearingChildSweep_EnumeratesEveryAffectedType12Element`) independently re-confirmed `0x1000048C` under `0x10000011` in layout `0x2100006F` as one of the affected elements — it now builds as a real -widget instead of being silently swallowed. The later controller work completed -the behavioral half described above. +widget instead of being silently swallowed. **Still open:** no controller +binds or drives its visible state (STILL the original ask — what triggers +retail's "new text" indicator, and what it does on click, remains +un-researched); this issue stays open for that behavioral half. **Where:** `src/AcDream.App/UI/Layout/ChatWindowController.cs` (behavior, still missing); `src/AcDream.App/UI/Layout/LayoutImporter.cs` @@ -5405,11 +5089,8 @@ this session's hard constraints excluded client launches. ## #359 — 0x019E PlayerKilled line prints to participants — retail suppresses it -**Status:** FIXED 2026-08-28. The live social route now supplies the canonical -local-player GUID to `ChatLog.OnPlayerKilled`; the latter suppresses the -0x019E line for both victim and killer while preserving it for an uninvolved -bystander, matching `ClientCombatSystem::HandlePlayerDeathEvent @0x0056C320`. -Core and live-router regression tests cover all three recipients. +**Status:** OPEN — filed 2026-08-09 at the CH1 Opus review. Pre-existing (not +introduced by CH1); candidate for CH4/CH5. **Symptom:** `ChatLog.OnPlayerKilled` (`src/AcDream.Core/Chat/ChatLog.cs`) always appends the death message for every recipient of the `0x019E` @@ -5653,10 +5334,7 @@ wrap threshold is the authored PIXEL width, not a character count. ## #352 — Vendor range-watcher cylinder metric: discriminating unit test deferred -**Status:** CLOSED AS TEST DEBT 2026-08-28 — the behavior passed its live gate; -the remaining request is optional discriminating coverage, not a vendor defect. - -The EnforceRange cylinder-gap fix +**Status:** OPEN (filed 2026-08-08). The EnforceRange cylinder-gap fix (acceptance-band self-close, vendor-verify-gate.log evidence) landed with the existing 17-range/lifecycle tests green but WITHOUT a unit test that discriminates cylinder-vs-center (needs an IPhysicsObjHost fake — 38 @@ -5688,8 +5366,7 @@ configs. ## #350 — Render-shadow ledger overflow after 2h42m: lifetime int counters in a never-reset accumulator -**Status:** CLOSED 2026-08-28 — the production lifetime counters are `long`; -the old pending-landing label is stale. +**Status:** FIXED IN TREE 2026-08-08 (pending clean-room + landing). **Evidence:** `vendor-buy-gate.log` ~1606 — checked OverflowException in `RenderSceneShadowRuntime.Add` from `UpdateFrameOrchestrator.Tick`, exit 82, 2h42m into a SINGLE world generation (login 20:28 -> crash 23:10, one @@ -5715,9 +5392,7 @@ legitimately unbounded lifetime telemetry that was simply undersized. ## #348 — Render-loop death by Win32 cursor-handle exhaustion: Silk recreates the native cursor on every alternation -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIX IN TREE (2026-08-08) pending the vendor-gate relaunch. +**Status:** FIX IN TREE (2026-08-08) pending the vendor-gate relaunch. **Evidence:** `vendor-gate.log` — `Silk.NET.GLFW.GlfwException: PlatformError: Win32: Failed to create cursor: Not enough memory` thrown from `RetailCursorManager.ApplyGlobal` inside `RenderFrameOrchestrator.Render`, @@ -6089,10 +5764,9 @@ reasoned-from-source diagnoses. ## #346 — `PortalProjectionTests.ProjectToClipLease_ReusesPooledWorkWithoutResultArrays` is a SIXTH load-sensitive flake -**Status:** FIXED 2026-08-18 by `dfc841b7`; ledger reconciled 2026-08-28. LOW. -The test now crosses tiered-JIT thresholds before measurement and accepts the -best of five warmed batches while retaining a threshold hundreds of times -below the former per-call allocation regression. +**Status:** OPEN. LOW. Allocation-count assertion, passed in isolation and on +two subsequent full runs. Same FILE as #302 but a DIFFERENT test — filed +separately per the never-conflate rule. **Filed:** 2026-08-08, observed during #344's suite runs. **2026-08-16 recurrence (Campaign #409 tooltip review-fix round).** The Opus @@ -6186,9 +5860,8 @@ two AD-66 skips retired).** AD-66's register row is retired; the historical flip stands recorded as unexplained-but-unreproducible. AD-69 remains the one follow-up in that block. Pending: the user's hover-look slope gate. -**Status:** CLOSED 2026-08-28 — the relanded fix passed its ten-run, -bit-identical gate; the contradictory historical measurement is retained only -as evidence. +**Status:** OPEN — HIGH priority for the next physics session; the fix itself +is byte-proven, the BLOCKER is that the measurement chain contradicted itself. **Filed:** 2026-08-07 (overnight), at the S4 landing split. Retail's `adjust_offset` safety push-out uses the BARE sphere radius in both @@ -6361,31 +6034,26 @@ stays ACTIVE. **The byte evidence was never the open question — do not ## #342 — `Issue265SteepSlopeCaptureBisectTests.cs:920` is a tautology: `Assert.Equal(x.Z > 0.01f, x.Z > 0.01f)` -**Status:** CLOSED 2026-08-28 — the current assertion compares the old-model -and new-model values rather than the same expression to itself. +**Status:** OPEN. LOW — a dead assertion that can never fail, in the +steep-slope family Campaign S leans on. Found by the S4 review (F8), +out of that slice's scope. Fix = recover the intended comparison from the +test's context, not just delete. **Filed:** 2026-08-07. --- ## #340 — `StreamingWorkBudgetTests.DestinationAndEmptyUnloadPriorityNeverBypassPublicationBudget` is a FIFTH load-sensitive flake -**Status:** FIXED 2026-08-18 by `dfc841b7`; ledger reconciled 2026-08-28. LOW. +**Status:** OPEN. LOW. **Filed:** 2026-08-07 (overnight), first observed in a clean-room full-suite run; passes standalone immediately after. Distinct from #302, #308, #321 and #336 per the never-conflate rule. Same class: load-sensitive, deterministic in isolation. -**Resolution:** `StreamingController` gained an injected meter clock and this -fixture now uses a constant timestamp. Scheduler policy is fully deterministic -under suite load instead of racing the production `Stopwatch`. - --- ## #339 — Stuck in portal space / at login: the reveal never becomes ready — MECHANISM CAUGHT 2026-08-07 evening, full stack -**Status:** CLOSED 2026-08-28 — fixed and live-validated 2026-08-07; the -original filing below is retained as historical evidence. - **BREAK: the crash is caught.** The Session-B gate launch reproduced the hang at LOGIN (cell 0xA8B4002F, all readiness flags False forever) and this time the log carries an unhandled `System.OverflowException` with a full stack: @@ -6449,7 +6117,8 @@ separately below. ## #339 (original) — Stuck in portal space: the destination reveal generation never becomes ready -**Historical status:** OPEN — observed live 2026-08-07, evidence captured. +**Status:** OPEN — observed live 2026-08-07, evidence captured. **Not chased**; +user directed it be fixed later. **Severity:** HIGH when it fires — the session is unrecoverable without closing the client. The player never leaves portal space. **Component:** streaming / world reveal (NOT physics — see below). @@ -6641,9 +6310,7 @@ different mechanisms and need separate gates. ## #337 — Neftet rock plateaus: wedged at the top, jumps sink into the mesh, corpses fall through — FIXED, awaiting live acceptance -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIXED 2026-08-06 by #333's fix — the query-site broadphase reach +**Status:** FIXED 2026-08-06 by #333's fix — the query-site broadphase reach filter is **deleted**, because retail has none. Awaiting the user's live acceptance at the Neftet plateau; the offline gate is `Issue333BroadphaseReachFilterTests.OffCentreBspFloorStopsAFallingMover`, @@ -6797,9 +6464,7 @@ byte-settled; the one-ULP WhichSide tie) live in the narrowed AP-159 row. ## #333 — The shadow broadphase reach filter measures from the PART ORIGIN, so an off-centre BSP part can be in the right cell and still never be tested -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIXED 2026-08-06 — **the filter is deleted, not re-centred.** +**Status:** FIXED 2026-08-06 — **the filter is deleted, not re-centred.** Re-centring it would have kept an invention retail does not have; the disassembly below establishes that retail walks the cell's shadow list unconditionally. Cell membership IS retail's broad phase, and the BSP walk's @@ -7200,7 +6865,7 @@ because the intent is to fix it. ## #325 — Gate A's teleport test is narrower than retail's: a ForcePosition carrying a NEWER teleport stamp is misrouted into a full Apply -**Status:** FIXED 2026-08-28 +**Status:** OPEN **Severity:** MEDIUM (no observed symptom; reachability against ACE is unmeasured — see below. The behaviour when reached is four simultaneous divergences, not one.) @@ -7276,14 +6941,6 @@ covered by a discriminating test at the disposition boundary AND at the classifier's authority validation; the three consumers in item 2 checked against a stale-but-equal teleport pair; AP-148 retired in the same commit. -**Resolution:** Gate A now uses retail's wrap-safe not-older predicate, so an -equal or newer wire TELEPORT_TS takes `ForcePosition` without consuming the -stored teleport channel. Runtime authority accepts a local non-regressed -force pair while still rejecting remote and regressed authority. Core tests -cover ordinary and wrap-boundary newer stamps; merge/route/drive tests prove -the accepted pair remains stale-but-equal, velocity is preserved, no teleport -hook is armed, and the immediate position acknowledgement still occurs. - ## #324 — The graphical and no-window hosts run parallel, non-shared inbound entity routes **Status:** OPEN @@ -7348,23 +7005,12 @@ the same commit; the eight D1 sabotages still discriminate. ## #320 — The local player's canonical cell does not track ordinary movement (follow-up from #319) -**Status:** FIXED 2026-08-28 (the Runtime physics cutover had already retired -the defect; this audit added the missing local-player retirement proof) -**Severity:** CLOSED +**Status:** OPEN +**Severity:** LOW today (no observed symptom — see below); the correctness +question is real and unresolved **Filed:** 2026-08-05, filed in the #319 fix commit per that contract's §2.2/§4/§9 **Component:** physics / entity lifetime / local player canonical cell -**Resolution.** The premise became stale at the per-session Runtime physics -cutover (`7e6033d0`). `RuntimeOrdinaryPhysicsUpdater.Complete` commits the -transition resolver's exact `FullCellId` through -`RuntimePhysicsState.CommitOrdinaryCell`; `CellCommitted` then synchronously -rebuckets the App projection. This is the same canonical transition family as -retail's per-frame `SetPositionInternal`, and it is suppressed while portal -space owns placement. The 2026-08-28 audit changed the existing transition -test into an exact local-player fixture and proved that retiring the source -landblock after the player walks across the boundary does not select or park -the player. No production change was required. - **Description.** Retail writes the local player's cell on EVERY physics tick (`CPhysicsObj::SetPositionInternal` @0x00515330, unconditional). acdream's canonical `FullCellId` for the LOCAL player is written only at three edges: @@ -7473,8 +7119,7 @@ verdict. ## #318 — C4 route 3 §8 items 8/9/10 residual: no end-to-end composition test, no local-player shadow assertion, no T8 ordering -**Status:** CLOSED AS TEST DEBT 2026-08-28 — no current product defect is -established; any desired extra composition coverage belongs in the test plan. +**Status:** OPEN **Severity:** LOW (does not block round-3 acceptance per both reviewers; carried into C5) **Filed:** 2026-08-05, C4 route 3 round-3 review (retail B5/A5, architecture @@ -7533,13 +7178,7 @@ shadow registry specifically. ## #317 — `TryCommitAuthoritativeVelocity`'s call site has no established retail basis -**Status:** FIXED 2026-08-28 — the full accepted-Position chain confirms the -PositionPack vector is only passed into `CPhysicsObj::MoveOrTeleport`, which -never reads it; neither `UnpackPositionEvent` nor the remote branch calls -`set_velocity`. The acdream-only body write was removed. Position velocity is -still retained for server-controlled animation/dead reckoning, while the -separate VectorUpdate route remains the authoritative `set_velocity` path. -The 28-case OnPosition collapse matrix includes a non-overwrite regression. +**Status:** OPEN **Severity:** LOW (tracking only; no known observable defect) **Filed:** 2026-08-04, C4 route 5 (projectile authoritative placement) round-2 review, MINOR (c) @@ -7590,7 +7229,7 @@ carried over from the route 4b-3 round-2 reviews. Evidence: ## #313 — `DeclareValid`'s `SetSelectedObject` split-recovery is not ported -**Status:** FIXED 2026-08-28 +**Status:** OPEN **Severity:** LOW (selection UX, not placement) **Filed:** 2026-08-04 **Component:** UI / inventory / selection @@ -7628,12 +7267,6 @@ recovery window. Out of C4 scope — do not implement as part of a placement- focused change; this is selection UX and mixing it into a placement closure makes the landing un-reviewable (per the route 6 contract). -**Resolution:** `InventoryWorldDropProjectionController` now borrows the -session's canonical `SelectionState` and transfers selection to the recovered -split-result GUID immediately after successful hydration. Tests cover the -matching partial-stack result, the ten-second recovery behavior, and prove a -second unrelated unknown GUID cannot steal selection. - ## #314 — Split recovery throws instead of recovering when the source's retained Movement/ServerControlledMove timestamps are nonzero **Status:** CLOSED 2026-08-04 by `daef7c98` — `BuildSpawn`'s `Timestamps` @@ -7762,7 +7395,7 @@ the delta-review round on the same route's remediation. Evidence: ## #309 — Cancelled lost-cell park re-shows the entity where retail would keep it hidden -**Status:** CLOSED AS ACCEPTED DIVERGENCE 2026-08-28 — user decision 2026-08-06. +**Status:** **DEFERRED as an ACCEPTED DIVERGENCE — user decision 2026-08-06.** The standing record is register row **AP-136**, which already carries the retail mechanism, the exact divergence, and the observable; this issue is no longer a planned fix and does not block the placement-cutover campaign or C5c. @@ -7917,7 +7550,7 @@ Steps 1-5 are the user-visible acceptance for AP-136's residual. ## #310 — Retained preparation retry stalls landblock retirement with no bound -**Status:** FIXED 2026-08-28 +**Status:** OPEN **Severity:** HIGH **Filed:** 2026-08-04 **Component:** physics / streaming @@ -7953,22 +7586,9 @@ permission is refused before `ParkCollisionResidents` is ever entered. indefinitely — either the deadline is driven in production or the retirement can proceed past stale placement debt. -**Resolution:** Prefix retirement now supersedes an authored mover that is -still waiting for its first preparation. Before ordinary placement-debt -evaluation, Runtime cancels the exact unprepared operation touching the -retiring prefix; the resident then enters the normal withdrawal/acknowledgement -handshake. Prepared, wakeable-lost-cell, and dormant-local operations retain -their existing authority. The production-shaped regression proves the original -token is displaced and retirement converges without an asset-ready edge or a -new inbound packet. - ## #311 — RetryPendingProjections allocates a fresh array on every non-empty call -**Status:** FIXED 2026-08-28. The retry pump now uses retained call-depth -scratch lists, preserving deterministic snapshot semantics and re-entrant -behavior without allocating a fresh array. The warmed fixture fell from -424 B/retry to the existing 72 B event-publication floor; all 73 focused -runtime tests pass. +**Status:** OPEN **Severity:** LOW (perf, not correctness) **Filed:** 2026-08-04 **Component:** physics / headless @@ -9215,8 +8835,7 @@ for a follow-up that did not exist. Both now cite #322. ## #274 — Restricted/barred-house entry needs a connected retail comparison -**Status:** CLOSED 2026-08-28 — this is a deferred comparison/gate request, -not an observed client failure. +**Status:** OPEN — explicitly deferred by the user on 2026-07-31 **Severity:** LOW (validation debt; no confirmed failure) **Filed:** 2026-07-31 **Component:** physics / EnvCell entry restrictions @@ -9235,9 +8854,7 @@ character access state, and result before closing. ## #273 — ACDream can squeeze through tight world gaps that block retail -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN — live mismatch confirmed 2026-07-31; exact location/capture +**Status:** OPEN — live mismatch confirmed 2026-07-31; exact location/capture still required **Severity:** MEDIUM (world traversal differs from retail) **Filed:** 2026-07-31 @@ -9451,9 +9068,8 @@ row refresh after the final retained-UI invalidation correction. ## #267 — Vitae does not update the character panel's skills/attributes display -**Status:** CLOSED 2026-08-28 — Campaign P's final connected user matrix was -accepted 2026-07-31, including the live vitae/buff panel values and immediate -skill-row refresh. Retail finding: primary attributes are +**Status:** IMPLEMENTED 2026-07-30 (`cf2605fa`, merged) — closure pends +the user visual check. Retail finding: primary attributes are VITAE-IMMUNE (`EnchantAttribute` 0x00594570 never references the vitae singleton) — only skills and vitals take the penalty. Panel now shows effective values; skill footer shows the vitae parenthetical (e.g. @@ -9509,9 +9125,8 @@ ACE's >= reading" warning. ## #265 — Steep-slope response set: uphill-jump bounce, roof slides lost, edge wedge (TS-4 removal fallout — REVERTED) -**Status:** CLOSED 2026-08-28 — Campaign P's final connected user matrix was -accepted 2026-07-31: downhill bounce chain, flat pop, and clean uphill landing. -The named culprit for symptoms (b) and (c) +**Status:** IMPLEMENTED 2026-07-30 (landing-bounce rework: retail check_contact seed + SetPositionInternal commit + live 5% elasticity reflect; docs/research/2026-07-30-landing-bounce-family.md) — pending user live gate (downhill bounce chain, flat pop, uphill clean landing) +user's visual-gate acceptance. The named culprit for symptoms (b) and (c) was capture-bisected to a THIRD, pre-existing (frozen-phase, predates Campaign P by ten days) mechanism — neither the S1 nor S2 suspects named below — and is now ported. Symptom (a) is confirmed a SEPARATE, @@ -9607,9 +9222,7 @@ model). ## #263 — Drudge Scrying Orb still occludes its particles after the composite-translucency fix -**Status:** CLOSED 2026-08-28 — owner-confirmed after validity audit. - -**Previous status:** OPEN (deferred by user 2026-07-29) +**Status:** OPEN (deferred by user 2026-07-29) **Severity:** LOW (single known item; "very subtle" per user) **Component:** rendering / world translucency / particles @@ -9658,9 +9271,7 @@ regressing the #225 lifestone/candle compositing. ## #264 — Water semantics: WATER_CONTACT_TS consumer + two unverified swim behaviors -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN (filed 2026-07-30, Campaign P Slice P4 AP-10 closeout) +**Status:** OPEN (filed 2026-07-30, Campaign P Slice P4 AP-10 closeout) **Severity:** LOW (no confirmed divergence; research/verification follow-up) **Component:** physics / terrain / water @@ -9708,8 +9319,7 @@ sub-item resolves. ## #262 — Run-on-the-spot at first login: no displacement until a recall reset -**Status:** CLOSED 2026-08-28 — owner reports the first-login movement issue -is solved. +**Status:** OPEN **Severity:** MEDIUM (self-heals via any teleport; first-login only so far) **Filed:** 2026-07-29 (Campaign N acceptance run 1 on Coldeve) **Component:** login flow / local movement / physics readiness @@ -9806,10 +9416,7 @@ scenario 11 (20 fresh logins) provides the structured re-test. ## #261 — Wire LinkStatusSnapshot.PacketLossPercentage from retail's formula -**Status:** FIXED 2026-08-28 — ported retail's 2-second heartbeat and -40-sample ushort windows from `CLinkStatusAverages::AddSnapshot @ 0x00546650`; -`GetAveragePacketLoss @ 0x00546610` now receives live NAK, retransmit, -received, and sent counters. Focused transport/link-status gate: 33/33. +**Status:** OPEN **Severity:** LOW **Filed:** 2026-07-29 **Component:** net (link-status presentation) @@ -9912,8 +9519,8 @@ attach→GPU-resource path for create-without-release. Do NOT patch the symptom ## #259 — Win32 Vulkan surface creation fails machine-wide (`ERROR_UNKNOWN`) -**Status:** CLOSED 2026-08-28 — confirmed machine/driver environment fault, -not an acdream product defect; retained only as diagnostic history. +**Status:** OPEN — environment fault, not a product defect; recorded so the next +reader does not bisect the tree for it **Severity:** HIGH while it lasts (the client cannot start at all) **Filed:** 2026-07-29 **Component:** host machine / AMD driver / Win32 WSI @@ -9963,8 +9570,7 @@ self-differential is still available once a window can be created. ## #258 — Developer panels have no host after V11 deleted ImGui -**Status:** CLOSED 2026-08-28 — owner classified the deleted developer-panel -host request as void; it is not desired product scope. +**Status:** OPEN **Severity:** MEDIUM (developer capability regression; no player-facing effect) **Filed:** 2026-07-29 **Component:** developer tooling / retained UI @@ -10167,8 +9773,7 @@ afterwards. **The test was not modified.** ## #256 — Server-spawned objects go invisible after repeated portal runs -**Status:** CLOSED 2026-08-28 — superseded by #260; its discriminator found no -missing-object drift and the transport-loss mechanism was fixed elsewhere. +**Status:** OPEN **Severity:** HIGH (world objects invisible but interactive; live-server observed) **Filed:** 2026-07-28 **Component:** live-entity render publication / streaming (backend attribution pending) @@ -10245,9 +9850,7 @@ CSVs, difference maps, `churn-soak.json`). ## #257 — Working set balloons to ~1.5 GB over a live portal-churn session -**Status:** CLOSED 2026-08-28 — the suspected leak was not reproduced by its -own churn discriminator, and the measured rendering/package architecture has -since been replaced. +**Status:** OPEN **Severity:** HIGH (memory; live-server observed) **Filed:** 2026-07-28 **Component:** GPU/streaming resource lifetime (backend attribution pending) @@ -10322,9 +9925,7 @@ re-run with walked portal transits rather than `/teleloc`. Artifacts: ## #253 — Attribute/skill icons: not centered in their cells, and fully opaque -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW (visual fidelity; user-observed) **Filed:** 2026-07-28 **Component:** retail UI / character sheet (D.2b) @@ -10429,8 +10030,7 @@ its only `CameraDiagnostics` writer, but four classes there call ## #251 — glClientWaitSync returned 0 and crashed the render loop, once in nine connected runs -**Status:** CLOSED 2026-08-28 — obsolete OpenGL-path report; the production -renderer is Vulkan-only and no `glClientWaitSync` path remains. +**Status:** OPEN **Severity:** MEDIUM (one observed occurrence; kills the process when it fires) **Filed:** 2026-07-28 **Component:** rendering / GL frame-flight fences @@ -10487,9 +10087,7 @@ unexpected status is exactly the signal §5.5 spent three days wishing it had. ## #250 — Zero-allocation tests fail intermittently, roughly 1 run in 3 -**Status:** CLOSED 2026-08-28 — all four probes now use the shared tiered-JIT/ -OSR-safe `ZeroAllocationProbe`; the former roughly-one-in-three family passed -12/12 current repeated runs under concurrent test-process load. +**Status:** OPEN **Severity:** MEDIUM (undermines every "tests green" gate) **Filed:** 2026-07-27 **Component:** tests / allocation assertions @@ -10626,8 +10224,8 @@ the probe removes, so it could likely be tightened to zero on the probe now. ## #249 — Bindless handles stay resident after their table slot is released -**Status:** FIXED 2026-08-28 -**Severity:** CLOSED +**Status:** OPEN +**Severity:** MEDIUM **Filed:** 2026-07-27 **Component:** rendering / GPU resource lifetime @@ -10957,9 +10555,7 @@ then its description after one retail paragraph break, without the bogus ## #235 — Capped/RDP jump presentation aliases the 30 Hz object clock -**Status:** CLOSED 2026-08-28 — owner-confirmed after validity audit. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW **Filed:** 2026-07-23 **Component:** local animation / render interpolation / frame pacing @@ -11154,20 +10750,11 @@ reports decoded bytes, budget, and evictions. ## #241 — InteriorEntityPartition never uses its per-landblock AABBs to cull -**Status:** FIXED 2026-08-28 +**Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-07-24 **Component:** render -**Resolution:** every partition overload now accepts the prepared frame -frustum and skips a rejected landblock before touching its entity list. The -production `RetailPViewRenderer` passes the same prepared frustum and exempts -the player's current landblock, matching the existing dispatcher safeguard. -The no-frustum overload remains conservative for one-shot/tests, and a focused -test proves a rejected landblock is omitted while the camera-landblock -exception survives. This matters even after the retained-scene cutover because -the current-render oracle still builds the legacy partition every frame. - **Description:** `Partition` receives per-landblock `AabbMin`/`AabbMax` but walks every near-tier landblock's full entity list every frame without a landblock-level frustum test — while `WorldSceneDiagnosticsController` @@ -11190,8 +10777,7 @@ Slice H-a / G. ## #242 — Static publication rebuilds a third dictionary and re-sorts per completion attempt -**Status:** CLOSED 2026-08-28 — current publication prepares the replacement -snapshot once and reuses it across completion retries. +**Status:** OPEN **Severity:** LOW **Filed:** 2026-07-24 **Component:** streaming @@ -11434,8 +11020,7 @@ from crossing into a new world. ## #228 — Clean Release build emits 17 test-project warnings -**Status:** CLOSED 2026-08-28 — disproved by a clean current Release build with -zero warnings and zero errors. +**Status:** OPEN **Severity:** LOW **Filed:** 2026-07-20 **Component:** tests / build hygiene @@ -11470,9 +11055,7 @@ the full 6,558-pass / 5-skip suite remains green. ## #226 — Retail building/EnvCell detail-texture overlay is not rendered -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** IMPLEMENTED + CONNECTED-VISUAL-VERIFIED 2026-08-21; re-ported to +**Status:** IMPLEMENTED + CONNECTED-VISUAL-VERIFIED 2026-08-21; re-ported to the single-pass path 2026-08-22 by Campaign VM VM1 after VM2's live cdb read found retail hardware never takes the two-pass fallback this issue originally ported (see @@ -11546,9 +11129,7 @@ during B and restored true, and logout was ACE-confirmed graceful. ## #225 — Scene particles overpaint translucent world objects -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** IN-PROGRESS — implementation/reviews and connected stress gate pass; final visual gate pending +**Status:** IN-PROGRESS — implementation/reviews and connected stress gate pass; final visual gate pending **Severity:** MEDIUM **Filed:** 2026-07-18 **Component:** rendering / world translucency / particles @@ -12103,8 +11684,7 @@ right-aligned Total Experience/progress, and Unassigned Experience all updated. ## #213 — Retail client commands were sent to ACE as chat text -**Status:** CLOSED 2026-08-28 — retail commands route through the dedicated -client-command controller; the pending re-gate label was stale. +**Status:** IN-PROGRESS — command-family gate passed except two corrective fixes, pending re-gate **Severity:** MEDIUM **Component:** retained UI / chat commands / net @@ -12166,8 +11746,7 @@ in-world SmartBox readout. ## #212 — Toolbar shortcut numbers turn gray in physical combat -**Status:** CLOSED 2026-08-28 — implementation and regression coverage are -present; the pending-gate label was stale. +**Status:** IN-PROGRESS — implementation complete 2026-07-13, pending user gate **Severity:** LOW **Component:** retained UI / toolbar @@ -12274,9 +11853,7 @@ combat mode, and the jump bar still fills and hides normally. ## #209 — Retail jump power bar missing -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** IN-PROGRESS — implementation complete 2026-07-13, pending live visual gate +**Status:** IN-PROGRESS — implementation complete 2026-07-13, pending live visual gate **Severity:** MEDIUM **Component:** retained UI / movement @@ -12306,9 +11883,7 @@ the jump starts. Repeat after moving/resizing the bar and after relogging. ## #208 — Combat bar appears after logging in peacefully -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate +**Status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate **Severity:** MEDIUM **Component:** retained UI / combat lifecycle @@ -12470,11 +12045,9 @@ sequencer identity across the update. User verified the exact equip/unequip flow in the Release client. ## #191 — Tapping W (brief forward press) glides forward without playing the step animation -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. ## #194 — WbDrawDispatcher._groups is never pruned (minor) -**Status:** CLOSED 2026-08-28 — `BeginFrame` prunes old instance groups and -current regression coverage pins the bounded lifetime. +**Status:** OPEN — filed 2026-07-10. `post-M2`, LOW priority. Surfaced during the #193 heap analysis. **Severity:** LOW — bounded, not a crash risk (unlike #193 which was the real OOM). **Component:** render — `WbDrawDispatcher._groups` (`Dictionary`, WbDrawDispatcher.cs:236). @@ -12482,9 +12055,7 @@ current regression coverage pins the bounded lifetime. ## #193 — Client OOMs after extended play (~50 min) — FIXED -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** ✅ FIXED + measurement-verified 2026-07-10 (`119a2326`). Root cause: `WbDrawDispatcher.InstanceGroup.Opacities` (a `List` added by #188) was appended one float per drawn instance per frame but never cleared — the per-frame reset loop (WbDrawDispatcher.cs:959) cleared its four sibling parallel lists but not Opacities. `List` capacity-doubling → ~128 MB/512 MB LOH `float[]` → ~1 GB/min → OOM after ~50 min. Fix: extract the reset into `InstanceGroup.ClearPerInstanceData()` clearing ALL FIVE parallel lists (so a future 6th can't drift out); TDD `InstanceGroupClearTests`. **Before/after (same 6-min churny roam, dotnet-counters):** working set 1.6→7.6 GB (leaked) vs 0.75→1.36 GB (fixed); LOH 1.1→6.1 GB (climbing) vs 0.24→0.64 GB then FLAT (240→607→641→641→641). No crash on the fixed build. (Move to Recently closed on next tidy.) +**Status:** ✅ FIXED + measurement-verified 2026-07-10 (`119a2326`). Root cause: `WbDrawDispatcher.InstanceGroup.Opacities` (a `List` added by #188) was appended one float per drawn instance per frame but never cleared — the per-frame reset loop (WbDrawDispatcher.cs:959) cleared its four sibling parallel lists but not Opacities. `List` capacity-doubling → ~128 MB/512 MB LOH `float[]` → ~1 GB/min → OOM after ~50 min. Fix: extract the reset into `InstanceGroup.ClearPerInstanceData()` clearing ALL FIVE parallel lists (so a future 6th can't drift out); TDD `InstanceGroupClearTests`. **Before/after (same 6-min churny roam, dotnet-counters):** working set 1.6→7.6 GB (leaked) vs 0.75→1.36 GB (fixed); LOH 1.1→6.1 GB (climbing) vs 0.24→0.64 GB then FLAT (240→607→641→641→641). No crash on the fixed build. (Move to Recently closed on next tidy.) **Severity:** was MEDIUM→HIGH (crashed after extended sessions). RESOLVED. **Component:** render — `WbDrawDispatcher` per-frame instance-group reset. @@ -12497,9 +12068,7 @@ current regression coverage pins the bounded lifetime. **How to investigate (capture-first, do NOT guess):** measure ACTUAL memory growth over time — managed heap (`GC.GetTotalMemory` / dotnet-counters) AND unmanaged/GPU (native heap, GL texture/buffer/mesh allocations, the WB mesh caches, `TextureCache`, `GlobalMeshBuffer`). Prime suspects to rule in/out with real measurements: GPU resource accumulation at the High preset (aniso16x/MSAA4x/25×25 far window), per-revisit landblock/mesh/texture cache growth, or a genuine managed leak in the entity/streaming path. Snapshot the working set every N minutes of a roam and diff. Possible contributor to tonight's phantom-door-regression episode (memory pressure → GC thrash → dropped inbound motion packets) — see `feedback_phantom_regression_runtime_state.md`. -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** MEDIUM (visible every time a player taps instead of holds a movement key — likely a common input pattern) **Filed:** 2026-07-09 @@ -12769,7 +12338,7 @@ Then Slice 2 (unify Path A + `RemotePhysicsUpdater` extraction) and Slice 3 (Set ## #183 — Floating distant scenery: trees from another biome hover in the distance -**Status:** CLOSED 2026-08-28 — owner-confirmed during ledger cleanup. +**Status:** OPEN **Severity:** LOW **Filed:** 2026-07-07 **Component:** rendering / scenery / streaming @@ -12793,9 +12362,7 @@ the wrong scene set, or a far-LOD placement offset. Not yet investigated. ## #182 — Player wedges in a packed monster crowd, can't wiggle free (hand-rolled SphereCollision) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** VELOCITY-MODEL REBUILD SHIPPED (Slices 1+2, `8bb8b204`→`54d56229`) — +**Status:** VELOCITY-MODEL REBUILD SHIPPED (Slices 1+2, `8bb8b204`→`54d56229`) — **awaiting the user visual gate** (crowd glide/land + normal-locomotion regression pass). **Severity:** MEDIUM **Filed:** 2026-07-07 @@ -12886,9 +12453,7 @@ artifact's LOOK (brightness) without killing the motion-flicker — re-character before assuming tonight's captures still describe it. **Original filing (mechanism evidence trail below remains valid):** -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN — mechanism PINNED from live evidence; the knife-edge test not yet identified +**Status:** OPEN — mechanism PINNED from live evidence; the knife-edge test not yet identified **Severity:** HIGH (THE user-visible #176 flicker that survived the #180 camera fixes: washed regions with hard screen-space rectangle boundaries pulsing at a parked camera) **Filed:** 2026-07-06 (split from #176 after the #180 fixes exonerated the camera) @@ -13006,8 +12571,7 @@ pixel diffs show no region-shaped changes; the user sees no flicker; #176 re-gat ## #180 — Camera-collision sweep bistable at a compressed boom → per-frame eye strobe (the #176 "stripes") -**Status:** CLOSED 2026-08-28 — both fixes shipped and were log-verified; the -remaining visual was tracked separately in now-closed #181. +**Status:** 🟡 BOTH FIXES SHIPPED 2026-07-06 + LOG-VERIFIED; user gate pends on #181 (the residual visible flicker turned out to be render-side — see #181). Fix 1 `48aaab81`: stateful sought-position per `CameraManager::UpdateCamera` 0x00456660 (pseudocode `docs/research/2026-07-06-camera-sought-position-pseudocode.md`; register @@ -13069,7 +12633,7 @@ stay continuous in `[flap-sweep]`); camera glides along walls like retail. ## #178 — Retire the A8 double-sided cell-shell stopgap (CullMode.Landblock → None) -**Status:** CLOSED — USER-ACCEPTED 2026-08-28 +**Status:** OPEN **Severity:** LOW-MEDIUM (correctness/perf debt; 2× shell fragment load) **Filed:** 2026-07-06 **Component:** render — EnvCellRenderer MDI draw @@ -13090,26 +12654,6 @@ investigation (`docs/research/2026-07-06-176-177-render-pair-investigation.md`). **2026-07-09 triage:** investigated, verdict STILL_OPEN — the `CullMode.Landblock -> CullMode.None` double-sided stopgap is still present verbatim at `EnvCellRenderer.cs:1394-1399` with no follow-up commit or roadmap reference retiring it. -**2026-08-28 fix:** named-retail proves that the CellStruct polygon -`sides_type` is a geometry-expansion instruction, not a GPU cull mode: -`D3DPolyRender::ConstructMesh @ 0x0059DFA0` expands the authored faces and -`RenderMeshSubset @ 0x0059CA10` draws the constructed mesh with -`D3DCULL_CW`. Our extractor already emits retail's exact fans and reversed -faces. Both production EnvCell paths now use the corresponding clockwise -cull state, and the two `Landblock -> None` overrides are gone. The installed -DAT audit covered 38,189 polygons in 3,168 CellStructs with no invalid side -types or missing vertices. Hermetic winding/cull-policy tests and the -canonical Release gate pass: 16,321 passed, 0 skipped, 0 failed. No PAK -rebake is required. Research: -`docs/research/2026-08-28-issue178-cell-shell-culling.md`. - -**Gate owed:** owner visual acceptance at Holtburg and the Facility Hub; -walls, floors, ceilings, ramps, and stairs must remain visible from every -ordinary playable camera angle. - -**Owner gate 2026-08-28:** PASSED — “Ok looks good.” Holtburg/Facility-Hub -interior shell acceptance is complete; #178 is closed. - --- ## #177 — Dungeon stairs pop in/out across levels (invisible until entering the room; last step vanishes running down) @@ -13306,9 +12850,7 @@ over one session as CreateObject re-sends re-registered it under fresh entity ids — `[seam-ent]` L= showed `01D4:I100` four times) — fold into the A7 arc or fix with #180's gate.** -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** 🟡 lighting fix SHIPPED + verified; #180 (camera strobe) BOTH halves +**Status:** 🟡 lighting fix SHIPPED + verified; #180 (camera strobe) BOTH halves FIXED + log-verified 2026-07-06 (`48aaab81` + `f10fe4e9`); the site-A static light-stacking re-apply hole CLOSED (`87cddce2` — idempotent re-registration; whether it fully accounts for the observed `[seam-ent]` ×2→×4 growth needs a @@ -13327,9 +12869,7 @@ render; the physics fix landed (seam shake gone, user-gated) and the flash REMAINS — so it is a render-side issue in its own right, correlated with camera angle. -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN — root cause CONFIRMED; fix DEFERRED to the A7 +**Status:** OPEN — root cause CONFIRMED; fix DEFERRED to the A7 dungeon-lighting arc (see #177 for the revert story — same mechanism, same deferral). **Root cause (confirmed via the probe launches):** per-cell LIGHTING pops, @@ -13359,9 +12899,7 @@ camera angle at the corridor seams. ## #175 — Door collision registers the Setup PLACEMENT pose, not the motion-table CLOSED pose (phantom slab behind the visual door) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** 🟡 FIX SHIPPED 2026-07-05 (same session) — pending user gate (Facility Hub double door: closed blocks AT the visual panels from both sides, no embed, no phantom wall; Holtburg cottage door unregressed). +**Status:** 🟡 FIX SHIPPED 2026-07-05 (same session) — pending user gate (Facility Hub double door: closed blocks AT the visual panels from both sides, no embed, no phantom wall; Holtburg cottage door unregressed). **FIX:** `ShadowShapeBuilder.FromSetup` gains a `partPoseOverride` (BSP part shapes only; CylSphere/Sphere unchanged); `RegisterServerEntityCollision` derives it via `GameWindow.MotionTableDefaultPose` — the wire MotionTableId's @@ -13423,9 +12961,7 @@ unregressed (door apparatus green). ## #174 — Door Use dies after the first jump: the RemoveLinkAnimations seam stripped animations without retail's queue drain -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** 🟡 FIX SHIPPED 2026-07-05 (same session) — pending user gate (jump around, then use the Facility Hub door from close AND from ~3 m). +**Status:** 🟡 FIX SHIPPED 2026-07-05 (same session) — pending user gate (jump around, then use the Facility Hub door from close AND from ~3 m). **FIX:** the `MotionInterpreter.RemoveLinkAnimations` seam is retail `CPhysicsObj::RemoveLinkAnimations` 0x0050fe20 — a tailcall to `CPartArray::HandleEnterWorld` 0x00517d70 → `MotionTableManager:: @@ -13531,9 +13067,7 @@ is verbatim retail), no deferral-skipping (turn-to-face is retail). ## #173 — Observed character jumping into a ceiling hovers at the roof until the arc decays (no collision-velocity response on remotes) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (watch a second client jump into the 0x0007 dungeon roof; it should bounce down immediately like the local player). +**Status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (watch a second client jump into the 0x0007 dungeon roof; it should bounce down immediately like the local player). **Severity:** MEDIUM (remote-motion fidelity indoors; lands visibly late) **Filed:** 2026-07-05 **Component:** physics — remote dead-reckoning collision response @@ -13575,9 +13109,7 @@ unchanged. ## #172 — Town-network portal platform blocks instead of stepping up (CCylSphere family was never ported) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (walk up onto the Holtburg portal platform, then the 0x0007 dungeon run). +**Status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (walk up onto the Holtburg portal platform, then the 0x0007 dungeon run). **Severity:** HIGH (blocks dungeon access — gates the whole #137 repro) **Filed:** 2026-07-05 **Component:** physics — CylSphere object collision response @@ -13942,9 +13474,7 @@ actually steps at (Player_Move.cs / Creature GetRunRate usage). ## #165 — Remote entities penetrate walls ("swallowed a bit") before stopping -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** MEDIUM (visual-only, remote view) **Filed:** 2026-07-03 (user observation during the R2-R4 visual pass) **Component:** physics, remote dead-reckoning @@ -14027,9 +13557,7 @@ wall from acdream, matching the retail-observer view side-by-side. ## #166 — Slope-landing glide + bounce absent (retail "sled" on downhill jumps) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIX IMPLEMENTED 2026-07-30 (this session) — closure pends the +**Status:** FIX IMPLEMENTED 2026-07-30 (this session) — closure pends the user's visual-gate acceptance. The visual-matrix recheck this note asked for DID happen (Campaign P matrix scenario 5) and found the glide/bounce still missing even with all four register-predicted deviations @@ -14205,9 +13733,7 @@ R5/R6 touches the action list. **Where:** ## #202 — Port the portal String-table lookup for WeenieError / UseDone text -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW **Filed:** 2026-07-03 **Component:** ui / net @@ -14222,8 +13748,7 @@ R5/R6 touches the action list. **Where:** ## #195 — Retail chat ChatVM lacks Fps/Position providers — /framerate and /loc degrade -**Status:** CLOSED 2026-08-28 — obsolete architecture; the duplicate -ChatVM/provider shape is gone and local commands use the dedicated route. +**Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-07-02 **Component:** ui @@ -14326,8 +13851,7 @@ states `0x28` and `0x29`. ## #199 — Port the server-authoritative character raise flow -**Status:** CLOSED 2026-08-28 — Campaign CA replaced optimistic mutation with -the authoritative one-request-in-flight response flow. +**Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-07-02 **Component:** ui / core @@ -14346,8 +13870,7 @@ the authoritative one-request-in-flight response flow. ## #200 — Migrate remaining retail-window mounts (vitals/chat/toolbar/inventory + MockupDesktop) to RetailWindowFrame -**Status:** CLOSED 2026-08-28 — stale two-stack migration list; its inline -mounts and MockupDesktop targets no longer describe current composition. +**Status:** OPEN **Severity:** LOW **Filed:** 2026-07-02 **Component:** ui @@ -14392,9 +13915,7 @@ BCL-only. ## #158 — Character window — deferred polish -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW **Filed:** 2026-06-26 **Component:** ui @@ -14450,9 +13971,7 @@ passed in the connected visual gate. ## #151 — Far-town (Arwic) collision broken at login: terrain barely grounds + city/perimeter walls never block -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** 🟡 city/perimeter walls **FIXED + user-verified** (`9743537`, 2026-06-24); terrain-grounding sub-question OPEN but re-scoped LOW (likely not a real defect). +**Status:** 🟡 city/perimeter walls **FIXED + user-verified** (`9743537`, 2026-06-24); terrain-grounding sub-question OPEN but re-scoped LOW (likely not a real defect). **Severity:** MEDIUM (walls were the HIGH part — fixed; terrain residual unverified) **Filed:** 2026-06-24 **Component:** physics — building collision-shell registration (walls); far-town terrain grounding (residual) @@ -14629,9 +14148,7 @@ collision working. Also re-checks **#138**. ## #148 — Status-bar backpack icon should toggle the inventory window (stateful open/closed) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-06-22 @@ -14664,9 +14181,7 @@ D.2b inventory visual gate. ## #146 — D.2b inventory capacity-bar visual polish -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW **Filed:** 2026-06-22 @@ -14751,9 +14266,7 @@ See divergence register **AP-59**. ## #144 — Empty item-slot press+drag+release still emits a Click -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW **Filed:** 2026-06-20 **Component:** ui — D.2b drag-drop spine (B.1) @@ -14817,9 +14330,7 @@ See divergence register **AP-59**. ## #139 — D.2b retail UI polish: chat buttons -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN (narrowed 2026-08-09 — the chat-text-colors half CLOSED, see below) +**Status:** OPEN (narrowed 2026-08-09 — the chat-text-colors half CLOSED, see below) **Severity:** LOW (cosmetic fit-and-finish — the widget generalization works and matches the prior hand-made build; this is polish vs a side-by-side retail client) **Filed:** 2026-06-16 **Component:** ui — D.2b retail UI (chat buttons) @@ -15178,9 +14689,7 @@ neighbour load/unload churn). ## #104 — Scene VFX particles not clipped to the PView visible cell set -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW **Filed:** 2026-06-02 **Component:** render, vfx @@ -15217,9 +14726,7 @@ pass, deliberately deferred out of the Phase W seal (which covers sky/terrain/wa ## #102 — A8.F PortalVisibilityBuilder — port retail update_count fixpoint (replace MaxReprocessPerCell cap) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** PARTIALLY RESOLVED (Phase U.2a, 2026-05-30, commit `d880775`) +**Status:** PARTIALLY RESOLVED (Phase U.2a, 2026-05-30, commit `d880775`) **Severity:** MEDIUM → LOW (residual is diamond-topology clip-completeness only) **Filed:** 2026-05-29 **Component:** rendering, visibility, EnvCell portal traversal @@ -15477,9 +14984,7 @@ Retail oracle for cell-id hysteresis: `acclient_2013_pseudo_c.txt:308742-308783` ## #94 — Held items project spotlight on walls -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN — **UNBLOCKED 2026-07-11.** Retail ParentEvent/CreateObject child +**Status:** OPEN — **UNBLOCKED 2026-07-11.** Retail ParentEvent/CreateObject child parenting now renders hand-held objects and follows the animated attachment part. The original lighting symptom can now be reproduced and investigated after the held-item visual gate; no lighting conclusion has been drawn yet. @@ -16021,8 +15526,7 @@ divergence and update commit messages / code comments to match. ## #73 — Retail-message centralization plan — per-feature string sweeps -**Status:** CLOSED 2026-08-28 — retained as an engineering policy, not a -concrete client defect. +**Status:** OPEN **Severity:** LOW (per-feature work, not infrastructure) **Filed:** 2026-05-16 **Component:** ui / retail messages @@ -16251,9 +15755,7 @@ doors work after the heartbeat bump. ## #64 — Local-player pickup animation does not render -**Status:** CLOSED 2026-08-28 — owner-confirmed after validity audit. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW (visual feedback only — pickup completes correctly) **Filed:** 2026-05-14 (B.5 visual verification) **Component:** motion / animation routing for local player @@ -16493,10 +15995,7 @@ for full evidence and rationale. ## #55 — Static-entity slow path reports ~1.45M `meshMissing` per 5s at r4 standstill -**Status:** CLOSED 2026-08-28 — stale pre-#128 observation. The point-of-use -mesh re-arm in `WbMeshAdapter.EnsureLoaded` removed the permanent-miss path; -the later broken-stairs connected evidence records `meshMissing=0` with -`entSeen == entDrawn` while the affected content was on screen. +**Status:** OPEN **Severity:** LOW (no visible regression — affects a diagnostic counter, not rendered output) **Filed:** 2026-05-11 **Component:** rendering / `WbDrawDispatcher` static-entity classification path @@ -17136,9 +16635,7 @@ the local terrain normal, not the actor's facing. ## #41 — Residual sub-decimeter blips on observed player remotes (M3 baseline) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIX IMPLEMENTED 2026-07-17 — pending acdream-observer visual gate +**Status:** FIX IMPLEMENTED 2026-07-17 — pending acdream-observer visual gate **Severity:** MEDIUM **Filed:** 2026-05-05 **Component:** physics / motion / animation (per-tick remote prediction) @@ -18218,8 +17715,7 @@ additional flash-shader work). ## #3 — Client clock drifts from retail after ~10 minutes (periodic TimeSync missing) -**Status:** CLOSED 2026-08-28 — periodic TimeSync is parsed and routed into -the world clock; the old missing-clock premise is no longer current. +**Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-04-25 **Component:** net / sky @@ -18673,9 +18169,7 @@ flip-a → []; flip-b → [0x100]. ## #114 — Indoor PView shell-clip regions are not draw-quality (clip scoped to outdoor roots) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** MEDIUM-HIGH (blocks the indoor half of retail's draw-side portal clip; several user-visible indoor artifacts to re-test ride on it) **Filed:** 2026-06-11 (first user gate on `927fd8f`) @@ -18710,9 +18204,7 @@ clip against the accumulated portal view (`planeMask=0xffffffff` :427922). ## #115 — Camera feels draggy/jittery vs retail when turning in cramped interiors -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN +**Status:** OPEN **Severity:** LOW-MEDIUM (feel; no geometry errors reported) **Filed:** 2026-06-11 (user, same gate session) **Component:** camera (collision sweep / smoothing) @@ -18729,9 +18221,7 @@ retail's viewer-distance smoothing (update_viewer region) before touching. ## #116 — Slide-response divergence family: near-perpendicular lateral slide lost + first-airborne-frame in-frame slide vs hard stop -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** OPEN (narrowed further, 2026-07-30) — **shape-2 CLOSED** +**Status:** OPEN (narrowed further, 2026-07-30) — **shape-2 CLOSED** (D4 un-skipped and passing, oracle-plan-confirmed, no cdb needed after all — see the 2026-07-30 update below); **shape-1 narrowed, not closed**: a real, independently-decomp-confirmed Path-6 head-sphere fix landed, but @@ -19138,9 +18628,7 @@ re-validate against the real resolver before un-skipping. ## #120 — [pv-ERROR] in-place propagation tripwire: convergence invariant broken at depth 128 (cottage interior cells) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIXED 2026-06-11 (`dede7e4`) — pending re-gate (watch for +**Status:** FIXED 2026-06-11 (`dede7e4`) — pending re-gate (watch for zero `[pv-ERROR]` lines in the next launch log) **Severity:** HIGH (self-detected invariant break in the new flood growth) **Filed:** 2026-06-11 (T5 launch log; fired during normal cottage play) @@ -19193,9 +18681,7 @@ Revisit on the next firing (the #117/#118 re-gate launch will carry it). ## #121 — All world portals invisible (portal swirl VFX gone everywhere) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIXED 2026-06-11 — pending re-gate +**Status:** FIXED 2026-06-11 — pending re-gate **Severity:** HIGH (user: "all portals that were previously showing at various places are now gone") **Filed:** 2026-06-11 (re-gate launch) @@ -19422,9 +18908,7 @@ staircase entity's per-frame draw decision. ## #129 — Doors/doorways leak through terrain and houses from over a landblock away -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIX SHIPPED — awaiting user visual gate +**Status:** FIX SHIPPED — awaiting user visual gate **Severity:** MEDIUM (visible at distance during normal outdoor play) **Filed:** 2026-06-12 (user report, post-#119-close session) **Component:** render — aperture depth punch at distance (#117 family, AD-18) @@ -19461,10 +18945,7 @@ cap constant (0.5 m) is the tuning knob — see AD-18. ## #130 — Background-color strip along the TOP outer edge of a doorway when looking out from inside -**Status:** CLOSED AS OWNER-ACCEPTED RESIDUAL 2026-08-28 — the connected -visual re-gate confirmed the thin top-edge strip still exists; owner direction -is to leave it as-is. The two shipped correctness fixes and their regression -coverage remain, but no further rendering change is requested. +**Status:** FIX 2 SHIPPED — awaiting user visual re-gate **Severity:** LOW-MEDIUM (small strip, but on the most-stared-at pixels in the game) **Filed:** 2026-06-12 (user report, post-#119-close session) **Component:** render — drawn-shell lift vs draw-space portal consumers (AP-32) @@ -19649,24 +19130,6 @@ the post-world PView replay is deleted. **Gate:** both sides — indoors with the opening behind the candle, and outdoors at the angle that previously erased it. -**2026-08-29 recurrence fixed (register AP-236 retired in the same commit):** -the #451 correction moved outdoor-static flames into the LScape-stage drain, -which reopened this class — every opaque pass after that drain (doors, -creatures, look-in interiors, DynamicsLast) overwrote the already-composited -flames, and depth/barrier A/Bs were no-ops because the eraser is opaque color -painted later. Fix, both halves retail-cited and user-gated live at Holtburg -(candle whole in front of sign + door) and the cathedral (waterfalls -contained at every zoom): (1) an OUTDOOR root skips the stage-boundary drain -and runs `FlushLandscapeAlpha()` after `DrawDynamicsLast` — retail's walk -draws every cell's objects before `PView::DrawCells`' boundary flush -(`DrawSortCell @0x005A17C0`, flush `@0x005A4872`); (2) before -`DrawExitPortalMasks`, a partial drain flushes everything at or beyond the -nearest punched cell (`ExitPortalMaskBarrierDistance`) — retail `DrawBuilding -@0x0059F2A0` runs `FlushAlphaList(0f)` BEFORE its portal-only far-Z pass, so -in the far→near walk nothing already drained can meet a punched aperture's -falsified depth (the waterfall-through-aperture regression the first half -exposed). Interior roots keep the pre-clear stage-boundary drain unchanged. - --- # Recently closed @@ -21283,7 +20746,7 @@ visible seam lines that formed a cube outline across the view. ## #316 — The player arm's LANDING TRANSITION block never publishes the collision shadow -**Status:** FIXED 2026-08-28 +**Status:** OPEN **Severity:** UNKNOWN pending one measurement (see "The open question") — either a ~33 ms shadow lag (cosmetic, invisible in practice) or the #184 invisible-but-solid class (real). Do not act on it before measuring. @@ -21336,17 +20799,9 @@ by `LandingPacket_PlayerGuid_QueueClearedNoShadowPublish_316Preserved` / `LandingPacket_CreatureGuid_ShadowPublishedQueueNotCleared` in `tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs`. -**Resolution:** The explicit player-GUID skip is removed. Every accepted -`AirborneSnap` now publishes the resolved body pose through the common -collision-shadow tail. The player landing regression test proves the shadow -lands at the authoritative pose rather than retaining its spawn pose; the -complete 27-row collapse matrix passes. - ## #319 — A player-parented child never receives a canonical cell (ParentInstanceSequence hardcoded 0) -**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. - -**Previous status:** FIX IMPLEMENTED, awaiting the connected acceptance gate (§7 of the +**Status:** FIX IMPLEMENTED, awaiting the connected acceptance gate (§7 of the contract) and commit — NOT YET COMMITTED in this worktree. Do not mark DONE until the gate runs and the change lands. **Severity:** LOW for the user (no observable symptom — verified, not assumed), @@ -21467,9 +20922,7 @@ Full analysis, with the headless test's structural immunity explained: ## #321 — `DatSoundCacheTests` concurrent-decode-dedup fails under full-suite load -**Status:** FIXED 2026-08-28 — a stale miss now rechecks resident/negative -publication and acquires the in-flight decode under the admission gate. A -deterministic paused-stale-caller test proves only one DAT read/decode occurs. +**Status:** OPEN **Severity:** LOW (test-only so far; no production symptom observed) **Filed:** 2026-08-05 **Component:** content / audio cache @@ -21503,13 +20956,7 @@ strictly worse than a red test. ## #322 — Two callers compute the same two pre-placement flags from the same two inputs -**Status:** FIXED 2026-08-28. A single -`RuntimeAuthoritativePositionRouteClassifier.DerivePrePlacementFlags` -function now owns retail's disposition/HasAnims truth table. Both the -steady-state snapshot merge and every accepted classifier route consume it; -the route itself was not widened into the merge. The former equality test is -re-argued as an end-to-end application matrix plus an explicit six-row truth -table. Controller, classifier, and continuation-executor tests pass 169/169. +**Status:** OPEN **Severity:** LOW (internal refactor debt; NOT a retail divergence) **Filed:** 2026-08-05 (C5b review, finding S1 — the successor #275 closed without filing) @@ -21556,9 +21003,7 @@ finding is that these two flags need no route, no `playerDistance` and no ## #323 — A far-snap store can silently stale a pending initial-create completion receipt -**Status:** FIXED 2026-08-28 — initial-create completion now proves the retained -body's exact pose still equals the captured receipt before applying it. A -pose-only supersession acknowledges without snapping the sidecar back. +**Status:** OPEN **Severity:** LOW (narrow, self-healing within one broadcast interval; no observed live symptom) **Filed:** 2026-08-05 (C5b review, finding D2 — found while establishing that diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index a7716216..0fec343b 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -1,4 +1,4 @@ -# Retail Divergence Register — current through 2026-08-28 +# Retail Divergence Register — current through 2026-08-26 **What this is.** The single auditable register of every known place acdream's runtime behavior can deviate from the retail client (Sept 2013 EoR build, @@ -67,7 +67,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 88 active rows (AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 RETIRED 2026-08-28 — #386's named-retail message trace confirmed the vendor popup is content-sized and installed-DAT property 0x79 hides its disabled scrollbar; both behaviors are now ported; AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal- +## 2. Adaptation (AD) — 89 active rows (AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal- -to-line-break shaping, which retail's `ItemExamineUI::AddItemInfo @0x004AC050` does not do (wire text appends verbatim; the escape decode retail runs at `StringInfo` resolution now lives at our string source, `DatStringResolver` → `RetailStringEscapes`); AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate @@ -196,7 +196,7 @@ readiness/requeue adaptation. See | AD-85 | **Filed 2026-08-12 at Campaign FA slice FA5. NARROWED 2026-08-13 (social gate round 2):** the row's items 2 and 3 — the three LOCAL Swear/Break/Kick confirmation dialogs and the server-driven type-1 accept-swear dialog (plus the type-4 fellowship invite) — are PORTED: `DatStringResolver.ResolveTemplate` composes the exact `0x23000001` templates (`ID_Allegiance_SwearConfirmation`/`BreakConfirmation`/`KickConfirmation`, `ID_Allegiance_AcceptSwearConfirmation`, `ID_Fellowship_FellowshipRequest`) by the `StringTable::GetString @0x004300D0` fragment/PLAYER-variable interleave (no-metalanguage branch `@0x004303B7`; all five templates verified token-free — `docs/research/2026-08-13-confirm-and-weenie-error-display.md` §1.2). What REMAINS recorded: item 1 — the numeric fields: self/monarch followers (`0x10000252`/`0x10000258`) and self rank (`0x10000253`) now carry `Followers:`/`Rank: [n]` label text but not retail's `StringInfo`-resolved sentence, and the "experience passed up" text (`0x10000492` ×2, the vassal row's `0x10000269`) renders bare numbers, same disposition as AD-81's `"{level} {pct}%"`. Those templates are multi-variable and were not verified token-free; they can move onto `ResolveTemplate` after the same verification. | `src/AcDream.App/UI/Layout/SocialAllegiancePageController.cs` (`RefreshSelfBlock`, `RefreshMonarchBlock`, `RefreshPatronBlock`, `UpdateRow`) | Same argument as AD-81 for the remainder: the numeric-field templates have not been dumped/verified token-free, and guessing meta-token behavior would risk silently-wrong substitution. The dialog templates WERE verified, which is why they moved. | A user sees bare numbers instead of retail's full sentences for followers/rank/XP-passed-up. The confirmation dialogs now read retail's full sentences ("Do you wish to swear to X?", "X would like to swear allegiance to you. Do you accept?"). | `gmAllegianceUI::UpdatePlayerData @0x00491330`, `UpdateMonarchData @0x00491B40`, `UpdatePatronData @0x004917C0`, `UpdateVassalsData @0x00492340` (lane C/A field sources); `MakeSwearConfirmationDialog @0x004927B0` family (lane A §5.1); `StringTable::GetString @0x004300D0` (ported for token-free templates); `StringTableMetaLanguage::RenderString @0x004302B1` (still unported — AD-81) | | AD-86 | **Filed 2026-08-12 at Campaign FA slice FA5, item 4.** ACE deliberately zeroes or empties NINE `AllegianceProfile`/`AllegianceData` fields on the wire — officers, officer titles, MOTD, MOTD-set-by, name-last-set-time, lock state, and approved vassal are always empty/false/zero regardless of the allegiance's real state; `timeOnline`/`allegianceAge` (the remaining two) are hard-coded 0 forever (lane C §5.1). acdream's FA1 parser reads all of these (to keep the byte cursor aligned for the fields after them) but drops most at increasing layers: `AllegianceMemberRecord` never surfaces `timeOnline`/`allegianceAge` as fields at all; `RuntimeAllegianceState.ApplyUpdate` (FA2) does not forward `Motd`/`MotdSetBy`/`ChatRoomId`/`NameLastSetTime`/`IsLocked`/`ApprovedVassal` from the parsed `AllegianceUpdate` record to `RuntimeAllegianceSnapshot` even though the C# record itself carries them; retail's own `gmAllegianceUI` (FA5) has no widget for any of the seven either (lane A §3.3: "No allegiance MOTD / officer / ban / hometown UI" — they are chat-verb-only in the 2013 client, out of this campaign's scope per the plan's §4). | `src/AcDream.Core.Net/Messages/ClientCommandResponses.cs` (`ReadAllegianceProfileBody`, `AllegianceMemberRecord`, `AllegianceUpdate`); `src/AcDream.Runtime/Gameplay/RuntimeAllegianceState.cs` (`ApplyUpdate`) | Retail's own client renders nothing for these seven fields either (no panel widget consumes them) — dropping them past the parse layer matches retail's OWN presentation exactly, and is strictly safer than surfacing values that are always wrong/empty against ACE. | Any FUTURE consumer (the chat-verb-only officer/MOTD/lock/ban management features, §2 master table features #11-31 of the allegiance wire research, explicitly out of Campaign FA's scope) that reads these fields off the Runtime layer will find them permanently zero/empty against ACE regardless of the allegiance's real server-side state — do not chase this as a parser bug; it is ACE's own zeroing. | ACE `Network/Structure/AllegianceHierarchy.cs:53-56,62-64,74-75,78-83,86-89,153-155` (broadcast counters/isLocked/officers/officerTitles/motd/approvedVassal); ACE `Network/Structure/AllegianceData.cs:59-60,86-89,111-112` (timeOnline/allegianceAge); `docs/research/2026-08-11-fa-allegiance-wire.md` §5.1 | | AD-87 | **Filed 2026-08-12 at Campaign FA slice FA6.** The allegiance-swear half of the two-bot headless connected gate (`FellowshipAllegianceLeaderBotPolicy`/`FellowshipAllegianceRecruitBotPolicy`) is written and wired end-to-end (proximity, `0x001D` swear, the confirmation-relay seam, `0x0020` tree-reseed assertions, break, reconnect-idempotence) but has never actually been verified to complete over the wire — `AllegianceGateEnabled = false` in both classes keeps it unreachable by default. Six live runs against local ACE all reproduced the same result: the fellowship half passes decisively (the Recruit bot's own `RuntimeFellowshipState` flips, proven three separate times), but ACE returns nothing at all to the `0x001D` swear (no `0x0274` confirmation, no `0x0020`, no error) even at 0.005 m separation — see docs/ISSUES.md #384 for the full evidence trail. So while the FELLOWSHIP two-session machinery is proven live, the ALLEGIANCE two-session machinery (Runtime commands, wire builders, `RuntimeAllegianceState` reseed) remains unverified end-to-end over a real connection — only its unit/fixture-level tests and its (successful) LOCAL echo on the swearer's own client are exercised. | `src/AcDream.Headless/Policies/HeadlessBotPolicy.cs` (`FellowshipAllegianceLeaderBotPolicy.AllegianceGateEnabled`, `FellowshipAllegianceRecruitBotPolicy.AllegianceGateEnabled`, both `false`) | Shipping the fellowship gate ALONE (rather than blocking the whole slice on the allegiance blocker) matches the campaign's own D8/item-6 split — fellowship and allegiance are independent retail systems with independent wire families, and the fellowship half's proof stands on its own regardless of the allegiance outcome. Disabling rather than deleting the allegiance code keeps a reviewed-quality, ready-to-run harness in place for whoever closes #384. | Anyone reading "the FA6 bot-vs-ACE gate passed" without the qualifier could assume the allegiance swear/break/reconnect path is proven over the wire when it is not — only its LOCAL send-and-echo behavior is proven; ACE's actual acceptance of the swear is the open question #384 tracks. | docs/ISSUES.md #384; `docs/research/2026-08-11-fa-allegiance-wire.md` §1.3 (the expected `0x0274`/`0x0275`/`0x0020` handshake); run6 evidence (0.005 m distance, zero inbound after swear) | -| ~~AD-88~~ | **RETIRED 2026-08-28 (#386).** Named-retail tracing resolved the prior screenshot ambiguity: `VendorItemsUI::OpenVendor @0x004c16d0` inserts only present categories; `UIElement_ListBox::UpdateLayout @0x0046e460` sums their heights; `ResizeScrollableArea @0x00474730` broadcasts message `0x32`; and `UIElement_Menu::ListenToElementMessage` calls `RecalculatePopupSize @0x0046caf0`. The four-edge-docked vendor ListBox therefore resizes the popup to all present rows, uncapped. Installed-DAT inspection additionally found scrollbar `0x10000351` property `0x79=true` (`HideWhenDisabled`). Same-day visual re-gate found acdream's flattened popup still reserved the hidden sibling's 16-pixel width; the shared presentation predicate now also collapses that width to zero, matching the final effect of retail `UIElement_Scrollbar::UpdateLayout @0x004710d0` calling `SetVisible(false)`. | `src/AcDream.App/UI/UiMenu.cs` (`PopupSizeToContent`, `PopupScrollbarHideWhenDisabled`, shared presentation gate); `src/AcDream.App/UI/Layout/VendorUiController.cs` (both authored behaviors enabled) | The purported fixed-six-row retail screenshot was a misread, and the proposed "items inserted before message registration" explanation was disproven: `MakePopup` registers popup element messages during initialization, before `OpenVendor` inserts categories. | None. Short vendor lists now shrink without the striped scrollbar artifact; long lists grow to every category exactly through the retail content-sizing rule. | docs/ISSUES.md #386; `VendorItemsUI::OpenVendor @0x004c16d0`; `UIElement_ListBox::UpdateLayout @0x0046e460`; `UIElement_Menu::RecalculatePopupSize @0x0046caf0` | +| AD-88 | **Filed 2026-08-13 at the #385 dropdown fix (classification: UNCLEAR).** The vendor category dropdown ships G5's fixed 6-row scrollable popup window, but its authored popup ListBox (`0x21000043/0x10000350`) is edge-docked on all four sides (L=T=R=B=1, measured by menuprobe3 `OptionsPanelLiveMountProbeTests.ProbeMenuPopupSizingAndTextStyle`) — the exact authored condition that arms retail `UIElement_Menu::RecalculatePopupSize @0x0046caf0`, which resizes the popup to the ListBox's summed content height, uncapped (`0x0046e5f4..0046e66c`). The Config option-menus' identical docked shape now drives `UiMenu.PopupSizeToContent=true` (#385); vendor deliberately keeps `false`. | `src/AcDream.App/UI/Layout/VendorUiController.cs` (its UiMenu wiring leaves `PopupSizeToContent` at the class-default false) | The G5 vendor-gate retail screenshot was read as a ~6-row-with-scrollbar look and the vendor connected gate USER-PASSED on that shape — reworking a user-gated surface on decomp inference alone would invert the retail-oracle rule. The two pieces of evidence conflict; the row records the conflict rather than silently picking a side. | If retail actually opens the category popup full-height, our vendor dropdown shows a 6-row scroll window where retail shows every category at once — visible at any vendor with >6 categories. If retail truly shows 6 rows, the mechanism question (why the docked ListBox does not trigger RecalculatePopupSize there) is unanswered and could mislead the next dropdown port. | docs/ISSUES.md #386 (the retail side-by-side to run + the two candidate resolutions); #385 (the Config fix that exposed the conflict) | | AD-90 | **Filed 2026-08-13 at the #389 mechanism-review fix round (finding M1).** Retail's smartbox divisor aspect is not raw width/height: `RenderDevice::ComputeAspectForViewport @0x0054f150` yields `(w/h) × m_DisplayAspectRatio × 0.75`, with `m_DisplayAspectRatio` fed by the registered `Render.AspectRatio` preference. At that preference's DEFAULT (4:3) the factor is exactly 1.0f and the expression collapses to raw w/h — which is what acdream uses. acdream carries no AspectRatio preference at all. Also folded in: retail's `SetFOVRad` gate arithmetic ACCEPTS NaN (x87 unordered-compare quirk) where acdream's port rejects it — unreachable in practice, deliberately not reproduced (mechanism review M3). | `src/AcDream.App/Rendering/RetailFieldOfView.cs` (class doc names this row) | Bit-exact at retail's registered default; the preference existed for 2003-era stretched-CRT correction with no modern counterpart. Reproducing it would add a user knob retail itself defaulted away. | A retail user who had changed `Render.AspectRatio` saw framing acdream cannot reproduce; anyone porting FOV behavior from a capture made with a non-default AspectRatio preference will measure a mismatch against our law. | `RenderDevice::ComputeAspectForViewport @0x0054f150`; `Render::SetFOVRad @0x0054b2d0`; consumer `D3DXMatrixPerspectiveFovLH @0x0059ab71`; docs/research/2026-08-13-389-fov-mechanism-review.md | | AD-91 | **Filed 2026-08-13 at the #390 port.** acdream's display-change clamp covers ALL registered floating windows; retail's does not — every retail floaty overrides `MoveTo` with the clamp `x = max(0, min(x, parentW − selfW))` EXCEPT `gmFloatyChatUI` (floating chats 2–4), which has no clamp and can genuinely strand off-screen on a resolution change (decomp finding, `docs/research/2026-08-13-retail-ui-display-change.md`). The display block's product requirement ("UI windows must stay reachable on resolution change", the 2026-08-13 /goal) overrides the exception. | `src/AcDream.App/UI/RetailWindowLayoutPersistence.cs` (`ClampAllToScreen` — clamps every attached handle, floating chats included) | User-directed reachability beats reproducing a retail defect-shaped gap; the clamp math itself is retail's own, applied uniformly. | A retail-parity comparison that deliberately strands a floating chat window will find acdream rescuing it where retail leaves it lost. | `UIElementManager::RefreshEvent @0x0045C530`; `UIElement::UpdateForParentSizeChange @0x00462640`; the per-floaty `MoveTo` clamp overrides; docs/research/2026-08-13-retail-ui-display-change.md | | AD-92 | **Filed 2026-08-13 at the #376/#388 review fix round (blast M6 / mechanism M4).** Two switcher adaptations with no retail counterpart: (1) the fullscreen refresh rate is the monitor's HIGHEST for the picked WxH — retail passed the device mode's own refresh as-is (`Device::ForceDisplayResolution`); (2) an invalid/unsupported fullscreen request is a logged refusal that leaves the window unchanged — retail attempted the switch and surfaced the device error. The persisted-flag divergence a refusal leaves behind is ISSUES #392. | `src/AcDream.App/Settings/DisplayModeSwitching.cs` (`TryFindRefreshRate`, the refusal paths); `src/AcDream.App/Settings/RuntimeSettingsTargets.cs` (`Apply`'s refused-mode logging) | Highest-refresh is strictly better on modern variable-refresh panels (retail predates them); refuse-and-log is #388's own no-crash requirement. | A capture comparing retail's exact chosen refresh for a mode will differ; a server/tooling flow expecting an error dialog on an invalid mode sees a console line instead. | `Device::ForceDisplayResolution @gmClient::Init 0x004047af`; docs/research/2026-08-13-376-388-{mechanism,blast}-review.md | @@ -217,11 +217,6 @@ readiness/requeue adaptation. See ## 3. Documented approximation (AP) — 162 active rows (AP-202 RETIRED 2026-08-26 by #446 — retail PFile `.keymap` Load/Save/startup/shutdown persistence is now live; AP-235 filed 2026-08-25 at the Campaign CT4 fix round — `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# switches instead of a live `EnumMapper` read; AP-234 filed 2026-08-23 at the #426 solid-face extraction fix — cell-wall geometry keeps approximating retail's "skip untextured subsets in a cell" with the polygon's NoPos flag rather than the Surface's own Type; AP-233 filed 2026-08-23 at the Holtburg windmill fix — the render-side inter-frame animation blend, now holding the boundary frame at every seam; AP-232 filed 2026-08-22 at Campaign VM VM1 — the #226 two-draw detail blend weight on TRANSLUCENT subsets versus retail's single stage-1 output alpha; AP-185 RETIRED 2026-08-20 — `RetailWindowLockPresentationController` now swaps all eight imported locked/live chrome blocks, hides live-only floating-chat and SmartBox grips, suppresses only the nine-slice grip overlay, and applies the current lock before a late-mounted window's first `OnShown`; the radar's persistent B7/B8 semantic face is pinned against pointer-state clobber and covered by a real-fixture draw cycle; AP-231 filed 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the Skills page formula-connector-text approximation in `ComposeFormula`, see the row's own text for the full disclosure of what is byte-verified versus best-derived; AP-213 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the remaining flat-list-vs-four-bucket-sorted-model half is now ported: `ChargenSkillDetail`/`ChargenSkillFormula` (Core) thread `SkillBase.MinLevel`/`Description`/`Formula` from the global SkillTable through `ChargenOptions.TryGetSkillDetail` (`ChargenTableReader.Project` populates it, live-DAT-pinned at 38 entries — 23 MinLevel<=1/15 MinLevel==2, matching the Batch F investigation's own recorded finding exactly), and `CharacterCreationSkillsPage` now groups every costable skill into `SkillBucket` (Specialized/Trained/UseableUntrained/UnuseableUntrained, `UpdateSkillEntry`'s own `iMinlevel <= 1` test), sorts each bucket alphabetically (`InsertEntrySorted`'s `wcscmp`, ported as `string.CompareOrdinal`), and builds one `Templates[0]` header row per bucket ahead of that bucket's `Templates[1]` skill rows — `DoSkillRecords`'s own unconditional 4-header-then-populate build order. A level change re-buckets the row (detected per-refresh against each row's own cached bucket, then a full rebuild — the observable placement matches retail's incremental single-row `InsertEntrySorted` move without reproducing its internal mechanism, a documented and harmless substitution). 3 new fixture tests (`SkillsPage_BucketHeaders_AlwaysBuildAllFour_InRetailOrder`, `SkillsPage_UntrainedSkill_BucketsByMinLevel`, `SkillsPage_AdvancingASkill_MovesItsRowIntoTheNewBucket`) plus 1 new live-DAT test (`InstalledSkillTable_GlobalSkillDetails_MinLevelDistributionMatchesCostCoverage`); AP-216/AP-217 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 1 — both rows' STOPPED items are now landed: `CharacterCreationUiController.AppearancePalSetSource`/`AppearanceClothingTableSource`/`AppearancePaletteColorSource` wire a DAT-backed `ChargenAppearanceCatalog` into the Appearance page from `LivePresentationComposition` (mirroring the existing `AppearancePreviewControl` seam), and `UiButton`/`UiDatElement` both gained a per-instance `Tint` property threaded into every existing `DrawSprite` call they make; `CharacterCreationAppearancePage` now sets `Tint` directly on each swatch button and the GradCircle element instead of layering a flat-fill `ChargenSwatchColorTile` overlay on top (that class is deleted) — a genuine multiplicative sprite tint on the widget's OWN authored art, matching retail's `SurfaceWindow::BlitAndColor(..., Blit_Multiply, color)` exactly rather than approximating it with an opaque rectangle. Both fixture test suites (`CharacterCreationAppearancePageSwatchColorTests`, 8 tests) and the live-DAT color pins (`ChargenAppearanceCatalogColorTests`) pass unchanged against the new mechanism; AP-218 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-6) — `gmCGAppearancePage::Update`'s heritage-flavored static Hair/Eyes/Skin spin caption (`ID_CharGen_HairStyle`/`_Eyes`/`_Skin`, Gearknight `GearText_*`, Olthoi/OlthoiAcid `OlthoiText_*`) is now ported verbatim by `RefreshSpinCaptions`, replacing the prior ordinal substitution outright — see AP-215's own rewritten row for what remains open (the icon-thumbnail gap, restated); recount at this same edit: the row count this header carried before Batch B was already one LOW relative to the physical table (Batch A's own ending state: header said 164, the physical table already held 165 rows — verified by direct count against that commit) — a pre-existing drift this edit corrects to the counted total, not an artifact of Batch B's own net change (F12 correction, gate round 1 closeout, 2026-08-16: this note originally said "one high", the inverted direction — the header was UNDER-counting, not over-counting); AP-222 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch B fix (GF-11b) — the Appearance spins' current-part highlight and the Town buttons' Normal-to-white caption swap both port retail's actual mechanism (per-state label color/outline commit off the REQUESTED retail state id, independent of art-media availability — `UiButton.SetPerStateLabelStyle`/`ComputeRequestedStateId`), closing the row's own "not yet resolved which side is wrong" question: NEITHER client's spin ART changes (no Highlight media exists on either), but BOTH clients' spin TEXT does, matching retail's `SetState(1)`/`SetState(6)` property commit exactly (live-DAT-measured 218,167,85 -> 255,221,131, outline off -> on); AP-215 NARROWED the same batch (GF-9) — item 1 (the swatch-selection substitution) is RETIRED now that the real companion-overlay mechanism (`SetColor`'s `m_tColorWheel[...][0x10][iCurColor*7]->SetVisible`) is ported (`CharacterCreationAppearancePage`'s nine `SwatchOverlayIds`), leaving only item 2 (the icon-less style-spin ordinal label) open; AP-230 filed 2026-08-16 at the Campaign CC gate round 1 Batch A fix (GF-13) — the chargen-scoped-vs-general-importer-wide honor split for dat property 0x3B (Invisible: `UIElement::OnSetAttribute` case 8 hides an element), with the general client-wide honor deferred as its own visual gate (docs/ISSUES.md #408, 1,083 elements affected); AP-213 NARROWED the same gate round (GF-5) — the Skills page's click-to-advance/double-click-retreat single-button substitution is RETIRED now that the real per-row `pSkillUpButton`/`pSkillDownButton` arrows are wired to retail's own plain-click dispatch, leaving open only the flat-list-vs-four-bucket-sorted-model half; AP-229 filed 2026-08-16 at the Campaign CC CC7 review-fix round, F1 — the screen-layering divergence: retail's `UIFlow::UseNewMode` destroys/reconstructs the current UI framework on every mode switch where acdream's CC7 keeps both `CharacterManagementUiController` and `CharacterCreationUiController` mounted for the whole lifetime and only reveals/occludes them; AP-228 filed 2026-08-16 at the CC5 re-review residual round (R4) — the Summary listbox's skill-row KEY source, same divergence class as AP-226 filed the same round, a few retail lines away; AP-227 filed 2026-08-16 at the same review-fix round, F9 — an empty Summary name-field commit calls `SetName("")` (clearing the state), where retail's own NUL-inclusive length gate leaves `CharGenState.name` UNCHANGED for that specific case; AP-226 filed 2026-08-16 at the Campaign CC CC5 review-fix round, F11 — the Summary page's DAT-sourced labels versus retail's static `pcProfessions`/`pcGender`/`pcHeritage`/`pcTown` tables, including the non-human-heritage-renders-bare-"Heritage: " retail quirk; AP-225 RETIRED the same round, F6 — the reviewer re-derived `gmCGSummaryPage::ListenToElementMessage @0x0047bf40`'s length check and proved the 32-vs-33 threshold this row flagged as "not fully certain" does NOT exist: the compared length is NUL-inclusive (an empty field's length is 1, matching AP-226's own F11/F9 finding), so `length > 0x21` is EXACTLY `visibleChars > 32` — acdream's `MaxNameLength = 32` was always byte-correct, not merely internally-consistent; AP-223/AP-224 filed 2026-08-15 at Campaign CC slice CC5 — the acdream-only `HeritageOrGenderUnset` Finish refusal and the Summary listbox's two-bucket (Specialized/Trained only) skill-list narrowing (AP-224 corrected 2026-08-16 at the same review-fix round, F3 — its "template mechanism ported exactly" claim was FALSE as shipped, now fixed and true again, see its own row); AP-214 RETIRED the same slice — `RandomizeCharacter` is now ported and wired at the screen-open edge, closing the honest-blank-open gap it recorded; AP-212 NARROWED the same slice — the Appearance/Summary Random-button primitives are now real faithful ports, not uniform-pick approximations, leaving only Heritage/Profession/Town (still uniform-pick) and Skills (still unported) open; AP-222 filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (N2) — the current-part spin highlight is a measured no-op for all nine spins, no Highlight media authored on any of them; AP-221 filed the same re-review (R2) — the chargen preview's one-shot-composition-vs-retryable-coordinator binding gap; AP-217 rewritten and AP-220 tightened the same re-review (R3 corrects the GradCircle from a dead click target to unported paint-art; N1 narrows the Gearknight-exit wording to non-Olthoi); AP-216..AP-220 filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round, F2 — DoColorSpots swatch-art, the inert GradCircle, spin-caption/heritage-swap loss, the Skin-spin MoveTo reposition, and the Gearknight-boundary randomize calls; AP-215 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Appearance page's swatch-highlight (`UiButton.Selected` vs retail's separate overlay toggle) and icon-less style-spin ordinal-label substitutions; AP-214 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — retail's `gmCharGenMainUI` ctor rolls a full `RandomizeCharacter` BEFORE any page constructs, so retail's chargen screen is never actually blank on open (and the Appearance page's own gender-flip-on-init always fires against a real gender); acdream opens honestly blank instead, closing out the campaign plan's risk item 5; AP-212/AP-213 filed 2026-08-15 at Campaign CC slice CC4 — the Random button's uniform-pick approximation of retail's three unported randomize algorithms, and the Skills page's flat-listbox simplification of retail's four-bucket sorted skill model; AP-211 filed 2026-08-15 at the Campaign CC slice CC3 review-fix round — the client-side roster-vs-slotCount refusal in `RuntimeCharacterCreationState.TryBeginFinish` has no retail counterpart at that layer, retail enforces the cap in char-select UI instead; AP-207..AP-210 filed 2026-08-15 at Campaign CC slice CC3 — the FitTemplateToCharacter FPU-unrecoverable auto-detect skip, the shared-ClothingColors-list color-count approximation, the classID DAT-DID-lookup placeholder, and the ApplyTemplate atomic-replace-vs-per-attribute-guard simplification; AP-205 filed 2026-08-11 at Campaign OP gate 4 (#381) — the Apply/Reset/Defaults footer's opaque backing field is a genuine acdream synthesis with no authored retail counterpart; ~~AP-201~~ RETIRED 2026-08-11 at the Campaign OP gate-3 fix round — `UiScrollablePanel` now keeps a straddling row visible and CLIPS it to the viewport (`ClipsChildren` → `UiRenderContext.PushClip`, which existed by then), replacing the whole-row cull this row recorded; the user-observed symptom (the Chat tab's per-window filter blocks vanishing into a void at the DEFAULT scroll offset) closed issue #371; AP-203 RETIRED 2026-08-26 by #446 — all 306 installed-DAT rows now have distinct identities and concrete consumers; AP-204 corrected and RETIRED the same day — exact capture/conflict/button semantics now follow named retail; AP-202 RETIRED 2026-08-26 by #446 — retail `.keymap` file interchange and profile lifetime now ship; AP-200 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Chat Font Face/Size menu rows are store-only, distinct fields from the existing live `ChatSettings.FontSize`; AP-199 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Sound Features menu, Interface Sound trio, and Play Sound Only When Active are store-only (the Interface trio cites AP-174's existing "retail's own dead knob" finding); AP-198 filed 2026-08-11 at Campaign OP slice OP6, row count reconciled at the OP6 rework round (2026-08-11, review N1) — the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix) are store-only, the Vulkan+one-aggregate-QualityPreset renderer having no per-feature knobs; AP-197 filed 2026-08-11 at the OP4 review-fix round (SF-1/S4) — Display Timestamps hardcodes retail's constructor-default format string instead of the per-character GenericQualitiesData key-1 override the parser reads and discards; ~~AP-196~~ RETIRED 2026-08-11 at Campaign OP slice OP9 — originally filed at the OP4 review-fix round (MUST-FIX 3 / blast M2) for the Group-C re-point's observable-default changes (ViewCombatTarget true→false) and the PARTIAL GameplaySettings retirement (AutoTarget/AutoRepeatAttack/ViewCombatTarget deleted, the other five kept as write-behind mirrors); OP9 deleted `GameplaySettings` outright (all 13 remaining members were already re-pointed to the server-bit seam at OP4), closing the write-behind-mirror gap for good — see its retirement note below; AP-195 RETIRED 2026-08-11 at Campaign OP slice OP5 — ported both halves left open at OP2 re-review closure: the ALL-set LED media swap (`UiButton.FaceFileOverride`, driven by the block-level `P0x10000082`/`P0x10000083` sprites now threaded through `ElementInfo`/`DatWidgetFactory`) and the `CreateChildren` self-sizing tail (`UiCheckboxBitfield64.Height` grows with `_contentHeight` per row; the ENCLOSING page ListBox reflows around the block's FINAL size via the new `UiTemplateListBox.AddPrebuiltRow`, reusing the ListBox's own stacking exactly as the row's own disposition menu allowed, rather than a third stacking path); AP-194 filed 2026-08-10 at Campaign OP slice OP1 — the GetDefaultOptionValue vs constructor-default disagreement for ConfirmVolatileRareUse/ShowHelm/ShowCloak (see the row below); AP-193 filed 2026-08-10 at Campaign OP slice OP1 — the 0x34 HearPKDeathMessages id/mask mapping is ACE-sourced (see the row below); AP-192 filed 2026-08-10 at the Campaign CH round-5 polish (S2) — authored outline `0x21`/`0x22` now reaches every text-bearing widget, but only at the element's effective-default state; per-STATE outline switching (dialog/character/combat buttons author `0x21` in state `0x3` only) is not ported; AP-191 filed 2026-08-10 at Campaign CH round 4 items 1+2 — the chat transcript's missing tag-colour (`0x1D`, green) and tag-font (`0x1C`) are deferred, needing a per-run tag concept `UiText.Line` does not have yet; AP-184 RETIRED 2026-08-10 at Campaign CH round 4 — the three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings, `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290` fully decoded (the "vftable slot" operands are the same pooled/mislabeled-data artifact as AP-186's own precedent, not real vtable dispatch — reading the function's own disassembly for the `push imm32` preceding each constructor call resolves them), closing ISSUES.md #364 (full retirement note later in this same list, at its own "AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09..." entry); AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09 at Campaign CH user-gate round 2, item 3, recording that three of the seven retail `/help` group-topic listings (channels/chatting/commands) remained PARTIAL because their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which the filing believed "not decodable with confidence from a static string sweep" because Binary Ninja renders its three internal string operands as dereferences of unrelated vtable slots (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc.). That belief was WRONG — the same pooled/mislabeled-data artifact this register already documented elsewhere (AP-113's retirement note) applies here too: reading the function's own disassembly for the `push imm32` immediately preceding each `PStringBase::PStringBase` constructor call (rather than trusting BN's line-grouped rendering, which hides the true instruction order) resolves all three operands directly — `"@"` + a one-character tag sliced from a shared wide literal `U"fvpca"`/`U"mh,."` (a wide string read through a narrow `char*` truncates at the first zero high byte, the "hack" retail's own function name calls out) + `" - Sends a broadcast to your "` + `ChannelSystem::GetChannelName`'s own literal switch-table result + `".\n"`. `ChannelsGroupDetail` (entirely 6 such calls), `ChattingGroupDetail` (6 more, plus a `HelpReply@0x00577A50` Summary-branch quirk that unconditionally emits reply+pr+mr together — read directly, not assumed), and `CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of every other group's Detail branch plus a handful of its own short one-liners, including a CONFIRMED retail saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the four (death/status/text/allegiances) the original filing already had. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` for the full per-line address citations. Round 2 item 2 also deletes `PortalWaitNoticeController` (the dedicated centered-overlay presentation the user reported was the wrong retail surface) and reroutes the portal-space wait-cue notice through the same `AddText`/SpewBox chokepoint every other on-screen interface-text site uses — AP-178's open SpewBox position/extent/font/colour questions now cover this notice too, since its separate controller and consts are gone; no new row was needed for the surface mismatch itself, since it was never separately registered (`PortalWaitNoticeController`'s own doc comment asserted "not a chat message" as an accepted design, not a flagged divergence). AP-150 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item D (#329) — `PortalTunnelPresentation.TickRotation` now emits `"In Portal Space - Please Wait..."` unconditionally on every rotation-segment expiry, exactly matching `gmSmartBoxUI::UseTime`'s `else`-arm at 0x004D6FCD, instead of gating on `_waitCueVisible`, which only ever went true after the invented 5-second `RuntimeWorldTransitState.RetailWaitCueDelay` hold; `RetailWaitCueDelay`/`ObserveWait`/`SetWaitCue` remain as `LocalPlayerTeleportController`'s own hold-delay telemetry (`RuntimePortalSnapshot.WaitCueShown`) but no longer gate the on-screen cue, so they are not a residual of this row — closes issue #329; AP-183 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) -**2026-08-28 count correction:** #408 retired AP-230 by moving property -`0x3B` visibility into the shared importer and every retained stateful widget; -the active AP total is **161**. The historical header narrative above retains -the filing chronology, while the retired row below records the final gate. - Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84 collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered AP-94..AP-112 for the confirmed retail-UI completion gaps. @@ -247,13 +242,6 @@ double-click-to-buy is absent is superseded by this correction. The active-row count in the heading is therefore one lower than the retained historical heading text. -**AP-148 retirement correction (2026-08-28):** #325 ports Gate A's exact -wrap-safe not-older predicate in both the timestamp gate and Runtime authority -classifier. A newer force stamp now leaves TELEPORT_TS unconsumed, preserves -heading/velocity/parent state, arms no teleport hook, and still acknowledges -immediately. The long AP-148 row below is retained only as historical filing -research and is no longer active. - | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| | AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001` → `ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` | @@ -261,7 +249,7 @@ research and is no longer active. | AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` | | AP-232 | **Filed 2026-08-22 at Campaign VM slice VM1 (the #226 single-pass re-port; deviation introduced at `05970306`, row owed since then).** Retail's single-pass detail combine produces ONE pixel per subset whose OUTPUT alpha is stage 1's `MODULATE(TEXTURE, CURRENT)` (`D3DPolyRender::SetSurface @0x0059c4d0`, op at `0x0059c549`) — for a delayed-alpha (translucent) subset that product is the framebuffer blend weight. acdream draws the base subset with its own alpha, then a second `mesh_detail` draw weighted by `detail.a * instanceOpacity` under `SRCALPHA + INVSRCALPHA`. For OPAQUE subsets (base alpha 1) the two compose to exactly `lerp(base, detail, detail.a*opacity)` and, with both draws fogged, to retail's fog-after-combine pixel (identity pinned by `RetailDetailTextureContractTests`). For TRANSLUCENT building/EnvCell subsets the destination after the base draw is `mix(behind, foggedBase, baseAlpha)`, not `foggedBase`, so the detail weight differs from retail's single product. | `src/AcDream.App/Rendering/Shaders/mesh_detail.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (transparent interleave); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (transparent interleave) | Opaque subsets are the overwhelming majority of building shells and interior walls and are exact; translucent detail-bearing subsets (ClipMap/alpha/additive/inverse-alpha glass and grates) get a bounded weight difference that never exceeds the detail texture's own alpha (mean 0.132 on the live Dereth category texture). Collapsing to one draw would require the base pipelines to sample the detail texture, i.e. a second `mesh_modern` variant on the retail path. | A translucent building/EnvCell surface with the detail preference on reads visibly different from retail against a bright background. Separate from AP-34 (queue ORDER); this row is about the blend WEIGHT. | `D3DPolyRender::SetSurface @0x0059c4d0` (stage table), `RenderMeshSubset @0x0059ca10`; VM2 cdb note `docs/research/2026-08-22-vm2-retail-detail-path-cdb.md` | | AP-231 | **Filed 2026-08-16 at the Campaign CC gate round 1 closeout, Group 2 (Skills page info-box completion).** `CharacterCreationSkillsPage.ComposeFormula` ports `gmCGSkillsPage::MakeSkillFormula @0x00480e10` with HIGH CONFIDENCE for the `"Formula : "` prefix, the per-attribute `"(%u x %s)"`-vs-bare-name choice (a term's own multiplier > 1 gets the parenthesized form, else just the attribute name), the `" / %u"` divisor suffix (gated on `Divisor != 1`), and the `" +%u"` additive-bonus suffix (gated on `AdditiveBonus != 0`) — every one of those is a directly-read compiled string literal or a field the DatReaderWriter binding already exposes by name (`SkillFormula`'s six fields map 1:1 onto the decompiled struct's own `_w/_x/_y/_z/_attr1/_attr2` offsets, confirmed by their exact 0x28/0x2c/0x30/0x34/0x38/0x3c stride). LOWER CONFIDENCE: the CONNECTOR text between a two-attribute formula's two terms. This port renders `" + "` — the well-known "(Attr1 + Attr2) / N" shape most published AC skill formulas use — but the decompiled function's own two candidate connector literals (`data_7a01a4`, appended between the terms; `data_797584`, appended again immediately after BOTH terms are present, an apparently redundant second literal whose exact role this session could not resolve) sit behind reference-counted `PStringBase` appends whose actual wide-character content Binary Ninja's HLIL does not surface as a literal — this session had no live cdb attach and no running Ghidra MCP instance to recover the raw bytes. A two-attribute skill's formula therefore renders as `"Formula : (2 x Strength) + Endurance / 4 +2"`-shaped text that is very likely retail-correct in STRUCTURE but not byte-verified. | `src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs` (`ComposeFormula`, `AppendAttributeTerm`) | The single-attribute majority of skills render byte-correct today; only the minority of two-attribute formulas carry the unverified connector, and the gap is disclosed in the method's own doc rather than silently guessed. | A live retail capture of a two-attribute skill's formula text (e.g. via the cdb toolchain) could reveal `" + "` is wrong — the actual connector might be `" and "`, `" / "` (an OR-style formula, common for some AC skills that use whichever attribute is higher), or something else the two unresolved literals encode; `data_797584`'s role (appended after both terms) is also unexplained and could indicate a THIRD text segment this port omits entirely. | `gmCGSkillsPage::MakeSkillFormula @0x00480e10`; `SkillFormula` struct (`acclient.h`) | -| ~~AP-230~~ | **RETIRED 2026-08-28 by #408.** `LayoutImporter.BuildWidget` now applies every effective `P0x3B=true` as initial `Visible=false`; `UiDatElement`, `UiText`, and `UiButton` apply the committed named or DirectState value through their state machines; and the chargen-only walk is deleted. The installed-DAT sweep enumerates 1,019 resolved authored-invisible elements across 38 layouts, builds 990 after normal child consumption, and proves 990/990 start hidden. The complete automated App lane passes 6,511/6,511. | `src/AcDream.App/UI/Layout/LayoutImporter.cs`; `src/AcDream.App/UI/Layout/UiDatElement.cs`; `src/AcDream.App/UI/UiText.cs`; `src/AcDream.App/UI/UiButton.cs`; `tests/AcDream.App.Tests/UI/Layout/LayoutImporterInvisibleSweepTests.cs` | The old scoped-vs-general implementation split no longer exists. | None for property 0x3B visibility; runtime controllers may intentionally issue later visibility changes exactly as retail does. | `UIElement::OnSetAttribute @0x00462d80` case 8; `UIElement::SetState @0x00464e70` | +| AP-230 | **Filed 2026-08-16 at the Campaign CC gate round 1 Batch A fix (GF-13).** Retail's `UIElement::OnSetAttribute @0x00462d80` case 8 (`GetPropertyName()-0x33==8`, property id `0x3B`, "Invisible") hides ANY element authoring that property `true` via `SetVisible(value==0)` — a general, importer-level mechanism. The blast-radius sweep this fix's investigation ran found **1,083 elements client-wide** author `P0x3B=true` (the Summary page's GM-only `0x10000403`/`0x10000494` labels among them — the user-reported "-Non-admin or Non-envoy" leak). Honoring the flag client-wide in `LayoutImporter`/`DatWidgetFactory` is its own separately-gated visual sweep (docs/ISSUES.md #408, since a mis-hidden element among 1,083 untested ones would silently vanish a control nobody asked to disappear); this fix instead reads the flag as a PURE DATA ADDITION (`ElementInfo.Invisible`, `UiElement.AuthoredInvisible` — populated everywhere, acted on nowhere by the shared path) and only the chargen screen's own mount (`CharacterCreationUiController.HideAuthoredInvisibleElements`, called once at construction) walks its own subtree and hides whatever the dat itself marked hidden. **Second narrow honor added (F5/F6, gate round 1 closeout, 2026-08-16):** `LayoutImporter.BuildWidget`'s Batch C `UiText or UiField` un-consumed-children carve-out now ALSO honors `AuthoredInvisible`, scoped to exactly the children it builds through that one loop — a live-DAT sweep found the chat transcript's new-text indicator (`0x1000048C`) is one of the 37 carve-out (layout, element) pairs' children and authors `Invisible=true` itself, so the carve-out was building it as a visible phantom element retail never shows. Verified in both directions (`MediaBearingChildSweep_EnumeratesWhichAffectedChildrenAuthorInvisible` + `MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets`): the chat indicator now builds hidden, and the eight gold-frame pieces this carve-out ALSO covers do not author Invisible and stay visible. Still narrower than #408: only these two honor sites exist (chargen's own screen walk; this one carve-out loop) — every OTHER AuthoredInvisible-bearing element client-wide, reached through the ordinary generic-container recursion, remains data-only. | `src/AcDream.App/UI/Layout/ElementReader.cs` (`ElementInfo.Invisible`, `ApplyCanonicalLegacyProjection`'s `0x3Bu` read); `src/AcDream.App/UI/UiElement.cs` (`AuthoredInvisible`); `src/AcDream.App/UI/Layout/LayoutImporter.cs` (`BuildWidget`'s passthrough assignment AND the `UiText or UiField` carve-out's own honor); `src/AcDream.App/UI/Layout/CharacterCreationUiController.cs` (`HideAuthoredInvisibleElements`) | The scoped fix closes the ONE reported, live-DAT-confirmed symptom (chargen's two GM labels) without touching any of the other 1,083 elements' visibility, each of which needs its OWN visual gate before the general importer-wide honor can ship safely — narrowing blast radius to a screen this same gate round is already re-testing end-to-end. | Every OTHER screen with an authored-invisible element still renders it (the general honor is #408, not yet shipped) — this row and #408 both retire together once the general sweep lands and passes its own visual gate. | `UIElement::OnSetAttribute @0x00462d80` (case 8, `SetVisible(value==0)`) | | AP-229 | **Filed 2026-08-16 at the Campaign CC CC7 review-fix round, F1.** Retail does NOT stack screens: `UIFlow::QueueUIMode @0x004793c0` sets `_nextMode`, then `UIFlow::UseNewMode @0x004796a0` calls `_curUI->vtable->Show(0)` on the current framework, immediately DESTROYS it (`_curUI->vtable->__vecDelDtor(1)`), constructs the new framework, and calls `Show(1)` on it — so retail TEARS DOWN `gmCharacterManagementUI` the instant Create fires and RE-CONSTRUCTS it when Exit confirms (Exit-confirm's `RecvNotice_CloseDialog @0x004e9883-0x004e989c` issues `QueueUIMode(0x1000000a)`, the reverse transition). acdream's CC7 instead keeps BOTH `CharacterManagementUiController` and `CharacterCreationUiController` mounted as permanent siblings under the shared `Host.Root` and only reveals/occludes them (`Root.Visible` + `_host.BringToFront(Root)`) — this was already true since the CC4 FixedCanvas-arbiter work, but CC7 made it the production Create/Exit path rather than a dev-only shortcut. **Confirmed working within this narrower surface:** selection/world-name persistence across the round trip is retail-faithful (retail's own `UIPersistantData::m_iidSelectedAvatar`, `UIPersistantData::UIPersistantData @0x00479a00`, persists exactly this data across the destroy/reconstruct — acdream gets the same outcome for free by never tearing the screen down at all); input cannot bleed from the visible chargen screen through to the occluded management screen underneath (chargen's `Root.ClickThrough = false` over the full authored canvas, plus a `_host.BringToFront(Root)` call every tick chargen is open, keeps it strictly on top and input-opaque); and the two controllers share ONE `RetailDialogFactory` instance (`RetailUiRuntime.EnsureDialogFactory`), so `UiRoot.Modal` stays a single coherent stack instead of two independent ones. **Residual risk the reviewer named:** because character-management is never deactivated while chargen sits on top of it, its own `ReconcileDialogs` keeps running every tick (`CharacterManagementUiController.cs:663-667`'s `if (snapshot.Error is { } error)` arm) and can call `EnsureError` → `_dialogs.MakeMessage(...)` on the SAME shared factory chargen uses. `RetailDialogFactory.RefreshModal` (`RetailDialogFactory.cs:587`, `_host.Modal = _openOrder[^1].View?.Root`) always promotes the most-recently-opened dialog to `Modal` — an inbound `CharacterError` reaching the occluded management screen while chargen is the visible, active screen could take `UiRoot.Modal` away from chargen and hand it to a dialog owned by the screen underneath. Retail cannot have this race by construction: character-management's C++ object no longer exists once Create fires, so there is nothing left to receive a stray inbound event. **Dialog-as-sibling addendum (F3, gate round 1 closeout, 2026-08-16):** the same flat-sibling-list mechanism that motivates this row ALSO covers `RetailDialogFactory`'s own open dialogs — a dialog's root is a direct sibling of the chargen/character-management screen roots under the SAME `Host.Root`, and `RetailWindowManager.BringToFront` is a simple "highest ZOrder among siblings + 1", so whichever sibling's own `BringToFront` call runs LAST in a frame wins z-order. This was GF-15's actual root cause (a dialog opened while chargen is active got buried the very next frame because the screen's own per-tick `BringToFront` ran after the dialog's one-time open-time raise) and is now closed by `RetailDialogFactory.Tick()` re-raising every open dialog, in `_openOrder`, every tick — but the underlying divergence (dialogs and screens sharing one z-order list at all, where retail's dialog layer is architecturally separate from `UIFlow`'s single current framework) remains; any FUTURE sibling that calls its own unconditional per-tick `BringToFront` could reintroduce the same failure class against a dialog OR against chargen itself. | `src/AcDream.App/UI/RetailUiRuntime.cs:3845-3847` (`ConfigureCharacterManagement`'s cross-screen `RequestCreate` seam, both controllers mounted as permanent siblings); `src/AcDream.App/UI/Layout/CharacterCreationUiController.cs:465-473` (`Tick`'s reveal/occlude, not destroy/reconstruct); `src/AcDream.App/UI/Layout/CharacterCreationUiController.cs:265` (`Root.ClickThrough = false`); `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs:663-672` (`ReconcileDialogs`' `snapshot.Error` arm, still ticking underneath); `src/AcDream.App/UI/Layout/RetailDialogFactory.cs:587` (`RefreshModal`, the shared `Modal` stack) | Both screens existing as permanent siblings is deliberately simpler than a byte-port of retail's destroy/reconstruct lifecycle (no framework-factory table, no `Show`/`__vecDelDtor` lifecycle to replicate), and every observable behavior a user can drive through the ordinary UI today matches retail (selection persists, input doesn't bleed, dialogs stay single-stacked) — the residual is a narrow, not-yet-observed race on a specific inbound-error timing, not a general design flaw. | If an inbound `CharacterError` lands on the character-management channel while chargen is the visible, focused screen, `UiRoot.Modal` could flip to a dialog owned by the occluded screen underneath, stealing input from the still-visible chargen screen — a state retail cannot reach because the occluded screen simply does not exist there. | `UIFlow::QueueUIMode @0x004793c0`; `UIFlow::UseNewMode @0x004796a0` (`Show(0)` → `__vecDelDtor(1)` → construct → `Show(1)`); `RecvNotice_CloseDialog @0x004e9883-0x004e989c` (Exit-confirm's `QueueUIMode(0x1000000a)`); `UIPersistantData::UIPersistantData @0x00479a00` (`m_iidSelectedAvatar`) | | AP-228 | **Filed 2026-08-16 at the CC5 re-review residual round (R4).** The Summary listbox's skill-row KEY (the skill's display name) sources from `ItemAppraisalTextFormatter.SkillName(int)` — a hardcoded English `switch` over the 54 skill ids — where retail's own `gmCGSummaryPage::SetSummaryText @ 0x0047b1d0` builds that same key from the DAT-sourced `SkillBase->_name` field via a `%hs` format substitution (`data_79f3f0`, `0x0047b90f`-`0x0047b915`). Same divergence CLASS as AP-226 (a hardcoded acdream string standing in for a DAT-sourced retail field) but the polarity is REVERSED: AP-226 is retail-static-vs-acdream-DAT-sourced, while here retail is the DAT-sourced side and acdream is the hardcoded side. The identical pattern is ALSO present at a second call site, CC4's Skills page (`CharacterCreationSkillsPage`), which builds its own row labels through the SAME `ItemAppraisalTextFormatter.SkillName` call — not a second, independent divergence, the same one surfacing twice. | `src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs` (`SkillName`), consumed by `src/AcDream.App/UI/Layout/CharacterCreationSummaryPage.cs` (`AddSkillBucket`) and `src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs` | `SkillName` already backs every OTHER retail skill-name surface acdream has shipped (item-appraisal skill lines, wield-requirement text, usage-limit text — `ItemAppraisalTextFormatter`'s whole existing surface) — the Summary/Skills chargen pages reusing it keeps one skill-name source across the client instead of introducing a second, DAT-reading one for chargen alone. English-only is consistent with the rest of the client's current localization posture (no other surface reads a localized skill name from the DAT either). | A non-English or modded DAT install would show its real, localized skill names on retail's character sheet and item-examine windows but acdream's chargen Summary/Skills pages would keep showing the hardcoded English name regardless — a localization-only divergence, never a wire or gameplay difference (the skill id sent over the wire is unaffected). | `gmCGSummaryPage::SetSummaryText @ 0x0047b1d0` (`data_79f3f0`, `%hs` substitution `0x0047b90f`-`0x0047b915`) | | AP-227 | **Filed 2026-08-16 at the Campaign CC CC5 review-fix round, F9 (the Summary name field's empty-commit behavior).** Byte-decoded `gmCGSummaryPage::ListenToElementMessage @0x0047bf40` (`~0x0047bf93`): the length field it reads is NUL-inclusive (an empty field's length is 1 — the SAME finding AP-225's retirement/AP-226 both cite), and the WHOLE commit block — the `>32` check, `CharGenState::SetName`, AND `DoNameLimitDialog` — sits behind `if (length != 1)`. Blurring an EMPTIED field in retail is therefore a complete no-op: `CharGenState.name` stays whatever it held before, and the field visually shows empty while the internal name (what `DoFinish` actually sends) does not change. `CharacterCreationSummaryPage.CommitNameFromField` instead calls `SetName` unconditionally, including for an empty commit — the state always matches what the field just showed. | `src/AcDream.App/UI/Layout/CharacterCreationSummaryPage.cs` (`CommitNameFromField`) | Porting the exact skip was evaluated and rejected: it would fight `Refresh`'s own field-sync block (the F1 fix) — the NEXT unrelated Runtime revision bump (e.g. changing an attribute on another page, then returning to Summary) would see `field.Text ("") != snapshot.Name (the stale unchanged name)` and forcibly restore the OLD name into the emptied field, a spontaneous repopulation retail's own non-continuously-refreshed UI never produces. Always-clearing avoids that new failure mode at the cost of retail's exact one-frame field/state divergence. | A pixel-level side-by-side against retail would show: blur an emptied field, don't retype, click Finish — retail creates the character under the OLD (uncleared) name; acdream shows the `NoNameWarning` dialog instead (state genuinely empty). A narrow, one-interaction-wide behavioral difference, never silent (both paths produce a visible outcome, just a different one). | `gmCGSummaryPage::ListenToElementMessage @0x0047bf40` (`~0x0047bf93` length gate, `~0x0047bfb1` the gated block); `CharGenState::SetName` | @@ -456,7 +444,7 @@ research and is no longer active. | AP-212 | **Filed 2026-08-15 at Campaign CC slice CC4 (the Random button, element `0x100003cb`); primitives named+cited in the review fix round (F8, 2026-08-15). NARROWED 2026-08-15 at Campaign CC slice CC5 — Appearance and Summary CLOSED.** `gmCharGenMainUI::DoRandom @ 0x004e7d70` switches on the current page and dispatches to six NAMED, fully decompiled retail primitives, one per page: Heritage -> `CharGenState::RandomizeHeritageGroup(state, hasToD) @ 0x005c6a20`; Profession -> `CharGenState::RandomizeTemplate(state) @ 0x005c6500`; Skills -> `CharGenState::RandomizeSkills(state) @ 0x005c57e0`; Appearance -> `CharGenState::RandomizeAppearance(state, 0) @ 0x005c4f10` or `CharGenState::RandomizeClothing(state, 1) @ 0x005c6770`; Town -> `CharGenState::SetStartArea(state, RandInt(hasToD ? 4 : 3))`; Summary -> `CharGenState::RandomizeCharacter(state, hasToD) @ 0x005c6d80`. CC5 ports the Appearance/Summary primitives faithfully into `RuntimeCharacterCreationState` (`RandomizeAppearanceLocked`/`RandomizeClothingLocked`/`RandomizeCharacterLocked`, exposed as `TryRandomizeAppearance`/`TryRandomizeClothing`/`TryRandomizeCharacter`) and wires both pages' Random buttons to them — those two gaps are CLOSED, not approximated. **Still open:** Heritage/Profession/Town's Random handlers still use CC4's UNIFORM pick over every valid option (not `RandomizeHeritageGroup`'s hasToD-bounded roll, `RandomizeTemplate`'s exclude-current-preset roll, or `SetStartArea`'s literal 3/4 bound) — narrowing those three was not in CC5's scope; Skills' Random stays hard-disabled (`RandomizeSkills` remains unported). | `src/AcDream.App/UI/Layout/CharacterCreationUiController.cs` (`OnRandom`, `ApplyProgressState`'s `_random.Enabled` gate); `src/AcDream.App/UI/Layout/CharacterCreationHeritagePage.cs` (`Randomize`); `src/AcDream.App/UI/Layout/CharacterCreationProfessionPage.cs` (`Randomize`); `src/AcDream.App/UI/Layout/CharacterCreationTownPage.cs` (`Randomize`); `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs` (`Randomize`, CC5 — real primitive, retired from this row); `src/AcDream.Runtime/Session/RuntimeCharacterCreationState.cs` (CC5's Randomize section) | Random is a convenience affordance, not a gate any create can fail without — every value it can produce is independently reachable (and independently retail-cited) through the page's own ordinary Select commands; a uniform distribution over "every DAT-installed option" is the closest available stand-in for the THREE remaining pages without porting three more retail algorithms this round did not scope (Heritage/Profession/Town's own roll algorithms, now the only ones left). | A retail-parity test that checks the STATISTICAL distribution of repeated Random clicks on Heritage/Profession/Town would find acdream's uniform-over-all-options distribution differs from retail's own (e.g. `RandomizeTemplate`'s exclude-current-preset weighting, or the ToD-account-gated 3-vs-4 town bound — see AD-102); Appearance/Summary now match retail's real distribution exactly (RandInt/RollDice ported verbatim). Skills has no Random affordance at all until `RandomizeSkills` lands. | `gmCharGenMainUI::DoRandom @ 0x004e7d70`; `CharGenState::RandomizeHeritageGroup @ 0x005c6a20`; `CharGenState::RandomizeTemplate @ 0x005c6500`; `CharGenState::RandomizeSkills @ 0x005c57e0`; `CharGenState::SetStartArea` random-bound call site | | AP-211 | **Filed 2026-08-15 at the Campaign CC slice CC3 review-fix round (F12). Updated 2026-08-16 at Campaign CC slice CC7** — the row's own predicted resolution has now happened; text corrected rather than retired (see below). `RuntimeCharacterCreationState.TryBeginFinish` refuses locally (`RuntimeCharacterCreationLocalRefusal.RosterFull`) when `rosterCount >= slotCount`, gating a Finish attempt against the account's CharacterSet slot cap. `gmCharGenMainUI::DoFinish @ 0x004E9170` itself has NO such check — the decomp shows only the name/credit/verification-state gates (see the row's own doc comment history). Retail instead enforces the slot cap ONE LAYER UP, in the char-select UI that ghosts/un-ghosts the Create button (`gmCharacterManagementUI::UpdateButtons @ 0x004ec240`, ~0x004ec319-0x004ec32e: `_charSet.set_.m_num < _charSet.numAllowedCharacters_`) — CC7 ported that exact gate into `RuntimeCharacterSelectionButtons.CanCreate` (`RuntimeCharacterSelectionState.BuildButtons`) and wired `CharacterManagementUiController`'s Create button to it, closing the citation gap this row previously left open. ACE never checks the cap server-side either way. | `src/AcDream.Runtime/Session/RuntimeCharacterCreationState.cs` (`TryBeginFinish`, `RuntimeCharacterCreationLocalRefusal.RosterFull`); `src/AcDream.Runtime/Session/RuntimeCharacterSelectionState.cs` (`CanCreate`, CC7's retail-cited gate); `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` (Create's `Enabled` binding, CC7) | Both layers are now intentionally KEPT, matching this row's own prediction: the Create-button gate reproduces retail's real enforcement point for the ordinary UI path, while `TryBeginFinish`'s own refusal remains defense-in-depth for any caller that reaches Finish without going through that button (a headless bot, a future scripted client, or a UI bug that lets Finish fire while stale) — exactly the residual case the row's own risk column called out. | None remaining for the ordinary UI path (both layers now agree with retail's real enforcement site); a caller that bypasses the Create-button gate entirely still hits `TryBeginFinish`'s own refusal, which has no direct `DoFinish` citation (by design — retail's OWN `DoFinish` never checks this, only its UI layer does). | `gmCharGenMainUI::DoFinish @ 0x004E9170` (no slot-cap check present); `gmCharacterManagementUI::UpdateButtons @ 0x004ec240` (the retail enforcement site, now ported); `docs/plans/2026-08-15-character-creation-campaign.md` (Risks item 3) | -## 4. Temporary stopgap (TS) — 47 active rows (TS-68 retired 2026-08-28 by #360 — complete allegiance/house/MOTD grammar and wire; TS-69 retired 2026-08-28 by #361 — day/log/render now execute locally; TS-85 filed 2026-08-16 at #409 (client-wide retail tooltip system), REWRITTEN same-day at the F3 review round — two unported tooltip sub-mechanisms: (1) the `m_TTText`/`SetTooltip` runtime-text family headed by the `P0xD0` truncated-text auto-tooltip (187 of 430 live-DAT-probed tooltip-property-authoring elements have no literal StringInfo text and show nothing; the ORIGINAL filing's "dynamic InqProperty(0x49) override" framing was FALSE — retail's own base `InqProperty` reads the same authored bags this port already does, so most of those 187 show nothing in retail too — see the row's own full text for the correction), and (2) the per-element P0x3D wrap-max-width override (RetailTooltipPresenter always wraps at the display width, the confirmed retail fallback — no probed element authors P0x3D); TS-82 RETIRED 2026-08-15 at Campaign CC slice CC5 — the Summary page is now fully built (name field with NameInputFilter, the three-template listbox, its own live-idle-animated `gmCG3DView` preview, and the Finish gate's real UI), closing the last placeholder this row tracked (narrowed to Summary-only at CC6b-MOUNT after the Appearance page landed); TS-83 RETIRED 2026-08-15 at Campaign CC slice CC6b (pre-mount half) — the chargen 3D preview now plays retail's live 30fps idle loop (`ChargenPreviewAnimator`, `RetailAnimationCyclePlayback`) by default, exactly matching the decomp-verified finding that `gmCGAppearancePage::Update`'s own trailing gate calls `StartAnimation` whenever `m_bZoomedIn == 0` — CORRECTED at the same-round review (F1): the original filing argued this from the ctor never touching `m_bZoomedIn`, an unsound "elided/uninitialized byte" inference (heap `operator new` memory is indeterminate, not zero); the real, sound evidence is `gmCGAppearancePage::InitializePage @ 0x0047FDD0`'s EXPLICIT `this->m_bZoomedIn = 0;` at `0x004802C3`, written immediately after that same function sets the camera to the zoomed-IN per-heritage eye (`0x00480286-0x0048029E`) — a genuine retail quirk this implies: the character starts framed close-up AND not-zoomed-in at the same time, so the FIRST Zoom In click tweens close-eye→close-eye (visually null) while still freezing the animation, which the port reproduces faithfully — and only freezes to the held rest pose once the (not-yet-mounted) Zoom In button fires; the row's own citation "CreatureMode::set_sequence_animation... not yet located precisely" is resolved: the actual mechanism is `CPhysicsObj::set_sequence_animation @ 0x0050F6F0` called from `gmCG3DView::StartAnimation @ 0x004EE600` with a constant 30fps DID and no further motion traffic, which CC6b reproduces via a shared, Core, unit-tested advance-with-wrap-then-lerp/slerp primitive; TS-84 filed 2026-08-15 at Campaign CC slice CC6a (renumbered from its branch-local TS-82 at the CC6b-PRE merge: the CC4 branch independently allocated TS-82 for the Appearance/Summary placeholder pages, and landed first), corrected at the same-session review fix round (F2/F7) — the chargen 3D preview's un-ported `ClothingTable::BuildObjDesc` Setup-substitution chain, measured (not assumed) and now PINNED by a real assertion to leave Undead's default preview unclothed on ALL FOUR clothing slots (not three); TS-81 filed 2026-08-12 at Campaign FA slice FA2 — the AllegianceLoginNotification chat-text gap, BN-mislabeled string symbols pending DAT lookup; TS-80 partially narrowed same slice — the fellowship-create shareXp wire mechanism now exists, the option-bit reader is still FA4 scope; TS-75..TS-80 filed and TS-73 NARROWED 2026-08-11 at Campaign OP slice OP4 — the Character tab's 50-row consumer wiring: TS-73 narrowed to `DisableMostWeatherEffects`/`PersistentAtDay` only (`ViewCombatTarget`/`DisableDistanceFog` now work via App-layer poll bindings, not `TrySetOption`'s own switch); TS-75 "Always Daylight Outdoors" has no day/night time-of-day force (and corrects the plan's own `ForcedDayGroupIndex` mechanism-mismatch citation — that field is the WEATHER-VARIETY selector, not a time-of-day force); TS-76 five Character-tab rows with no consumer surface at all (3D tooltips, side-by-side vitals, spell durations, advanced combat UI, stay-in-chat-mode); TS-77 "Filter Language" has no profanity-filter subsystem; TS-78 "Use Main Pack as Default" has no client-side preferred-container consumer; TS-79 Group D salvage/housing (no salvage UI, no housing subsystem); TS-80 "Share Fellowship Experience and Luminance" is client-sourced (needs the fellowship-CREATE packet field, not just the stored bit) and unaudited this slice; TS-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's "Use Mouse Turning Settings" macro sends `PlayerOption.UseMouseTurning` and persists its five client-local siblings, but acdream has no persistent mouse-turning camera MODE for the bit to drive; TS-73 filed 2026-08-11 at the Campaign OP OP1 review-fix round — `RuntimeCharacterOptionsState.TrySetOption`'s port of `CPlayerModule::OnChanged`'s local side-effect switch (MF-2) covers only the two `PlayerModule`-state-mutating cases (0x02/0x12 fellowship mutual exclusion); the four presentation-binding cases (weather/day/combat-target/fog) remain unmodeled, pre-anchored to Campaign OP OP4's Group B consumer binds (see the row below); TS-71 RETIRED 2026-08-11 at the same round — both remaining `SetCharacterOptions (0x01A1)` flush triggers (the 480 s auto-save timer, the pre-logoff flush) are now wired through `LiveSessionController`'s own tick/stop transaction (`ConfigureAutoSaveTick`/`ConfigurePreLogoffFlush`, wired once by `GameRuntime`'s constructor), matching the plan's stated target; TS-72 RETIRED 2026-08-11 at the Campaign OP OP2 rework (double-REJECT fix round) — the click-toggle bit math is now decomp-CONFIRMED against `UIOption_CheckboxBitfield64::ListenToElementMessage @0x00485AE0` (`BitUtils::SetBitsOnOrOff`: OR-in-on / AND-NOT-off, which was already correct) and `::Refresh @0x004859C0` (the checked-state predicate, which WAS wrong — the shipped code required ALL mask bits set; retail checks on ANY mask bit — and is now fixed to match); the widget is still not reachable by any user (Campaign OP slice OP5 wires it), but nothing about its own click/checked mechanism remains genuinely unverified, so the row is retired rather than rewritten; TS-70 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item E (#362) — `ClientCommandResponses.cs` now parses and renders all four named inbound GameEvents (`ChannelIndex 0x0149`, `ChannelList 0x0148`, `AvailableHouses 0x0271`, `AllegianceInfoResponse 0x027C`), each wired into `GameEventWiring.cs` and rendering retail-shaped `LogTextType 0x00` lines ported from the named-retail decomp (`Handle_Communication__ChannelIndex`/`ChannelList` @0x0057d0c0/@0x0057d230, `Handle_House__Recv_AvailableHouses` + `DisplayListOfCoords` @0x00585d50/@0x00585c20, `Handle_Allegiance__AllegianceInfoResponseEvent` @0x0056a1d0); the row's `@on`/`@off` mention was never itself missing a handler (both already resolve through the pre-existing `WeenieErrorWithString` registration) so nothing there needed a fix; TS-68/TS-69 filed 2026-08-09, Campaign CH slice CH4 — the deferred allegiance/house subcommand dispatchers, the three unported pure-local commands (day/log/render); TS-66/TS-67 filed and TS-29 retired 2026-08-08, Campaign A slice A5 — the region ambient system landed, so TS-29's ambient half is ported and its music half turned out to have nothing to port; TS-66 is the omitted `seen_outside` interior case and TS-67 the in-plane contribution weight. TS-64/TS-65 filed 2026-08-08, Campaign A slice A2 — TS-64 the two unimplemented retail sound preferences (unfocused-app silence, pan disable) plus the three enable bools; TS-65 the volume-squared quirk, applied on the ambient path where two lanes byte-confirmed it and deliberately NOT on the hook path where the pre-multiplying overload is unpinned. TS-62/TS-63 filed 2026-08-02, continuation-executor slice; TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable `ObjectInfoState` bits via the new `ClientObjectTable`-backed `EntityCollisionFlagsExt.ResolveMoverPvpState` — **narrative corrected 2026-08-03 (#297): "real" only became true at #297. Until then the bits existed but the source `PublicWeenieBitfield` was frozen at CreateObject, so every one of those sites read a stale value for the whole session. The site enumeration is also incomplete: `RuntimeSetPositionMoverPreparation.cs:183-188` is a SEVENTH mover-flags site that decodes `record.Snapshot.ObjectDescriptionFlags` directly rather than calling `ResolveMoverPvpState`, and it also derives `ObjectInfoState.IsPlayer` from the PWD bit, contradicting `EntityCollisionFlags.cs:119-123`'s claim that every site uses a GUID-prefix heuristic. See AP-134.** — and `PlayerWeenie.JumpStaminaCost`'s `pk` parameter reads the real `PlayerKillerStatus`/`LastPkAttackTimestamp` pair against a 20-second window instead of a hardcoded `false`; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since `ResolveMoverPvpState` and the PK-timer predicate both resolve to a no-op for `PublicWeenieBitfield` absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (`CPhysicsObj::transition` 0x00512dc0 → `SPHEREPATH::init_sphere` 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (`CPartArray::GetStepUpHeight`/`GetStepDownHeight`, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) +## 4. Temporary stopgap (TS) — 50 active rows (TS-85 filed 2026-08-16 at #409 (client-wide retail tooltip system), REWRITTEN same-day at the F3 review round — two unported tooltip sub-mechanisms: (1) the `m_TTText`/`SetTooltip` runtime-text family headed by the `P0xD0` truncated-text auto-tooltip (187 of 430 live-DAT-probed tooltip-property-authoring elements have no literal StringInfo text and show nothing; the ORIGINAL filing's "dynamic InqProperty(0x49) override" framing was FALSE — retail's own base `InqProperty` reads the same authored bags this port already does, so most of those 187 show nothing in retail too — see the row's own full text for the correction), and (2) the per-element P0x3D wrap-max-width override (RetailTooltipPresenter always wraps at the display width, the confirmed retail fallback — no probed element authors P0x3D); TS-82 RETIRED 2026-08-15 at Campaign CC slice CC5 — the Summary page is now fully built (name field with NameInputFilter, the three-template listbox, its own live-idle-animated `gmCG3DView` preview, and the Finish gate's real UI), closing the last placeholder this row tracked (narrowed to Summary-only at CC6b-MOUNT after the Appearance page landed); TS-83 RETIRED 2026-08-15 at Campaign CC slice CC6b (pre-mount half) — the chargen 3D preview now plays retail's live 30fps idle loop (`ChargenPreviewAnimator`, `RetailAnimationCyclePlayback`) by default, exactly matching the decomp-verified finding that `gmCGAppearancePage::Update`'s own trailing gate calls `StartAnimation` whenever `m_bZoomedIn == 0` — CORRECTED at the same-round review (F1): the original filing argued this from the ctor never touching `m_bZoomedIn`, an unsound "elided/uninitialized byte" inference (heap `operator new` memory is indeterminate, not zero); the real, sound evidence is `gmCGAppearancePage::InitializePage @ 0x0047FDD0`'s EXPLICIT `this->m_bZoomedIn = 0;` at `0x004802C3`, written immediately after that same function sets the camera to the zoomed-IN per-heritage eye (`0x00480286-0x0048029E`) — a genuine retail quirk this implies: the character starts framed close-up AND not-zoomed-in at the same time, so the FIRST Zoom In click tweens close-eye→close-eye (visually null) while still freezing the animation, which the port reproduces faithfully — and only freezes to the held rest pose once the (not-yet-mounted) Zoom In button fires; the row's own citation "CreatureMode::set_sequence_animation... not yet located precisely" is resolved: the actual mechanism is `CPhysicsObj::set_sequence_animation @ 0x0050F6F0` called from `gmCG3DView::StartAnimation @ 0x004EE600` with a constant 30fps DID and no further motion traffic, which CC6b reproduces via a shared, Core, unit-tested advance-with-wrap-then-lerp/slerp primitive; TS-84 filed 2026-08-15 at Campaign CC slice CC6a (renumbered from its branch-local TS-82 at the CC6b-PRE merge: the CC4 branch independently allocated TS-82 for the Appearance/Summary placeholder pages, and landed first), corrected at the same-session review fix round (F2/F7) — the chargen 3D preview's un-ported `ClothingTable::BuildObjDesc` Setup-substitution chain, measured (not assumed) and now PINNED by a real assertion to leave Undead's default preview unclothed on ALL FOUR clothing slots (not three); TS-81 filed 2026-08-12 at Campaign FA slice FA2 — the AllegianceLoginNotification chat-text gap, BN-mislabeled string symbols pending DAT lookup; TS-80 partially narrowed same slice — the fellowship-create shareXp wire mechanism now exists, the option-bit reader is still FA4 scope; TS-75..TS-80 filed and TS-73 NARROWED 2026-08-11 at Campaign OP slice OP4 — the Character tab's 50-row consumer wiring: TS-73 narrowed to `DisableMostWeatherEffects`/`PersistentAtDay` only (`ViewCombatTarget`/`DisableDistanceFog` now work via App-layer poll bindings, not `TrySetOption`'s own switch); TS-75 "Always Daylight Outdoors" has no day/night time-of-day force (and corrects the plan's own `ForcedDayGroupIndex` mechanism-mismatch citation — that field is the WEATHER-VARIETY selector, not a time-of-day force); TS-76 five Character-tab rows with no consumer surface at all (3D tooltips, side-by-side vitals, spell durations, advanced combat UI, stay-in-chat-mode); TS-77 "Filter Language" has no profanity-filter subsystem; TS-78 "Use Main Pack as Default" has no client-side preferred-container consumer; TS-79 Group D salvage/housing (no salvage UI, no housing subsystem); TS-80 "Share Fellowship Experience and Luminance" is client-sourced (needs the fellowship-CREATE packet field, not just the stored bit) and unaudited this slice; TS-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's "Use Mouse Turning Settings" macro sends `PlayerOption.UseMouseTurning` and persists its five client-local siblings, but acdream has no persistent mouse-turning camera MODE for the bit to drive; TS-73 filed 2026-08-11 at the Campaign OP OP1 review-fix round — `RuntimeCharacterOptionsState.TrySetOption`'s port of `CPlayerModule::OnChanged`'s local side-effect switch (MF-2) covers only the two `PlayerModule`-state-mutating cases (0x02/0x12 fellowship mutual exclusion); the four presentation-binding cases (weather/day/combat-target/fog) remain unmodeled, pre-anchored to Campaign OP OP4's Group B consumer binds (see the row below); TS-71 RETIRED 2026-08-11 at the same round — both remaining `SetCharacterOptions (0x01A1)` flush triggers (the 480 s auto-save timer, the pre-logoff flush) are now wired through `LiveSessionController`'s own tick/stop transaction (`ConfigureAutoSaveTick`/`ConfigurePreLogoffFlush`, wired once by `GameRuntime`'s constructor), matching the plan's stated target; TS-72 RETIRED 2026-08-11 at the Campaign OP OP2 rework (double-REJECT fix round) — the click-toggle bit math is now decomp-CONFIRMED against `UIOption_CheckboxBitfield64::ListenToElementMessage @0x00485AE0` (`BitUtils::SetBitsOnOrOff`: OR-in-on / AND-NOT-off, which was already correct) and `::Refresh @0x004859C0` (the checked-state predicate, which WAS wrong — the shipped code required ALL mask bits set; retail checks on ANY mask bit — and is now fixed to match); the widget is still not reachable by any user (Campaign OP slice OP5 wires it), but nothing about its own click/checked mechanism remains genuinely unverified, so the row is retired rather than rewritten; TS-70 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item E (#362) — `ClientCommandResponses.cs` now parses and renders all four named inbound GameEvents (`ChannelIndex 0x0149`, `ChannelList 0x0148`, `AvailableHouses 0x0271`, `AllegianceInfoResponse 0x027C`), each wired into `GameEventWiring.cs` and rendering retail-shaped `LogTextType 0x00` lines ported from the named-retail decomp (`Handle_Communication__ChannelIndex`/`ChannelList` @0x0057d0c0/@0x0057d230, `Handle_House__Recv_AvailableHouses` + `DisplayListOfCoords` @0x00585d50/@0x00585c20, `Handle_Allegiance__AllegianceInfoResponseEvent` @0x0056a1d0); the row's `@on`/`@off` mention was never itself missing a handler (both already resolve through the pre-existing `WeenieErrorWithString` registration) so nothing there needed a fix; TS-68/TS-69 filed 2026-08-09, Campaign CH slice CH4 — the deferred allegiance/house subcommand dispatchers, the three unported pure-local commands (day/log/render); TS-66/TS-67 filed and TS-29 retired 2026-08-08, Campaign A slice A5 — the region ambient system landed, so TS-29's ambient half is ported and its music half turned out to have nothing to port; TS-66 is the omitted `seen_outside` interior case and TS-67 the in-plane contribution weight. TS-64/TS-65 filed 2026-08-08, Campaign A slice A2 — TS-64 the two unimplemented retail sound preferences (unfocused-app silence, pan disable) plus the three enable bools; TS-65 the volume-squared quirk, applied on the ambient path where two lanes byte-confirmed it and deliberately NOT on the hook path where the pre-multiplying overload is unpinned. TS-62/TS-63 filed 2026-08-02, continuation-executor slice; TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable `ObjectInfoState` bits via the new `ClientObjectTable`-backed `EntityCollisionFlagsExt.ResolveMoverPvpState` — **narrative corrected 2026-08-03 (#297): "real" only became true at #297. Until then the bits existed but the source `PublicWeenieBitfield` was frozen at CreateObject, so every one of those sites read a stale value for the whole session. The site enumeration is also incomplete: `RuntimeSetPositionMoverPreparation.cs:183-188` is a SEVENTH mover-flags site that decodes `record.Snapshot.ObjectDescriptionFlags` directly rather than calling `ResolveMoverPvpState`, and it also derives `ObjectInfoState.IsPlayer` from the PWD bit, contradicting `EntityCollisionFlags.cs:119-123`'s claim that every site uses a GUID-prefix heuristic. See AP-134.** — and `PlayerWeenie.JumpStaminaCost`'s `pk` parameter reads the real `PlayerKillerStatus`/`LastPkAttackTimestamp` pair against a 20-second window instead of a hardcoded `false`; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since `ResolveMoverPvpState` and the PK-timer predicate both resolve to a no-op for `PublicWeenieBitfield` absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (`CPhysicsObj::transition` 0x00512dc0 → `SPHEREPATH::init_sphere` 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (`CPartArray::GetStepUpHeight`/`GetStepDownHeight`, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| @@ -517,8 +505,8 @@ research and is no longer active. | TS-64 | **Retail's sound-preference surface is only partly present.** Retail registers eight `[Sound]` keys in `SoundManager::InitPrefs` @ `0x005503F0`; two are unimplemented in acdream. (a) `s_bPlaySoundOnlyWhenActive` (default **1**) is checked against `Device::m_bIsActiveApp` in every entry point and in both `PlaySoundInternal` overloads, so an unfocused retail client is SILENT; acdream keeps playing when the window loses focus. (b) `s_SoundFeatures == 1` forces pan to dead centre; acdream's `RetailSoundMixer.Mix`/`GetPan` take a `panningEnabled` flag with conformance coverage, but no preference is wired behind it, so panning can never be turned off. The three enable bools (`Sound Disabled`, `Ambient Sound Disabled`, `Interface Sound Disabled`) also have no acdream counterpart — note retail's on-disk polarity is inverted relative to its backing variables, so a future reader must not assume the sense. | `src/AcDream.App/Audio/OpenAlAudioEngine.cs` (no focus gate); `src/AcDream.Core/Audio/RetailSoundMixer.cs` (`panningEnabled`, unwired) | Slice A2 kept its blast radius on the mixing model: window-focus state and a preference surface are host plumbing rather than mixing math, and the mixer parameter exists so wiring them later needs no math change. | Alt-tabbed acdream keeps making noise where retail goes quiet; users cannot disable panning or the individual sound classes. | `SoundManager::InitPrefs @ 0x005503F0`; `SoundManager::PlaySoundInternal @ 0x0054FEC0` and `@ 0x00550170`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` §1 | | TS-65 | **Volume-squared quirk applied on the ambient path only.** Retail multiplies its volume knob twice on several paths: `PlaySoundA(DataID, CPhysicsObj*)` passes `effect_sound_volume` as the `vol` argument and `GetAttenuation` then multiplies by `effect_sound_volume` again, and both `PlayAmbientSound*` entry points pre-multiply by `ambient_sound_volume` before that same second multiply — so those sliders are effectively squared. acdream's `RetailSoundMixer.TryGetAttenuation` applies the knob exactly once (which is what `GetAttenuation` itself does) and the animation-hook path does not pre-multiply. Slice A5 squares the ambient path, where two independent lanes byte-confirmed the double application. | `src/AcDream.Core/Audio/RetailSoundMixer.cs` (`TryGetAttenuation` remarks); `src/AcDream.App/Audio/OpenAlAudioEngine.cs` (`Play3DWave`) | Which `PlaySoundA` overload the animation-hook path reaches was not pinned by the lane-1 decode, and inventing a squaring on an unconfirmed overload would change every hook sound's loudness curve on a guess. Single-multiply is the conservative, decoded-function-exact choice; the open question is cheap to settle with a cdb breakpoint on the two overloads. | At a non-unity effect slider, hook sounds are louder than retail (slider 0.5 gives −6 dB where retail gives −12). At the default slider of 1.0 the two are identical, so this is inert until the user moves the slider. | `SoundManager::PlaySoundA @ 0x00550AF0`/`@ 0x00550B70`/`@ 0x005507A0`; `SoundManager::GetAttenuation @ 0x00550020`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` §3 D12 | | ~~TS-66~~ | **RETIRED 2026-08-08 (Campaign A listening-gate fix; user-reported).** `seen_outside` interiors now keep the OUTDOOR ambient set: the listener source resolves the per-cell `CEnvCell.seen_outside` bit through the physics cache's `CellPhysics` record (the same #107 field `AdjustPosition` reads) and converts the ENVCELL-local origin through the cell's `WorldTransform` into landblock coordinates before the 3×3 walk centres on it — an outdoor Position's origin is already landblock-local, an envcell's is not, and skipping the conversion would centre the walk on a wrong point by up to a landblock. A cell record not yet resident resolves to silence for that rebuild rather than a wrong walk. Sealed interiors (dungeons) remain silent, which is retail-correct. | retired | — | — | `Ambient` gate per `docs/research/2026-08-08-audio-retail-ambient-authoring.md` §6/§8; `CEnvCell::add_ambient_sounds` (folded `ret`); user listening gate 2026-08-08 ("in retail I get both outside ambient and the ambient from indoors") | -| ~~TS-68~~ | **RETIRED 2026-08-28 (#360).** The full named-retail `@allegiance`/`@all`, `@house`/`@hou`, and standalone `@motd` grammar now executes locally through one Runtime dispatcher shared by graphical and headless hosts. All management branches have byte-verified GameAction builders, exact argument/refusal behavior, client-local unknown-subcommand ownership, shared sequence allocation, and active-session routing. | retired | — | — | `ClientCommunicationSystem::DoAllegiance @ 0x0057D5A0`; `DoHouse @ 0x00580860`; ACE `GameActionType` readers; docs/ISSUES.md #360 | -| ~~TS-69~~ | **RETIRED 2026-08-28 (#361).** `@day`, `@log`, and `@render` now execute locally: persistent noon landscape lighting, reconnect-safe chat-file lifecycle, and named-retail radius/FOV parsing and replies. | retired | — | — | `ClientCommunicationSystem::DoDay @ 0x005706F0`; `DoSetOutput @ 0x0057E4F0`; `DoRenderOption @ 0x0057E120`; docs/ISSUES.md #361 | +| TS-68 | **Filed 2026-08-09 (Campaign CH slice CH4); corrected 2026-08-09 at the CH4 REJECT-review, Blocker 1.** `@allegiance`/`@all` and `@house`/`@hou` are real retail management-command dispatchers with 12 and 15 subcommands respectively (registry doc §2.5/§2.5b). acdream ports only the subset with simple parameterless/single-field wire shapes (allegiance `info`/`hometown`/`ho`; house `recall`/`re`/`mansion_recall`/`alleg_recall`/`ma`/`abandon`). For `@house`, every other subcommand (open, close, storage, remove, boot, boot_all, remove_all, guest, available, hooks, on, off) still falls through to ACE server-passthrough (which replies "Unknown command") — unchanged from the original filing. **The original filing was WRONG for `@allegiance`/`@all`: retail's own `DoAllegiance` never reaches DoChannelCommand/server-passthrough for an unrecognized subcommand** — it prints "Please see @help Allegiance for more information on how to use this command." locally (`label_57da4b`, 0x0057DA4B) and stays entirely client-side. **Corrected again 2026-08-09 at the CH4 re-review, SHOULD-FIX 3.** Retail does NOT refuse boot/ban/officer/title/motd/name/lock/house/chat/broadcast — `DoAllegiance`'s dispatcher table EXECUTES each one locally through its own handler (e.g. `DoAllegianceBoot @ 0x0057D646` is the dispatcher's call site into `ClientCommunicationSystem::DoAllegianceBoot`; `DoAllegianceBan`/`DoAllegianceOfficer`/`DoAllegianceOfficerTitle`/`DoMotd`/`DoAllegianceName`/`DoAllegianceLock`/`DoAllegianceHouse` are its siblings in the same table). acdream has none of those nine handlers ported (tracked by issue #360) and instead shows the SAME unrecognized-subcommand refusal ("Please see @help Allegiance...", `label_57da4b`, 0x0057DA4B) for every one of them, pending the #360 port. What matches retail here is the OWNERSHIP RULE — the verb never reaches `DoChannelCommand`/server-passthrough for `@allegiance`/`@all` regardless of subcommand — NOT the subcommand's actual behavior, which retail executes and acdream does not yet. This still closes the real bug the original filing named (the unmatched subcommand text broadcast to the Allegiance chat channel, 0x02000000). The standalone `@motd` verb (reached directly, not via `@allegiance motd`) remains a separate, still-open gap. `RetailClientCommandCatalog.TryMatchHouse`/`TryMatchAllegiance` (`src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs`) | Retail would execute these locally (with its own usage/confirmation/refusal text). House's unported subcommands still reach ACE, which does not implement them as chat commands either — no functional loss on a real server, but a user typing e.g. `@house open` gets ACE's generic "Unknown command" instead of retail's real behavior. Allegiance's unported subcommands correctly stay local (never reach ACE) but show a generic refusal instead of retail's real per-subcommand execution — a user typing e.g. `@allegiance boot Name` gets "Please see @help Allegiance..." instead of retail's real boot confirmation/effect, until #360 ports the nine `DoAllegiance*`/`DoMotd`/`DoAllegianceHouse` handlers. | `ClientCommunicationSystem::DoAllegiance @ 0x0057D5A0`; `DoHouse @ 0x00580860`; ACE `GameActionType` opcodes for each subcommand (all exist server-side) | +| TS-69 | **Filed 2026-08-09 (Campaign CH slice CH4).** `@day`, `@log`, and `@render` are registered retail verbs acdream recognizes only in the `/help ` lookup table, not as executable client commands. `@day` needs a sky/time-of-day override hook the renderer doesn't expose; `@log` needs a safely-lifecycled chat-to-file writer (deferred to avoid an unaudited file-handle leak across reconnects); `@render` has no acdream equivalent to retail's `SmartBox::HandleRenderOption` render-option surface. All three fall through to server passthrough. `RetailCommandHelpTable` (`src/AcDream.UI.Abstractions/Panels/Chat/RetailCommandHelpTable.cs`) | A user typing `@day`/`@log`/`@render` gets ACE's "Unknown command" instead of retail's local toggle/file-copy/render-option behavior — cosmetic/QoL only, no gameplay impact | `ClientCommunicationSystem::DoDay @ 0x005706F0`; `DoSetOutput @ 0x0057E4F0`; `DoRenderOption @ 0x0057E120` | | TS-67 | **Ambient contributions are computed in-plane.** Retail's `CLandBlock::add_ambient_sounds` @ `0x530310` positions each contributing land cell at its own SW terrain VERTEX, including that vertex's height, and `Ambient::CalcWeight` deliberately includes Z in its distance (where `CalcDir` deliberately excludes it — the two differ on purpose). acdream's gatherer supplies Z = 0 for the offset, so a cell's weight ignores the height difference between the listener and the terrain under that cell. | `src/AcDream.Core/Audio/AmbientSoundGatherer.cs` (`ContributeLandblock`) | Sampling the height needs the landblock's height table threaded into the walk alongside the terrain words; the walk already runs only on a 24 m crossing so the cost is not the obstacle, the extra plumbing at slice end was. The error is bounded by terrain relief inside 120 m and affects the crossfade weight only, never the direction. | On steep ground an ambient reads slightly louder than retail, because the true 3-D distance is longer than the planar one. | `CLandBlock::add_ambient_sounds @ 0x530310`; `Ambient::CalcWeight @ 0x550DD0` | | TS-74 | **Filed 2026-08-11 at Campaign OP slice OP3; What/Where extended 2026-08-11 at the OP3 review-fix round (mechanism review S5).** acdream has no persistent "turn to face camera" mouse-turning MODE — `MouseLookState` only implements retail's MMB-hold `CameraInstantMouseLook`. The Options panel's "Use Mouse Turning Settings" button still sends the `PlayerOption.UseMouseTurning` bit (`SetSingleCharacterOption 0x0005`) and persists the five client-local `CameraTurningSettings` preferences exactly as retail does — but flipping the bit ON has NO observable effect on acdream's camera today, because the mode it is supposed to enable was never built. **All five persisted preferences are STORE-ONLY with no consumer, not just the camera mode itself:** `Camera_Stiffness`, `Camera_AdjustmentSpeed`, `Camera_AlignToSlope`, `Input_MouseLookSensitivity`, and `Input_InvertMouseLookYAxis` (research doc `2026-08-10-options-panel-structure.md` §4) land in `settings.json`'s `cameraTurning` section and are read back only by the macro itself — acdream's ACTUALLY-live mouse sensitivity lives entirely separately, in `CameraPointerInputController`'s `_chase`/`_flySensitivity`/`_orbitSensitivity` fields (F8/F9-adjustable), so the macro's chat lines quote a `Default`-seeded "from" value (e.g. `0.550000`) that describes no live client state on a fresh profile. **LANDED 2026-08-11 at Campaign OP slice OP6**: the Config tab now surfaces all five as its own Camera/Input rows (`ConfigOptionsPageController.BindCameraSection`/`BindInputSection`), plus a SIXTH, previously-unmodeled field — `CameraTurningSettings.UseMouseTurning` (`Input_UseMouseTurning`, the Config tab's OWN client-local checkbox, distinct from the server-synced `PlayerOption.UseMouseTurning` bit this row already describes) — with the SAME store-only disposition; the "two stores for one concept" symptom below is now directly observable rather than latent. | `src/AcDream.UI.Abstractions/Input/MouseLookState.cs` (the only mouse-look mode present); `src/AcDream.App/UI/Layout/MouseTurningSettingsMacro.cs` (sends the bit regardless); `src/AcDream.UI.Abstractions/Panels/Settings/CameraTurningSettings.cs` (the six store-only keys); `src/AcDream.App/Input/CameraPointerInputController.cs` (the SEPARATE, actually-live sensitivity fields); `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (the Config tab's own rows, OP6) | Building the persistent mouse-turning camera mode is a camera/physics-scope feature, out of the Options-panel campaign's scope; the STORE-and-SEND half is honest and complete (matches every other stored-but-unconsumed option class in this register), so the bit round-trips correctly for any future consumer or a retail client reading the same character. | A user who clicks "Use Mouse Turning Settings" expecting the camera to start turning with mouse movement sees no camera change — only the (unwired) preferences persisting and the wire bit flipping. Beyond that: a user who separately tunes acdream's live F8/F9 mouse sensitivity, then clicks this button, sees a chat line quoting an UNRELATED stored value, not their live sensitivity — two stores for one concept, now user-visible in the Config tab UI (OP6). | `PlayerModule::UseMouseTurning @0x005D3380`; `CharacterOptions2.UseMouseTurning 0x00400000`; `claude-memory/project_camera_visibility_coupling.md` | diff --git a/docs/launch-options.md b/docs/launch-options.md index 9decd203..0d6178ec 100644 --- a/docs/launch-options.md +++ b/docs/launch-options.md @@ -40,13 +40,11 @@ Assume a flag has a side effect until its row says otherwise. - **Everything diagnostic is OFF by default.** Every probe, dump, capture, and measurement flag in this document is inert until its variable is explicitly set — an unset environment runs zero diagnostics. Exactly - five flags default ON, and none is a diagnostic: `ACDREAM_RETAIL_CHASE`, + four flags default ON, and none is a diagnostic: `ACDREAM_RETAIL_CHASE`, `ACDREAM_CAMERA_COLLIDE`, `ACDREAM_CAMERA_ALIGN_SLOPE`, and `ACDREAM_RETAIL_CLOSE_DEGRADES` are retail *behaviors* wearing an A/B - off-switch (`=0` disables the behavior for a comparison run), while - `ACDREAM_RETAIL_UI` is the product's only gameplay presentation and uses - the same explicit diagnostic opt-out. That five-flag set is frozen by - `LaunchOptionsDocumentationTests` — a new + off-switch (`=0` disables the behavior for a comparison run). That + four-flag set is frozen by `LaunchOptionsDocumentationTests` — a new default-on flag fails the build. - `=1` means the code tests for exactly the string `1`. Setting `true`, `yes`, or `0` does **not** enable such a flag (and `0` does not disable @@ -77,13 +75,14 @@ $env:ACDREAM_TEST_HOST = "127.0.0.1" $env:ACDREAM_TEST_PORT = "9000" $env:ACDREAM_TEST_USER = "testaccount" $env:ACDREAM_TEST_PASS = "testpassword" +$env:ACDREAM_RETAIL_UI = "1" dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Release ``` | Flag | Value | What it does | Side effects | Default | Read by | |---|---|---|---|---|---| | `ACDREAM_A2C` | `unset/""` keep preset; `"0"/"false"/"False"/"FALSE"` → off; any other non-empty → on | Overrides preset's `AlphaToCoverage` blend flag | Changes MSAA alpha-to-coverage blending mode for foliage/translucent draws — a visual-behavior change, not just perf | preset's `AlphaToCoverage` (High/Ultra=true, Low/Medium=false) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:52`) | -| `ACDREAM_AC_DIR` | `=` | Points at a real retail AC install dir; loads `/controls/controls.ini` to source retail keybind display strings for the retained UI. | Only has any effect while the default-on retained UI is composed (`ACDREAM_RETAIL_UI` is not `0`). Unset → `ControlsIni.Parse(string.Empty)`, an empty (not error) controls table — silent, no fallback file is searched. | unset (null) → empty controls table | `RuntimeOptions.AcDir` → `InteractionRetainedUiComposition.cs:610` | +| `ACDREAM_AC_DIR` | `=` | Points at a real retail AC install dir; loads `/controls/controls.ini` to source retail keybind display strings for the retained UI. | Only has any effect when `ACDREAM_RETAIL_UI=1` (retained UI composed). Unset → `ControlsIni.Parse(string.Empty)`, an empty (not error) controls table — silent, no fallback file is searched. | unset (null) → empty controls table | `RuntimeOptions.AcDir` → `InteractionRetainedUiComposition.cs:610` | | `ACDREAM_ANISOTROPIC` | `=` (`int.TryParse`, invariant) | Overrides preset's `AnisotropicLevel` texture filtering | Changes GPU texture sampling filter level (visual sharpness), not just perf | preset's `AnisotropicLevel` (Low=4, Medium=8, High/Ultra=16) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:49`) | | `ACDREAM_CACHE_DIR` | `=` | Overrides the resolved cache-root directory (used for `DiagnosticsDirectory`, etc.) | none beyond redirecting cache I/O | Windows: `%LOCALAPPDATA%\acdream\cache`; Linux: `$XDG_CACHE_HOME/acdream` or `~/.cache/acdream` | `ApplicationPathSet.Resolve` (`ApplicationPathSet.cs:83`), via `IApplicationPathEnvironment` seam | | `ACDREAM_CAMERA_ALIGN_SLOPE` | `=0` disables (anything else/unset = on) | selects whether the chase camera basis tilts to the player's 5-frame averaged velocity vs staying flat/horizontal on slopes | alters camera orientation / rendered view every frame; startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| true (on) | `AcDream.Core.Rendering.CameraDiagnostics.AlignToSlope` | @@ -116,7 +115,7 @@ dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Release | `ACDREAM_RESIDENCY_STANDALONE_UNOWNED_MIB` | `=` (`>0`, else default) | Byte ceiling for unowned standalone texture memory | Same eviction-cadence caveat | 32 MiB | `ResidencyBudgetOptions.Parse` (`:73-75`) | | `ACDREAM_RETAIL_CHASE` | `=0` disables (anything else/unset = on) | selects the retail-faithful `RetailChaseCamera` vs. the legacy rigid-follow `ChaseCamera` | swaps the entire active camera implementation — changes camera motion/feel; startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| true (retail camera on) | `CameraDiagnostics.UseRetailChaseCamera` | | `ACDREAM_RETAIL_CLOSE_DEGRADES` | inverted: `="0"` disables; any other value (incl. unset) enables | Default-**on** real gameplay behavior: applies retail's close-range LOD mesh-part swap (`GfxObjDegradeResolver`) to humanoid setups (issue #47), matching retail's close-detail degrade. | Inverted default (opposite of every other boolean flag in this table — presence of the literal string `"0"` is what disables it, not presence of `"1"` enabling it). Documented explicitly as "set only for before/after diagnostic comparisons" — so although default-on production behavior, its *disable* path exists purely for A/B measurement. | `true` (enabled) unless value is exactly `"0"` | `RuntimeOptions.RetailCloseDegrades` → `DatLiveEntityProjectionMaterializer.cs:275-276,480-498` | -| `ACDREAM_RETAIL_UI` | `=0` disables; anything else/unset enables | Controls the retained retail UI host tree (`UiHost`/`UiRoot`, D.2b), the product's only gameplay UI. | Disabling it leaves world rendering with no gameplay interface and is intended only for diagnostics. The same rule applies to bare env and `--session-config`/launcher launches; no path force-overrides it. | `true` | `RuntimeOptions.RetailUi` → `LivePresentationComposition.cs` (gates retained-UI mount via `InteractionRetainedUiComposition`) | +| `ACDREAM_RETAIL_UI` | `=1` | Switches on the retained retail UI host tree (`UiHost`/`UiRoot`, D.2b). Without it, no retained UI is composed at all — e.g. no chargen Appearance page, no Summary page. | **Forced to `true` unconditionally** for every `--session-config` / launcher launch (`RuntimeOptions.cs:282`, "a session-config launch IS a product launch — the retail UI is the shipped UI, not a dev option"), regardless of this env var's value — the env var only matters for the bare env-var dev-flow launch path. | `false` for the env-var dev flow; `true` always for `--session-config` launches | `RuntimeOptions.RetailUi` → `LivePresentationComposition.cs:1108-1131` (gates retained-UI mount via `InteractionRetainedUiComposition`), `GameWindow.cs:455,566` (comments), `RuntimeOptions.cs:277` | | `ACDREAM_TEST_HOST` | `=` | ACE server hostname for live-mode connect. | none | `"127.0.0.1"` | `RuntimeOptions.LiveHost` (`RuntimeOptions.cs:142`) | | `ACDREAM_TEST_PASS` | `=` | ACE account password for live-mode connect. | Redacted in `RuntimeOptions.ToString()`/diagnostic printing by design (`PrintMembers` override, `RuntimeOptions.cs:326-342`) — defense-in-depth so it can never leak into a log/exception via the record's default printing. | `null` (empty → `HasLiveCredentials` false) | `RuntimeOptions.LivePass` (`RuntimeOptions.cs:145`) | | `ACDREAM_TEST_PORT` | `=` | ACE server port for live-mode connect. | none | `9000` | `RuntimeOptions.LivePort` (`RuntimeOptions.cs:143`) | @@ -185,7 +184,7 @@ fall back to `ACDREAM_DAT_DIR`. | `ACDREAM_ORBIT_YAW_DEGREES` | `=`, must be finite | Diagnostic-only initial orbit camera heading. | Non-finite values silently rejected (→ `null`). | `null` | `RuntimeOptions.InitialOrbitYawDegrees` → `GameWindow.cs:1413` | | `ACDREAM_PROBE_REVEAL_RADIUS` | `==1` (unparsable or `<1` → override absent; floor is 1, not 0) | #280 A/B measurement probe: forces the OUTDOOR reveal gate to use this landblock radius instead of the derived streaming window (near radius clamped to it), so a route can be measured with the pre-#280 behavior (`=1`, old `OutdoorNeighborhoodRadius`) vs. current | **Changes what gets revealed, not just measured** — genuinely resizes the reveal/visible window used by the live reveal gate. CLAUDE.md: "Leave it unset for any measurement or gate run — with it set you are measuring a different window than production." `=0` is rejected by the parser specifically because it would hang the very A/B route it exists to measure (`RequiredRenderRadius==0` fails `invalid-readiness-shape`). Not a user setting, not in Settings/RuntimeOptions, not persisted. | unset (derivation in charge, no override) | `StreamingDiagnostics.RevealRadiusOverride` (`StreamingDiagnostics.cs:25-27,76-80`), applied by `StreamingDiagnostics.ApplyRevealRadiusOverride` | | `ACDREAM_PROBE_WORLD_FRAME` | `=1` | gates one `[world-frame] agree` line per projected conversion in `DatLiveEntityProjectionMaterializer`, recording the world-frame center both `LiveWorldOriginState` (App) and Runtime's physics-state owner used (issue #283, "measurement only; it never gates placement") | print-only | off | `PhysicsDiagnostics.ProbeWorldFrameEnabled` | -| `ACDREAM_SKY_PHASE_SECONDS` | `=` (any finite value; negative accepted, taken mod 1 per axis) | Campaign V slice V7 instrument-determinism pin: freezes the sky's cloud-sheet UV scroll to a fixed elapsed-seconds value instead of monotonic real elapsed time, so two launches of a differential/offline gate agree about cloud position. | **Non-obvious dual effect**: this ONE var pins TWO independently-designed clocks that happen to share a name-adjacent purpose — the sky renderer's cloud scroll (`SkyRenderer.AnimationPhaseSecondsOverride`) AND, since Campaign VM slice VM6, the atmospheric post-process graph's foliage-wind clock (`_windClockSecondsOverride`). A gate that only knows about "sky clouds" and sets this to freeze them will *also* freeze foliage-wind evolution — deliberately snapped-to-target on the first advance per an A6 review fix, but still a second surface a naive reader wouldn't expect this var to touch. Distinct from `ACDREAM_DAY_GROUP`/`ACDREAM_WORLD_TIME`, which pin the OTHER sky clock (day group/sun angle) — retail's clouds drift independently of the calendar date by design. | `null` → monotonic elapsed time (every ordinary run) | `RuntimeOptions.SkyAnimationPhaseSeconds` → `SkyRenderer.AnimationPhaseSecondsOverride` (cloud/rain UV scroll) **and** `AtmosphericPostProcessGraph`'s foliage-wind clock | +| `ACDREAM_SKY_PHASE_SECONDS` | `=` (any finite value; negative accepted, taken mod 1 per axis) | Campaign V slice V7 instrument-determinism pin: freezes the sky's cloud-sheet UV scroll to a fixed elapsed-seconds value instead of wall-clock time, so two launches of a differential/offline gate agree about cloud position. | **Non-obvious dual effect**: this ONE var pins TWO independently-designed clocks that happen to share a name-adjacent purpose — the sky renderer's cloud scroll (`SkyRenderer.AnimationPhaseSecondsOverride`) AND, since Campaign VM slice VM6, the atmospheric post-process graph's foliage-wind clock (`_windClockSecondsOverride`). A gate that only knows about "sky clouds" and sets this to freeze them will *also* freeze foliage-wind evolution — deliberately snapped-to-target on the first advance per an A6 review fix, but still a second surface a naive reader wouldn't expect this var to touch. Distinct from `ACDREAM_DAY_GROUP`/`ACDREAM_WORLD_TIME`, which pin the OTHER sky clock (day group/sun angle) — retail's clouds drift independently of the calendar date by design. | `null` → wall-clock driven (every ordinary run) | `RuntimeOptions.SkyAnimationPhaseSeconds` → `SkyRenderer.cs:79,85` (cloud UV scroll) **and** `AtmosphericPostProcessGraph.cs:560,586,671` (foliage-wind clock) | | `ACDREAM_STREAM_WORK_COMPLETIONS` | `=` (`>0`, else default) | Per-frame ceiling on streaming completion admissions on the update thread | Class doc comment states explicitly: this whole `ACDREAM_STREAM_WORK_*` family "exists for A/B measurement only" — not a user/production setting. Directly changes streaming throughput per frame; do not compare a measurement taken with this set against a default run. | 64 | `StreamingWorkBudgetOptions.Parse` (`StreamingWorkBudgetOptions.cs:56-58`) | | `ACDREAM_STREAM_WORK_CPU_MIB` | `=` (`>0`, else default) | Per-frame ceiling on adopted (newly resident) CPU bytes on the update thread | A/B-measurement-only family; changes per-frame CPU admission budget | 8 MiB | `StreamingWorkBudgetOptions.Parse` (`:59-61`) | | `ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT` | `=`, exclusive `0 < x < 100`, else default; stored as fraction (`percent/100`) | Fraction of the per-frame work budget reserved for the active reveal destination lane vs. background streaming | A/B-measurement-only family; reallocates frame budget between destination-lane and background streaming work, changing reveal-latency characteristics | 0.75 (75%) | `StreamingWorkBudgetOptions.Parse`/`ParseReservePercent` (`:71-73,154-169`) | diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index 903f5875..6947bfae 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -503,12 +503,6 @@ elapsed seconds that `TexVelocityX/Y` accumulate against with a fixed value. **Unset — the default, and every ordinary run — keeps the wall clock**, so nothing the user or the offline gate sees changes unless a gate asks. -**2026-08-28 follow-up:** the ordinary sky-animation source is now a monotonic -`Stopwatch` clock, matching retail's accumulated timer deltas and preventing an -OS time correction from jumping rain/cloud UVs. The V7 pin and its gate -semantics are unchanged; the wall-clock wording above describes the original -V7 implementation. - It exists because `ACDREAM_DAY_GROUP` and the `AcdreamCycleTimeOfDay` override pin only the *other* sky clock: the Dereth date, which chooses the day group, the keyframe and the sun angle. The cloud sheet does not read that clock at all @@ -2597,10 +2591,6 @@ Holtburg pair went from 23,090 differing pixels to 1,211. **The alternative was `-MaskTopPixels`, which would have permanently blinded the campaign's strictest instrument to the entire sky.** See §5.1. -The 2026-08-28 rain-timing parity follow-up replaced that adjustable wall clock -with monotonic `Stopwatch` elapsed time; this historical V7 measurement and the -diagnostic pin remain otherwise unchanged. - **3. The world clock was never pinned at all, and that was most of the number** (`1f25a609`). The route opened by pressing `AcdreamCycleTimeOfDay` three times. The mechanism underneath is `WorldTimeService.SetDebugTime`, and diff --git a/docs/research/2026-08-09-chat-retail-command-registry.md b/docs/research/2026-08-09-chat-retail-command-registry.md index 19d838e5..84d2450a 100644 --- a/docs/research/2026-08-09-chat-retail-command-registry.md +++ b/docs/research/2026-08-09-chat-retail-command-registry.md @@ -13,12 +13,6 @@ > channel ids) remains accurate and is still the authority. Only the columns > describing what acdream does are out of date; verify against > `ChatInputParser.cs` before trusting them. -> -> **CORRECTION 2026-08-28 — issue #360 is complete.** The full local -> `@allegiance`/`@all`, `@house`/`@hou`, and standalone `@motd` dispatcher -> families are implemented with the named-retail grammar and exact GameAction -> wire layouts in both graphical and headless hosts. The older MISSING status -> cells in §2.5/§2.5b and the priority list are historical. Date: 2026-08-09 diff --git a/docs/research/2026-08-17-map-house-recon.md b/docs/research/2026-08-17-map-house-recon.md index 81c007e5..008dfca3 100644 --- a/docs/research/2026-08-17-map-house-recon.md +++ b/docs/research/2026-08-17-map-house-recon.md @@ -199,27 +199,6 @@ parsed at `CreateObject.cs:611-619`) — reused rather than re-defined. `RuntimeHouseState` raw-field owner), and file the seven line builders as an ISSUES entry rather than porting them tonight. -### 2026-08-28 owned-house closeout - -Issue #413 completed this deliberately deferred builder chain. Direct byte -disassembly of the PDB-paired `acclient.exe` recovered the literals and the -x87-obscured branches: - -- buy and rent rows use `HousePaymentList::ComposeText`/`ComposeText2` with - retail's singular/plural fallback (`s`, or `es` after lowercase `s`/`x`); -- normal houses use 2,592,000-second periods and apartments use 7,776,000; - paid/maintenance-free rent advances the next-due row by two periods; -- location is `Location: %.1f%s, %.1f%s`, Y/S-N first and X/W-E second; - apartments emit no location; -- paid and unpaid warnings select `HousePanelTextColor` indices 1 and 2 - through the row template's authored font-color palette; -- 0x0227 installs the new rent time and clears paid counts, while 0x0228 - replaces the rent list; both rebuild the whole display just as retail does. - -`RuntimeHouseStateTests`, `LiveSessionEventRouterTests`, and -`MapHousePanelControllerTests` cover the full synthetic wire/state/UI path. -The canonical Release gate passes 16,315/16,315 on 2026-08-28. - ## Wire — GameEventType already has all four ids (corrects the handoff) The handoff claimed "0x0227/0x0228 absent from the enum". Checked diff --git a/docs/research/2026-08-28-issue178-cell-shell-culling.md b/docs/research/2026-08-28-issue178-cell-shell-culling.md deleted file mode 100644 index bc205435..00000000 --- a/docs/research/2026-08-28-issue178-cell-shell-culling.md +++ /dev/null @@ -1,68 +0,0 @@ -# Issue #178 — retail EnvCell shell culling (2026-08-28) - -## Outcome - -The Phase A8 `CullMode.Landblock -> CullMode.None` workaround is removed -from both production EnvCell draw paths. Constructed cell-shell batches now -use retail's clockwise cull state instead of drawing every ordinary shell -face twice. No PAK rebuild is required: the package already contains the -authored `sides_type` and the correctly expanded index geometry; this change -selects the correct GPU state when that geometry is drawn. - -The source and installed-DAT gates are complete. The owner visual gate passed -2026-08-28 (“Ok looks good”) at the requested interior matrix; #178 is closed. - -## Retail oracle - -The misleading detail is that `DatReaderWriter.Enums.CullMode` is used for -the CellStruct polygon's `sides_type`; its values are not direct GPU cull -states. - -- `D3DPolyRender::ConstructMesh @ 0x0059DFA0` expands `sides_type` 0 as the - positive face, type 1 as that face plus a reversed copy, and type 2 as the - positive and negative surfaces. Its polygon fan is `[0, i-1, i]`; the - reversed copy is `[i, i-1, 0]`. -- `D3DPolyRender::RenderMeshSubset @ 0x0059CA10` draws the constructed mesh - with `D3DCULL_CW` on the ordinary path. -- `RenderDeviceD3D::DrawEnvCell @ 0x0059F170` uses that constructed-mesh - route. The immediate-mode exception for a type-1 polygon does not apply to - the EnvCell mesh. - -`MeshExtractor.PrepareCellStructMeshData` already reproduces retail's fan and -the required reversed geometry. The render policy therefore must be -clockwise culling for every constructed shell batch; mapping DAT value 0 to -`None` was the obsolete workaround, while mapping it to the generic -`Landblock` render state would cull the opposite side. - -## Installed-DAT catalog - -`A8CellAudit cell-winding-catalog` scanned the installed DATs: - -- 772 environments and 3,168 CellStructs -- 38,189 polygons and 70,091 generated fan triangles -- 37,843 `Landblock(0)` polygons and 346 `None(1)` polygons -- no unknown or unsupported `sides_type` values -- no missing polygon vertices - -Vertex-normal orientation was recorded as a diagnostic, not treated as a -contract: CellStruct vertex normals may be smoothed rather than geometric, -and retail submits the identical authored fan. - -## Implementation and gates - -- `EnvCellRenderer.ResolveRetailCellShellCullMode` documents and enforces the - constructed-mesh policy in both the main and shadow-receiver draw paths. -- Hermetic extraction tests pin the exact type-0 fan and type-1 reversed-face - expansion. -- Renderer tests pin all four source enum values to the retail clockwise - constructed-mesh state. -- Installed-DAT audit: passed. -- Canonical Release gate: 16,321 passed, 0 skipped, 0 failed across 14 test - assemblies; Release build completed with 0 warnings and 0 errors. - -## Owner visual acceptance - -In Holtburg buildings and the Facility Hub, rotate the camera through walls, -floors, ceilings, ramps, and stairs from their ordinary playable sides. -Nothing should vanish at any camera angle. Acceptance of that matrix closes -#178. The owner accepted this matrix on 2026-08-28 (“Ok looks good”). diff --git a/docs/research/2026-08-28-open-issue-validity-audit.md b/docs/research/2026-08-28-open-issue-validity-audit.md deleted file mode 100644 index 4957f732..00000000 --- a/docs/research/2026-08-28-open-issue-validity-audit.md +++ /dev/null @@ -1,141 +0,0 @@ -# Open-issue validity audit — 2026-08-28 - -## Scope and verdicts - -This is a read-only product audit of every non-final entry in -`docs/ISSUES.md`. It does not fix product code and it does not close or -reprioritize anything. The ledger contains **88** such entries, not 87: -`#341` still says `OPEN` on its status line even though its heading and its -own evidence say it closed with a 10/10 gate. - -- **CONFIRMED CURRENT** means the current source, a current missing path, or - repeated recorded evidence supports the material claim. This bucket also - identifies whether the item is a product defect, missing feature, - maintenance/performance debt, or test-infrastructure flake. -- **GHOST / NOT A CURRENT CLIENT ISSUE** means the entry is fixed, stale, - superseded, based on a deleted path, an accepted decision, an external - system/environment condition, or is a research/test-plan note misfiled as a - client issue. “Ghost” does not mean the original report was fabricated. -- **NEEDS VERIFICATION** means there was plausible historical evidence, but - current source inspection cannot prove the user-visible symptom still - exists. These should not be called either fixed or real until a focused - current-binary gate reproduces or clears them. - -The audit checked the current source and tests, the evidence/status text in -the ledger, superseding issue/campaign records, and a clean current Release -build. The build completed with **0 warnings and 0 errors**. - -## Confirmed current — 35 - -| ID | Type | Verification basis | -|---:|---|---| -| #178 | Closed after audit (owner-accepted 2026-08-28) | Named-retail confirms CellStruct `sides_type` controls constructed geometry and every constructed EnvCell subset uses clockwise culling. Both double-sided overrides are removed, the installed-DAT catalog found no invalid side types or missing vertices across 38,189 polygons, winding/policy tests plus the 16,321-test Release gate passed, and the owner accepted the Holtburg/Facility-Hub visual matrix. | -| #241 | Fixed after audit (2026-08-28) | The always-on parity partition now rejects off-frustum landblocks before walking entities and preserves the player's current landblock, using the prepared frame frustum already available at the call site. | -| #258 | Owner-voided 2026-08-28 | The former ImGui host is deleted, but the owner classified a replacement developer-panel host as undesired scope and closed the entry. | -| #261 | Telemetry defect | Production `LinkStatusSnapshot` still receives no packet-loss calculation, so `PacketLossPercentage` remains its default. | -| #310 | Fixed after audit (2026-08-28) | Collision-prefix retirement now supersedes an exact authored mover still awaiting first preparation, then completes through the normal withdrawal handshake; the former indefinite-poll regression converges. | -| #311 | Fixed after audit (2026-08-28) | `RetryPendingProjections` now uses retained, depth-safe scratch lists; the warmed fixture drops from 424 B to the event stream's 72 B publication floor and re-entry is regression-tested. | -| #313 | Fixed after audit (2026-08-28) | Successful split-to-world recovery now selects the resulting GUID through the canonical `SelectionState`; unrelated unknown spawns cannot steal selection. | -| #316 | Fixed after audit (2026-08-28) | The player-only `AirborneSnap` shadow skip is gone; every accepted landing publishes the resolved collision pose and the 27-row routing matrix passes. | -| #320 | Fixed after audit (2026-08-28; stale ledger entry) | The Runtime physics cutover already committed every ordinary transition's exact cell and rebucketed from `CellCommitted`; the added local-player test proves source-landblock retirement cannot park a player who walked across the boundary. | -| #322 | Fixed after audit (2026-08-28) | Both production callers now consume one pure disposition/HasAnims pre-placement derivation; the end-to-end application matrix and explicit truth table pass. | -| #324 | Architecture debt | Graphical and no-window hosts still own parallel inbound entity-routing composition. Accurate structural debt; no current symptom is proven. | -| #325 | Fixed after audit (2026-08-28) | Gate A and Runtime authority now accept equal-or-newer teleport stamps without consuming TELEPORT_TS; wrap, stale-pair, velocity, hook, and acknowledgement tests pass. | -| #330 | Headless capability defect | The headless composition still registers no live-entity collision owner, so bots cannot collide with dynamic creatures/objects. | -| #332 | Headless capability gap | Headless composition still lacks the graphical host's remote dead-reckoning path. Whether this is required product scope remains a decision. | -| #340 | Fixed before audit; stale ledger entry | `dfc841b7` injected a deterministic meter clock and the fixture uses a constant timestamp, removing suite load from the policy contract. | -| #346 | Fixed before audit; stale ledger entry | `dfc841b7` added tiered-JIT warmup plus five-batch sampling while retaining a threshold far below the former linear allocation regression. | -| #359 | Fixed after audit (2026-08-28) | The live route now passes the canonical player GUID and `ChatLog.OnPlayerKilled` suppresses victim/killer recipients exactly like retail; regression tests cover victim, killer, and bystander. | -| #360 | Fixed after audit (2026-08-28) | One shared Runtime dispatcher now implements the complete named-retail allegiance, house, and standalone `@motd` grammar for graphical and headless hosts; all GameAction payloads are byte-verified and the 16,309-test Release gate passes. | -| #361 | Fixed after audit (2026-08-28) | `@log` has a reconnect-safe file lifecycle; `@day` now toggles persistent noon landscape lighting; `@render radius/fov` implements the named-retail parsing, bounds, replies, and persisted renderer settings. | -| #370 | Headless movement defect | The released-jump probe reproduced 3/3 after the threading hypothesis was eliminated. No later fix is recorded. | -| #393 | Void/closed after audit (2026-08-28) | High-resolution DAT use is already implemented in both live and pak paths. The alleged separate retail toggle was a research misread; the remaining old texture-level degradation is explicitly unwanted by owner direction. | -| #400 | Fixed after audit (2026-08-28) | The exact authored two-root `gmCreditsUI` flow is mounted: 2,345 localized fragments, seven cyclic pictures, retail timing/shared scroll, Please Wait exit, and return to character management. Installed-DAT and controller gates pass. | -| #401 | Fixed after audit (2026-08-28) | Retained UI is now default-on across all launch paths, literal `ACDREAM_RETAIL_UI=0` opts out, the session-config force is removed, and authoritative launch docs/tests are updated. | -| #402 | Fixed before audit; stale ledger entry | `dfc841b7` moved the contract to a named dedicated thread with bounded start/block/join and captured worker failure, eliminating the fragile scheduling observation. | -| #403 | Fixed after audit (2026-08-28) | The live presenter now delegates advance/wrap and no-sequence interpolation to `RetailAnimationCyclePlayback`; Core and presenter regression suites pass. | -| #404 | Fixed after audit (2026-08-28) | Resolver now consumes `Runtime.CharacterCreation.Options`; the second raw SkillTable read and lock are gone, with focused, synthetic-projection, and installed-DAT gates passing. | -| #408 | Fixed after audit (2026-08-28) | The shared importer now applies DAT `0x3B` client-wide; all stateful retained widget types apply named/DirectState visibility, the scoped chargen path is deleted, and an installed-DAT sweep proves 990/990 built authored-invisible widgets across 38 layouts start hidden. | -| #410 | Fixed after audit (2026-08-28) | Both justification axes now share retail's exact 1/3/5 table, unauthored text defaults Left/Top, merge uses property presence, and automated plus installed-DAT UI suites pass. | -| #413 | Fixed after audit (2026-08-28) | All owned-house builders now match the recovered retail strings/math/colors; 0x0227/0x0228 refresh the shared Runtime owner, synthetic wire/state/UI coverage passes, and the 16,315-test Release gate is green. | -| #421 | Performance debt | The directional-shadow renderer still owns and uploads `DirectionalShadowTransformBufferSet` separately from the main instance SSBO. | -| #422 | Intermittent native crash | Heap corruption at graceful exit has multiple independent sightings, including a connected run. It lacks a stack, not evidence. | -| #423 | Rendering-policy mismatch | The atmospheric pack still declares and evaluates raw `ActiveDayGroupMultiplier` values instead of a `WeatherKind`-keyed policy. | -| #428 | Fixed after audit (2026-08-28) | Installed DATs proved the scripted sky carriers have one identity-transformed part 0; publishing that exact synthetic part lets legitimate particle hooks resolve, while the day-group-flip regression proves teardown prevents later dispatch. | -| #438 | Missing diagnostic feature | Launcher crash bundles are genuinely not implemented. This is an approved enhancement, not an existing launcher malfunction. | -| #442 | Fixed before audit; stale ledger entry | `d123c4b6` moved the warmed dense path to the shared 64-frame zero-allocation measurement probe; the strict zero allocation contract remains. | - -## Ghost / not a current client issue — 33 - -| ID | Why it should not remain framed as a current client defect | -|---:|---| -| #3 | Fixed: periodic `TimeSync` is parsed and routed into the world clock. | -| #73 | A process policy for future string sweeps, not a concrete defect; its own text says no infrastructure work remains. | -| #180 | Both camera-collision fixes shipped and were log-verified; the residual visual was moved to now-closed #181. | -| #194 | Fixed: `WbDrawDispatcher.BeginFrame` prunes old instance groups and has coverage. | -| #195 | Obsolete architecture: the duplicate ChatVM/provider shape described by the issue no longer exists; local commands have dedicated routing. | -| #199 | Fixed by the Campaign CA server-authoritative one-request-in-flight raise flow; optimistic local mutation was removed. | -| #200 | Stale migration list: the old inline mounts/MockupDesktop path named by the issue no longer describes current composition. | -| #212 | Implementation and regression coverage are present; the status remained `IN-PROGRESS` only for an old user gate. | -| #213 | Fixed: client commands are intercepted by `ClientCommandController` instead of being sent to ACE as chat. | -| #228 | Directly disproved by the current Release build: 0 warnings, not 17. | -| #242 | Fixed: static presentation orders/prepares snapshots once and reuses the prepared replacement rather than rebuilding a third dictionary per attempt. | -| #249 | Obsolete: it targets the deleted OpenGL/bindless backend; production is Vulkan-only. | -| #251 | Obsolete: it targets deleted `glClientWaitSync`/OpenGL fence code. | -| #256 | Superseded by #260; its discriminator found no missing-object drift and the transport-loss mechanism was fixed elsewhere. | -| #257 | Superseded/refuted as a leak: its own portal-churn discriminator was flat/negative and later PAK/runtime work replaced the measured architecture. | -| #259 | Explicitly a machine-wide Vulkan/environment failure, not an acdream product defect. | -| #274 | A request for a connected retail comparison, not an observed failure. It belongs in a gate/research checklist. | -| #309 | Explicit owner-accepted divergence recorded in the divergence register, with no planned fix. | -| #318 | A test-plan/composition-coverage residual, not evidence of a product defect. Track as test debt if still desired. | -| #339 | Fixed and live-validated; current mesh-publication guards are present. The status header was never finalized. | -| #341 | Internally contradictory ghost: heading/evidence say closed and 10/10 bit-identical, but the status line still says `OPEN`. | -| #342 | Fixed: the current assertion compares old-model and new-model values rather than the same expression to itself. | -| #343 | Fixed; the issue body already records the guarded native-release lifecycle correction. | -| #344 | Fixed; the issue body already records the teleport-authority discriminator and clean suite. | -| #350 | Fixed in current source: the lifetime render-shadow counters are widened to `long`. | -| #352 | The behavior was live-gate verified; the only remaining request is one extra discriminating unit test. This is test debt, not a vendor bug. | -| #366 | Fixed: the chat controller now owns the unread indicator, unseen-text state, tick/click behavior, and tests. | -| #369 | An unanswered retail research question with no demonstrated mismatch, so it does not belong as a product issue. | -| #383 | Installed-DAT/committed-fixture provenance drift is environment/fixture-maintenance work, not a current client defect. | -| #384 | The client sends the swear action; the missing response is an ACE server behavior/blocker, not an acdream client bug. | -| #396 | Fixed and live-verified crash-free; only a visual re-check of the already-mounted instruction dialog remained. | -| #425 | Fixed; both resolution-scaled pack budgets and activation memo behavior are recorded in the issue itself. | -| #427 | Fixed and owner-reported; the sky/fog seam correction is already recorded in the issue itself. | - -## Needs current verification — 20 - -| ID | What is known and what is still required | -|---:|---| -| #2 | The old lightning presentation mismatch was plausible, but the sky/PES pipeline changed substantially. Reproduce side-by-side on the current binary. | -| #29 | The thin-cloud observation has no current post-renderer visual gate. A present-day retail comparison is required. | -| #55 | The 1.45M `meshMissing` figure belongs to an old streaming diagnostic. Re-measure current PAK-v2 production before treating it as real. | -| #130 | Closed as owner-accepted residual (2026-08-28) | The connected visual re-gate confirmed the thin top-edge strip remains; owner direction is to leave it as-is. | -| #177 | Strong historical dungeon evidence exists, but the renderer/portal pipeline changed afterward. Repeat the named stair routes on the current binary. | -| #183 | Owner-closed 2026-08-28 after the validity audit. | -| #250 | The zero-allocation flakes were real historically, but the stated roughly-one-in-three frequency predates major runtime/test changes. Run a current parallel stress lane. | -| #262 | Owner reports the first-login movement issue solved; closed 2026-08-28. | -| #265 | Closed during follow-through: Campaign P's final user matrix already accepted downhill bounce, flat pop, and uphill landing on 2026-07-31. | -| #267 | Closed during follow-through: Campaign P's final user matrix already accepted live vitae/buff values and immediate skill-row refresh on 2026-07-31. | -| #317 | The call still exists, but “has no retail basis” is a research conclusion, not proof that current behavior is wrong or reachable. | -| #321 | One old full-suite sound-cache failure is not enough to establish a current flake after later suite/runtime changes. Stress the exact class in parallel. | -| #323 | The stale-receipt mechanism is plausible, but production reachability and a user-visible symptom were never established. | -| #377 | The fullscreen crash was once deterministic but is explicitly not reproducible on current display code. A current cold-start matrix decides it. | -| #397 | Closed during follow-through: the real Windows supervisor stopped a connected Release headless host gracefully, and a separately supervised process re-entered the same account after the documented 2.5-second ACE account-release quiescence; both exited code 0. | -| #431 | CA2–CA4 implemented the missing inbound/recompute flow and the first CA5 drive passed Quickness/run updates. The original title is no longer proven; remaining CA5 cases need live gating. | -| #433 | Owner-observed stale entities are credible, but the issue is intermittent and has no current captured reproduction. | -| #439 | One full-suite failure followed by an isolated pass and clean rerun is only a flake candidate. Run the established timing lane before confirming it. | -| #441 | Owner-closed 2026-08-28 after two old observations were followed by a healthy probed baseline and no recurrence. | -| #452 | Closed during follow-through: both fixed `app-release24` sessions have retained logs proving graceful logout and orderly teardown after the 100-switch/30-minute stress. | - -## Recommended ledger cleanup order - -1. Close or archive the 33 ghost entries after owner review. -2. Keep type labels on confirmed items so - enhancements, maintenance debt, and flaky tests are not mistaken for - gameplay defects. -3. Run one focused verification batch for the 20 uncertain entries. Close a - fixed-awaiting-gate item when its current gate passes; close an old symptom - as stale when its documented current reproduction route no longer fails. -4. Prioritize only the confirmed product defects after the ledger is clean; - do not mix them with refactors, test debt, or external ACE/environment work. diff --git a/docs/research/2026-08-28-owner-closed-issue-validity-audit.md b/docs/research/2026-08-28-owner-closed-issue-validity-audit.md deleted file mode 100644 index a41a995d..00000000 --- a/docs/research/2026-08-28-owner-closed-issue-validity-audit.md +++ /dev/null @@ -1,108 +0,0 @@ -# Owner-closed issue validity audit — 2026-08-28 - -## Scope and verdicts - -This is a read-only companion to the -[open-entry validity audit](2026-08-28-open-issue-validity-audit.md). It checks -the exact **57 unique issue IDs** in the owner's 2026-08-28 closure request. -Fifty-six entries were newly marked closed; `#167` was already `DONE` before -that request. This audit changes no issue status and fixes no product code. - -- **OWNER-CONFIRMED CLOSED DESPITE REMAINING EVIDENCE** means current source or - still-applicable recorded evidence supports unresolved work, but the owner - explicitly chose on 2026-08-28 to keep the entry closed. This records a - scope/status decision, not technical proof that the symptom is fixed. -- **VERIFY BEFORE KEEPING CLOSED** means the historical report was credible, - but later architectural changes or an implemented fix make a current live - gate necessary before deciding whether the closure is correct. -- **SAFE TO REMAIN CLOSED** means current code/tests and later project history - show the issue fixed, superseded, disproved, owner-accepted, or not a valid - product defect in the first place. - -The audit inspected current source and focused tests, each issue's complete -evidence trail, later campaign/user-gate records, and the current architecture. -The supporting focused Release tests completed with **321 passed, 1 skipped, -0 failed**. The companion open-entry audit's clean Release build completed with -**0 warnings and 0 errors**. - -## Owner-confirmed closed despite remaining evidence — 3 - -| ID | Remaining technical evidence | -|---:|---| -| #64 | The local pickup transaction sends/accepts the pickup, but the current local-player animation path still has no pickup one-shot trigger and no admitted server-motion equivalent. The original missing local bend/grab animation remains an unimplemented path. | -| #235 | The live animation presenter still publishes part poses at admitted object-clock frames. Root presentation is smoothed, but no render-time interpolation exists between animated part poses, which is the exact capped/RDP cadence alias described by the issue. | -| #263 | The Drudge Scrying Orb residual was user-confirmed after the general translucency fix and explicitly deferred. No later orb-specific ClipMap/order fix or clearing visual gate exists. | - -## Verify before keeping closed — 11 - -| ID | What is known and what should be gated | -|---:|---| -| #41 | The remote-root-motion correction was implemented, but the entry never received its final two-client sub-decimeter-blip visual gate. Observe another player starting/stopping/turning on the current binary. | -| #94 | The original held-light report became testable only after child parenting landed. Current lighting distinguishes authored point and spot lights, but there is no post-parenting side-by-side reproduction or clearance of the wall spotlight symptom. | -| #115 | The cramped-interior camera feel report was credible, but camera collision, viewer stepping, display pacing, and the renderer changed substantially afterward. Repeat the same pressed-wall turns against retail. | -| #129 | The long-distance door/doorway leak fix shipped, but the entry retained an explicit distant-view visual gate. Revisit the named over-one-landblock views before treating it as conclusively closed. | -| #165 | Remote wall penetration was observed, while the leading physics explanations were later ruled out and the remaining presentation hypothesis was never proven. A current observer/client reproduction decides it. | -| #181 | The Facility Hub artifact survived its original seven-fix ladder, but its early attribution was partly retracted and the camera/render pipeline later changed. Repeat the exact pressed-camera pose without capture overlays. | -| #208 | The peaceful-login combat-bar correction is present, but the issue's final fresh-login visual check was never recorded. Gate a fresh peaceful login and one relog. | -| #225 | The particle/translucent-object ordering implementation and stress tests pass, but the issue explicitly retained its final lifestone/candle visual matrix. Run that matrix; `#263` remains a separate orb-specific residual. | -| #337 | The Neftet rock behavior is covered by the `#333` broadphase fix, but the exact rock-plateau live acceptance was never recorded. Re-test that fixture before relying only on the general mechanism. | -| #409 | The client-wide tooltip system is code-complete and one live path was proven, but the remaining connected hover paths in its acceptance matrix were still owed. Complete that small UI gate. | -| #418 | The original 27 s/32-blocks-per-second limiter was fixed and reduced dramatically, so the title is stale. Cold-login readiness was still above the retail-feel target at the last measured checkpoint, and later PAK/runtime work was not re-measured with the same probe; run one current cold-login timing gate. | - -## Safe to remain closed — 43 - -| ID | Why the closure is safe | -|---:|---| -| #102 | Fixed: `PortalVisibilityBuilder` now uses retail's in-place `update_count` watermark/fixpoint propagation; the old per-cell cap is gone and focused convergence tests pass. | -| #104 | Fixed: VFX instances carry owner-cell identity and `ParticleVisibilityController` gates them against the current PView visible-cell set. | -| #114 | Superseded by the current indoor renderer: the old `gl_ClipDistance` shell-chop mechanism named by the issue was deliberately deleted in favor of whole-shell depth/order handling. | -| #116 | The second divergence was fixed; the remaining first shape was traced to a simplified fixture while the real Path-5/Path-6 behavior is retail-exact. No confirmed engine defect remains. | -| #120 | Fixed: the reciprocal/in-place convergence case is pinned by the current portal-visibility replay tests. | -| #121 | Fixed and superseded by later portal campaigns/gates; world-portal VFX are routed through the current scene-particle path rather than the broken visibility path described here. | -| #139 | Fixed: retained buttons and menus now implement the authored normal, rollover, pressed, highlight, and open-state transitions with regression coverage. | -| #144 | Not an established defect: the empty-slot release is a harmless no-op and the entry never established that retail suppresses the click notification differently. | -| #146 | Non-actionable polish list: the capacity-bar visuals were already accepted and the entry asks for unspecified additional polish rather than describing a remaining defect. | -| #148 | Fixed: the authored backpack button toggles the inventory panel and projects open/closed state back to Highlight/Normal, with tests. | -| #151 | Fixed for the reported defect: the Arwic/city wall collision was user-verified. The residual terrain note was a question, not a demonstrated mismatch. | -| #158 | Not a bug by its own description; it is an old, vague character-window polish list against presentation paths that were subsequently replaced. | -| #166 | Fixed and user-gated as part of the closed Campaign P landing-momentum/bounce family. | -| #167 | Already `DONE` before the cleanup request; the retail leash behavior and constants were ported and covered. | -| #172 | Fixed and covered by the user-accepted Campaign P portal-platform/step-up gate. | -| #173 | Fixed and covered by the user-accepted Campaign P remote ceiling-response gate. | -| #174 | Fixed: door-use animation-link queue handling shipped and the later physics/door gate was accepted. | -| #175 | Fixed: door collision now follows the closed motion-table pose; the later physics/door gate was accepted. | -| #176 | The original purple flashing/lighting defect was fixed and verified. Its distinct pressed-camera residual was split into `#181`, so keeping this parent closed is correct. | -| #182 | Fixed by the retail collision-velocity rebuild and covered by the accepted crowded-movement Campaign P matrix. | -| #191 | Fixed by the root-motion presentation path; current coverage asserts that a brief forward edge cannot glide when the animation contributes no root delta. | -| #193 | Fixed and measurement-verified: the retained-resource/OOM correction survived the extended-play resource gates. | -| #202 | Fixed: the current `WeenieErrorMessages` table and routing replace the old missing lookup and cover interpolation plus `UseDone` behavior. | -| #209 | Fixed and subsequently live-gated: the jump/combat power-bar modes and captions were accepted in the later keyboard/powerbar rounds. | -| #226 | Fixed and connected-visual verified: building/EnvCell detail-texture overlay is present in the modern renderer. | -| #253 | Superseded/disproved by later byte-exact retail-DAT research: the shared row template authors a flush-left 20×20 icon cell, and the current sprite draw preserves authored texture alpha. The earlier guessed centering offset was not retail ground truth. | -| #264 | Research note, not a confirmed client defect: it explicitly records no known `WATER_CONTACT_TS` divergence and asks for xref/swim investigation. Any discovered mismatch should be filed as a concrete new issue. | -| #273 | Fixed: the exact captured Holtburg gap fixture now blocks traversal, requires both real geometry participants, and matches graph/flat collision paths in focused tests. | -| #319 | Fixed and user-gated in placement Campaign C4 route 7: player-parented children receive canonical-cell propagation through the unified owner. | -| #333 | Fixed: the broadphase no longer rejects an off-centre BSP part by measuring reach only from the part origin; regression coverage pins the case. | -| #348 | Fixed: cursor objects are process-lifetime cached instead of being recreated on every alternation; later long connected sessions did not reproduce handle exhaustion. | -| #394 | Fixed: Configure Keyboard row captions use the authored serif DAT font, with current layout tests and later keyboard gates. | -| #395 | Fixed: key captions resolve through retail's DAT/localized key-name pipeline rather than raw enum spellings. | -| #414 | Fixed: logout teardown no longer leaves the cursor in raw-captured fly mode. | -| #415 | Fixed: deferred UI-probe wait verbs are bound independently of the optional artifact-directory setting. | -| #416 | Fixed: roster rollover state clears when the pointer leaves a row. | -| #417 | Fixed: ambience ownership is reset on world logout and does not continue/re-fire at character selection. | -| #419 | Fixed and explicitly owner-accepted: the portal rim/ring artifact disappeared after matching retail's view-plane exit animation. | -| #420 | Fixed: `UiButton` safely handles the character-select media-state case that previously produced the null-key crash. | -| #424 | Fixed: exclusive-fullscreen alt-tab no longer sends a zero-area extent into render-pack activation validation. | -| #426 | Fixed: NO_POS_UVS/solid-colour faces are extracted and rendered instead of being discarded as having no positive face. | -| #432 | Disproved as a production performance bug: the allocation/frame-time spike came from diagnostics being enabled, not the normal client path. | -| #443 | Fixed and owner-passed: paperdoll/private-viewport ownership now hydrates on first open without the intermittent missing/delayed doll. | - -## Recommended action - -1. Keep `#64`, `#235`, and `#263` closed per the owner's explicit post-audit - decision. If one becomes a priority later, reopen it or file a fresh issue - with a current reproduction. -2. Run one compact current-binary gate for the 11 verification items; keep each - closed if the named route passes, otherwise reopen it with the new evidence. -3. Leave the remaining 43 closed. Do not resurrect their stale titles merely - because their original status text once said `OPEN`. diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index dc94659c..bfb78468 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -11,7 +11,6 @@ using AcDream.Runtime; using AcDream.Runtime.Session; using AcDream.Core.Combat; using AcDream.Core.Lighting; -using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Selection; using AcDream.Core.World; @@ -394,10 +393,7 @@ internal sealed class FrameRootCompositionPhase live.EnvCellRenderer!, foundation.SceneLighting!, d.RenderRange, - skyPesFrame, - persistentDaylight: () => - d.Runtime.CharacterOwner.Options.GetOptionBit( - CharacterOptionId.PersistentAtDay)); + skyPesFrame); var worldRenderFrameBuilder = new WorldRenderFrameBuilder( new RuntimeWorldFrameCameraSource( host.CameraController, @@ -762,8 +758,7 @@ internal sealed class FrameRootCompositionPhase var framePreparation = new RenderFramePreparationController( renderFrameResources, devTools: null, - renderWeatherFrame, - live.PaperdollPresenter); + renderWeatherFrame); IRenderFramePostDiagnosticsPhase postDiagnostics = renderSceneShadowComparison is not null && lifecycleAutomation is not null diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index af8a6608..cb0899e0 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -27,6 +27,7 @@ using AcDream.UI.Abstractions.Input; using AcDream.UI.Abstractions.Panels.Chat; using AcDream.UI.Abstractions.Panels.Vitals; using DatReaderWriter; +using DatReaderWriter.DBObjs; using Silk.NET.Input; using Silk.NET.Windowing; @@ -715,12 +716,19 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory // outside the lock matches this file's existing pattern // elsewhere (construct once, lock only around Resolve calls). var characterCreationStrings = new DatStringResolver(d.Dats); - // #404: ChargenTableReader already loads the global SkillTable and - // projects its formulas into this immutable options model. Reuse - // that single source of truth; summary score reads are pure and - // need neither another DAT read nor another DatLock acquisition. - var chargenSkillScoreResolver = new ChargenSkillScoreResolver( - d.Runtime.CharacterCreation.Options); + // CC5 review fix round F3 (2026-08-16): read the global + // SkillTable (portal.dat 0x0E000004 — the SAME file + // ChargenOptions.GlobalSkillCostsBySkillId's own doc comment and + // LiveSessionRuntimeFactory.CreateCharacterBindings already read) + // ONCE at composition time, under the DatLock DatCollection's + // thread-safety contract requires — mirrors LiveSkillCreditResolver's + // own constructor-time load. The resolver itself does no further + // DAT access per call (pure SkillFormula arithmetic), so the + // Summary page's GetSkillScore binding below needs no lock. + SkillTable? chargenSkillTable; + lock (d.DatLock) + chargenSkillTable = d.Dats.Get(0x0E000004u); + var chargenSkillScoreResolver = new ChargenSkillScoreResolver(chargenSkillTable); // Campaign QT slice QT5: lazily loaded on first open (the panel // is hidden at mount), then held for the session. AcDream.Core.Quests.ContractCatalog? contractCatalog = null; @@ -1114,9 +1122,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory MapHouse: new MapHouseRuntimeBindings( CurrentCalendar: d.CurrentCalendar, PlayerCellId: () => d.PlayerController.Controller?.CellId ?? 0u, - HousePosition: () => d.Runtime.HouseOwner.Position, - HouseLines: () => d.Runtime.HouseOwner.Lines, - HousePanelLines: () => d.Runtime.HouseOwner.PanelLines), + HouseLines: () => d.Runtime.HouseOwner.Lines), // Campaign QT slice QT5. The catalog is read from the dats // ONCE and cached: it is immutable installed content, and the // panel would otherwise re-read a 322-entry table on every diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index 7ff381ab..da60f580 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -742,7 +742,6 @@ internal sealed class SessionPlayerCompositionPhase d.EntityObjects.Objects, live.LiveEntities, hydration, - d.Actions.Selection, () => d.UpdateClock.SimulationTimeSeconds); bindings.Adopt( "inventory world-drop projection", diff --git a/src/AcDream.App/Net/LiveSessionCommandRouter.cs b/src/AcDream.App/Net/LiveSessionCommandRouter.cs index 48c1dbb0..2a72e03a 100644 --- a/src/AcDream.App/Net/LiveSessionCommandRouter.cs +++ b/src/AcDream.App/Net/LiveSessionCommandRouter.cs @@ -383,8 +383,6 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting ToggleFrameRate: () => InvokeClient(static b => b.ToggleFrameRate()), ToggleUiLock: () => InvokeClient(static b => b.ToggleUiLock()), ShowSystemMessage: text => InvokeClient(b => b.ShowSystemMessage(text)), - ShowClientLocalMessage: text => - InvokeClient(b => b.ShowClientLocalMessage(text)), ShowWeenieError: error => InvokeClient(b => b.ShowWeenieError(error)), PlayerPublicWeenieBitfield: () => ReadClient(static b => b.PlayerPublicWeenieBitfield(), default(uint?)), @@ -445,91 +443,7 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting LeaveGmChannel: channelId => InvokeClient(b => b.LeaveGmChannel(channelId)), RecallAllegianceHometown: () => InvokeClient(static b => b.RecallAllegianceHometown()), RequestAllegianceInfo: name => InvokeClient(b => b.RequestAllegianceInfo(name)), - AbandonHouse: () => InvokeClient(static b => b.AbandonHouse()), - Administration: BuildGuardedAdministration(), - IsPersistentDaylight: () => - ReadClient(static b => b.IsPersistentDaylight(), false), - SetPersistentDaylight: value => - InvokeClient(b => b.SetPersistentDaylight(value)), - SetLandscapeRadius: radius => - InvokeClient(b => b.SetLandscapeRadius(radius)), - SetFieldOfView: degrees => - InvokeClient(b => b.SetFieldOfView(degrees))); - - private ClientCommandController.AdministrationBindings BuildGuardedAdministration() => - new( - BreakAllegianceBoot: (name, account) => - InvokeClient(b => b.Administration.BreakAllegianceBoot(name, account)), - AllegianceChatBoot: (name, reason) => - InvokeClient(b => b.Administration.AllegianceChatBoot(name, reason)), - AllegianceChatGag: (name, enabled) => - InvokeClient(b => b.Administration.AllegianceChatGag(name, enabled)), - AllegianceBroadcast: text => - InvokeClient(b => b.Administration.AllegianceBroadcast(text)), - ListAllegianceBans: () => - InvokeClient(static b => b.Administration.ListAllegianceBans()), - AddAllegianceBan: name => - InvokeClient(b => b.Administration.AddAllegianceBan(name)), - RemoveAllegianceBan: name => - InvokeClient(b => b.Administration.RemoveAllegianceBan(name)), - ListAllegianceOfficers: () => - InvokeClient(static b => b.Administration.ListAllegianceOfficers()), - ClearAllegianceOfficers: () => - InvokeClient(static b => b.Administration.ClearAllegianceOfficers()), - SetAllegianceOfficer: (name, level) => - InvokeClient(b => b.Administration.SetAllegianceOfficer(name, level)), - RemoveAllegianceOfficer: name => - InvokeClient(b => b.Administration.RemoveAllegianceOfficer(name)), - ListAllegianceOfficerTitles: () => - InvokeClient(static b => b.Administration.ListAllegianceOfficerTitles()), - ClearAllegianceOfficerTitles: () => - InvokeClient(static b => b.Administration.ClearAllegianceOfficerTitles()), - SetAllegianceOfficerTitle: (level, title) => - InvokeClient(b => b.Administration.SetAllegianceOfficerTitle(level, title)), - QueryAllegianceName: () => - InvokeClient(static b => b.Administration.QueryAllegianceName()), - SetAllegianceName: name => - InvokeClient(b => b.Administration.SetAllegianceName(name)), - ClearAllegianceName: () => - InvokeClient(static b => b.Administration.ClearAllegianceName()), - AllegianceLockAction: action => - InvokeClient(b => b.Administration.AllegianceLockAction(action)), - SetAllegianceApprovedVassal: name => - InvokeClient(b => b.Administration.SetAllegianceApprovedVassal(name)), - AllegianceHouseAction: action => - InvokeClient(b => b.Administration.AllegianceHouseAction(action)), - QueryMotd: () => - InvokeClient(static b => b.Administration.QueryMotd()), - SetMotd: motd => - InvokeClient(b => b.Administration.SetMotd(motd)), - ClearMotd: () => - InvokeClient(static b => b.Administration.ClearMotd()), - SetOpenHouseStatus: open => - InvokeClient(b => b.Administration.SetOpenHouseStatus(open)), - AddPermanentGuest: name => - InvokeClient(b => b.Administration.AddPermanentGuest(name)), - RemovePermanentGuest: name => - InvokeClient(b => b.Administration.RemovePermanentGuest(name)), - RemoveAllPermanentGuests: () => - InvokeClient(static b => b.Administration.RemoveAllPermanentGuests()), - ChangeStoragePermission: (name, enabled) => - InvokeClient(b => b.Administration.ChangeStoragePermission(name, enabled)), - AddAllStoragePermission: () => - InvokeClient(static b => b.Administration.AddAllStoragePermission()), - RemoveAllStoragePermission: () => - InvokeClient(static b => b.Administration.RemoveAllStoragePermission()), - RequestFullGuestList: () => - InvokeClient(static b => b.Administration.RequestFullGuestList()), - BootSpecificHouseGuest: name => - InvokeClient(b => b.Administration.BootSpecificHouseGuest(name)), - BootEveryone: () => - InvokeClient(static b => b.Administration.BootEveryone()), - SetHooksVisibility: visible => - InvokeClient(b => b.Administration.SetHooksVisibility(visible)), - ModifyAllegianceGuestPermission: enabled => - InvokeClient(b => b.Administration.ModifyAllegianceGuestPermission(enabled)), - ModifyAllegianceStoragePermission: enabled => - InvokeClient(b => b.Administration.ModifyAllegianceStoragePermission(enabled))); + AbandonHouse: () => InvokeClient(static b => b.AbandonHouse())); private bool InvokeClient(Action invoke) { diff --git a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs index 5607a521..ec26a68b 100644 --- a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs +++ b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs @@ -414,8 +414,7 @@ internal sealed class LiveSessionRuntimeFactory Allegiance: _domain.Runtime.AllegianceOwner, Trade: _domain.Runtime.TradeOwner, House: _domain.Runtime.HouseOwner, - Contracts: _domain.Runtime.ContractsOwner, - PlayerGuid: () => _player.Identity.ServerGuid)); + Contracts: _domain.Runtime.ContractsOwner)); return new GraphicalSessionEventRoute( route, _domain.Runtime, @@ -617,9 +616,6 @@ internal sealed class LiveSessionRuntimeFactory // unstarted — CH4/CH5 scope at the earliest, not CH2. ShowSystemMessage: text => _domain.Communication.Chat.OnSystemMessage(text, 0x00u), - ShowClientLocalMessage: - text => _domain.Communication.AddText( - text, RetailLogTextType.ClientLocal), // SHOULD-FIX 3 (docs/research/2026-08-09-ch2-review-findings.md): // route through the AddText chokepoint instead of the deleted // ChatLog.OnWeenieError, which hardcoded LogTextType 0x00 — @@ -732,66 +728,7 @@ internal sealed class LiveSessionRuntimeFactory LeaveGmChannel: session.SendOffChannel, RecallAllegianceHometown: session.SendRecallAllegianceHometown, RequestAllegianceInfo: session.SendAllegianceInfoRequest, - AbandonHouse: session.SendAbandonHouse, - Administration: new ClientCommandController.AdministrationBindings( - BreakAllegianceBoot: session.SendBreakAllegianceBoot, - AllegianceChatBoot: session.SendAllegianceChatBoot, - AllegianceChatGag: session.SendAllegianceChatGag, - AllegianceBroadcast: text => - session.SendChannel(0x02000000u, text), - ListAllegianceBans: session.SendListAllegianceBans, - AddAllegianceBan: session.SendAddAllegianceBan, - RemoveAllegianceBan: session.SendRemoveAllegianceBan, - ListAllegianceOfficers: session.SendListAllegianceOfficers, - ClearAllegianceOfficers: session.SendClearAllegianceOfficers, - SetAllegianceOfficer: session.SendSetAllegianceOfficer, - RemoveAllegianceOfficer: session.SendRemoveAllegianceOfficer, - ListAllegianceOfficerTitles: session.SendListAllegianceOfficerTitles, - ClearAllegianceOfficerTitles: session.SendClearAllegianceOfficerTitles, - SetAllegianceOfficerTitle: session.SendSetAllegianceOfficerTitle, - QueryAllegianceName: session.SendQueryAllegianceName, - SetAllegianceName: session.SendSetAllegianceName, - ClearAllegianceName: session.SendClearAllegianceName, - AllegianceLockAction: session.SendAllegianceLockAction, - SetAllegianceApprovedVassal: session.SendSetAllegianceApprovedVassal, - AllegianceHouseAction: session.SendAllegianceHouseAction, - QueryMotd: session.SendQueryMotd, - SetMotd: session.SendSetMotd, - ClearMotd: session.SendClearMotd, - SetOpenHouseStatus: session.SendSetOpenHouseStatus, - AddPermanentGuest: session.SendAddPermanentGuest, - RemovePermanentGuest: session.SendRemovePermanentGuest, - RemoveAllPermanentGuests: session.SendRemoveAllPermanentGuests, - ChangeStoragePermission: session.SendChangeStoragePermission, - AddAllStoragePermission: session.SendAddAllStoragePermission, - RemoveAllStoragePermission: session.SendRemoveAllStoragePermission, - RequestFullGuestList: session.SendRequestFullGuestList, - BootSpecificHouseGuest: session.SendBootSpecificHouseGuest, - BootEveryone: session.SendBootEveryone, - SetHooksVisibility: session.SendSetHooksVisibility, - ModifyAllegianceGuestPermission: - session.SendModifyAllegianceGuestPermission, - ModifyAllegianceStoragePermission: - session.SendModifyAllegianceStoragePermission), - IsPersistentDaylight: () => - _domain.Character.Options.GetOptionBit( - CharacterOptionId.PersistentAtDay), - SetPersistentDaylight: enabled => - SendSingleCharacterOption( - (uint)CharacterOptionId.PersistentAtDay, - enabled), - SetLandscapeRadius: radius => - _interaction.Settings.SaveDisplay( - _interaction.Settings.Display with - { - LandscapeDrawDistance = radius, - }), - SetFieldOfView: degrees => - _interaction.Settings.SaveDisplay( - _interaction.Settings.Display with - { - FieldOfView = degrees, - })), + AbandonHouse: session.SendAbandonHouse), _domain.Communication.Chat, _domain.Communication.TurbineChat, PlayerGuid: () => _player.Identity.ServerGuid, diff --git a/src/AcDream.App/Net/RetailSkillFormula.cs b/src/AcDream.App/Net/RetailSkillFormula.cs index 5b914d47..18aab4d5 100644 --- a/src/AcDream.App/Net/RetailSkillFormula.cs +++ b/src/AcDream.App/Net/RetailSkillFormula.cs @@ -19,35 +19,16 @@ internal static class RetailSkillFormula { ArgumentNullException.ThrowIfNull(formula); - return TryCalculate( - formula.AdditiveBonus, - formula.Attribute1Multiplier, - formula.Attribute2Multiplier, - formula.Divisor, - attribute1, - attribute2, - out result); - } - - private static bool TryCalculate( - int additiveBonus, - int attribute1Multiplier, - int attribute2Multiplier, - int divisorStorage, - uint attribute1, - uint attribute2, - out uint result) - { - uint divisor = unchecked((uint)divisorStorage); + uint divisor = unchecked((uint)formula.Divisor); if (divisor == 0u) { result = 0u; return false; } - uint x = unchecked((uint)attribute1Multiplier); - uint y = unchecked((uint)attribute2Multiplier); - uint w = unchecked((uint)additiveBonus); + uint x = unchecked((uint)formula.Attribute1Multiplier); + uint y = unchecked((uint)formula.Attribute2Multiplier); + uint w = unchecked((uint)formula.AdditiveBonus); uint numerator = unchecked(x * attribute1 + y * attribute2 + w); result = (uint)Math.Floor((double)numerator / divisor + 0.5d); return true; @@ -105,39 +86,6 @@ internal static class RetailSkillFormula }; } - /// - /// Projection-owned sibling used by chargen presentation. Keeping the - /// arithmetic here means the UI consumes the immutable model loaded by - /// ChargenTableReader instead of reading the global SkillTable a - /// second time. - /// - public static uint CalculateChargenScore( - ChargenSkillDetail skillDetail, - uint attribute1, - uint attribute2, - ChargenSkillAdvancementClass level) - { - ChargenSkillFormula formula = skillDetail.Formula; - if (!TryCalculate( - formula.AdditiveBonus, - formula.Attribute1Multiplier, - formula.Attribute2Multiplier, - formula.Divisor, - attribute1, - attribute2, - out uint result)) - { - return 0u; - } - - return level switch - { - ChargenSkillAdvancementClass.Trained => result + 5u, - ChargenSkillAdvancementClass.Specialized => result + 10u, - _ => result, - }; - } - /// /// Retail SkillSystem::InqAttributeName @ 0x005c8d90 — the six /// hardcoded attribute display names (matched exactly against @@ -309,7 +257,7 @@ internal sealed class LiveSkillCreditResolver(SkillTable? skillTable) /// Campaign CC CC5 review fix round, F3 (2026-08-16). Chargen-side sibling /// of : resolves /// against the SAME -/// global SkillTable projection in , fed by a +/// global SkillTable (portal.dat 0x0E000004), fed by a /// candidate character's CHARGEN attribute spread (, /// keyed the same way AcDream.Runtime.Session.ChargenAttributeId /// already does — verified against DatReaderWriter's own @@ -322,33 +270,36 @@ internal sealed class LiveSkillCreditResolver(SkillTable? skillTable) /// dependency of its own — same shape as that composition's existing /// ResolveText binding. /// -internal sealed class ChargenSkillScoreResolver(ChargenOptions options) +internal sealed class ChargenSkillScoreResolver(SkillTable? skillTable) { public uint Resolve( uint skillId, ChargenAttributeValues attributes, ChargenSkillAdvancementClass level) { - if (!options.TryGetSkillDetail(skillId, out ChargenSkillDetail skillDetail)) + if (skillTable?.Skills is null + || !skillTable.Skills.TryGetValue( + (DatReaderWriter.Enums.SkillId)skillId, + out var skillBase)) { return 0u; } - uint attribute1 = ResolveAttribute(skillDetail.Formula.Attribute1, attributes); - uint attribute2 = ResolveAttribute(skillDetail.Formula.Attribute2, attributes); - return RetailSkillFormula.CalculateChargenScore(skillDetail, attribute1, attribute2, level); + uint attribute1 = ResolveAttribute(skillBase.Formula.Attribute1, attributes); + uint attribute2 = ResolveAttribute(skillBase.Formula.Attribute2, attributes); + return RetailSkillFormula.CalculateChargenScore(skillBase, attribute1, attribute2, level); } private static uint ResolveAttribute( - uint attributeId, + DatReaderWriter.Enums.AttributeId attributeId, ChargenAttributeValues attributes) => attributeId switch { - 1u => (uint)Math.Max(0, attributes.Strength), - 2u => (uint)Math.Max(0, attributes.Endurance), - 3u => (uint)Math.Max(0, attributes.Quickness), - 4u => (uint)Math.Max(0, attributes.Coordination), - 5u => (uint)Math.Max(0, attributes.Focus), - 6u => (uint)Math.Max(0, attributes.Self), + DatReaderWriter.Enums.AttributeId.Strength => (uint)Math.Max(0, attributes.Strength), + DatReaderWriter.Enums.AttributeId.Endurance => (uint)Math.Max(0, attributes.Endurance), + DatReaderWriter.Enums.AttributeId.Quickness => (uint)Math.Max(0, attributes.Quickness), + DatReaderWriter.Enums.AttributeId.Coordination => (uint)Math.Max(0, attributes.Coordination), + DatReaderWriter.Enums.AttributeId.Focus => (uint)Math.Max(0, attributes.Focus), + DatReaderWriter.Enums.AttributeId.Self => (uint)Math.Max(0, attributes.Self), _ => 0u, }; } diff --git a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs index f7e39298..e7722f2c 100644 --- a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs +++ b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs @@ -1123,8 +1123,9 @@ internal sealed class LiveEntityNetworkUpdateController /// carve-out is live for both — its AirborneSnap result is exactly /// the dissolved landing scenario, for every guid (see /// ToConstraintArm's A1 mapping and OnPosition's own - /// arm is AirborneSnap handling for the remaining player-guid - /// interp-clear. #316 retired the former shadow-publish skip.) + /// arm is AirborneSnap handling for the two guid-preserved + /// extras — #316's shadow-publish skip and the interp-clear — that ride + /// along with it). /// /// /// @@ -1937,6 +1938,8 @@ internal sealed class LiveEntityNetworkUpdateController RuntimeEntityRecord acceptedPositionCanonical = accepted.Canonical; ulong acceptedPositionAuthorityVersion = accepted.PositionAuthorityVersion; + ulong acceptedPositionVelocityAuthorityVersion = + accepted.VelocityAuthorityVersion; if (!_liveEntities.TryGetProjection( acceptedPositionCanonical, out LiveEntityRecord acceptedPositionRecord) @@ -2387,21 +2390,39 @@ internal sealed class LiveEntityNetworkUpdateController update.Position.LandblockId); } - // #317: a PositionPack carries an optional velocity vector, but - // retail only passes it through HandleReceivedPosition to - // CPhysicsObj::MoveOrTeleport; that function never reads the - // argument (0x00516330-0x00516438). UnpackPositionEvent has no - // set_velocity either. Do not overwrite the physics body's - // velocity here. The wire vector remains available below as the - // remote server-controlled animation/dead-reckoning sample; - // actual authoritative body velocity arrives through the - // separate VectorUpdate handler, which calls set_velocity. + // 4a-family correction (2026-08-04, found and reported while + // pinning C4 route 5's D-P5 no-velocity design): the previous + // comment here claimed "MoveOrTeleport installs that exact vector + // with set_velocity". A byte-level disassembly of the PDB-paired + // binary (0x00516330-0x00516438, every branch) shows + // MoveOrTeleport never reads its velocity argument's stack slot, + // and UnpackPositionEvent performs no set_velocity either — the + // only set_velocity in the whole accepted-Position chain zeroes + // the LOCAL player (@0x004541B4). This call's actual retail + // justification is therefore NOT yet established and needs its + // own audit; what IS still true and unaffected by that finding: + // the canonical seam below wakes the retained ObjectClock and + // body in one operation, and the Position-delta velocity further + // down remains animation diagnostics, never substituted into + // physics. if (!_liveEntities.IsCurrentPositionAuthority( positionRecord, acceptedPositionAuthorityVersion)) { return; } + if (_liveEntities.IsCurrentVelocityAuthority( + positionRecord, + acceptedPositionVelocityAuthorityVersion) + && !_liveEntities.TryCommitAuthoritativeVelocity( + positionRecord, + rmState.Body, + acceptedSpawn.Physics?.Velocity + ?? System.Numerics.Vector3.Zero, + _physicsScriptGameTime)) + { + return; + } // C4 route 4b-3 / D4: retail's single ConstrainTo arming site // (@0x00454272) is now entirely post-operation @@ -2471,10 +2492,15 @@ internal sealed class LiveEntityNetworkUpdateController // survive, both named and justified rather than silently kept: // • TS-44 sticky suppression (below) — an NPC-only steady-state // gate; its own register row already describes it that way. - // • The AirborneSnap arm's interp-clear (further below) remains - // player-only because its steep-non-walkable equivalence has - // not been proven. #316 later unified collision-shadow - // publication for both guid ranges at the common tail. + // • The AirborneSnap arm's interp-clear and collision-shadow + // publish (further below) — PRESERVED, not unified, because + // unifying either way would be an unauthorized behaviour + // change: #316 (filed 2026-08-04) is a real, UNMEASURED + // pre-existing player-guid defect (no shadow publish on + // landing) that this behaviour-preserving collapse must not + // fix, and the interp-clear's equivalence could not be proven + // for the steep-non-walkable-landing edge case (see the + // comment at that arm). // nowSec is captured ONCE, shared by both guid ranges (was two // independent DateTime.UtcNow reads before this collapse — a // microsecond-scale skew in acdream-only bookkeeping/diagnostics). @@ -2663,11 +2689,17 @@ internal sealed class LiveEntityNetworkUpdateController // A1 fix) supplies the same arm value the old landing // block hard-coded. // - // #316 fixed 2026-08-28: the shared tail now publishes - // the resolved collision shadow for player-guid landings - // exactly as it already did for the same NPC arm. The old - // player-only skip left render/body at the landing pose - // while collision remained at its pre-snap position. + // #316 (filed 2026-08-04, deliberately NOT fixed here): + // the player-guid copy of this scenario has never + // published the collision shadow (the tail below does, + // for every OTHER arm and for this SAME arm on NPC + // guids) — a real, UNMEASURED pre-existing defect that + // contradicts the file's own #184 Slice 2b design intent + // ("player shadows now follow the resolved body ... + // exactly like NPCs"). Fixing it is a behaviour change + // this collapse may not make; the skip is reproduced + // verbatim at the tail below, keyed on this same `arm` + // value. // // The interp-queue clear is preserved alongside it rather // than unified either way. AdjustOffset's CONTACT_TS gate @@ -2814,22 +2846,29 @@ internal sealed class LiveEntityNetworkUpdateController // rmState.CellId is the server cell adopted above. The root // frame is committed before collision publication, as in retail // SetPositionInternal. The ONE entity-sync + shadow-publish tail - // now covers every guid and every arm, including #316's formerly - // skipped player-guid AirborneSnap landing. + // for every guid and every arm — except the #316-preserved + // exception: a player-guid AirborneSnap arm still commits the + // render entity from the resolved body but does NOT publish the + // shadow, matching its pre-collapse behaviour exactly (see the + // comment at that arm, above). entity.SetPosition(rmState.Body.Position); entity.ParentCellId = rmState.CellId; entity.Rotation = rmState.Body.Orientation; - AcDream.App.Physics.LiveEntityShadowPublisher.TryPublishRemote( - _liveEntities, - positionRecord, - entity, - rmState, - acceptedPositionAuthorityVersion, - () => _remotePhysicsUpdater.SyncRemoteShadowToBody( - entity.Id, + if (arm is not RemoteContactArm.AirborneSnap + || !IsPlayerGuid(update.Guid)) + { + AcDream.App.Physics.LiveEntityShadowPublisher.TryPublishRemote( + _liveEntities, + positionRecord, + entity, rmState, - _origin.CenterX, - _origin.CenterY)); + acceptedPositionAuthorityVersion, + () => _remotePhysicsUpdater.SyncRemoteShadowToBody( + entity.Id, + rmState, + _origin.CenterX, + _origin.CenterY)); + } } // F751 is only a notification gate; the accepted Position may arrive diff --git a/src/AcDream.App/Rendering/InteriorEntityPartition.cs b/src/AcDream.App/Rendering/InteriorEntityPartition.cs index efb24b94..08da6b15 100644 --- a/src/AcDream.App/Rendering/InteriorEntityPartition.cs +++ b/src/AcDream.App/Rendering/InteriorEntityPartition.cs @@ -109,17 +109,10 @@ public static class InteriorEntityPartition HashSet visibleCells, IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, - IReadOnlyDictionary? AnimatedById)> landblockEntries, - FrustumPlanes? frustum = null, - uint neverCullLandblockId = 0u) + IReadOnlyDictionary? AnimatedById)> landblockEntries) { var result = new Result(); - Partition( - result, - visibleCells, - landblockEntries, - frustum, - neverCullLandblockId); + Partition(result, visibleCells, landblockEntries); return result; } @@ -138,23 +131,11 @@ public static class InteriorEntityPartition HashSet visibleCells, IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, - IReadOnlyDictionary? AnimatedById)> landblockEntries, - FrustumPlanes? frustum = null, - uint neverCullLandblockId = 0u) + IReadOnlyDictionary? AnimatedById)> landblockEntries) { result.ClearForReuse(); foreach (var entry in landblockEntries) { - if (!IsLandblockVisible( - entry.LandblockId, - entry.AabbMin, - entry.AabbMax, - frustum, - neverCullLandblockId)) - { - continue; - } - foreach (var e in entry.Entities) { if (e.MeshRefs.Count == 0) continue; @@ -195,18 +176,11 @@ public static class InteriorEntityPartition IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, IReadOnlyDictionary? AnimatedById)> landblockEntries, - IObserver? observer, - FrustumPlanes? frustum = null, - uint neverCullLandblockId = 0u) + IObserver? observer) { if (observer is null) { - Partition( - result, - visibleCells, - landblockEntries, - frustum, - neverCullLandblockId); + Partition(result, visibleCells, landblockEntries); return; } @@ -216,16 +190,6 @@ public static class InteriorEntityPartition result.ClearForReuse(); foreach (var entry in landblockEntries) { - if (!IsLandblockVisible( - entry.LandblockId, - entry.AabbMin, - entry.AabbMax, - frustum, - neverCullLandblockId)) - { - continue; - } - foreach (var e in entry.Entities) { if (e.MeshRefs.Count == 0) continue; @@ -283,14 +247,4 @@ public static class InteriorEntityPartition /// public static bool IsIndoorCellId(uint? cellId) => cellId is uint c && IsIndoorCellId(c); - - private static bool IsLandblockVisible( - uint landblockId, - Vector3 aabbMin, - Vector3 aabbMax, - FrustumPlanes? frustum, - uint neverCullLandblockId) => - frustum is null - || landblockId == neverCullLandblockId - || FrustumCuller.IsAabbVisible(frustum.Value, aabbMin, aabbMax); } diff --git a/src/AcDream.App/Rendering/LiveEntityAnimationPresenter.cs b/src/AcDream.App/Rendering/LiveEntityAnimationPresenter.cs index fc4d2ae7..26e92693 100644 --- a/src/AcDream.App/Rendering/LiveEntityAnimationPresenter.cs +++ b/src/AcDream.App/Rendering/LiveEntityAnimationPresenter.cs @@ -159,12 +159,16 @@ internal sealed class LiveEntityAnimationPresenter continue; if (span > 0 && legacyAdvanceSeconds > 0f) { - animation.CurrFrame = RetailAnimationCyclePlayback.Advance( - animation.CurrFrame, - animation.LowFrame, - animation.HighFrame, - animation.Framerate, - legacyAdvanceSeconds); + animation.CurrFrame += legacyAdvanceSeconds * animation.Framerate; + if (animation.CurrFrame > animation.HighFrame) + { + float over = animation.CurrFrame - animation.LowFrame; + animation.CurrFrame = animation.LowFrame + (over % (span + 1)); + } + else if (animation.CurrFrame < animation.LowFrame) + { + animation.CurrFrame = animation.LowFrame; + } } } @@ -275,14 +279,33 @@ internal sealed class LiveEntityAnimationPresenter return false; } - return RetailAnimationCyclePlayback.TryInterpolatePart( - animation.Animation, - animation.CurrFrame, - animation.LowFrame, - animation.HighFrame, - partIndex, - out origin, - out orientation); + int frameIndex = (int)Math.Floor(animation.CurrFrame); + if (frameIndex < animation.LowFrame + || frameIndex > animation.HighFrame + || frameIndex >= animation.Animation.PartFrames.Count) + { + frameIndex = animation.LowFrame; + } + int nextIndex = frameIndex + 1; + if (nextIndex > animation.HighFrame + || nextIndex >= animation.Animation.PartFrames.Count) + { + nextIndex = animation.LowFrame; + } + float t = Math.Clamp(animation.CurrFrame - frameIndex, 0f, 1f); + var frames = animation.Animation.PartFrames[frameIndex].Frames; + var nextFrames = animation.Animation.PartFrames[nextIndex].Frames; + if (partIndex < frames.Count) + { + var first = frames[partIndex]; + var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first; + origin = Vector3.Lerp(first.Origin, next.Origin, t); + orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t); + return true; + } + origin = default; + orientation = default; + return false; } private static void EnsureRetainedPoses(LiveEntityAnimationState animation) diff --git a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs index b19eba1d..815a794c 100644 --- a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs +++ b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs @@ -24,8 +24,6 @@ internal interface IPaperdollFrameView bool TryGetVisibleSize(out int width, out int height); void SetTextureHandle(uint textureHandle); - - void ClearTextureHandle(); } internal interface IPaperdollInventoryVisibility @@ -53,9 +51,7 @@ internal interface IPaperdollPoseApplicator /// presentation edge. The renderer remains a borrowed resource disposed by /// the existing window shutdown transaction. /// -internal sealed class PaperdollFramePresenter : - IPrivateEntityViewportFrame, - IPrivateEntityViewportResourcePreparation +internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame { private readonly IPaperdollDollRenderer _renderer; private readonly IPaperdollFrameView _view; @@ -77,30 +73,22 @@ internal sealed class PaperdollFramePresenter : public void MarkDirty() => _dirty = true; - /// - /// Pre-world resource phase: rebuild/redress the private clone and advance - /// its mesh/composite readiness BEFORE world draws can consume the - /// bounded per-frame composite-upload budget. In a dense scene the late - /// presentation phase never wins that budget, which is why the doll was - /// visible only while portal space quiesced the world (#443). - /// - public void PrepareResources() + public void Render() { if (_dirty) { if (_factory.TryBuild(out WorldEntity? doll)) { - // Redress every accepted live-player refresh, including an - // appearance-equivalent ObjDesc after portal/relogin. The - // private clone belongs to the current presentation - // generation; retaining the old object merely because its - // pixels compare equal can leave it attached to retired - // mesh/composite readiness that never completes again. - // PrivateEntityViewportRenderer promotes replacements in two - // phases, so the last completed target remains visible until - // this fresh clone is completely drawable. - _renderer.SetDoll(doll); - _doll = doll; + // Same-generation CreateObject refreshes can repeat the exact + // player ObjDesc at a portal boundary. Retail redresses its + // private inventory object in place; releasing and reacquiring + // an identical synthetic owner briefly blanks the viewport and + // churns its texture composites. + if (!HasEquivalentAppearance(_doll, doll)) + { + _renderer.SetDoll(doll); + _doll = doll; + } _dirty = false; } else @@ -108,25 +96,16 @@ internal sealed class PaperdollFramePresenter : // gmPaperDollUI::RedressCreature @ 0x004A3BC0 leaves its // private m_pInventoryObject intact when the SmartBox player // is temporarily unavailable. Keep the successful doll and - // retry this dirty redress on the next frame. + // retry this dirty redress on the next visible frame. } } _renderer.Prepare(); - } - public void Render() - { if (!_view.TryGetVisibleSize(out int width, out int height)) return; - // Zero is a transient not-ready result, not a request to erase a - // previously completed paperdoll. Session reset clears explicitly in - // ResetSession; ordinary mesh/composite upload latency keeps the last - // good image instead of intermittently blanking the viewport. - uint textureHandle = _renderer.Render(width, height); - if (textureHandle != 0u) - _view.SetTextureHandle(textureHandle); + _view.SetTextureHandle(_renderer.Render(width, height)); } /// @@ -136,10 +115,80 @@ internal sealed class PaperdollFramePresenter : public void ResetSession() { _renderer.SetDoll(null); - _view.ClearTextureHandle(); _doll = null; _dirty = true; } + + private static bool HasEquivalentAppearance( + WorldEntity? current, + WorldEntity? candidate) + { + if (current is null || candidate is null) + return ReferenceEquals(current, candidate); + if (current.SourceGfxObjOrSetupId != candidate.SourceGfxObjOrSetupId + || current.Scale != candidate.Scale + || current.HiddenPartsMask != candidate.HiddenPartsMask + || current.MeshRefs.Count != candidate.MeshRefs.Count + || current.PartOverrides.Count != candidate.PartOverrides.Count) + { + return false; + } + + for (int i = 0; i < current.MeshRefs.Count; i++) + { + MeshRef left = current.MeshRefs[i]; + MeshRef right = candidate.MeshRefs[i]; + if (left.GfxObjId != right.GfxObjId + || left.PartTransform != right.PartTransform + || !DictionaryEquals( + left.SurfaceOverrides, + right.SurfaceOverrides)) + { + return false; + } + } + + for (int i = 0; i < current.PartOverrides.Count; i++) + { + if (current.PartOverrides[i] != candidate.PartOverrides[i]) + return false; + } + + PaletteOverride? leftPalette = current.PaletteOverride; + PaletteOverride? rightPalette = candidate.PaletteOverride; + if (leftPalette is null || rightPalette is null) + return leftPalette is null && rightPalette is null; + if (leftPalette.BasePaletteId != rightPalette.BasePaletteId + || leftPalette.SubPalettes.Count != rightPalette.SubPalettes.Count) + { + return false; + } + for (int i = 0; i < leftPalette.SubPalettes.Count; i++) + { + if (leftPalette.SubPalettes[i] != rightPalette.SubPalettes[i]) + return false; + } + return true; + } + + private static bool DictionaryEquals( + IReadOnlyDictionary? left, + IReadOnlyDictionary? right) + { + if (left is null || right is null) + return left is null && right is null; + if (left.Count != right.Count) + return false; + foreach ((uint key, uint value) in left) + { + if (!right.TryGetValue(key, out uint rightValue) + || rightValue != value) + { + return false; + } + } + return true; + } } /// Retained-UI visibility and texture publication for the doll view. @@ -177,9 +226,6 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView /// public void SetTextureHandle(uint textureHandle) => _viewport.TextureSlot = UiTextureTableHandle.ToSlot(textureHandle); - - public void ClearTextureHandle() => - _viewport.TextureSlot = GpuTextureSlot.Unassigned; } /// Narrow visibility adapter for the paperdoll's inventory host. diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs index 9766089b..6a963121 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.cs @@ -234,8 +234,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable IReadOnlySet attachedOwnerIds, bool includeUnattached = false, IReadOnlySet? excludedAttachedOwnerIds = null, - uint clipSlot = 0, - UnattachedEmitterCellScope unattachedCellScope = UnattachedEmitterCellScope.Any) + uint clipSlot = 0) { if (camera is null) return; @@ -245,8 +244,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable attachedOwnerIds, includeUnattached, _scopedEmitterScratch, - excludedAttachedOwnerIds, - unattachedCellScope); + excludedAttachedOwnerIds); Matrix4x4.Invert(camera.View, out Matrix4x4 invView); Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13)); Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23)); diff --git a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs index c395af3f..84961aab 100644 --- a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs @@ -20,8 +20,8 @@ internal interface IPrivateEntityViewportCamera : ICamera /// /// Shared render-to-texture implementation for the private 3-D creature /// viewports used by paperdoll and examination UI. Each instance owns one -/// per encountered GPU flight slot, one -/// synthetic render identity, and one balanced texture-owner lease. +/// , one synthetic render identity, and one +/// balanced texture-owner lease. /// /// Campaign V slice V6k (V4g's first half). The target used to be a /// hand-rolled FBO, colour texture and depth renderbuffer, and the resulting GL @@ -65,6 +65,7 @@ internal sealed class PrivateEntityViewportRenderer : { private const uint PrivateLandblockId = 0u; + private readonly IGpuDevice _device; private readonly ICurrentGpuFrameSource _frames; /// @@ -93,12 +94,17 @@ internal sealed class PrivateEntityViewportRenderer : /// feature does not exist for them, not just "unused". private readonly EntitySlot? _backdropSlot; - // A target written by frame N cannot also be sampled by an unretired frame - // N-1. Vulkan permits those command buffers to overlap, so one shared image - // is a cross-frame write/read race. The current frame slot selects one - // bounded target + texture-table handle; the retained UI samples that exact - // handle later in the same command buffer. - private readonly PrivateViewportFlightTargets _flightTargets; + // One stable sampled texture-table slot is part of the retained viewport's + // presentation contract. Rotating the slot with the Vulkan flight index + // made the UI sample a freshly-created/cleared sibling after world reveal. + // The frame submission order already protects this target's write -> sample + // transition; keep its identity stable until resize or disposal. + private IGpuRenderTarget? _target; + private IGpuSampler? _sampler; + private GpuTextureSlot _slot = GpuTextureSlot.Unassigned; + private int _fbW; + private int _fbH; + private bool _hasRenderedScene; public PrivateEntityViewportRenderer( IWorldPassScope scope, @@ -121,7 +127,7 @@ internal sealed class PrivateEntityViewportRenderer : _scope = scope ?? throw new ArgumentNullException( nameof(scope), "The viewport must publish a world pass scope to draw into."); - ArgumentNullException.ThrowIfNull(device); + _device = device ?? throw new ArgumentNullException(nameof(device)); _frames = frames ?? throw new ArgumentNullException(nameof(frames)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _lightUbo = lightUbo ?? throw new ArgumentNullException(nameof(lightUbo)); @@ -131,9 +137,6 @@ internal sealed class PrivateEntityViewportRenderer : _diagnosticName = string.IsNullOrWhiteSpace(diagnosticName) ? "creature viewport" : diagnosticName; - _flightTargets = new PrivateViewportFlightTargets( - device, - _diagnosticName); IEntityTextureLifetime textureLifetimeChecked = textureLifetime ?? throw new ArgumentNullException(nameof(textureLifetime)); @@ -170,17 +173,7 @@ internal sealed class PrivateEntityViewportRenderer : /// public bool TextureIsBottomUp => false; - public void SetEntity(WorldEntity? entity) - { - _mainSlot.Set(entity); - if (entity is null) - { - // A character-session reset explicitly invalidates the sampled - // scenes. Do not let a replacement that is still uploading expose - // the previous character through any flight target. - _flightTargets.InvalidateCompletedScenes(); - } - } + public void SetEntity(WorldEntity? entity) => _mainSlot.Set(entity); /// /// Advances the private entity's mesh and texture-composite readiness @@ -233,50 +226,51 @@ internal sealed class PrivateEntityViewportRenderer : /// public uint Render(int width, int height) { - if (width <= 0 || height <= 0) - return 0u; - - IGpuFrame frame = _frames.CurrentFrame - ?? throw new InvalidOperationException( - $"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); - int frameSlot = frame.SlotIndex; - // #443: acquiring a synthetic mesh reference only schedules CPU // preparation/GPU upload; it does not make the mesh drawable. Keep the - // current flight slot's last completed private scene intact until every - // drawable mesh in the replacement has crossed that upload barrier. - // On first open there is no completed scene, so return zero and let the - // authored panel art show through instead of publishing a cleared target. + // last completed private scene intact until every drawable mesh in the + // replacement has crossed that upload barrier. On first open there is + // no completed scene, so return zero and let the authored panel art + // show through instead of publishing a freshly-cleared black target. bool mainReady = _mainSlot.PrepareForDraw(); bool backdropReady = _backdropSlot?.PrepareForDraw() ?? true; if (!mainReady || !backdropReady) { return _mainSlot.Entity is not null - ? _flightTargets.CompletedHandle(frameSlot) + && _hasRenderedScene + && _slot.IsAssigned + ? UiTextureTableHandle.FromSlot(_slot) : 0u; } WorldEntity? entity = _mainSlot.Entity; - if (entity is null || entity.MeshRefs.Count == 0) + if (entity is null || entity.MeshRefs.Count == 0 || width <= 0 || height <= 0) return 0u; IReadOnlyList drawEntities = BuildDrawEntities( _backdropSlot?.Entity, entity); if (!_dispatcher.PreparePrivateEntityResources(drawEntities)) - return _flightTargets.CompletedHandle(frameSlot); + { + return _hasRenderedScene && _slot.IsAssigned + ? UiTextureTableHandle.FromSlot(_slot) + : 0u; + } - PrivateViewportFlightTargets.TargetSlot? targetSlot = - _flightTargets.Ensure(frameSlot, width, height); - if (targetSlot is null) + EnsureRenderTarget(width, height); + if (_target is null) return 0u; _camera.Aspect = width / (float)height; + IGpuFrame frame = _frames.CurrentFrame + ?? throw new InvalidOperationException( + $"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); + using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription { Name = _diagnosticName, Color = new GpuColorAttachment( - Target: targetSlot.Target, + Target: _target, Load: GpuLoadOp.Clear, Store: GpuStoreOp.Store, ClearColor: Vector4.Zero), @@ -310,12 +304,6 @@ internal sealed class PrivateEntityViewportRenderer : null), }; - // #443: a private pass must not append its transforms into the shared - // world transform frame — the default mesh shaders index parallel - // per-instance arrays zero-based, so a non-zero arena base zeroes the - // doll's per-instance opacity and the target stays blank whenever a - // world frame is active. See NextClassicDrawIsPrivatePass. - _dispatcher.NextClassicDrawIsPrivatePass = true; _dispatcher.Draw( _camera, entries, @@ -323,8 +311,8 @@ internal sealed class PrivateEntityViewportRenderer : neverCullLandblockId: PrivateLandblockId, visibleCellIds: null, animatedEntityIds: _animatedIds); - targetSlot.HasRenderedScene = true; - return UiTextureTableHandle.FromSlot(targetSlot.TextureSlot); + _hasRenderedScene = true; + return UiTextureTableHandle.FromSlot(_slot); } /// @@ -375,6 +363,69 @@ internal sealed class PrivateEntityViewportRenderer : }); } + private void EnsureRenderTarget(int width, int height) + { + if (_target is not null && width == _fbW && height == _fbH) + return; + ReleaseRenderTarget(); + + IGpuRenderTarget target; + try + { + target = _device.CreateRenderTarget(new GpuRenderTargetDescription( + _diagnosticName, + width, + height, + GpuTextureFormat.Rgba8UnormRenderTarget, + // Depth24Stencil8, as the hand-rolled renderbuffer was: nothing + // samples it, and the stencil aspect keeps the attachment shape + // the depth/stencil renderers already expect. + GpuTextureFormat.Depth24Stencil8, + SampleCount: 1)); + } + catch (Exception failure) + { + Console.WriteLine( + $"[{_diagnosticName}] render target unavailable ({width}x{height}): {failure.Message}"); + return; + } + + try + { + // The retained UI blits this attachment as an ordinary table entry. + // Linear/clamped is the filtering the hand-rolled colour texture set + // on itself before the §7.1 seam registered it. + _sampler = _device.CreateSampler(GpuSamplerDescription.WorldClamp); + _slot = _device.RegisterTexture(target.ColorTexture, _sampler); + } + catch + { + target.Dispose(); + _sampler = null; + _slot = GpuTextureSlot.Unassigned; + throw; + } + + _target = target; + _fbW = width; + _fbH = height; + } + + private void ReleaseRenderTarget() + { + if (_slot.IsAssigned) + { + _device.ReleaseTextureSlot(_slot); + _slot = GpuTextureSlot.Unassigned; + } + _sampler = null; + _target?.Dispose(); + _target = null; + _fbW = 0; + _fbH = 0; + _hasRenderedScene = false; + } + public void Dispose() { List? failures = null; @@ -396,7 +447,7 @@ internal sealed class PrivateEntityViewportRenderer : } try { - _flightTargets.Dispose(); + ReleaseRenderTarget(); } catch (Exception error) { @@ -411,163 +462,6 @@ internal sealed class PrivateEntityViewportRenderer : } } - /// - /// Bounded render-target ownership keyed by . - /// A frame slot is reopened only after its previous submission retires, so - /// the target selected here can be written and sampled within that frame - /// without racing a different in-flight command buffer. - /// - internal sealed class PrivateViewportFlightTargets : IDisposable - { - internal sealed class TargetSlot( - IGpuRenderTarget target, - GpuTextureSlot textureSlot) - { - internal IGpuRenderTarget Target { get; } = target; - internal GpuTextureSlot TextureSlot { get; } = textureSlot; - internal bool HasRenderedScene { get; set; } - } - - private readonly IGpuDevice _device; - private readonly string _diagnosticName; - private readonly List _slots = []; - private int _width; - private int _height; - private bool _disposed; - - internal PrivateViewportFlightTargets( - IGpuDevice device, - string diagnosticName) - { - _device = device ?? throw new ArgumentNullException(nameof(device)); - _diagnosticName = string.IsNullOrWhiteSpace(diagnosticName) - ? "creature viewport" - : diagnosticName; - } - - internal int AllocatedSlotCount => - _slots.Count(static slot => slot is not null); - - internal TargetSlot? Ensure(int frameSlot, int width, int height) - { - ObjectDisposedException.ThrowIf(_disposed, this); - ArgumentOutOfRangeException.ThrowIfNegative(frameSlot); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); - - if (_width != 0 && (_width != width || _height != height)) - ReleaseAll(); - - while (_slots.Count <= frameSlot) - _slots.Add(null); - if (_slots[frameSlot] is { } existing) - return existing; - - IGpuRenderTarget target; - try - { - target = _device.CreateRenderTarget( - new GpuRenderTargetDescription( - $"{_diagnosticName}-flight-{frameSlot}", - width, - height, - GpuTextureFormat.Rgba8UnormRenderTarget, - // Depth24Stencil8, as the original private viewport - // renderbuffer was. Nothing samples this attachment. - GpuTextureFormat.Depth24Stencil8, - SampleCount: 1)); - } - catch (Exception failure) - { - Console.WriteLine( - $"[{_diagnosticName}] render target unavailable " - + $"({width}x{height}, flight {frameSlot}): {failure.Message}"); - return null; - } - - try - { - // The device de-duplicates immutable samplers. Retained UI - // blits this target through its ordinary texture-table entry. - IGpuSampler sampler = _device.CreateSampler( - GpuSamplerDescription.WorldClamp); - GpuTextureSlot textureSlot = _device.RegisterTexture( - target.ColorTexture, - sampler); - var created = new TargetSlot(target, textureSlot); - _slots[frameSlot] = created; - _width = width; - _height = height; - return created; - } - catch - { - target.Dispose(); - throw; - } - } - - internal uint CompletedHandle(int frameSlot) - { - if ((uint)frameSlot >= (uint)_slots.Count - || _slots[frameSlot] is not { HasRenderedScene: true } slot) - { - return 0u; - } - - return UiTextureTableHandle.FromSlot(slot.TextureSlot); - } - - internal void InvalidateCompletedScenes() - { - for (int i = 0; i < _slots.Count; i++) - { - if (_slots[i] is { } slot) - slot.HasRenderedScene = false; - } - } - - private void ReleaseAll() - { - List? failures = null; - for (int i = 0; i < _slots.Count; i++) - { - TargetSlot? slot = _slots[i]; - if (slot is null) - continue; - try - { - _device.ReleaseTextureSlot(slot.TextureSlot); - } - catch (Exception error) - { - (failures ??= []).Add(error); - } - try - { - slot.Target.Dispose(); - } - catch (Exception error) - { - (failures ??= []).Add(error); - } - } - _slots.Clear(); - _width = 0; - _height = 0; - if (failures is { Count: > 0 }) - throw new AggregateException(failures); - } - - public void Dispose() - { - if (_disposed) - return; - _disposed = true; - ReleaseAll(); - } - } - /// /// One private entity's mesh-reference/texture-owner lifetime, independent /// of every other slot on the renderer. Publication is two-phase: a candidate owns its mesh diff --git a/src/AcDream.App/Rendering/PrivatePresentationRenderer.cs b/src/AcDream.App/Rendering/PrivatePresentationRenderer.cs index fa59bf91..1a3d5db8 100644 --- a/src/AcDream.App/Rendering/PrivatePresentationRenderer.cs +++ b/src/AcDream.App/Rendering/PrivatePresentationRenderer.cs @@ -16,19 +16,6 @@ internal interface IPrivateEntityViewportFrame void Render(); } -/// -/// Optional pre-world resource preparation for a private viewport whose -/// visibility must not compete with world composite uploads. The composite -/// upload budget opens with the frame's resource phase; a dense world can -/// consume all of it every frame, so a viewport that only prepares during -/// late presentation can starve indefinitely (#443's paperdoll: visible in -/// portal space — where the world is quiesced — and nowhere busy). -/// -internal interface IPrivateEntityViewportResourcePreparation -{ - void PrepareResources(); -} - internal interface IRetainedGameplayUiFrame { void Render(double deltaSeconds, int width, int height); diff --git a/src/AcDream.App/Rendering/RenderFramePreparationController.cs b/src/AcDream.App/Rendering/RenderFramePreparationController.cs index 820ddaa1..82306d67 100644 --- a/src/AcDream.App/Rendering/RenderFramePreparationController.cs +++ b/src/AcDream.App/Rendering/RenderFramePreparationController.cs @@ -29,29 +29,20 @@ internal sealed class RenderFramePreparationController : IRenderFrameResourcePha private readonly IRenderFrameResourcePhase _resources; private readonly IDevToolsFrameLifecycle? _devTools; private readonly IRenderWeatherFramePhase _weather; - private readonly IPrivateEntityViewportResourcePreparation? _privateViewports; public RenderFramePreparationController( IRenderFrameResourcePhase resources, IDevToolsFrameLifecycle? devTools, - IRenderWeatherFramePhase weather, - IPrivateEntityViewportResourcePreparation? privateViewports = null) + IRenderWeatherFramePhase weather) { _resources = resources ?? throw new ArgumentNullException(nameof(resources)); _devTools = devTools; _weather = weather ?? throw new ArgumentNullException(nameof(weather)); - _privateViewports = privateViewports; } public void Prepare(RenderFrameInput input) { _resources.Prepare(input); - // The composite upload budget opens with the resource phase. Give the - // paperdoll's private object its prewarm slot before the world can - // consume the complete per-frame budget (#443 — the doll rendered - // only while portal space quiesced the world); presentation samples - // the result later, after the world pass has closed. - _privateViewports?.PrepareResources(); _devTools?.BeginFrame((float)input.DeltaSeconds); _weather.Tick(input.DeltaSeconds); } diff --git a/src/AcDream.App/Rendering/RetailAlphaQueue.cs b/src/AcDream.App/Rendering/RetailAlphaQueue.cs index 3f828f0a..9fea6157 100644 --- a/src/AcDream.App/Rendering/RetailAlphaQueue.cs +++ b/src/AcDream.App/Rendering/RetailAlphaQueue.cs @@ -239,106 +239,6 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame } } - /// - /// Drains only the entries at or beyond - /// and keeps every nearer entry queued with the frame open. This is the - /// pre/inter-building barrier semantics: retail's far→near land walk means - /// DrawBuilding's FlushAlphaList(0f) @0x0059F2A0 can only - /// flush content from cells FARTHER than that building — a nearer emitter - /// has not been inserted yet and composites after the building at a later - /// flush (the float there is a COUNT threshold, not a depth). The batched - /// landscape has no per-cell walk, so the same outcome is restored by - /// draining the far prefix of the established far→near order (AP-236). - /// Sources are deliberately NOT reset: retained tokens must stay valid - /// for the remaining entries' later . - /// - public void FlushFartherThan(float minViewerDistance) - { - if (!IsCollecting) - throw new InvalidOperationException("Retail alpha flush requires an active frame."); - if (_submissions.Count == 0) - return; - - float threshold = NormalizeDistance(minViewerDistance); - SortRetailOrder(); - int prefix = 0; - while (prefix < _submissions.Count - && _submissions[prefix].ViewerDistance >= threshold) - { - prefix++; - } - - if (prefix == 0) - return; - - try - { - EnsureTokenCapacity(prefix); - EnsureSourceCapacity(_sources.Count); - Array.Clear(_sourceDrawOffsets, 0, _sources.Count); - - for (int sourceIndex = 0; sourceIndex < _sources.Count; sourceIndex++) - { - IRetailAlphaDrawSource source = _sources[sourceIndex]; - int sourceCount = 0; - for (int i = 0; i < prefix; i++) - { - RetailAlphaSubmission submission = _submissions[i]; - if (ReferenceEquals(submission.Source, source)) - _tokenScratch[sourceCount++] = submission.Token; - } - - if (sourceCount > 0) - source.PrepareAlphaDraws(_tokenScratch.AsSpan(0, sourceCount)); - } - - int start = 0; - while (start < prefix) - { - IRetailAlphaDrawSource source = _submissions[start].Source; - int end = start + 1; - while (end < prefix - && ReferenceEquals(_submissions[end].Source, source)) - end++; - - int count = end - start; - int sourceIndex = FindSourceIndex(source); - int firstPreparedDraw = _sourceDrawOffsets[sourceIndex]; - source.DrawPreparedAlphaBatch(firstPreparedDraw, count); - _sourceDrawOffsets[sourceIndex] += count; - start = end; - } - } - catch - { - // Converge to the full-drain failure shape: the retained suffix - // cannot be trusted once a source threw mid-prepare/draw. - _submissions.Clear(); - List? resetFailures = null; - for (int i = 0; i < _sources.Count; i++) - { - try - { - _sources[i].ResetAlphaSubmissions(); - } - catch (Exception error) - { - (resetFailures ??= []).Add(error); - } - } - _sources.Clear(); - if (resetFailures is { Count: > 0 }) - { - throw new AggregateException( - "Retail alpha partial drain failed and its submissions could not be fully reset.", - resetFailures); - } - throw; - } - - _submissions.RemoveRange(0, prefix); - } - public void EndFrame() { if (!IsCollecting) diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 037c9f17..5b90654a 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -444,9 +444,20 @@ internal sealed class RetailPViewPassExecutor : animatedEntityIds: frame.AnimatedEntityIds); } - // Late-stage particle owners submit ONCE per frame through - // DrawLandscapeStaticParticles after the slice loop (retail: one - // unclipped alpha-list insertion per emitter), not per slice here. + _particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds); + + if (_particleClassifications.Outdoor.Count > 0 + && _particles is not null + && _particleRenderer is not null) + { + _particleRenderer.DrawForOwners( + frame.Camera, + frame.CameraWorldPosition, + ParticleRenderPass.Scene, + _particleClassifications.Outdoor, + clipSlot: (uint)context.Slice.Slot); + } + EnableClipDistances(); if (frame.RenderSky && frame.RenderWeather) { @@ -481,12 +492,8 @@ internal sealed class RetailPViewPassExecutor : RetailPViewFrameInput frame, RetailPViewLandscapeStaticParticleContext context) { - // One unclipped submission per owner per frame. Retail never clips a - // particle to a portal view — its polys join the one alpha list during - // the owner cell's walk turn and the depth test at the flush decides - // occlusion (FlushAlphaList @0x0059D2E0). The former per-slice call - // with the slice's clip slot both hardware-cut effects at aperture - // boundaries and double-submitted owners visible in two slices. + bool scissor = BeginDoorwayScissor(context.Slice.NdcAabb); + _surface.BindTerrainClip(); DisableClipDistances(); _particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds); @@ -499,9 +506,11 @@ internal sealed class RetailPViewPassExecutor : frame.CameraWorldPosition, ParticleRenderPass.Scene, _particleClassifications.Outdoor, - clipSlot: 0); + clipSlot: (uint)context.Slice.Slot); } + if (scissor) + _surface.EndScissor(); _entities.ClearClipRouting(); DisableClipDistances(); } @@ -564,19 +573,11 @@ internal sealed class RetailPViewPassExecutor : public void DrawUnattachedSceneParticles( RetailPViewFrameInput frame, - bool outdoorCells) + ClipViewSlice slice) { if (_particles is null || _particleRenderer is null) return; - // Retail draws an unattached emitter once, during its owner CELL's - // walk turn, with NO portal-view clip (CPhysicsObj::ShouldDrawParticles - // @0x0050FE60 gates by cell in-view + distance; occlusion is the depth - // test at FlushAlphaList @0x0059D2E0). Outdoor-cell emitters submit in - // the landscape stage, interior-cell emitters in the final world stage. - // The former once-per-OutsideView-slice submission with that slice's - // hardware clip slot made effects vanish by view direction (zero - // outside slices in view = zero submissions) — invented behavior. DisableClipDistances(); _particleRenderer.DrawForOwners( frame.Camera, @@ -584,17 +585,11 @@ internal sealed class RetailPViewPassExecutor : ParticleRenderPass.Scene, _noSceneParticleEntityIds, includeUnattached: true, - clipSlot: 0, - unattachedCellScope: outdoorCells - ? UnattachedEmitterCellScope.OutdoorCells - : UnattachedEmitterCellScope.InteriorCells); + clipSlot: (uint)slice.Slot); } public void FlushLandscapeAlpha() => _alpha.Flush(); - public void FlushLandscapeAlphaFartherThan(float minViewerDistance) => - _alpha.FlushFartherThan(minViewerDistance); - public void DrawCellParticles( RetailPViewFrameInput frame, RetailPViewCellSliceContext context) @@ -613,14 +608,12 @@ internal sealed class RetailPViewPassExecutor : return; DisableClipDistances(); - // Retail never clips cell particles to a portal view: the owner - // cell's walls own occlusion via the depth test at the alpha flush. _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, visible, - clipSlot: 0); + clipSlot: (uint)context.Slice.Slot); DisableClipDistances(); } diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index f1c71440..0aa5b947 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -28,19 +28,6 @@ public sealed class RetailPViewRenderer private static readonly IReadOnlySet NoParticleOwners = new HashSet(); - // Frame unions for the once-per-frame particle submissions (retail: one - // unclipped alpha-list insertion per emitter; occlusion by depth at the - // flush). Per-slice owner culls still run — these accumulate their union. - private readonly HashSet _staticParticleUnionScratch = new(); - private readonly HashSet _cellParticleUnionScratch = new(); - - // Every cell drawn as a building look-in this frame. Retail marks each - // drawn non-player part for the frame (DrawMeshInternal @0x0059F360, - // GetDrawnThisFrame), so an object whose cell drew with a look-in cannot - // draw again in a later pass; dynamics-last consults this set to honor - // the same drawn-once contract. - private readonly HashSet _lookInCellIds = new(); - private readonly HashSet _oneCell = new(1); // Shell-batch scratch: all of a pass's cells collected for ONE batched // opaque Render call (instead of one heavy Render per cell). Reused across @@ -179,19 +166,13 @@ public sealed class RetailPViewRenderer // contains them). drawableCells itself stays the MAIN flood: it feeds the // seals, the outside-stage predicate, and the frame result. var prepareCells = drawableCells; - _lookInCellIds.Clear(); if (_lookInFrames.Count > 0) { _lookInPrepareScratch.Clear(); _lookInPrepareScratch.UnionWith(drawableCells); foreach (var f in _lookInFrames) - { foreach (uint c in f.OrderedVisibleCells) - { _lookInPrepareScratch.Add(c); - _lookInCellIds.Add(c); - } - } prepareCells = _lookInPrepareScratch; } @@ -258,9 +239,7 @@ public sealed class RetailPViewRenderer _partitionResult, prepareCells, ctx.LandblockEntries, - _partitionObserver, - ctx.Frustum, - ctx.PlayerLandblockId ?? 0u); + _partitionObserver); partition = _partitionResult; } @@ -270,13 +249,10 @@ public sealed class RetailPViewRenderer RenderProjectionCounts sourceCounts = frameViewBorrowed ? frameView.SourceDigest.Counts : LegacySourceCounts(partition!); - // prepareCells is exactly "main flood ∪ look-in cells" — the cells - // this traversal actually reached, i.e. retail's in-view set. RetailPViewFrameResult result = _frameResultScratch.Reset( pvFrame, clipAssembly, drawableCells, - prepareCells, counts, sourceCounts, partition); @@ -318,31 +294,6 @@ public sealed class RetailPViewRenderer frameEntityPasses, in frameView); passes.UseIndoorMembershipOnlyRouting(); - - // Retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE - // its portal-only far-Z pass. In retail's strict far→near walk - // everything queued at that instant is FARTHER than the structure - // being punched, so no already-drained poly can meet a punched - // aperture's falsified depth, and everything drained later is - // NEARER than the punched structure and legitimately composites in - // front of it. The batched outdoor frame reproduces that invariant - // here: drain the far prefix — every entry at or beyond the - // nearest cell whose exit-portal mask is about to punch far-Z — - // against still-true landscape depth. Without this, an exterior - // waterfall beyond the cathedral drains after the punches and - // z-passes across every aperture pixel whose true depth the punch - // erased (#132 regression found at the 2026-08-29 cathedral gate). - // Interior roots keep their pre-clear stage-boundary drain. - if (ctx.RootCell.IsOutdoorNode) - { - passes.FlushLandscapeAlphaFartherThan( - ExitPortalMaskBarrierDistance( - pvFrame, - drawableCells, - ctx.Cells, - ctx.CameraWorldPosition)); - } - DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells); DrawEnvCellShells(passes, pvFrame); DrawCellObjectLists( @@ -364,24 +315,6 @@ public sealed class RetailPViewRenderer frameEntityPasses, in frameView); - // OUTDOOR root: the LScape-boundary alpha drain deferred from the - // landscape stage runs HERE, after punches, interior shells, cell - // objects, and the dynamics pass — the frame's complete opaque - // world. Retail's walk draws all of those before its boundary - // flush (LScape::draw includes every cell's objects, - // DrawSortCell 0x005A17C0), so this is the same one-list far→near - // composite over finished depth; draining at the stage end instead - // let every later opaque mesh overwrite the flames (#132). - if (ctx.RootCell.IsOutdoorNode) - passes.FlushLandscapeAlpha(); - - // Interior-cell UNATTACHED emitters (spell ground effects and - // swirls anchored in EnvCells) draw in this final world scope — - // the cells' walls and the seals already own the depth buffer, so - // one unclipped submission matches retail's cell-walk insertion. - // Outdoor-cell unattached emitters drew in the landscape stage. - passes.DrawUnattachedSceneParticles(ctx, outdoorCells: false); - if (entityFrameOpen) { frameEntityPasses!.CompleteEntityFrame(in frameView); @@ -493,71 +426,6 @@ public sealed class RetailPViewRenderer } } - /// - /// Conservative barrier drain threshold for one look-in frame: the viewer - /// distance to the frame's nearest anchor-cell ORIGIN. Cell origins sit - /// inside the building, so this over-estimates the building's - /// nearest-point distance and under-drains; anything conservatively - /// retained still composites correctly at the later depth-tested drains. - /// Retail needs no threshold — its far→near walk guarantees only farther - /// content is queued when DrawBuilding flushes (@0x0059F2A0). Returns 0 - /// (full drain, today's behavior) when no cell resolves. - /// - /// - /// The pre-punch barrier threshold for : - /// the nearest drawable cell whose exit-portal mask is about to write - /// far-Z. Every queued alpha entry at or beyond it must drain first - /// (retail DrawBuilding @0x0059F2A0's FlushAlphaList(0f) before the - /// portal-only pass), because after the punch those entries would z-pass - /// across aperture pixels whose true depth no longer exists. No punched - /// cells → MaxValue → the partial drain retains everything. - /// - internal static float ExitPortalMaskBarrierDistance( - PortalVisibilityFrame frame, - HashSet drawableCells, - IRetailPViewCellSource cells, - Vector3 viewerPosition) - { - float best = float.PositiveInfinity; - for (int i = 0; i < frame.OrderedVisibleCells.Count; i++) - { - uint cellId = frame.OrderedVisibleCells[i]; - if (!drawableCells.Contains(cellId)) - continue; - LoadedCell? cell = cells.Find(cellId); - if (cell is null) - continue; - float distance = Vector3.Distance( - cell.WorldTransform.Translation, - viewerPosition); - if (distance < best) - best = distance; - } - - return float.IsFinite(best) ? best : float.MaxValue; - } - - internal static float LookInBarrierDrainDistance( - PortalVisibilityFrame frame, - IRetailPViewCellSource cells, - Vector3 viewerPosition) - { - float best = float.PositiveInfinity; - for (int i = 0; i < frame.OrderedVisibleCells.Count; i++) - { - LoadedCell? cell = cells.Find(frame.OrderedVisibleCells[i]); - if (cell is null) - continue; - float distance = Vector3.Distance( - cell.WorldTransform.Translation, - viewerPosition); - if (distance < best) - best = distance; - } - - return float.IsFinite(best) ? best : 0f; - } - private void RecycleLookInFrames() { for (int i = 0; i < _lookInFrames.Count; i++) @@ -615,22 +483,12 @@ public sealed class RetailPViewRenderer { PortalVisibilityFrame frame = _lookInFrames[frameIndex]; - // Retail enters DrawBuilding once per building and drains the - // alpha accumulated by the preceding building before punching the - // next building's portals — and because retail's far→near walk - // has only inserted FARTHER content by then, that drain can never - // composite an emitter nearer than this building - // (FlushAlphaList(0f) @0x0059F2A0 under the walk; AP-236). - // The first building uses the pre-look-in barrier in - // DrawLandscapeThroughOutsideView. + // Retail enters DrawBuilding once per building and drains every + // alpha submission accumulated by the preceding building before + // punching the next building's portals. The first building uses + // the pre-look-in barrier in DrawLandscapeThroughOutsideView. if (frameIndex > 0) - { - passes.FlushLandscapeAlphaFartherThan( - LookInBarrierDrainDistance( - frame, - ctx.Cells, - ctx.CameraWorldPosition)); - } + passes.FlushLandscapeAlpha(); // Pass 1: far-Z punch every aperture of this building. foreach (ExteriorPortalSeed seed in frame.ExteriorSeedPortals) @@ -690,8 +548,6 @@ public sealed class RetailPViewRenderer _cellStaticScratch.Add(e); } - bool cellDrewObjects = false; - _cellParticleUnionScratch.Clear(); foreach (ClipViewSlice slice in cellSlices) { int routeIndex = lookInRouteIndex++; @@ -737,21 +593,12 @@ public sealed class RetailPViewRenderer _cellStaticScratch, _oneCell); - cellDrewObjects = true; - _cellParticleUnionScratch.UnionWith( - _cellParticleOwnerScratch); + // The nested DrawCells object pass includes emitters and + // retains the exact setup_view clip until alpha playback. + passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext( + cellId, slice, _cellParticleOwnerScratch)); } } - - // The nested DrawCells object pass includes emitters: ONE - // unclipped submission per look-in cell (retail draws a - // particle during its cell's walk turn; the cell walls own - // occlusion by depth at alpha playback — never a view clip). - if (cellDrewObjects) - { - passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext( - cellId, NoClipSlice, _cellParticleUnionScratch)); - } } // The ordinary exterior building shell is clipped by the outer @@ -763,7 +610,6 @@ public sealed class RetailPViewRenderer // anchor EnvCell; never let an unrelated building repaint a // look-in merely because both happen to be nearby. int sliceIndex = 0; - _staticParticleUnionScratch.Clear(); foreach (ClipViewSlice slice in clipAssembly.OutsideViewSlices) { int shellRouteIndex = LookInBuildingShellRouteIndex( @@ -842,20 +688,14 @@ public sealed class RetailPViewRenderer _lateParticleOwnerScratch, _buildingShellScratch); } - _staticParticleUnionScratch.UnionWith( - _lateParticleOwnerScratch); + passes.DrawLandscapeStaticParticles( + ctx, + new RetailPViewLandscapeStaticParticleContext( + slice, + _lateParticleOwnerScratch)); } sliceIndex++; } - - // ONE unclipped submission for this look-in frame's shell-route - // owners (retail: one alpha-list insertion per emitter, - // depth-occluded at the flush — never re-drawn per outside view). - passes.DrawLandscapeStaticParticles( - ctx, - new RetailPViewLandscapeStaticParticleContext( - _staticParticleUnionScratch)); - _staticParticleUnionScratch.Clear(); } } @@ -953,19 +793,14 @@ public sealed class RetailPViewRenderer bool hasBuildingLookIns = _lookInFrames.Count > 0; if (hasBuildingLookIns) { - // Ownerless OUTDOOR-cell emitters cannot ride an entity route. - // Retail inserts each one into the single alpha list once, during - // its cell's landscape walk turn, with no portal-view clip; the - // interior-cell ownerless emitters submit in the final world - // scope instead (see DrawDynamicsLast). - passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true); - - _staticParticleUnionScratch.Clear(); - int outsideSliceTotal = clipAssembly.OutsideViewSlices.Length; - for (int barrierSliceIndex = 0; - barrierSliceIndex < outsideSliceTotal; - barrierSliceIndex++) + int barrierSliceIndex = 0; + foreach (var slice in clipAssembly.OutsideViewSlices) { + // Ownerless outdoor emitters cannot ride an entity route. Retail + // draws their meshes once for every installed outside_view; + // retain that slot through deferred alpha playback. + passes.DrawUnattachedSceneParticles(ctx, slice); + _lateParticleOwnerScratch.Clear(); if (partition is not null) { @@ -998,30 +833,15 @@ public sealed class RetailPViewRenderer barrierSliceIndex, 0); } - _staticParticleUnionScratch.UnionWith( - _lateParticleOwnerScratch); - } - // ONE unclipped submission for the union of every slice's cone - // survivors, then retail's pre-building barrier drain. Under - // retail's far→near walk, DrawBuilding's FlushAlphaList(0f) - // @0x0059F2A0 can only ever flush content from cells FARTHER - // than the building it precedes — a nearer emitter (the Holtburg - // candle in front of a door) has not been inserted yet and - // composites at a later flush, after that building's opaques. - // Drain the far prefix only; nearer entries stay queued for the - // DrawCells-boundary flush, which runs after the late dynamics - // (AP-236 retirement). - passes.DrawLandscapeStaticParticles( - ctx, - new RetailPViewLandscapeStaticParticleContext( - _staticParticleUnionScratch)); - _staticParticleUnionScratch.Clear(); - passes.FlushLandscapeAlphaFartherThan( - LookInBarrierDrainDistance( - _lookInFrames[0], - ctx.Cells, - ctx.CameraWorldPosition)); + passes.DrawLandscapeStaticParticles( + ctx, + new RetailPViewLandscapeStaticParticleContext( + slice, + _lateParticleOwnerScratch)); + barrierSliceIndex++; + } + passes.FlushLandscapeAlpha(); } // #124: far-building look-ins draw HERE — still inside the landscape @@ -1040,10 +860,8 @@ public sealed class RetailPViewRenderer // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn // pre-clear so the seal protects their aperture pixels; AFTER the // look-ins so a translucent portal mesh blends over a far interior - // instead of being overpainted). The scene-particle owners (statics + - // dynamics cone survivors) accumulate across the slices and submit - // ONCE, unclipped, after the loop. - _staticParticleUnionScratch.Clear(); + // instead of being overpainted) + the scene-particle owners (statics + + // dynamics cone survivors — flames ride here for the same reason). probeSliceIndex = 0; foreach (var slice in clipAssembly.OutsideViewSlices) { @@ -1071,15 +889,7 @@ public sealed class RetailPViewRenderer if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r)) { _outdoorStaticScratch.Add(e); - // Particles emit in the stage matching the PARENT CELL: - // an INTERIOR dynamic whose sphere merely straddles an - // exit-portal plane keeps its mesh in both stages (#118) - // but its particles belong to the final pass — draining - // them at the pre-clear boundary lets the interior stage - // repaint over them except on seal-protected aperture - // pixels (the cathedral middle-cell spell-star cut). - if (!InteriorEntityPartition.IsIndoorCellId(e.ParentCellId)) - _lateParticleOwnerScratch.Add(e.Id); + _lateParticleOwnerScratch.Add(e.Id); } } if (frameEntityPasses is not null) @@ -1119,75 +929,37 @@ public sealed class RetailPViewRenderer 0, ctx.PlayerLandblockId ?? 0); probeSliceIndex++; - _staticParticleUnionScratch.UnionWith(_lateParticleOwnerScratch); passes.DrawLandscapeSliceLate( ctx, new RetailPViewLandscapeLateSliceContext( slice, - _outdoorStaticScratch) + _outdoorStaticScratch, + _lateParticleOwnerScratch) { EntityDraw = entityDraw, }); } - // ONE unclipped submission for every late-stage particle owner — - // OUTDOOR-parented outside-stage dynamics' emitters plus, without - // look-ins, the outdoor statics' emitters (retail: one alpha-list - // insertion per emitter during the landscape walk; per-slice - // re-submission with clip slots was the direction-dependent - // disappearance class). Interior-parented straddlers appear in BOTH - // the LandscapeOutsideDynamic and DynamicLast routes; their particles - // emit only in the final pass, so remove them here. - if (frameEntityPasses is not null) - { - RenderFrameRouteOwnerSelector.ExceptRoute( - _staticParticleUnionScratch, - in frameView, - RenderFrameCandidateRoute.DynamicLast); - } - if (_staticParticleUnionScratch.Count > 0) - { - passes.DrawLandscapeStaticParticles( - ctx, - new RetailPViewLandscapeStaticParticleContext( - _staticParticleUnionScratch)); - _staticParticleUnionScratch.Clear(); - } - // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, // campfires, ground effects anchored at a position) have no owner id - // to ride any of the id-filtered particle passes. OUTDOOR-cell ones - // submit ONCE in the landscape stage, unclipped — retail inserts each - // particle into the single alpha list during its owner cell's walk - // turn (ShouldDrawParticles @0x0050FE60 gates by cell + distance; - // FlushAlphaList @0x0059D2E0 depth-tests at composition). The former - // once-per-outside-slice submission with that slice's clip slot cut - // effects at aperture boundaries and drew NOTHING when no outside - // slice was in view. Interior-cell unattached emitters submit in the - // final world scope (DrawDynamicsLast) — in the landscape stage the - // upcoming depth clear + interior repaint would erase them. + // to ride any of the id-filtered particle passes. Draw once per + // installed outside_view for BOTH root kinds, matching retail's + // landscape-stage placement and preserving the slot in each deferred + // draw. The former outdoor-root post-world tail ran after building + // cells and let exterior alpha repaint the cathedral transition. + // With no look-ins they drain at the end of the landscape stage; the + // look-in path submits them at its pre-building barrier so later opaque + // cell floors can cover them. if (!hasBuildingLookIns) - passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true); + { + foreach (ClipViewSlice slice in clipAssembly.OutsideViewSlices) + passes.DrawUnattachedSceneParticles(ctx, slice); + } // Retail PView::DrawCells 0x005A4872 drains the landscape alpha list // immediately after LScape::draw and before the optional depth clear. // The queue remains active for the post-clear/final-world scope. - // - // Only an INTERIOR root drains here: its full depth clear follows, and - // a flame drained after that clear would z-pass through every interior - // wall. An OUTDOOR root has no depth clear (retail gates it on - // portalsDrawnCount, pc:432731), and retail's LScape::draw walk has - // already drawn every building interior and every cell object via - // DrawSortCell 0x005A17C0 before that boundary — while our outdoor - // frame draws punches, interior shells, cell objects, and ALL dynamics - // (doors, creatures, NPCs) after this point. Draining here painted the - // flames first and let each of those later opaque meshes overwrite - // them (#132: "the door draws over the candle"); the outdoor drain - // therefore runs after DrawDynamicsLast, where world depth is complete - // and the one far-to-near list composites over everything, exactly as - // retail's boundary flush does relative to its finished walk. - if (!ctx.RootCell.IsOutdoorNode) - passes.FlushLandscapeAlpha(); + passes.FlushLandscapeAlpha(); // T1: retail clears the FULL depth buffer ONCE between the outside // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 — @@ -1360,13 +1132,13 @@ public sealed class RetailPViewRenderer Array.Empty(), visibleCellIds: null); - // Particles emit exactly once, in the stage matching the parent - // cell. Pure-outdoor dynamics are absent from the DynamicLast - // route (they draw only in the outside stage), and interior - // straddlers — present in BOTH routes — emit their particles - // HERE so the interior stage cannot repaint over them; the late - // landscape submission excludes DynamicLast owners for the same - // reason. + // An owner routed through any pre-clear outside slice already had + // its alpha particles drawn there. Meshes may be submitted in both + // stages, but particles must be emitted exactly once. + RenderFrameRouteOwnerSelector.ExceptRoute( + _dynamicParticleOwnerScratch, + in frameView, + RenderFrameCandidateRoute.LandscapeOutsideDynamic); if (_dynamicParticleOwnerScratch.Count > 0) { passes.DrawDynamicsParticles( @@ -1403,14 +1175,6 @@ public sealed class RetailPViewRenderer $"cell=0x{(e.ParentCellId ?? 0):X8} indoor=False rootOutdoor={rootIsOutdoor} -> CULLED(outside-stage)"); continue; } - // Drawn-once (retail DrawMeshInternal @0x0059F360 marks every - // non-player part for the frame): a dynamic whose cell drew as a - // building LOOK-IN already rendered with that cell inside the - // landscape stage (#131). Redrawing it here would land AFTER the - // boundary alpha drain and overpaint nearer flames — the Holtburg - // door repainting the candle in front of it. - if (indoor && _lookInCellIds.Contains(e.ParentCellId!.Value)) - continue; bool visible = indoor ? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r) : viewcone.SphereVisibleOutside(c, r); @@ -1457,28 +1221,22 @@ public sealed class RetailPViewRenderer // particles must not double-draw, unlike the depth-idempotent meshes). if (frameEntityPasses is not null) { - // Parent-cell stage split: every DynamicLast owner emits its - // particles here. Pure-outdoor dynamics are absent from this - // route (outside stage only), and interior straddlers — whose - // meshes drew in both stages — must emit HERE so the interior - // stage cannot repaint over them (matches the production - // partition-null path above). RenderFrameRouteOwnerSelector.Replace( _dynamicParticleOwnerScratch, in frameView, RenderFrameCandidateRoute.DynamicLast, 0, 0); + RenderFrameRouteOwnerSelector.ExceptRoute( + _dynamicParticleOwnerScratch, + in frameView, + RenderFrameCandidateRoute.LandscapeOutsideDynamic); } else { _dynamicParticleOwnerScratch.Clear(); - // Interior-parented dynamics — INCLUDING exit-portal straddlers - // whose mesh also drew in the outside stage — emit particles in - // this final pass; outdoor-parented ones emitted in the late - // landscape submission (parent-cell stage split). foreach (var e in _dynamicsScratch) - if (InteriorEntityPartition.IsIndoorCellId(e.ParentCellId)) + if (!_outsideStageDynamics.Contains(e)) _dynamicParticleOwnerScratch.Add(e.Id); } if (_dynamicParticleOwnerScratch.Count > 0) @@ -1870,29 +1628,10 @@ public interface IRetailPViewPassExecutor RetailPViewFrameInput frame, RetailPViewCellSliceContext context, int portalIndex); - /// - /// One unclipped submission for every renderable UNATTACHED emitter whose - /// owner cell matches the scope: outdoor landcells in the landscape stage, - /// interior EnvCells in the final world scope. Retail inserts each such - /// particle into the single alpha list during its owner cell's walk turn - /// and never clips it to a portal view. - /// void DrawUnattachedSceneParticles( RetailPViewFrameInput frame, - bool outdoorCells); + ClipViewSlice slice); void FlushLandscapeAlpha(); - - /// - /// Pre/inter-building barrier drain: composites only the queued alpha at - /// or beyond and retains nearer - /// entries for the later boundary flush — retail's far→near walk outcome - /// (DrawBuilding's FlushAlphaList(0f) @0x0059F2A0 can only ever flush - /// content from cells farther than that building; AP-236). The default - /// falls back to a full flush so non-production executors keep today's - /// behavior until they opt in. - /// - void FlushLandscapeAlphaFartherThan(float minViewerDistance) => - FlushLandscapeAlpha(); void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context); void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlySet ownerIds); void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result); @@ -2169,23 +1908,6 @@ public sealed class RetailPViewFrameResult public PortalVisibilityFrame PortalFrame { get; private set; } = null!; public ClipFrameAssembly ClipAssembly { get; private set; } = null!; public HashSet DrawableCells { get; private set; } = null!; - - /// - /// Every cell this completed view actually reached: the main flood - /// () plus the building look-in cells. This is - /// retail's per-cell in_view answer for effect consumers — - /// CPhysicsObj::ShouldDrawParticles @0x0050FE60 gates on - /// cell->IsInView(), and a cell entered through a building portal - /// (PView::ConstructView @0x005A57B0, installed by - /// RenderDeviceD3D::DrawBuilding @0x0059F2A0) is drawn by the same - /// PView::DrawCells traversal as a flooded cell, so retail marks it - /// in view identically. acdream's look-in adaptation keeps those cells out - /// of (seals / outside-stage predicate stay - /// main-flood scoped, #124); particle and light visibility must consume - /// THIS set or look-in rooms render with frozen emitters and dark lights. - /// - public HashSet InViewCells { get; private set; } = null!; - internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; } internal RenderProjectionCounts SourceCounts { get; private set; } internal InteriorEntityPartition.Result? DiagnosticPartition @@ -2195,7 +1917,6 @@ public sealed class RetailPViewFrameResult PortalVisibilityFrame portalFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells, - HashSet inViewCells, RenderFrameDiagnosticCounts diagnosticCounts, RenderProjectionCounts sourceCounts, InteriorEntityPartition.Result? diagnosticPartition) @@ -2203,7 +1924,6 @@ public sealed class RetailPViewFrameResult PortalFrame = portalFrame; ClipAssembly = clipAssembly; DrawableCells = drawableCells; - InViewCells = inViewCells; DiagnosticCounts = diagnosticCounts; SourceCounts = sourceCounts; DiagnosticPartition = diagnosticPartition; @@ -2219,7 +1939,6 @@ public sealed class RetailPViewFrameResult portalFrame, clipAssembly, drawableCells, - drawableCells, RetailPViewRenderer.LegacyDiagnosticCounts( diagnosticPartition), RetailPViewRenderer.LegacySourceCounts( @@ -2235,13 +1954,12 @@ public readonly record struct RetailPViewLandscapeSliceContext( } /// -/// Scene-particle owners for ONE unclipped landscape-stage submission (the -/// union of every outside slice's cone survivors). Mesh alpha for the same -/// owners is already queued by the entity routes; retail inserts each -/// emitter's polys into the single alpha list once, during its owner cell's -/// walk turn, with no portal-view clip. +/// Outdoor-static emitters submitted at retail's pre-building alpha barrier. +/// Mesh alpha for the same owners is already queued by the early landscape +/// entity route. /// public readonly record struct RetailPViewLandscapeStaticParticleContext( + ClipViewSlice Slice, IReadOnlySet ParticleOwnerIds); /// Retail DrawBuilding's ordinary exterior-shell pass, issued after @@ -2258,7 +1976,8 @@ public readonly record struct RetailPViewLandscapeBuildingShellSliceContext( /// submitted at a pre-building barrier. public readonly record struct RetailPViewLandscapeLateSliceContext( ClipViewSlice Slice, - IReadOnlyList Dynamics) + IReadOnlyList Dynamics, + IReadOnlySet ParticleOwnerIds) { internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } } diff --git a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs index 10bb06e3..61f2d3e7 100644 --- a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs +++ b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs @@ -1128,12 +1128,6 @@ internal sealed class RenderScenePViewFrameBuilder private RenderProjectionRecord[] _cell = []; private RenderProjectionRecord[] _dirty = []; private RenderProjectionRecord[] _survivors = []; - - // Cells drawn as building look-ins this frame — the DynamicLast route - // honors retail's drawn-once contract (DrawMeshInternal @0x0059F360 - // marks every non-player part): an object whose cell drew with a look-in - // must not enter the final dynamics route again. - private readonly HashSet _lookInCellScratch = new(); private RenderProjectionRecord[] _cellRoute = []; private readonly Dictionary _outdoorPositions = []; @@ -1534,16 +1528,6 @@ internal sealed class RenderScenePViewFrameBuilder RenderFrameWriter writer, in RenderScenePViewBuildInput input) { - _lookInCellScratch.Clear(); - for (int frameIndex = 0; - frameIndex < input.LookInFrames.Count; - frameIndex++) - { - PortalVisibilityFrame frame = input.LookInFrames[frameIndex]; - for (int i = 0; i < frame.OrderedVisibleCells.Count; i++) - _lookInCellScratch.Add(frame.OrderedVisibleCells[i]); - } - int count = 0; EnsureCapacity(ref _survivors, _dynamicCount); for (int i = 0; i < _dynamicCount; i++) @@ -1555,14 +1539,6 @@ internal sealed class RenderScenePViewFrameBuilder if (!input.RootIsOutdoor && !indoor) continue; - // Drawn-once (retail DrawMeshInternal @0x0059F360): a dynamic - // whose cell drew as a building look-in already rendered with - // that cell in the landscape stage; re-entering the final route - // would draw it after the boundary alpha drain and overpaint - // nearer flames (the Holtburg candle-behind-door class). - if (indoor && _lookInCellScratch.Contains(parentCellId!.Value)) - continue; - Sphere(in record, out Vector3 center, out float radius); bool visible = indoor ? input.Viewcone.SphereVisibleInCell( diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index 0bfc9210..aec455b6 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Numerics; using AcDream.Core.Meshing; @@ -53,17 +52,15 @@ public sealed partial class SkyRenderer : IDisposable // Lazily-built GPU resources per sky-GfxObj. private readonly Dictionary> _gpuByGfxObj = new(); - // Retail advances animated texture coordinates from Timer::cur_time - // deltas in CPhysics::UseTime. Stopwatch is the matching monotonic clock: - // unlike wall time, OS clock synchronization cannot make rain/cloud UVs - // jump forward or backward. - private readonly long _animationStartedAtTimestamp = Stopwatch.GetTimestamp(); + // When did we start running — used to accumulate TexVelocityX/Y over + // real time (independent of the day-fraction clock). + private readonly DateTime _startedAt = DateTime.UtcNow; /// /// Campaign V slice V7: pins the sky's scroll phase to a fixed number of - /// seconds instead of advancing the live animation clock, so two launches agree. + /// seconds instead of reading the wall clock, so two launches agree. /// null — the default, and what every ordinary run gets — keeps the - /// monotonic real-elapsed-time clock. + /// wall clock. /// /// Why the sky needs its own pin when the world clock is already /// pinnable. Two independent clocks drive this renderer. The Dereth clock @@ -242,9 +239,7 @@ public sealed partial class SkyRenderer : IDisposable var replaces = PickReplaces(group, dayFraction); float secondsSinceStart = AnimationPhaseSecondsOverride - ?? ElapsedAnimationSeconds( - _animationStartedAtTimestamp, - Stopwatch.GetTimestamp()); + ?? (float)(DateTime.UtcNow - _startedAt).TotalSeconds; for (int i = 0; i < group.SkyObjects.Count; i++) { @@ -459,9 +454,6 @@ public sealed partial class SkyRenderer : IDisposable } } - internal static float ElapsedAnimationSeconds(long startTimestamp, long currentTimestamp) - => (float)Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp).TotalSeconds; - /// /// Campaign V slice V6e: the table slot for one (texture, wrap-mode) pair, /// interning a resident bindless handle on first use. diff --git a/src/AcDream.App/Rendering/SkyPesFrameController.cs b/src/AcDream.App/Rendering/SkyPesFrameController.cs index 64304022..ab087831 100644 --- a/src/AcDream.App/Rendering/SkyPesFrameController.cs +++ b/src/AcDream.App/Rendering/SkyPesFrameController.cs @@ -43,9 +43,6 @@ namespace AcDream.App.Rendering; /// internal sealed class SkyPesFrameController { - private static readonly Matrix4x4[] IdentityPartPose = - [Matrix4x4.Identity]; - private readonly record struct SkyPesKey( int ObjectIndex, uint GfxObjId, @@ -125,23 +122,12 @@ internal sealed class SkyPesFrameController ? ParticleRenderPass.SkyPostScene : ParticleRenderPass.SkyPreScene; _particles.SetEntityRenderPass(ownerId, renderPass); - // The sky cell follows the viewer. Keep the script dispatch - // anchor on that same current-frame pose: SoundTweaked hooks in - // the Rainy carriers are ordinary world sounds, and a stale - // creation-time anchor falls beyond retail's audible radius as - // soon as login/teleport/movement displaces the camera. - _scripts.SetOwnerAnchor(ownerId, cameraWorldPosition); Quaternion rotation = Rotation(skyObject, dayFraction); _poses.Publish( ownerId, Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(cameraWorldPosition), - // Dereth's scripted sky carriers (including lightning Setup - // 0x02000BA6) are one-part dummy anchors whose default part-0 - // frame is identity. Their CreateParticle hooks target part - // 0, not the -1 root sentinel, so a root-only synthetic pose - // makes a live carrier look pose-less to ParticleHookSink. - IdentityPartPose, + Array.Empty(), cellId: 0u); if (_active.Contains(key) || _missing.Contains(key)) diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs index bf51a132..1e22472b 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs @@ -264,8 +264,11 @@ public sealed unsafe partial class EnvCellRenderer { MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex]; int groupIndex = drawRange.GroupIndex; - CullMode cullMode = ResolveRetailCellShellCullMode( - (CullMode)(groupIndex % 4)); + var cullMode = (CullMode)(groupIndex % 4); + // Phase A8 visual-gate evidence: cell meshes use CullMode.Landblock + // uniformly, but the room surfaces need to be visible from inside. + // Render cell polys double-sided, exactly as the GL arm does. + if (cullMode == CullMode.Landblock) cullMode = CullMode.None; bool isAdditive = groupIndex >= 4; IGpuPipeline rangeBasePipeline = isAdditive @@ -358,8 +361,9 @@ public sealed unsafe partial class EnvCellRenderer for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++) { MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex]; - CullMode cullMode = ResolveRetailCellShellCullMode( - (CullMode)(drawRange.GroupIndex % 4)); + var cullMode = (CullMode)(drawRange.GroupIndex % 4); + if (cullMode == CullMode.Landblock) + cullMode = CullMode.None; SetCullMode(encoder, cullMode); pushConstants.DrawIdOffset = drawRange.FirstCommand; encoder.SetPushConstants(in pushConstants); @@ -413,25 +417,6 @@ public sealed unsafe partial class EnvCellRenderer } } - /// - /// Resolves a CellStruct polygon's DAT sides_type to the render - /// state used by retail's constructed EnvCell mesh. The similarly named - /// values on Polygon.SidesType are not GPU - /// cull states: 0 emits the positive face, 1 emits that face twice with - /// reversed indices, and 2 emits the positive and negative surface. - /// D3DPolyRender::ConstructMesh @ 0x0059DFA0 performs that geometry - /// expansion, then every subset is drawn with D3DCULL_CW through - /// RenderMeshSubset @ 0x0059CA10. - /// already performs the identical expansion, so every shell batch must - /// cull clockwise here. Returning for DAT 0 - /// was #178's Phase-A8 double-sided stopgap. - /// - internal static CullMode ResolveRetailCellShellCullMode(CullMode sidesType) - { - _ = sidesType; - return CullMode.Clockwise; - } - /// /// Reserves this frame's ring, copies into it, and binds the slice. A /// logically empty section still reserves one element so the bound range is diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs index 2d9fac4b..30b22946 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs @@ -924,24 +924,6 @@ public sealed unsafe partial class WbDrawDispatcher return new RhiSection(allocation.Buffer, allocation.OffsetBytes, (uint)byteCount); } - /// - /// #443 — the next classic draw is a PRIVATE pass (paperdoll, appraisal, - /// chargen preview) and must take the plain ring transform path with - /// firstInstance = 0, never an append into the shared world - /// transform frame. The default mesh shaders index every parallel - /// per-instance array (clip slots, light sets, indoor, opacity, selection, - /// detail category) zero-based — only the packed world submission's - /// shader convention subtracts a shared-arena prefix — so an - /// arena-appended classic draw with a non-zero base reads zeroed - /// per-instance data (opacity 0 ⇒ an invisible doll whenever a world - /// frame is active; portal space worked only because the arena was - /// inactive there). The private pass owns its own camera, lighting, and - /// target; per the self-contained-render-state rule it must not depend on - /// the world frame's pose address space at all. Consumed and cleared by - /// the next . - /// - internal bool NextClassicDrawIsPrivatePass; - private RhiSection WriteWorldTransformSection( IGpuFrame frame, ReadOnlySpan matrixFloats, @@ -957,9 +939,7 @@ public sealed unsafe partial class WbDrawDispatcher ObserveOrdinaryTransformDemand( frame.Serial, checked((uint)(matrixFloats.Length / 16))); - bool privatePass = NextClassicDrawIsPrivatePass; - NextClassicDrawIsPrivatePass = false; - if (privatePass || !_worldTransformFrames.IsActive) + if (!_worldTransformFrames.IsActive) { firstInstance = 0; return WriteRingSection(frame, matrixFloats); diff --git a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs index 19371426..5426231e 100644 --- a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs +++ b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs @@ -462,7 +462,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation private readonly SceneLightingUboBinding? _lightingUbo; private readonly IWorldRenderRangeSource _ranges; private readonly SkyPesFrameController? _skyPes; - private readonly Func _persistentDaylight; private readonly HashSet _visibleCells = []; private bool _visibleCellsValid; @@ -474,8 +473,7 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation EnvCellRenderer? environmentCells, SceneLightingUboBinding? lightingUbo, IWorldRenderRangeSource ranges, - SkyPesFrameController? skyPes, - Func? persistentDaylight = null) + SkyPesFrameController? skyPes) { _options = options ?? throw new ArgumentNullException(nameof(options)); _worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime)); @@ -485,7 +483,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation _lightingUbo = lightingUbo; _ranges = ranges ?? throw new ArgumentNullException(nameof(ranges)); _skyPes = skyPes; - _persistentDaylight = persistentDaylight ?? (static () => false); } public void Prepare( @@ -503,14 +500,7 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation activeDayGroup, camera.Position); - // LScape::set_landscape_lighting @0x005054D0 keeps the live sky/fog - // clock but, when PersistentAtDay is set, asks the active region for - // lighting at exactly 0.5 (noon). Do not pin WorldTime.DayFraction: - // clouds, celestial objects, fog, and scripts must keep advancing. - SkyKeyframe landscapeLighting = _persistentDaylight() - ? _worldTime.SkyAtDayFraction(0.5f) - : foundation.Sky; - UpdateSunFromSky(landscapeLighting, roots.PlayerInsideCell); + UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell); _lighting.UpdateViewerLight(roots.PlayerViewPosition); _lighting.Tick(camera.Position); _lighting.BuildPointLightSnapshot( diff --git a/src/AcDream.App/Rendering/WorldSceneRenderer.cs b/src/AcDream.App/Rendering/WorldSceneRenderer.cs index 39ea601d..9d7ec0d1 100644 --- a/src/AcDream.App/Rendering/WorldSceneRenderer.cs +++ b/src/AcDream.App/Rendering/WorldSceneRenderer.cs @@ -254,14 +254,8 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase camera.Camera.View, _diagnostics.CameraCellResolution)); - // Effect visibility consumes InViewCells (main flood ∪ look-in - // cells), not the flood-only DrawableCells: retail's - // ShouldDrawParticles @0x0050FE60 asks cell->IsInView(), and a - // look-in cell drawn via DrawBuilding @0x0059F2A0 is in view - // exactly like a flooded cell. Flood-only scoping froze - // emitters and darkened lights in visible adjacent rooms. - _particleVisibility.MarkVisibleCells(pviewResult.InViewCells); - _frames.ObserveDrawableCells(pviewResult.InViewCells); + _particleVisibility.MarkVisibleCells(pviewResult.DrawableCells); + _frames.ObserveDrawableCells(pviewResult.DrawableCells); _diagnostics.EmitPViewInput( pviewResult.PortalFrame, camera.ViewProjection, diff --git a/src/AcDream.App/RuntimeOptions.cs b/src/AcDream.App/RuntimeOptions.cs index fa0ad2b0..41790ed5 100644 --- a/src/AcDream.App/RuntimeOptions.cs +++ b/src/AcDream.App/RuntimeOptions.cs @@ -173,13 +173,7 @@ public sealed record RuntimeOptions( // Legacy override for ACDREAM_STREAM_RADIUS. Caller applies it on // top of the quality preset's radii. Null when unset or invalid. LegacyStreamRadius: TryParseNonNegativeInt(env("ACDREAM_STREAM_RADIUS")), - // The retained retail UI is the product's only presentation - // stack. It is therefore default-on for every launch path; - // literal 0 remains an explicit diagnostic/headless opt-out. - RetailUi: !string.Equals( - env("ACDREAM_RETAIL_UI"), - "0", - StringComparison.Ordinal), + RetailUi: IsExactlyOne(env("ACDREAM_RETAIL_UI")), OpenCharacterCreationOnStart: IsExactlyOne(env("ACDREAM_OPEN_CHARGEN")), AcDir: NullIfEmpty(env("ACDREAM_AC_DIR")), @@ -299,6 +293,14 @@ public sealed record RuntimeOptions( PreparedAssetEffectiveRecipeVersion = content?.PreparedAssetEffectiveRecipeVersion, LiveMode = true, + // Campaign LA gate round 2: a session-config launch IS a product + // launch — the retail UI is the shipped UI, not a dev option. + // ACDREAM_RETAIL_UI remains the opt-in for env-var dev launches, + // but the launcher strips ACDREAM_* from children (LA11 isolation), + // so inheriting the env default here shipped a client with world + // rendering and NO interface at all — the guiSelect flow's + // character screen included. + RetailUi = true, LiveHost = session.Endpoint.Host, LivePort = session.Endpoint.Port, LiveUser = session.Account, diff --git a/src/AcDream.App/Settings/RuntimeSettingsController.cs b/src/AcDream.App/Settings/RuntimeSettingsController.cs index ef12d68f..7838812d 100644 --- a/src/AcDream.App/Settings/RuntimeSettingsController.cs +++ b/src/AcDream.App/Settings/RuntimeSettingsController.cs @@ -161,18 +161,11 @@ internal sealed class RuntimeSettingsController : Func? characterOptionValue = null) { _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - Display = _storage.LoadDisplay(); - // Render.LandscapeDrawDistance is one of retail's actual quality - // dimensions, not a menu index. Apply it to the production far tier - // before environment overrides; ACDREAM_FAR_RADIUS therefore keeps - // its documented highest precedence for diagnostic runs. - _resolveQuality = resolveQuality - ?? (preset => ResolveQuality( - preset, - Display.LandscapeDrawDistance)); + _resolveQuality = resolveQuality ?? ResolveQuality; _log = log ?? Console.WriteLine; _characterOptionValue = characterOptionValue; + Display = _storage.LoadDisplay(); Audio = _storage.LoadAudio(); Chat = _storage.LoadChat(); _defaultCharacter = _storage.LoadCharacter(DefaultToonKey); @@ -620,35 +613,6 @@ internal sealed class RuntimeSettingsController : /// public void NotifyServerOptionsSeeded() => ServerOptionsSeeded?.Invoke(); - private static QualitySettings ResolveQuality( - QualityPreset preset, - int landscapeDrawDistance) - { - QualitySettings quality = ApplyLandscapeDrawDistance( - QualitySettings.From(preset), - landscapeDrawDistance); - return QualitySettings.WithEnvOverrides(quality); - } - - internal static QualitySettings ApplyLandscapeDrawDistance( - QualitySettings quality, - int landscapeDrawDistance) - { - // Retail's enum contains 3,5,8,11,15,25 and @render accepts every - // integer in [5,25]. Values outside the union's structural [3,25] - // range can only come from an old/corrupt settings file, so preserve - // the preset rather than constructing an invalid streaming window. - if (landscapeDrawDistance is >= 3 and <= 25) - { - quality = quality with - { - NearRadius = Math.Min( - quality.NearRadius, - landscapeDrawDistance), - FarRadius = landscapeDrawDistance, - }; - } - - return quality; - } + private static QualitySettings ResolveQuality(QualityPreset preset) => + QualitySettings.WithEnvOverrides(QualitySettings.From(preset)); } diff --git a/src/AcDream.App/UI/ClientCommandController.cs b/src/AcDream.App/UI/ClientCommandController.cs index 532f1184..0b34596e 100644 --- a/src/AcDream.App/UI/ClientCommandController.cs +++ b/src/AcDream.App/UI/ClientCommandController.cs @@ -3,7 +3,6 @@ using AcDream.Core.Chat; using AcDream.Core.Physics; using AcDream.Core.Ui; using AcDream.Core.Social; -using AcDream.Runtime.Chat; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Panels.Chat; @@ -16,50 +15,6 @@ namespace AcDream.App.UI; /// public sealed class ClientCommandController { - /// - /// Typed network effects used by retail's allegiance/house management - /// command family. Grouped separately so the main command binding remains - /// readable and every operation keeps a semantic name instead of exposing - /// raw opcodes to the UI layer. - /// - public sealed record AdministrationBindings( - Action BreakAllegianceBoot, - Action AllegianceChatBoot, - Action AllegianceChatGag, - Action AllegianceBroadcast, - Action ListAllegianceBans, - Action AddAllegianceBan, - Action RemoveAllegianceBan, - Action ListAllegianceOfficers, - Action ClearAllegianceOfficers, - Action SetAllegianceOfficer, - Action RemoveAllegianceOfficer, - Action ListAllegianceOfficerTitles, - Action ClearAllegianceOfficerTitles, - Action SetAllegianceOfficerTitle, - Action QueryAllegianceName, - Action SetAllegianceName, - Action ClearAllegianceName, - Action AllegianceLockAction, - Action SetAllegianceApprovedVassal, - Action AllegianceHouseAction, - Action QueryMotd, - Action SetMotd, - Action ClearMotd, - Action SetOpenHouseStatus, - Action AddPermanentGuest, - Action RemovePermanentGuest, - Action RemoveAllPermanentGuests, - Action ChangeStoragePermission, - Action AddAllStoragePermission, - Action RemoveAllStoragePermission, - Action RequestFullGuestList, - Action BootSpecificHouseGuest, - Action BootEveryone, - Action SetHooksVisibility, - Action ModifyAllegianceGuestPermission, - Action ModifyAllegianceStoragePermission); - public sealed record Bindings( Action TeleportToLifestone, Action TeleportToMarketplace, @@ -72,7 +27,6 @@ public sealed class ClientCommandController Action ToggleFrameRate, Action ToggleUiLock, Action ShowSystemMessage, - Action ShowClientLocalMessage, Action ShowWeenieError, Func PlayerPublicWeenieBitfield, Func ClientVersion, @@ -122,63 +76,13 @@ public sealed class ClientCommandController Action LeaveGmChannel, Action RecallAllegianceHometown, Action RequestAllegianceInfo, - Action AbandonHouse, - AdministrationBindings Administration, - Func IsPersistentDaylight, - Action SetPersistentDaylight, - Action SetLandscapeRadius, - Action SetFieldOfView); + Action AbandonHouse); private readonly Bindings _bindings; - private readonly RetailAdministrationCommandDispatcher _administration; public ClientCommandController(Bindings bindings) { _bindings = bindings ?? throw new ArgumentNullException(nameof(bindings)); - AdministrationBindings actions = bindings.Administration; - _administration = new RetailAdministrationCommandDispatcher( - new RetailAdministrationCommandDispatcher.FeedbackBindings( - bindings.ShowSystemMessage, - bindings.ShowClientLocalMessage, - bindings.SetSingleCharacterOption, - bindings.RequestAllegianceInfo), - new RetailAdministrationCommandDispatcher.ActionBindings( - actions.BreakAllegianceBoot, - actions.AllegianceChatBoot, - actions.AllegianceChatGag, - actions.AllegianceBroadcast, - actions.ListAllegianceBans, - actions.AddAllegianceBan, - actions.RemoveAllegianceBan, - actions.ListAllegianceOfficers, - actions.ClearAllegianceOfficers, - actions.SetAllegianceOfficer, - actions.RemoveAllegianceOfficer, - actions.ListAllegianceOfficerTitles, - actions.ClearAllegianceOfficerTitles, - actions.SetAllegianceOfficerTitle, - actions.QueryAllegianceName, - actions.SetAllegianceName, - actions.ClearAllegianceName, - actions.AllegianceLockAction, - actions.SetAllegianceApprovedVassal, - actions.AllegianceHouseAction, - actions.QueryMotd, - actions.SetMotd, - actions.ClearMotd, - actions.SetOpenHouseStatus, - actions.AddPermanentGuest, - actions.RemovePermanentGuest, - actions.RemoveAllPermanentGuests, - actions.ChangeStoragePermission, - actions.AddAllStoragePermission, - actions.RemoveAllStoragePermission, - actions.RequestFullGuestList, - actions.BootSpecificHouseGuest, - actions.BootEveryone, - actions.SetHooksVisibility, - actions.ModifyAllegianceGuestPermission, - actions.ModifyAllegianceStoragePermission)); } public void Execute(ExecuteClientCommandCmd command) @@ -242,23 +146,6 @@ public sealed class ClientCommandController case ClientCommandId.ToggleFrameRate: _bindings.ToggleFrameRate(); break; - // ClientCommunicationSystem::DoDay @0x005706F0. Retail toggles - // LScape::m_fAlwaysDaylight, then writes the same value to the - // PersistentAtDay character option and prints one exact line. - case ClientCommandId.TogglePersistentDaylight: - { - bool enabled = !_bindings.IsPersistentDaylight(); - _bindings.SetPersistentDaylight(enabled); - _bindings.ShowSystemMessage(enabled - ? "Let there be light!" - : "Normality has been restored."); - break; - } - // ClientCommunicationSystem::DoRenderOption @0x0057E120 -> - // GraphicsOptions::HandleRenderOption @0x00455C30. - case ClientCommandId.RenderOption: - ExecuteRenderOption(command.Arguments); - break; // DoLockUI @ 0x005703B0 toggles PlayerModule::LockUI and // broadcasts the new state to every UI element. case ClientCommandId.ToggleUiLock: @@ -454,29 +341,9 @@ public sealed class ClientCommandController case ClientCommandId.AllegianceHometown: _bindings.RecallAllegianceHometown(); break; - // ClientCommunicationSystem::DoAllegiance/DoHouse management - // family. The shared Runtime dispatcher keeps graphical and - // headless grammar/refusal behavior identical. + // GameActionAllegianceInfoRequest — "@allegiance info [name]". case ClientCommandId.AllegianceInfo: - case ClientCommandId.AllegianceBoot: - case ClientCommandId.AllegianceBan: - case ClientCommandId.AllegianceChat: - case ClientCommandId.AllegianceBroadcast: - case ClientCommandId.AllegianceOfficer: - case ClientCommandId.AllegianceOfficerTitle: - case ClientCommandId.AllegianceName: - case ClientCommandId.AllegianceLock: - case ClientCommandId.AllegianceHouse: - case ClientCommandId.AllegianceMotd: - case ClientCommandId.AllegianceUnrecognizedSubcommand: - case ClientCommandId.HouseOpenStatus: - case ClientCommandId.HouseStorage: - case ClientCommandId.HouseBoot: - case ClientCommandId.HouseBootAll: - case ClientCommandId.HouseGuests: - case ClientCommandId.HouseHooks: - case ClientCommandId.HouseUnrecognizedSubcommand: - _ = _administration.TryExecute(command.Command, command.Arguments); + _bindings.RequestAllegianceInfo(command.Arguments.Trim()); break; // GameActionHouseAbandon — "@house abandon". Retail's abandon // branch (DoHouse @ 0x00580D58) opens a FIRST confirmation @@ -517,7 +384,6 @@ public sealed class ClientCommandController } } - private void ExecuteUiProfile(string arguments, bool save) { string[] parts = SplitArguments(arguments); @@ -539,94 +405,6 @@ public sealed class ClientCommandController else _bindings.LoadUi(name); } - /// - /// Exact retail GraphicsOptions::HandleRenderOption @0x00455C30 - /// surface. It has only two options, ignores surplus argv entries, uses - /// C atoi semantics for the value, and silently accepts an unknown - /// option name. - /// - private void ExecuteRenderOption(string arguments) - { - string[] parts = SplitArguments(arguments); - if (parts.Length == 0 - || parts[0].Equals("usage", StringComparison.OrdinalIgnoreCase)) - { - _bindings.ShowSystemMessage(RetailCommandHelpTable.Render.TrimEnd('\n')); - return; - } - - if (parts[0].Equals("radius", StringComparison.OrdinalIgnoreCase)) - { - if (parts.Length < 2) - { - _bindings.ShowSystemMessage("Must specify a radius"); - return; - } - - int radius = RetailAtoi(parts[1]); - if (radius is < 5 or > 25) - { - _bindings.ShowSystemMessage("Radius must be between 5 and 25"); - return; - } - - _bindings.SetLandscapeRadius(radius); - _bindings.ShowSystemMessage("Landscape radius set"); - return; - } - - if (!parts[0].Equals("fov", StringComparison.OrdinalIgnoreCase)) - return; - - if (parts.Length < 2) - { - _bindings.ShowSystemMessage("Must specify a field of view"); - return; - } - - int fieldOfView = RetailAtoi(parts[1]); - if (fieldOfView is < 10 or > 160) - { - _bindings.ShowSystemMessage( - "Field of view must be between 10 and 160"); - return; - } - - _bindings.SetFieldOfView(fieldOfView); - _bindings.ShowSystemMessage("Field of view set"); - } - - private static int RetailAtoi(string value) - { - if (string.IsNullOrEmpty(value)) - return 0; - - int index = 0; - int sign = 1; - if (value[0] is '+' or '-') - { - if (value[0] == '-') - sign = -1; - index++; - } - - long result = 0; - bool sawDigit = false; - while (index < value.Length && value[index] is >= '0' and <= '9') - { - sawDigit = true; - result = Math.Min( - (long)int.MaxValue + (sign < 0 ? 1L : 0L), - result * 10L + (value[index] - '0')); - index++; - } - - if (!sawDigit) - return 0; - long signed = sign < 0 ? -result : result; - return (int)Math.Clamp(signed, int.MinValue, int.MaxValue); - } - private bool RequireNoArguments(string arguments, string usage) { if (string.IsNullOrWhiteSpace(arguments)) return true; diff --git a/src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs b/src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs index a341ac2c..40bac999 100644 --- a/src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs +++ b/src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs @@ -306,9 +306,41 @@ internal sealed class CharacterCreationSkillsPage : IDisposable _infoTitle = UiElement.FindDescendant(pageRoot, 0x100003FBu) as UiText; _infoText = UiElement.FindDescendant(pageRoot, 0x100003FCu) as UiText; - // The two panes author no 0x15. The shared importer now applies - // retail's constructor default (raw 4 -> Top), so no page-local - // justification correction is needed. + // R3-3 (Campaign CC gate round 1 re-test 2): the title + // (0x100003fb, Y=435, Height=100) and description (0x100003fc, + // Y=460, Height=100) panes' own AUTHORED boxes overlap by 75px + // (live-DAT-measured) — retail relies on vertical JUSTIFICATION, + // not disjoint rects, to keep the two visually separate. Neither + // element authors dat property 0x15 (live-DAT-probe-confirmed + // absent on both), so both fall to whatever the unauthored default + // resolves to. Byte-traced against retail's own + // UIElement_Text::UIElement_Text ctor @0x004685ff + // (this->m_eVerticalJustification = 4) cross-referenced with + // UIElement_Text::CalcJustification @0x00467260 (the ACTUAL + // enum semantics: ecx_5==1 -> Center, ecx_5==3||5 -> the FAR edge + // (Bottom), any other value including the ctor's own default of 4 + // -> edi=0, the NEAR edge, i.e. Top): the correct unauthored + // default is TOP, not Center. This port's shared + // ElementReader/DatWidgetFactory VJustify mapping and field + // default both currently resolve an absent 0x15 to Center — a + // client-wide mismatch with real retail semantics that is NOT + // fixed here (filed as ISSUES.md #410; the blast radius spans + // every already-shipped DAT-imported UiText that relies on the + // CURRENT Center default, so a global remap needs its own + // dedicated investigation + regression sweep, not a bundled + // fix inside this page). Scoped correction: force these two + // specific panes to the value retail's ctor actually resolves + // to. Under Top justification the title (OneLine, ~1 line) sits + // near its box's own top (global Y~435) and the description + // (multi-line, honoring the SAME justification via + // ConfigureDatState's _honorDatVerticalJustification) starts near + // ITS box's own top (global Y~460) — the two boxes' TOP edges are + // 25px apart, so short/typical content no longer collides even + // though the boxes' full 100px extents still overlap on paper. + if (_infoTitle is { } infoTitle) + infoTitle.VerticalJustify = VJustify.Top; + if (_infoText is { } infoText) + infoText.VerticalJustify = VJustify.Top; // R4-3 (Campaign CC gate round 1 re-test 3): the description pane's // own raw box (0x100003fc, Y=460 H=100 -> bottom Y=560, live-DAT- diff --git a/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs b/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs index 2bd11d05..7cbe7810 100644 --- a/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs +++ b/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs @@ -309,6 +309,40 @@ internal sealed class CharacterCreationUiController : IDisposable _townTab.OnClick = () => ApplyProgressState(Page.Town); _summaryTab.OnClick = () => ApplyProgressState(Page.Summary); + // GF-13 (Campaign CC gate round 1, Batch A): honor the authored + // Invisible flag (dat property 0x3B) chargen-scoped only — see + // HideAuthoredInvisibleElements's own doc comment. + HideAuthoredInvisibleElements(Root); + } + + /// + /// GF-13 (Campaign CC gate round 1, Batch A). The user's live gate + /// reported an acdream-only "-Non-admin or Non-envoy" text leak below the + /// Summary name field. Root cause: elements 0x10000403 ("Non- + /// Admin") and 0x10000494 ("Non-Envoy") author dat property + /// 0x3B (Invisible) = — retail's + /// UIElement::OnSetAttribute @0x00462d80 case 8 + /// (GetPropertyName()-0x33 == 8, property id 0x3B) hides any + /// element authoring it via SetVisible(value == 0). acdream's + /// shared never read this property at all + /// (it now does, into / + /// , a pure data addition), so + /// every one of the 1,083 elements client-wide that author it rendered + /// regardless. A blanket importer-wide honor is its own separately-gated + /// visual sweep (docs/ISSUES.md #408) — this method is the NARROW, + /// chargen-scoped fix: walk this screen's own mounted subtree once at + /// construction and hide anything the dat itself marked hidden, by the + /// AUTHORED FLAG rather than a hardcoded id list, so any other + /// authored-invisible element under this root (not just the two the user + /// happened to see) is honored the same way. Register AP-230 records the + /// scoped-vs-general split. + /// + private static void HideAuthoredInvisibleElements(UiElement element) + { + if (element.AuthoredInvisible) + element.Visible = false; + foreach (UiElement child in element.Children) + HideAuthoredInvisibleElements(child); } internal UiElement Root => _layout.Root; diff --git a/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs b/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs index 53df4ffa..f42e241a 100644 --- a/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs +++ b/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs @@ -23,7 +23,7 @@ internal sealed class CharacterManagementUiController : IDisposable /// gmCharacterManagementUI::ListenToElementMessage@0x004ed5a0's element-id /// switch is keyed off idElement - 0x1000039d (the listbox base); /// offset 6 -> QueueUIMode(0x10000005), the mode gmCreditsUI registers - /// (Register@0x004E7500). + /// (Register@0x0047a69e) — out of scope this round (finding 1 note). /// internal const uint CreditsElementId = 0x100003A3u; /// Offset 7 from the listbox base -> MakeConfirmExitDialog@0x004ed250. @@ -69,7 +69,6 @@ internal sealed class CharacterManagementUiController : IDisposable private uint _errorDialogContext; private uint _confirmExitDialogContext; private bool _active; - private bool _presentationSuppressed; private bool _restoreCommandInFlight; private bool _suppressDialogCallbacks; private bool _disposed; @@ -87,8 +86,7 @@ internal sealed class CharacterManagementUiController : IDisposable UiButton exit, RetailDialogFactory dialogs, CharacterSelectionRuntimeBindings bindings, - DialogStrings strings, - Action? openCredits) + DialogStrings strings) { _host = host; _layout = layout; @@ -141,12 +139,13 @@ internal sealed class CharacterManagementUiController : IDisposable _delete.OnClick = RequestDelete; _restore.OnClick = RestoreSelected; - // ListenToElementMessage @0x004ED5A0 case 6 queues UI mode - // 0x10000005, registered by gmCreditsUI::Register @0x004E7500. - // The local composition callback performs that same screen swap. + // Credits (retail QueueUIMode(0x10000005) -> gmCreditsUI) is out of + // scope this round (finding 1 note) — same "future campaign, visibly + // ghosted, no invented action" treatment as Create above. Filed as + // issue #400. _credits.Visible = true; - _credits.Enabled = openCredits is not null; - _credits.OnClick = openCredits; + _credits.Enabled = false; + _credits.OnClick = null; _exit.OnClick = RequestExit; // World name (retail UpdateWorldName@0x004ec120 / @@ -169,7 +168,6 @@ internal sealed class CharacterManagementUiController : IDisposable { if (_disposed) return; - _presentationSuppressed = false; Deactivate(); _lastRevision = long.MinValue; } @@ -180,8 +178,7 @@ internal sealed class CharacterManagementUiController : IDisposable Func templateResolver, RetailDialogFactory dialogs, CharacterSelectionRuntimeBindings bindings, - DialogStrings strings, - Action? openCredits = null) + DialogStrings strings) { CharacterManagementUiController? controller = CreateDetached( host, @@ -189,8 +186,7 @@ internal sealed class CharacterManagementUiController : IDisposable templateResolver, dialogs, bindings, - strings, - openCredits); + strings); if (controller is null) return null; @@ -212,8 +208,7 @@ internal sealed class CharacterManagementUiController : IDisposable Func templateResolver, RetailDialogFactory dialogs, CharacterSelectionRuntimeBindings bindings, - DialogStrings strings, - Action? openCredits = null) + DialogStrings strings) { ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(layout); @@ -260,8 +255,7 @@ internal sealed class CharacterManagementUiController : IDisposable exit, dialogs, bindings, - strings, - openCredits); + strings); } catch { @@ -270,7 +264,6 @@ internal sealed class CharacterManagementUiController : IDisposable enter.OnClick = null; delete.OnClick = null; restore.OnClick = null; - credits.OnClick = null; exit.OnClick = null; throw; } @@ -299,13 +292,6 @@ internal sealed class CharacterManagementUiController : IDisposable if (_disposed) return; - if (_presentationSuppressed) - { - Deactivate(); - _lastRevision = long.MinValue; - return; - } - IRuntimeCharacterSelectionView? view = _bindings.View(); RuntimeCharacterSelectionSnapshot snapshot = view?.Snapshot ?? default; if (view is null || !snapshot.IsActive) @@ -394,7 +380,6 @@ internal sealed class CharacterManagementUiController : IDisposable _enter.OnClick = null; _delete.OnClick = null; _restore.OnClick = null; - _credits.OnClick = null; _exit.OnClick = null; foreach (UiButton row in _rows) { @@ -409,21 +394,6 @@ internal sealed class CharacterManagementUiController : IDisposable } } - /// - /// Local UI-mode seam used by . Retail - /// destroys/recreates the two frameworks through QueueUIMode; hiding this - /// retained root while preserving Runtime's character-selection owner has - /// the same observable behavior without duplicating state. - /// - internal void SetPresentationSuppressed(bool suppressed) - { - if (_disposed || _presentationSuppressed == suppressed) - return; - _presentationSuppressed = suppressed; - _lastRevision = long.MinValue; - Tick(); - } - private static bool TryCaptureRoster( IRuntimeCharacterSelectionView view, RuntimeCharacterSelectionSnapshot expected, diff --git a/src/AcDream.App/UI/Layout/CharacterManagementUiMountCoordinator.cs b/src/AcDream.App/UI/Layout/CharacterManagementUiMountCoordinator.cs index dc487514..e2748193 100644 --- a/src/AcDream.App/UI/Layout/CharacterManagementUiMountCoordinator.cs +++ b/src/AcDream.App/UI/Layout/CharacterManagementUiMountCoordinator.cs @@ -20,15 +20,13 @@ internal sealed class CharacterManagementUiMountCoordinator : IDisposable private readonly CharacterSelectionRuntimeBindings _bindings; private readonly Func _ensureDialogs; private readonly Func _loadResources; - private readonly Action? _openCredits; private bool _disposed; public CharacterManagementUiMountCoordinator( UiRoot host, CharacterSelectionRuntimeBindings bindings, Func ensureDialogs, - Func loadResources, - Action? openCredits = null) + Func loadResources) { _host = host ?? throw new ArgumentNullException(nameof(host)); _bindings = bindings ?? throw new ArgumentNullException(nameof(bindings)); @@ -36,7 +34,6 @@ internal sealed class CharacterManagementUiMountCoordinator : IDisposable ?? throw new ArgumentNullException(nameof(ensureDialogs)); _loadResources = loadResources ?? throw new ArgumentNullException(nameof(loadResources)); - _openCredits = openCredits; } public CharacterManagementUiController? Controller { get; private set; } @@ -61,10 +58,9 @@ internal sealed class CharacterManagementUiMountCoordinator : IDisposable _host, resources.Layout, resources.TemplateResolver, - dialogs, - _bindings, - resources.Strings, - _openCredits); + dialogs, + _bindings, + resources.Strings); if (candidate is null) return; diff --git a/src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs b/src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs index b2998b35..be320d65 100644 --- a/src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs +++ b/src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs @@ -1272,11 +1272,6 @@ public static class ConfigOptionsPageController "ID_Graphics_Value_High", "ID_Graphics_Value_VeryHigh", "ID_Graphics_Value_Extreme", }; - private static readonly int[] LandscapeDrawDistanceValues = - { - 3, 5, 8, 11, 15, 25, - }; - private static void BindRenderingQualitySection( UiTemplateListBox listBox, OptionPage page, @@ -1316,18 +1311,17 @@ public static class ConfigOptionsPageController storeOnly: true, // AP-198 resolveSprite, datFont, debugFont); - // Retail's SetEnumChoices carries the six integer payloads - // {3,5,8,11,15,25}; captions are indices only in presentation. - // SetDefaultValue(8) therefore selects Medium, and @render radius - // writes the same preference value this row reads. + // UNRESOLVED (see class doc / register row): retail's own + // SetDefaultValue(8) does not index this 6-entry choice array. + // Reproduced as an opaque int; the menu simply shows no + // highlighted item at the default (no crash, no invented mapping). BuildMenuRow( listBox, "ID_Graphics_LandscapeDrawDistance", LandscapeDrawDistanceChoices, page, resolveString, read: () => bindings.LoadDisplay().LandscapeDrawDistance, apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeDrawDistance = value }), defaultValue: 8, - storeOnly: false, - resolveSprite, datFont, debugFont, - payloadValues: LandscapeDrawDistanceValues); + storeOnly: true, // AP-198 + resolveSprite, datFont, debugFont); BuildToggleRow( listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString, @@ -1742,8 +1736,7 @@ public static class ConfigOptionsPageController bool storeOnly, Func? resolveSprite, UiDatFont? datFont, - BitmapFont? debugFont, - IReadOnlyList? payloadValues = null) + BitmapFont? debugFont) { UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex); if (row is null) @@ -1775,13 +1768,7 @@ public static class ConfigOptionsPageController if (tooltip is not null) menu.TooltipText = tooltip; - if (payloadValues is not null && payloadValues.Count != choiceKeys.Length) - throw new ArgumentException( - "Menu payload count must match the choice count.", - nameof(payloadValues)); - string[] choiceLabels = new string[choiceKeys.Length]; - int[] choiceValues = new int[choiceKeys.Length]; var items = new UiMenu.MenuItem[choiceKeys.Length]; for (int i = 0; i < choiceKeys.Length; i++) { @@ -1792,8 +1779,7 @@ public static class ConfigOptionsPageController $"[D.2b] ConfigOptionsPageController: menu choice '{choiceKeys[i]}' " + $"(for '{labelKey}') did not resolve — item renders with no caption " + "rather than invented English."); - choiceValues[i] = payloadValues?[i] ?? i; - items[i] = new UiMenu.MenuItem(choiceLabels[i], choiceValues[i]); + items[i] = new UiMenu.MenuItem(choiceLabels[i], i); } menu.Items = items; @@ -1802,8 +1788,7 @@ public static class ConfigOptionsPageController menu.ButtonLabelProvider = () => { int current = menu.Selected is int selected ? selected : initial; - int choiceIndex = Array.IndexOf(choiceValues, current); - return choiceIndex >= 0 ? choiceLabels[choiceIndex] : string.Empty; + return current >= 0 && current < choiceLabels.Length ? choiceLabels[current] : string.Empty; }; var row_ = new IntOptionRow( diff --git a/src/AcDream.App/UI/Layout/CreditsUiController.cs b/src/AcDream.App/UI/Layout/CreditsUiController.cs deleted file mode 100644 index f0790ad7..00000000 --- a/src/AcDream.App/UI/Layout/CreditsUiController.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System.Numerics; - -namespace AcDream.App.UI.Layout; - -internal sealed record CreditsUiResources( - uint LayoutId, - ImportedLayout PictureLayout, - ImportedLayout TextLayout, - IReadOnlyList TextFragments, - IReadOnlyList PictureIds, - float SectionSeconds, - string PleaseWait); - -/// -/// Retained-mode port of retail gmCreditsUI @0x004E6E70..0x004E79E0. -/// Retail composes two selected roots from layout enum 0x10000004, -/// scrolls the localized ID_Credits1..N glyph block and a cyclic strip -/// of authored pictures by the same pixel delta, then returns to character -/// management on completion or any input action. -/// -internal sealed class CreditsUiController : IDisposable -{ - internal const uint RootEnum = 0x10000004u; - internal const uint PictureRootElementId = 0x10000413u; - internal const uint TextRootElementId = 0x10000410u; - internal const uint TextAreaElementId = 0x10000411u; - internal const uint DynamicPictureElementId = 0x10000415u; - - private readonly UiRoot _host; - private readonly ImportedLayout _pictureLayout; - private readonly ImportedLayout _textLayout; - private readonly UiText _textArea; - private readonly IReadOnlyList _textFragments; - private readonly IReadOnlyList _pictureIds; - private readonly float _sectionSeconds; - private readonly RetailDialogFactory _dialogs; - private readonly string _pleaseWait; - private readonly Func _nowSeconds; - private readonly Func _resolveSprite; - private readonly Action _returnToCharacterManagement; - private readonly CreditsActionSurface _actionSurface; - private readonly List _pictures = []; - private readonly Vector2 _authoredCanvas; - - private UiText.Line[] _lines = []; - private float _textHeight; - private double _startTime; - private double _duration; - private float _lastProgress; - private int _nextPicture; - private long _tickSequence; - private long _returnAtTick = long.MaxValue; - private uint _waitContext; - private bool _active; - private bool _returnPending; - private bool _disposed; - - private CreditsUiController( - UiRoot host, - ImportedLayout pictureLayout, - ImportedLayout textLayout, - UiText textArea, - IReadOnlyList textFragments, - IReadOnlyList pictureIds, - float sectionSeconds, - RetailDialogFactory dialogs, - string pleaseWait, - Func nowSeconds, - Func resolveSprite, - Action returnToCharacterManagement) - { - _host = host; - _pictureLayout = pictureLayout; - _textLayout = textLayout; - _textArea = textArea; - _textFragments = textFragments; - _pictureIds = pictureIds; - _sectionSeconds = sectionSeconds; - _dialogs = dialogs; - _pleaseWait = pleaseWait; - _nowSeconds = nowSeconds; - _resolveSprite = resolveSprite; - _returnToCharacterManagement = returnToCharacterManagement; - - float width = MathF.Max( - PictureRoot.Left + PictureRoot.Width, - TextRoot.Left + TextRoot.Width); - float height = MathF.Max( - PictureRoot.Top + PictureRoot.Height, - TextRoot.Top + TextRoot.Height); - _authoredCanvas = new Vector2( - width > 0f ? width : 800f, - height > 0f ? height : 600f); - - PictureRoot.Visible = false; - TextRoot.Visible = false; - _actionSurface = new CreditsActionSurface(BeginReturn) - { - Width = _authoredCanvas.X, - Height = _authoredCanvas.Y, - Visible = false, - ZOrder = int.MaxValue, - }; - } - - internal UiElement PictureRoot => _pictureLayout.Root; - internal UiElement TextRoot => _textLayout.Root; - internal UiText TextArea => _textArea; - internal IReadOnlyList Pictures => _pictures; - internal bool IsActive => _active; - internal double DurationSeconds => _duration; - - internal static CreditsUiController? CreateDetached( - UiRoot host, - CreditsUiResources resources, - RetailDialogFactory dialogs, - Func nowSeconds, - Func resolveSprite, - Action returnToCharacterManagement) - { - ArgumentNullException.ThrowIfNull(host); - ArgumentNullException.ThrowIfNull(resources); - ArgumentNullException.ThrowIfNull(dialogs); - ArgumentNullException.ThrowIfNull(nowSeconds); - ArgumentNullException.ThrowIfNull(resolveSprite); - ArgumentNullException.ThrowIfNull(returnToCharacterManagement); - - if (resources.PictureLayout.Root.DatElementId != PictureRootElementId - || resources.TextLayout.Root.DatElementId != TextRootElementId - || resources.TextLayout.FindElement(TextAreaElementId) is not UiText textArea - || resources.TextFragments.Count == 0 - || resources.PictureIds.Count == 0 - || !float.IsFinite(resources.SectionSeconds) - || resources.SectionSeconds <= 0f) - { - Console.WriteLine( - "[UI] credits: authored root/text/picture contract is incomplete."); - return null; - } - - return new CreditsUiController( - host, - resources.PictureLayout, - resources.TextLayout, - textArea, - resources.TextFragments, - resources.PictureIds, - resources.SectionSeconds, - dialogs, - resources.PleaseWait, - nowSeconds, - resolveSprite, - returnToCharacterManagement); - } - - internal void Activate() - { - ObjectDisposedException.ThrowIf(_disposed, this); - if (_active) - return; - - AttachRoots(); - ResetRun(); - _active = true; - PictureRoot.Visible = true; - TextRoot.Visible = true; - _actionSurface.Visible = true; - _host.DeclareFixedCanvas(this, _authoredCanvas); - _host.BringToFront(PictureRoot); - _host.BringToFront(TextRoot); - _host.BringToFront(_actionSurface); - _host.SetKeyboardFocus(_actionSurface); - - // Update order is retail's: ScrollText first, then ScrollPictures. - // At progress zero that creates the first picture one pixel below - // the picture field, exactly as CreateAndAddPicture @0x004E7592. - Tick(); - } - - internal void Tick() - { - if (_disposed || !_active) - return; - - _tickSequence++; - if (_returnPending) - { - if (_tickSequence >= _returnAtTick) - CompleteReturn(); - return; - } - - double elapsed = Math.Max(0d, _nowSeconds() - _startTime); - float progress = _duration <= 0d - ? 1f - : Math.Clamp((float)(elapsed / _duration), 0f, 1f); - // Timer::compute_time is monotonic in retail. Preserve that invariant - // even when a deterministic test clock is moved backwards. - progress = MathF.Max(progress, _lastProgress); - _lastProgress = progress; - - float fieldHeight = TextRoot.Height; - int oldTop = (int)MathF.Round(_textArea.Top); - int travel = (int)MathF.Round( - (fieldHeight + _textHeight) * progress, - MidpointRounding.ToEven); - int newTop = (int)MathF.Round(fieldHeight) - travel; - _textArea.Left = 0f; - _textArea.Top = newTop; - ScrollPictures(oldTop - newTop); - - if (progress >= 1f) - BeginReturn(); - } - - internal void ResetSession() - { - if (_disposed || !_active) - return; - Deactivate(); - _returnToCharacterManagement(); - } - - public void Dispose() - { - if (_disposed) - return; - bool restoreCharacterManagement = _active; - _disposed = true; - Deactivate(); - _host.RemoveChild(PictureRoot); - _host.RemoveChild(TextRoot); - _host.RemoveChild(_actionSurface); - if (restoreCharacterManagement) - _returnToCharacterManagement(); - } - - private void AttachRoots() - { - if (PictureRoot.Parent is null) - _host.AddChild(PictureRoot); - if (TextRoot.Parent is null) - _host.AddChild(TextRoot); - if (_actionSurface.Parent is null) - _host.AddChild(_actionSurface); - } - - private void ResetRun() - { - CloseWait(); - ClearPictures(); - _returnPending = false; - _returnAtTick = long.MaxValue; - _lastProgress = 0f; - _nextPicture = 0; - - _textArea.Width = TextRoot.Width; - float maximumWidth = Math.Max( - 1f, - _textArea.Width - - (_textArea.Padding + _textArea.MarginLeft) - - (_textArea.Padding + _textArea.MarginRight)); - Func measure = _textArea.DatFont is { } font - ? font.MeasureWidth - : static value => value.Length * 8f; - string allText = string.Concat(_textFragments); - IReadOnlyList wrapped = UiText.WrapWords( - allText, - measure, - maximumWidth); - if (wrapped.Count == 0) - wrapped = [string.Empty]; - _lines = [.. wrapped.Select( - line => new UiText.Line(line, _textArea.DefaultColor))]; - _textArea.LinesProvider = () => _lines; - - float lineHeight = _textArea.DatFont?.LineHeight ?? 16f; - _textHeight = Math.Max(lineHeight, lineHeight * _lines.Length); - _textArea.Left = 0f; - _textArea.Top = TextRoot.Height; - _textArea.Height = _textHeight; - - // Initialize @0x004E726C. The loop cursor is N+1 when the first - // invalid ID_Credits key terminates enumeration, so preserve that - // exact denominator rather than substituting the valid count. - float terminatorIndex = _textFragments.Count + 1f; - _duration = _sectionSeconds - * (TextRoot.Height + _textHeight) - / (TextRoot.Height + _textHeight / terminatorIndex); - _startTime = _nowSeconds(); - } - - private void ScrollPictures(int deltaPixels) - { - if (deltaPixels != 0) - foreach (UiPanel picture in _pictures) - picture.Top -= deltaPixels; - - if (_pictures.Count > 0 - && _pictures[0].Top + _pictures[0].Height < 0f) - { - UiPanel expired = _pictures[0]; - _pictures.RemoveAt(0); - PictureRoot.RemoveChild(expired); - } - - if (_pictures.Count == 0) - AddPicture(); - - if (_pictures.Count > 0 - && _pictures[^1].Top < PictureRoot.Height) - { - AddPicture(); - } - } - - private void AddPicture() - { - if (_pictureIds.Count == 0) - return; - - uint pictureId = _pictureIds[_nextPicture]; - _nextPicture = (_nextPicture + 1) % _pictureIds.Count; - (uint texture, int width, int height) = _resolveSprite(pictureId); - if (texture == 0u || width <= 0 || height <= 0) - return; - - float top = _pictures.Count == 0 - ? PictureRoot.Height + 1f - : _pictures[^1].Top + _pictures[^1].Height + 1f; - var picture = new UiPanel - { - DatElementId = DynamicPictureElementId, - Left = 0f, - Top = top, - Width = width, - Height = height, - BackgroundColor = Vector4.Zero, - BorderColor = Vector4.Zero, - BorderThickness = 0f, - BackgroundSprite = pictureId, - SpriteResolve = _resolveSprite, - ClickThrough = true, - }; - PictureRoot.AddChild(picture); - _pictures.Add(picture); - } - - private void BeginReturn() - { - if (_disposed || !_active || _returnPending) - return; - - _returnPending = true; - _waitContext = _dialogs.MakeWait(_pleaseWait); - // QueueUIMode is asynchronous in retail. Keep the wait visible for one - // complete presented frame, then perform the local mode swap. - _returnAtTick = _tickSequence + 2; - } - - private void CompleteReturn() - { - if (!_active) - return; - Deactivate(); - _returnToCharacterManagement(); - } - - private void Deactivate() - { - _returnPending = false; - _returnAtTick = long.MaxValue; - _active = false; - PictureRoot.Visible = false; - TextRoot.Visible = false; - _actionSurface.Visible = false; - if (ReferenceEquals(_host.KeyboardFocus, _actionSurface)) - _host.SetKeyboardFocus(null); - _host.RevokeFixedCanvas(this); - CloseWait(); - ClearPictures(); - } - - private void CloseWait() - { - uint context = _waitContext; - _waitContext = 0u; - if (context != 0u) - _dialogs.CloseDialog(context); - } - - private void ClearPictures() - { - foreach (UiPanel picture in _pictures) - PictureRoot.RemoveChild(picture); - _pictures.Clear(); - } - - private sealed class CreditsActionSurface : UiElement - { - private readonly Action _onAction; - - public override bool HandlesClick => true; - - public CreditsActionSurface(Action onAction) - { - _onAction = onAction ?? throw new ArgumentNullException(nameof(onAction)); - AcceptsFocus = true; - ClickThrough = false; - } - - public override bool OnEvent(in UiEvent e) - { - if (!Enabled || !Visible) - return false; - if (e.Type is UiEventType.KeyDown - or UiEventType.MouseDown - or UiEventType.RightDown - or UiEventType.MiddleDown - or UiEventType.Scroll) - { - _onAction(); - return true; - } - return e.Type is UiEventType.KeyUp - or UiEventType.MouseUp - or UiEventType.RightUp - or UiEventType.MiddleUp - or UiEventType.Click - or UiEventType.RightClick; - } - } -} diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index ef1f1b53..5091ec3f 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -706,11 +706,10 @@ public static class DatWidgetFactory if (state.Properties.Values.TryGetValue(0x14u, out var justify) && justify.Kind == UiPropertyKind.Enum) { - align = ElementReader.MapHorizontalJustification( - justify.UnsignedValue) switch + align = justify.UnsignedValue switch { - HJustify.Left => UiMeterLabelAlign.Left, - HJustify.Right => UiMeterLabelAlign.Right, + 0u or 2u => UiMeterLabelAlign.Left, + 3u or 5u => UiMeterLabelAlign.Right, _ => UiMeterLabelAlign.Center, }; } @@ -894,7 +893,12 @@ public static class DatWidgetFactory // afterward will override these — this is only the dat-driven default. bool centered = info.HJustify == HJustify.Center; bool rightAligned = info.HJustify == HJustify.Right; - var vJustify = info.VJustify; + var vJustify = info.VJustify switch + { + VJustify.Top => VJustify.Top, + VJustify.Bottom => VJustify.Bottom, + _ => VJustify.Center, + }; var t = new UiText { diff --git a/src/AcDream.App/UI/Layout/ElementReader.cs b/src/AcDream.App/UI/Layout/ElementReader.cs index 08813b8c..cd335cc6 100644 --- a/src/AcDream.App/UI/Layout/ElementReader.cs +++ b/src/AcDream.App/UI/Layout/ElementReader.cs @@ -14,8 +14,7 @@ public enum HJustify : byte { Left = 0, Center = 1, Right = 2 } /// /// Vertical text justification read from dat property 0x15 (UIElement VerticalJustification). -/// Retail CalcJustification @ 0x00467260 treats 1 as Center, -/// 3/5 as Bottom, and every other value (including constructor default 4) as Top. +/// Values: 2=Top, 4=Bottom; absent/other = Center. /// public enum VJustify : byte { Top = 0, Center = 1, Bottom = 2 } @@ -110,17 +109,18 @@ public sealed class ElementInfo /// /// Horizontal text justification from dat Properties[0x14] - /// (EnumBaseProperty: 1=Center, 3/5=Right, all others=Left). - /// Retail's constructor default is raw 2, which resolves to Left. + /// (EnumBaseProperty: 0=Left, 1=Center, 3/5=Right). + /// Default is to preserve existing behavior where + /// controllers set Centered=true and no property was read. /// - public HJustify HJustify = HJustify.Left; + public HJustify HJustify = HJustify.Center; /// /// Vertical text justification from dat Properties[0x15] - /// (EnumBaseProperty: 1=Center, 3/5=Bottom, all others=Top). - /// Retail's constructor default is raw 4, which resolves to Top. + /// (EnumBaseProperty: 2=Top, 4=Bottom; absent/other = Center). + /// Default is to preserve existing behavior. /// - public VJustify VJustify = VJustify.Top; + public VJustify VJustify = VJustify.Center; /// /// Font color from dat Properties[0x1B] (ColorBaseProperty, ARGB bytes). @@ -251,8 +251,13 @@ public sealed class ElementInfo /// an authored true HIDES the element at construction. Populated the /// same way as / /// (recomputed fresh from the effective merged state every call), but this - /// feeds the shared , which applies the retail - /// construction-time visibility write uniformly to every built widget. + /// is a PURE DATA ADDITION: the shared / + /// path does not act on it. 1,083 elements + /// author this flag client-wide (docs/ISSUES.md #408, its own separately- + /// gated general-honor item) — only screens that explicitly walk their own + /// mounted subtree and check this field may hide elements by it (see + /// CharacterCreationUiController's chargen-scoped honor, register + /// AP-230). /// public bool Invisible; @@ -578,11 +583,12 @@ public static class ElementReader ZLevel = derived.ZLevel != 0 ? derived.ZLevel : base_.ZLevel, DefaultStateId = derived.DefaultStateId != 0 ? derived.DefaultStateId : base_.DefaultStateId, FontDid = derived.FontDid != 0 ? derived.FontDid : base_.FontDid, - // Presence, not a semantic enum value, decides inheritance. Center - // is a legitimate authored override; using it as an "unset" - // sentinel silently lost derived raw value 1. - HJustify = HasEffectiveEnum(derived, 0x14u) ? derived.HJustify : base_.HJustify, - VJustify = HasEffectiveEnum(derived, 0x15u) ? derived.VJustify : base_.VJustify, + // HJustify/VJustify: derived wins when it carries an explicit non-Center value + // (the dat property was present and read); otherwise inherit the base prototype's value. + // Center is the default (= "not set by this element") so Center-derived never overrides + // a non-Center base — matching the FontDid "non-zero wins" convention. + HJustify = derived.HJustify != HJustify.Center ? derived.HJustify : base_.HJustify, + VJustify = derived.VJustify != VJustify.Center ? derived.VJustify : base_.VJustify, // FontColor: derived wins when it has an explicit (non-null) color; otherwise inherit the base. // Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base. FontColor = derived.FontColor ?? base_.FontColor, @@ -644,13 +650,23 @@ public static class ElementReader if (info.TryGetEffectiveProperty(0x14u, out var horizontal) && horizontal.Kind == UiPropertyKind.Enum) { - info.HJustify = MapHorizontalJustification(horizontal.UnsignedValue); + info.HJustify = horizontal.UnsignedValue switch + { + 0u or 2u => HJustify.Left, + 3u or 5u => HJustify.Right, + _ => HJustify.Center, + }; } if (info.TryGetEffectiveProperty(0x15u, out var vertical) && vertical.Kind == UiPropertyKind.Enum) { - info.VJustify = MapVerticalJustification(vertical.UnsignedValue); + info.VJustify = vertical.UnsignedValue switch + { + 2u => VJustify.Top, + 4u => VJustify.Bottom, + _ => VJustify.Center, + }; } if (info.TryGetEffectiveProperty(0x1Bu, out var color)) @@ -804,24 +820,6 @@ public static class ElementReader info.MinHeight = minHeight; } - internal static HJustify MapHorizontalJustification(ulong raw) => raw switch - { - 1UL => HJustify.Center, - 3UL or 5UL => HJustify.Right, - _ => HJustify.Left, - }; - - internal static VJustify MapVerticalJustification(ulong raw) => raw switch - { - 1UL => VJustify.Center, - 3UL or 5UL => VJustify.Bottom, - _ => VJustify.Top, - }; - - private static bool HasEffectiveEnum(ElementInfo info, uint propertyId) => - info.TryGetEffectiveProperty(propertyId, out UiPropertyValue value) - && value.Kind == UiPropertyKind.Enum; - private static List ReadTabTable(ElementInfo info) { var entries = new List(); diff --git a/src/AcDream.App/UI/Layout/HousePageController.cs b/src/AcDream.App/UI/Layout/HousePageController.cs index ba456d85..5ea2613f 100644 --- a/src/AcDream.App/UI/Layout/HousePageController.cs +++ b/src/AcDream.App/UI/Layout/HousePageController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using AcDream.Runtime.Gameplay; namespace AcDream.App.UI.Layout; @@ -32,11 +31,15 @@ namespace AcDream.App.UI.Layout; /// ParseUpdateRentTime/ParseUpdateRentPayment, /// GameEventWiring's four delegate holes, the outbound HouseQuery /// action). The House-tab ownership-text closer session (also 2026-08-17) -/// wired to RuntimeHouseState; issue -/// #413 later completed all seven Display* builders for both -/// houseless and owned-house snapshots. -/// preserves retail's Normal/RentPaid/RentNotPaid font-palette index while -/// the original string callback remains a compatibility fallback. The +/// wired to the minimal RuntimeHouseState +/// owner and ported DisplayPurchaseTimeText @0x004a3110's expired +/// branch — a fresh houseless character's House tab shows the single +/// decomp-verified line "You may buy another house immediately.", +/// live-connected-gate-verified (screenshot + structural UI-tree dump +/// against the real +Acdream character on a local ACE server). The +/// other six Display* line builders DisplayHouseData calls +/// (owned-house-only content: buy/rent payments and times, location, +/// warning text) remain unported — ISSUES #413's surviving scope. The /// night-round review (F2, 2026-08-17) moved the outbound HouseQuery send /// from a House-tab-open trigger to retail's real login-complete edge (see /// 's own doc), so by the time a player opens @@ -75,15 +78,11 @@ public sealed class HousePageController // always returns null (no resolver = no row), so Refresh silently // produced zero rows regardless of Lines — the gap this session // closes alongside the text composition itself. - Func? TemplateResolver = null, - // Issue #413: typed rows preserve retail's HousePanelTextColor - // palette index. The string-only callback remains for compatibility - // with standalone fixtures and older embedding callers. - Func>? PanelLines = null); + Func? TemplateResolver = null); private readonly UiTemplateListBox _listBox; private readonly Bindings _bindings; - private IReadOnlyList _lastLines = Array.Empty(); + private IReadOnlyList _lastLines = Array.Empty(); private HousePageController(UiTemplateListBox listBox, Bindings bindings) { @@ -105,7 +104,7 @@ public sealed class HousePageController listBox.TemplateResolver = bindings.TemplateResolver; var controller = new HousePageController(listBox, bindings); - controller.Refresh(controller.CurrentLines()); + controller.Refresh(bindings.Lines()); return controller; } @@ -114,43 +113,22 @@ public sealed class HousePageController /// other social-panel pages' revision-gated rebuild discipline). public void Tick() { - IReadOnlyList lines = CurrentLines(); + IReadOnlyList lines = _bindings.Lines(); if (lines.SequenceEqual(_lastLines)) return; Refresh(lines); } public void OnShown() => _bindings.OnShown?.Invoke(); - private IReadOnlyList CurrentLines() - { - if (_bindings.PanelLines is { } styled) - return styled(); - - IReadOnlyList plain = _bindings.Lines(); - if (plain.Count == 0) - return Array.Empty(); - - var projected = new HousePanelLine[plain.Count]; - for (int i = 0; i < plain.Count; i++) - projected[i] = new HousePanelLine(plain[i], HousePanelTextColor.Normal); - return projected; - } - - private void Refresh(IReadOnlyList lines) + private void Refresh(IReadOnlyList lines) { _lastLines = lines; _listBox.Flush(); - foreach (HousePanelLine line in lines) + foreach (string line in lines) { UiElement? row = _listBox.AddItemFromTemplateList(0); if (row is UiText text) - { - int colorIndex = (int)line.Color; - Vector4 color = colorIndex >= 0 && colorIndex < text.FontColorPalette.Count - ? text.FontColorPalette[colorIndex] - : text.DefaultColor; - text.LinesProvider = () => [new UiText.Line(line.Text, color)]; - } + text.LinesProvider = () => [new UiText.Line(line, Vector4.One)]; } } } diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index d5a8f5e8..01f23454 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -128,13 +128,9 @@ public static class LayoutImporter // #409: see the Build overload's own sourceLayoutDid doc comment. w.SourceLayoutDid = sourceLayoutDid; - // #408: retail applies P0x3B through UIElement::OnSetAttribute case 8 - // for every element: Invisible=true means SetVisible(false). Keep the - // authored bit for diagnostics while making its initial behavior a - // property of the shared importer, not of individual screens. + // GF-13: pure data passthrough — see UiElement.AuthoredInvisible's own + // doc comment for why this does NOT set Visible here. w.AuthoredInvisible = info.Invisible; - if (info.Invisible) - w.Visible = false; // #409: the six per-element tooltip properties, same pure-data- // passthrough shape as AuthoredInvisible above. TooltipText is the @@ -227,6 +223,19 @@ public static class LayoutImporter if (child.StateMedia.Count == 0) continue; var cw = BuildWidget(child, resolve, datFont, fontResolve, stringResolve, byId, sourceLayoutDid); if (cw is null) continue; + // F5/F6 (Campaign CC gate round 1 closeout): a NARROW honor + // of AuthoredInvisible, scoped to children reached through + // THIS carve-out only — e.g. the chat new-text indicator + // (0x1000048C, live-DAT-confirmed Invisible=true on every + // layout it appears in) would otherwise render as a phantom + // element retail never shows, now that this carve-out + // builds it as a real widget instead of silently dropping + // it. This is NOT the general client-wide honor (#408, + // 1,083 elements) — every OTHER AuthoredInvisible consumer + // stays data-only, acted on nowhere but chargen's own + // HideAuthoredInvisibleElements walk (register AP-230). + if (cw.AuthoredInvisible) + cw.Visible = false; w.AddChild(cw); } } @@ -386,9 +395,9 @@ public static class LayoutImporter /// (the character footer's three state-groups; the tab-page content areas) manage /// visibility purely at runtime via C++ controller code. Retail uses /// UIElement::SetState(stateId) on the parent to propagate state, then - /// C++ getters access the right sub-group by element id. Sibling groups which - /// do not author property 0x3B start visible; the relevant controllers - /// () perform their runtime show/hide. + /// C++ getters access the right sub-group by element id. All groups are shipped + /// as visible in the imported widget tree; the relevant controllers + /// () perform the initial show/hide. /// /// /// Optional per-element font resolver (see @@ -668,6 +677,38 @@ public static class LayoutImporter if (sd.Properties is not null) { + // HorizontalJustification (0x14): EnumBaseProperty. + // Retail CalcJustification @ 0x00467260: 1=Center, 3/5=Right, + // every other value (including constructor default 2)=Left. + // Only update if still at the default (Center); derived-wins handled in Merge. + if (info.HJustify == HJustify.Center + && sd.Properties.TryGetValue(0x14u, out var hRaw) + && hRaw is EnumBaseProperty hEnum) + { + info.HJustify = hEnum.Value switch + { + 0u or 2u => HJustify.Left, + 1u => HJustify.Center, + 3u => HJustify.Right, + 5u => HJustify.Right, + _ => HJustify.Left, + }; + } + + // VerticalJustification (0x15): EnumBaseProperty. + // Retail values: 2=Top, 4=Bottom; absent/other = Center. + if (info.VJustify == VJustify.Center + && sd.Properties.TryGetValue(0x15u, out var vRaw) + && vRaw is EnumBaseProperty vEnum) + { + info.VJustify = vEnum.Value switch + { + 2u => VJustify.Top, + 4u => VJustify.Bottom, + _ => VJustify.Center, + }; + } + // ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A). // Only read when not already set (first dat state wins; Merge propagates from base). if (info.FontColor is null diff --git a/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs b/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs index 38b909d5..8f54e684 100644 --- a/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs +++ b/src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs @@ -652,8 +652,13 @@ public sealed class RetailTooltipPresenter : IDisposable // elements author P0x3D" sweep only covered hover TARGETS, never // the popup skins' text children.) // (b) Tooltip text is LEFT-aligned: the text child authors no - // justification, so the shared importer supplies retail's raw-2 - // constructor default (Left). + // justification and retail's unauthored default is Left, while + // our importer's ElementInfo default is Center — the same + // wrong-default class as #410's VJustify finding. Point-fixed + // here (the chat transcript does the same); the client-wide + // default remains #410's scope. + text.Centered = false; + text.RightAligned = false; // (c) The text child's authored margins (P0x23-0x26 — L2/R2/U2/D2 on // the popup skins) participate exactly as InqSizewMargins does: diff --git a/src/AcDream.App/UI/Layout/UiDatElement.cs b/src/AcDream.App/UI/Layout/UiDatElement.cs index aa7276e6..733cd3ff 100644 --- a/src/AcDream.App/UI/Layout/UiDatElement.cs +++ b/src/AcDream.App/UI/Layout/UiDatElement.cs @@ -85,7 +85,6 @@ public class UiDatElement : UiElement, IUiDatStateful public bool TrySetRetailState(uint stateId) { - uint appliedStateId = stateId; UiStateInfo? selectedState = null; if (stateId == UiStateInfo.DirectStateId) { @@ -115,15 +114,23 @@ public class UiDatElement : UiElement, IUiDatStateful // Normal_rollover/Highlight media but NO 'Normal' state at // all, so the row's PassToChildren 'Normal' hover-leave // cascade landed here and the bars never cleared. Retail's - // state-0 arm applies and cascades the BASE descriptor. + // state-0 arm cascades state 0 to children off the BASE + // descriptor's own PassToChildren (m_desc.m_bPassToChildren, + // @0x00464eca), and the per-state Invisible honor below stays + // scoped to NAMED authored states exactly as before (the #408 + // gate) — selectedState remains null on this path. ActiveState = ""; - appliedStateId = UiStateInfo.DirectStateId; - Info.States.TryGetValue(appliedStateId, out selectedState); - } - else - { - ActiveState = stateName; + if (Info.States.TryGetValue( + UiStateInfo.DirectStateId, out UiStateInfo? baseState) + && baseState.PassToChildren) + { + foreach (UiElement child in Children) + if (child is IUiDatStateful stateful) + stateful.TrySetRetailState(UiStateInfo.DirectStateId); + } + return true; } + ActiveState = stateName; } // Per-state Invisible (dat property 0x3B): retail's SetState applies @@ -137,11 +144,21 @@ public class UiDatElement : UiElement, IUiDatStateful // Normal_rollover={0x3B:false}, i.e. hidden at rest, shown on // rollover. // - // #408: DirectState is not a special exception. It reaches the same - // OnSetAttribute switch during construction and whenever retail falls - // back to state 0, so it must be able to restore authored visibility - // after a named state changed it. - if (selectedState is not null + // SCOPED TO NAMED STATES ONLY: a 0x3B authored in the unnamed + // DirectState is the CONSTRUCTION-time "authored invisible" class + // (1,083 elements client-wide — docs/ISSUES.md #408, its own + // separately-gated general-honor item; ElementReader.Invisible/GF-13 + // captures it and only chargen's scoped walk acts on it, register + // AP-230). Honoring it here would un-gate #408 through the back + // door: LayoutImporter.BuildWidget's post-children state reapply + // calls TrySetRetailState(DirectStateId) on every built widget, so + // a DirectState honor would hide all 1,083 at import (measured + // same-round: 10 combat-layout elements incl. 0x10000454 went + // un-hit-testable, breaking the spell-favorite drag tests). The + // NAMED-state flip below is a live visibility state machine that + // cannot work at all without the honor — that is this port's line. + if (stateId != UiStateInfo.DirectStateId + && selectedState is not null && selectedState.Properties.TryGetValue(0x3Bu, out var invisibleProp) && invisibleProp.Kind == UiPropertyKind.Bool) Visible = !invisibleProp.BoolValue; @@ -150,7 +167,7 @@ public class UiDatElement : UiElement, IUiDatStateful { foreach (UiElement child in Children) if (child is IUiDatStateful stateful) - stateful.TrySetRetailState(appliedStateId); + stateful.TrySetRetailState(stateId); } return true; } diff --git a/src/AcDream.App/UI/Layout/VendorUiController.cs b/src/AcDream.App/UI/Layout/VendorUiController.cs index ca9fb3bd..247268f9 100644 --- a/src/AcDream.App/UI/Layout/VendorUiController.cs +++ b/src/AcDream.App/UI/Layout/VendorUiController.cs @@ -169,8 +169,8 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag /// the absent authored sprite exactly rather than inventing one. /// /// - /// G5 correction (vendor gate finding): it is a single scrollable - /// ListBox, not a 3-column grid. The F1 review's "column-major grid" + /// G5 correction (vendor gate finding): it is a SCROLLABLE single + /// column, not a 3-column grid. The F1 review's "column-major grid" /// framing was wrong — a live-dat scan (tools/VendorLayoutScan, /// dump/resolved 0x21000043 0x1000034F) shows /// 0x1000034F has TWO children, not one: the ListBox @@ -183,17 +183,15 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag /// 0x06004C60/63/66, up button (element /// 0x10000072, retail-seated on top) 0x06004C6C/6D/6E, down button /// (element 0x10000071, retail-seated on the bottom) 0x06004C69/6A/6B, - /// track 0x06004C5F). The later named-retail trace resolves the - /// apparent fixed-six-row ambiguity: OpenVendor inserts only present - /// categories, UIElement_ListBox::UpdateLayout @0x0046e460 sums their - /// row heights, ResizeScrollableArea broadcasts message 0x32, - /// and UIElement_Menu::RecalculatePopupSize @0x0046caf0 resizes this - /// four-edge-docked popup to that content, uncapped. The sibling scrollbar's - /// installed-DAT property 0x79=true hides it when the resized content - /// fits. keeps the authored single-column - /// structure while and - /// port those two retail - /// behaviors. Chat's own popup + /// track 0x06004C5F). With 18 authored categories and only 6 + /// visible rows, retail's actual rendering is a single scrolling column + /// (matching the user's reference screenshot: ~visible rows + scrollbar + + /// highlight — not our earlier 3-column x 6-row grid showing all 18 at + /// once). switches the popup to this + /// shape; keeps its existing meaning + /// as the authored visible-row count (still 6 — 108px ListBox height / + /// 18px row height, now interpreted as "rows before scrolling" instead + /// of "rows before wrapping to a new column"). Chat's own popup /// (LayoutDesc 0x21000006, element 0x1000001C) has NO /// sibling scrollbar element and is unaffected — /// never sets Scrollable, so @@ -574,8 +572,6 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag // scrollbar, not a column-major grid — see the class doc's "G5 // correction" paragraph above. _typeMenu.Scrollable = true; - _typeMenu.PopupSizeToContent = true; - _typeMenu.PopupScrollbarHideWhenDisabled = true; _typeMenu.ScrollbarWidth = TypeMenuScrollbarWidth; _typeMenu.ScrollButtonExtent = TypeMenuScrollButtonExtent; _typeMenu.ScrollTrackSprite = TypeMenuScrollTrackSprite; diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 384b7f27..2683d01e 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -321,8 +321,7 @@ public sealed record SocialRuntimeBindings( /// /// Batch C (overnight hover/UI round, 2026-08-17): bindings for the /// two-tab Map/House panel. defaults to -/// "no house" and / -/// to empty when the caller doesn't +/// "no house" and to empty when the caller doesn't /// wire the House wire groundwork — the panel still mounts and the Map tab /// still works standalone. /// @@ -331,8 +330,7 @@ public sealed record MapHouseRuntimeBindings( Func PlayerCellId, Func? HousePosition = null, Func>? HouseLines = null, - Action? HouseShown = null, - Func>? HousePanelLines = null); + Action? HouseShown = null); /// /// Campaign QT slice QT5: what the Journal panel's Contracts page reads — @@ -561,7 +559,6 @@ public sealed class RetailUiRuntime : IDisposable private ProjectileDebugOverlayController? _projectileDebugOverlay; private Layout.VitalsSideBySideController? _vitalsSideBySide; private CharacterManagementUiMountCoordinator? _characterManagementMount; - private CreditsUiController? _creditsController; private CharacterCreationUiMountCoordinator? _characterCreationMount; private PluginSidePanel? _pluginSidePanel; private IDisposable? _characterSheetSubscription; @@ -783,7 +780,6 @@ public sealed class RetailUiRuntime : IDisposable public MapHousePanelController? MapHousePanelController { get; private set; } internal CharacterManagementUiController? CharacterManagementController => _characterManagementMount?.Controller; - internal CreditsUiController? CreditsController => _creditsController; internal CharacterCreationUiController? CharacterCreationController => _characterCreationMount?.Controller; @@ -968,7 +964,6 @@ public sealed class RetailUiRuntime : IDisposable _itemCooldownController?.Tick(); _characterManagementMount?.Tick(); CharacterManagementController?.Tick(); - _creditsController?.Tick(); _characterCreationMount?.Tick(); CharacterCreationController?.Tick(); DialogFactory?.Tick(); @@ -1364,7 +1359,6 @@ public sealed class RetailUiRuntime : IDisposable { try { - _creditsController?.ResetSession(); CharacterManagementController?.ResetSession(); DialogFactory?.Reset(); TooltipPresenter?.HideCurrent(); @@ -4179,8 +4173,7 @@ public sealed class RetailUiRuntime : IDisposable // Same generic template resolver the Map tab's town // hotspots use — see HousePageController.Bindings. // TemplateResolver's own doc for why reusing it is correct. - TemplateResolver: ResolveHotspotTemplate, - PanelLines: mh.HousePanelLines)); + TemplateResolver: ResolveHotspotTemplate)); Layout.MapHousePanelController? controller; lock (_bindings.Assets.DatLock) @@ -5106,194 +5099,7 @@ public sealed class RetailUiRuntime : IDisposable Host.Root, bindings with { RequestCreate = () => CharacterCreationController?.Open() }, EnsureDialogFactory, - LoadCharacterManagementResources, - OpenCredits); - } - - private void OpenCredits() - { - if (_disposed) - return; - - CharacterManagementUiController? characters = - CharacterManagementController; - if (characters is null) - return; - - if (_creditsController is null) - { - RetailDialogFactory? dialogs = EnsureDialogFactory(); - CreditsUiResources? resources = LoadCreditsResources(); - if (dialogs is null || resources is null) - return; - - _creditsController = CreditsUiController.CreateDetached( - Host.Root, - resources, - dialogs, - static () => System.Diagnostics.Stopwatch.GetTimestamp() - / (double)System.Diagnostics.Stopwatch.Frequency, - _bindings.Assets.ResolveSprite, - ReturnFromCredits); - if (_creditsController is null) - return; - } - - characters.SetPresentationSuppressed(true); - try - { - _creditsController.Activate(); - } - catch (Exception error) - { - characters.SetPresentationSuppressed(false); - Console.WriteLine( - $"[UI] credits activation failed: {error.Message}"); - } - } - - private void ReturnFromCredits() - => CharacterManagementController?.SetPresentationSuppressed(false); - - private CreditsUiResources? LoadCreditsResources() - { - uint layoutId; - ElementInfo? pictureInfo; - ElementInfo? textInfo; - ImportedLayout? pictureLayout; - ImportedLayout? textLayout; - var strings = new DatStringResolver(_bindings.Assets.Dats); - lock (_bindings.Assets.DatLock) - { - layoutId = RetailDataIdResolver.Resolve( - _bindings.Assets.Dats, - CreditsUiController.RootEnum, - 5u); - pictureInfo = layoutId == 0u - ? null - : LayoutImporter.ImportInfos( - _bindings.Assets.Dats, - layoutId, - CreditsUiController.PictureRootElementId); - textInfo = layoutId == 0u - ? null - : LayoutImporter.ImportInfos( - _bindings.Assets.Dats, - layoutId, - CreditsUiController.TextRootElementId); - pictureLayout = layoutId == 0u - ? null - : LayoutImporter.Import( - _bindings.Assets.Dats, - layoutId, - CreditsUiController.PictureRootElementId, - _bindings.Assets.ResolveSprite, - _bindings.Assets.DefaultFont, - _bindings.Assets.ResolveFont); - textLayout = layoutId == 0u - ? null - : LayoutImporter.Import( - _bindings.Assets.Dats, - layoutId, - CreditsUiController.TextRootElementId, - _bindings.Assets.ResolveSprite, - _bindings.Assets.DefaultFont, - _bindings.Assets.ResolveFont); - } - - if (pictureInfo is null - || textInfo is null - || pictureLayout is null - || textLayout is null - || !TryGetCreditsDataId( - textInfo, - 0x10000002u, - out uint textAreaId) - || textAreaId != CreditsUiController.TextAreaElementId - || !TryGetCreditsDataId( - textInfo, - 0x10000003u, - out uint stringTableId) - || !textInfo.TryGetEffectiveFloat( - 0x10000004u, - out float sectionSeconds) - || !pictureInfo.TryGetEffectiveProperty( - 0x10000005u, - out UiPropertyValue pictureProperty) - || pictureProperty.Kind != UiPropertyKind.Array) - { - Console.WriteLine( - "[UI] credits: enum-table-5 properties could not be imported."); - return null; - } - - uint[] pictureIds = - [ - .. pictureProperty.ArrayValue - .Where(static value => value.Kind is - UiPropertyKind.DataId or UiPropertyKind.Enum) - .Select(static value => checked((uint)value.UnsignedValue)) - .Where(static value => value != 0u), - ]; - if (pictureIds.Length == 0) - return null; - - var textFragments = new List(); - string? pleaseWait; - lock (_bindings.Assets.DatLock) - { - for (int index = 1; index <= 4096; index++) - { - string? fragment = strings.Resolve( - stringTableId, - DatStringResolver.ComputeHash($"ID_Credits{index}")); - if (fragment is null) - break; - textFragments.Add(fragment); - } - - // MakePleaseWaitDialog @0x004E76F0 resolves table enum - // 0x10000001, the installed EoR table DID 0x23000001. - pleaseWait = strings.Resolve( - 0x23000001u, - DatStringResolver.ComputeHash("ID_Wait_PleaseWait")); - } - - if (textFragments.Count == 0 || pleaseWait is null) - { - Console.WriteLine( - "[UI] credits: localized credit/wait strings are unavailable."); - return null; - } - - Console.WriteLine( - $"[UI] retail credits ready (layout 0x{layoutId:X8}, " - + $"{textFragments.Count} text fragments, {pictureIds.Length} pictures). "); - return new CreditsUiResources( - layoutId, - pictureLayout, - textLayout, - textFragments, - pictureIds, - sectionSeconds, - pleaseWait); - } - - private static bool TryGetCreditsDataId( - ElementInfo info, - uint propertyId, - out uint value) - { - if (info.TryGetEffectiveProperty(propertyId, out UiPropertyValue property) - && property.Kind is UiPropertyKind.DataId or UiPropertyKind.Enum - && property.UnsignedValue <= uint.MaxValue) - { - value = (uint)property.UnsignedValue; - return true; - } - - value = 0u; - return false; + LoadCharacterManagementResources); } private RetailDialogFactory? EnsureDialogFactory() @@ -5586,7 +5392,6 @@ public sealed class RetailUiRuntime : IDisposable () => _itemConfirmationController?.Dispose(), () => { - _creditsController?.Dispose(); _characterManagementMount?.Dispose(); _characterCreationMount?.Dispose(); _gameplayConfirmationController?.Dispose(); diff --git a/src/AcDream.App/UI/UiButton.cs b/src/AcDream.App/UI/UiButton.cs index 3d2783e4..0d4cc3d5 100644 --- a/src/AcDream.App/UI/UiButton.cs +++ b/src/AcDream.App/UI/UiButton.cs @@ -385,13 +385,11 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful if (ToggleBehavior && stateId is UiButtonStateMachine.Normal or UiButtonStateMachine.Highlight) { Selected = stateId == UiButtonStateMachine.Highlight; - ApplyStateVisibility(stateId); return true; } if (stateId == UiButtonStateMachine.Ghosted) { Enabled = false; - ApplyStateVisibility(stateId); return true; } if (!Enabled && stateId != UiButtonStateMachine.Ghosted) @@ -426,14 +424,12 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful if (!HasStateMedia("")) return false; ActiveState = ""; - ApplyStateVisibility(stateId); CascadeStateToChildren(stateId); return true; } if (TryFindState(stateId, out var state)) { ActiveState = state.Name; - ApplyStateVisibility(stateId); CascadeStateToChildren(stateId); return true; } @@ -443,28 +439,12 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful if (!string.IsNullOrEmpty(stateName) && HasStateMedia(stateName)) { ActiveState = stateName; - ApplyStateVisibility(stateId); CascadeStateToChildren(stateId); return true; } return false; } - /// - /// #408: UIElement::SetState applies the committed descriptor's properties - /// through UIElement::OnSetAttribute for buttons too. Property 0x3B is the - /// shared Invisible switch; it is not a generic-container-only behavior. - /// - private void ApplyStateVisibility(uint stateId) - { - if (_info.States.TryGetValue(stateId, out var state) - && state.Properties.Values.TryGetValue(0x3Bu, out var invisible) - && invisible.Kind == UiPropertyKind.Bool) - { - Visible = !invisible.BoolValue; - } - } - /// Merged for this element. /// Dat file-id → (GL texture handle, native px width, native px height). /// Returns (0,0,0) when the texture is not yet uploaded. diff --git a/src/AcDream.App/UI/UiElement.cs b/src/AcDream.App/UI/UiElement.cs index 17cfc809..e0c3874f 100644 --- a/src/AcDream.App/UI/UiElement.cs +++ b/src/AcDream.App/UI/UiElement.cs @@ -59,11 +59,14 @@ public abstract class UiElement /// /// GF-13 (Campaign CC gate round 1, Batch A): mirrors - /// ElementInfo.Invisible (dat property 0x3B), retained for - /// diagnostics after LayoutImporter.BuildWidget applies retail's - /// construction-time SetVisible(value == 0) behavior client-wide. - /// Runtime controllers may subsequently call just - /// as retail may issue a later SetVisible. + /// ElementInfo.Invisible (dat property 0x3B) — a PURE DATA + /// PASSTHROUGH set by LayoutImporter.BuildWidget at construction. + /// The shared importer does NOT act on this flag (1,083 elements author + /// it client-wide, docs/ISSUES.md #408); it exists only so a screen that + /// owns its own mounted subtree can honor it explicitly, the way + /// CharacterCreationUiController does for the chargen screen + /// (register AP-230). Reading this never changes by + /// itself. /// public bool AuthoredInvisible { get; internal set; } diff --git a/src/AcDream.App/UI/UiMenu.cs b/src/AcDream.App/UI/UiMenu.cs index d33df13b..f2b7237b 100644 --- a/src/AcDream.App/UI/UiMenu.cs +++ b/src/AcDream.App/UI/UiMenu.cs @@ -85,20 +85,21 @@ public sealed class UiMenu : UiElement public float ColumnWidth { get; set; } = 191f; // dat item template W=191 /// - /// Retail's authored vendor category popup + /// G5 (vendor gate finding): retail's authored vendor category popup /// (LayoutDesc 0x21000043, root 0x1000034F) pairs its /// ListBox (element 0x10000350, type 0x5) with a SIBLING /// UIElement_Scrollbar (element 0x10000351, type 0xB, /// 16px wide, docked immediately right of the list at x=100) — verified - /// via installed-DAT inspection against client_local_English.dat. - /// The authored 100x108 ListBox starts with a six-row viewport, but retail - /// UIElement_ListBox::UpdateLayout @0x0046e460 resizes its scrollable - /// content to the number of inserted categories. Because all four edges are - /// docked, message 0x32 reaches - /// UIElement_Menu::RecalculatePopupSize @0x0046caf0 and grows or - /// shrinks the popup to that content. The sibling scrollbar authors property - /// 0x79=true, so it disappears once the resized viewport fits the - /// content. Chat's own popup (LayoutDesc 0x21000006) has + /// via a live-dat scan (tools/VendorLayoutScan) against + /// client_local_English.dat: the ListBox reads a single-column + /// shape (attributes resolving to m_nCols=1/m_nRows=6) and + /// the row template (0x10000352) is 100×18 — a SCROLLABLE single + /// column with 6 visible rows, not our earlier column-major grid + /// approximation (which showed all 18 categories at once across 3 + /// columns, never matching the retail screenshot's ~one-column-with- + /// scrollbar look). becomes the VISIBLE ROW + /// COUNT in this mode (still authored-driven — 108px ListBox height / 18px + /// row height = 6). Chat's own popup (LayoutDesc 0x21000006) has /// NO sibling scrollbar element and keeps the class default false — the /// legacy column-major grid path below is untouched for it. /// @@ -139,24 +140,6 @@ public sealed class UiMenu : UiElement public uint ScrollUpSprite { get; set; } public uint ScrollDownSprite { get; set; } - /// - /// Retail scrollbar property 0x79 for the popup's authored sibling - /// scrollbar. When the content fits and the scrollbar is therefore disabled, - /// hide the sibling completely: no chrome, pointer target, or reserved width. - /// Vendor popup element 0x10000351 authors this true. Retail owns a real - /// sibling widget and calls SetVisible(false) from - /// UIElement_Scrollbar::UpdateLayout @0x004710d0; this procedural popup - /// must also remove the sibling's width from its flattened geometry to produce - /// the same visible result. - /// - public bool PopupScrollbarHideWhenDisabled { get; set; } - - /// Presentation projection shared by drawing and pointer dispatch. - /// Internal for the same focused-test purpose as - /// . - internal bool IsPopupScrollbarPresentationVisible - => !PopupScrollbarHideWhenDisabled || PopupContentOverflows; - private bool _draggingPopupThumb; private float _popupThumbDragOffset; @@ -269,8 +252,9 @@ public sealed class UiMenu : UiElement /// popup ListBox (0x21000043/0x10000358) reads edges L=T=R=B=1 /// (menuprobe3, OptionsPanelLiveMountProbeTests), so /// sets - /// this true. Vendor's ListBox has the same four docked edges and therefore - /// enables it too; chat's grid popup keeps the class default false. + /// this true; chat's grid popup and vendor's shipped 6-row window keep + /// the class default false (vendor's authored ListBox is ALSO docked — + /// tracked as its own issue, not silently reworked here). /// When set, stops being the visible-window /// height and the popup shows every item with no scroll overflow. /// @@ -349,14 +333,12 @@ public sealed class UiMenu : UiElement // Interior = the row content; Outer = interior + the 8-piece bevel ring. // Scrollable: always exactly one column (RowsPerColumn is the VISIBLE window, - // not a wrap threshold), widened by the docked scrollbar only while that - // sibling is presentation-visible. Retail property 0x79 removes a disabled - // scrollbar completely; keeping its width caused #386's empty placeholder. + // not a wrap threshold), widened by the docked scrollbar's own authored width. private int ColumnCount => Scrollable ? 1 : (Items.Count + RowsPerColumn - 1) / System.Math.Max(1, RowsPerColumn); private float InteriorW => Scrollable - ? ColumnWidth + EffectiveScrollbarWidth + ? ColumnWidth + ScrollbarWidth : ColumnCount * ColumnWidth; /// The popup's visible row count. Size-to-content (retail's @@ -370,15 +352,6 @@ public sealed class UiMenu : UiElement ? System.Math.Max(1, Items.Count) : RowsPerColumn; - /// UiMenu rows have a fixed authored height, so this is the same - /// overflow decision publishes to - /// , but is stable before the first draw/event has - /// configured that model. - private bool PopupContentOverflows => Items.Count > EffectiveVisibleRows; - - private float EffectiveScrollbarWidth - => IsPopupScrollbarPresentationVisible ? ScrollbarWidth : 0f; - private float InteriorH => EffectiveVisibleRows * RowHeight; private float OuterW => InteriorW + 2 * Border; private float OuterH => InteriorH + 2 * Border; @@ -389,11 +362,6 @@ public sealed class UiMenu : UiElement /// a full render pass. public float PopupOuterHeight => OuterH; - /// The popup's outer (bevel-inclusive) width. Exposed alongside - /// so the retail hide-disabled scrollbar rule - /// can be pinned without a GPU render harness. - public float PopupOuterWidth => OuterW; - /// /// G7 (vendor gate finding, item 2 — popup direction): port of retail /// UIElement_Menu::Open (pc:120210-120252, 0x0046cc30)'s Y placement: @@ -590,11 +558,10 @@ public sealed class UiMenu : UiElement /// /// G5: single-column popup with a docked scrollbar — port of the vendor category /// dropdown's authored shape (LayoutDesc 0x21000043, see 's - /// doc comment). Draws rows, sliced from - /// starting at , plus the scrollbar - /// chrome when its authored disabled-presentation rule allows it. Thumb geometry - /// is the same helper itself uses - /// (). + /// doc comment). Draws exactly rows (the authored visible + /// window), sliced from starting at , plus + /// the scrollbar chrome using the SAME thumb geometry itself + /// uses (). /// private void DrawScrollablePopup(UiRenderContext ctx, Func resolve) { @@ -635,9 +602,9 @@ public sealed class UiMenu : UiElement { int lineHeight = System.Math.Max(1, (int)MathF.Round(RowHeight)); PopupScroll.LineHeight = lineHeight; - // Size-to-content: view == content, so HasOverflow is false. Whether - // the disabled scrollbar remains visible is its authored 0x79 property, - // projected by IsPopupScrollbarPresentationVisible. + // Size-to-content: view == content, so HasOverflow is false and the + // scrollbar draws its chrome with no thumb (retail's authored + // scrollbar sibling stretches with the docked popup the same way). PopupScroll.SetExtents(Items.Count * lineHeight, EffectiveVisibleRows * lineHeight); } @@ -658,8 +625,6 @@ public sealed class UiMenu : UiElement private void DrawPopupScrollbar( UiRenderContext ctx, Func resolve, float x, float y) { - if (!IsPopupScrollbarPresentationVisible) return; - DrawSprite(ctx, resolve, ScrollTrackSprite, x, y, ScrollbarWidth, InteriorH); float decExtent = System.Math.Clamp(ScrollButtonExtent, 0f, InteriorH); @@ -836,9 +801,7 @@ public sealed class UiMenu : UiElement } float scrollbarX = ColumnWidth; - if (IsPopupScrollbarPresentationVisible - && ix >= scrollbarX && ix < scrollbarX + ScrollbarWidth - && iy >= 0 && iy < InteriorH) + if (ix >= scrollbarX && ix < scrollbarX + ScrollbarWidth && iy >= 0 && iy < InteriorH) { ConfigurePopupScroll(); float decExtent = System.Math.Clamp(ScrollButtonExtent, 0f, InteriorH); diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs index a477a4a0..71fc7202 100644 --- a/src/AcDream.App/UI/UiText.cs +++ b/src/AcDream.App/UI/UiText.cs @@ -485,13 +485,15 @@ public sealed class UiText : UiElement, IUiDatStateful // Per-state Invisible (dat property 0x3B): retail's SetState applies the // committed state's properties through UIElement::OnSetAttribute, whose // case 8 (@0x00462DAE, property id 0x33 + 8 = 0x3B) is - // `SetVisible(value == 0)`. #408 includes DirectState: returning to - // state 0 must restore its authored visibility after a named state - // changed it. First consumer here: the vitals cur/max number labels + // `SetVisible(value == 0)`. Same NAMED-states-only scoping as + // UiDatElement.TrySetRetailState (a DirectState 0x3B is the + // construction-time "authored invisible" class — #408, separately + // gated). First consumer here: the vitals cur/max number labels // (0x100000EB/ED/EF) author HideDetail={0x3B:false} / // ShowDetail={0x3B:true} — the numbers hide when the click toggle // switches the window to the graphical icon mode. - if (state is not null + if (stateId != UiStateInfo.DirectStateId + && state is not null && state.Properties.Values.TryGetValue(0x3Bu, out var invisibleProp) && invisibleProp.Kind == UiPropertyKind.Bool) Visible = !invisibleProp.BoolValue; diff --git a/src/AcDream.App/World/InventoryWorldDropProjectionController.cs b/src/AcDream.App/World/InventoryWorldDropProjectionController.cs index 3f671d28..40db242e 100644 --- a/src/AcDream.App/World/InventoryWorldDropProjectionController.cs +++ b/src/AcDream.App/World/InventoryWorldDropProjectionController.cs @@ -2,7 +2,6 @@ using AcDream.App.UI; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; -using AcDream.Core.Selection; namespace AcDream.App.World; @@ -27,7 +26,6 @@ internal sealed class InventoryWorldDropProjectionController : IDisposable private readonly ClientObjectTable _objects; private readonly LiveEntityRuntime _runtime; private readonly LiveEntityHydrationController _hydration; - private readonly SelectionState _selection; private readonly PendingSplitToWorldProjection _pending; private readonly Func _now; private bool _disposed; @@ -37,7 +35,6 @@ internal sealed class InventoryWorldDropProjectionController : IDisposable ClientObjectTable objects, LiveEntityRuntime runtime, LiveEntityHydrationController hydration, - SelectionState selection, Func now) { _interaction = interaction @@ -46,7 +43,6 @@ internal sealed class InventoryWorldDropProjectionController : IDisposable _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _hydration = hydration ?? throw new ArgumentNullException(nameof(hydration)); - _selection = selection ?? throw new ArgumentNullException(nameof(selection)); _now = now ?? throw new ArgumentNullException(nameof(now)); _pending = new PendingSplitToWorldProjection(); @@ -68,15 +64,7 @@ internal sealed class InventoryWorldDropProjectionController : IDisposable // Consume before the synchronous CreateObject graph runs. Re-entrant // callbacks cannot bind the same split intent to a second GUID. _hydration.OnCreate(spawn); - bool recovered = _runtime.TryGetSnapshot(update.Guid, out _); - if (recovered) - { - // ACCWeenieObject::DeclareValid @0x0058E481 transfers the one - // global selection to the recognized split result. This is an - // automatic system transition, not a synthetic inventory click. - _selection.Select(update.Guid, SelectionChangeSource.System); - } - return recovered; + return _runtime.TryGetSnapshot(update.Guid, out _); } public void Dispose() diff --git a/src/AcDream.App/World/LiveEntityRuntime.cs b/src/AcDream.App/World/LiveEntityRuntime.cs index 69f55273..0b835795 100644 --- a/src/AcDream.App/World/LiveEntityRuntime.cs +++ b/src/AcDream.App/World/LiveEntityRuntime.cs @@ -1222,10 +1222,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource } if (record.FullCellId != token.ExactCellId || record.Canonical.PlacementCommitVersion - != token.PlacementCommitVersion - || record.Canonical.PhysicsBody is not { } body - || body.Position != projection.WorldPosition - || body.Orientation != projection.Orientation) + != token.PlacementCommitVersion) { // A newer move superseded this receipt's facts after the drain. // @@ -1267,13 +1264,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource // That is a behaviour change in the wrong direction, not a // restoration. // - // #323: compare the retained body's exact pose too. The receipt - // copied these floats directly from that body, so exact equality - // is the correct proof that its pose is still current. This - // catches StoreAcceptedDestinationPose's legitimate pose-only - // fallback without abusing PositionAuthorityVersion (which can - // advance while the receipt facts remain unchanged) or redefining - // PlacementCommitVersion for a store that committed no cell. + // The ONE supersession neither term covers is + // RuntimeRemotePlacementDriveController.StoreAcceptedDestinationPose: + // the far-snap Refused/Contention arm writes `body.Position` and + // `body.Orientation` with no placement commit and no cell move, so + // it can stale this receipt's pose silently. That is pre-existing + // (it predates C5b, which changed nothing about that arm) and is + // filed as docs/ISSUES.md #323 rather than papered over here. return true; } diff --git a/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs b/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs index 00e54a9e..35ff578d 100644 --- a/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs +++ b/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs @@ -60,41 +60,6 @@ public static class ClientCommandRequests public const uint AddPlayerPermissionOpcode = 0x0219u; public const uint RemovePlayerPermissionOpcode = 0x021Au; public const uint AbandonHouseOpcode = 0x021Fu; - public const uint QueryAllegianceNameOpcode = 0x0030u; - public const uint ClearAllegianceNameOpcode = 0x0031u; - public const uint SetAllegianceNameOpcode = 0x0033u; - public const uint SetAllegianceOfficerOpcode = 0x003Bu; - public const uint SetAllegianceOfficerTitleOpcode = 0x003Cu; - public const uint ListAllegianceOfficerTitlesOpcode = 0x003Du; - public const uint ClearAllegianceOfficerTitlesOpcode = 0x003Eu; - public const uint DoAllegianceLockActionOpcode = 0x003Fu; - public const uint SetAllegianceApprovedVassalOpcode = 0x0040u; - public const uint AllegianceChatGagOpcode = 0x0041u; - public const uint DoAllegianceHouseActionOpcode = 0x0042u; - public const uint AddPermanentGuestOpcode = 0x0245u; - public const uint RemovePermanentGuestOpcode = 0x0246u; - public const uint SetOpenHouseStatusOpcode = 0x0247u; - public const uint ChangeStoragePermissionOpcode = 0x0249u; - public const uint BootSpecificHouseGuestOpcode = 0x024Au; - public const uint RemoveAllStoragePermissionOpcode = 0x024Cu; - public const uint RequestFullGuestListOpcode = 0x024Du; - public const uint SetMotdOpcode = 0x0254u; - public const uint QueryMotdOpcode = 0x0255u; - public const uint ClearMotdOpcode = 0x0256u; - public const uint AddAllStoragePermissionOpcode = 0x025Cu; - public const uint RemoveAllPermanentGuestsOpcode = 0x025Eu; - public const uint BootEveryoneOpcode = 0x025Fu; - public const uint SetHooksVisibilityOpcode = 0x0266u; - public const uint ModifyAllegianceGuestPermissionOpcode = 0x0267u; - public const uint ModifyAllegianceStoragePermissionOpcode = 0x0268u; - public const uint BreakAllegianceBootOpcode = 0x0277u; - public const uint AllegianceChatBootOpcode = 0x02A0u; - public const uint AddAllegianceBanOpcode = 0x02A1u; - public const uint RemoveAllegianceBanOpcode = 0x02A2u; - public const uint ListAllegianceBansOpcode = 0x02A3u; - public const uint RemoveAllegianceOfficerOpcode = 0x02A5u; - public const uint ListAllegianceOfficersOpcode = 0x02A6u; - public const uint ClearAllegianceOfficersOpcode = 0x02A7u; // Batch C (Map/House toolbar panel, 2026-08-17): the query the House // tab needs to populate. ACE GameActionHouseQuery.cs: [GameAction( // GameActionType.HouseQuery)] (0x021E), Handle reads no payload and @@ -318,9 +283,8 @@ public static class ClientCommandRequests public static byte[] BuildRecallAllegianceHometown(uint sequence) => BuildParameterless(sequence, RecallAllegianceHometownOpcode); - // "@allegiance info " — GameActionAllegianceInfoRequest.Handle: - // ReadString16L() player name. Retail refuses an empty argument before - // constructing this request. + // "@allegiance info [name]" — GameActionAllegianceInfoRequest.Handle: + // ReadString16L() player name (empty string = self). public static byte[] BuildAllegianceInfoRequest(uint sequence, string playerName) => BuildString(sequence, AllegianceInfoRequestOpcode, playerName); @@ -344,133 +308,6 @@ public static class ClientCommandRequests public static byte[] BuildAbandonHouse(uint sequence) => BuildParameterless(sequence, AbandonHouseOpcode); - // Complete named-retail @allegiance/@house administration family. - // Opcodes and field order are cross-checked against ACE's individual - // GameAction readers; the client-side grammar is ported separately by - // ClientCommandController from DoAllegiance/DoHouse and their helpers. - public static byte[] BuildBreakAllegianceBoot( - uint sequence, string playerName, bool accountBoot) => - BuildStringUInt32( - sequence, BreakAllegianceBootOpcode, playerName, accountBoot ? 1u : 0u); - - public static byte[] BuildAllegianceChatBoot( - uint sequence, string playerName, string reason) => - BuildTwoStrings(sequence, AllegianceChatBootOpcode, playerName, reason); - - public static byte[] BuildAllegianceChatGag( - uint sequence, string playerName, bool enabled) => - BuildStringUInt32( - sequence, AllegianceChatGagOpcode, playerName, enabled ? 1u : 0u); - - public static byte[] BuildAddAllegianceBan(uint sequence, string playerName) => - BuildString(sequence, AddAllegianceBanOpcode, playerName); - - public static byte[] BuildRemoveAllegianceBan(uint sequence, string playerName) => - BuildString(sequence, RemoveAllegianceBanOpcode, playerName); - - public static byte[] BuildListAllegianceBans(uint sequence) => - BuildParameterless(sequence, ListAllegianceBansOpcode); - - public static byte[] BuildSetAllegianceOfficer( - uint sequence, string playerName, uint officerLevel) => - BuildStringUInt32( - sequence, SetAllegianceOfficerOpcode, playerName, officerLevel); - - public static byte[] BuildRemoveAllegianceOfficer( - uint sequence, string playerName) => - BuildString(sequence, RemoveAllegianceOfficerOpcode, playerName); - - public static byte[] BuildListAllegianceOfficers(uint sequence) => - BuildParameterless(sequence, ListAllegianceOfficersOpcode); - - public static byte[] BuildClearAllegianceOfficers(uint sequence) => - BuildParameterless(sequence, ClearAllegianceOfficersOpcode); - - public static byte[] BuildSetAllegianceOfficerTitle( - uint sequence, uint officerLevel, string title) => - BuildUInt32String( - sequence, SetAllegianceOfficerTitleOpcode, officerLevel, title); - - public static byte[] BuildListAllegianceOfficerTitles(uint sequence) => - BuildParameterless(sequence, ListAllegianceOfficerTitlesOpcode); - - public static byte[] BuildClearAllegianceOfficerTitles(uint sequence) => - BuildParameterless(sequence, ClearAllegianceOfficerTitlesOpcode); - - public static byte[] BuildQueryAllegianceName(uint sequence) => - BuildParameterless(sequence, QueryAllegianceNameOpcode); - - public static byte[] BuildSetAllegianceName(uint sequence, string name) => - BuildString(sequence, SetAllegianceNameOpcode, name); - - public static byte[] BuildClearAllegianceName(uint sequence) => - BuildParameterless(sequence, ClearAllegianceNameOpcode); - - public static byte[] BuildAllegianceLockAction(uint sequence, uint action) => - BuildUInt32(sequence, DoAllegianceLockActionOpcode, action); - - public static byte[] BuildSetAllegianceApprovedVassal( - uint sequence, string playerName) => - BuildString(sequence, SetAllegianceApprovedVassalOpcode, playerName); - - public static byte[] BuildAllegianceHouseAction(uint sequence, uint action) => - BuildUInt32(sequence, DoAllegianceHouseActionOpcode, action); - - public static byte[] BuildQueryMotd(uint sequence) => - BuildParameterless(sequence, QueryMotdOpcode); - - public static byte[] BuildSetMotd(uint sequence, string motd) => - BuildString(sequence, SetMotdOpcode, motd); - - public static byte[] BuildClearMotd(uint sequence) => - BuildParameterless(sequence, ClearMotdOpcode); - - public static byte[] BuildSetOpenHouseStatus(uint sequence, bool isOpen) => - BuildUInt32(sequence, SetOpenHouseStatusOpcode, isOpen ? 1u : 0u); - - public static byte[] BuildAddPermanentGuest(uint sequence, string playerName) => - BuildString(sequence, AddPermanentGuestOpcode, playerName); - - public static byte[] BuildRemovePermanentGuest(uint sequence, string playerName) => - BuildString(sequence, RemovePermanentGuestOpcode, playerName); - - public static byte[] BuildRemoveAllPermanentGuests(uint sequence) => - BuildParameterless(sequence, RemoveAllPermanentGuestsOpcode); - - public static byte[] BuildChangeStoragePermission( - uint sequence, string playerName, bool enabled) => - BuildStringUInt32( - sequence, ChangeStoragePermissionOpcode, playerName, enabled ? 1u : 0u); - - public static byte[] BuildAddAllStoragePermission(uint sequence) => - BuildParameterless(sequence, AddAllStoragePermissionOpcode); - - public static byte[] BuildRemoveAllStoragePermission(uint sequence) => - BuildParameterless(sequence, RemoveAllStoragePermissionOpcode); - - public static byte[] BuildRequestFullGuestList(uint sequence) => - BuildParameterless(sequence, RequestFullGuestListOpcode); - - public static byte[] BuildBootSpecificHouseGuest( - uint sequence, string playerName) => - BuildString(sequence, BootSpecificHouseGuestOpcode, playerName); - - public static byte[] BuildBootEveryone(uint sequence) => - BuildParameterless(sequence, BootEveryoneOpcode); - - public static byte[] BuildSetHooksVisibility(uint sequence, bool visible) => - BuildUInt32(sequence, SetHooksVisibilityOpcode, visible ? 1u : 0u); - - public static byte[] BuildModifyAllegianceGuestPermission( - uint sequence, bool enabled) => - BuildUInt32( - sequence, ModifyAllegianceGuestPermissionOpcode, enabled ? 1u : 0u); - - public static byte[] BuildModifyAllegianceStoragePermission( - uint sequence, bool enabled) => - BuildUInt32( - sequence, ModifyAllegianceStoragePermissionOpcode, enabled ? 1u : 0u); - // Queries the local player's house info (owned house data, or a // no-house status) — GameActionHouseQuery.Handle: no payload read. public static byte[] BuildHouseQuery(uint sequence) => @@ -500,39 +337,6 @@ public static class ClientCommandRequests return body; } - private static byte[] BuildStringUInt32( - uint sequence, uint opcode, string value, uint number) - { - byte[] packed = PackString16L(value); - byte[] body = CreateBody(sequence, opcode, packed.Length + 4); - packed.CopyTo(body, 12); - BinaryPrimitives.WriteUInt32LittleEndian( - body.AsSpan(12 + packed.Length), number); - return body; - } - - private static byte[] BuildUInt32String( - uint sequence, uint opcode, uint number, string value) - { - byte[] packed = PackString16L(value); - byte[] body = CreateBody(sequence, opcode, 4 + packed.Length); - BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), number); - packed.CopyTo(body, 16); - return body; - } - - private static byte[] BuildTwoStrings( - uint sequence, uint opcode, string first, string second) - { - byte[] packedFirst = PackString16L(first); - byte[] packedSecond = PackString16L(second); - byte[] body = CreateBody( - sequence, opcode, packedFirst.Length + packedSecond.Length); - packedFirst.CopyTo(body, 12); - packedSecond.CopyTo(body, 12 + packedFirst.Length); - return body; - } - private static byte[] CreateBody(uint sequence, uint opcode, int payloadLength) { byte[] body = new byte[12 + payloadLength]; diff --git a/src/AcDream.Core.Net/Transport/AckNakScheduler.cs b/src/AcDream.Core.Net/Transport/AckNakScheduler.cs index eefb1924..1b46a51a 100644 --- a/src/AcDream.Core.Net/Transport/AckNakScheduler.cs +++ b/src/AcDream.Core.Net/Transport/AckNakScheduler.cs @@ -275,6 +275,5 @@ internal sealed class AckNakScheduler outboundIsaac: null); _send(datagram.Slice(0, datagramLength)); _stats.NaksSent++; - _stats.NakIdsSent += count; } } diff --git a/src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs b/src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs index e852c877..26c2ee87 100644 --- a/src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs +++ b/src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs @@ -263,23 +263,6 @@ internal sealed class OutboundFlowQueue : IDisposable { for (int i = 0; i < _pendingResends.Count; i++) { - // A later cumulative ACK can overtake an earlier NAK in the - // receive-owner queue before this frame reaches its sweep. - // Such a request is stale: the server has already advanced - // its ordered watermark past the requested packet. Replaying - // the old encrypted key is actively harmful against ACE, - // whose CRC/key search runs before duplicate-sequence - // rejection and can burn the remaining 256-word window. - // The equal case is NOT stale: ids[0] of an ordinary NAK is - // also its implicit ACK and still needs retransmission. - if (AckWatermark != 0 - && SequenceMath.IsNewer( - AckWatermark, - _pendingResends[i])) - { - continue; - } - // A pending id can leave the cache between NAK arrival and // this sweep only if a newer ack already covered it — the // server has it; serving nothing is correct. diff --git a/src/AcDream.Core.Net/Transport/ReliableTransport.cs b/src/AcDream.Core.Net/Transport/ReliableTransport.cs index 91f609ec..6fff835f 100644 --- a/src/AcDream.Core.Net/Transport/ReliableTransport.cs +++ b/src/AcDream.Core.Net/Transport/ReliableTransport.cs @@ -41,9 +41,6 @@ internal sealed class ReliableTransport : IDisposable public TransportStats Stats { get; } - /// Retail's 40-heartbeat packet-loss percentage. - public double PacketLossPercentage => _packetLoss.Percentage; - /// /// N6: retail's ephemeral-info flush cadence /// (Indicator::FlushTimedOutEphInfo @ 0x0054A3D0, the x87 compare @@ -54,7 +51,6 @@ internal sealed class ReliableTransport : IDisposable public const double AssemblerSweepSeconds = 5.0; private readonly FragmentAssembler? _assembler; - private readonly RetailPacketLossAverager _packetLoss; private readonly long _assemblerSweepTicks; private long _assemblerSweepTimestamp; @@ -75,19 +71,13 @@ internal sealed class ReliableTransport : IDisposable _assemblerSweepTicks = (long)Math.Round(AssemblerSweepSeconds * Clock.Frequency); _assemblerSweepTimestamp = Clock.GetTimestamp(); - void CountedSend(ReadOnlySpan datagram) - { - send(datagram); - Stats.PacketsSent++; - } - Outbound = new OutboundFlowQueue( outboundIsaac, sessionClientId, sessionIteration, Clock, Stats, - CountedSend, + send, pool); Inbound = new InboundSequenceTracker(inboundIsaac, Stats); Scheduler = new AckNakScheduler( @@ -97,9 +87,8 @@ internal sealed class ReliableTransport : IDisposable sessionClientId, sessionIteration, Stats, - CountedSend); + send); Stats.CacheDepthSource = () => Outbound.CacheDepth; - _packetLoss = new RetailPacketLossAverager(Clock, Stats); } /// Last reliable sequence on the wire — the value unsequenced @@ -123,12 +112,6 @@ internal sealed class ReliableTransport : IDisposable { Clock.Update(); long now = Clock.GetTimestamp(); - // Retail snapshots the preceding two-second interval inside - // ClientNet::ProcessConnection before this heartbeat emits its - // ACK/NAK control traffic. Keep boundary control packets in the new - // sample rather than attributing them to the interval that just - // ended. - _packetLoss.Sweep(now, Stats); Scheduler.Sweep(now); Outbound.TransmitPendingResends(); diff --git a/src/AcDream.Core.Net/Transport/RetailPacketLossAverager.cs b/src/AcDream.Core.Net/Transport/RetailPacketLossAverager.cs deleted file mode 100644 index c2e0fec7..00000000 --- a/src/AcDream.Core.Net/Transport/RetailPacketLossAverager.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace AcDream.Core.Net.Transport; - -/// -/// Retail's packet-loss telemetry window from -/// CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610 and -/// CLinkStatusAverages::AddSnapshot @ 0x00546650. -/// -/// -/// -/// ClientNet::ProcessConnection @ 0x00545450 snapshots the four -/// packet counters on its 2.0-second heartbeat. Each counter is a -/// CAverager<unsigned short,40>. The loss function divides the -/// windowed NAK + retransmit totals by received + sent totals and reports a -/// percentage. A zero denominator reports zero. -/// -/// -/// The input values here are cumulative acdream counters; each sample stores -/// their delta since the preceding heartbeat. The unchecked ushort conversion -/// preserves retail's snapshot-field width. -/// -/// -internal sealed class RetailPacketLossAverager -{ - public const int WindowSize = 40; - public const double SnapshotSeconds = 2.0; - - private readonly Sample[] _samples = new Sample[WindowSize]; - private readonly long _snapshotTicks; - private long _snapshotTimestamp; - private long _lastPacketsSent; - private long _lastRetransmitsSent; - private long _lastPacketsReceived; - private long _lastNakIdsSent; - private long _sentTotal; - private long _retransmitTotal; - private long _receivedTotal; - private long _nakTotal; - private int _next; - private int _count; - - public RetailPacketLossAverager(TransportClock clock, TransportStats stats) - { - ArgumentNullException.ThrowIfNull(clock); - ArgumentNullException.ThrowIfNull(stats); - - _snapshotTicks = (long)Math.Round(SnapshotSeconds * clock.Frequency); - _snapshotTimestamp = clock.GetTimestamp(); - CaptureBaselines(stats); - } - - public double Percentage - { - get - { - long denominator = _receivedTotal + _sentTotal; - return denominator <= 0 - ? 0d - : 100d * (_nakTotal + _retransmitTotal) / denominator; - } - } - - public void Sweep(long now, TransportStats stats) - { - ArgumentNullException.ThrowIfNull(stats); - if (now - _snapshotTimestamp < _snapshotTicks) - return; - - // ProcessConnection records one snapshot when the heartbeat branch - // runs; it does not synthesize empty samples for skipped frames. - _snapshotTimestamp = now; - - var sample = new Sample( - DeltaAsRetailUShort(stats.PacketsSent, ref _lastPacketsSent), - DeltaAsRetailUShort(stats.ResendsSent, ref _lastRetransmitsSent), - DeltaAsRetailUShort(stats.PacketsReceived, ref _lastPacketsReceived), - DeltaAsRetailUShort(stats.NakIdsSent, ref _lastNakIdsSent)); - - if (_count == WindowSize) - Remove(_samples[_next]); - else - _count++; - - _samples[_next] = sample; - _next = (_next + 1) % WindowSize; - Add(sample); - } - - private void CaptureBaselines(TransportStats stats) - { - _lastPacketsSent = stats.PacketsSent; - _lastRetransmitsSent = stats.ResendsSent; - _lastPacketsReceived = stats.PacketsReceived; - _lastNakIdsSent = stats.NakIdsSent; - } - - private static ushort DeltaAsRetailUShort(long current, ref long previous) - { - long delta = current - previous; - previous = current; - return unchecked((ushort)Math.Max(0L, delta)); - } - - private void Add(Sample sample) - { - _sentTotal += sample.Sent; - _retransmitTotal += sample.Retransmitted; - _receivedTotal += sample.Received; - _nakTotal += sample.Naked; - } - - private void Remove(Sample sample) - { - _sentTotal -= sample.Sent; - _retransmitTotal -= sample.Retransmitted; - _receivedTotal -= sample.Received; - _nakTotal -= sample.Naked; - } - - private readonly record struct Sample( - ushort Sent, - ushort Retransmitted, - ushort Received, - ushort Naked); -} diff --git a/src/AcDream.Core.Net/Transport/TransportStats.cs b/src/AcDream.Core.Net/Transport/TransportStats.cs index 105f176e..4b998af9 100644 --- a/src/AcDream.Core.Net/Transport/TransportStats.cs +++ b/src/AcDream.Core.Net/Transport/TransportStats.cs @@ -9,15 +9,6 @@ namespace AcDream.Core.Net.Transport; /// internal sealed class TransportStats { - /// Checksum-valid post-negotiation datagrams received. Used by - /// retail's 2-second/40-sample link-status loss window. - public long PacketsReceived; - - /// Datagrams successfully emitted after transport negotiation. - /// Includes reliable traffic, retransmits, cumulative ACKs, and NAKs, - /// matching retail's packet-level denominator. - public long PacketsSent; - /// Datagrams re-emitted in response to a server NAK. public long ResendsSent; @@ -66,11 +57,6 @@ internal sealed class TransportStats /// ReceiverData::GetNaks @ 0x005490C0, ≤114 ids each). public long NaksSent; - /// Total missing sequence ids carried by emitted NAKs. Retail's - /// CLinkStatusSnapshot::nPktsNAKed counts missing packets, not the - /// number of control datagrams used to request them. - public long NakIdsSent; - /// N4: mis-parked keystream words reclaimed from validated /// cleartext RejectRetransmit sequences (the AD-51 ACE /// adaptation; always zero against a retail server). diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index d2ede3e4..6bc9ea59 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -652,8 +652,7 @@ public sealed class WorldSession : IDisposable Volatile.Read(ref _lastInboundPacketTicks), Stopwatch.GetTimestamp(), Stopwatch.Frequency, - PingRoundTripSeconds, - _transport?.PacketLossPercentage ?? 0d); + PingRoundTripSeconds); internal double? PingRoundTripSeconds { @@ -1862,8 +1861,6 @@ public sealed class WorldSession : IDisposable // last-heard clock only for valid server traffic. Record at checksum // acceptance, before any heavy render-thread message handling. Volatile.Write(ref _lastInboundPacketTicks, Stopwatch.GetTimestamp()); - if (_transport is { } acceptedTransport) - acceptedTransport.Stats.PacketsReceived++; // N6: the first checksum-valid post-negotiation packet confirms the // server accepted our ConnectResponse and stops the handshake @@ -2817,234 +2814,13 @@ public sealed class WorldSession : IDisposable SendGameAction(ClientCommandRequests.BuildRecallAllegianceHometown(seq)); } - /// Send retail "@allegiance info <name>" (0x027B). + /// Send retail "@allegiance info [name]" (0x027B). public void SendAllegianceInfoRequest(string playerName) { uint seq = NextGameActionSequence(); SendGameAction(ClientCommandRequests.BuildAllegianceInfoRequest(seq, playerName)); } - public void SendBreakAllegianceBoot(string playerName, bool accountBoot) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildBreakAllegianceBoot( - seq, playerName, accountBoot)); - } - - public void SendAllegianceChatBoot(string playerName, string reason) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildAllegianceChatBoot( - seq, playerName, reason)); - } - - public void SendAllegianceChatGag(string playerName, bool enabled) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildAllegianceChatGag( - seq, playerName, enabled)); - } - - public void SendAddAllegianceBan(string playerName) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildAddAllegianceBan(seq, playerName)); - } - - public void SendRemoveAllegianceBan(string playerName) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildRemoveAllegianceBan(seq, playerName)); - } - - public void SendListAllegianceBans() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildListAllegianceBans(seq)); - } - - public void SendSetAllegianceOfficer(string playerName, uint level) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildSetAllegianceOfficer( - seq, playerName, level)); - } - - public void SendRemoveAllegianceOfficer(string playerName) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildRemoveAllegianceOfficer( - seq, playerName)); - } - - public void SendListAllegianceOfficers() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildListAllegianceOfficers(seq)); - } - - public void SendClearAllegianceOfficers() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildClearAllegianceOfficers(seq)); - } - - public void SendSetAllegianceOfficerTitle(uint level, string title) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildSetAllegianceOfficerTitle( - seq, level, title)); - } - - public void SendListAllegianceOfficerTitles() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildListAllegianceOfficerTitles(seq)); - } - - public void SendClearAllegianceOfficerTitles() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildClearAllegianceOfficerTitles(seq)); - } - - public void SendQueryAllegianceName() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildQueryAllegianceName(seq)); - } - - public void SendSetAllegianceName(string name) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildSetAllegianceName(seq, name)); - } - - public void SendClearAllegianceName() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildClearAllegianceName(seq)); - } - - public void SendAllegianceLockAction(uint action) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildAllegianceLockAction(seq, action)); - } - - public void SendSetAllegianceApprovedVassal(string playerName) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildSetAllegianceApprovedVassal( - seq, playerName)); - } - - public void SendAllegianceHouseAction(uint action) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildAllegianceHouseAction(seq, action)); - } - - public void SendQueryMotd() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildQueryMotd(seq)); - } - - public void SendSetMotd(string motd) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildSetMotd(seq, motd)); - } - - public void SendClearMotd() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildClearMotd(seq)); - } - - public void SendSetOpenHouseStatus(bool isOpen) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildSetOpenHouseStatus(seq, isOpen)); - } - - public void SendAddPermanentGuest(string playerName) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildAddPermanentGuest(seq, playerName)); - } - - public void SendRemovePermanentGuest(string playerName) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildRemovePermanentGuest(seq, playerName)); - } - - public void SendRemoveAllPermanentGuests() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildRemoveAllPermanentGuests(seq)); - } - - public void SendChangeStoragePermission(string playerName, bool enabled) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildChangeStoragePermission( - seq, playerName, enabled)); - } - - public void SendAddAllStoragePermission() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildAddAllStoragePermission(seq)); - } - - public void SendRemoveAllStoragePermission() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildRemoveAllStoragePermission(seq)); - } - - public void SendRequestFullGuestList() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildRequestFullGuestList(seq)); - } - - public void SendBootSpecificHouseGuest(string playerName) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildBootSpecificHouseGuest( - seq, playerName)); - } - - public void SendBootEveryone() - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildBootEveryone(seq)); - } - - public void SendSetHooksVisibility(bool visible) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildSetHooksVisibility(seq, visible)); - } - - public void SendModifyAllegianceGuestPermission(bool enabled) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildModifyAllegianceGuestPermission( - seq, enabled)); - } - - public void SendModifyAllegianceStoragePermission(bool enabled) - { - uint seq = NextGameActionSequence(); - SendGameAction(ClientCommandRequests.BuildModifyAllegianceStoragePermission( - seq, enabled)); - } - // ── Campaign FA slice FA2 (2026-08-12): fellowship + allegiance // outbound wrappers. SocialActions/AllegianceRequests ship the byte // builders (repaired/added in FA1); this is the missing diff --git a/src/AcDream.Core/Audio/DatSoundCache.cs b/src/AcDream.Core/Audio/DatSoundCache.cs index 22605c65..a04190e4 100644 --- a/src/AcDream.Core/Audio/DatSoundCache.cs +++ b/src/AcDream.Core/Audio/DatSoundCache.cs @@ -81,7 +81,6 @@ public sealed class DatSoundCache private readonly Dictionary> _waveEntries = new(); private readonly LinkedList _waveLru = new(); private readonly long _maxWaveBytes; - private readonly Action? _afterInitialWaveMiss; private long _residentWaveBytes; private long _hits; private long _misses; @@ -99,24 +98,11 @@ public sealed class DatSoundCache /// exercised deterministically without allocating tens of megabytes. /// public DatSoundCache(IDatObjectSource dats, long maxWaveBytes) - : this(dats, maxWaveBytes, afterInitialWaveMiss: null) - { - } - - /// - /// Deterministic concurrency seam used to park a caller after its fast - /// resident lookup but before the authoritative locked recheck. - /// - internal DatSoundCache( - IDatObjectSource dats, - long maxWaveBytes, - Action? afterInitialWaveMiss) { ArgumentNullException.ThrowIfNull(dats); ArgumentOutOfRangeException.ThrowIfLessThan(maxWaveBytes, 1); _dats = dats; _maxWaveBytes = maxWaveBytes; - _afterInitialWaveMiss = afterInitialWaveMiss; } /// @@ -142,37 +128,14 @@ public sealed class DatSoundCache return null; } - _afterInitialWaveMiss?.Invoke(waveId); + Interlocked.Increment(ref _misses); - Lazy lazy; - lock (_gate) - { - // A caller can pause after the fast miss while another caller - // decodes, admits, and removes the in-flight Lazy. Recheck the - // authoritative caches and acquire/create the Lazy under the same - // gate used by admission so that stale caller cannot start a - // second decode after publication. - if (_waveEntries.TryGetValue(waveId, out var node)) - { - Interlocked.Increment(ref _hits); - Touch(node); - return node.Value.Wave; - } - - if (_negativeWaveIds.ContainsKey(waveId)) - { - Interlocked.Increment(ref _hits); - return null; - } - - Interlocked.Increment(ref _misses); - lazy = _inflight.GetOrAdd( - waveId, - static (id, self) => new Lazy( - () => self.DecodeUncached(id), - LazyThreadSafetyMode.ExecutionAndPublication), - this); - } + Lazy lazy = _inflight.GetOrAdd( + waveId, + static (id, self) => new Lazy( + () => self.DecodeUncached(id), + LazyThreadSafetyMode.ExecutionAndPublication), + this); try { WaveData? decoded = lazy.Value; diff --git a/src/AcDream.Core/Chat/ChatLog.cs b/src/AcDream.Core/Chat/ChatLog.cs index aa8efedc..56d86d8f 100644 --- a/src/AcDream.Core/Chat/ChatLog.cs +++ b/src/AcDream.Core/Chat/ChatLog.cs @@ -179,28 +179,14 @@ public sealed class ChatLog /// PlayerKilled (0x019E) — death announcement. /// - /// Retail ClientCombatSystem::HandlePlayerDeathEvent @0x0056C320 - /// suppresses this bystander-facing line when the local player is either - /// the victim or killer; those participants receive their dedicated - /// 0x01AC/0x01AD notification instead. Death messages that survive that - /// gate are routed as so they share styling - /// with other server announcements. The + /// Death messages are routed as so they + /// share styling with other server announcements. The /// SenderGuid field carries the victim guid; the /// ChannelId field carries the killer guid (a small misuse /// of the field but avoids a schema change). /// - public void OnPlayerKilled( - string deathMessage, - uint victimGuid, - uint killerGuid, - uint localPlayerGuid = 0u) + public void OnPlayerKilled(string deathMessage, uint victimGuid, uint killerGuid) { - if (localPlayerGuid != 0u - && (localPlayerGuid == victimGuid || localPlayerGuid == killerGuid)) - { - return; - } - Append(new ChatEntry( Kind: ChatKind.System, Sender: "", diff --git a/src/AcDream.Core/Lighting/LightManager.cs b/src/AcDream.Core/Lighting/LightManager.cs index 7a63e96c..c25a476d 100644 --- a/src/AcDream.Core/Lighting/LightManager.cs +++ b/src/AcDream.Core/Lighting/LightManager.cs @@ -271,10 +271,8 @@ public sealed class LightManager /// geometrically closer than the player's own room's torches and win the cap, /// leaving the visible room dark. Scoping candidacy to the frame's actual /// visible cells (the render already computes this — callers pass last frame's - /// RetailPViewFrameResult.InViewCells, the main flood PLUS building - /// look-in cells, one frame of latency, to avoid re-threading a mid-render - /// callback; flood-only scoping darkened look-in rooms' lanterns) removes - /// non-visible cells from contention before the + /// RetailPViewFrameResult.DrawableCells, one frame of latency, to avoid + /// re-threading a mid-render callback) removes those from contention before the /// cap ever applies. The distance-sort anchor stays the PLAYER either way — this /// parameter only narrows candidacy, it does not change the sort (the #176 /// correction: CAMERA anchoring, not cell scoping itself, caused the earlier diff --git a/src/AcDream.Core/Physics/PhysicsTimestampGate.cs b/src/AcDream.Core/Physics/PhysicsTimestampGate.cs index a919f72b..0b9e5ad2 100644 --- a/src/AcDream.Core/Physics/PhysicsTimestampGate.cs +++ b/src/AcDream.Core/Physics/PhysicsTimestampGate.cs @@ -192,12 +192,11 @@ public sealed class PhysicsTimestampGate _timestamps[ForcePosition] = forcePosition; // SmartBox::HandleReceivedPosition 0x00453FD0: a fresh local - // FORCE_POSITION whose teleport stamp is not older (equal OR - // newer) blips immediately, assigns POSITION_TS directly (even - // equal/older), preserves the current heading, sends a position - // event, and returns WITHOUT advancing TELEPORT_TS. The same - // wrap-safe predicate is used by IsFreshTeleportStart above. - if (!IsNewer(teleport, _timestamps[Teleport])) + // FORCE_POSITION whose teleport is exactly equal blips immediately, + // assigns POSITION_TS directly (even equal/older), preserves the + // current heading, sends a position event, and returns WITHOUT + // advancing TELEPORT_TS. + if (teleport == _timestamps[Teleport]) { _timestamps[Position] = position; return PositionTimestampDisposition.ForcePosition; diff --git a/src/AcDream.Core/Physics/RetailAnimationCyclePlayback.cs b/src/AcDream.Core/Physics/RetailAnimationCyclePlayback.cs index 4bdc9d7d..788dfe8f 100644 --- a/src/AcDream.Core/Physics/RetailAnimationCyclePlayback.cs +++ b/src/AcDream.Core/Physics/RetailAnimationCyclePlayback.cs @@ -16,13 +16,26 @@ namespace AcDream.Core.Physics; /// transitional blending. /// /// -/// This is the shared implementation used by both the "legacy" (no -/// ) NPC idle-cycle path and the chargen -/// preview. The live presenter has a live entity, a +/// This exact advance-with-wrap-then-lerp/slerp algorithm already exists as +/// an inline, App-layer-only implementation for the "legacy" (no +/// ) NPC idle-cycle path — +/// LiveEntityAnimationPresenter.Present's non-sequencer branch +/// (CurrFrame += legacyAdvanceSeconds * Framerate with the same +/// modulo wrap) and its private TryResolvePartFrame helper (the same +/// frame-bracket lerp/slerp). That call site has a live entity, a /// LiveEntityRuntime membership, and per-tick elapsed time supplied by /// the render loop; the chargen preview has none of that (there is no live -/// entity — character creation hasn't happened yet), so neither consumer can -/// own the primitive. Keeping it here prevents the two paths from drifting. +/// entity — character creation hasn't happened yet), so it cannot reuse that +/// class directly. Rather than re-typing the same formula a second time, +/// this Core, pure, unit-testable class is the shared primitive: the +/// chargen preview (AcDream.App.Rendering.ChargenPreviewAnimator) +/// consumes it directly, and it is safe for a future pass to redirect +/// LiveEntityAnimationPresenter's inline copy through it as a +/// behavior-preserving mechanical follow-up (not done here — that file is +/// live, heavily tested production entity-rendering code with zero relation +/// to this preview-only feature, so touching it is out of this slice's +/// blast radius by design, not oversight). Tracked as +/// docs/ISSUES.md #403 so the follow-up has an owner. /// /// public static class RetailAnimationCyclePlayback @@ -30,15 +43,14 @@ public static class RetailAnimationCyclePlayback /// /// Advances by elapsedSeconds * framerate /// and wraps it back into [lowFrame, highFrame] with the SAME modulo - /// shape retail playback and the live presenter's legacy branch use + /// shape LiveEntityAnimationPresenter.Present's legacy branch uses /// (over % (span + 1), not a plain clamp — a frame position that /// overshoots the end by more than one span wraps around more than once /// rather than sticking at the boundary, matching a long stall/resume). /// Returns unchanged for a degenerate cycle - /// ( <= ) or a - /// non-positive . A negative framerate - /// advances backward and clamps at the low frame, matching the former - /// live-presenter implementation exactly. + /// ( <= ), a + /// non-positive , or a non-positive + /// . /// public static float Advance( float currFrame, @@ -48,7 +60,7 @@ public static class RetailAnimationCyclePlayback float elapsedSeconds) { int span = highFrame - lowFrame; - if (span <= 0 || elapsedSeconds <= 0f) + if (span <= 0 || framerate <= 0f || elapsedSeconds <= 0f) return currFrame; float next = currFrame + elapsedSeconds * framerate; @@ -72,7 +84,8 @@ public static class RetailAnimationCyclePlayback /// back to ). Returns false — with /// default outputs — when is outside /// the bracketing frame's part list, matching - /// the live presenter's no-sequence-frames branch exactly. + /// LiveEntityAnimationPresenter.TryResolvePartFrame's no- + /// sequence-frames branch exactly. /// public static bool TryInterpolatePart( Animation animation, diff --git a/src/AcDream.Core/Vfx/ParticleSystem.cs b/src/AcDream.Core/Vfx/ParticleSystem.cs index 818587e6..4345c6f3 100644 --- a/src/AcDream.Core/Vfx/ParticleSystem.cs +++ b/src/AcDream.Core/Vfx/ParticleSystem.cs @@ -526,8 +526,7 @@ public sealed class ParticleSystem : IParticleSystem IReadOnlySet attachedOwnerIds, bool includeUnattached, List destination, - IReadOnlySet? excludedAttachedOwnerIds = null, - UnattachedEmitterCellScope unattachedCellScope = UnattachedEmitterCellScope.Any) + IReadOnlySet? excludedAttachedOwnerIds = null) { ArgumentNullException.ThrowIfNull(attachedOwnerIds); ArgumentNullException.ThrowIfNull(destination); @@ -540,11 +539,8 @@ public sealed class ParticleSystem : IParticleSystem foreach (int handle in _renderableUnattachedHandlesByPass[passIndex]) { LastRenderScopeEmitterVisitCount++; - if (_byHandle.TryGetValue(handle, out ParticleEmitter? emitter) - && MatchesUnattachedCellScope(emitter, unattachedCellScope)) - { + if (_byHandle.TryGetValue(handle, out ParticleEmitter? emitter)) destination.Add(emitter); - } } } @@ -570,29 +566,6 @@ public sealed class ParticleSystem : IParticleSystem destination.Sort(static (left, right) => left.Handle.CompareTo(right.Handle)); } - /// - /// Splits unattached emitters by their owner cell kind so each draws once - /// in its retail stage: an outdoor landcell emitter belongs to the - /// landscape stage (before the depth clear), an interior EnvCell emitter - /// to the final world stage (after the seals). Retail gets this for free - /// because a particle draws during its owner CELL's walk turn - /// (CPhysicsObj::ShouldDrawParticles @0x0050FE60 reads the one cell). - /// AC cell convention: low word < 0x0100 is an outdoor landcell, - /// 0x0100..0xFFFD is an interior EnvCell. Cell 0 matches neither scoped - /// mode — such an emitter cannot pass the world in-view gate anyway. - /// - private static bool MatchesUnattachedCellScope( - ParticleEmitter emitter, - UnattachedEmitterCellScope scope) - { - if (scope == UnattachedEmitterCellScope.Any) - return true; - uint low = emitter.OwnerCellId & 0xFFFFu; - return scope == UnattachedEmitterCellScope.OutdoorCells - ? low != 0 && low < 0x0100u - : low >= 0x0100u; - } - public readonly struct LiveEmitterEnumerable : IEnumerable { private readonly ParticleSystem _owner; diff --git a/src/AcDream.Core/Vfx/VfxModel.cs b/src/AcDream.Core/Vfx/VfxModel.cs index a019a304..bdeb661c 100644 --- a/src/AcDream.Core/Vfx/VfxModel.cs +++ b/src/AcDream.Core/Vfx/VfxModel.cs @@ -46,20 +46,6 @@ public enum ParticleRenderPass SkyPostScene = 2, } -/// -/// Which unattached emitters a scoped render copy admits, by owner cell kind. -/// Retail draws every particle during its owner CELL's walk turn, so an -/// outdoor-cell emitter renders in the landscape stage and an interior-cell -/// emitter in the final world stage; acdream draws each group once in the -/// matching stage instead of per portal slice. -/// -public enum UnattachedEmitterCellScope -{ - Any = 0, - OutdoorCells = 1, - InteriorCells = 2, -} - /// /// Authority used by retail's particle presentation gate. World-owned /// emitters follow CPhysicsObj::ShouldDrawParticles; examination and diff --git a/src/AcDream.Core/World/SkyState.cs b/src/AcDream.Core/World/SkyState.cs index 6f4ab17f..f38e4a19 100644 --- a/src/AcDream.Core/World/SkyState.cs +++ b/src/AcDream.Core/World/SkyState.cs @@ -505,15 +505,6 @@ public sealed class WorldTimeService /// Current sky lighting state. public SkyKeyframe CurrentSky => _sky.Interpolate((float)DayFraction); - /// - /// Interpolate this clock's active day-group provider at an explicit day - /// fraction without changing the clock. Retail LScape::SetDay - /// uses this distinction: the sky continues advancing normally while - /// landscape lighting is sampled at noon. - /// - public SkyKeyframe SkyAtDayFraction(float dayFraction) => - _sky.Interpolate(dayFraction); - /// Convenience: current sun direction from derived sky state. public Vector3 CurrentSunDirection => SkyStateProvider.SunDirectionFromKeyframe(CurrentSky); diff --git a/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs b/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs index 9ff1ed74..46ab383f 100644 --- a/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs +++ b/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs @@ -4,7 +4,6 @@ using AcDream.Headless.Diagnostics; using AcDream.Headless.Plugins; using AcDream.Headless.Policies; using AcDream.Content.CharGen; -using AcDream.Core.Chat; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Runtime; @@ -958,16 +957,6 @@ internal sealed class HeadlessSessionHost : IDisposable case ClientCommandId.QueryBirth: session.SendQueryBirth(); return; - case ClientCommandId.TogglePersistentDaylight: - { - bool enabled = !runtime.CharacterOwner.Options.GetOptionBit( - CharacterOptionId.PersistentAtDay); - _ = runtime.CharacterOwner.Options.TrySetOption( - (uint)CharacterOptionId.PersistentAtDay, - enabled, - session.SendSetSingleCharacterOption); - return; - } case ClientCommandId.Emote when !string.IsNullOrWhiteSpace(command.Arguments): session.SendEmote(command.Arguments.Trim()); @@ -1015,27 +1004,7 @@ internal sealed class HeadlessSessionHost : IDisposable session.SendRecallAllegianceHometown(); return; case ClientCommandId.AllegianceInfo: - case ClientCommandId.AllegianceBoot: - case ClientCommandId.AllegianceBan: - case ClientCommandId.AllegianceChat: - case ClientCommandId.AllegianceBroadcast: - case ClientCommandId.AllegianceOfficer: - case ClientCommandId.AllegianceOfficerTitle: - case ClientCommandId.AllegianceName: - case ClientCommandId.AllegianceLock: - case ClientCommandId.AllegianceHouse: - case ClientCommandId.AllegianceMotd: - case ClientCommandId.AllegianceUnrecognizedSubcommand: - case ClientCommandId.HouseOpenStatus: - case ClientCommandId.HouseStorage: - case ClientCommandId.HouseBoot: - case ClientCommandId.HouseBootAll: - case ClientCommandId.HouseGuests: - case ClientCommandId.HouseHooks: - case ClientCommandId.HouseUnrecognizedSubcommand: - _ = CreateAdministrationDispatcher(session, runtime).TryExecute( - command.Command, - command.Arguments); + session.SendAllegianceInfoRequest(command.Arguments.Trim()); return; case ClientCommandId.Permit: ExecutePermit(session, command.Arguments); @@ -1108,61 +1077,6 @@ internal sealed class HeadlessSessionHost : IDisposable } } - private static RetailAdministrationCommandDispatcher - CreateAdministrationDispatcher( - AcDream.Core.Net.WorldSession session, - GameRuntime runtime) => new( - new RetailAdministrationCommandDispatcher.FeedbackBindings( - ShowSystemMessage: text => runtime.CommunicationOwner.AddText( - text, RetailLogTextType.Default), - ShowClientLocalMessage: text => runtime.CommunicationOwner.AddText( - text, RetailLogTextType.ClientLocal), - SetSingleCharacterOption: (optionId, enabled) => - _ = runtime.CharacterOwner.Options.TrySetOption( - optionId, - enabled, - session.SendSetSingleCharacterOption), - RequestAllegianceInfo: session.SendAllegianceInfoRequest), - new RetailAdministrationCommandDispatcher.ActionBindings( - BreakAllegianceBoot: session.SendBreakAllegianceBoot, - AllegianceChatBoot: session.SendAllegianceChatBoot, - AllegianceChatGag: session.SendAllegianceChatGag, - AllegianceBroadcast: text => session.SendChannel(0x02000000u, text), - ListAllegianceBans: session.SendListAllegianceBans, - AddAllegianceBan: session.SendAddAllegianceBan, - RemoveAllegianceBan: session.SendRemoveAllegianceBan, - ListAllegianceOfficers: session.SendListAllegianceOfficers, - ClearAllegianceOfficers: session.SendClearAllegianceOfficers, - SetAllegianceOfficer: session.SendSetAllegianceOfficer, - RemoveAllegianceOfficer: session.SendRemoveAllegianceOfficer, - ListAllegianceOfficerTitles: session.SendListAllegianceOfficerTitles, - ClearAllegianceOfficerTitles: session.SendClearAllegianceOfficerTitles, - SetAllegianceOfficerTitle: session.SendSetAllegianceOfficerTitle, - QueryAllegianceName: session.SendQueryAllegianceName, - SetAllegianceName: session.SendSetAllegianceName, - ClearAllegianceName: session.SendClearAllegianceName, - AllegianceLockAction: session.SendAllegianceLockAction, - SetAllegianceApprovedVassal: session.SendSetAllegianceApprovedVassal, - AllegianceHouseAction: session.SendAllegianceHouseAction, - QueryMotd: session.SendQueryMotd, - SetMotd: session.SendSetMotd, - ClearMotd: session.SendClearMotd, - SetOpenHouseStatus: session.SendSetOpenHouseStatus, - AddPermanentGuest: session.SendAddPermanentGuest, - RemovePermanentGuest: session.SendRemovePermanentGuest, - RemoveAllPermanentGuests: session.SendRemoveAllPermanentGuests, - ChangeStoragePermission: session.SendChangeStoragePermission, - AddAllStoragePermission: session.SendAddAllStoragePermission, - RemoveAllStoragePermission: session.SendRemoveAllStoragePermission, - RequestFullGuestList: session.SendRequestFullGuestList, - BootSpecificHouseGuest: session.SendBootSpecificHouseGuest, - BootEveryone: session.SendBootEveryone, - SetHooksVisibility: session.SendSetHooksVisibility, - ModifyAllegianceGuestPermission: - session.SendModifyAllegianceGuestPermission, - ModifyAllegianceStoragePermission: - session.SendModifyAllegianceStoragePermission)); - private ILiveSessionEventRouting CreateEventRoute( AcDream.Core.Net.WorldSession session) { @@ -1345,9 +1259,7 @@ internal sealed class HeadlessSessionHost : IDisposable (text, type) => Runtime.CommunicationOwner.AddText(text, type), Fellowship: Runtime.FellowshipOwner, Allegiance: Runtime.AllegianceOwner, - House: Runtime.HouseOwner, - Contracts: Runtime.ContractsOwner, - PlayerGuid: () => Runtime.PlayerIdentity.ServerGuid)); + Contracts: Runtime.ContractsOwner)); var eventRoute = new HeadlessSessionEventRoute( route, Runtime, diff --git a/src/AcDream.Runtime/Chat/ClientCommandId.cs b/src/AcDream.Runtime/Chat/ClientCommandId.cs index 95e72ed2..e6ca5f7b 100644 --- a/src/AcDream.Runtime/Chat/ClientCommandId.cs +++ b/src/AcDream.Runtime/Chat/ClientCommandId.cs @@ -18,10 +18,6 @@ public enum ClientCommandId QueryAge, QueryBirth, ToggleFrameRate, - /// @day — toggle retail's persistent noon landscape lighting. - TogglePersistentDaylight, - /// @render — modify retail's landscape radius or field of view. - RenderOption, ToggleUiLock, ShowVersion, ShowLocation, @@ -88,52 +84,18 @@ public enum ClientCommandId OffChannel, /// @alh / @ah / "@allegiance hometown" / "@allegiance ho" — recall to the allegiance bindstone. AllegianceHometown, - /// "@allegiance info <name>" — request allegiance member info. + /// "@allegiance info [name]" — request allegiance member info. AllegianceInfo, - /// "@allegiance boot [-account] <name>". - AllegianceBoot, - /// "@allegiance ban ..." administration dispatcher. - AllegianceBan, - /// "@allegiance chat ..." administration dispatcher. - AllegianceChat, - /// "@allegiance broadcast <text>". - AllegianceBroadcast, - /// "@allegiance officer ..." administration dispatcher. - AllegianceOfficer, - /// "@allegiance title ..." officer-title dispatcher. - AllegianceOfficerTitle, - /// "@allegiance name ..." dispatcher. - AllegianceName, - /// "@allegiance lock ..." dispatcher. - AllegianceLock, - /// "@allegiance house ..." allegiance-house dispatcher. - AllegianceHouse, - /// "@allegiance motd ..." and standalone "@motd ...". - AllegianceMotd, /// "@house abandon" — abandon the character's house. HouseAbandon, - /// "@house open|close". - HouseOpenStatus, - /// "@house storage ...". - HouseStorage, - /// "@house remove|boot ...". - HouseBoot, - /// "@house boot_all|remove_all". - HouseBootAll, - /// "@house guest ...". - HouseGuests, - /// "@house hooks on|off". - HouseHooks, /// - /// A genuinely unrecognized or incomplete @allegiance subcommand. - /// The catalog claims it locally and emits retail's Allegiance help - /// refusal rather than allowing channel fallback or server passthrough. + /// CH4 REJECT-review Blocker 1 (2026-08-09): "@allegiance"/"@all" with + /// any subcommand beyond the 2 ported ones (info, hometown/ho). Never + /// dispatched — + /// is always + /// false for this id, so ChatCommandRouter shows retail's own + /// "Please see @help Allegiance..." refusal and never publishes an + /// ExecuteClientCommandCmd. /// AllegianceUnrecognizedSubcommand, - - /// - /// An unrecognized or incomplete @house subcommand. Like retail, - /// this is claimed locally and prints the House help refusal. - /// - HouseUnrecognizedSubcommand, } diff --git a/src/AcDream.Runtime/Chat/RetailAdministrationCommandDispatcher.cs b/src/AcDream.Runtime/Chat/RetailAdministrationCommandDispatcher.cs deleted file mode 100644 index b06cb84a..00000000 --- a/src/AcDream.Runtime/Chat/RetailAdministrationCommandDispatcher.cs +++ /dev/null @@ -1,597 +0,0 @@ -using AcDream.Core.Net.Messages; - -namespace AcDream.Runtime.Chat; - -/// -/// Presentation-independent port of retail's nested allegiance and house -/// command handlers. Both graphical and headless hosts use this one parser so -/// their argument grammar, local refusals, and wire effects cannot drift. -/// -/// -/// Ported from ClientCommunicationSystem::DoAllegiance @ 0x0057D5A0, -/// DoHouse @ 0x00580860, and their named helper functions in the -/// September 2013 retail decompile. The action bindings name ACE's matching -/// GameAction readers; no raw opcode is exposed at this layer. -/// -public sealed class RetailAdministrationCommandDispatcher -{ - public sealed record FeedbackBindings( - Action ShowSystemMessage, - Action ShowClientLocalMessage, - Action SetSingleCharacterOption, - Action RequestAllegianceInfo); - - public sealed record ActionBindings( - Action BreakAllegianceBoot, - Action AllegianceChatBoot, - Action AllegianceChatGag, - Action AllegianceBroadcast, - Action ListAllegianceBans, - Action AddAllegianceBan, - Action RemoveAllegianceBan, - Action ListAllegianceOfficers, - Action ClearAllegianceOfficers, - Action SetAllegianceOfficer, - Action RemoveAllegianceOfficer, - Action ListAllegianceOfficerTitles, - Action ClearAllegianceOfficerTitles, - Action SetAllegianceOfficerTitle, - Action QueryAllegianceName, - Action SetAllegianceName, - Action ClearAllegianceName, - Action AllegianceLockAction, - Action SetAllegianceApprovedVassal, - Action AllegianceHouseAction, - Action QueryMotd, - Action SetMotd, - Action ClearMotd, - Action SetOpenHouseStatus, - Action AddPermanentGuest, - Action RemovePermanentGuest, - Action RemoveAllPermanentGuests, - Action ChangeStoragePermission, - Action AddAllStoragePermission, - Action RemoveAllStoragePermission, - Action RequestFullGuestList, - Action BootSpecificHouseGuest, - Action BootEveryone, - Action SetHooksVisibility, - Action ModifyAllegianceGuestPermission, - Action ModifyAllegianceStoragePermission); - - private readonly FeedbackBindings _feedback; - private readonly ActionBindings _actions; - - public RetailAdministrationCommandDispatcher( - FeedbackBindings feedback, - ActionBindings actions) - { - _feedback = feedback ?? throw new ArgumentNullException(nameof(feedback)); - _actions = actions ?? throw new ArgumentNullException(nameof(actions)); - } - - /// - /// Executes one typed management command. Returns false only when the id - /// is outside this dispatcher's family. - /// - public bool TryExecute(ClientCommandId command, string arguments) - { - ArgumentNullException.ThrowIfNull(arguments); - switch (command) - { - case ClientCommandId.AllegianceInfo: - ExecuteAllegianceInfo(arguments); - return true; - case ClientCommandId.AllegianceBoot: - ExecuteAllegianceBoot(arguments); - return true; - case ClientCommandId.AllegianceBan: - ExecuteAllegianceBan(arguments); - return true; - case ClientCommandId.AllegianceChat: - ExecuteAllegianceChat(arguments); - return true; - case ClientCommandId.AllegianceBroadcast: - ExecuteAllegianceBroadcast(arguments); - return true; - case ClientCommandId.AllegianceOfficer: - ExecuteAllegianceOfficer(arguments); - return true; - case ClientCommandId.AllegianceOfficerTitle: - ExecuteAllegianceOfficerTitle(arguments); - return true; - case ClientCommandId.AllegianceName: - ExecuteAllegianceName(arguments); - return true; - case ClientCommandId.AllegianceLock: - ExecuteAllegianceLock(arguments); - return true; - case ClientCommandId.AllegianceHouse: - ExecuteAllegianceHouse(arguments); - return true; - case ClientCommandId.AllegianceMotd: - ExecuteAllegianceMotd(arguments); - return true; - case ClientCommandId.AllegianceUnrecognizedSubcommand: - ShowAllegianceHelpRefusal(); - return true; - case ClientCommandId.HouseOpenStatus: - _actions.SetOpenHouseStatus( - arguments.Equals("open", StringComparison.OrdinalIgnoreCase)); - return true; - case ClientCommandId.HouseStorage: - ExecuteHouseStorage(arguments); - return true; - case ClientCommandId.HouseBoot: - ExecuteHouseBoot(arguments); - return true; - case ClientCommandId.HouseBootAll: - _actions.BootEveryone(); - return true; - case ClientCommandId.HouseGuests: - ExecuteHouseGuests(arguments); - return true; - case ClientCommandId.HouseHooks: - ExecuteHouseHooks(arguments); - return true; - case ClientCommandId.HouseUnrecognizedSubcommand: - ShowHouseHelpRefusal(); - return true; - default: - return false; - } - } - - private void ExecuteAllegianceInfo(string arguments) - { - string name = arguments.Trim(); - if (name.Length == 0) - ShowClientLocal("Please specify an actual name."); - else - _feedback.RequestAllegianceInfo(name); - } - - private void ExecuteAllegianceBoot(string arguments) - { - string name = arguments.Trim(); - if (name.Length == 0) - { - ShowClientLocal("Please specify an actual name."); - return; - } - - int accountFlag = name.IndexOf("-account", StringComparison.OrdinalIgnoreCase); - bool accountBoot = accountFlag >= 0; - if (accountBoot) - name = name.Remove(accountFlag, "-account".Length).Trim(); - - _feedback.ShowSystemMessage( - $"Attempting to boot {name}{(accountBoot ? " (Account)" : string.Empty)}..."); - _actions.BreakAllegianceBoot(name, accountBoot); - } - - private void ExecuteAllegianceBan(string arguments) - { - string operation = FirstArgument(arguments); - if (operation.Equals("list", StringComparison.OrdinalIgnoreCase)) - { - _actions.ListAllegianceBans(); - return; - } - - string name = RemainderAfterFirstArgument(arguments).Trim(); - if (name.Length == 0) - { - ShowClientLocal("Please specify an actual name."); - return; - } - - if (operation.Equals("add", StringComparison.OrdinalIgnoreCase)) - _actions.AddAllegianceBan(name); - else if (operation.Equals("remove", StringComparison.OrdinalIgnoreCase)) - _actions.RemoveAllegianceBan(name); - else - ShowAllegianceHelpRefusal(); - } - - private void ExecuteAllegianceChat(string arguments) - { - string operation = FirstArgument(arguments); - if (operation.Equals("on", StringComparison.OrdinalIgnoreCase) - || operation.Equals("off", StringComparison.OrdinalIgnoreCase)) - { - _feedback.SetSingleCharacterOption( - (uint)CharacterOptionId.ListenToAllegianceChat, - operation.Equals("on", StringComparison.OrdinalIgnoreCase)); - return; - } - - string remainder = RemainderAfterFirstArgument(arguments).Trim(); - if (operation.Equals("kick", StringComparison.OrdinalIgnoreCase)) - { - int comma = remainder.IndexOf(','); - string name = comma < 0 ? remainder : remainder[..comma].Trim(); - string reason = comma < 0 - ? "No reason given." - : remainder[(comma + 1)..].Trim(); - _actions.AllegianceChatBoot(name, reason); - return; - } - - bool gag = operation.Equals("gag", StringComparison.OrdinalIgnoreCase); - bool ungag = operation.Equals("ungag", StringComparison.OrdinalIgnoreCase); - if (!gag && !ungag) - { - ShowAllegianceHelpRefusal(); - return; - } - - if (remainder.Length == 0) - { - ShowClientLocal("Please specify an actual name."); - return; - } - - _actions.AllegianceChatGag(remainder, gag); - } - - private void ExecuteAllegianceBroadcast(string arguments) - { - string message = arguments.Trim(); - if (message.Length == 0) - ShowAllegianceHelpRefusal(); - else - _actions.AllegianceBroadcast(message); - } - - private void ExecuteAllegianceOfficer(string arguments) - { - string operation = FirstArgument(arguments); - if (operation.Length == 0 - || operation.Equals("list", StringComparison.OrdinalIgnoreCase)) - { - _actions.ListAllegianceOfficers(); - return; - } - - if (operation.Equals("clear", StringComparison.OrdinalIgnoreCase)) - { - _actions.ClearAllegianceOfficers(); - return; - } - - string remainder = RemainderAfterFirstArgument(arguments); - if (operation.Equals("remove", StringComparison.OrdinalIgnoreCase)) - { - string name = remainder.Trim(); - if (name.Length == 0) - ShowClientLocal("Please specify the name of an allegiance member."); - else - _actions.RemoveAllegianceOfficer(name); - return; - } - - if (!operation.Equals("add", StringComparison.OrdinalIgnoreCase) - && !operation.Equals("set", StringComparison.OrdinalIgnoreCase)) - { - ShowAllegianceHelpRefusal(); - return; - } - - string levelText = FirstArgument(remainder); - int level = RetailStrtolBaseZero(levelText); - if (level is < 1 or > 3) - { - ShowClientLocal( - "Please specify a valid officer level as a number between 1 and 3. " - + "Check the game help files for more information on officer levels."); - return; - } - - string officerName = RemainderAfterFirstArgument(remainder).Trim(); - if (officerName.Length == 0) - { - ShowClientLocal("Please specify the name of an allegiance member."); - return; - } - - _actions.SetAllegianceOfficer(officerName, (uint)level); - } - - private void ExecuteAllegianceOfficerTitle(string arguments) - { - string operation = FirstArgument(arguments); - if (operation.Length == 0 - || operation.Equals("list", StringComparison.OrdinalIgnoreCase)) - { - _actions.ListAllegianceOfficerTitles(); - return; - } - - if (operation.Equals("clear", StringComparison.OrdinalIgnoreCase)) - { - _actions.ClearAllegianceOfficerTitles(); - return; - } - - if (!operation.Equals("set", StringComparison.OrdinalIgnoreCase)) - { - ShowAllegianceHelpRefusal(); - return; - } - - string remainder = RemainderAfterFirstArgument(arguments); - string levelText = FirstArgument(remainder); - int level = RetailStrtolBaseZero(levelText); - if (level is < 1 or > 3) - { - ShowClientLocal( - "Please specify a valid officer level as a number between 1 and 3."); - return; - } - - string title = RemainderAfterFirstArgument(remainder).Trim(); - _actions.SetAllegianceOfficerTitle((uint)level, title); - } - - private void ExecuteAllegianceName(string arguments) - { - string operation = FirstArgument(arguments); - if (operation.Length == 0) - _actions.QueryAllegianceName(); - else if (operation.Equals("set", StringComparison.OrdinalIgnoreCase)) - _actions.SetAllegianceName(RemainderAfterFirstArgument(arguments).Trim()); - else if (operation.Equals("clear", StringComparison.OrdinalIgnoreCase)) - _actions.ClearAllegianceName(); - else - ShowAllegianceHelpRefusal(); - } - - private void ExecuteAllegianceLock(string arguments) - { - string operation = FirstArgument(arguments); - uint? action = operation.ToLowerInvariant() switch - { - "" or "check" => 4u, - "off" => 1u, - "on" => 2u, - "toggle" => 3u, - _ => null, - }; - if (action is not null) - { - _actions.AllegianceLockAction(action.Value); - return; - } - - if (!operation.Equals("bypass", StringComparison.OrdinalIgnoreCase)) - { - ShowAllegianceHelpRefusal(); - return; - } - - string approved = RemainderAfterFirstArgument(arguments).Trim(); - if (approved.Length == 0) - _actions.AllegianceLockAction(5u); - else if (approved.Equals("clear", StringComparison.OrdinalIgnoreCase)) - _actions.AllegianceLockAction(6u); - else - _actions.SetAllegianceApprovedVassal(approved); - } - - private void ExecuteAllegianceHouse(string arguments) - { - string category = FirstArgument(arguments); - if (category.Length == 0) - { - _actions.AllegianceHouseAction(1u); - return; - } - - string state = FirstArgument(RemainderAfterFirstArgument(arguments)); - uint action = (category.ToLowerInvariant(), state.ToLowerInvariant()) switch - { - ("guest", "open") => 2u, - ("guest", "close") => 3u, - ("storage", "open") => 4u, - ("storage", "close") => 5u, - _ => 0u, - }; - if (action == 0u) - ShowAllegianceHelpRefusal(); - else - _actions.AllegianceHouseAction(action); - } - - private void ExecuteAllegianceMotd(string arguments) - { - string operation = FirstArgument(arguments); - if (operation.Length == 0) - _actions.QueryMotd(); - else if (operation.Equals("set", StringComparison.OrdinalIgnoreCase)) - _actions.SetMotd(RemainderAfterFirstArgument(arguments).Trim()); - else if (operation.Equals("clear", StringComparison.OrdinalIgnoreCase)) - _actions.ClearMotd(); - else - ShowAllegianceHelpRefusal(); - } - - private void ExecuteHouseGuests(string arguments) - { - string operation = FirstArgument(arguments); - string name = RemainderAfterFirstArgument(arguments).Trim(); - if (operation.Equals("add", StringComparison.OrdinalIgnoreCase) - || operation.Equals("remove", StringComparison.OrdinalIgnoreCase)) - { - if (name.Length == 0) - { - ShowClientLocal("Please specify the guest's name."); - return; - } - - if (operation.Equals("add", StringComparison.OrdinalIgnoreCase)) - _actions.AddPermanentGuest(name); - else - _actions.RemovePermanentGuest(name); - return; - } - - if (operation.Equals("remove_all", StringComparison.OrdinalIgnoreCase)) - _actions.RemoveAllPermanentGuests(); - else if (operation.Equals("list", StringComparison.OrdinalIgnoreCase) - || operation.Equals("show", StringComparison.OrdinalIgnoreCase)) - _actions.RequestFullGuestList(); - else if (operation.Equals("add_allegiance", StringComparison.OrdinalIgnoreCase)) - _actions.ModifyAllegianceGuestPermission(true); - else if (operation.Equals("remove_allegiance", StringComparison.OrdinalIgnoreCase)) - _actions.ModifyAllegianceGuestPermission(false); - else - ShowHouseHelpRefusal(); - } - - private void ExecuteHouseStorage(string arguments) - { - string operation = FirstArgument(arguments); - string name = RemainderAfterFirstArgument(arguments).Trim(); - if (operation.Equals("add", StringComparison.OrdinalIgnoreCase) - || operation.Equals("remove", StringComparison.OrdinalIgnoreCase)) - { - if (name.Length == 0) - { - ShowClientLocal("Please specify an actual name."); - return; - } - - bool enabled = operation.Equals("add", StringComparison.OrdinalIgnoreCase); - if (name.Equals("-all", StringComparison.OrdinalIgnoreCase)) - { - if (enabled) - _actions.AddAllStoragePermission(); - else - _actions.RemoveAllStoragePermission(); - } - else - { - _actions.ChangeStoragePermission(name, enabled); - } - return; - } - - if (operation.Equals("remove_all", StringComparison.OrdinalIgnoreCase)) - _actions.RemoveAllStoragePermission(); - else if (operation.Equals("list", StringComparison.OrdinalIgnoreCase) - || operation.Equals("show", StringComparison.OrdinalIgnoreCase)) - _actions.RequestFullGuestList(); - else if (operation.Equals("add_allegiance", StringComparison.OrdinalIgnoreCase)) - _actions.ModifyAllegianceStoragePermission(true); - else if (operation.Equals("remove_allegiance", StringComparison.OrdinalIgnoreCase)) - _actions.ModifyAllegianceStoragePermission(false); - else - ShowHouseHelpRefusal(); - } - - private void ExecuteHouseBoot(string arguments) - { - string name = arguments.Trim(); - if (name.Length == 0) - { - ShowHouseHelpRefusal(); - return; - } - - if (name.Equals("-all", StringComparison.OrdinalIgnoreCase)) - _actions.BootEveryone(); - else - _actions.BootSpecificHouseGuest(name); - } - - private void ExecuteHouseHooks(string arguments) - { - string state = FirstArgument(arguments); - if (state.Equals("on", StringComparison.OrdinalIgnoreCase)) - _actions.SetHooksVisibility(true); - else if (state.Equals("off", StringComparison.OrdinalIgnoreCase)) - _actions.SetHooksVisibility(false); - else - ShowHouseHelpRefusal(); - } - - private void ShowAllegianceHelpRefusal() => ShowClientLocal( - "Please see @help Allegiance for more information on how to use this command."); - - private void ShowHouseHelpRefusal() => ShowClientLocal( - "Please see @help House for more information on how to use this command."); - - private void ShowClientLocal(string text) => - _feedback.ShowClientLocalMessage(text); - - private static int RetailStrtolBaseZero(string value) - { - if (string.IsNullOrEmpty(value)) - return 0; - - int index = 0; - int sign = 1; - if (value[index] is '+' or '-') - { - if (value[index] == '-') - sign = -1; - if (++index == value.Length) - return 0; - } - - int numberBase = 10; - if (value[index] == '0') - { - numberBase = 8; - if (index + 2 < value.Length - && value[index + 1] is 'x' or 'X' - && HexDigit(value[index + 2]) >= 0) - { - numberBase = 16; - index += 2; - } - } - - long result = 0; - bool sawDigit = false; - while (index < value.Length) - { - int digit = HexDigit(value[index]); - if (digit < 0 || digit >= numberBase) - break; - sawDigit = true; - result = Math.Min( - (long)int.MaxValue + (sign < 0 ? 1L : 0L), - result * numberBase + digit); - index++; - } - - if (!sawDigit) - return 0; - long signed = sign < 0 ? -result : result; - return (int)Math.Clamp(signed, int.MinValue, int.MaxValue); - - static int HexDigit(char c) => c switch - { - >= '0' and <= '9' => c - '0', - >= 'a' and <= 'f' => c - 'a' + 10, - >= 'A' and <= 'F' => c - 'A' + 10, - _ => -1, - }; - } - - private static string FirstArgument(string arguments) - { - string trimmed = arguments.Trim(); - int separator = trimmed.IndexOfAny([' ', '\t', '\r', '\n']); - return separator < 0 ? trimmed : trimmed[..separator]; - } - - private static string RemainderAfterFirstArgument(string arguments) - { - string trimmed = arguments.Trim(); - int separator = trimmed.IndexOfAny([' ', '\t', '\r', '\n']); - return separator < 0 ? string.Empty : trimmed[(separator + 1)..].TrimStart(); - } -} diff --git a/src/AcDream.Runtime/Chat/RetailClientCommandCatalog.cs b/src/AcDream.Runtime/Chat/RetailClientCommandCatalog.cs index de9325ad..ed534a74 100644 --- a/src/AcDream.Runtime/Chat/RetailClientCommandCatalog.cs +++ b/src/AcDream.Runtime/Chat/RetailClientCommandCatalog.cs @@ -111,23 +111,6 @@ public static class RetailClientCommandCatalog "/framerate", "/framerate - Toggles the framerate display."); - // ClientCommunicationSystem::DoDay @0x005706F0 ignores argc and toggles - // LScape::m_fAlwaysDaylight plus PlayerModule::PersistentAtDay. - private static readonly Definition Day = AnyArguments( - ClientCommandId.TogglePersistentDaylight, - "/day", - RetailCommandHelpTable.Day); - - // ClientCommunicationSystem::DoRenderOption @0x0057E120 forwards the - // complete argv to GraphicsOptions::HandleRenderOption @0x00455C30. - // That handler owns its usage/error reporting, so every argument shape - // must reach the application executor rather than the generic catalog - // validation refusal. - private static readonly Definition Render = AnyArguments( - ClientCommandId.RenderOption, - "/render + /// flags; since C5b (#275) the steady-state + /// caller passes the same two values, + /// derived pre-merge from (disposition, hasAnimations) rather than read + /// off a route. internal bool ApplyAcceptedPositionSnapshot( uint guid, WorldSession.EntityPositionUpdate update, @@ -806,9 +817,13 @@ public sealed class InboundPhysicsStateController /// captured at admission time. /// /// / - /// (Round 3 B6) carry retail's two PRE-PLACEMENT gates. Both callers now - /// consume the single DerivePrePlacementFlags truth table. Both - /// flags are false only for the FORCE_POSITION branch, which + /// (Round 3 B6) carry retail's two PRE-PLACEMENT gates. Since C5b (#275) + /// BOTH callers supply the same classified values, from the same two + /// inputs: the continuation executor reads its classified route's + /// ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting, + /// and the steady-state merge derives them + /// pre-merge from (disposition, hasAnimations) - see the truth table + /// there. Both are false only for the FORCE_POSITION branch, which /// retail's HandleReceivedPosition Gate A returns from immediately, /// BEFORE either call; is /// additionally false whenever HasAnims is true. diff --git a/src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs b/src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs index dce4b8be..0c0a91dc 100644 --- a/src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs +++ b/src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs @@ -1,15 +1,14 @@ using System.Globalization; using AcDream.Core.Items; using AcDream.Core.Net.Messages; -using AcDream.Core.Ui; using AcDream.Core.Properties; namespace AcDream.Runtime.Gameplay; /// /// Canonical presentation-independent owner for the House tab of retail's -/// two-tab Map/House panel (gmHouseUI). Deliberately lightweight — -/// a full +/// two-tab Map/House panel (gmHouseUI). Deliberately MINIMAL — +/// "houseless-status only" per ISSUES #413's own sizing note: a full /// RuntimeTradeState-weight owner (construction-transaction /// Fault() injection point, disposal ordering, convergence tracking) /// is disproportionate for what this slice needs, since (unlike Trade) this @@ -29,8 +28,8 @@ namespace AcDream.Runtime.Gameplay; /// (DisplayRentPayment, DisplayBuyTime, /// DisplayRentTimes, DisplayLocation, /// DisplayWarningText) open with if (this->m_pHouseData != 0) -/// and emit NOTHING when houseless. The owned-house path retains the exact -/// 0x0225 snapshot and ports all seven builders in retail's fixed order. +/// and emit NOTHING when houseless — those remain unported, ISSUES #413 +/// item 3. /// /// /// CORRECTED at the 2026-08-17 morning gate round (user finding 2: the @@ -64,9 +63,7 @@ namespace AcDream.Runtime.Gameplay; /// m_pHouseData == 0 (still houseless) and emits the literal at /// data_7ab7f0: "You may buy another house immediately." /// (owns-a-house sibling at data_7ab818, byte-re-verified this -/// round). The owned branch now composes the retained buy list through the -/// retail HousePaymentList rules. So a queried houseless character's -/// House tab shows exactly TWO +/// round). So a queried houseless character's House tab shows exactly TWO /// lines, in builder order: "You do not currently own a house." then the /// purchase-time line — which this class now renders. /// @@ -104,9 +101,7 @@ public sealed class RuntimeHouseState private readonly object _gate = new(); private bool _hasReceivedNotice; private bool _ownsHouse; - private GameEvents.HouseData? _houseData; private IReadOnlyList _lines = Array.Empty(); - private IReadOnlyList _panelLines = Array.Empty(); /// Borrows the canonical object table (optional for bare /// fixtures) to read the local player's own @@ -128,28 +123,6 @@ public sealed class RuntimeHouseState get { lock (_gate) return _lines; } } - /// The same rows with retail's HousePanelTextColor - /// index preserved for the authored row template. - public IReadOnlyList PanelLines - { - get { lock (_gate) return _panelLines; } - } - - /// The owned-house position used by retail's Map-page house - /// marker. Apartments intentionally expose no landscape position. - public CreateObject.ServerPosition? Position - { - get - { - lock (_gate) - { - return _houseData is { Type: not 4u, Position: { LandblockId: not 0u } } data - ? data.Position - : null; - } - } - } - /// Whether any of the four House notices (0x0225-0x0228) has /// arrived this session. public bool HasReceivedNotice @@ -158,48 +131,15 @@ public sealed class RuntimeHouseState } /// 0x0225 HouseData — RecvNotice_UpdateHouseData - /// (owned-house case). The snapshot is retained defensively because its - /// payment lists are later replaced by 0x0227/0x0228 notices. + /// (owned-house case). Only is consumed today; + /// the owned-house payload itself (buy/rent payments, times, location) + /// feeds ISSUES #413's remaining six builders, not yet ported. public void ApplyHouseData(GameEvents.HouseData data, uint selfGuid) { lock (_gate) { _hasReceivedNotice = true; _ownsHouse = true; - _houseData = Copy(data); - Recompute(selfGuid); - } - } - - /// 0x0227 UpdateRentTime. Retail installs the new period start, - /// clears every paid count, then rebuilds the complete panel. - public void ApplyRentTime(uint rentTime, uint selfGuid) - { - lock (_gate) - { - if (_houseData is not { } data) - return; - - GameEvents.HousePayment[] rent = data.Rent - .Select(static payment => payment with { Paid = 0 }) - .ToArray(); - _houseData = data with { RentTime = rentTime, Rent = rent }; - Recompute(selfGuid); - } - } - - /// 0x0228 UpdateRentPayment. Retail replaces the complete rent - /// list, then rebuilds the complete panel. - public void ApplyRentPayment( - IReadOnlyList rent, uint selfGuid) - { - ArgumentNullException.ThrowIfNull(rent); - lock (_gate) - { - if (_houseData is not { } data) - return; - - _houseData = data with { Rent = rent.ToArray() }; Recompute(selfGuid); } } @@ -218,7 +158,6 @@ public sealed class RuntimeHouseState { _hasReceivedNotice = true; _ownsHouse = false; - _houseData = null; Recompute(selfGuid); } } @@ -232,67 +171,29 @@ public sealed class RuntimeHouseState { _hasReceivedNotice = false; _ownsHouse = false; - _houseData = null; _lines = Array.Empty(); - _panelLines = Array.Empty(); } } - /// Port of gmHouseUI::DisplayHouseData @0x004a3380's - /// fixed seven-builder order. DisplayRentTimes emits two rows, so - /// an owned outdoor house produces eight rows total. Must hold - /// . + /// The ported share of gmHouseUI::Update's fixed + /// seven-builder order: DisplayBuyPayment @0x004a2b30's + /// houseless branch (first) and DisplayPurchaseTimeText + /// @0x004a3110 (last), both branches. The five houseless-silent + /// builders between them, and DisplayBuyPayment's owned branch, are + /// ISSUES #413 item 3. Must hold . private void Recompute(uint selfGuid) { - var lines = new List(_ownsHouse ? 8 : 2); + var lines = new List(2); // gmHouseUI::DisplayBuyPayment @0x004a2b30 — NOT houseless-silent // (2026-08-17 morning gate correction; see the class doc): the // m_pHouseData gate only selects WHICH text, and the emit runs in - // both branches. The owned prefix and every remaining literal were - // recovered from the PDB-paired binary for issue #413. - if (_houseData is { } data) - { - lines.Add(Normal( - "The purchase price for this dwelling is:\n" - + ComposePayments(data.Buy, includePaid: false))); - lines.Add(Normal( - "Rent:\n" + ComposePayments(data.Rent, includePaid: true))); - lines.Add(Normal("Bought: " + ConvertTime(data.BuyTime))); - - long period = GetRentPeriodSeconds(data.Type); - bool paid = data.MaintenanceFree || IsPaidInFull(data.Rent); - lines.Add(Normal( - "This maintenance period ends: " - + ConvertTime((long)data.RentTime + period))); - lines.Add(Normal( - "Maintenance is next due: " - + ConvertTime((long)data.RentTime + (paid ? 2L : 1L) * period))); - - if (data.Type != 4u - && RadarCoordinates.TryFromCell( - data.Position.LandblockId, out RadarCoordinates coordinates)) - { - lines.Add(Normal( - $"Location: {coordinates.YText}, {coordinates.XText}")); - } - - lines.Add(paid - ? new HousePanelLine( - "The maintenance has already been paid for this period. " - + "You may not prepay next period's maintenance.", - HousePanelTextColor.RentPaid) - : new HousePanelLine( - "Warning! You have not paid your maintenance costs for the last " - + (period / 86_400L).ToString(CultureInfo.InvariantCulture) - + " day maintenance period. Please pay these costs by this deadline" - + " or you will lose your house, and all your items within it.", - HousePanelTextColor.RentNotPaid)); - } - else - { - lines.Add(Normal("You do not currently own a house.")); - } + // both branches. Houseless (@0x004a2b57, byte-decoded data_7ab688): + // this exact literal. Owned (@0x004a2b63, data_7ab65c "The purchase + // price for this dwelling is:\n" + HousePaymentList::ComposeText): + // unported, #413 item 3 — the owned case adds nothing here yet. + if (!_ownsHouse) + lines.Add("You do not currently own a house."); // gmHouseUI::DisplayPurchaseTimeText @0x004a3110, both branches. int timestamp = _objects?.Get(selfGuid)?.Properties @@ -320,95 +221,18 @@ public sealed class RuntimeHouseState timestamp + PurchaseWaitPeriodSeconds); DateTime expiryLocal = TimeZoneInfo.ConvertTime( expiryUtc, _timeProvider.LocalTimeZone).DateTime; - lines.Add(Normal( + lines.Add( "You may buy another landscape house at " + expiryLocal.ToString(CultureInfo.CurrentCulture) - + ". This restriction does not apply to apartments.")); + + ". This restriction does not apply to apartments."); } else { - lines.Add(Normal(_ownsHouse + lines.Add(_ownsHouse ? "You may buy another house immediately after you abandon this one." - : "You may buy another house immediately.")); + : "You may buy another house immediately."); } - _panelLines = lines; - _lines = lines.Select(static line => line.Text).ToArray(); - } - - private HousePanelLine Normal(string text) => - new(text, HousePanelTextColor.Normal); - - private static GameEvents.HouseData Copy(GameEvents.HouseData data) => - data with - { - Buy = (data.Buy ?? Array.Empty()).ToArray(), - Rent = (data.Rent ?? Array.Empty()).ToArray(), - }; - - private static bool IsPaidInFull(IReadOnlyList payments) - { - for (int i = 0; i < payments.Count; i++) - { - if (payments[i].Paid < payments[i].Num) - return false; - } - return true; - } - - private static string ComposePayments( - IReadOnlyList payments, bool includePaid) - { - if (payments.Count == 0) - return string.Empty; - - var parts = new string[payments.Count]; - for (int i = 0; i < payments.Count; i++) - { - GameEvents.HousePayment payment = payments[i]; - string quantity = includePaid - ? $"{payment.Paid.ToString(CultureInfo.InvariantCulture)}/{payment.Num.ToString(CultureInfo.InvariantCulture)}" - : payment.Num.ToString(CultureInfo.InvariantCulture); - parts[i] = quantity + " " + PaymentName(payment); - } - return string.Join(", ", parts); - } - - private static string PaymentName(GameEvents.HousePayment payment) - { - if (payment.Num == 1) - return payment.Name; - if (!string.IsNullOrEmpty(payment.PluralName)) - return payment.PluralName; - - return payment.Name.EndsWith('s') || payment.Name.EndsWith('x') - ? payment.Name + "es" - : payment.Name + "s"; - } - - private static long GetRentPeriodSeconds(uint houseType) => - houseType == 4u ? 7_776_000L : 2_592_000L; - - private string ConvertTime(long epochSeconds) - { - if (epochSeconds == 0L) - return "N/A"; - - DateTimeOffset utc = DateTimeOffset.FromUnixTimeSeconds(epochSeconds); - DateTime local = TimeZoneInfo.ConvertTime( - utc, _timeProvider.LocalTimeZone).DateTime; - return local.ToString(CultureInfo.CurrentCulture); + _lines = lines; } } - -/// Retail HousePanelTextColor; the numeric value is the -/// authored row template's font-color palette index. -public enum HousePanelTextColor -{ - Normal = 0, - RentPaid = 1, - RentNotPaid = 2, -} - -public readonly record struct HousePanelLine( - string Text, HousePanelTextColor Color); diff --git a/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs index 9d6ee70d..6c1f9b3d 100644 --- a/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs +++ b/src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs @@ -131,16 +131,6 @@ internal readonly record struct RuntimeAcceptedPositionRouteRequest( bool HasAnimations, RuntimePositionPlacementFacts PlacementFacts); -/// -/// Retail's two writes before MoveOrTeleport: the force-position -/// self-echo returns before both; every ordinary accepted Position unparents, -/// and only an object without animations receives the wire placement frame. -/// These facts depend solely on timestamp disposition and HasAnims. -/// -internal readonly record struct RuntimeAcceptedPositionPrePlacementFlags( - bool UnparentBeforeRouting, - bool ApplyPlacementFrameBeforeRouting); - /// /// Immutable action plan for retail HandleReceivedPosition/MoveOrTeleport. /// It deliberately contains no renderer, world entity, UI, or host callback. @@ -199,20 +189,6 @@ internal static class RuntimeAuthoritativePositionRouteClassifier internal static bool IsValidCreateWirePosition( in CreateObject.ServerPosition position) => ValidPosition(position); - internal static RuntimeAcceptedPositionPrePlacementFlags DerivePrePlacementFlags( - PositionTimestampDisposition disposition, - bool hasAnimations) => disposition switch - { - PositionTimestampDisposition.Apply => new( - UnparentBeforeRouting: true, - ApplyPlacementFrameBeforeRouting: !hasAnimations), - PositionTimestampDisposition.ForcePosition => default, - // Rejected packets use the timestamp-only merge and never inspect - // either flag. Returning default keeps that branch explicit. - PositionTimestampDisposition.Rejected => default, - _ => throw new ArgumentOutOfRangeException(nameof(disposition), disposition, null), - }; - internal static RuntimeAuthoritativePositionRoute ClassifyCreate( in RuntimeCreatePositionRouteRequest request) { @@ -332,10 +308,6 @@ internal static class RuntimeAuthoritativePositionRouteClassifier if (!ValidPosition(request.AcceptedWirePosition)) return RejectedData(request.Authority, operation, reporting); - RuntimeAcceptedPositionPrePlacementFlags prePlacement = - DerivePrePlacementFlags( - request.Authority.TimestampDisposition, - request.HasAnimations); bool force = request.Authority.TimestampDisposition is PositionTimestampDisposition.ForcePosition; if (force) @@ -348,8 +320,8 @@ internal static class RuntimeAuthoritativePositionRouteClassifier operation, AuthoritativeTeleportFlags, request.PlacementFrame ?? 0u, - prePlacement.UnparentBeforeRouting, - prePlacement.ApplyPlacementFrameBeforeRouting, + UnparentBeforeRouting: false, + ApplyPlacementFrameBeforeRouting: false, LeaveWorld: false, TeleportHookPhase: RuntimeTeleportHookPhase.None, StopInterpolating: false, @@ -371,8 +343,8 @@ internal static class RuntimeAuthoritativePositionRouteClassifier operation, AuthoritativeTeleportFlags, placement, - prePlacement.UnparentBeforeRouting, - prePlacement.ApplyPlacementFrameBeforeRouting, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, LeaveWorld: false, TeleportHookPhase: RuntimeTeleportHookPhase.AfterPositionOperation, StopInterpolating: false, @@ -393,8 +365,8 @@ internal static class RuntimeAuthoritativePositionRouteClassifier operation, PhysicsSetPositionFlags.None, placement, - prePlacement.UnparentBeforeRouting, - prePlacement.ApplyPlacementFrameBeforeRouting, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, LeaveWorld: false, TeleportHookPhase: RuntimeTeleportHookPhase.None, StopInterpolating: false, @@ -424,8 +396,8 @@ internal static class RuntimeAuthoritativePositionRouteClassifier operation, AuthoritativeTeleportFlags, placement, - prePlacement.UnparentBeforeRouting, - prePlacement.ApplyPlacementFrameBeforeRouting, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, LeaveWorld: false, TeleportHookPhase: RuntimeTeleportHookPhase.BeforePositionOperation, StopInterpolating: false, @@ -450,8 +422,8 @@ internal static class RuntimeAuthoritativePositionRouteClassifier operation, PhysicsSetPositionFlags.None, placement, - prePlacement.UnparentBeforeRouting, - prePlacement.ApplyPlacementFrameBeforeRouting, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, LeaveWorld: false, TeleportHookPhase: RuntimeTeleportHookPhase.None, StopInterpolating: false, @@ -480,8 +452,8 @@ internal static class RuntimeAuthoritativePositionRouteClassifier operation, nearby ? PhysicsSetPositionFlags.None : AuthoritativeTeleportFlags, placement, - prePlacement.UnparentBeforeRouting, - prePlacement.ApplyPlacementFrameBeforeRouting, + UnparentBeforeRouting: true, + ApplyPlacementFrameBeforeRouting: !request.HasAnimations, LeaveWorld: false, TeleportHookPhase: RuntimeTeleportHookPhase.None, StopInterpolating: !nearby, @@ -508,7 +480,9 @@ internal static class RuntimeAuthoritativePositionRouteClassifier { PositionTimestampDisposition.Apply => true, PositionTimestampDisposition.ForcePosition => - kind is RuntimePositionEntityKind.LocalPlayer, + kind is RuntimePositionEntityKind.LocalPlayer + && authority.PreviousTeleportSequence + == authority.AcceptedTeleportSequence, _ => false, }; } diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs index 2ddebec5..fe3ae34d 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs @@ -597,14 +597,6 @@ internal sealed class RuntimeSetPositionState : IDisposable _deferredByCellGeneration = []; private SortedDictionary _pendingProjection = []; - // Issue #311: retry is a per-tick pump while a host receipt remains - // unacknowledged. Retain one snapshot list per synchronous call depth - // instead of allocating Values.ToArray() every tick. PublishPlacement can - // invoke arbitrary observers, so the depth-indexed shape preserves the - // old snapshot semantics even if an observer re-enters this method. - private readonly List> - _pendingProjectionRetryScratchByDepth = [new()]; - private int _pendingProjectionRetryDepth; private readonly List _deferredBucketOrder = []; private readonly Dictionary> _unboundDeferredByCell = []; @@ -942,14 +934,6 @@ internal sealed class RuntimeSetPositionState : IDisposable if (HasPendingProjectionThrough(current.ProjectionBarrierSequence)) return false; - // #310: collision retirement is the stronger authority for an - // authored mover that is still waiting for its first preparation. - // Such an operation has no canonical placement result or projection - // to preserve; waiting for an asset that may never resolve used to - // pin this prefix forever. Cancel the exact unprepared operation - // before evaluating ordinary placement debt, then let the resident - // enter the retirement park below. - CancelUnpreparedPrefixPlacementDebt(current); if (HasOldPrefixPlacementDebt(current)) return false; @@ -1133,42 +1117,18 @@ internal sealed class RuntimeSetPositionState : IDisposable internal void RetryPendingProjections() { EnsureNotDisposed(); - int depth = _pendingProjectionRetryDepth; - if (depth == _pendingProjectionRetryScratchByDepth.Count) + RuntimePlacementProjectionSnapshot[] snapshot = + _pendingProjection.Values.ToArray(); + for (int index = 0; index < snapshot.Length; index++) { - _pendingProjectionRetryScratchByDepth.Add([]); - } - List snapshot = - _pendingProjectionRetryScratchByDepth[depth]; - _pendingProjectionRetryDepth = depth + 1; - try - { - snapshot.Clear(); - // Enumerate the dictionary itself. SortedDictionary.Values exposes - // its enumerator through an interface and boxes it (~72 B/call), - // which would retain a smaller version of the allocation this - // issue removes. - foreach (KeyValuePair - entry in _pendingProjection) + RuntimePlacementProjectionSnapshot projection = snapshot[index]; + if (_pendingProjection.TryGetValue( + projection.Token.Sequence, + out RuntimePlacementProjectionSnapshot current) + && current == projection) { - snapshot.Add(entry.Value); + PublishPlacement(projection); } - for (int index = 0; index < snapshot.Count; index++) - { - RuntimePlacementProjectionSnapshot projection = snapshot[index]; - if (_pendingProjection.TryGetValue( - projection.Token.Sequence, - out RuntimePlacementProjectionSnapshot current) - && current == projection) - { - PublishPlacement(projection); - } - } - } - finally - { - snapshot.Clear(); - _pendingProjectionRetryDepth = depth; } } @@ -4141,50 +4101,6 @@ internal sealed class RuntimeSetPositionState : IDisposable return false; } - private void CancelUnpreparedPrefixPlacementDebt( - CollisionPrefixQuiescence state) - { - uint prefix = state.Token.LandblockPrefix; - List? cancelled = null; - foreach (Operation operation in _operations.Values) - { - if (operation.WakeableLostCell - || operation.DormantLocalActivation - || operation.Stage is not RuntimeEntityPlacementStage - .AwaitingPreparation - || !_moverPreparationAuthorities.TryGetValue( - operation.Key, - out MoverPreparationAuthority preparation) - || preparation.OperationId != operation.Token.OperationId - || preparation.Prepared - || !((preparation.AcceptedPosition.LandblockId - & 0xFFFF0000u) == prefix - || IsAffectedCollisionResident( - operation.Record, - prefix, - state.IncludeOutdoorCells))) - { - continue; - } - - (cancelled ??= []).Add(operation.Key); - } - - if (cancelled is null) - return; - - for (int index = 0; index < cancelled.Count; index++) - { - _ = CancelCoreDeferred( - cancelled[index], - cancelLostFamily: false, - preserveLostFamily: false, - out RuntimePlacementProjectionSnapshot? discard); - if (discard is { } projection) - PublishPlacement(projection); - } - } - private static bool PlacementTouchesPrefix( in PhysicsSetPositionRequest request, uint prefix) => diff --git a/src/AcDream.Runtime/Session/LiveSessionEventRouter.cs b/src/AcDream.Runtime/Session/LiveSessionEventRouter.cs index eb9d5e02..a0df61ed 100644 --- a/src/AcDream.Runtime/Session/LiveSessionEventRouter.cs +++ b/src/AcDream.Runtime/Session/LiveSessionEventRouter.cs @@ -95,11 +95,7 @@ public sealed record LiveSocialSessionBindings( RuntimeHouseState? House = null, // Campaign QT (2026-08-21): the fourth sibling J-owner, same // trailing/optional compatibility convention. - RuntimeContractState? Contracts = null, - // Issue #359: retail suppresses 0x019E PlayerKilled for the victim and - // killer. Optional for compatibility with state-only tests; production - // hosts always supply their canonical identity owner. - Func? PlayerGuid = null); + RuntimeContractState? Contracts = null); /// /// Owns every inbound subscription for one exact live session. Domain state @@ -342,12 +338,6 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting onHouseStatus: social.House is { } houseStatus ? weenieError => houseStatus.ApplyHouseStatus(weenieError, inventory.PlayerGuid()) : null, - onHouseUpdateRentTime: social.House is { } houseRentTime - ? rentTime => houseRentTime.ApplyRentTime(rentTime, inventory.PlayerGuid()) - : null, - onHouseUpdateRentPayment: social.House is { } houseRentPayment - ? rent => houseRentPayment.ApplyRentPayment(rent, inventory.PlayerGuid()) - : null, // Campaign QT (2026-08-21): same conditional delegate-hole // discipline as house above. onContractTable: social.Contracts is { } contractTable @@ -461,8 +451,7 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting killed => social.Chat.OnPlayerKilled( killed.DeathMessage, killed.VictimGuid, - killed.KillerGuid, - social.PlayerGuid?.Invoke() ?? 0u)); + killed.KillerGuid)); Subscribe( h => session.TurbineChatReceived += h, h => session.TurbineChatReceived -= h, diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs b/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs index a2301c04..33e48982 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs @@ -180,9 +180,10 @@ public sealed record DisplaySettings( int LandscapeTextureDetail = 2, int EnvironmentTextureDetail = 1, int TextureFiltering = 1, - // Retail stores the VALUE, not the caption index. Its six enum payloads - // are {3,5,8,11,15,25}; SetDefaultValue(8) therefore selects Medium. - // #361 corrected the former index-shaped Config binding. + // UNRESOLVED (OP6, cite in register row): retail's own + // SetDefaultValue(8) does not index its 6-entry SetEnumChoices array + // (VeryLow..Extreme) — reproduced faithfully as an opaque int, not + // guessed into a clamped index. int LandscapeDrawDistance = 8, bool BuildingDetailTextures = true, bool MultiPassAlpha = false) diff --git a/tests/AcDream.App.Tests/Configuration/RuntimeOptionsSessionConfigTests.cs b/tests/AcDream.App.Tests/Configuration/RuntimeOptionsSessionConfigTests.cs index 29503082..612dcf47 100644 --- a/tests/AcDream.App.Tests/Configuration/RuntimeOptionsSessionConfigTests.cs +++ b/tests/AcDream.App.Tests/Configuration/RuntimeOptionsSessionConfigTests.cs @@ -97,33 +97,6 @@ public sealed class RuntimeOptionsSessionConfigTests Assert.Null(options.StatusFilePath); } - [Fact] - public void SessionConfigPreservesExplicitRetailUiOptOut() - { - var config = new SessionConfiguration { Version = 1 }; - var session = new SessionDescriptor - { - Id = "no-ui", - Endpoint = new SessionEndpointDescriptor { Host = "127.0.0.1", Port = 9000 }, - Account = "account", - Credential = new SessionCredentialDescriptor - { - Provider = SessionCredentialProviderKind.Environment, - Reference = "X", - }, - }; - - RuntimeOptions options = RuntimeOptions.FromSessionConfig( - "D:\\dat", - key => key == "ACDREAM_RETAIL_UI" ? "0" : null, - "session.json", - config, - session, - "password"); - - Assert.False(options.RetailUi); - } - [Fact] public void ProcessContentOverridesDatDirectoryAndPreparedAssetPath() { diff --git a/tests/AcDream.App.Tests/Diagnostics/LaunchOptionsDocumentationTests.cs b/tests/AcDream.App.Tests/Diagnostics/LaunchOptionsDocumentationTests.cs index 4281ebe5..6652db65 100644 --- a/tests/AcDream.App.Tests/Diagnostics/LaunchOptionsDocumentationTests.cs +++ b/tests/AcDream.App.Tests/Diagnostics/LaunchOptionsDocumentationTests.cs @@ -107,7 +107,7 @@ public sealed class LaunchOptionsDocumentationTests } /// - /// The five flags that default ON. All are product/retail behaviors wearing an + /// The four flags that default ON. All are retail behaviors wearing an /// A/B off-switch (=0 disables) — none is a diagnostic. FROZEN: /// a diagnostic that activates without its env var set taxes every run /// and every measurement silently, so growing this set fails. @@ -119,7 +119,6 @@ public sealed class LaunchOptionsDocumentationTests "ACDREAM_CAMERA_COLLIDE", "ACDREAM_CAMERA_ALIGN_SLOPE", "ACDREAM_RETAIL_CLOSE_DEGRADES", - "ACDREAM_RETAIL_UI", }; /// @@ -134,7 +133,7 @@ public sealed class LaunchOptionsDocumentationTests RegexOptions.Compiled); [Fact] - public void OnlyTheFiveProductBehaviorFlagsDefaultOn() + public void OnlyTheFourRetailBehaviorFlagsDefaultOn() { var defaultOn = new HashSet(StringComparer.Ordinal); foreach ((string path, _) in SourceFiles()) diff --git a/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs b/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs index 66d98302..b02f7826 100644 --- a/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs +++ b/tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs @@ -101,39 +101,6 @@ public sealed class LiveSessionCommandRouterTests calls); } - [Fact] - public void AdministrationBindings_UseTheSameActivationAndDisposalGuard() - { - var calls = new List(); - ClientCommandController.Bindings client = NewClientBindings() with - { - Administration = NewAdministrationBindings() with - { - SetMotd = text => calls.Add("motd:" + text), - BootSpecificHouseGuest = name => calls.Add("boot:" + name), - }, - }; - var router = NewRouter(clientBindings: client); - - Publish(); - router.Activate(); - Publish(); - router.Dispose(); - Publish(); - - Assert.Equal(["motd:Welcome", "boot:Lord Bob"], calls); - - void Publish() - { - router.Publish(new ExecuteClientCommandCmd( - ClientCommandId.AllegianceMotd, - "set Welcome")); - router.Publish(new ExecuteClientCommandCmd( - ClientCommandId.HouseBoot, - "Lord Bob")); - } - } - [Fact] public void InactiveAndDisposedRouter_CannotReachTransport() { @@ -864,7 +831,6 @@ public sealed class LiveSessionCommandRouterTests ToggleFrameRate: () => { }, ToggleUiLock: () => { }, ShowSystemMessage: _ => { }, - ShowClientLocalMessage: _ => { }, ShowWeenieError: _ => { }, PlayerPublicWeenieBitfield: () => null, ClientVersion: () => "test", @@ -913,49 +879,5 @@ public sealed class LiveSessionCommandRouterTests LeaveGmChannel: _ => { }, RecallAllegianceHometown: () => { }, RequestAllegianceInfo: _ => { }, - AbandonHouse: () => { }, - Administration: NewAdministrationBindings(), - IsPersistentDaylight: () => false, - SetPersistentDaylight: _ => { }, - SetLandscapeRadius: _ => { }, - SetFieldOfView: _ => { }); - - private static ClientCommandController.AdministrationBindings - NewAdministrationBindings() => new( - BreakAllegianceBoot: (_, _) => { }, - AllegianceChatBoot: (_, _) => { }, - AllegianceChatGag: (_, _) => { }, - AllegianceBroadcast: _ => { }, - ListAllegianceBans: () => { }, - AddAllegianceBan: _ => { }, - RemoveAllegianceBan: _ => { }, - ListAllegianceOfficers: () => { }, - ClearAllegianceOfficers: () => { }, - SetAllegianceOfficer: (_, _) => { }, - RemoveAllegianceOfficer: _ => { }, - ListAllegianceOfficerTitles: () => { }, - ClearAllegianceOfficerTitles: () => { }, - SetAllegianceOfficerTitle: (_, _) => { }, - QueryAllegianceName: () => { }, - SetAllegianceName: _ => { }, - ClearAllegianceName: () => { }, - AllegianceLockAction: _ => { }, - SetAllegianceApprovedVassal: _ => { }, - AllegianceHouseAction: _ => { }, - QueryMotd: () => { }, - SetMotd: _ => { }, - ClearMotd: () => { }, - SetOpenHouseStatus: _ => { }, - AddPermanentGuest: _ => { }, - RemovePermanentGuest: _ => { }, - RemoveAllPermanentGuests: () => { }, - ChangeStoragePermission: (_, _) => { }, - AddAllStoragePermission: () => { }, - RemoveAllStoragePermission: () => { }, - RequestFullGuestList: () => { }, - BootSpecificHouseGuest: _ => { }, - BootEveryone: () => { }, - SetHooksVisibility: _ => { }, - ModifyAllegianceGuestPermission: _ => { }, - ModifyAllegianceStoragePermission: _ => { }); + AbandonHouse: () => { }); } diff --git a/tests/AcDream.App.Tests/Net/RetailSkillFormulaTests.cs b/tests/AcDream.App.Tests/Net/RetailSkillFormulaTests.cs index df50c9c0..1e1e3547 100644 --- a/tests/AcDream.App.Tests/Net/RetailSkillFormulaTests.cs +++ b/tests/AcDream.App.Tests/Net/RetailSkillFormulaTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Frozen; using AcDream.App.Net; using AcDream.Core.CharGen; using DatReaderWriter.DBObjs; @@ -259,26 +258,22 @@ public sealed class RetailSkillFormulaTests AttributeId attributeId) { const uint skillId = 0x10u; - var options = ChargenOptions.Empty with + var skillTable = new SkillTable(); + skillTable.Skills.Add((SkillId)skillId, new SkillBase { - GlobalSkillDetailsBySkillId = new Dictionary + Formula = new SkillFormula { - [skillId] = new ChargenSkillDetail( - skillId, - MinLevel: 1u, - Description: string.Empty, - new ChargenSkillFormula( - AdditiveBonus: 0, - Attribute1Multiplier: 1, - Attribute2Multiplier: 0, - Divisor: 1, - Attribute1: (uint)attributeId, - // Attribute2Multiplier=0 means the second attribute - // cannot mask a wrong Attribute1 mapping. - Attribute2: 0u)), - }.ToFrozenDictionary(), - }; - var resolver = new ChargenSkillScoreResolver(options); + AdditiveBonus = 0, + Attribute1Multiplier = 1, + Attribute2Multiplier = 0, + Divisor = 1, + Attribute1 = attributeId, + // Attribute2 deliberately left at its zero default (Strength) — + // Attribute2Multiplier=0 means whatever it reads contributes + // nothing, so it cannot mask a wrong Attribute1 case. + }, + }); + var resolver = new ChargenSkillScoreResolver(skillTable); ChargenAttributeValues attributes = AttributeValuesWith(attributeId, 42); uint result = resolver.Resolve(skillId, attributes, ChargenSkillAdvancementClass.Untrained); @@ -286,19 +281,6 @@ public sealed class RetailSkillFormulaTests Assert.Equal(42u, result); } - [Fact] - public void ChargenSkillScoreResolver_MissingProjectedSkill_ReturnsZero() - { - var resolver = new ChargenSkillScoreResolver(ChargenOptions.Empty); - - uint result = resolver.Resolve( - 0x10u, - new ChargenAttributeValues(10, 20, 30, 40, 50, 60), - ChargenSkillAdvancementClass.Specialized); - - Assert.Equal(0u, result); - } - private static ChargenAttributeValues AttributeValuesWith(AttributeId attributeId, int value) => attributeId switch { diff --git a/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs b/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs index 5afcc329..91b1a9ff 100644 --- a/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs +++ b/tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs @@ -219,7 +219,7 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests // ── Scenario 2: landing packet (the preserved rows 2a/2b asymmetry) ─ [Fact] - public void LandingPacket_PlayerGuid_QueueClearedAndShadowPublished() + public void LandingPacket_PlayerGuid_QueueClearedNoShadowPublish_316Preserved() { using var fixture = new Fixture(PlayerGuid); EntityPhysicsHost host = fixture.InstallHost(); @@ -253,13 +253,14 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests // Row 2a, PRESERVED for player guids: the interp queue IS cleared. Assert.False(fixture.Remote.Interp.IsActive); - // #316: the same resolved landing pose must publish to collision in - // this packet, not wait for a later physics tick to self-heal. + // Row 2b / #316, PRESERVED (NOT fixed): the shadow is NOT + // republished for a player-guid landing — it stays at whatever it + // was before this packet. ShadowEntry shadowEntry = Assert.Single( fixture.Shadows.AllEntriesForDebug(), entry => entry.EntityId == fixture.Entity.Id); - Assert.Equal(landingPos, shadowEntry.Position); - Assert.NotEqual(spawnShadowPos, shadowEntry.Position); + Assert.Equal(spawnShadowPos, shadowEntry.Position); + Assert.NotEqual(landingPos, shadowEntry.Position); // AP-135's cell-adopt bookkeeping still ran (via the post-routing // wire-cell adopt, not suppressed for AirborneSnap). @@ -573,26 +574,6 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests _ = host; } - [Fact] - public void PositionPackVelocity_DoesNotOverwriteAuthoritativeBodyVelocity() - { - using var fixture = new Fixture(CreatureGuid); - Vector3 authoritativeVectorUpdate = new(4f, 5f, 6f); - fixture.Remote.Body.Velocity = authoritativeVectorUpdate; - - // Retail's airborne/non-teleport Position arm performs no placement, - // isolating the PositionPack velocity rule from contact resolution. - fixture.Controller.OnPosition(fixture.Update( - new Vector3(12f, 14f, SpawnHeight + 2f), - SourceCell, - teleportSequence: 1, - guid: CreatureGuid, - isGrounded: false, - velocity: new Vector3(0.7f, 0.2f, -0.1f))); - - Assert.Equal(authoritativeVectorUpdate, fixture.Remote.Body.Velocity); - } - // ── Sabotage check (contract §5, one-time, manual) ────────────────── // // Performed by hand during implementation, not committed as a test (a diff --git a/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs b/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs index 813c3fae..e71dc71c 100644 --- a/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs +++ b/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs @@ -12,7 +12,7 @@ namespace AcDream.App.Tests.Physics; public sealed class LiveEntityOrdinaryPhysicsUpdaterTests { - private const uint Guid = 0x50000071u; + private const uint Guid = 0x70000071u; private const uint SourceCell = 0xA9B40039u; private const uint DestinationCell = 0xAAB40001u; @@ -37,8 +37,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = SourceCell, - }, - isLocalPlayer: true); + }); WorldEntity entity = Assert.IsType(record.WorldEntity); var remote = new AcDream.Runtime.Physics.RemoteMotion @@ -85,17 +84,6 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests Assert.True(entity.Position.X > 192f); Assert.Equal(entity.Position, record.PhysicsBody.Position); Assert.Equal(epoch, record.ObjectClockEpoch); - - // #320: retiring the landblock the player walked out of must not - // sweep the still-live player into a DeferredCell park. Before the - // ordinary-cell commit existed, FullCellId remained SourceCell and - // this exact retirement selected the player as an affected spatial - // root. - live.Physics.SetPosition.ParkCollisionResidents( - SourceCell, - includeOutdoorCells: true); - Assert.Equal(DestinationCell, record.FullCellId); - Assert.True(live.Physics.IsSpatialRoot(record.Canonical)); } [Fact] diff --git a/tests/AcDream.App.Tests/Rendering/InteriorEntityPartitionTests.cs b/tests/AcDream.App.Tests/Rendering/InteriorEntityPartitionTests.cs index 4be2a103..24c4b2fe 100644 --- a/tests/AcDream.App.Tests/Rendering/InteriorEntityPartitionTests.cs +++ b/tests/AcDream.App.Tests/Rendering/InteriorEntityPartitionTests.cs @@ -109,36 +109,4 @@ public class InteriorEntityPartitionTests Assert.Empty(result.Dynamics); Assert.Empty(result.OutdoorStatic); } - - [Fact] - public void FrustumRejectsWholeLandblockBeforeWalkingItsEntities() - { - const uint culledLandblock = 0xA8B4FFFFu; - const uint cameraLandblock = 0xA9B4FFFFu; - var culled = Ent(10, serverGuid: 0x80000010u, parentCell: OutdoorCell); - var camera = Ent(11, serverGuid: 0x80000011u, parentCell: OutdoorCell); - var entries = new[] - { - (culledLandblock, - new Vector3(10f), - new Vector3(11f), - (IReadOnlyList)new[] { culled }, - (IReadOnlyDictionary?)null), - (cameraLandblock, - new Vector3(10f), - new Vector3(11f), - (IReadOnlyList)new[] { camera }, - (IReadOnlyDictionary?)null), - }; - - InteriorEntityPartition.Result result = - InteriorEntityPartition.Partition( - new HashSet(), - entries, - FrustumPlanes.FromViewProjection(Matrix4x4.Identity), - neverCullLandblockId: cameraLandblock); - - Assert.DoesNotContain(culled, result.Dynamics); - Assert.Equal(camera, Assert.Single(result.Dynamics)); - } } diff --git a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs index d131c80a..89478c1d 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs @@ -381,50 +381,6 @@ public sealed class LiveEntityAnimationPresenterTests Assert.Equal(0.5f, fixture.State.CurrFrame); } - [Fact] - public void LegacyCycle_WrapsAndInterpolatesThroughSharedRetailPlayback() - { - var fixture = Build(partCount: 1); - fixture.State.Sequencer = null; - fixture.State.LowFrame = 0; - fixture.State.HighFrame = 2; - fixture.State.Framerate = 2f; - fixture.State.CurrFrame = 2.5f; - var animation = new Animation(); - foreach (float x in new[] { 0f, 10f, 20f }) - { - var frame = new AnimationFrame(1); - frame.Frames.Add(new Frame - { - Origin = new Vector3(x, 0f, 0f), - Orientation = Quaternion.Identity, - }); - animation.PartFrames.Add(frame); - } - fixture.State.Animation = animation; - var schedule = new LiveEntityAnimationSchedule( - SequenceFrames: null, - LegacyAdvanceSeconds: 0.5f, - ComposeParts: true, - fixture.Record, - fixture.Entity, - fixture.State, - fixture.Record.ObjectClockEpoch, - fixture.Record.ProjectionMutationVersion, - fixture.State.PresentationRevision); - var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context()); - - presenter.Present(new Dictionary - { - [fixture.Record.ProjectionKey!.Value] = schedule, - }); - - // Legacy arithmetic: 2.5 + (0.5 * 2) = 3.5, wrapped over the - // inclusive 0..2 span to 0.5, then interpolated halfway 0 -> 10. - Assert.Equal(0.5f, fixture.State.CurrFrame); - Assert.Equal(new Vector3(5f, 0f, 0f), fixture.Entity.MeshRefs[0].PartTransform.Translation); - } - private static LiveEntityAnimationPresenter Presenter( LiveEntityRuntime live, EntityEffectPoseRegistry poses, diff --git a/tests/AcDream.App.Tests/Rendering/PaperdollFramePresenterTests.cs b/tests/AcDream.App.Tests/Rendering/PaperdollFramePresenterTests.cs index 5b4ee653..41f47c47 100644 --- a/tests/AcDream.App.Tests/Rendering/PaperdollFramePresenterTests.cs +++ b/tests/AcDream.App.Tests/Rendering/PaperdollFramePresenterTests.cs @@ -8,14 +8,6 @@ namespace AcDream.App.Tests.Rendering; public sealed class PaperdollFramePresenterTests { - /// One frame in production order: the pre-world resource phase - /// (build/redress + prewarm) then the late presentation phase. - private static void Frame(PaperdollFramePresenter presenter) - { - presenter.PrepareResources(); - presenter.Render(); - } - [Fact] public void HiddenView_BuildsAndPrewarmsWithoutRendering() { @@ -24,7 +16,7 @@ public sealed class PaperdollFramePresenterTests var factory = new RecordingFactory { Doll = CreateDoll() }; var presenter = new PaperdollFramePresenter(renderer, view, factory); - Frame(presenter); + presenter.Render(); Assert.False(presenter.IsDirty); Assert.Equal(1, factory.BuildCount); @@ -42,7 +34,7 @@ public sealed class PaperdollFramePresenterTests var factory = new RecordingFactory { Doll = doll }; var presenter = new PaperdollFramePresenter(renderer, view, factory); - Frame(presenter); + presenter.Render(); Assert.False(presenter.IsDirty); Assert.Equal(1, factory.BuildCount); @@ -62,25 +54,20 @@ public sealed class PaperdollFramePresenterTests }; var presenter = new PaperdollFramePresenter(renderer, view, factory); - Frame(presenter); - Frame(presenter); + presenter.Render(); + presenter.Render(); presenter.MarkDirty(); - Frame(presenter); + presenter.Render(); Assert.Equal(2, factory.BuildCount); - Assert.Equal(2, renderer.Dolls.Count); + Assert.Single(renderer.Dolls); Assert.Equal(3, renderer.RenderCount); Assert.False(presenter.IsDirty); } [Fact] - public void PortalRefresh_RedressesEvenAnEquivalentAppearance() + public void EquivalentPortalRefresh_KeepsPrivateDollAndTextureOwner() { - // #443: the private clone belongs to the current presentation - // generation. Retaining an old clone because its pixels compare equal - // can pin retired mesh/composite readiness that never completes - // again; the renderer's two-phase promote keeps the last completed - // image visible while the fresh clone becomes drawable. WorldEntity first = CreateDoll(); WorldEntity repeated = CreateDoll(); var renderer = new RecordingRenderer(); @@ -88,13 +75,13 @@ public sealed class PaperdollFramePresenterTests var factory = new RecordingFactory { Doll = first }; var presenter = new PaperdollFramePresenter(renderer, view, factory); - Frame(presenter); + presenter.Render(); factory.Doll = repeated; presenter.MarkDirty(); - Frame(presenter); + presenter.Render(); Assert.Equal(2, factory.BuildCount); - Assert.Equal([first, repeated], renderer.Dolls); + Assert.Equal([first], renderer.Dolls); Assert.Equal(2, renderer.RenderCount); Assert.False(presenter.IsDirty); } @@ -109,34 +96,14 @@ public sealed class PaperdollFramePresenterTests var factory = new RecordingFactory { Doll = first }; var presenter = new PaperdollFramePresenter(renderer, view, factory); - Frame(presenter); + presenter.Render(); factory.Doll = changed; presenter.MarkDirty(); - Frame(presenter); + presenter.Render(); Assert.Equal([first, changed], renderer.Dolls); } - [Fact] - public void TransientZeroRender_NeverErasesThePublishedTexture() - { - // #443: zero is a not-ready result (mesh/composite upload latency), - // not a request to blank a completed paperdoll. - var renderer = new RecordingRenderer { TextureHandle = 91u }; - var view = new RecordingView(); - var factory = new RecordingFactory { Doll = CreateDoll() }; - var presenter = new PaperdollFramePresenter(renderer, view, factory); - - Frame(presenter); - renderer.TextureHandle = 0u; - Frame(presenter); - renderer.TextureHandle = 91u; - Frame(presenter); - - Assert.Equal([91u, 91u], view.TextureHandles); - Assert.Equal(0, view.ClearCount); - } - private static WorldEntity CreateDoll(float scale = 1f) => new() { Id = 42u, @@ -156,11 +123,11 @@ public sealed class PaperdollFramePresenterTests var factory = new RecordingFactory { Doll = firstDoll }; var presenter = new PaperdollFramePresenter(renderer, view, factory); - Frame(presenter); + presenter.Render(); factory.CanBuild = false; presenter.MarkDirty(); - Frame(presenter); - Frame(presenter); + presenter.Render(); + presenter.Render(); Assert.True(presenter.IsDirty); Assert.Equal(3, factory.BuildCount); @@ -179,17 +146,14 @@ public sealed class PaperdollFramePresenterTests var factory = new RecordingFactory { Doll = firstDoll }; var presenter = new PaperdollFramePresenter(renderer, view, factory); - Frame(presenter); + presenter.Render(); presenter.ResetSession(); factory.Doll = secondDoll; - Frame(presenter); + presenter.Render(); Assert.False(presenter.IsDirty); Assert.Equal(2, factory.BuildCount); Assert.Equal([firstDoll, null, secondDoll], renderer.Dolls); - // The session boundary is the ONE explicit viewport clear (#443); - // an old character must not linger while the next one uploads. - Assert.Equal(1, view.ClearCount); } [Fact] @@ -288,7 +252,7 @@ public sealed class PaperdollFramePresenterTests private sealed class RecordingRenderer : IPaperdollDollRenderer { - public uint TextureHandle { get; set; } + public uint TextureHandle { get; init; } public List Dolls { get; } = []; public List<(int Width, int Height)> RenderSizes { get; } = []; public int RenderCount => RenderSizes.Count; @@ -321,10 +285,6 @@ public sealed class PaperdollFramePresenterTests public void SetTextureHandle(uint textureHandle) => TextureHandles.Add(textureHandle); - - public int ClearCount { get; private set; } - - public void ClearTextureHandle() => ClearCount++; } private sealed class RecordingFactory : IPaperdollDollFactory diff --git a/tests/AcDream.App.Tests/Rendering/PrivateViewportFlightTargetsTests.cs b/tests/AcDream.App.Tests/Rendering/PrivateViewportFlightTargetsTests.cs deleted file mode 100644 index ac2d7bf5..00000000 --- a/tests/AcDream.App.Tests/Rendering/PrivateViewportFlightTargetsTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Gpu; -using AcDream.App.Tests.Rendering.Gpu; - -namespace AcDream.App.Tests.Rendering; - -public sealed class PrivateViewportFlightTargetsTests -{ - [Fact] - public void FlightSlotsOwnDistinctTargetsAndPublishOnlyCompletedScenes() - { - using var device = new RecordingGpuDevice(); - using var targets = - new PrivateEntityViewportRenderer.PrivateViewportFlightTargets( - device, - "paperdoll"); - - var first = Assert.IsType< - PrivateEntityViewportRenderer.PrivateViewportFlightTargets.TargetSlot>( - targets.Ensure(0, 120, 180)); - var second = Assert.IsType< - PrivateEntityViewportRenderer.PrivateViewportFlightTargets.TargetSlot>( - targets.Ensure(1, 120, 180)); - - Assert.NotSame(first.Target, second.Target); - Assert.NotEqual(first.TextureSlot, second.TextureSlot); - Assert.Equal(2, targets.AllocatedSlotCount); - Assert.Equal(0u, targets.CompletedHandle(0)); - Assert.Equal(0u, targets.CompletedHandle(1)); - - first.HasRenderedScene = true; - - Assert.NotEqual(0u, targets.CompletedHandle(0)); - Assert.Equal(0u, targets.CompletedHandle(1)); - Assert.Same(first, targets.Ensure(0, 120, 180)); - - targets.InvalidateCompletedScenes(); - - Assert.Equal(0u, targets.CompletedHandle(0)); - Assert.Equal(2, targets.AllocatedSlotCount); - } - - [Fact] - public void ResizeRetiresEveryFlightTargetBeforeCreatingTheNewExtent() - { - using var device = new RecordingGpuDevice(); - using var targets = - new PrivateEntityViewportRenderer.PrivateViewportFlightTargets( - device, - "paperdoll"); - var first = targets.Ensure(0, 120, 180)!; - var second = targets.Ensure(1, 120, 180)!; - var firstTarget = Assert.IsType(first.Target); - var secondTarget = Assert.IsType(second.Target); - - var resized = targets.Ensure(1, 160, 220)!; - - Assert.True(firstTarget.IsDisposed); - Assert.True(secondTarget.IsDisposed); - Assert.Equal(1, targets.AllocatedSlotCount); - Assert.Equal(160, resized.Target.Description.Width); - Assert.Equal(220, resized.Target.Description.Height); - Assert.Equal(3, device.CreatedRenderTargets.Count); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs b/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs index d60cc655..2141e611 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs @@ -75,80 +75,6 @@ public sealed class RetailAlphaQueueTests Assert.Equal(2, source.ResetCount); } - [Fact] - public void FlushFartherThan_DrainsOnlyTheFarPrefixAndRetainsNearerEntries() - { - // The pre-building barrier: retail's far→near walk means DrawBuilding's - // FlushAlphaList(0f) @0x0059F2A0 can only flush content from cells - // farther than that building; a nearer candle flame is not inserted - // yet and composites after the building at a later flush (AP-236). - var log = new List(); - var objects = new RecordingSource("object", log); - var particles = new RecordingSource("particle", log); - var queue = new RetailAlphaQueue(); - - queue.BeginFrame(); - queue.Submit(particles, 0, 30f); // far waterfall - queue.Submit(objects, 0, 25f); // far translucent part - queue.Submit(particles, 1, 10f); // exactly at the building threshold - queue.Submit(particles, 2, 5f); // near candle flame — must be kept - queue.FlushFartherThan(10f); - - Assert.True(queue.IsCollecting); - Assert.Equal(1, queue.PendingCount); - Assert.Equal(new[] { "particle:0", "object:0", "particle:1" }, log); - Assert.Equal(0, objects.ResetCount); - Assert.Equal(0, particles.ResetCount); - - queue.EndFrame(); - - Assert.Equal( - new[] { "particle:0", "object:0", "particle:1", "particle:2" }, - log); - Assert.Equal(1, objects.ResetCount); - Assert.Equal(1, particles.ResetCount); - Assert.Equal(2, particles.PrepareCount); - } - - [Fact] - public void FlushFartherThan_WithNoFarEntries_LeavesTheQueueUntouched() - { - var log = new List(); - var source = new RecordingSource("alpha", log); - var queue = new RetailAlphaQueue(); - - queue.BeginFrame(); - queue.Submit(source, 1, 4f); - queue.FlushFartherThan(10f); - - Assert.Empty(log); - Assert.Equal(1, queue.PendingCount); - Assert.Equal(0, source.PrepareCount); - Assert.Equal(0, source.ResetCount); - queue.EndFrame(); - Assert.Equal(new[] { "alpha:1" }, log); - } - - [Fact] - public void FlushFartherThan_DegenerateThreshold_DrainsAllWithoutResettingSources() - { - var log = new List(); - var source = new RecordingSource("alpha", log); - var queue = new RetailAlphaQueue(); - - queue.BeginFrame(); - queue.Submit(source, 1, 8f); - queue.Submit(source, 2, 2f); - queue.FlushFartherThan(0f); - - Assert.Equal(new[] { "alpha:1", "alpha:2" }, log); - Assert.Equal(0, queue.PendingCount); - Assert.Equal(0, source.ResetCount); - Assert.True(queue.IsCollecting); - queue.EndFrame(); - Assert.Equal(1, source.ResetCount); - } - [Fact] public void Flush_BatchesOnlyAdjacentEntriesFromSameRenderer() { diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs index 36628dbf..71068650 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs @@ -34,20 +34,13 @@ public sealed class RetailPViewPassExecutorTests "terrain-clip", "clear-routing", "landscape-late", - "unattached-particles-outdoor", - // #132: an OUTDOOR root does NOT drain at the stage boundary. - // The far prefix drains at the pre-punch barrier (retail - // DrawBuilding @0x0059F2A0 flushes before its portal-only - // far-Z pass), and the full drain runs after the dynamics - // pass, where the frame's opaque world depth is complete. + "unattached-particles", + "landscape-alpha", "indoor-routing", "indoor-routing", - "landscape-alpha-farther", "exit-mask", "indoor-routing", "opaque-shells", - "landscape-alpha", - "unattached-particles-interior", ], executor.Operations); } @@ -72,7 +65,7 @@ public sealed class RetailPViewPassExecutorTests string.Join('|', executor.Operations), "landscape-early", "landscape-late", - "unattached-particles-outdoor", + "unattached-particles", "landscape-alpha", "interior-depth-clear", "indoor-routing", @@ -100,32 +93,6 @@ public sealed class RetailPViewPassExecutorTests Assert.DoesNotContain("interior-depth-clear", executor.Operations); } - [Fact] - public void DrawInside_interior_without_an_outside_slice_still_draws_interior_unattached_particles() - { - // Repro (Sanctuary middle cell, looking north): spell ground effects - // vanished whenever no exit portal was in view, because unattached - // emitters submitted once PER outside slice under that slice's - // hardware clip slot — zero slices meant zero submissions. Retail - // draws such an emitter during its owner cell's walk turn - // (ShouldDrawParticles @0x0050FE60) and never clips it to a view. - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - var root = new LoadedCell - { - CellId = 0xA9B40100u, - WorldTransform = Matrix4x4.Identity, - InverseWorldTransform = Matrix4x4.Identity, - }; - - renderer.DrawInside(Frame(root), executor); - - Assert.Contains("unattached-particles-interior", executor.Operations); - Assert.DoesNotContain( - "unattached-particles-outdoor", - executor.Operations); - } - [Fact] public void Particle_classifications_reset_before_an_empty_following_frame() { @@ -310,9 +277,9 @@ public sealed class RetailPViewPassExecutorTests AssertAppearsInOrder( string.Join('|', executor.Operations), "landscape-early", - "unattached-particles-outdoor", + "unattached-particles", "landscape-static-particles", - "landscape-alpha-farther", + "landscape-alpha", "look-in-punch", "landscape-late", "landscape-alpha", @@ -386,7 +353,7 @@ public sealed class RetailPViewPassExecutorTests "look-in-punch", "landscape-building-shell", "landscape-static-particles", - "landscape-alpha-farther", + "landscape-alpha", "look-in-punch", "landscape-building-shell"); } @@ -796,13 +763,8 @@ public sealed class RetailPViewPassExecutorTests int portalIndex) => Operations.Add("look-in-punch"); public void DrawUnattachedSceneParticles( RetailPViewFrameInput frame, - bool outdoorCells) => Operations.Add( - outdoorCells - ? "unattached-particles-outdoor" - : "unattached-particles-interior"); + ClipViewSlice slice) => Operations.Add("unattached-particles"); public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha"); - public void FlushLandscapeAlphaFartherThan(float minViewerDistance) => - Operations.Add("landscape-alpha-farther"); public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles"); public void DrawDynamicsParticles( RetailPViewFrameInput frame, diff --git a/tests/AcDream.App.Tests/Rendering/Sky/RainAnimationClockTests.cs b/tests/AcDream.App.Tests/Rendering/Sky/RainAnimationClockTests.cs deleted file mode 100644 index c2fa5bea..00000000 --- a/tests/AcDream.App.Tests/Rendering/Sky/RainAnimationClockTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using AcDream.App.Rendering.Sky; -using Xunit; - -namespace AcDream.App.Tests.Rendering.Sky; - -public sealed class RainAnimationClockTests -{ - [Fact] - public void AnimationPhaseUsesMonotonicStopwatchTicks() - { - const long start = 1234; - - Assert.Equal( - 1f, - SkyRenderer.ElapsedAnimationSeconds(start, start + Stopwatch.Frequency)); - } - - [Fact] - public void RendererDoesNotReadTheAdjustableSystemClock() - { - string source = File.ReadAllText(Path.Combine( - RepositoryRoot(), "src", "AcDream.App", "Rendering", "Sky", "SkyRenderer.cs")); - - Assert.Contains("Stopwatch.GetTimestamp()", source, StringComparison.Ordinal); - Assert.DoesNotContain("DateTime.UtcNow", source, StringComparison.Ordinal); - } - - private static string RepositoryRoot() - { - var directory = new DirectoryInfo(AppContext.BaseDirectory); - while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) - directory = directory.Parent; - return directory?.FullName - ?? throw new InvalidOperationException("Could not locate repository root."); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/SkyPesFrameControllerTests.cs b/tests/AcDream.App.Tests/Rendering/SkyPesFrameControllerTests.cs index 04cac29d..8567e977 100644 --- a/tests/AcDream.App.Tests/Rendering/SkyPesFrameControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/SkyPesFrameControllerTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; -using AcDream.Core.Physics; using AcDream.Core.Vfx; using AcDream.Core.World; using DatReaderWriter.Types; @@ -23,59 +22,20 @@ public sealed class SkyPesFrameControllerTests { private const uint AuroraSetup = 0x02000714u; private const uint AuroraScript = 0x330007DBu; - private const uint LightningEmitter = 0x320002C2u; private sealed class Harness { - private sealed class RecordingHookSink : IAnimationHookSink - { - public List<(uint EntityId, Vector3 Position, AnimationHook Hook)> Calls { get; } = []; - - public void OnHook( - uint entityId, - Vector3 entityWorldPosition, - AnimationHook hook) => - Calls.Add((entityId, entityWorldPosition, hook)); - } - public readonly List ResolvedScriptIds = []; public readonly List Diagnostics = []; - public readonly List<(uint EntityId, Vector3 Position, AnimationHook Hook)> HookCalls; public readonly PhysicsScriptRunner Runner; public readonly SkyPesFrameController Controller; - public readonly ParticleSystem Particles; - public Harness(AnimationHook? hook = null, double hookTime = 0.0) + public Harness() { var registry = new EmitterDescRegistry(); - registry.Register(new EmitterDesc - { - DatId = LightningEmitter, - Type = ParticleType.Still, - Flags = EmitterFlags.Billboard, - EmitterKind = ParticleEmitterKind.BirthratePerSec, - MaxParticles = 2, - InitialParticles = 1, - LifetimeMin = 0.01f, - LifetimeMax = 0.01f, - Lifespan = 0.01f, - StartSize = 1f, - EndSize = 1f, - StartAlpha = 1f, - EndAlpha = 1f, - Birthrate = 1000f, - }); - Particles = new ParticleSystem(registry, new Random(42)); + var system = new ParticleSystem(registry, new Random(42)); var poses = new EntityEffectPoseRegistry(); - var sink = new ParticleHookSink(Particles, poses) - { - DiagnosticSink = Diagnostics.Add, - }; - var recording = new RecordingHookSink(); - HookCalls = recording.Calls; - var router = new AnimationHookRouter(); - router.Register(sink); - router.Register(recording); + var sink = new ParticleHookSink(system, poses); Runner = new PhysicsScriptRunner( id => { @@ -83,12 +43,12 @@ public sealed class SkyPesFrameControllerTests var script = new DatPhysicsScript(); script.ScriptData.Add(new PhysicsScriptData { - StartTime = hookTime, - Hook = hook ?? new SoundHook(), + StartTime = 0.0, + Hook = new SoundHook(), }); return script; }, - router); + sink); Controller = new SkyPesFrameController( Runner, sink, @@ -209,52 +169,4 @@ public sealed class SkyPesFrameControllerTests h.Controller.Update(0.1f, null, Vector3.Zero); Assert.Equal(0, h.Runner.ActiveScriptCount); } - - [Fact] - public void LightningPartZeroUsesTheLiveCarrierPoseAndStopsOnDayGroupFlip() - { - var create = new CreateParticleHook - { - EmitterInfoId = LightningEmitter, - EmitterId = 1u, - PartIndex = 0u, - Offset = new Frame(), - }; - var h = new Harness(create); - - h.Controller.Update(0.1f, Group(Carrier()), new Vector3(4f, 5f, 6f)); - h.Runner.Tick(0.0); - - Assert.Equal(1, h.Particles.ActiveEmitterCount); - Assert.Empty(h.Diagnostics); - - h.Controller.Update(0.1f, null, Vector3.Zero); - h.Runner.Tick(1.0); - - Assert.Equal(0, h.Runner.ActiveScriptCount); - Assert.Empty(h.Diagnostics); - } - - [Fact] - public void PersistentWeatherCarrier_RefreshesSoundAnchorToCurrentCamera() - { - var sound = new SoundTweakedHook - { - SoundId = 0x0A00038Bu, - Volume = 0.1f, - Priority = 1f, - }; - var h = new Harness(sound, hookTime: 1.0); - var initialCamera = new Vector3(10f, 20f, 30f); - var currentCamera = new Vector3(410f, 520f, 630f); - - h.Controller.Update(0.3f, Group(Carrier()), initialCamera); - h.Controller.Update(0.4f, Group(Carrier()), currentCamera); - h.Runner.Tick(1.0); - - var call = Assert.Single(h.HookCalls); - Assert.Same(sound, call.Hook); - Assert.Equal(currentCamera, call.Position); - Assert.Single(h.ResolvedScriptIds); - } } diff --git a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs index 1b7467e0..4ae2c907 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs @@ -30,25 +30,11 @@ using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -using CullMode = DatReaderWriter.Enums.CullMode; namespace AcDream.App.Tests.Rendering.Wb; public class EnvCellRendererTests { - [Theory] - [InlineData(CullMode.Landblock)] - [InlineData(CullMode.None)] - [InlineData(CullMode.Clockwise)] - [InlineData(CullMode.CounterClockwise)] - public void CellShellCullPolicy_UsesRetailConstructedMeshClockwiseCull( - CullMode sourceSidesType) - { - Assert.Equal( - CullMode.Clockwise, - EnvCellRenderer.ResolveRetailCellShellCullMode(sourceSidesType)); - } - private sealed class NullPreparedAssetSource : IPreparedAssetSource { public PreparedAssetSourceStats Stats => default; diff --git a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs index afefb701..7bea5c2c 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs @@ -300,56 +300,6 @@ public sealed class WorldRenderFrameBuilderTests Assert.Contains(hiddenLight, lighting.PointSnapshot); } - [Fact] - public void PersistentAtDay_UsesNoonLandscapeLightingWithoutChangingTheSkyClock() - { - SkyStateProvider sky = SkyStateProvider.Default(); - var clock = new WorldTimeService(sky); - clock.PinnedDayFraction = 0f; - var lighting = new LightManager(); - bool persistentDaylight = true; - var environment = new RuntimeWorldFrameEnvironmentPreparation( - RuntimeOptions.Parse("test-dat", _ => null), - clock, - lighting, - dispatcher: null, - environmentCells: null, - lightingUbo: null, - new WorldRenderRangeState(4, 12), - skyPes: null, - persistentDaylight: () => persistentDaylight); - WorldCameraFrame camera = CameraFrame(new FlyCamera()); - WorldRootFrame roots = default; - SkyKeyframe midnight = sky.Interpolate(0f); - SkyKeyframe noon = sky.Interpolate(0.5f); - var foundation = new RenderFrameFoundation( - PortalViewportVisible: false, - Sky: midnight, - Atmosphere: default); - - environment.Prepare( - in camera, - in roots, - in foundation, - activeDayGroup: null); - - Assert.Equal(noon.AmbientColor, lighting.CurrentAmbient.AmbientColor); - Assert.NotNull(lighting.Sun); - Assert.Equal(noon.SunColor, lighting.Sun!.ColorLinear); - Assert.Equal(0d, clock.DayFraction, precision: 5); - - persistentDaylight = false; - environment.Prepare( - in camera, - in roots, - in foundation, - activeDayGroup: null); - - Assert.Equal(midnight.AmbientColor, lighting.CurrentAmbient.AmbientColor); - Assert.NotNull(lighting.Sun); - Assert.Equal(midnight.SunColor, lighting.Sun!.ColorLinear); - } - [Fact] public void Environment_preparation_keeps_lighting_snapshot_before_ubo_upload() { diff --git a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs index c67028eb..a8baa7b9 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs @@ -251,29 +251,6 @@ public sealed class WorldSceneRendererTests Assert.Equal(4, rig.PView.LastInput.RenderRadius); } - [Fact] - public void PViewWorld_PublishesLookInCellsToParticleAndLightVisibility() - { - var root = new LoadedCell - { - CellId = 0x01010001u, - IsOutdoorNode = false, - }; - var rig = new Rig(portalVisible: false, waitingForLogin: false, clipRoot: root); - - rig.Renderer.Render(default); - - // Retail gates effects on cell->IsInView() (ShouldDrawParticles - // @0x0050FE60), and a cell entered through a building portal - // (DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0) is - // drawn by the same traversal as a flooded cell. The particle gate and - // the light-candidate scope must therefore receive InViewCells (flood - // plus look-ins), not the flood-only DrawableCells. - Assert.Contains(0x01010003u, rig.Visibility.MarkedCells); - Assert.NotNull(rig.Frames.ObservedCells); - Assert.Contains(0x01010003u, rig.Frames.ObservedCells!); - } - [Fact] public void PViewWorld_ReusesOneSynchronousFrameInputAcrossFrames() { @@ -559,8 +536,7 @@ public sealed class WorldSceneRendererTests Frames = new FrameBuilder(Calls, frame); Selection = new SelectionFrame(Calls); var alpha = new AlphaFrame(Calls); - Visibility = new ParticleVisibility(Calls); - var visibility = Visibility; + var visibility = new ParticleVisibility(Calls); PView = new PViewRenderer(Calls); Passes = new PassExecutor(Calls); var diagnostics = new Diagnostics(Calls); @@ -597,8 +573,6 @@ public sealed class WorldSceneRendererTests public SelectionFrame Selection { get; } - public ParticleVisibility Visibility { get; } - public PViewRenderer PView { get; } public PassExecutor Passes { get; } @@ -647,13 +621,8 @@ public sealed class WorldSceneRendererTests return frame; } - public IReadOnlySet? ObservedCells { get; private set; } - - public void ObserveDrawableCells(IReadOnlySet drawableCells) - { + public void ObserveDrawableCells(IReadOnlySet drawableCells) => calls.Add("frame:observe-cells"); - ObservedCells = new HashSet(drawableCells); - } public void ClearDrawableCells() => calls.Add("frame:clear-cells"); } @@ -715,13 +684,8 @@ public sealed class WorldSceneRendererTests private sealed class ParticleVisibility(List calls) : IWorldSceneParticleVisibility { - public HashSet MarkedCells { get; } = []; - - public void MarkVisibleCells(HashSet cellIds) - { + public void MarkVisibleCells(HashSet cellIds) => calls.Add("visibility:mark"); - MarkedCells.UnionWith(cellIds); - } public void CompleteFrame() => calls.Add("visibility:complete"); @@ -737,17 +701,11 @@ public sealed class WorldSceneRendererTests public PViewRenderer(List calls) { _calls = calls; - // Distinct flood-only vs in-view sets: 0x01010003 is a look-in - // cell that is drawn but never part of the main flood. - var interiorPartition = new InteriorEntityPartition.Result(); _interiorResult = new RetailPViewFrameResult().Reset( new PortalVisibilityFrame(), new ClipFrameAssembly(), - [0x01010001u], - [0x01010001u, 0x01010003u], - RetailPViewRenderer.LegacyDiagnosticCounts(interiorPartition), - RetailPViewRenderer.LegacySourceCounts(interiorPartition), - interiorPartition); + [], + new InteriorEntityPartition.Result()); var outdoorPortalFrame = new PortalVisibilityFrame(); outdoorPortalFrame.OutsideView.Add(new ViewPolygon( [ diff --git a/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs b/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs index 7ba12add..387a1752 100644 --- a/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs +++ b/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs @@ -6,10 +6,11 @@ namespace AcDream.App.Tests; public class RuntimeOptionsRetailUiTests { [Fact] - public void Parse_RetailUiIsDefaultOnAndReadsAcDir() + public void Parse_ReadsRetailUiAndAcDir() { var env = new Dictionary { + ["ACDREAM_RETAIL_UI"] = "1", ["ACDREAM_AC_DIR"] = @"C:\Turbine\Asheron's Call", }; var opts = RuntimeOptions.Parse("dats", k => env.GetValueOrDefault(k)); @@ -18,21 +19,11 @@ public class RuntimeOptionsRetailUiTests } [Fact] - public void Parse_DefaultsRetailUiOnAndAcDirNull() + public void Parse_DefaultsRetailUiOffAndAcDirNull() { var opts = RuntimeOptions.Parse("dats", _ => null); - Assert.True(opts.RetailUi); - Assert.Null(opts.AcDir); - } - - [Fact] - public void Parse_LiteralZeroOptsOutOfRetailUi() - { - var opts = RuntimeOptions.Parse( - "dats", - key => key == "ACDREAM_RETAIL_UI" ? "0" : null); - Assert.False(opts.RetailUi); + Assert.Null(opts.AcDir); } [Fact] diff --git a/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs b/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs index 4c364a10..dfb1984a 100644 --- a/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs +++ b/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs @@ -11,40 +11,6 @@ namespace AcDream.App.Tests.Settings; public sealed class RuntimeSettingsControllerTests { - [Theory] - [InlineData(3, 3, 3)] - [InlineData(5, 4, 5)] - [InlineData(8, 4, 8)] - [InlineData(12, 4, 12)] - [InlineData(25, 4, 25)] - public void LandscapeDrawDistance_IsTheRetailFarRadiusValue( - int value, - int expectedNear, - int expectedFar) - { - QualitySettings result = RuntimeSettingsController - .ApplyLandscapeDrawDistance( - QualitySettings.From(QualityPreset.High), - value); - - Assert.Equal(expectedNear, result.NearRadius); - Assert.Equal(expectedFar, result.FarRadius); - } - - [Theory] - [InlineData(0)] - [InlineData(2)] - [InlineData(26)] - public void InvalidStoredLandscapeDistance_PreservesThePreset(int value) - { - QualitySettings original = QualitySettings.From(QualityPreset.High); - - QualitySettings result = RuntimeSettingsController - .ApplyLandscapeDrawDistance(original, value); - - Assert.Equal(original, result); - } - [Fact] public void ConstructionLoadsEachBagOnceAndPublishesOneStartupSnapshot() { diff --git a/tests/AcDream.App.Tests/UI/ClientCommandControllerTests.cs b/tests/AcDream.App.Tests/UI/ClientCommandControllerTests.cs index 82c5f2fb..5d5a85a7 100644 --- a/tests/AcDream.App.Tests/UI/ClientCommandControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/ClientCommandControllerTests.cs @@ -1,4 +1,3 @@ -using System.Globalization; using AcDream.App.UI; using AcDream.Core.Chat; using AcDream.Core.Physics; @@ -553,355 +552,11 @@ public sealed class ClientCommandControllerTests Assert.Equal("my chat log.txt", match.Arguments); } - // ── #361: retail's @day / @render ───────────────────────────────── - - [Fact] - public void Day_TogglesThePersistentOptionAndPrintsRetailsExactLines() - { - bool persistentDaylight = false; - var values = new List(); - var messages = new List(); - ClientCommandController controller = NewController( - messages: messages, - isPersistentDaylight: () => persistentDaylight, - setPersistentDaylight: value => - { - persistentDaylight = value; - values.Add(value); - }); - - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.TogglePersistentDaylight, - "ignored exactly like retail")); - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.TogglePersistentDaylight, - string.Empty)); - - Assert.Equal([true, false], values); - Assert.Equal( - ["Let there be light!", "Normality has been restored."], - messages); - } - - [Theory] - [InlineData("radius 5", 5)] - [InlineData("RADIUS 25 extra ignored", 25)] - [InlineData("radius 12suffix", 12)] - public void RenderRadius_AcceptsRetailRangeAndAtoiPrefix( - string arguments, - int expected) - { - var radii = new List(); - var messages = new List(); - ClientCommandController controller = NewController( - messages: messages, - setLandscapeRadius: radii.Add); - - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.RenderOption, - arguments)); - - Assert.Equal([expected], radii); - Assert.Equal(["Landscape radius set"], messages); - } - - [Theory] - [InlineData("fov 10", 10f)] - [InlineData("FOV 160 extra", 160f)] - [InlineData("fov 91degrees", 91f)] - public void RenderFov_AcceptsRetailRangeAndAtoiPrefix( - string arguments, - float expected) - { - var values = new List(); - var messages = new List(); - ClientCommandController controller = NewController( - messages: messages, - setFieldOfView: values.Add); - - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.RenderOption, - arguments)); - - Assert.Equal([expected], values); - Assert.Equal(["Field of view set"], messages); - } - - [Theory] - [InlineData("radius", "Must specify a radius")] - [InlineData("radius 4", "Radius must be between 5 and 25")] - [InlineData("radius nope", "Radius must be between 5 and 25")] - [InlineData("fov", "Must specify a field of view")] - [InlineData("fov 161", "Field of view must be between 10 and 160")] - public void Render_InvalidValuesPrintRetailsExactReply( - string arguments, - string expected) - { - var calls = new List(); - var messages = new List(); - ClientCommandController controller = NewController( - calls, - messages: messages); - - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.RenderOption, - arguments)); - - Assert.DoesNotContain(calls, call => - call.StartsWith("radius:", StringComparison.Ordinal) - || call.StartsWith("fov:", StringComparison.Ordinal)); - Assert.Equal([expected], messages); - } - - [Fact] - public void Render_UsageAndUnknownOptionMatchRetail() - { - var messages = new List(); - ClientCommandController controller = NewController(messages: messages); - - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.RenderOption, - string.Empty)); - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.RenderOption, - "usage")); - controller.Execute(new ExecuteClientCommandCmd( - ClientCommandId.RenderOption, - "unknown 1")); - - string usage = RetailCommandHelpTable.Render.TrimEnd('\n'); - Assert.Equal([usage, usage], messages); - } - - [Fact] - public void AllegianceAdministration_ExecutesEveryRetailDispatcherBranch() - { - var calls = new List(); - var system = new List(); - var clientLocal = new List(); - ClientCommandController controller = NewController( - calls: calls, - messages: system, - clientLocalMessages: clientLocal); - - Execute(ClientCommandId.AllegianceInfo, "Lord Bob"); - Execute(ClientCommandId.AllegianceBoot, "Lord Bob"); - Execute(ClientCommandId.AllegianceBoot, "-account Account Bob"); - // Retail validates non-empty BEFORE removing -account, so this odd - // form deliberately sends an empty name with accountBoot=true. - Execute(ClientCommandId.AllegianceBoot, "-account"); - Execute(ClientCommandId.AllegianceBan, "list ignored"); - Execute(ClientCommandId.AllegianceBan, "add Lord Bob"); - Execute(ClientCommandId.AllegianceBan, "remove Lord Bob"); - Execute(ClientCommandId.AllegianceChat, "on"); - Execute(ClientCommandId.AllegianceChat, "off"); - Execute(ClientCommandId.AllegianceChat, "kick Bob"); - Execute(ClientCommandId.AllegianceChat, "kick Bob, Bad manners"); - Execute(ClientCommandId.AllegianceChat, "kick "); - Execute(ClientCommandId.AllegianceChat, "gag Lord Bob"); - Execute(ClientCommandId.AllegianceChat, "ungag Lord Bob"); - Execute(ClientCommandId.AllegianceBroadcast, "Hear ye"); - Execute(ClientCommandId.AllegianceOfficer, ""); - Execute(ClientCommandId.AllegianceOfficer, "clear ignored"); - Execute(ClientCommandId.AllegianceOfficer, "remove Lord Bob"); - Execute(ClientCommandId.AllegianceOfficer, "add 0x2 Lord Bob"); - Execute(ClientCommandId.AllegianceOfficer, "set 03 Aunt Alice"); - Execute(ClientCommandId.AllegianceOfficerTitle, ""); - Execute(ClientCommandId.AllegianceOfficerTitle, "clear ignored"); - Execute(ClientCommandId.AllegianceOfficerTitle, "set 0x2 High Regent"); - Execute(ClientCommandId.AllegianceOfficerTitle, "set 1"); - Execute(ClientCommandId.AllegianceName, ""); - Execute(ClientCommandId.AllegianceName, "set The Best Allegiance"); - Execute(ClientCommandId.AllegianceName, "set"); - Execute(ClientCommandId.AllegianceName, "clear ignored"); - Execute(ClientCommandId.AllegianceLock, ""); - Execute(ClientCommandId.AllegianceLock, "off"); - Execute(ClientCommandId.AllegianceLock, "on"); - Execute(ClientCommandId.AllegianceLock, "toggle"); - Execute(ClientCommandId.AllegianceLock, "check"); - Execute(ClientCommandId.AllegianceLock, "bypass"); - Execute(ClientCommandId.AllegianceLock, "bypass clear"); - Execute(ClientCommandId.AllegianceLock, "bypass Lord Bob"); - Execute(ClientCommandId.AllegianceHouse, ""); - Execute(ClientCommandId.AllegianceHouse, "guest open"); - Execute(ClientCommandId.AllegianceHouse, "guest close"); - Execute(ClientCommandId.AllegianceHouse, "storage open"); - Execute(ClientCommandId.AllegianceHouse, "storage close"); - Execute(ClientCommandId.AllegianceMotd, ""); - Execute(ClientCommandId.AllegianceMotd, "set Welcome everyone"); - Execute(ClientCommandId.AllegianceMotd, "set"); - Execute(ClientCommandId.AllegianceMotd, "clear ignored"); - - Assert.Equal( - [ - "alleginfo:Lord Bob", - "allegboot:Lord Bob:False", - "allegboot:Account Bob:True", - "allegboot::True", - "allegban:list", - "allegban:add:Lord Bob", - "allegban:remove:Lord Bob", - "charoption:27:True", - "charoption:27:False", - "allegchatboot:Bob:No reason given.", - "allegchatboot:Bob:Bad manners", - "allegchatboot::No reason given.", - "allegchatgag:Lord Bob:True", - "allegchatgag:Lord Bob:False", - "allegbroadcast:Hear ye", - "allegofficer:list", - "allegofficer:clear", - "allegofficer:remove:Lord Bob", - "allegofficer:set:2:Lord Bob", - "allegofficer:set:3:Aunt Alice", - "allegtitle:list", - "allegtitle:clear", - "allegtitle:set:2:High Regent", - "allegtitle:set:1:", - "allegname:query", - "allegname:set:The Best Allegiance", - "allegname:set:", - "allegname:clear", - "alleglock:4", - "alleglock:1", - "alleglock:2", - "alleglock:3", - "alleglock:4", - "alleglock:5", - "alleglock:6", - "alleglock:bypass:Lord Bob", - "alleghouse:1", - "alleghouse:2", - "alleghouse:3", - "alleghouse:4", - "alleghouse:5", - "motd:query", - "motd:set:Welcome everyone", - "motd:set:", - "motd:clear", - ], - calls); - Assert.Equal( - [ - "Attempting to boot Lord Bob...", - "Attempting to boot Account Bob (Account)...", - "Attempting to boot (Account)...", - ], - system); - Assert.Empty(clientLocal); - - void Execute(ClientCommandId command, string arguments) => - controller.Execute(new ExecuteClientCommandCmd(command, arguments)); - } - - [Fact] - public void HouseAdministration_ExecutesEveryRetailDispatcherBranch() - { - var calls = new List(); - ClientCommandController controller = NewController(calls: calls); - - Execute(ClientCommandId.HouseOpenStatus, "open"); - Execute(ClientCommandId.HouseOpenStatus, "close"); - Execute(ClientCommandId.HouseGuests, "add Lord Bob"); - Execute(ClientCommandId.HouseGuests, "remove Lord Bob"); - Execute(ClientCommandId.HouseGuests, "remove_all ignored"); - Execute(ClientCommandId.HouseGuests, "list ignored"); - Execute(ClientCommandId.HouseGuests, "show ignored"); - Execute(ClientCommandId.HouseGuests, "add_allegiance ignored"); - Execute(ClientCommandId.HouseGuests, "remove_allegiance ignored"); - Execute(ClientCommandId.HouseStorage, "add Lord Bob"); - Execute(ClientCommandId.HouseStorage, "remove Lord Bob"); - Execute(ClientCommandId.HouseStorage, "add -all"); - Execute(ClientCommandId.HouseStorage, "remove -all"); - Execute(ClientCommandId.HouseStorage, "remove_all ignored"); - Execute(ClientCommandId.HouseStorage, "list ignored"); - Execute(ClientCommandId.HouseStorage, "show ignored"); - Execute(ClientCommandId.HouseStorage, "add_allegiance ignored"); - Execute(ClientCommandId.HouseStorage, "remove_allegiance ignored"); - Execute(ClientCommandId.HouseBoot, "Lord Bob"); - Execute(ClientCommandId.HouseBoot, "-all"); - Execute(ClientCommandId.HouseBootAll, "ignored"); - Execute(ClientCommandId.HouseHooks, "on ignored"); - Execute(ClientCommandId.HouseHooks, "off ignored"); - - Assert.Equal( - [ - "houseopen:True", - "houseopen:False", - "houseguest:add:Lord Bob", - "houseguest:remove:Lord Bob", - "houseguest:remove_all", - "houseguest:list", - "houseguest:list", - "houseguest:allegiance:True", - "houseguest:allegiance:False", - "housestorage:True:Lord Bob", - "housestorage:False:Lord Bob", - "housestorage:add_all", - "housestorage:remove_all", - "housestorage:remove_all", - "houseguest:list", - "houseguest:list", - "housestorage:allegiance:True", - "housestorage:allegiance:False", - "houseboot:Lord Bob", - "houseboot:all", - "houseboot:all", - "househooks:True", - "househooks:False", - ], - calls); - - void Execute(ClientCommandId command, string arguments) => - controller.Execute(new ExecuteClientCommandCmd(command, arguments)); - } - - [Theory] - [InlineData(ClientCommandId.AllegianceInfo, "", "Please specify an actual name.")] - [InlineData(ClientCommandId.AllegianceBoot, "", "Please specify an actual name.")] - [InlineData(ClientCommandId.AllegianceBan, "add", "Please specify an actual name.")] - [InlineData(ClientCommandId.AllegianceChat, "gag", "Please specify an actual name.")] - [InlineData(ClientCommandId.AllegianceBroadcast, "", "Please see @help Allegiance for more information on how to use this command.")] - [InlineData(ClientCommandId.AllegianceOfficer, "remove", "Please specify the name of an allegiance member.")] - [InlineData(ClientCommandId.AllegianceOfficer, "add nope Bob", "Please specify a valid officer level as a number between 1 and 3. Check the game help files for more information on officer levels.")] - [InlineData(ClientCommandId.AllegianceOfficer, "add 2", "Please specify the name of an allegiance member.")] - [InlineData(ClientCommandId.AllegianceOfficerTitle, "set 4 Regent", "Please specify a valid officer level as a number between 1 and 3.")] - [InlineData(ClientCommandId.AllegianceName, "nope", "Please see @help Allegiance for more information on how to use this command.")] - [InlineData(ClientCommandId.AllegianceLock, "nope", "Please see @help Allegiance for more information on how to use this command.")] - [InlineData(ClientCommandId.AllegianceHouse, "guest nope", "Please see @help Allegiance for more information on how to use this command.")] - [InlineData(ClientCommandId.AllegianceMotd, "nope", "Please see @help Allegiance for more information on how to use this command.")] - [InlineData(ClientCommandId.AllegianceUnrecognizedSubcommand, "nope", "Please see @help Allegiance for more information on how to use this command.")] - [InlineData(ClientCommandId.HouseGuests, "add", "Please specify the guest's name.")] - [InlineData(ClientCommandId.HouseStorage, "add", "Please specify an actual name.")] - [InlineData(ClientCommandId.HouseBoot, "", "Please see @help House for more information on how to use this command.")] - [InlineData(ClientCommandId.HouseHooks, "maybe", "Please see @help House for more information on how to use this command.")] - [InlineData(ClientCommandId.HouseUnrecognizedSubcommand, "nope", "Please see @help House for more information on how to use this command.")] - public void AdministrationInvalidForms_EmitExactRetailClientLocalText( - ClientCommandId command, - string arguments, - string expected) - { - var calls = new List(); - var system = new List(); - var clientLocal = new List(); - ClientCommandController controller = NewController( - calls: calls, - messages: system, - clientLocalMessages: clientLocal); - - controller.Execute(new ExecuteClientCommandCmd(command, arguments)); - - Assert.Empty(calls); - Assert.Empty(system); - Assert.Equal([expected], clientLocal); - } - private static ClientCommandController NewController( List? calls = null, List? errors = null, uint? playerBitfield = 0x02000028u, List? messages = null, - List? clientLocalMessages = null, bool isAway = false, bool acceptsLootPermits = true, FriendsState? friends = null, @@ -914,16 +569,11 @@ public sealed class ClientCommandControllerTests // Defaults to "always accept" so every pre-existing single-stage // test (Die, etc.) keeps its original behavior unchanged. Queue? confirmationResponses = null, - Func? chatLog = null, - Func? isPersistentDaylight = null, - Action? setPersistentDaylight = null, - Action? setLandscapeRadius = null, - Action? setFieldOfView = null) + Func? chatLog = null) { calls ??= []; errors ??= []; messages ??= []; - clientLocalMessages ??= messages; return new ClientCommandController(new ClientCommandController.Bindings( () => calls.Add("ls"), () => calls.Add("mp"), @@ -936,7 +586,6 @@ public sealed class ClientCommandControllerTests () => calls.Add("fps"), () => calls.Add("lock"), messages.Add, - clientLocalMessages.Add, errors.Add, () => playerBitfield, () => "1.2.3", @@ -1004,61 +653,6 @@ public sealed class ClientCommandControllerTests channelId => calls.Add("off:" + channelId), () => calls.Add("alh"), name => calls.Add("alleginfo:" + name), - () => calls.Add("houseabandon"), - NewAdministrationBindings(calls), - isPersistentDaylight ?? (() => false), - setPersistentDaylight ?? (value => calls.Add("day:" + value)), - setLandscapeRadius ?? (value => calls.Add("radius:" + value)), - setFieldOfView ?? (value => calls.Add( - "fov:" + value.ToString(CultureInfo.InvariantCulture))))); + () => calls.Add("houseabandon"))); } - - private static ClientCommandController.AdministrationBindings - NewAdministrationBindings(List calls) => new( - BreakAllegianceBoot: (name, account) => - calls.Add($"allegboot:{name}:{account}"), - AllegianceChatBoot: (name, reason) => - calls.Add($"allegchatboot:{name}:{reason}"), - AllegianceChatGag: (name, enabled) => - calls.Add($"allegchatgag:{name}:{enabled}"), - AllegianceBroadcast: text => calls.Add("allegbroadcast:" + text), - ListAllegianceBans: () => calls.Add("allegban:list"), - AddAllegianceBan: name => calls.Add("allegban:add:" + name), - RemoveAllegianceBan: name => calls.Add("allegban:remove:" + name), - ListAllegianceOfficers: () => calls.Add("allegofficer:list"), - ClearAllegianceOfficers: () => calls.Add("allegofficer:clear"), - SetAllegianceOfficer: (name, level) => - calls.Add($"allegofficer:set:{level}:{name}"), - RemoveAllegianceOfficer: name => - calls.Add("allegofficer:remove:" + name), - ListAllegianceOfficerTitles: () => calls.Add("allegtitle:list"), - ClearAllegianceOfficerTitles: () => calls.Add("allegtitle:clear"), - SetAllegianceOfficerTitle: (level, title) => - calls.Add($"allegtitle:set:{level}:{title}"), - QueryAllegianceName: () => calls.Add("allegname:query"), - SetAllegianceName: name => calls.Add("allegname:set:" + name), - ClearAllegianceName: () => calls.Add("allegname:clear"), - AllegianceLockAction: action => calls.Add("alleglock:" + action), - SetAllegianceApprovedVassal: name => - calls.Add("alleglock:bypass:" + name), - AllegianceHouseAction: action => calls.Add("alleghouse:" + action), - QueryMotd: () => calls.Add("motd:query"), - SetMotd: text => calls.Add("motd:set:" + text), - ClearMotd: () => calls.Add("motd:clear"), - SetOpenHouseStatus: open => calls.Add("houseopen:" + open), - AddPermanentGuest: name => calls.Add("houseguest:add:" + name), - RemovePermanentGuest: name => calls.Add("houseguest:remove:" + name), - RemoveAllPermanentGuests: () => calls.Add("houseguest:remove_all"), - ChangeStoragePermission: (name, enabled) => - calls.Add($"housestorage:{enabled}:{name}"), - AddAllStoragePermission: () => calls.Add("housestorage:add_all"), - RemoveAllStoragePermission: () => calls.Add("housestorage:remove_all"), - RequestFullGuestList: () => calls.Add("houseguest:list"), - BootSpecificHouseGuest: name => calls.Add("houseboot:" + name), - BootEveryone: () => calls.Add("houseboot:all"), - SetHooksVisibility: visible => calls.Add("househooks:" + visible), - ModifyAllegianceGuestPermission: enabled => - calls.Add("houseguest:allegiance:" + enabled), - ModifyAllegianceStoragePermission: enabled => - calls.Add("housestorage:allegiance:" + enabled)); } diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs index 60c129bc..93ba62fa 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs @@ -1059,8 +1059,8 @@ public sealed class CharacterCreationLiveDatTests /// GF-13: the BEHAVIOR half — after the real controller mounts through /// (not a raw /// call), the two authored-invisible - /// elements are not . Exercises the shared - /// importer-wide #408 behavior through a real chargen controller mount, + /// elements are not . Exercises + /// HideAuthoredInvisibleElements's real chargen-scoped honor path, /// not just the data plumbing the sibling test above pins. /// [InstalledDatFact] diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationUiControllerTests.cs index ed326b6e..859ed34d 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationUiControllerTests.cs @@ -446,7 +446,7 @@ public sealed class CharacterCreationUiControllerTests /// Center default. /// [Fact] - public void SkillsPage_InfoBoxPanes_InheritRetailTopDefault_ToAvoidTitleDescriptionOverlap() + public void SkillsPage_InfoBoxPanes_ForceTopVerticalJustify_ToAvoidTitleDescriptionOverlap() { using var environment = new EnvironmentHarness(); environment.Controller.Open(); diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs index 779f4b2b..ec21c062 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs @@ -168,31 +168,6 @@ public sealed class CharacterManagementUiControllerTests string.Join(" ", worldText.LinesProvider().Select(static line => line.Text))); } - [Fact] - public void CreditsButton_QueuesTheCreditsMode_AndPresentationCanBeSuppressedForIt() - { - int openCalls = 0; - using var environment = new EnvironmentHarness(() => openCalls++); - CharacterManagementUiController controller = environment.Controller; - UiButton credits = environment.Button( - CharacterManagementUiController.CreditsElementId); - - Assert.True(credits.Visible); - Assert.True(credits.Enabled); - Assert.NotNull(credits.OnClick); - credits.OnClick!(); - Assert.Equal(1, openCalls); - - controller.SetPresentationSuppressed(true); - Assert.False(controller.Root.Visible); - Assert.Null(environment.Host.FixedCanvasSize); - - controller.SetPresentationSuppressed(false); - Assert.True(controller.Root.Visible); - Assert.Equal(new Vector2(800f, 600f), environment.Host.FixedCanvasSize); - Assert.Equal(3, controller.Rows.Count); - } - [Fact] public void RowHeight_UsesAllowedSlotsAndClampsAtOneTenthForLargeRosters() { @@ -907,7 +882,7 @@ public sealed class CharacterManagementUiControllerTests private sealed class EnvironmentHarness : IDisposable { - public EnvironmentHarness(Action? openCredits = null) + public EnvironmentHarness() { Host = new UiRoot { Width = 800f, Height = 600f }; Screen = BuildScreen(); @@ -926,8 +901,7 @@ public sealed class CharacterManagementUiControllerTests static (_, _) => BuildRow(), Dialogs, Runtime.Bindings, - TestStrings(), - openCredits)); + TestStrings())); } public UiRoot Host { get; } diff --git a/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs index 04225261..9bcd6453 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs @@ -1115,7 +1115,7 @@ public sealed class ConfigOptionsPageControllerTests 2, // 19 Landscape Texture Detail 1, // 20 Environment Texture Detail 1, // 21 Texture Filtering - 8, // 22 Landscape Draw Distance (retail radius payload) + 8, // 22 Landscape Draw Distance (opaque — AP-198 sub-note) true, // 23 Building Detail Textures false, // 24 Multi-Pass Alpha @@ -1418,7 +1418,7 @@ public sealed class ConfigOptionsPageControllerTests (23, RowKind.Menu, true, "Landscape Texture Detail"), // AP-198 (24, RowKind.Menu, true, "Environment Texture Detail"), // AP-198 (25, RowKind.Menu, true, "Texture Filtering"), // AP-198 - (26, RowKind.Menu, false, "Landscape Draw Distance"), // LIVE — #361 + (26, RowKind.Menu, true, "Landscape Draw Distance"), // AP-198 (27, RowKind.Toggle, false, "Building Detail Textures"), // LIVE — #226 (28, RowKind.Toggle, true, "Multi-Pass Alpha"), // AP-198 (31, RowKind.Slider, true, "Mouse Look Sensitivity"), // TS-74 @@ -1485,27 +1485,6 @@ public sealed class ConfigOptionsPageControllerTests } } - [Fact] - public void LandscapeDrawDistance_UsesRetailRadiusPayloads_AndAppliesSelection() - { - (OptionsPanelController controller, FakeBindings bindings, bool bound) = BindReal(); - Assert.True(bound); - - var configSlot = UiElement.FindDescendant(controller.TabPanel, ConfigPageSlotId)!; - List menus = CollectMenus(configSlot); - UiMenu drawDistance = menus[5]; - - Assert.Equal(8, drawDistance.Selected); - Assert.Equal( - [3, 5, 8, 11, 15, 25], - drawDistance.Items.Select(item => Assert.IsType(item.Payload)).ToArray()); - - drawDistance.OnSelect!(25); - controller.ConfigPage.Apply(); - - Assert.Equal(25, bindings.Display.LandscapeDrawDistance); - } - // ── #412-class regression: Config tab content escaping the window frame ── // // 2026-08-16/17 overnight hover/UI round, Batch A bug 2. The user's diff --git a/tests/AcDream.App.Tests/UI/Layout/CreditsLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/CreditsLiveDatTests.cs deleted file mode 100644 index 06fdbe6c..00000000 --- a/tests/AcDream.App.Tests/UI/Layout/CreditsLiveDatTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using AcDream.App.UI.Layout; -using AcDream.App.UI; -using AcDream.Content; -using DatReaderWriter; -using DatReaderWriter.Options; - -namespace AcDream.App.Tests.UI.Layout; - -/// -/// Installed-retail-DAT oracle for issue #400's gmCreditsUI port. -/// Retail creates two independent enum-table-5 roots: the picture strip -/// and the scrolling text field. -/// -[Trait("Lane", "InstalledDat")] -public sealed class CreditsLiveDatTests -{ - [InstalledDatFact] - public void Category4_ResolvesAuthoredPictureAndTextRoots() - { - string datDirectory = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") - ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Documents", - "Asheron's Call"); - using var dats = new DatCollection(datDirectory, DatAccessType.Read); - - // CreateAndAddRootElement(layoutEnum=0x10000004, rootElementId) - // uses the ordinary table-5 layout map, exactly like character - // management's (0x10000005, 0x1000039A) pair. - uint pictureLayout = RetailDataIdResolver.Resolve(dats, 0x10000004u, 5u); - Assert.Equal(0x21000003u, pictureLayout); - - ElementInfo picture = Assert.IsType( - LayoutImporter.ImportInfos(dats, pictureLayout, 0x10000413u)); - ElementInfo text = Assert.IsType( - LayoutImporter.ImportInfos(dats, pictureLayout, 0x10000410u)); - - Assert.Equal(CreditsUiController.PictureRootElementId, picture.Id); - Assert.Equal(3u, picture.Type); - Assert.Equal((0f, 0f, 400f, 600f), - (picture.X, picture.Y, picture.Width, picture.Height)); - Assert.Equal(CreditsUiController.TextRootElementId, text.Id); - Assert.Equal(3u, text.Type); - Assert.Equal((400f, 0f, 400f, 600f), - (text.X, text.Y, text.Width, text.Height)); - - Assert.True(TryDataId(text, 0x10000002u, out uint textAreaId)); - Assert.Equal(CreditsUiController.TextAreaElementId, textAreaId); - Assert.True(TryDataId(text, 0x10000003u, out uint stringTableId)); - Assert.Equal(0x23000008u, stringTableId); - Assert.True(text.TryGetEffectiveFloat(0x10000004u, out float seconds)); - Assert.Equal(20f, seconds); - - ElementInfo textArea = Assert.Single(text.Children); - Assert.Equal(CreditsUiController.TextAreaElementId, textArea.Id); - Assert.Equal(12u, textArea.Type); - Assert.Equal(0x40000000u, textArea.FontDid); - Assert.Equal(HJustify.Center, textArea.HJustify); - Assert.Equal(VJustify.Center, textArea.VJustify); - - Assert.True(picture.TryGetEffectiveProperty( - 0x10000005u, - out UiPropertyValue pictureArray)); - Assert.Equal(UiPropertyKind.Array, pictureArray.Kind); - Assert.Equal( - Enumerable.Range(0, 7).Select(index => 0x06005F14u + (uint)index), - pictureArray.ArrayValue.Select(static value => (uint)value.UnsignedValue)); - - ImportedLayout builtText = Assert.IsType( - LayoutImporter.Import( - dats, - pictureLayout, - CreditsUiController.TextRootElementId, - static id => (id, 1, 1), - null)); - Assert.IsType(builtText.FindElement( - CreditsUiController.TextAreaElementId)); - - var strings = new DatStringResolver(dats); - int creditsCount = 0; - for (int i = 1; i <= 4096; i++) - { - string? value = strings.Resolve( - stringTableId, - DatStringResolver.ComputeHash($"ID_Credits{i}")); - if (value is null) - break; - creditsCount++; - } - Assert.Equal(2345, creditsCount); - Assert.NotNull(strings.Resolve( - 0x23000001u, - DatStringResolver.ComputeHash("ID_Wait_PleaseWait"))); - } - - private static bool TryDataId( - ElementInfo info, - uint propertyId, - out uint value) - { - if (info.TryGetEffectiveProperty(propertyId, out UiPropertyValue property) - && property.Kind is UiPropertyKind.DataId or UiPropertyKind.Enum) - { - value = checked((uint)property.UnsignedValue); - return true; - } - value = 0u; - return false; - } -} diff --git a/tests/AcDream.App.Tests/UI/Layout/CreditsUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CreditsUiControllerTests.cs deleted file mode 100644 index 57824eb5..00000000 --- a/tests/AcDream.App.Tests/UI/Layout/CreditsUiControllerTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Numerics; -using AcDream.App.UI; -using AcDream.App.UI.Layout; - -namespace AcDream.App.Tests.UI.Layout; - -public sealed class CreditsUiControllerTests -{ - [Fact] - public void Activate_UsesAuthoredCanvas_TextTiming_AndCyclicPictureStrip() - { - using var environment = new EnvironmentHarness(); - CreditsUiController controller = environment.Controller; - - controller.Activate(); - - Assert.True(controller.IsActive); - Assert.Equal(new Vector2(800f, 600f), environment.Host.FixedCanvasSize); - Assert.True(controller.PictureRoot.Visible); - Assert.True(controller.TextRoot.Visible); - Assert.Equal(600f, controller.TextArea.Top); - Assert.Equal(48f, controller.TextArea.Height); - Assert.Single(controller.Pictures); - Assert.Equal(601f, controller.Pictures[0].Top); - Assert.Equal(0x06000001u, controller.Pictures[0].BackgroundSprite); - Assert.Equal( - 20d * (600d + 48d) / (600d + 48d / 3d), - controller.DurationSeconds, - precision: 5); - - environment.Now += controller.DurationSeconds * 0.5d; - controller.Tick(); - - Assert.Equal(276f, controller.TextArea.Top); - Assert.True(controller.Pictures[0].Top < 600f); - Assert.Equal(2, controller.Pictures.Count); - Assert.Equal(0x06000002u, controller.Pictures[1].BackgroundSprite); - Assert.Equal( - controller.Pictures[0].Top + controller.Pictures[0].Height + 1f, - controller.Pictures[1].Top); - } - - [Fact] - public void AnyKey_ShowsWaitForAFrame_ThenReturnsToCharacterManagement() - { - using var environment = new EnvironmentHarness(); - CreditsUiController controller = environment.Controller; - controller.Activate(); - - environment.Host.OnKeyDown(123); - Assert.Equal(1, environment.Dialogs.ActiveCount); - - controller.Tick(); - Assert.True(controller.IsActive); - Assert.Equal(0, environment.ReturnCalls); - - controller.Tick(); - Assert.False(controller.IsActive); - Assert.Equal(1, environment.ReturnCalls); - Assert.Equal(0, environment.Dialogs.ActiveCount); - Assert.Null(environment.Host.FixedCanvasSize); - Assert.False(controller.PictureRoot.Visible); - Assert.False(controller.TextRoot.Visible); - } - - [Fact] - public void NaturalCompletion_UsesTheSameWaitAndReturnPath() - { - using var environment = new EnvironmentHarness(); - CreditsUiController controller = environment.Controller; - controller.Activate(); - - environment.Now += controller.DurationSeconds; - controller.Tick(); - Assert.Equal(1, environment.Dialogs.ActiveCount); - - controller.Tick(); - Assert.True(controller.IsActive); - controller.Tick(); - - Assert.False(controller.IsActive); - Assert.Equal(1, environment.ReturnCalls); - Assert.Equal(0, environment.Dialogs.ActiveCount); - } - - private sealed class EnvironmentHarness : IDisposable - { - public EnvironmentHarness() - { - Host = new UiRoot { Width = 1280f, Height = 720f }; - Dialogs = new RetailDialogFactory( - Host, - RetailDialogFactoryTests.BuildDialogLayout); - CreditsUiResources resources = BuildResources(); - Controller = Assert.IsType( - CreditsUiController.CreateDetached( - Host, - resources, - Dialogs, - () => Now, - ResolveSprite, - () => ReturnCalls++)); - } - - public UiRoot Host { get; } - public RetailDialogFactory Dialogs { get; } - public CreditsUiController Controller { get; } - public double Now { get; set; } = 100d; - public int ReturnCalls { get; private set; } - - public void Dispose() - { - Controller.Dispose(); - Dialogs.Dispose(); - } - - private static (uint tex, int w, int h) ResolveSprite(uint id) - => (id, 400, 300); - - private static CreditsUiResources BuildResources() - { - ImportedLayout picture = LayoutImporter.Build( - new ElementInfo - { - Id = CreditsUiController.PictureRootElementId, - Type = 3u, - X = 0f, - Y = 0f, - Width = 400f, - Height = 600f, - }, - ResolveSprite, - null); - var textRoot = new ElementInfo - { - Id = CreditsUiController.TextRootElementId, - Type = 3u, - X = 400f, - Y = 0f, - Width = 400f, - Height = 600f, - }; - textRoot.Children.Add(new ElementInfo - { - Id = CreditsUiController.TextAreaElementId, - Type = 12u, - Width = 0f, - Height = 0f, - HJustify = HJustify.Center, - VJustify = VJustify.Center, - }); - ImportedLayout text = LayoutImporter.Build( - textRoot, - ResolveSprite, - null); - return new CreditsUiResources( - 0x21000003u, - picture, - text, - ["One\n", "Two\n"], - [0x06000001u, 0x06000002u], - 20f, - "Please Wait"); - } - } -} diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs index 929f0a9f..467f8155 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs @@ -1111,7 +1111,7 @@ public class DatWidgetFactoryTests // ── Justification build-time application (new for importer Fix A) ──────── /// - /// A Type-12 text element with authored HJustify=Center must produce a + /// A Type-12 text element with HJustify=Center (the default) must produce a /// UiText with Centered=true and RightAligned=false at build time. /// This proves BuildText applies the dat's HJustify at construction without a /// controller binding step. @@ -1123,18 +1123,7 @@ public class DatWidgetFactoryTests var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null)); Assert.True(t.Centered); Assert.False(t.RightAligned); - Assert.Equal(VJustify.Top, t.VerticalJustify); - } - - [Fact] - public void BuildText_UnauthoredJustification_UsesRetailNearEdges() - { - var info = new ElementInfo { Type = 12, Width = 100, Height = 20 }; - var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null)); - - Assert.False(t.Centered); - Assert.False(t.RightAligned); - Assert.Equal(VJustify.Top, t.VerticalJustify); + Assert.Equal(VJustify.Center, t.VerticalJustify); } /// diff --git a/tests/AcDream.App.Tests/UI/Layout/ElementReaderTests.cs b/tests/AcDream.App.Tests/UI/Layout/ElementReaderTests.cs index d0f67fee..c01af433 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ElementReaderTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ElementReaderTests.cs @@ -182,40 +182,40 @@ public class ElementReaderTests // ── HJustify / VJustify — Merge propagation ───────────────────────────── /// - /// Authored justification is presence-based: raw 3 on the derived element - /// overrides raw 1 on its base. + /// When the derived element has a non-Center HJustify, the derived value wins + /// (same "non-default wins" rule as FontDid). /// [Fact] public void Merge_DerivedHJustifyRight_OverridesBaseCenter() { - ElementInfo base_ = WithJustification(0x14u, 1u); - ElementInfo derived = WithJustification(0x14u, 3u); + var base_ = new ElementInfo { HJustify = HJustify.Center }; + var derived = new ElementInfo { HJustify = HJustify.Right }; var merged = ElementReader.Merge(base_, derived); Assert.Equal(HJustify.Right, merged.HJustify); } /// - /// Center is a real authored raw value, not an unset sentinel, and must - /// therefore override a Left base. + /// When the derived element has the default HJustify (Center), the base value + /// is inherited — Center from the derived does NOT override a Left base. /// [Fact] - public void Merge_DerivedAuthoredHJustifyCenter_OverridesBaseLeft() + public void Merge_DerivedHJustifyCenter_InheritsBaseLeft() { - ElementInfo base_ = WithJustification(0x14u, 2u); - ElementInfo derived = WithJustification(0x14u, 1u); + var base_ = new ElementInfo { HJustify = HJustify.Left }; + var derived = new ElementInfo { HJustify = HJustify.Center }; // default — no explicit dat property var merged = ElementReader.Merge(base_, derived); - Assert.Equal(HJustify.Center, merged.HJustify); + Assert.Equal(HJustify.Left, merged.HJustify); } /// - /// VJustify=Top from the base propagates when the derived element authors - /// no vertical justification. + /// VJustify=Top from the base propagates when the derived element has no explicit + /// (Center) vertical justification. /// [Fact] - public void Merge_BaseVJustifyTop_InheritedWhenDerivedIsUnauthored() + public void Merge_BaseVJustifyTop_InheritedWhenDerivedIsCenter() { - ElementInfo base_ = WithJustification(0x15u, 4u); - var derived = new ElementInfo(); + var base_ = new ElementInfo { VJustify = VJustify.Top }; + var derived = new ElementInfo { VJustify = VJustify.Center }; // default var merged = ElementReader.Merge(base_, derived); Assert.Equal(VJustify.Top, merged.VJustify); } @@ -226,8 +226,8 @@ public class ElementReaderTests [Fact] public void Merge_DerivedVJustifyBottom_OverridesBaseCenter() { - ElementInfo base_ = WithJustification(0x15u, 1u); - ElementInfo derived = WithJustification(0x15u, 3u); + var base_ = new ElementInfo { VJustify = VJustify.Center }; + var derived = new ElementInfo { VJustify = VJustify.Bottom }; var merged = ElementReader.Merge(base_, derived); Assert.Equal(VJustify.Bottom, merged.VJustify); } @@ -380,54 +380,6 @@ public class ElementReaderTests return info; } - private static ElementInfo WithJustification(uint propertyId, uint raw) - { - ElementInfo info = WithDirectProperty(propertyId, EnumProp(raw)); - ElementReader.ApplyCanonicalLegacyProjection(info); - return info; - } - - [Fact] - public void Justification_UnauthoredDefaultsMatchRetailConstructors() - { - var info = new ElementInfo(); - - Assert.Equal(HJustify.Left, info.HJustify); - Assert.Equal(VJustify.Top, info.VJustify); - } - - [Theory] - [InlineData(0u, HJustify.Left)] - [InlineData(1u, HJustify.Center)] - [InlineData(2u, HJustify.Left)] - [InlineData(3u, HJustify.Right)] - [InlineData(4u, HJustify.Left)] - [InlineData(5u, HJustify.Right)] - public void HorizontalJustification_AllRetailRawValues( - uint raw, - HJustify expected) - { - ElementInfo info = WithJustification(0x14u, raw); - - Assert.Equal(expected, info.HJustify); - } - - [Theory] - [InlineData(0u, VJustify.Top)] - [InlineData(1u, VJustify.Center)] - [InlineData(2u, VJustify.Top)] - [InlineData(3u, VJustify.Bottom)] - [InlineData(4u, VJustify.Top)] - [InlineData(5u, VJustify.Bottom)] - public void VerticalJustification_AllRetailRawValues( - uint raw, - VJustify expected) - { - ElementInfo info = WithJustification(0x15u, raw); - - Assert.Equal(expected, info.VJustify); - } - [Fact] public void ReadTabTable_DecodesButtonPageDefaultInAuthoredOrder() { diff --git a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterInvisibleSweepTests.cs b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterInvisibleSweepTests.cs deleted file mode 100644 index e9aa726e..00000000 --- a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterInvisibleSweepTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.IO; -using System.Linq; -using AcDream.App.UI; -using AcDream.App.UI.Layout; -using AcDream.Content; -using DatReaderWriter; -using DatReaderWriter.DBObjs; -using DatReaderWriter.Options; - -namespace AcDream.App.Tests.UI.Layout; - -/// -/// #408 client-wide blast-radius gate for DAT property 0x3B (Invisible). -/// Retail applies the property in UIElement::OnSetAttribute for every imported -/// element. Enumerate every installed LayoutDesc, then prove every corresponding -/// widget which survives importer child-consumption starts hidden. -/// -[Trait("Lane", "InstalledDat")] -public sealed class LayoutImporterInvisibleSweepTests -{ - private static string DatDirectory => - System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") - ?? Path.Combine( - System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), - "Documents", - "Asheron's Call"); - - private readonly record struct Finding(uint LayoutId, uint ElementId); - - [InstalledDatFact] - public void EveryAuthoredInvisibleWidget_StartsHiddenAcrossAllLayouts() - { - using var dats = new DatCollection(DatDirectory, DatAccessType.Read); - - var authored = new List(); - var built = new List(); - var incorrectlyVisible = new List(); - - foreach (uint layoutId in dats.GetAllIdsOfType().OrderBy(static id => id)) - { - ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId); - if (tree is null) continue; - - CollectAuthored(layoutId, tree, authored); - - ImportedLayout layout = LayoutImporter.Build( - tree, _ => (0u, 0, 0), datFont: null, sourceLayoutDid: layoutId); - CollectBuilt(layoutId, layout.Root, built, incorrectlyVisible); - } - - foreach (IGrouping group in authored - .GroupBy(static f => f.LayoutId) - .OrderBy(static g => g.Key)) - { - Console.WriteLine( - $"[INVISIBLE] layout=0x{group.Key:X8} count={group.Count()} ids=[" - + string.Join(",", group.Select(static f => $"0x{f.ElementId:X8}")) - + "]"); - } - - Console.WriteLine( - $"[INVISIBLE] authored={authored.Count} layouts=" - + $"{authored.Select(static f => f.LayoutId).Distinct().Count()} " - + $"built={built.Count} incorrectlyVisible={incorrectlyVisible.Count}"); - - // Keep the global threshold resilient to an installed DAT revision while - // pinning landmarks from independent screens in both data and widgets. - Assert.True(authored.Count >= 1_000, - $"Expected the known client-wide 0x3B population, found {authored.Count}."); - Assert.Contains(authored, static f => f.ElementId == 0x10000403u); // chargen GM label - Assert.Contains(authored, static f => f.ElementId == 0x10000494u); // chargen envoy label - Assert.Contains(authored, static f => f.ElementId == 0x100006A4u); // combat root - Assert.Contains(authored, static f => f.ElementId == 0x1000048Cu); // chat new-text indicator - - Assert.Contains(built, static f => f.ElementId == 0x100006A4u); - Assert.Contains(built, static f => f.ElementId == 0x1000048Cu); - Assert.Empty(incorrectlyVisible); - } - - private static void CollectAuthored(uint layoutId, ElementInfo node, List findings) - { - if (node.Invisible) - findings.Add(new Finding(layoutId, node.Id)); - - foreach (ElementInfo child in node.Children) - CollectAuthored(layoutId, child, findings); - } - - private static void CollectBuilt( - uint layoutId, - UiElement node, - List built, - List incorrectlyVisible) - { - if (node.AuthoredInvisible) - { - var finding = new Finding(layoutId, node.DatElementId); - built.Add(finding); - if (node.Visible) - incorrectlyVisible.Add(finding); - } - - foreach (UiElement child in node.Children) - CollectBuilt(layoutId, child, built, incorrectlyVisible); - } -} diff --git a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs index 04e96f19..4b0341d1 100644 --- a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs @@ -363,29 +363,6 @@ public class LayoutImporterTests Assert.Null(found.AuthoredTooltipDelaySeconds); } - [Fact] - public void BuildWidget_AuthoredInvisible_HidesInitiallyButDoesNotLatchVisibility() - { - var root = new ElementInfo { Id = 0x1, Type = 3, Width = 100, Height = 40 }; - var hidden = new ElementInfo - { - Id = 0x2, - Type = 3, - Width = 20, - Height = 20, - Invisible = true, - }; - - ImportedLayout tree = LayoutImporter.BuildFromInfos(root, [hidden], NoTex, null); - UiElement found = tree.FindElement(0x2)!; - - Assert.True(found.AuthoredInvisible); - Assert.False(found.Visible); - - found.Visible = true; - Assert.True(found.Visible); - } - // ── Helpers ─────────────────────────────────────────────────────────────── private static ElementInfo BuildSliceContainer(uint id, uint ReadOrder, uint l, uint t, uint r) diff --git a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs index 00b919cd..f96352b6 100644 --- a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelControllerTests.cs @@ -1,10 +1,8 @@ using System.Linq; -using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Net.Messages; using AcDream.Core.World; -using AcDream.Runtime.Gameplay; namespace AcDream.App.Tests.UI.Layout; @@ -126,18 +124,7 @@ public sealed class MapHousePanelControllerTests /// UiButton hotspots above — a fresh instance per call, matching /// production's real resolver. private static UiElement? FakeHouseRowTemplate(uint layoutId, uint elementId) - => new UiText - { - Width = 280f, - Height = 28f, - DefaultColor = new Vector4(0.1f, 0.1f, 0.1f, 1f), - FontColorPalette = - [ - new Vector4(1f, 1f, 1f, 1f), - new Vector4(0f, 1f, 0f, 1f), - new Vector4(1f, 0f, 0f, 1f), - ], - }; + => new UiText { Width = 280f, Height = 28f }; private static MapHousePanelController.Callbacks MakeCallbacks( List? calls = null, @@ -145,8 +132,7 @@ public sealed class MapHousePanelControllerTests Func? playerCellId = null, Func? housePosition = null, Func>? houseLines = null, - Func? templateInfoResolver = null, - Func>? housePanelLines = null) + Func? templateInfoResolver = null) { calls ??= new List(); return new MapHousePanelController.Callbacks( @@ -161,8 +147,7 @@ public sealed class MapHousePanelControllerTests House: new HousePageController.Bindings( Lines: houseLines ?? (static () => Array.Empty()), OnShown: () => calls.Add("house-shown"), - TemplateResolver: FakeHouseRowTemplate, - PanelLines: housePanelLines)); + TemplateResolver: FakeHouseRowTemplate)); } [Fact] @@ -392,35 +377,4 @@ public sealed class MapHousePanelControllerTests "You may buy another house immediately.", Assert.Single(row.LinesProvider()).Text); } - - [Fact] - public void Tick_UsesRetailHousePanelColorAsAuthoredPaletteIndex() - { - ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); - ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); - HousePanelLine[] lines = - [ - new("paid", HousePanelTextColor.RentPaid), - new("unpaid", HousePanelTextColor.RentNotPaid), - ]; - MapHousePanelController? controller = MapHousePanelController.Bind( - rootInfo, - layout, - MakeCallbacks(housePanelLines: () => lines)); - Assert.NotNull(controller); - - controller!.Tick(0.016); - - var listBox = Assert.IsType( - UiElement.FindDescendant(controller.Root, HousePageController.TextBoxId)); - UiScrollablePanel viewport = Assert.IsType( - listBox.ViewportForTest); - Assert.Equal(2, viewport.Children.Count); - Assert.Equal( - new Vector4(0f, 1f, 0f, 1f), - Assert.Single(Assert.IsType(viewport.Children[0]).LinesProvider()).Color); - Assert.Equal( - new Vector4(1f, 0f, 0f, 1f), - Assert.Single(Assert.IsType(viewport.Children[1]).LinesProvider()).Color); - } } diff --git a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs index 73928281..3a6ca908 100644 --- a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs @@ -313,11 +313,9 @@ public sealed class OptionsPanelLiveMountProbeTests /// LayoutDesc, and Open @0x0046cc30 centers the POPUP over the /// button when bool attr 3 is authored. This probe measures all of that /// authored data for the Config option-menu chain (catalog 0x21000043, - /// base 0x10000353) and the vendor dropdown's sibling chain - /// (0x1000034F). The latter was once treated as a fixed-six-row contrast; - /// #386's named-retail trace corrected that reading: it follows the same - /// docked size-to-content message route and authors scrollbar property - /// 0x79 (hide when disabled). + /// base 0x10000353) with vendor's dropdown (0x1000034F chain — visibly a + /// FIXED 6-row + scrollbar popup in retail, user-gated during the vendor + /// campaign) as the contrast control. [Fact] [Trait("Purpose", "Diagnostic")] public void ProbeMenuPopupSizingAndTextStyle() @@ -373,7 +371,7 @@ public sealed class OptionsPanelLiveMountProbeTests Console.WriteLine("[menuprobe3] row template 0x1000035A FAILED to import"); } - Console.WriteLine("[menuprobe3] === CONTROL: vendor chain (content-sized; scrollbar 0x79 hides when disabled) ==="); + Console.WriteLine("[menuprobe3] === CONTROL: vendor chain (fixed 6-row + scrollbar in retail) ==="); DumpDockAndSize(dats, 0x21000043u, 0x1000034Fu, "Vendor popup root"); DumpDockAndSize(dats, 0x21000043u, 0x10000350u, "Vendor popup ListBox"); ElementInfo? vendorBase = LayoutImporter.ImportInfos(dats, 0x21000043u, 0x1000034Bu); diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs index ea16d262..8903bee2 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs @@ -18,11 +18,6 @@ public sealed class SpellcastingUiControllerTests ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); RetailCombatLayout.FitFavoriteSlots(layout); - // The authored combat root starts Invisible=true. Production's - // RetailWindowFrame/CombatUiController show it when combat mode opens - // the window; this standalone pointer fixture must model that mount - // edge explicitly now that #408 honors the DAT flag client-wide. - layout.Root.Visible = true; var spellbook = new Spellbook(); spellbook.OnSpellLearned(42u, 1f); spellbook.SetFavorite(0, 0, 42u); @@ -204,7 +199,6 @@ public sealed class SpellcastingUiControllerTests ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); RetailCombatLayout.FitFavoriteSlots(layout); - layout.Root.Visible = true; var spellbook = new Spellbook(); spellbook.OnSpellLearned(1u, 1f); spellbook.OnSpellLearned(2u, 1f); @@ -474,7 +468,6 @@ public sealed class SpellcastingUiControllerTests ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); RetailCombatLayout.FitFavoriteSlots(layout); - layout.Root.Visible = true; var spellbook = new Spellbook(); spellbook.OnSpellLearned(1u, 1f); spellbook.OnSpellLearned(2u, 1f); diff --git a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs index 6ca03d75..7a66e051 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs @@ -247,12 +247,13 @@ public class UiDatElementTests } /// - /// #408: retail's unauthored-state fallback commits state 0 and applies - /// its properties, including Invisible, through the same OnSetAttribute - /// path as a named state. + /// The state-0 fallback must NOT honor a base-DirectState Invisible + /// (dat 0x3B) — that is the #408 construction-time class, gated + /// separately; retail's per-state Invisible honor applies to NAMED + /// authored states only. /// [Fact] - public void TrySetRetailState_UnauthoredStateFallback_RestoresBaseInvisible() + public void TrySetRetailState_UnauthoredStateFallback_DoesNotHonorBaseInvisible() { var info = new ElementInfo(); var baseState = new UiStateInfo { Id = UiStateInfo.DirectStateId }; @@ -264,12 +265,12 @@ public class UiDatElementTests info.States[UiStateInfo.DirectStateId] = baseState; info.StateMedia["Normal_rollover"] = (0x06005EB6u, 1); var element = new UiDatElement(info, _ => (0u, 0, 0)); - element.Visible = true; + Assert.True(element.Visible); Assert.True(element.TrySetRetailState(UiButtonStateMachine.Normal)); Assert.Equal("", element.ActiveState); - Assert.False(element.Visible); + Assert.True(element.Visible); } /// diff --git a/tests/AcDream.App.Tests/UI/Layout/VendorUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/VendorUiControllerTests.cs index 22396f14..c9082885 100644 --- a/tests/AcDream.App.Tests/UI/Layout/VendorUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/VendorUiControllerTests.cs @@ -1005,10 +1005,6 @@ public sealed class VendorUiControllerTests Assert.Equal(6, h.TypeMenu.RowsPerColumn); Assert.Equal(18f, h.TypeMenu.RowHeight); Assert.Equal(100f, h.TypeMenu.ColumnWidth); - Assert.True(h.TypeMenu.PopupSizeToContent); - Assert.True(h.TypeMenu.PopupScrollbarHideWhenDisabled); - Assert.Equal(2 * h.TypeMenu.RowHeight + 2 * 5f, h.TypeMenu.PopupOuterHeight); - Assert.Equal(h.TypeMenu.ColumnWidth + 2 * 5f, h.TypeMenu.PopupOuterWidth); // Open via the real widget event path. Assert.True(h.TypeMenu.OnEvent(new UiEvent(0, h.TypeMenu, UiEventType.MouseDown, 0, 10, 5))); @@ -1058,7 +1054,7 @@ public sealed class VendorUiControllerTests // where nothing lives; UiMenu treats it as an ordinary button click // and just re-closes the still-open menu instead of picking a row. const int border = 5; - float outerH = h.TypeMenu.PopupOuterHeight; + float outerH = h.TypeMenu.RowsPerColumn * h.TypeMenu.RowHeight + 2 * border; const int targetRow = 1; float iy = targetRow * h.TypeMenu.RowHeight + h.TypeMenu.RowHeight / 2f; float oldUpwardLy = iy - outerH + border; diff --git a/tests/AcDream.App.Tests/UI/UiButtonTests.cs b/tests/AcDream.App.Tests/UI/UiButtonTests.cs index 5d37806e..966ad73b 100644 --- a/tests/AcDream.App.Tests/UI/UiButtonTests.cs +++ b/tests/AcDream.App.Tests/UI/UiButtonTests.cs @@ -732,65 +732,6 @@ public class UiButtonTests Assert.True(kid.Visible); } - [Fact] - public void TrySetRetailState_AppliesInvisibleToButtonNamedAndDirectStates() - { - var info = ButtonInfo("Normal", "Ghosted"); - info.StateMedia[""] = (99u, 1); - info.States[UiStateInfo.DirectStateId] = StateWithInvisible( - UiStateInfo.DirectStateId, "", invisible: true); - info.States[UiButtonStateMachine.Normal] = StateWithInvisible( - UiButtonStateMachine.Normal, "Normal", invisible: false); - info.States[UiButtonStateMachine.Ghosted] = StateWithInvisible( - UiButtonStateMachine.Ghosted, "Ghosted", invisible: true); - var button = CreateButton(info); - - button.Visible = false; - Assert.True(button.TrySetRetailState(UiButtonStateMachine.Normal)); - Assert.True(button.Visible); - - Assert.True(button.TrySetRetailState(UiButtonStateMachine.Ghosted)); - Assert.False(button.Visible); - - button.Visible = true; - Assert.True(button.TrySetRetailState(UiStateInfo.DirectStateId)); - Assert.False(button.Visible); - } - - [Fact] - public void UiText_ReturningToDirectState_RestoresItsInvisibleProperty() - { - var info = new ElementInfo { Type = 12, Width = 20, Height = 20 }; - info.States[UiStateInfo.DirectStateId] = StateWithInvisible( - UiStateInfo.DirectStateId, "", invisible: true); - info.States[UiButtonStateMachine.Normal] = StateWithInvisible( - UiButtonStateMachine.Normal, "Normal", invisible: false); - var text = new UiText(); - text.ConfigureDatState(info); - - text.Visible = true; - Assert.True(text.TrySetRetailState(UiStateInfo.DirectStateId)); - Assert.False(text.Visible); - Assert.True(text.TrySetRetailState(UiButtonStateMachine.Normal)); - Assert.True(text.Visible); - Assert.True(text.TrySetRetailState(UiStateInfo.DirectStateId)); - Assert.False(text.Visible); - } - - private static UiStateInfo StateWithInvisible( - uint id, - string name, - bool invisible) - { - var state = new UiStateInfo { Id = id, Name = name }; - state.Properties.Values[0x3Bu] = new UiPropertyValue - { - Kind = UiPropertyKind.Bool, - BoolValue = invisible, - }; - return state; - } - private static ElementInfo ButtonInfo(params string[] states) { var info = new ElementInfo { Type = 1, Width = 20, Height = 20 }; diff --git a/tests/AcDream.App.Tests/UI/UiMenuTests.cs b/tests/AcDream.App.Tests/UI/UiMenuTests.cs index 0c239ca6..d5c2cd55 100644 --- a/tests/AcDream.App.Tests/UI/UiMenuTests.cs +++ b/tests/AcDream.App.Tests/UI/UiMenuTests.cs @@ -177,11 +177,9 @@ public class UiMenuTests } // ── G5 (vendor gate finding): Scrollable single-column popup ────────────── - // Retail's vendor category dropdown (LayoutDesc 0x21000043) is a scrollable - // single-column ListBox with a docked scrollbar, not a column-major grid. - // Its four-edge docking subsequently sizes the popup to content and property - // 0x79 hides that scrollbar when disabled; the fixed-viewport tests below - // retain coverage of UiMenu's actual overflow path. Chat's own popup + // Retail's vendor category dropdown (LayoutDesc 0x21000043) is a SCROLLABLE + // single column with a docked scrollbar, not a column-major grid — see + // VendorUiController's "G5 correction" class-doc paragraph. Chat's own popup // (exercised by every test above, Scrollable left at its false default) is // completely unaffected: it's a structurally different code path // (DrawGridPopup / the grid branch of OnEvent's MouseDown handling). @@ -543,45 +541,6 @@ public class UiMenuTests Assert.False(menu.IsOpen); // picking closes, same as every other path } - [Fact] - public void SizeToContent_WithAuthoredHideWhenDisabled_HidesTheScrollbarAndItsInput() - { - UiMenu menu = MakeScrollableMenu(3, sizeToContent: true); - menu.PopupScrollbarHideWhenDisabled = true; - Assert.Equal(menu.ColumnWidth + 2 * 5f, menu.PopupOuterWidth); - Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open - - // Configure the exact content/view extents used by both drawing and - // pointer dispatch. Size-to-content makes them equal, so retail's - // authored scrollbar property 0x79 hides the entire sibling widget. - Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.Scroll, Data0: 0))); - Assert.False(menu.PopupScroll.HasOverflow); - Assert.False(menu.IsPopupScrollbarPresentationVisible); - - // The old 16px sibling column is outside the popup now. A direct event - // at that old coordinate must not page/line scroll or leave an invisible - // control holding the popup open (UiRoot hit testing would not target the - // menu there at all because PopupOuterWidth has already collapsed). - float lx = 5f + menu.ColumnWidth + menu.ScrollbarWidth / 2f; - float ly = menu.Height + 5f + menu.RowHeight / 2f; - Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, (int)lx, (int)ly))); - Assert.Equal(0, menu.PopupScroll.ScrollY); - Assert.False(menu.IsOpen); - } - - [Fact] - public void FixedViewport_WithOverflow_KeepsHideWhenDisabledScrollbarVisible() - { - UiMenu menu = MakeScrollableMenu(18, sizeToContent: false); - menu.PopupScrollbarHideWhenDisabled = true; - Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5))); // open - Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.Scroll, Data0: 0))); - - Assert.True(menu.PopupScroll.HasOverflow); - Assert.True(menu.IsPopupScrollbarPresentationVisible); - Assert.Equal(menu.ColumnWidth + menu.ScrollbarWidth + 2 * 5f, menu.PopupOuterWidth); - } - [Fact] public void EmptyItems_ButtonClickDoesNotOpen() { @@ -593,16 +552,14 @@ public class UiMenuTests } [Fact] - public void MenuRetailBehaviorFlags_DefaultOff_ForMenusThatDoNotAuthorThem() + public void TextStyleDefaults_PreserveChatAndVendorBehavior() { - // These are per-menu authored behaviors. Chat's grid popup keeps the - // generic left-aligned, non-resizing defaults; vendor opts into both - // content sizing and property-0x79 scrollbar hiding in its controller. + // The three 2026-08-13 additions are opt-in: chat's gold left-aligned + // caption and vendor's fixed 6-row window are untouched by default. var menu = new UiMenu(); Assert.False(menu.ButtonTextCentered); Assert.False(menu.ItemTextCentered); Assert.False(menu.PopupSizeToContent); - Assert.False(menu.PopupScrollbarHideWhenDisabled); } /// diff --git a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs index 11d38fba..56569778 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs @@ -2290,7 +2290,6 @@ public sealed class LiveEntityHydrationControllerTests Assert.True(fixture.Runtime.TryGetRecord(splitResultGuid, out LiveEntityRecord record)); Assert.NotNull(record.WorldEntity); Assert.Equal(1, fixture.Resources.RegisterCount); - Assert.Equal(splitResultGuid, drop.Selection.SelectedObjectId); } [Fact] @@ -2373,7 +2372,6 @@ public sealed class LiveEntityHydrationControllerTests Assert.False(secondRecovered); Assert.False(fixture.Runtime.TryGetSnapshot(secondUnknown, out _)); Assert.True(fixture.Runtime.TryGetSnapshot(firstUnknown, out _)); - Assert.Equal(firstUnknown, drop.Selection.SelectedObjectId); Assert.Equal(1, fixture.Resources.RegisterCount); } @@ -2478,7 +2476,7 @@ public sealed class LiveEntityHydrationControllerTests public readonly ItemInteractionController Interaction; public readonly InventoryWorldDropProjectionController Projection; public readonly StackSplitQuantityState SplitQuantity = new(); - public readonly AcDream.Core.Selection.SelectionState Selection = new(); + public uint SelectedObject; public DropHarness(Fixture fixture) { @@ -2500,7 +2498,7 @@ public sealed class LiveEntityHydrationControllerTests sendWield: null, sendDrop: _ => { }, sendSplitToWorld: (_, _) => { }, - selectedObjectId: () => Selection.SelectedObjectId ?? 0u, + selectedObjectId: () => SelectedObject, stackSplitQuantity: SplitQuantity, playerOnGround: () => true); Projection = new InventoryWorldDropProjectionController( @@ -2508,15 +2506,12 @@ public sealed class LiveEntityHydrationControllerTests fixture.Objects, fixture.Runtime, fixture.Controller, - Selection, () => 100.0); } public bool DispatchSplit(uint itemGuid, uint stackSize, uint splitAmount) { - Selection.Select( - itemGuid, - AcDream.Core.Selection.SelectionChangeSource.Inventory); + SelectedObject = itemGuid; SplitQuantity.Reset(stackSize); SplitQuantity.SetValue(splitAmount); var payload = new ItemDragPayload( diff --git a/tests/AcDream.App.Tests/World/LiveEntityTestFixture.cs b/tests/AcDream.App.Tests/World/LiveEntityTestFixture.cs index e0a5742c..28a8a597 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityTestFixture.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityTestFixture.cs @@ -28,13 +28,12 @@ internal static class LiveEntityTestFixture WorldSession.EntitySpawn spawn, Func? factory = null, LiveEntityProjectionKind projectionKind = LiveEntityProjectionKind.World, - Action? initializeProjection = null, - bool isLocalPlayer = false) + Action? initializeProjection = null) { ArgumentNullException.ThrowIfNull(runtime); LiveEntityRegistrationResult registration = - runtime.RegisterLiveEntity(spawn, isLocalPlayer); + runtime.RegisterLiveEntity(spawn); RuntimeEntityRecord canonical = registration.Canonical ?? throw new InvalidOperationException( "The fixture cannot materialize a stale CreateObject."); diff --git a/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs b/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs index b44b5eb4..45f889eb 100644 --- a/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs +++ b/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs @@ -356,48 +356,6 @@ public sealed class RuntimePlacementPresentationSinkTests Assert.Equal(priorVisible, record.IsSpatiallyVisible); } - [Fact] - public void ExecutorCompleted_PoseOnlySupersessionDoesNotSnapSidecarBack() - { - Fixture fixture = Fixture.Create(); - LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell)); - WorldEntity entity = record.WorldEntity!; - var body = new PhysicsBody - { - Position = entity.Position, - Orientation = entity.Rotation, - }; - record.Canonical.SetPhysicsBody(body); - RuntimePlacementProjectionSnapshot stale = Placement( - fixture, - record, - RuntimePlacementProjectionKind.ExecutorCompleted, - body.Position, - body.Orientation); - - // #323: StoreAcceptedDestinationPose has this exact shape — it moves - // the retained body without changing cell or PlacementCommitVersion. - // A delayed initial-create receipt must acknowledge without restoring - // the older pose to the graphical sidecar. - var currentPosition = body.Position + new Vector3(25f, 10f, 3f); - Quaternion currentOrientation = Quaternion.CreateFromAxisAngle( - Vector3.UnitZ, - 0.75f); - body.Position = currentPosition; - body.Orientation = currentOrientation; - entity.SetPosition(currentPosition); - entity.Rotation = currentOrientation; - - Assert.True(fixture.Sink.TryApply(in stale)); - - Assert.Equal(currentPosition, entity.Position); - Assert.Equal(currentOrientation, entity.Rotation); - Assert.Equal(stale.Token.ExactCellId, record.FullCellId); - Assert.Equal( - stale.Token.PlacementCommitVersion, - record.Canonical.PlacementCommitVersion); - } - [Fact] public void Place_RejectsStaleCanonicalVersionsWithoutChangingSidecar() { diff --git a/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs b/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs index d8e51d06..00121fa1 100644 --- a/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs +++ b/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs @@ -11,7 +11,6 @@ using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using DatReaderWriter.Types; using Microsoft.Extensions.Logging.Abstractions; -using RetailCullMode = DatReaderWriter.Enums.CullMode; namespace AcDream.Content.Tests; @@ -181,61 +180,6 @@ public sealed class MeshExtractorSolidFaceExtractionTests Assert.Equal(4 * 4 * 4, Assert.Single(group.Value).TextureData.Length); } - [Fact] - public void PrepareCellStructMeshData_LandblockSide_PreservesRetailTriangleFanWinding() - { - var dats = new FakeMeshExtractorDats(); - dats.Register(SolidSurfaceId, new Surface - { - Type = SurfaceType.Base1Solid, - ColorValue = new ColorARGB { Alpha = 255, Red = 64, Green = 96, Blue = 128 }, - }); - var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null); - - ObjectMeshData mesh = Assert.IsType( - extractor.PrepareCellStructMeshData( - id: 1, - BuildQuadCellStruct(RetailCullMode.Landblock), - surfaceOverrides: [1], - Matrix4x4.Identity, - CancellationToken.None)); - - TextureBatchData batch = Assert.Single(Assert.Single(mesh.TextureBatches).Value); - Assert.Equal(RetailCullMode.Landblock, batch.CullMode); - Assert.Equal([0, 1, 2, 0, 2, 3], batch.Indices); - Assert.Equal(4, mesh.Vertices.Length); - Assert.All(mesh.Vertices, vertex => Assert.Equal(Vector3.UnitZ, vertex.Normal)); - } - - [Fact] - public void PrepareCellStructMeshData_NoneSide_ExpandsReversedFaceExactlyLikeRetailConstructMesh() - { - var dats = new FakeMeshExtractorDats(); - dats.Register(SolidSurfaceId, new Surface - { - Type = SurfaceType.Base1Solid, - ColorValue = new ColorARGB { Alpha = 255, Red = 64, Green = 96, Blue = 128 }, - }); - var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null); - - ObjectMeshData mesh = Assert.IsType( - extractor.PrepareCellStructMeshData( - id: 1, - BuildQuadCellStruct(RetailCullMode.None), - surfaceOverrides: [1], - Matrix4x4.Identity, - CancellationToken.None)); - - TextureBatchData batch = Assert.Single(Assert.Single(mesh.TextureBatches).Value); - Assert.Equal(RetailCullMode.None, batch.CullMode); - Assert.Equal( - [0, 1, 2, 0, 2, 3, 6, 5, 4, 7, 6, 4], - batch.Indices); - Assert.Equal(8, mesh.Vertices.Length); - Assert.All(mesh.Vertices.Take(4), vertex => Assert.Equal(Vector3.UnitZ, vertex.Normal)); - Assert.All(mesh.Vertices.Skip(4), vertex => Assert.Equal(-Vector3.UnitZ, vertex.Normal)); - } - private static void RegisterTexturedQuad( FakeMeshExtractorDats dats, SurfaceType surfaceType, @@ -263,30 +207,6 @@ public sealed class MeshExtractorSolidFaceExtractionTests }); } - private static CellStruct BuildQuadCellStruct(RetailCullMode sidesType) => new() - { - VertexArray = new VertexArray - { - Vertices = new Dictionary - { - [0] = new() { Origin = new Vector3(0, 0, 0), Normal = Vector3.UnitZ }, - [1] = new() { Origin = new Vector3(1, 0, 0), Normal = Vector3.UnitZ }, - [2] = new() { Origin = new Vector3(1, 1, 0), Normal = Vector3.UnitZ }, - [3] = new() { Origin = new Vector3(0, 1, 0), Normal = Vector3.UnitZ }, - }, - }, - Polygons = new Dictionary - { - [0] = new() - { - SidesType = sidesType, - PosSurface = 0, - NegSurface = -1, - VertexIds = [0, 1, 2, 3], - }, - }, - }; - /// One quad (4 verts), single polygon, PosSurface referencing . private static GfxObj BuildQuadGfxObj(uint surfaceId, bool noPos) { diff --git a/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs b/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs index 0171dbff..fdfd6d2a 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs @@ -17,20 +17,6 @@ public sealed class ClientCommandRequestsTests { ClientCommandRequests.BuildClearFriends, ClientCommandRequests.ClearFriendsOpcode }, { ClientCommandRequests.BuildClearConsent, ClientCommandRequests.ClearConsentOpcode }, { ClientCommandRequests.BuildDisplayConsent, ClientCommandRequests.DisplayConsentOpcode }, - { ClientCommandRequests.BuildQueryAllegianceName, 0x0030u }, - { ClientCommandRequests.BuildClearAllegianceName, 0x0031u }, - { ClientCommandRequests.BuildListAllegianceOfficerTitles, 0x003Du }, - { ClientCommandRequests.BuildClearAllegianceOfficerTitles, 0x003Eu }, - { ClientCommandRequests.BuildListAllegianceBans, 0x02A3u }, - { ClientCommandRequests.BuildListAllegianceOfficers, 0x02A6u }, - { ClientCommandRequests.BuildClearAllegianceOfficers, 0x02A7u }, - { ClientCommandRequests.BuildQueryMotd, 0x0255u }, - { ClientCommandRequests.BuildClearMotd, 0x0256u }, - { ClientCommandRequests.BuildRemoveAllPermanentGuests, 0x025Eu }, - { ClientCommandRequests.BuildAddAllStoragePermission, 0x025Cu }, - { ClientCommandRequests.BuildRemoveAllStoragePermission, 0x024Cu }, - { ClientCommandRequests.BuildRequestFullGuestList, 0x024Du }, - { ClientCommandRequests.BuildBootEveryone, 0x025Fu }, }; [Theory] @@ -84,15 +70,6 @@ public sealed class ClientCommandRequestsTests { ClientCommandRequests.BuildSoulEmote, ClientCommandRequests.SoulEmoteOpcode }, { ClientCommandRequests.BuildAddFriend, ClientCommandRequests.AddFriendOpcode }, { ClientCommandRequests.BuildRemoveConsent, ClientCommandRequests.RemoveConsentOpcode }, - { ClientCommandRequests.BuildAddAllegianceBan, 0x02A1u }, - { ClientCommandRequests.BuildRemoveAllegianceBan, 0x02A2u }, - { ClientCommandRequests.BuildRemoveAllegianceOfficer, 0x02A5u }, - { ClientCommandRequests.BuildSetAllegianceName, 0x0033u }, - { ClientCommandRequests.BuildSetAllegianceApprovedVassal, 0x0040u }, - { ClientCommandRequests.BuildSetMotd, 0x0254u }, - { ClientCommandRequests.BuildAddPermanentGuest, 0x0245u }, - { ClientCommandRequests.BuildRemovePermanentGuest, 0x0246u }, - { ClientCommandRequests.BuildBootSpecificHouseGuest, 0x024Au }, }; [Theory] @@ -121,94 +98,6 @@ public sealed class ClientCommandRequestsTests Assert.Equal(1u, Read(body, 12)); } - [Theory] - [InlineData(0x003Fu, 6u)] - [InlineData(0x0042u, 5u)] - public void AllegianceAction_PreservesRetailUInt32Payload( - uint expectedOpcode, - uint action) - { - byte[] body = expectedOpcode == 0x003Fu - ? ClientCommandRequests.BuildAllegianceLockAction(12u, action) - : ClientCommandRequests.BuildAllegianceHouseAction(12u, action); - - Assert.Equal(16, body.Length); - Assert.Equal(expectedOpcode, Read(body, 8)); - Assert.Equal(action, Read(body, 12)); - } - - [Theory] - [InlineData(0x0247u)] - [InlineData(0x0266u)] - [InlineData(0x0267u)] - [InlineData(0x0268u)] - public void HouseBooleanAction_UsesRetailOneOrZeroPayload(uint expectedOpcode) - { - byte[] enabled = expectedOpcode switch - { - 0x0247u => ClientCommandRequests.BuildSetOpenHouseStatus(13u, true), - 0x0266u => ClientCommandRequests.BuildSetHooksVisibility(13u, true), - 0x0267u => ClientCommandRequests.BuildModifyAllegianceGuestPermission(13u, true), - _ => ClientCommandRequests.BuildModifyAllegianceStoragePermission(13u, true), - }; - - Assert.Equal(16, enabled.Length); - Assert.Equal(expectedOpcode, Read(enabled, 8)); - Assert.Equal(1u, Read(enabled, 12)); - } - - [Fact] - public void AllegianceStringBooleanActions_PreserveStringThenUInt32Order() - { - byte[] boot = ClientCommandRequests.BuildBreakAllegianceBoot( - 14u, "Bjørn", accountBoot: true); - byte[] gag = ClientCommandRequests.BuildAllegianceChatGag( - 15u, "Bjørn", enabled: false); - byte[] storage = ClientCommandRequests.BuildChangeStoragePermission( - 16u, "Bjørn", enabled: true); - - AssertStringThenUInt32(boot, 0x0277u, 1u); - AssertStringThenUInt32(gag, 0x0041u, 0u); - AssertStringThenUInt32(storage, 0x0249u, 1u); - } - - [Fact] - public void SetAllegianceOfficer_PreservesNameThenLevelOrder() - { - byte[] body = ClientCommandRequests.BuildSetAllegianceOfficer( - 17u, "Bjørn", 3u); - - AssertStringThenUInt32(body, 0x003Bu, 3u); - } - - [Fact] - public void SetAllegianceOfficerTitle_PreservesLevelThenTitleOrder() - { - byte[] body = ClientCommandRequests.BuildSetAllegianceOfficerTitle( - 18u, 2u, "Marshal"); - - Assert.Equal(28, body.Length); - Assert.Equal(0x003Cu, Read(body, 8)); - Assert.Equal(2u, Read(body, 12)); - Assert.Equal(7, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(16))); - Assert.Equal("Marshal"u8.ToArray(), body[18..25]); - Assert.Equal([0, 0, 0], body[25..28]); - } - - [Fact] - public void AllegianceChatBoot_PreservesTwoAlignedString16LFields() - { - byte[] body = ClientCommandRequests.BuildAllegianceChatBoot( - 19u, "Bob", "No."); - - Assert.Equal(28, body.Length); - Assert.Equal(0x02A0u, Read(body, 8)); - Assert.Equal(3, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(12))); - Assert.Equal("Bob"u8.ToArray(), body[14..17]); - Assert.Equal(3, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20))); - Assert.Equal("No."u8.ToArray(), body[22..25]); - } - [Fact] public void FriendAndGlobalSquelchIntegerActions_MatchRetailPayloads() { @@ -302,17 +191,4 @@ public sealed class ClientCommandRequestsTests private static uint Read(byte[] body, int offset) => BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(offset)); - - private static void AssertStringThenUInt32( - byte[] body, - uint expectedOpcode, - uint expectedValue) - { - Assert.Equal(24, body.Length); - Assert.Equal(expectedOpcode, Read(body, 8)); - Assert.Equal(5, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(12))); - Assert.Equal([0x42, 0x6A, 0xF8, 0x72, 0x6E], body[14..19]); - Assert.Equal(0, body[19]); - Assert.Equal(expectedValue, Read(body, 20)); - } } diff --git a/tests/AcDream.Core.Net.Tests/Transport/AceSessionModel.cs b/tests/AcDream.Core.Net.Tests/Transport/AceSessionModel.cs index eaedf5d1..60f377ab 100644 --- a/tests/AcDream.Core.Net.Tests/Transport/AceSessionModel.cs +++ b/tests/AcDream.Core.Net.Tests/Transport/AceSessionModel.cs @@ -273,9 +273,6 @@ internal sealed class AceSessionModel public IReadOnlyCollection CachedPacketSequences => _cachedPackets.Keys; /// Packets silently dropped by CRC/Search failure (NetworkSession.cs:277-280). public int CrcDropCount { get; private set; } - public uint LastCrcDropSequence { get; private set; } - public PacketHeaderFlags LastCrcDropFlags { get; private set; } - public uint LastCrcDropWatermark { get; private set; } /// Packets dropped by the duplicate-rejection rule (NetworkSession.cs:342-347). public int DuplicateDropCount { get; private set; } /// Packets dropped by the Session.CheckState gate (Session.cs:93-110) — before CRC. @@ -340,9 +337,6 @@ internal sealed class AceSessionModel if (!VerifyCrc(packet)) { CrcDropCount++; - LastCrcDropSequence = packet.Header.Sequence; - LastCrcDropFlags = packet.Header.Flags; - LastCrcDropWatermark = _lastReceivedPacketSequence; return; } diff --git a/tests/AcDream.Core.Net.Tests/Transport/LossyTransportDecoratorTests.cs b/tests/AcDream.Core.Net.Tests/Transport/LossyTransportDecoratorTests.cs index 1088122f..2cba13f4 100644 --- a/tests/AcDream.Core.Net.Tests/Transport/LossyTransportDecoratorTests.cs +++ b/tests/AcDream.Core.Net.Tests/Transport/LossyTransportDecoratorTests.cs @@ -239,12 +239,7 @@ public sealed class LossyTransportDecoratorTests // 256-key crypto window is intact at convergence. // ===================================================================== - // The real WorldSession receive owner is deliberately part of this - // end-to-end test. Under full-suite CPU contention its scheduling affects - // when virtual-time server traffic reaches the frame thread, so run it in - // the repository's serialized timing lane (#439). [Fact] - [Trait("Lane", "Timing")] public void LossySession_FivePercentSeeded_ZeroMessageLoss_Headroom256() { var fake = new FakeAceTransport @@ -343,15 +338,7 @@ public sealed class LossyTransportDecoratorTests + $"dropped-in={lossy.InboundDropped} " + $"resends={session.Transport!.Stats.ResendsSent} " + $"naks-sent={session.Transport.Stats.NaksSent} " - + $"headroom={fake.Model.Crypto.Headroom} " - + $"crc-drops={fake.Model.CrcDropCount} " - + $"last-crc-seq={fake.Model.LastCrcDropSequence} " - + $"last-crc-flags={fake.Model.LastCrcDropFlags} " - + $"last-crc-watermark={fake.Model.LastCrcDropWatermark} " - + $"dup-drops={fake.Model.DuplicateDropCount} " - + $"ooo={fake.Model.OutOfOrderPacketCount} " - + $"last-seq={fake.Model.LastReceivedPacketSequence} " - + $"cache={session.Transport.Outbound.CacheDepth}"; + + $"headroom={fake.Model.Crypto.Headroom}"; // Zero message loss, both directions. Assert.True(s2cReceived == MessagesEachWay, $"S2C loss: {ledger}"); @@ -367,8 +354,8 @@ public sealed class LossyTransportDecoratorTests // ACE's crypto search window never eroded (no re-key, no // unrequested resend) — Headroom back to the full 256. - Assert.True(fake.Model.Crypto.Headroom == 256, ledger); - Assert.True(fake.Model.Crypto.OrphanCount == 0, ledger); + Assert.Equal(256, fake.Model.Crypto.Headroom); + Assert.Equal(0, fake.Model.Crypto.OrphanCount); Assert.False(fake.Model.IsTerminated); Assert.Equal(WorldSession.State.InWorld, session.CurrentState); } diff --git a/tests/AcDream.Core.Net.Tests/Transport/OutboundReliableTransportTests.cs b/tests/AcDream.Core.Net.Tests/Transport/OutboundReliableTransportTests.cs index 19598477..78527549 100644 --- a/tests/AcDream.Core.Net.Tests/Transport/OutboundReliableTransportTests.cs +++ b/tests/AcDream.Core.Net.Tests/Transport/OutboundReliableTransportTests.cs @@ -392,29 +392,6 @@ public sealed class OutboundReliableTransportTests Assert.Equal(5u, wrapQueue.AckWatermark); } - [Fact] - public void NewerAckOvertakingQueuedNak_SuppressesStaleEncryptedResend() - { - (OutboundFlowQueue queue, _, _, TransportStats stats, - List sent) = CreateQueue(); - queue.SendGameMessage(MakeMessage(2), GameMessageGroup.UIQueue); - queue.SendGameMessage(MakeMessage(3), GameMessageGroup.UIQueue); - queue.SendGameMessage(MakeMessage(4), GameMessageGroup.UIQueue); - - // The NAK for seq 2 is queued first. Before the frame sweep, a later - // cumulative ACK proves the server has already advanced through 3. - Nak(queue, 2u); - queue.OnAckSequence(3u); - sent.Clear(); - - queue.TransmitPendingResends(); - - Assert.Empty(sent); - Assert.Equal(0, stats.ResendsSent); - Assert.Equal(2, queue.CacheDepth); // watermark itself + newer seq 4 - Assert.Equal(0, queue.PendingResendCount); - } - // ===================================================================== // Conformance against the N0 ACE-behaviour double // ===================================================================== diff --git a/tests/AcDream.Core.Net.Tests/Transport/RetailPacketLossAveragerTests.cs b/tests/AcDream.Core.Net.Tests/Transport/RetailPacketLossAveragerTests.cs deleted file mode 100644 index 03931ba9..00000000 --- a/tests/AcDream.Core.Net.Tests/Transport/RetailPacketLossAveragerTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using AcDream.Core.Net.Transport; - -namespace AcDream.Core.Net.Tests.Transport; - -public sealed class RetailPacketLossAveragerTests -{ - [Fact] - public void Sweep_BeforeRetailHeartbeat_DoesNotPublishPartialSample() - { - long now = 0; - var clock = new TransportClock(() => now, frequency: 10); - var stats = new TransportStats { PacketsSent = 5, NakIdsSent = 1 }; - var sut = new RetailPacketLossAverager(clock, stats); - - stats.PacketsSent += 10; - stats.NakIdsSent += 2; - now = 19; - sut.Sweep(now, stats); - - Assert.Equal(0d, sut.Percentage); - } - - [Fact] - public void Sweep_UsesRetailNakPlusRetransmitOverReceivedPlusSentFormula() - { - long now = 0; - var clock = new TransportClock(() => now, frequency: 10); - var stats = new TransportStats(); - var sut = new RetailPacketLossAverager(clock, stats); - - stats.PacketsSent = 60; - stats.PacketsReceived = 40; - stats.NakIdsSent = 2; - stats.ResendsSent = 3; - now = 20; - sut.Sweep(now, stats); - - Assert.Equal(5d, sut.Percentage); - } - - [Fact] - public void Sweep_EvictsTheOldestSampleAfterFortyHeartbeats() - { - long now = 0; - var clock = new TransportClock(() => now, frequency: 10); - var stats = new TransportStats(); - var sut = new RetailPacketLossAverager(clock, stats); - - stats.PacketsSent = 10; - stats.NakIdsSent = 10; - now = 20; - sut.Sweep(now, stats); - - for (int i = 0; i < RetailPacketLossAverager.WindowSize; i++) - { - stats.PacketsSent += 10; - now += 20; - sut.Sweep(now, stats); - } - - Assert.Equal(0d, sut.Percentage); - } - - [Fact] - public void Sweep_ZeroTrafficReportsZero() - { - long now = 0; - var clock = new TransportClock(() => now, frequency: 10); - var stats = new TransportStats(); - var sut = new RetailPacketLossAverager(clock, stats); - - now = 20; - sut.Sweep(now, stats); - - Assert.Equal(0d, sut.Percentage); - } -} diff --git a/tests/AcDream.Core.Net.Tests/WorldSessionAdministrationCommandTests.cs b/tests/AcDream.Core.Net.Tests/WorldSessionAdministrationCommandTests.cs deleted file mode 100644 index 11e91ca9..00000000 --- a/tests/AcDream.Core.Net.Tests/WorldSessionAdministrationCommandTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Net; -using AcDream.Core.Net.Messages; - -namespace AcDream.Core.Net.Tests; - -public sealed class WorldSessionAdministrationCommandTests -{ - [Fact] - public void EveryAdministrationWrapper_UsesSharedSequenceAndExactBuilder() - { - using var session = new WorldSession( - new IPEndPoint(IPAddress.Loopback, 65000)); - var actual = new List(); - session.GameActionCapture = actual.Add; - - session.SendBreakAllegianceBoot("Bob", true); - session.SendAllegianceChatBoot("Bob", "Reason"); - session.SendAllegianceChatGag("Bob", false); - session.SendAddAllegianceBan("Bob"); - session.SendRemoveAllegianceBan("Bob"); - session.SendListAllegianceBans(); - session.SendSetAllegianceOfficer("Bob", 2u); - session.SendRemoveAllegianceOfficer("Bob"); - session.SendListAllegianceOfficers(); - session.SendClearAllegianceOfficers(); - session.SendSetAllegianceOfficerTitle(2u, "Regent"); - session.SendListAllegianceOfficerTitles(); - session.SendClearAllegianceOfficerTitles(); - session.SendQueryAllegianceName(); - session.SendSetAllegianceName("Guild"); - session.SendClearAllegianceName(); - session.SendAllegianceLockAction(3u); - session.SendSetAllegianceApprovedVassal("Bob"); - session.SendAllegianceHouseAction(4u); - session.SendQueryMotd(); - session.SendSetMotd("Welcome"); - session.SendClearMotd(); - session.SendSetOpenHouseStatus(true); - session.SendAddPermanentGuest("Bob"); - session.SendRemovePermanentGuest("Bob"); - session.SendRemoveAllPermanentGuests(); - session.SendChangeStoragePermission("Bob", true); - session.SendAddAllStoragePermission(); - session.SendRemoveAllStoragePermission(); - session.SendRequestFullGuestList(); - session.SendBootSpecificHouseGuest("Bob"); - session.SendBootEveryone(); - session.SendSetHooksVisibility(false); - session.SendModifyAllegianceGuestPermission(true); - session.SendModifyAllegianceStoragePermission(false); - - byte[][] expected = - [ - ClientCommandRequests.BuildBreakAllegianceBoot(1u, "Bob", true), - ClientCommandRequests.BuildAllegianceChatBoot(2u, "Bob", "Reason"), - ClientCommandRequests.BuildAllegianceChatGag(3u, "Bob", false), - ClientCommandRequests.BuildAddAllegianceBan(4u, "Bob"), - ClientCommandRequests.BuildRemoveAllegianceBan(5u, "Bob"), - ClientCommandRequests.BuildListAllegianceBans(6u), - ClientCommandRequests.BuildSetAllegianceOfficer(7u, "Bob", 2u), - ClientCommandRequests.BuildRemoveAllegianceOfficer(8u, "Bob"), - ClientCommandRequests.BuildListAllegianceOfficers(9u), - ClientCommandRequests.BuildClearAllegianceOfficers(10u), - ClientCommandRequests.BuildSetAllegianceOfficerTitle(11u, 2u, "Regent"), - ClientCommandRequests.BuildListAllegianceOfficerTitles(12u), - ClientCommandRequests.BuildClearAllegianceOfficerTitles(13u), - ClientCommandRequests.BuildQueryAllegianceName(14u), - ClientCommandRequests.BuildSetAllegianceName(15u, "Guild"), - ClientCommandRequests.BuildClearAllegianceName(16u), - ClientCommandRequests.BuildAllegianceLockAction(17u, 3u), - ClientCommandRequests.BuildSetAllegianceApprovedVassal(18u, "Bob"), - ClientCommandRequests.BuildAllegianceHouseAction(19u, 4u), - ClientCommandRequests.BuildQueryMotd(20u), - ClientCommandRequests.BuildSetMotd(21u, "Welcome"), - ClientCommandRequests.BuildClearMotd(22u), - ClientCommandRequests.BuildSetOpenHouseStatus(23u, true), - ClientCommandRequests.BuildAddPermanentGuest(24u, "Bob"), - ClientCommandRequests.BuildRemovePermanentGuest(25u, "Bob"), - ClientCommandRequests.BuildRemoveAllPermanentGuests(26u), - ClientCommandRequests.BuildChangeStoragePermission(27u, "Bob", true), - ClientCommandRequests.BuildAddAllStoragePermission(28u), - ClientCommandRequests.BuildRemoveAllStoragePermission(29u), - ClientCommandRequests.BuildRequestFullGuestList(30u), - ClientCommandRequests.BuildBootSpecificHouseGuest(31u, "Bob"), - ClientCommandRequests.BuildBootEveryone(32u), - ClientCommandRequests.BuildSetHooksVisibility(33u, false), - ClientCommandRequests.BuildModifyAllegianceGuestPermission(34u, true), - ClientCommandRequests.BuildModifyAllegianceStoragePermission(35u, false), - ]; - - Assert.Equal(expected.Length, actual.Count); - for (int i = 0; i < expected.Length; i++) - Assert.Equal(expected[i], actual[i]); - } -} diff --git a/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs b/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs index 361d9a2a..9a16ca5a 100644 --- a/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs +++ b/tests/AcDream.Core.Tests/Audio/DatSoundCacheTests.cs @@ -142,6 +142,11 @@ public sealed class DatSoundCacheTests [Trait("Status", "KnownFailure")] public void GetWave_ConcurrentSameId_PublishesOneCanonicalWaveAndDecodesOnce() { + // #321: a stale caller can pass the resident-cache check, pause, and + // reach _inflight after the winning caller has decoded, admitted, and + // removed its Lazy. That second Lazy performs a duplicate decode. + // R3 preserves this product defect as an explicit known-failure lane; + // fixing the cache algorithm is outside the test-only cleanup scope. var dats = new FakeDatObjectSource(); dats.AddWave(1, MakePcmWave(1, dataBytes: 1000)); var cache = new DatSoundCache(dats, maxWaveBytes: 10_000); @@ -156,55 +161,6 @@ public sealed class DatSoundCacheTests Assert.Equal(1, dats.GetCallCount(1)); } - [Fact] - public async Task GetWave_StaleMissCannotDecodeAgainAfterWinnerPublishes() - { - using var winnerDecodeEntered = new ManualResetEventSlim(); - using var releaseWinnerDecode = new ManualResetEventSlim(); - using var staleMissParked = new ManualResetEventSlim(); - using var releaseStaleMiss = new ManualResetEventSlim(); - - var dats = new FakeDatObjectSource - { - BeforeWaveGet = _ => - { - winnerDecodeEntered.Set(); - if (!releaseWinnerDecode.Wait(TimeSpan.FromSeconds(5))) - throw new TimeoutException("winner decode was never released"); - }, - }; - dats.AddWave(1, MakePcmWave(1, dataBytes: 1000)); - - int initialMissOrdinal = 0; - var cache = new DatSoundCache( - dats, - maxWaveBytes: 10_000, - afterInitialWaveMiss: _ => - { - if (Interlocked.Increment(ref initialMissOrdinal) != 2) - return; - - staleMissParked.Set(); - if (!releaseStaleMiss.Wait(TimeSpan.FromSeconds(5))) - throw new TimeoutException("stale miss was never released"); - }); - - Task winner = Task.Run(() => cache.GetWave(1)); - Assert.True(winnerDecodeEntered.Wait(TimeSpan.FromSeconds(5))); - - Task stale = Task.Run(() => cache.GetWave(1)); - Assert.True(staleMissParked.Wait(TimeSpan.FromSeconds(5))); - - releaseWinnerDecode.Set(); - WaveData? canonical = await winner; - releaseStaleMiss.Set(); - WaveData? recovered = await stale; - - Assert.NotNull(canonical); - Assert.Same(canonical, recovered); - Assert.Equal(1, dats.GetCallCount(1)); - } - [Fact] public void GetWave_ConcurrentMissingId_NeverThrowsAndAllReturnNull() { @@ -258,8 +214,6 @@ public sealed class DatSoundCacheTests public void AddWave(uint id, Wave wave) => _waves[id] = wave; - public Action? BeforeWaveGet { get; init; } - public int GetCallCount(uint id) { lock (_gate) @@ -271,7 +225,6 @@ public sealed class DatSoundCacheTests { if (typeof(T) == typeof(Wave)) { - BeforeWaveGet?.Invoke(fileId); lock (_gate) { _callCounts.TryGetValue(fileId, out int count); diff --git a/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs b/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs index 11612c6c..57e4970b 100644 --- a/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs +++ b/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs @@ -125,9 +125,7 @@ public sealed class ChatLogTests { var log = new ChatLog(); log.OnPlayerKilled("Caith was killed by a Drudge.", - victimGuid: 0x12345678u, - killerGuid: 0x90ABCDEFu, - localPlayerGuid: 0x11111111u); + victimGuid: 0x12345678u, killerGuid: 0x90ABCDEFu); var e = log.Snapshot()[0]; Assert.Equal(ChatKind.System, e.Kind); Assert.Equal("Caith was killed by a Drudge.", e.Text); @@ -135,25 +133,6 @@ public sealed class ChatLogTests Assert.Equal(0x90ABCDEFu, e.ChannelId); // killer guid stashed here } - [Theory] - [InlineData(0x12345678u)] - [InlineData(0x90ABCDEFu)] - public void OnPlayerKilled_SuppressesVictimAndKiller(uint localPlayerGuid) - { - var log = new ChatLog(); - int appended = 0; - log.EntryAppended += _ => appended++; - - log.OnPlayerKilled( - "Caith was killed by a Drudge.", - victimGuid: 0x12345678u, - killerGuid: 0x90ABCDEFu, - localPlayerGuid); - - Assert.Empty(log.Snapshot()); - Assert.Equal(0, appended); - } - // OnWeenieError-specific tests (plain code, interpolation, silent // client-control statuses) were removed here — REJECT-review rework // (SHOULD-FIX 3, docs/research/2026-08-09-ch2-review-findings.md) @@ -251,11 +230,7 @@ public sealed class ChatLogTests public void OnPlayerKilled_LogTextType_IsDefault() { var log = new ChatLog(); - log.OnPlayerKilled( - "Caith was killed by a Drudge.", - 0x1u, - 0x2u, - localPlayerGuid: 0x3u); + log.OnPlayerKilled("Caith was killed by a Drudge.", 0x1u, 0x2u); Assert.Equal(0x00u, log.Snapshot()[0].LogTextType); } diff --git a/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs b/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs index 496b04b6..e8562f3d 100644 --- a/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs +++ b/tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs @@ -265,28 +265,15 @@ public class PhysicsTimestampGateTests } [Fact] - public void FreshForceWithNewerTeleport_UsesForcePathWithoutConsumingTeleport() + public void FreshForceWithNewerTeleport_UsesNormalPositionPath() { var gate = new PhysicsTimestampGate(); gate.SeedForCreateObject(10, 0, 0, 0, 20, 0, 30, 0, 1); - Assert.Equal(PositionTimestampDisposition.ForcePosition, + Assert.Equal(PositionTimestampDisposition.Apply, gate.TryAcceptPositionEvent(1, 11, 21, 31, isLocalPlayer: true)); Assert.Equal((ushort)11, gate.PositionTimestamp); - Assert.Equal((ushort)20, gate.TeleportTimestamp); - Assert.Equal((ushort)31, gate.ForcePositionTimestamp); - } - - [Fact] - public void FreshForceWithWrappedNewerTeleport_UsesForcePathWithoutConsumingTeleport() - { - var gate = new PhysicsTimestampGate(); - gate.SeedForCreateObject(10, 0, 0, 0, ushort.MaxValue, 0, 30, 0, 1); - - Assert.Equal(PositionTimestampDisposition.ForcePosition, - gate.TryAcceptPositionEvent(1, 11, 0, 31, isLocalPlayer: true)); - Assert.Equal((ushort)11, gate.PositionTimestamp); - Assert.Equal(ushort.MaxValue, gate.TeleportTimestamp); + Assert.Equal((ushort)21, gate.TeleportTimestamp); Assert.Equal((ushort)31, gate.ForcePositionTimestamp); } diff --git a/tests/AcDream.Core.Tests/Physics/RetailAnimationCyclePlaybackTests.cs b/tests/AcDream.Core.Tests/Physics/RetailAnimationCyclePlaybackTests.cs index 2275822d..7b1987bc 100644 --- a/tests/AcDream.Core.Tests/Physics/RetailAnimationCyclePlaybackTests.cs +++ b/tests/AcDream.Core.Tests/Physics/RetailAnimationCyclePlaybackTests.cs @@ -76,22 +76,13 @@ public sealed class RetailAnimationCyclePlaybackTests } [Fact] - public void Advance_ZeroFramerateOrNonPositiveElapsed_ReturnsCurrFrameUnchanged() + public void Advance_NonPositiveFramerateOrElapsed_ReturnsCurrFrameUnchanged() { Assert.Equal(3f, RetailAnimationCyclePlayback.Advance(3f, 0, 29, framerate: 0f, elapsedSeconds: 1f)); Assert.Equal(3f, RetailAnimationCyclePlayback.Advance(3f, 0, 29, framerate: 30f, elapsedSeconds: 0f)); Assert.Equal(3f, RetailAnimationCyclePlayback.Advance(3f, 0, 29, framerate: 30f, elapsedSeconds: -1f)); } - [Fact] - public void Advance_NegativeFramerate_MovesBackwardAndClampsAtLowFrame() - { - Assert.Equal(3f, RetailAnimationCyclePlayback.Advance( - 5f, lowFrame: 0, highFrame: 29, framerate: -2f, elapsedSeconds: 1f)); - Assert.Equal(0f, RetailAnimationCyclePlayback.Advance( - 1f, lowFrame: 0, highFrame: 29, framerate: -2f, elapsedSeconds: 1f)); - } - [Fact] public void TryInterpolatePart_ExactFrame_ReturnsThatFramesPose() { diff --git a/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs b/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs index 466958b4..0d6f4a52 100644 --- a/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs +++ b/tests/AcDream.Core.Tests/Vfx/ParticleSystemTests.cs @@ -742,58 +742,6 @@ public sealed class ParticleSystemTests Assert.Equal(new[] { ownerNine }, destination.Select(emitter => emitter.Handle)); } - [Fact] - public void UnattachedCellScope_SplitsEmittersByOwnerCellKind() - { - // One submission per retail stage: outdoor-landcell unattached - // emitters ride the landscape stage and interior-EnvCell ones the - // final world stage, because retail draws each particle during its - // owner CELL's walk turn (ShouldDrawParticles @0x0050FE60). AC cell - // convention: low word < 0x0100 is a landcell, >= 0x0100 an EnvCell; - // cell 0 matches neither scoped mode. - var sys = MakeSystem(); - var desc = new EmitterDesc - { - DatId = 0x32000083u, - Type = ParticleType.Still, - MaxParticles = 1, - }; - int outdoor = sys.SpawnEmitter(desc, Vector3.Zero); - sys.UpdateEmitterOwnerCell(outdoor, 0xA9B40021u); - int interior = sys.SpawnEmitter(desc, Vector3.Zero); - sys.UpdateEmitterOwnerCell(interior, 0xA9B40100u); - int cellLess = sys.SpawnEmitter(desc, Vector3.Zero); - - var destination = new List(); - var none = new HashSet(); - - sys.CopyRenderableEmittersForOwners( - ParticleRenderPass.Scene, - none, - includeUnattached: true, - destination, - unattachedCellScope: UnattachedEmitterCellScope.OutdoorCells); - Assert.Equal(new[] { outdoor }, destination.Select(e => e.Handle)); - - sys.CopyRenderableEmittersForOwners( - ParticleRenderPass.Scene, - none, - includeUnattached: true, - destination, - unattachedCellScope: UnattachedEmitterCellScope.InteriorCells); - Assert.Equal(new[] { interior }, destination.Select(e => e.Handle)); - - sys.CopyRenderableEmittersForOwners( - ParticleRenderPass.Scene, - none, - includeUnattached: true, - destination, - unattachedCellScope: UnattachedEmitterCellScope.Any); - Assert.Equal( - new[] { outdoor, interior, cellLess }, - destination.Select(e => e.Handle)); - } - [Fact] public void SpatialReentryWaitsForFreshRetailViewBeforeBecomingRenderable() { diff --git a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs index 9c087ef1..0f71fc7d 100644 --- a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs @@ -2628,90 +2628,6 @@ public sealed class HeadlessSessionHostTests Assert.Equal(0, firstEntry.PendingCount); } - [Fact] - public void PublishedHeadlessPlayer_ChargedCommandJumpBecomesAirborne() - { - var operations = new FixtureSessionOperations(); - using var credential = new HeadlessCredentialSecret( - "fixture", - "password"); - using var host = new HeadlessSessionHost( - Descriptor(), - credential, - new HeadlessDiagnosticWriter(TextWriter.Null), - operations); - GameRuntime runtime = host.Runtime; - Assert.Equal( - RuntimeSessionStartStatus.Connected, - host.Start().Status); - - const uint player = 0x50000014u; - runtime.PlayerIdentity.ServerGuid = player; - runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration( - 0xA9B40000u, - 1UL); - AddFlatLandblock(runtime.EntityObjects.Physics.Engine); - runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration( - 0xA9B40000u, - 1UL, - ready: true); - AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry = - CreateFirstEntryDrive(runtime); - RuntimeEntityRecord record = runtime.EntityObjects - .RegisterEntityWithInitialResidence( - Spawn(player), - isLocalPlayer: true) - .Canonical!; - Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( - record, - record.CreateIntegrationVersion, - record.Snapshot, - replaceGeneration: false)); - - var projection = new HeadlessSessionWorldProjection( - runtime, - new GateControllableCollisionNeighborhood - { - QuiescentOverride = true, - }, - firstEntry); - projection.ProjectSpawn(record, isLocalPlayer: true); - for (int tick = 0; - tick < 200 - && runtime.MovementOwner.Controller is not - { IsRuntimePublished: true }; - tick++) - { - projection.PumpFirstEntry(); - } - - PlayerMovementController controller = Assert.IsType< - PlayerMovementController>(runtime.MovementOwner.Controller); - Assert.True(controller.IsRuntimePublished); - Assert.False(runtime.MovementOwner.Snapshot.IsAirborne); - var frame = new RuntimeLocalPlayerFrameController( - new HeadlessLocalPlayerFrameHost( - runtime, - CreateInertLiveSessionHost()), - new HeadlessMovementInputSource(runtime.MovementOwner)); - - Assert.True(host.Commands.Movement.SetIntent( - runtime.Generation, - new MovementInput(Jump: true)).Accepted); - for (int tick = 0; tick < 12; tick++) - frame.AdvanceBeforeNetwork(0.05f); - - Assert.True(runtime.MovementOwner.JumpCharge.IsCharging); - Assert.True(host.Commands.Movement.SetIntent( - runtime.Generation, - new MovementInput(Jump: false)).Accepted); - frame.AdvanceBeforeNetwork(0.016f); - - RuntimeMovementSnapshot movement = runtime.MovementOwner.Snapshot; - Assert.True(movement.IsAirborne); - Assert.True(movement.Velocity.Z > 0f); - } - /// /// #365 test 4: PumpFirstEntry must not call DriveAll while /// the collision neighborhood reports non-quiescent, and must call it on diff --git a/tests/AcDream.Runtime.Tests/Chat/RetailAdministrationCommandDispatcherTests.cs b/tests/AcDream.Runtime.Tests/Chat/RetailAdministrationCommandDispatcherTests.cs deleted file mode 100644 index c450ccec..00000000 --- a/tests/AcDream.Runtime.Tests/Chat/RetailAdministrationCommandDispatcherTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -using AcDream.Runtime.Chat; - -namespace AcDream.Runtime.Tests.Chat; - -public sealed class RetailAdministrationCommandDispatcherTests -{ - [Fact] - public void CompleteAdministrationIdFamily_IsOwnedByTheSharedDispatcher() - { - var calls = new List(); - RetailAdministrationCommandDispatcher dispatcher = NewDispatcher(calls); - ClientCommandId[] ids = - [ - ClientCommandId.AllegianceInfo, - ClientCommandId.AllegianceBoot, - ClientCommandId.AllegianceBan, - ClientCommandId.AllegianceChat, - ClientCommandId.AllegianceBroadcast, - ClientCommandId.AllegianceOfficer, - ClientCommandId.AllegianceOfficerTitle, - ClientCommandId.AllegianceName, - ClientCommandId.AllegianceLock, - ClientCommandId.AllegianceHouse, - ClientCommandId.AllegianceMotd, - ClientCommandId.AllegianceUnrecognizedSubcommand, - ClientCommandId.HouseOpenStatus, - ClientCommandId.HouseStorage, - ClientCommandId.HouseBoot, - ClientCommandId.HouseBootAll, - ClientCommandId.HouseGuests, - ClientCommandId.HouseHooks, - ClientCommandId.HouseUnrecognizedSubcommand, - ]; - - foreach (ClientCommandId id in ids) - Assert.True(dispatcher.TryExecute(id, string.Empty)); - - Assert.False(dispatcher.TryExecute(ClientCommandId.QueryAge, string.Empty)); - } - - [Fact] - public void RepresentativeMultiFieldGrammar_UsesSemanticHostBindings() - { - var calls = new List(); - RetailAdministrationCommandDispatcher dispatcher = NewDispatcher(calls); - - dispatcher.TryExecute(ClientCommandId.AllegianceOfficer, "add 0x2 Lord Bob"); - dispatcher.TryExecute(ClientCommandId.AllegianceChat, "on"); - dispatcher.TryExecute(ClientCommandId.AllegianceChat, "kick Bob, Be civil"); - dispatcher.TryExecute(ClientCommandId.HouseGuests, "add Lord Bob"); - dispatcher.TryExecute(ClientCommandId.HouseStorage, "remove -all"); - dispatcher.TryExecute(ClientCommandId.AllegianceMotd, "set Welcome home"); - - Assert.Equal( - [ - "officer:2:Lord Bob", - "option:27:True", - "chatboot:Bob:Be civil", - "guest:add:Lord Bob", - "storage:remove_all", - "motd:set:Welcome home", - ], - calls); - } - - [Fact] - public void InvalidForms_UseClientLocalFeedbackNotSystemChat() - { - var calls = new List(); - RetailAdministrationCommandDispatcher dispatcher = NewDispatcher(calls); - - dispatcher.TryExecute(ClientCommandId.AllegianceOfficer, "add nope Bob"); - dispatcher.TryExecute(ClientCommandId.HouseUnrecognizedSubcommand, "nope"); - - Assert.Equal( - [ - "local:Please specify a valid officer level as a number between 1 and 3. Check the game help files for more information on officer levels.", - "local:Please see @help House for more information on how to use this command.", - ], - calls); - } - - private static RetailAdministrationCommandDispatcher NewDispatcher( - List calls) => new( - new RetailAdministrationCommandDispatcher.FeedbackBindings( - ShowSystemMessage: text => calls.Add("system:" + text), - ShowClientLocalMessage: text => calls.Add("local:" + text), - SetSingleCharacterOption: (id, enabled) => - calls.Add($"option:{id}:{enabled}"), - RequestAllegianceInfo: name => calls.Add("info:" + name)), - new RetailAdministrationCommandDispatcher.ActionBindings( - BreakAllegianceBoot: (name, account) => calls.Add($"boot:{name}:{account}"), - AllegianceChatBoot: (name, reason) => calls.Add($"chatboot:{name}:{reason}"), - AllegianceChatGag: (name, enabled) => calls.Add($"gag:{name}:{enabled}"), - AllegianceBroadcast: text => calls.Add("broadcast:" + text), - ListAllegianceBans: () => calls.Add("ban:list"), - AddAllegianceBan: name => calls.Add("ban:add:" + name), - RemoveAllegianceBan: name => calls.Add("ban:remove:" + name), - ListAllegianceOfficers: () => calls.Add("officer:list"), - ClearAllegianceOfficers: () => calls.Add("officer:clear"), - SetAllegianceOfficer: (name, level) => calls.Add($"officer:{level}:{name}"), - RemoveAllegianceOfficer: name => calls.Add("officer:remove:" + name), - ListAllegianceOfficerTitles: () => calls.Add("title:list"), - ClearAllegianceOfficerTitles: () => calls.Add("title:clear"), - SetAllegianceOfficerTitle: (level, title) => calls.Add($"title:{level}:{title}"), - QueryAllegianceName: () => calls.Add("name:query"), - SetAllegianceName: name => calls.Add("name:set:" + name), - ClearAllegianceName: () => calls.Add("name:clear"), - AllegianceLockAction: action => calls.Add("lock:" + action), - SetAllegianceApprovedVassal: name => calls.Add("bypass:" + name), - AllegianceHouseAction: action => calls.Add("alleghouse:" + action), - QueryMotd: () => calls.Add("motd:query"), - SetMotd: text => calls.Add("motd:set:" + text), - ClearMotd: () => calls.Add("motd:clear"), - SetOpenHouseStatus: open => calls.Add("open:" + open), - AddPermanentGuest: name => calls.Add("guest:add:" + name), - RemovePermanentGuest: name => calls.Add("guest:remove:" + name), - RemoveAllPermanentGuests: () => calls.Add("guest:remove_all"), - ChangeStoragePermission: (name, enabled) => calls.Add($"storage:{enabled}:{name}"), - AddAllStoragePermission: () => calls.Add("storage:add_all"), - RemoveAllStoragePermission: () => calls.Add("storage:remove_all"), - RequestFullGuestList: () => calls.Add("guest:list"), - BootSpecificHouseGuest: name => calls.Add("houseboot:" + name), - BootEveryone: () => calls.Add("houseboot:all"), - SetHooksVisibility: visible => calls.Add("hooks:" + visible), - ModifyAllegianceGuestPermission: enabled => calls.Add("guest:all:" + enabled), - ModifyAllegianceStoragePermission: enabled => calls.Add("storage:all:" + enabled))); -} diff --git a/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs b/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs index 59a66ecf..40e55de7 100644 --- a/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs +++ b/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs @@ -298,11 +298,14 @@ public sealed class InboundPhysicsStateControllerTests } /// - /// #322: the merge and route classifier now consume one shared - /// pre-placement truth table. This remains an end-to-end packet matrix: - /// it proves the merge applies the shared result to placement and all - /// three parent fields, while the independently classified route exposes - /// that same result. Dual parent classes follow the #319 discipline. + /// C5b (#275) proof obligation 1: the merge's two pre-placement flags ARE + /// 's + /// ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting + /// rows. The production classifier is the oracle — this test does not + /// re-encode the truth table, it drives both computations over the packet + /// matrix and asserts the merged snapshot equals what the route's own + /// flags would have produced. Dual parent classes (player 0x5… and + /// creature 0x8…) per the #319 discipline. /// [Theory] // guid, isLocalPlayer, animated, parented, force @@ -318,7 +321,7 @@ public sealed class InboundPhysicsStateControllerTests [InlineData(0x80000032u, false, true, false, false)] [InlineData(0x80000033u, false, false, true, false)] [InlineData(0x80000034u, false, false, false, false)] - public void MergedPrePlacementFieldsUseSharedRetailFlags( + public void MergedPrePlacementFieldsMatchTheClassifiedRouteFlags( uint guid, bool isLocalPlayer, bool animated, @@ -440,28 +443,6 @@ public sealed class InboundPhysicsStateControllerTests merged.Physics!.Value.Parent); } - [Theory] - [InlineData(PositionTimestampDisposition.Rejected, false, false, false)] - [InlineData(PositionTimestampDisposition.Rejected, true, false, false)] - [InlineData(PositionTimestampDisposition.ForcePosition, false, false, false)] - [InlineData(PositionTimestampDisposition.ForcePosition, true, false, false)] - [InlineData(PositionTimestampDisposition.Apply, false, true, true)] - [InlineData(PositionTimestampDisposition.Apply, true, true, false)] - public void SharedPrePlacementFlagsMatchRetailTruthTable( - PositionTimestampDisposition disposition, - bool hasAnimations, - bool expectedUnparent, - bool expectedPlacementFrame) - { - RuntimeAcceptedPositionPrePlacementFlags flags = - RuntimeAuthoritativePositionRouteClassifier.DerivePrePlacementFlags( - disposition, - hasAnimations); - - Assert.Equal(expectedUnparent, flags.UnparentBeforeRouting); - Assert.Equal(expectedPlacementFrame, flags.ApplyPlacementFrameBeforeRouting); - } - [Fact] public void RemotePositionWithoutVelocityAppliesUnpackedZeroVector() { @@ -880,8 +861,8 @@ public sealed class InboundPhysicsStateControllerTests /// #307, the shipped consequence: a local player who has portalled or /// recalled this session holds a nonzero TELEPORT_TS. Retail's /// FORCE_POSITION branch (SmartBox::HandleReceivedPosition - /// 0x00453FD0) fires when the packet's teleport stamp is NOT OLDER than - /// the live one and never advances it, so the authority C4 route 2 builds from + /// 0x00453FD0) fires only when the packet's teleport stamp EQUALS the live + /// one and never advances it, so the authority C4 route 2 builds from /// these timestamps must satisfy /// PreviousTeleportSequence == AcceptedTeleportSequence. With /// PreviousTeleport pinned to 0 the classifier rejected the authority and @@ -945,37 +926,6 @@ public sealed class InboundPhysicsStateControllerTests Assert.Equal((ushort)10, timestamps.Teleport); } - [Fact] - public void LocalPlayerForcePositionWithNewerTeleportLeavesAStaleEqualAcceptedPair() - { - var controller = new InboundPhysicsStateController(); - WorldSession.EntitySpawn spawn = WithTimestamps( - Spawn(0x50000012u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u), - teleport: 10, - forcePosition: 0); - controller.AcceptCreate(spawn); - - Assert.True(controller.TryApplyPosition( - PositionUpdate( - spawn.Guid, - instance: 3, - position: 11, - teleport: 11, - forcePosition: 1), - isLocalPlayer: true, - forcePositionRotation: Quaternion.Identity, - currentLocalVelocity: new Vector3(1f, 2f, 3f), - out PositionTimestampDisposition disposition, - out WorldSession.EntitySpawn accepted, - out AcceptedPhysicsTimestamps timestamps)); - - Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition); - Assert.Equal((ushort)10, timestamps.PreviousTeleport); - Assert.Equal((ushort)10, timestamps.Teleport); - Assert.False(timestamps.TeleportAdvanced); - Assert.Equal(new Vector3(1f, 2f, 3f), accepted.Physics!.Value.Velocity); - } - private static WorldSession.EntityPositionUpdate PositionUpdate( uint guid, ushort instance, diff --git a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeHouseStateTests.cs b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeHouseStateTests.cs index cbd01b47..e2868eb6 100644 --- a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeHouseStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeHouseStateTests.cs @@ -1,16 +1,18 @@ using AcDream.Core.Items; using AcDream.Core.Net.Messages; -using AcDream.Core.Physics; using AcDream.Core.Properties; using AcDream.Runtime.Gameplay; namespace AcDream.Runtime.Tests.Gameplay; /// -/// House-tab conformance for retail's complete -/// gmHouseUI::DisplayHouseData @0x004a3380 builder chain: houseless -/// and owned rows, payment grammar, time/location math, warning colors, -/// incremental rent notices, and session reset. +/// House-tab conformance (Batch C, 2026-08-17; two-line correction at the +/// same-day morning gate round): the houseless case renders retail's exact +/// TWO lines in builder order — gmHouseUI::DisplayBuyPayment +/// @0x004a2b30's houseless branch ("You do not currently own a +/// house.", byte-decoded data_7ab688 — the m_pHouseData gate only +/// selects WHICH text; the emit is unconditional) followed by +/// DisplayPurchaseTimeText @0x004a3110's wait-period line. /// public sealed class RuntimeHouseStateTests { @@ -71,146 +73,17 @@ public sealed class RuntimeHouseStateTests } [Fact] - public void HouseData_OwnedUnpaidCottage_ComposesEveryRetailBuilderInOrder() + public void HouseData_OwnedHouseWithExpiredWaitPeriod_ShowsAbandonFirstLine() { - var clock = new ManualTimeProvider(); var objects = new ClientObjectTable(); objects.AddOrUpdate(new ClientObject { ObjectId = Self, Type = ItemType.Creature }); - var house = new RuntimeHouseState(objects, clock); - const uint buyTime = 1_700_000_000u; - const uint rentTime = 1_700_086_400u; - uint location = LandDefs.LcoordToGid(1200, 800); - var data = new GameEvents.HouseData( - BuyTime: buyTime, - RentTime: rentTime, - Type: 1u, - MaintenanceFree: false, - Buy: - [ - new GameEvents.HousePayment(1, 0, 273u, "Pyreal", "Pyreals"), - new GameEvents.HousePayment(2, 0, 274u, "Trade Note", string.Empty), - ], - Rent: - [ - new GameEvents.HousePayment(10, 4, 273u, "Pyreal", "Pyreals"), - new GameEvents.HousePayment(2, 2, 274u, "Box", string.Empty), - ], - Position: Position(location)); + var house = new RuntimeHouseState(objects); - house.ApplyHouseData(data, Self); + house.ApplyHouseData(SampleHouseData(), Self); Assert.Equal( - [ - "The purchase price for this dwelling is:\n1 Pyreal, 2 Trade Notes", - "Rent:\n4/10 Pyreals, 2/2 Boxes", - "Bought: " + RetailTime(buyTime), - "This maintenance period ends: " + RetailTime(rentTime + 2_592_000L), - "Maintenance is next due: " + RetailTime(rentTime + 2_592_000L), - "Location: 21.9S, 18.1E", - "Warning! You have not paid your maintenance costs for the last " - + "30 day maintenance period. Please pay these costs by this deadline" - + " or you will lose your house, and all your items within it.", - "You may buy another house immediately after you abandon this one.", - ], + ["You may buy another house immediately after you abandon this one."], house.Lines); - - Assert.All(house.PanelLines.Take(6), - line => Assert.Equal(HousePanelTextColor.Normal, line.Color)); - Assert.Equal(HousePanelTextColor.RentNotPaid, house.PanelLines[6].Color); - Assert.Equal(HousePanelTextColor.Normal, house.PanelLines[7].Color); - Assert.Equal(data.Position, house.Position); - } - - [Fact] - public void HouseData_PaidRent_UsesSecondPeriodDueDateAndPaidColor() - { - const uint rentTime = 1_700_086_400u; - var house = new RuntimeHouseState(timeProvider: new ManualTimeProvider()); - GameEvents.HouseData data = SampleHouseData() with - { - RentTime = rentTime, - Type = 1u, - Rent = [new GameEvents.HousePayment(5, 5, 1u, "Token", "Tokens")], - }; - - house.ApplyHouseData(data, Self); - - Assert.Equal( - "Maintenance is next due: " + RetailTime(rentTime + 5_184_000L), - house.Lines[4]); - Assert.Equal( - "The maintenance has already been paid for this period. " - + "You may not prepay next period's maintenance.", - house.Lines[^2]); - Assert.Equal(HousePanelTextColor.RentPaid, house.PanelLines[^2].Color); - } - - [Fact] - public void HouseData_Apartment_UsesNinetyDayPeriodAndOmitsLocation() - { - const uint rentTime = 1_700_086_400u; - var house = new RuntimeHouseState(timeProvider: new ManualTimeProvider()); - GameEvents.HouseData data = SampleHouseData() with - { - RentTime = rentTime, - Type = 4u, - MaintenanceFree = true, - Position = Position(LandDefs.LcoordToGid(1200, 800)), - }; - - house.ApplyHouseData(data, Self); - - Assert.Equal( - "This maintenance period ends: " + RetailTime(rentTime + 7_776_000L), - house.Lines[3]); - Assert.Equal( - "Maintenance is next due: " + RetailTime(rentTime + 15_552_000L), - house.Lines[4]); - Assert.DoesNotContain(house.Lines, line => line.StartsWith("Location: ")); - Assert.Null(house.Position); - Assert.Equal(HousePanelTextColor.RentPaid, house.PanelLines[^2].Color); - } - - [Fact] - public void RentNotices_UpdateRetainedSnapshotAndRefreshPanel() - { - var house = new RuntimeHouseState(timeProvider: new ManualTimeProvider()); - GameEvents.HouseData data = SampleHouseData() with - { - RentTime = 1_700_000_000u, - Rent = [new GameEvents.HousePayment(10, 10, 1u, "Pyreal", "Pyreals")], - }; - house.ApplyHouseData(data, Self); - Assert.Equal(HousePanelTextColor.RentPaid, house.PanelLines[^2].Color); - - house.ApplyRentTime(1_710_000_000u, Self); - - Assert.Equal("Rent:\n0/10 Pyreals", house.Lines[1]); - Assert.Equal(HousePanelTextColor.RentNotPaid, house.PanelLines[^2].Color); - Assert.Equal( - "This maintenance period ends: " + RetailTime(1_712_592_000L), - house.Lines[3]); - - house.ApplyRentPayment( - [new GameEvents.HousePayment(10, 10, 1u, "Pyreal", "Pyreals")], Self); - - Assert.Equal("Rent:\n10/10 Pyreals", house.Lines[1]); - Assert.Equal(HousePanelTextColor.RentPaid, house.PanelLines[^2].Color); - } - - [Fact] - public void HouseData_DefensivelyCopiesPaymentLists() - { - var buy = new List - { - new(1, 0, 1u, "Token", "Tokens"), - }; - var house = new RuntimeHouseState(); - house.ApplyHouseData(SampleHouseData() with { Buy = buy }, Self); - - buy[0] = new GameEvents.HousePayment(99, 0, 1u, "Changed", "Changed"); - - Assert.Equal("The purchase price for this dwelling is:\n1 Token", house.Lines[0]); } [Fact] @@ -308,8 +181,6 @@ public sealed class RuntimeHouseStateTests house.ResetSession(); Assert.Empty(house.Lines); - Assert.Empty(house.PanelLines); - Assert.Null(house.Position); Assert.False(house.HasReceivedNotice); } @@ -339,13 +210,6 @@ public sealed class RuntimeHouseStateTests Rent: Array.Empty(), Position: new CreateObject.ServerPosition(0u, 0f, 0f, 0f, 1f, 0f, 0f, 0f)); - private static CreateObject.ServerPosition Position(uint cellId) => - new(cellId, 0f, 0f, 0f, 1f, 0f, 0f, 0f); - - private static string RetailTime(long epochSeconds) => - DateTimeOffset.FromUnixTimeSeconds(epochSeconds).UtcDateTime - .ToString(System.Globalization.CultureInfo.CurrentCulture); - private sealed class ManualTimeProvider : TimeProvider { private DateTimeOffset _now = new(2026, 8, 17, 0, 0, 0, TimeSpan.Zero); diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs index 7ed71099..ef9867ee 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs @@ -275,24 +275,18 @@ public sealed class RuntimeAuthoritativePositionRouteClassifierTests } [Fact] - public void ForcePosition_AcceptsLocalNonRegressedTeleportAndRejectsRemoteOrRegressed() + public void ForcePosition_IsRejectedForRemoteOrUnequalTeleport() { RuntimeAuthoritativePositionAuthority force = Authority( 10, 10, PositionTimestampDisposition.ForcePosition); RuntimeAuthoritativePositionRoute remote = ClassifyRemote(authority: force); - RuntimeAuthoritativePositionRoute newer = ClassifyLocal( + RuntimeAuthoritativePositionRoute unequal = ClassifyLocal( Authority(10, 11, PositionTimestampDisposition.ForcePosition)); - RuntimeAuthoritativePositionRoute regressed = ClassifyLocal( - Authority(11, 10, PositionTimestampDisposition.ForcePosition)); Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority, remote.Disposition); - Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, - newer.Disposition); - Assert.False(newer.ZeroVelocity); - Assert.Equal(RuntimeTeleportHookPhase.None, newer.TeleportHookPhase); Assert.Equal(RuntimeAuthoritativePositionDisposition.RejectedAuthority, - regressed.Disposition); + unequal.Disposition); } [Fact] diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs index c6625730..57c5271c 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs @@ -330,18 +330,23 @@ public sealed partial class RuntimeCollisionPrefixQuiescenceTests /// root holds an operation. (Additionally, a parked record has /// FullCellId == 0, so it is not an affected resident at all.) /// - /// #310: collision retirement now supersedes an authored mover that - /// is still waiting for its first preparation. The exact unprepared - /// operation is cancelled before ordinary placement-debt evaluation, so - /// the resident enters the normal retirement park and the prefix can - /// converge without an inbound packet or asset-readiness edge. + /// The real hazard is not a throw, it is an unbounded streaming + /// stall — and this test PINS it as a stall, not as a pass. A retained + /// preparation retry keeps its landblock prefix in placement debt, so the + /// retirement is refused on EVERY poll and the landblock never retires. + /// There is no bound: the retirement coordinator simply retries, and + /// TickLostCellDeadlines — the only expiry that could break the + /// cycle — has NO production caller, so its deadline never fires. The only + /// thing that clears it is an inbound packet for that same entity, which + /// is exactly what a RetrySetupUnavailable on an asset that never + /// loads does not produce. /// /// This is a pre-existing hazard independent of route 4b-1, filed as /// its own issue. 4b-1 does NOT bound it; it only avoids widening it, by /// declining to retain operations for destinations it cannot service. /// [Fact] - public void RetainedPreparationRetryIsCancelledByPrefixRetirement() + public void RetainedPreparationRetryStallsPrefixRetirementIndefinitely() { using var fixture = new Fixture(); RuntimeEntityRecord record = fixture.Add( @@ -353,7 +358,7 @@ public sealed partial class RuntimeCollisionPrefixQuiescenceTests // A retained preparation retry: begun, never prepared — the shape a // RetrySetupUnavailable on an asset that never resolves leaves behind. RuntimeEntityPlacementToken retained = fixture.Lifetime.Physics - .SetPosition.BeginAuthoredPlacement( + .SetPosition.BeginAcceptedPlacement( record, record.PositionAuthorityVersion, RuntimeSetPositionOperationKind.RemoteAuthoritative); @@ -361,14 +366,29 @@ public sealed partial class RuntimeCollisionPrefixQuiescenceTests RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); - // The first production poll cancels the unprepared operation and - // enters the ordinary two-phase resident-withdrawal handshake. - Assert.False(fixture.TryAcquire(token, out _)); - Assert.False(fixture.Lifetime.Physics.SetPosition - .IsPlacementCurrent(retained)); - Assert.Equal(1, fixture.Lifetime.Physics.CaptureOwnership() - .SetPositionOperationCount); // the replacement retirement park + // The production retirement path, polled hard. Permission is refused + // every single time; nothing in the system advances it. + for (int poll = 0; poll < 1_000; poll++) + { + Assert.False( + fixture.TryAcquire(token, out _), + $"retirement unexpectedly acquired permission on poll {poll}; " + + "if this now succeeds the stall has been bounded and " + + "this test's pinned decision must be revisited"); + } + // ParkCollisionResidents was never entered, so its overlap throw could + // not fire — the contract's item 6, proven structurally. + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record)); + Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out _)); + + // And the discriminator: retiring the retained operation is what + // releases the prefix. Once the debt is gone the ordinary two-phase + // handshake proceeds — ParkCollisionResidents withdraws the residents + // and permission follows the withdrawal acknowledgements — so drain + // those exactly as the production host does. + _ = fixture.Lifetime.Physics.SetPosition.ForgetExactPlacement(retained); bool acquired = false; for (int poll = 0; poll < 32 && !acquired; poll++) { @@ -384,8 +404,8 @@ public sealed partial class RuntimeCollisionPrefixQuiescenceTests } Assert.True( acquired, - "prefix retirement must converge after it supersedes an " - + "unprepared mover operation"); + "clearing the retained preparation retry must let the prefix " + + "retire; if it does not, the stall has a second cause"); } [Fact] diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs index 748446c1..72d19276 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs @@ -116,73 +116,6 @@ public sealed class RuntimeSetPositionStateTests Assert.Equal(1, ownership.PreparedMoverCount); } - [Fact] - public void WarmedPendingProjectionRetryDoesNotAllocateSnapshotArray() - { - PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); - using var lifetime = new RuntimeEntityObjectLifetime(engine); - RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001042u, 1); - _ = AttachBody(lifetime, record, SourceCell); - var observer = new CountingPlacementObserver(); - using IDisposable subscription = lifetime.Events.SubscribePlacement(observer); - RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( - record, - record.PositionAuthorityVersion, - Command(Request(SourceCell, new Vector3(13f, 18f, 7f)))); - - // Prime both retained retry scratch and the event stream's dispatch - // storage before measuring the normal N>0 per-tick path. - lifetime.Physics.SetPosition.RetryPendingProjections(); - const int iterations = 256; - _ = GC.GetAllocatedBytesForCurrentThread(); - long before = GC.GetAllocatedBytesForCurrentThread(); - for (int iteration = 0; iteration < iterations; iteration++) - lifetime.Physics.SetPosition.RetryPendingProjections(); - long allocated = GC.GetAllocatedBytesForCurrentThread() - before; - - // The event stream itself currently costs 72 B/publication. The old - // Values.ToArray snapshot raised this exact fixture to 424 B/retry; - // keep enough runtime variance for the dispatch floor while making a - // fresh projection array (or equivalent regression) fail loudly. - Assert.InRange(allocated / iterations, 0L, 128L); - Assert.Equal(iterations + 2, observer.Count); - Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( - outcome.Projection)); - } - - [Fact] - public void PendingProjectionRetryKeepsIndependentSnapshotWhenReentered() - { - PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); - using var lifetime = new RuntimeEntityObjectLifetime(engine); - RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001044u, 1); - _ = AttachBody(lifetime, record, SourceCell); - bool retrying = false; - bool nested = false; - var observer = new PlacementObserver(_ => - { - if (!retrying || nested) - return; - nested = true; - lifetime.Physics.SetPosition.RetryPendingProjections(); - }); - using IDisposable subscription = lifetime.Events.SubscribePlacement(observer); - RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( - record, - record.PositionAuthorityVersion, - Command(Request(SourceCell, new Vector3(13f, 19f, 7f)))); - - retrying = true; - lifetime.Physics.SetPosition.RetryPendingProjections(); - - Assert.True(nested); - Assert.Equal(3, observer.Deltas.Count); - Assert.All(observer.Deltas, - delta => Assert.Equal(outcome.Projection, delta.Placement.Token)); - Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( - outcome.Projection)); - } - [Theory] [InlineData(PhysicsStateFlags.Hidden)] [InlineData(PhysicsStateFlags.Hidden | PhysicsStateFlags.NoDraw)] @@ -3715,13 +3648,6 @@ public sealed class RuntimeSetPositionStateTests } } - private sealed class CountingPlacementObserver : IRuntimePlacementObserver - { - internal int Count { get; private set; } - - public void OnPlacement(in RuntimePlacementDelta delta) => Count++; - } - /// /// C0-3 test double: a minimal /// serving exactly one Setup id (matching CanonicalSetupTableId's diff --git a/tests/AcDream.Runtime.Tests/Session/LiveSessionEventRouterTests.cs b/tests/AcDream.Runtime.Tests/Session/LiveSessionEventRouterTests.cs index 76f8443c..5b172849 100644 --- a/tests/AcDream.Runtime.Tests/Session/LiveSessionEventRouterTests.cs +++ b/tests/AcDream.Runtime.Tests/Session/LiveSessionEventRouterTests.cs @@ -156,43 +156,6 @@ public sealed class LiveSessionEventRouterTests router.Dispose(); } - [Fact] - public void PlayerKilled_SuppressesLocalParticipantsAndKeepsBystanderLine() - { - const uint self = 0x50000001u; - const uint other = 0x50000002u; - const uint third = 0x50000003u; - using var session = NewSession(); - var chat = new ChatLog(); - var social = new LiveSocialSessionBindings( - chat, - new TurbineChatState(), - new FriendsState(), - new SquelchState(), - PlayerGuid: () => self); - var router = new LiveSessionEventRouter( - session, - NoOpEntitySink(), - NoOpEnvironmentSink(), - NewInventoryBindings(), - NewCharacterBindings(), - social); - router.Attach(); - - Action deliver = - EventDelegate>( - session, - nameof(session.PlayerKilledReceived)); - deliver(new PlayerKilled.Parsed("self died", self, other)); - deliver(new PlayerKilled.Parsed("self killed", other, self)); - deliver(new PlayerKilled.Parsed("bystander", other, third)); - - ChatEntry entry = Assert.Single(chat.Snapshot()); - Assert.Equal("bystander", entry.Text); - Assert.Equal(other, entry.SenderGuid); - Assert.Equal(third, entry.ChannelId); - } - // ── Campaign FA slice FA2 fix-round SHOULD-FIX 4 (blast review) ──────── // The single production registration site (LiveSessionEventRouter.cs) // was untested — both router-test factories default Fellowship/ @@ -915,71 +878,6 @@ public sealed class LiveSessionEventRouterTests PlayPhysicsScriptType: _ => { }, SoundEvent: _ => { }); - [Fact] - public void HouseRentNotices_RouteIntoTheSharedRuntimeOwner() - { - const uint self = 0x50000001u; - using var session = NewSession(); - var objects = new ClientObjectTable(); - objects.AddOrUpdate(new ClientObject { ObjectId = self, Type = ItemType.Creature }); - var house = new RuntimeHouseState(objects); - house.ApplyHouseData( - new GameEvents.HouseData( - BuyTime: 0u, - RentTime: 1_700_000_000u, - Type: 1u, - MaintenanceFree: false, - Buy: [], - Rent: [new GameEvents.HousePayment(10, 10, 1u, "Pyreal", "Pyreals")], - Position: new CreateObject.ServerPosition()), - self); - - var router = new LiveSessionEventRouter( - session, - NoOpEntitySink(), - NoOpEnvironmentSink(), - new LiveInventorySessionBindings( - objects, - PlayerGuid: () => self, - OnShortcuts: null, - OnUseDone: null, - ItemMana: new ItemManaState(), - ExternalContainers: new ExternalContainerState()), - NewCharacterBindings(), - new LiveSocialSessionBindings( - new ChatLog(), - new TurbineChatState(), - new FriendsState(), - new SquelchState(), - House: house)); - router.Attach(); - - session.GameEvents.Dispatch(GameEventEnvelope.TryParse( - WrapGameEvent( - GameEventType.UpdateRentTime, - BitConverter.GetBytes(1_710_000_000u)))!.Value); - Assert.Equal("Rent:\n0/10 Pyreals", house.Lines[1]); - Assert.Equal(HousePanelTextColor.RentNotPaid, house.PanelLines[^2].Color); - - using var payload = new MemoryStream(); - using (var writer = new BinaryWriter( - payload, System.Text.Encoding.UTF8, leaveOpen: true)) - { - writer.Write(1u); - writer.Write(10); - writer.Write(10); - writer.Write(1u); - WriteString16L(writer, "Pyreal"); - WriteString16L(writer, "Pyreals"); - } - session.GameEvents.Dispatch(GameEventEnvelope.TryParse( - WrapGameEvent(GameEventType.UpdateRentPayment, payload.ToArray()))!.Value); - - Assert.Equal("Rent:\n10/10 Pyreals", house.Lines[1]); - Assert.Equal(HousePanelTextColor.RentPaid, house.PanelLines[^2].Color); - router.Dispose(); - } - private static LiveEnvironmentSessionSink NoOpEnvironmentSink() => new( EnvironChanged: _ => { }, ServerTimeUpdated: _ => { }); @@ -1119,24 +1017,6 @@ public sealed class LiveSessionEventRouterTests return body; } - private static byte[] WrapGameEvent(GameEventType type, byte[] payload) - { - byte[] body = new byte[GameEventEnvelope.HeaderSize + payload.Length]; - BinaryPrimitives.WriteUInt32LittleEndian(body, GameEventEnvelope.Opcode); - BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), 0u); - BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), 0u); - BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), (uint)type); - payload.CopyTo(body, GameEventEnvelope.HeaderSize); - return body; - } - - private static void WriteString16L(BinaryWriter writer, string value) - { - byte[] bytes = System.Text.Encoding.UTF8.GetBytes(value); - writer.Write((ushort)bytes.Length); - writer.Write(bytes); - } - private static WorldSession NewSession() => new(new IPEndPoint(IPAddress.Loopback, 9)); diff --git a/tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs b/tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs index be504159..21328b16 100644 --- a/tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs +++ b/tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs @@ -126,18 +126,21 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests } [Fact] - public void Committed_WhenForceAuthorityCarriesANewerTeleportStamp() + public void Rejected_WhenTheClassifierDeclinesTheFrame() { using StartedRuntime started = StartRuntime(); - (RuntimeEntityRecord record, _) = + (RuntimeEntityRecord record, PlayerMovementController controller) = EnterLocalPlayer(started.Runtime); + Vector3 positionBefore = controller.Position; RuntimeAcceptedPositionDriveController drive = CreateAcceptedPositionDrive(started.Runtime, out List gameActions); - // #325: retail Gate A accepts a ForcePosition whose teleport stamp is - // equal OR newer. The classifier independently preserves that wider - // authority set and still routes this as a force correction rather - // than a teleport. + // RuntimeAuthoritativePositionRouteClassifier.ValidAcceptedAuthority's + // ForcePosition case requires PreviousTeleportSequence == + // AcceptedTeleportSequence; a mismatch is a genuine data-validity + // rejection (this call site never re-derives the timestamp gate's + // own freshness rule — it is asserting the classifier's own + // independent structural check). RuntimeAcceptedPositionExecutionStatus status = drive.TryExecuteAcceptedLocalPosition( record, @@ -146,8 +149,9 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests Timestamps(teleport: 6), previousTeleportSequence: 5); - Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Committed, status); - Assert.Single(gameActions); + Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Rejected, status); + Assert.Equal(positionBefore, controller.Position); + Assert.Empty(gameActions); AssertConverged(started.Runtime); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs index f9f50ad6..6c0069db 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs @@ -312,8 +312,8 @@ public class ChatCommandRouterTests // own client-local refusal. [Theory] - [InlineData("/allegiance nope Bob")] - [InlineData("/all nope Bob")] + [InlineData("/allegiance boot Bob")] + [InlineData("/all boot Bob")] public void AllegianceUnrecognizedSubcommand_ShowsRetailRefusal_NeverBroadcastsOrSends(string input) { // #363 / register row AP-183: retail types this refusal 0x1A @@ -331,29 +331,6 @@ public class ChatCommandRouterTests Assert.Empty(log.Snapshot()); } - [Theory] - [InlineData("/allegiance boot Lord Bob", ClientCommandId.AllegianceBoot, "Lord Bob")] - [InlineData("/house storage add Lord Bob", ClientCommandId.HouseStorage, "add Lord Bob")] - [InlineData("/motd set Welcome home", ClientCommandId.AllegianceMotd, "set Welcome home")] - public void AdministrationCommand_PublishesTypedClientCommand( - string input, - ClientCommandId expected, - string expectedArguments) - { - var (vm, _, bus) = Fixture(); - - SubmitOutcome outcome = ChatCommandRouter.Submit( - input, - vm, - bus, - ChatChannelKind.Say); - - Assert.Equal(SubmitOutcome.ClientHandled, outcome); - var command = Assert.IsType(Assert.Single(bus.Published)); - Assert.Equal(expected, command.Command); - Assert.Equal(expectedArguments, command.Arguments); - } - [Fact] public void RegisteredChannelVerb_NeverReachesTheRawFallback() { diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailClientCommandCatalogTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailClientCommandCatalogTests.cs index 08e0ad27..e7e3e8e3 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailClientCommandCatalogTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailClientCommandCatalogTests.cs @@ -71,10 +71,6 @@ public sealed class RetailClientCommandCatalogTests [InlineData("/unfilter -combat", ClientCommandId.Unfilter)] [InlineData("/messagetypes", ClientCommandId.ListMessageTypes)] [InlineData("/fillcomps scarabs 500", ClientCommandId.FillComponents)] - [InlineData("/day", ClientCommandId.TogglePersistentDaylight)] - [InlineData("@day ignored", ClientCommandId.TogglePersistentDaylight)] - [InlineData("/render radius 12", ClientCommandId.RenderOption)] - [InlineData("@render usage", ClientCommandId.RenderOption)] public void CommandFamilies_ResolveToTypedClientCommands( string input, ClientCommandId expected) { @@ -95,46 +91,18 @@ public sealed class RetailClientCommandCatalogTests } [Theory] - [InlineData("/house open", ClientCommandId.HouseOpenStatus, "open")] - [InlineData("/house close ignored", ClientCommandId.HouseOpenStatus, "close")] - [InlineData("/house guest add Bob", ClientCommandId.HouseGuests, "add Bob")] - [InlineData("/house storage add Lord Bob", ClientCommandId.HouseStorage, "add Lord Bob")] - [InlineData("/house remove Bob", ClientCommandId.HouseBoot, "Bob")] - [InlineData("/house boot -all", ClientCommandId.HouseBoot, "-all")] - [InlineData("/house boot_all ignored", ClientCommandId.HouseBootAll, "ignored")] - [InlineData("/house remove_all", ClientCommandId.HouseBootAll, "")] - [InlineData("/house available Cottage", ClientCommandId.HouseAvailableList, "Cottage")] - [InlineData("/house hooks off", ClientCommandId.HouseHooks, "off")] - public void HouseManagementSubcommands_ResolveLocally( - string input, - ClientCommandId expected, - string expectedArguments) - { - Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match)); - Assert.True(match.HasValidArguments); - Assert.Equal(expected, match.Command); - Assert.Equal(expectedArguments, match.Arguments); - } - - [Theory] - [InlineData("/house")] + [InlineData("/house open")] + [InlineData("/house close")] + [InlineData("/house guest add Bob")] + [InlineData("/house storage add Bob")] [InlineData("/house nope")] - public void HouseUnknownOrMissingSubcommand_ShowsRetailRefusalLocally(string input) + [InlineData("/house available")] + public void UnsupportedHouseSubcommand_FallsThroughToServerPassthrough(string input) { - Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match)); - Assert.Equal(ClientCommandId.HouseUnrecognizedSubcommand, match.Command); - Assert.False(match.HasValidArguments); - Assert.Equal( - "Please see @help House for more information on how to use this command.", - match.InvalidArgumentsText); - } - - [Fact] - public void HouseAvailableWithoutType_UsesHslistArgumentValidation() - { - Assert.True(RetailClientCommandCatalog.TryMatch("/house available", out var match)); - Assert.Equal(ClientCommandId.HouseAvailableList, match.Command); - Assert.False(match.HasValidArguments); + // Campaign CH slice CH4 (2026-08-09), Tier 1 fix #4: unrecognized + // house subcommands must reach ACE (TS-68), not be swallowed + // locally with a wrong usage message. + Assert.False(RetailClientCommandCatalog.TryMatch(input, out _)); } [Theory] @@ -150,38 +118,24 @@ public sealed class RetailClientCommandCatalogTests } [Theory] - [InlineData("/allegiance boot Bob", ClientCommandId.AllegianceBoot, "Bob")] - [InlineData("/allegiance ban add Bob", ClientCommandId.AllegianceBan, "add Bob")] - [InlineData("/all chat gag Bob", ClientCommandId.AllegianceChat, "gag Bob")] - [InlineData("/all ch off", ClientCommandId.AllegianceChat, "off")] - [InlineData("/allegiance broadcast hello", ClientCommandId.AllegianceBroadcast, "hello")] - [InlineData("/allegiance br hello", ClientCommandId.AllegianceBroadcast, "hello")] - [InlineData("/all officer add 2 Bob", ClientCommandId.AllegianceOfficer, "add 2 Bob")] - [InlineData("/all title set 2 Regent", ClientCommandId.AllegianceOfficerTitle, "set 2 Regent")] - [InlineData("/allegiance motd", ClientCommandId.AllegianceMotd, "")] - [InlineData("/allegiance name set A Name", ClientCommandId.AllegianceName, "set A Name")] - [InlineData("/allegiance lock bypass Bob", ClientCommandId.AllegianceLock, "bypass Bob")] - [InlineData("/allegiance house guest open", ClientCommandId.AllegianceHouse, "guest open")] - [InlineData("/motd set Welcome", ClientCommandId.AllegianceMotd, "set Welcome")] - public void AllegianceManagementSubcommands_ResolveLocally( - string input, - ClientCommandId expected, - string expectedArguments) - { - Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match)); - Assert.True(match.HasValidArguments); - Assert.Equal(expected, match.Command); - Assert.Equal(expectedArguments, match.Arguments); - } - - [Theory] + [InlineData("/allegiance boot Bob")] + [InlineData("/allegiance ban add Bob")] + [InlineData("/allegiance motd")] [InlineData("/allegiance")] - [InlineData("/allegiance nope")] - [InlineData("/all nonsense")] - public void UnknownAllegianceSubcommand_ShowsRetailRefusal_ClientSide(string input) + [InlineData("/all officer add 2 Bob")] + public void UnsupportedAllegianceSubcommand_ShowsRetailRefusal_ClientSide(string input) { + // CH4 REJECT-review Blocker 1 (2026-08-09): unlike @house (whose + // unrecognized subcommands correctly reach ACE, see the test + // above), retail's own DoAllegiance NEVER falls through to + // DoChannelCommand/the server for an unrecognized subcommand — it + // claims the whole verb unconditionally and prints its own + // client-local refusal (label_57da4b, 0x0057DA4B). The earlier + // "falls through to server passthrough" behavior here was itself + // the bug: an unmatched subcommand used to escape all the way to + // the unregistered-tag channel fallback and broadcast to the + // Allegiance chat channel. Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match)); - Assert.Equal(ClientCommandId.AllegianceUnrecognizedSubcommand, match.Command); Assert.False(match.HasValidArguments); Assert.Equal( "Please see @help Allegiance for more information on how to use this command.", @@ -196,7 +150,7 @@ public sealed class RetailClientCommandCatalogTests [InlineData("/allegiance info", ClientCommandId.AllegianceInfo, "")] [InlineData("/allegiance info Bob", ClientCommandId.AllegianceInfo, "Bob")] [InlineData("/all info Bob", ClientCommandId.AllegianceInfo, "Bob")] - public void AllegianceSimpleSubcommands_Resolve( + public void AllegianceImplementedSubcommands_Resolve( string input, ClientCommandId expected, string expectedArguments) { Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match)); diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs index 85c43d41..bdea46ed 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs @@ -489,11 +489,8 @@ public sealed class RetailCommandHelpTableTests // ported -- see RetailCommandHelpTable.MessageTypesDetail) -- // 43/4/0, zero remaining unverified leaf verbs. // CT-B4 (2026-08-21): "log" joined the catalog, bringing its already- - // extracted retail Detail text with it -- 44/4/0. #361 adds the - // already-extracted @day and @render handlers -- 46/4/0. #360 adds - // standalone @motd, whose retail Detail text was already extracted - // in RetailCommandHelpTable.Motd -- 47/4/0. - Assert.Equal(47, extractedCount); + // extracted retail Detail text with it -- 44/4/0. + Assert.Equal(44, extractedCount); Assert.Equal(4, confirmedNullCount); Assert.Equal(0, unverifiedCount); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandRegistryConformanceTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandRegistryConformanceTests.cs index 494b12fc..df4142b3 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandRegistryConformanceTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandRegistryConformanceTests.cs @@ -20,9 +20,9 @@ namespace AcDream.UI.Abstractions.Tests.Panels.Chat; /// function pointer (verified byte-level in the registry doc); it can /// never execute in retail OR acdream. Typing it bare reaches the server /// as literal text. -/// — the deliberate -/// @loadfile product decision; it falls through to ACE as literal -/// text. +/// — a real retail verb +/// acdream has not yet ported (TS-68/TS-69/TS-70, or the deliberate +/// @loadfile product decision); falls through to ACE as literal text. /// /// The FIRST test enforces that every verb resolves to the RIGHT status. /// The SECOND enforces the ownership rule in reverse: nothing in @@ -118,7 +118,7 @@ public sealed class RetailCommandRegistryConformanceTests // §2.5 Allegiance management (6; "ab" already counted in §2.3) new(Status.Implemented, "allegiance", "all"), new(Status.Implemented, "alh", "ah"), - new(Status.Implemented, "motd"), + new(Status.ServerPassthrough, "motd"), // TS-68 new(Status.Implemented, "speaker"), // §2.5b Housing (7) @@ -141,12 +141,12 @@ public sealed class RetailCommandRegistryConformanceTests // §2.7 Status / display (8) new(Status.Implemented, "age"), new(Status.Implemented, "birth"), - new(Status.Implemented, "day"), + new(Status.ServerPassthrough, "day"), // TS-69 — no sky/time-of-day override hook new(Status.Implemented, "endurance"), new(Status.Implemented, "framerate"), new(Status.Implemented, "loc"), new(Status.Implemented, "version"), - new(Status.Implemented, "render"), + new(Status.ServerPassthrough, "render"), // TS-69 — no SmartBox equivalent // §2.8 Interface layout, emotes, social, components (18) new(Status.Implemented, "saveui"), @@ -220,8 +220,8 @@ public sealed class RetailCommandRegistryConformanceTests // two totals shift by one against the CH4 audit. The 152 verb total // is unchanged, which is what NoDuplicateVerbsAcrossEntries and the // section counts protect. - Assert.Equal(1, Registry.Where(e => e.Status == Status.ServerPassthrough).Sum(e => e.Verbs.Length)); - Assert.Equal(142, Registry.Where(e => e.Status == Status.Implemented).Sum(e => e.Verbs.Length)); + Assert.Equal(4, Registry.Where(e => e.Status == Status.ServerPassthrough).Sum(e => e.Verbs.Length)); + Assert.Equal(139, Registry.Where(e => e.Status == Status.Implemented).Sum(e => e.Verbs.Length)); } [Fact] diff --git a/tools/A8CellAudit/Program.cs b/tools/A8CellAudit/Program.cs index 654ccd56..ba9578e5 100644 --- a/tools/A8CellAudit/Program.cs +++ b/tools/A8CellAudit/Program.cs @@ -3,7 +3,6 @@ using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Options; -using DatEnvironment = DatReaderWriter.DBObjs.Environment; using SysEnv = System.Environment; string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR") @@ -17,11 +16,7 @@ if (!Directory.Exists(datDir)) } using var dats = new DatCollection(datDir, DatAccessType.Read); -if (args.Length > 0 && string.Equals(args[0], "cell-winding-catalog", StringComparison.OrdinalIgnoreCase)) -{ - return AuditCellWindingCatalog(dats); -} -else if (args.Length > 0 && string.Equals(args[0], "buildings", StringComparison.OrdinalIgnoreCase)) +if (args.Length > 0 && string.Equals(args[0], "buildings", StringComparison.OrdinalIgnoreCase)) { uint landblockId = args.Length > 1 ? ParseHex(args[1]) : 0xA9B40000u; int radius = args.Length > 2 ? int.Parse(args[2], System.Globalization.CultureInfo.InvariantCulture) : 0; @@ -57,128 +52,6 @@ else return 0; -static int AuditCellWindingCatalog(DatCollection dats) -{ - long environments = 0; - long cellStructs = 0; - long polygons = 0; - long triangles = 0; - long degenerateTriangles = 0; - long alignedToPlane = 0; - long opposedToPlane = 0; - long ambiguousToNormal = 0; - long inconsistentFans = 0; - var sideCounts = new Dictionary(); - var invalidSides = new List(); - var structuralFailures = new List(); - var windingObservations = new List(); - - foreach (uint environmentId in dats.GetAllIdsOfType().OrderBy(id => id)) - { - DatEnvironment? environment = dats.Get(environmentId); - if (environment is null) - continue; - - environments++; - foreach (var (cellStructId, cellStruct) in environment.Cells.OrderBy(pair => pair.Key)) - { - cellStructs++; - foreach (var (polygonId, polygon) in cellStruct.Polygons.OrderBy(pair => pair.Key)) - { - if (polygon.VertexIds.Count < 3) - continue; - - polygons++; - sideCounts[polygon.SidesType] = sideCounts.GetValueOrDefault(polygon.SidesType) + 1; - if (polygon.SidesType is not (CullMode.Landblock or CullMode.None or CullMode.Clockwise)) - { - if (invalidSides.Count < 20) - invalidSides.Add($"env=0x{environmentId:X8} struct=0x{cellStructId:X4} poly=0x{polygonId:X4} sides={polygon.SidesType}"); - } - - if (!cellStruct.VertexArray.Vertices.TryGetValue( - (ushort)polygon.VertexIds[0], - out var anchorVertex)) - { - if (structuralFailures.Count < 20) - structuralFailures.Add($"env=0x{environmentId:X8} struct=0x{cellStructId:X4} poly=0x{polygonId:X4} missing anchor"); - continue; - } - - Vector3 anchor = anchorVertex.Origin; - Vector3 authoredNormal = anchorVertex.Normal; - if (authoredNormal.LengthSquared() > 0f) - authoredNormal = Vector3.Normalize(authoredNormal); - - int? fanSign = null; - for (int point = 2; point < polygon.VertexIds.Count; point++) - { - triangles++; - if (!TryGetOrigin(cellStruct, (ushort)polygon.VertexIds[point - 1], out Vector3 b) - || !TryGetOrigin(cellStruct, (ushort)polygon.VertexIds[point], out Vector3 c)) - { - if (structuralFailures.Count < 20) - structuralFailures.Add($"env=0x{environmentId:X8} struct=0x{cellStructId:X4} poly=0x{polygonId:X4} missing fan vertex"); - continue; - } - - Vector3 cross = Vector3.Cross(b - anchor, c - anchor); - if (cross.LengthSquared() <= 1e-12f || authoredNormal.LengthSquared() <= 0f) - { - degenerateTriangles++; - continue; - } - - float dot = Vector3.Dot(Vector3.Normalize(cross), authoredNormal); - if (MathF.Abs(dot) < 1e-3f) - { - ambiguousToNormal++; - continue; - } - - int sign = dot >= 0f ? 1 : -1; - if (fanSign is null) - fanSign = sign; - else if (fanSign.Value != sign) - { - inconsistentFans++; - if (windingObservations.Count < 20) - windingObservations.Add($"env=0x{environmentId:X8} struct=0x{cellStructId:X4} poly=0x{polygonId:X4} fan changes orientation dot={dot:R}"); - } - - if (sign > 0) alignedToPlane++; - else opposedToPlane++; - } - } - } - } - - Console.WriteLine( - $"cell-winding-catalog environments={environments:N0} cellStructs={cellStructs:N0} " + - $"polygons={polygons:N0} triangles={triangles:N0} degenerate={degenerateTriangles:N0}"); - Console.WriteLine( - $"orientation alignedToAuthoredNormal={alignedToPlane:N0} " + - $"opposedToAuthoredNormal={opposedToPlane:N0} ambiguous={ambiguousToNormal:N0} " + - $"inconsistentReliableFans={inconsistentFans:N0}"); - Console.WriteLine( - "sides " + string.Join(", ", sideCounts.OrderBy(pair => (int)pair.Key) - .Select(pair => $"{pair.Key}({(int)pair.Key})={pair.Value:N0}"))); - - foreach (string failure in invalidSides) - Console.WriteLine($"INVALID-SIDES {failure}"); - foreach (string failure in structuralFailures) - Console.WriteLine($"STRUCTURAL-FAIL {failure}"); - foreach (string observation in windingObservations) - Console.WriteLine($"AUTHORED-FAN-OBSERVATION {observation}"); - - // Orientation observations describe the authored polygon fan. Retail - // submits the same [0,i-1,i] indices, so reproducing them is the parity - // contract; only missing vertices or an unknown sides_type are invalid. - return invalidSides.Count == 0 && structuralFailures.Count == 0 - ? 0 - : 1; -} - static uint ParseHex(string text) { text = text.Trim(); diff --git a/tools/PesChainAudit/Program.cs b/tools/PesChainAudit/Program.cs index c7cc79d3..89a717e6 100644 --- a/tools/PesChainAudit/Program.cs +++ b/tools/PesChainAudit/Program.cs @@ -74,11 +74,6 @@ foreach (var root in roots) case SoundHook sh: Console.WriteLine($"{head} sound=0x{(uint)sh.Id:X8}"); break; - case SoundTweakedHook tweaked: - Console.WriteLine( - $"{head} sound=0x{(uint)tweaked.SoundId:X8} " + - $"volume={tweaked.Volume:R} priority={tweaked.Priority:R}"); - break; case SoundTableHook stb: Console.WriteLine($"{head} soundType={stb.SoundType}"); break;