diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 00000000..102fe6f4 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,227 @@ +# Gitea Actions CI gate for the self-hosted runners. +# +# Deliberately does NOT use actions/setup-dotnet: data.forgejo.org (the mirror +# Gitea resolves actions from) does not host that action at all, and the +# self-hosted runners carry the pinned SDK band from global.json already. +# actions/checkout IS mirrored, so it is used normally. +# +# The suite runs through tools/run-release-gate.ps1 rather than a bare +# `dotnet test`: that script owns the xUnit trait-lane filter which excludes +# the InstalledDat / Live / Manual / OS-specific lanes. A bare `dotnet test` +# fails ~36 tests by design because those lanes assert their own preconditions. +name: CI +on: + push: + branches: [main] + # Docs-only pushes change nothing a test can fail on, and each gate run is + # ~7 minutes of clean build + 14k tests + a 121 MB release. Skip them; a + # code push (or manual dispatch) still runs everything from scratch — + # deliberately uncached, so the gate keeps proving a from-nothing build. + paths-ignore: + - 'docs/**' + - 'claude-memory/**' + - 'memory/**' + - '**.md' + workflow_dispatch: + +jobs: + windows-gate: + runs-on: windows-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + + - name: Verify the pinned SDK band resolves + shell: pwsh + run: | + dotnet --version + dotnet --list-sdks + + # NOT tools/run-release-gate.ps1 here. That script redirects every child + # process to its own log file, so the step emits nothing for minutes at a + # time; Forgejo treats a task that stops reporting as a zombie and fails + # it while the work is still running (observed: job marked failed with 20 + # dotnet processes still alive and a complete 8.7 MB TRX on disk). Running + # the projects directly keeps output streaming. The script stays the + # canonical LOCAL gate; the trait filter below is copied from its default. + - name: Build + shell: pwsh + run: dotnet build AcDream.slnx -c Release --nologo + + - name: Test (lane-filtered, streaming) + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $filter = 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' + $failed = @() + foreach ($proj in Get-ChildItem tests -Directory | Sort-Object Name) { + $csproj = Join-Path $proj.FullName "$($proj.Name).csproj" + if (-not (Test-Path $csproj)) { continue } + Write-Host "::group::$($proj.Name)" + dotnet test $csproj -c Release --no-build --nologo --filter $filter + if ($LASTEXITCODE -ne 0) { $failed += $proj.Name } + Write-Host "::endgroup::" + } + if ($failed.Count) { throw "Failed test projects: $($failed -join ', ')" } + + linux-portable: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + + - name: Portable closure (Linux lanes run here, not on Windows) + run: | + set -e + dotnet --version + # Core.Net runs SINGLE-THREADED here, on its own, and the split is + # measured rather than defensive: on this 6-core container the + # assembly FAILS in 40 s with default parallelism and PASSES in 10 s + # with one thread. Its sessions do real socket work on background + # threads, so contention both breaks and slows them. Windows has 18 + # cores, passes in ~7 s parallel, and REGRESSED when serialized, so + # this stays scoped to Linux. + echo '::group::AcDream.Core.Net.Tests (single-threaded)' + dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj \ + -c Release --nologo \ + --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' \ + -- xUnit.MaxParallelThreads=1 + echo '::endgroup::' + + for p in \ + tests/AcDream.Platform.Tests \ + tests/AcDream.Core.Tests \ + tests/AcDream.Content.Tests \ + tests/AcDream.Runtime.Tests \ + tests/AcDream.Headless.Tests \ + tests/AcDream.Launcher.Core.Tests \ + tests/AcDream.UI.Abstractions.Tests ; do + echo "::group::$p" + dotnet test "$p" -c Release --nologo \ + --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' + echo "::endgroup::" + done + + release: + # Same workflow rather than a workflow_run trigger: workflow_run is a + # GitHub feature whose Forgejo support is unreliable, while `needs` is + # guaranteed. A red gate therefore cannot publish. + needs: [windows-gate, linux-portable] + runs-on: windows-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + + - name: Compute release version + id: ver + shell: pwsh + run: | + $v = '0.1.0-build.{0}' -f ([DateTime]::UtcNow.ToString('yyyyMMddHHmm')) + "version=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + Write-Host "release version: $v" + + - name: Build payloads with release-attachment URLs + shell: pwsh + env: + TAG: ${{ steps.ver.outputs.version }} + run: | + ./tools/publish-bin.ps1 -Version $env:TAG -BaseUrl "${{ github.server_url }}/${{ github.repository }}/releases/download/$env:TAG" + + - name: Create the release and upload payloads + shell: pwsh + env: + TAG: ${{ steps.ver.outputs.version }} + TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + $ErrorActionPreference = 'Stop' + $api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + $headers = @{ Authorization = "token $env:TOKEN" } + $body = @{ + tag_name = $env:TAG + name = "acdream alpha $env:TAG" + body = "Automated alpha build from ${{ github.sha }}." + draft = $false + prerelease = $true + target_commitish = 'main' + } | ConvertTo-Json + $release = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers -ContentType 'application/json' -Body $body + Write-Host "created release id=$($release.id)" + foreach ($f in Get-ChildItem bin -File) { + Write-Host ("uploading {0} ({1:N1} MB)" -f $f.Name, ($f.Length/1MB)) + Invoke-RestMethod -Method Post -Headers $headers -Uri "$api/releases/$($release.id)/assets?name=$($f.Name)" -Form @{ attachment = Get-Item $f.FullName } | Out-Null + } + + - name: Republish the `latest` pointer release + shell: pwsh + env: + TAG: ${{ steps.ver.outputs.version }} + TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + $ErrorActionPreference = 'Stop' + $api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + $headers = @{ Authorization = "token $env:TOKEN" } + + # Forgejo has no /releases/latest/download/ route, so the launcher + # needs a pointer at a URL that never changes. A one-asset release on + # the fixed `latest` tag is that pointer. Keeping it in a release + # rather than in git means no payload branch, no bot commits on main, + # and no push that would retrigger this workflow. + $existing = Invoke-RestMethod -Method Get -Headers $headers ` + -Uri "$api/releases/tags/latest" -SkipHttpErrorCheck + if ($existing.id) { + Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($existing.id)" | Out-Null + # The tag outlives its release and would block recreation. + Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/tags/latest" -SkipHttpErrorCheck | Out-Null + Write-Host "removed the previous latest pointer" + } + + $body = @{ + tag_name = 'latest' + name = "Update feed -> $env:TAG" + body = "**Download ``launcher-win-x64.zip``**, unzip it, and run ``acdream-launcher.exe``. It installs the game and keeps itself and the client up to date.`n`nThis is build ``$env:TAG``." + draft = $false + prerelease = $false + target_commitish = 'main' + } | ConvertTo-Json + $pointer = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers ` + -ContentType 'application/json' -Body $body + + # Upload the payloads here too, not just the manifest. `latest` is the + # top of the Releases page and the first thing a person sees; a + # pointer-only release gives them nothing to click and makes them hunt + # for a build tagged with a timestamp. The launcher only needs + # manifest.json, but a friend needs launcher-win-x64.zip. + foreach ($f in Get-ChildItem bin -File) { + Invoke-RestMethod -Method Post -Headers $headers ` + -Uri "$api/releases/$($pointer.id)/assets?name=$($f.Name)" ` + -Form @{ attachment = Get-Item $f.FullName } | Out-Null + } + Write-Host "latest now carries $env:TAG and its downloads" + + - name: Prune old releases + shell: pwsh + env: + KEEP: '5' + TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + $ErrorActionPreference = 'Stop' + $api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + $headers = @{ Authorization = "token $env:TOKEN" } + $keep = [int]$env:KEEP + + # Each build is ~121 MB of attachments, so without this the server + # grows by that much on EVERY push to main. Keep the newest $keep + # versioned releases: enough to grab a previous build or bisect a + # regression, bounded at well under a gigabyte. + $releases = Invoke-RestMethod -Method Get -Headers $headers -Uri "$api/releases?limit=100" + # Never touch the `latest` pointer — it is the launcher's feed, not a build. + $builds = @($releases | Where-Object { $_.tag_name -ne 'latest' } | + Sort-Object -Property created_at -Descending) + + Write-Host "$($builds.Count) versioned release(s); keeping $keep" + foreach ($old in ($builds | Select-Object -Skip $keep)) { + Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($old.id)" | Out-Null + # The tag survives its release and would otherwise accumulate. + Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/tags/$($old.tag_name)" -SkipHttpErrorCheck | Out-Null + Write-Host " pruned $($old.tag_name)" + } diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f4841a25..d3d775c2 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -3,9 +3,6 @@ name: "Copilot Setup Steps" # This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server on: workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml jobs: # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent diff --git a/.github/workflows/headless-portability.yml b/.github/workflows/headless-portability.yml index 3586c816..2149e6b3 100644 --- a/.github/workflows/headless-portability.yml +++ b/.github/workflows/headless-portability.yml @@ -1,66 +1,6 @@ name: Headless portability on: - pull_request: - paths: - - ".github/workflows/headless-portability.yml" - - "AcDream.slnx" - - "src/AcDream.Platform/**" - - "src/AcDream.Launcher.Core/**" - - "src/AcDream.Launcher/**" - - "src/AcDream.Bake/**" - - "src/AcDream.Core/**" - - "src/AcDream.Core.Net/**" - - "src/AcDream.Content/**" - - "src/AcDream.Plugin.Abstractions/**" - - "src/AcDream.Runtime/**" - - "src/AcDream.Headless/**" - - "src/AcDream.App/**" - - "src/AcDream.UI.Abstractions/**" - - "tests/AcDream.Platform.Tests/**" - - "tests/AcDream.Launcher.Core.Tests/**" - - "tests/AcDream.Launcher.Tests/**" - - "tests/AcDream.Bake.Tests/**" - - "tests/AcDream.Core.Tests/**" - - "tests/AcDream.Core.Net.Tests/**" - - "tests/AcDream.Content.Tests/**" - - "tests/AcDream.Runtime.Tests/**" - - "tests/AcDream.Headless.Tests/**" - - "tests/AcDream.App.Tests/**" - - "tests/AcDream.UI.Abstractions.Tests/**" - - "tests/Fixtures/campaign-la/**" - - "tools/ShaderCompiler/**" - - "tools/compile-shaders.ps1" - push: - paths: - - ".github/workflows/headless-portability.yml" - - "AcDream.slnx" - - "src/AcDream.Platform/**" - - "src/AcDream.Launcher.Core/**" - - "src/AcDream.Launcher/**" - - "src/AcDream.Bake/**" - - "src/AcDream.Core/**" - - "src/AcDream.Core.Net/**" - - "src/AcDream.Content/**" - - "src/AcDream.Plugin.Abstractions/**" - - "src/AcDream.Runtime/**" - - "src/AcDream.Headless/**" - - "src/AcDream.App/**" - - "src/AcDream.UI.Abstractions/**" - - "tests/AcDream.Platform.Tests/**" - - "tests/AcDream.Launcher.Core.Tests/**" - - "tests/AcDream.Launcher.Tests/**" - - "tests/AcDream.Bake.Tests/**" - - "tests/AcDream.Core.Tests/**" - - "tests/AcDream.Core.Net.Tests/**" - - "tests/AcDream.Content.Tests/**" - - "tests/AcDream.Runtime.Tests/**" - - "tests/AcDream.Headless.Tests/**" - - "tests/AcDream.App.Tests/**" - - "tests/AcDream.UI.Abstractions.Tests/**" - - "tests/Fixtures/campaign-la/**" - - "tools/ShaderCompiler/**" - - "tools/compile-shaders.ps1" workflow_dispatch: permissions: diff --git a/.github/workflows/hygiene-assessment.lock.yml b/.github/workflows/hygiene-assessment.lock.yml index a7294727..3c5ebe78 100644 --- a/.github/workflows/hygiene-assessment.lock.yml +++ b/.github/workflows/hygiene-assessment.lock.yml @@ -49,9 +49,6 @@ name: "acdream Hygiene Assessment" on: - schedule: - - cron: "54 4 * * *" - # Friendly format: daily (scattered) workflow_dispatch: {} permissions: {} @@ -1348,4 +1345,3 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - diff --git a/.github/workflows/release-gate.yml b/.github/workflows/release-gate.yml index 949e26d2..b7240505 100644 --- a/.github/workflows/release-gate.yml +++ b/.github/workflows/release-gate.yml @@ -1,9 +1,6 @@ name: Complete Release gate on: - pull_request: - push: - branches: [main] workflow_dispatch: permissions: diff --git a/.gitignore b/.gitignore index 894a5f03..b223a2d6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ bin/ obj/ out/ +# NOTE: the repo-root /bin folder holds the alpha distribution feed written by +# tools/publish-bin.ps1. It stays IGNORED here on purpose so a stray `git add` +# can never put ~150 MB of payloads on main (GitHub also hard-rejects any file +# over 100 MB). tools/publish-dist.ps1 force-adds it onto the Gitea-only `dist` +# branch instead, which is what the launcher's update feed reads. # Rider / VS .idea/ @@ -59,6 +64,7 @@ tmp/ # Disposable dotnet test/build output redirected by local validation runs .test-out/ +**/TestResults/ # Connected-gate, benchmark, and visual-capture artifacts are machine-local logs/ @@ -109,3 +115,7 @@ studio-shots/ # Campaign V capture/evidence output - session-local, never tracked (423 MB lesson, 2026-07-29) artifacts/ 341-slope-capture.jsonl + +# IconForge DAT extraction scratch (geometry + textures dumped from the +# installed client dats; regenerate with tools/MosswartArt, never commit). +tools/IconForge/work/ diff --git a/AGENTS.md b/AGENTS.md index dd7f5549..5ac7376d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,8 +132,194 @@ user-accepted, including exact response flags, independent examination window, inscription transaction, complete creature/item/spell reports, favorite-spell press/right-click behavior, modern scarab/prismatic formula, DAT component icons, foreground stacking, and authored 310 x 400 extent. -Resume at Slice 4 equipped-child world picking, then vendor browse and -authoritative transactions. +Slice 4 equipped-child world picking passed its two-client Coldeve gate and +was user-accepted 2026-07-29. **Slices 5 and 6 (the complete vendor +experience — browse, staged buying, selling, walk-to-use, the authored +panel) closed user-accepted 2026-08-08; the six-slice program is COMPLETE +(see the plan's PROGRAM CLOSEOUT). The vendor arc also exposed and fixed +two latent client-wide crashers (#348 cursor-handle exhaustion, #350 +render-ledger overflow).** **Campaign P — physics retail-feel parity +(`docs/plans/2026-07-29-physics-parity-campaign.md`) is CLOSED 2026-07-31 +— final user matrix accepted.** Every physics-scope gap from the +2026-07-29 audit landed and user-gated: #266 run speed (retail's ==800 +sentinel — ACE's >=800 is a misread; never re-import), the #265/#166 +landing-momentum + bounce family +(`docs/research/2026-07-30-landing-bounce-family.md`), the #267 vitae +panel, #268 (panel colors + augmentation bonuses), #269 (slope-stop slide +— the live-trace contact-plane-restore fix), and TS-8 (0x02C2 StatMod +parse). See the plan doc for the retired-row ledger. **Campaign A — audio +retail parity (`docs/plans/2026-08-08-audio-parity-campaign.md`) is +CODE-COMPLETE 2026-08-08** with slices A1–A6 landed and listening-gate +rounds user-driven; open tail: #358 (Ctrl+M mute chord never fires) and +the formal plan-status flip. **Campaign CH — chat & interface-text retail +parity (`docs/plans/2026-08-09-chat-parity-campaign.md`) is CLOSED +USER-ACCEPTED 2026-08-10** after five connected gate rounds: retail +colors, the SpewBox with retail's two-plane glyph outlines, working side +channels, the 152-verb command registry, the CH6 window shell (floating +windows, all-corner resize, opacity), and verbatim /help. Carried tail: +#360/#361, #366, #369, AP-177/190/191, and the round-5 review S1–S3 +polish items. **Campaign OP — the retail four-tab Options panel +(`docs/plans/2026-08-10-options-panel-campaign.md`) is CODE-COMPLETE +2026-08-11.** Retail's Options panel (Gameplay Options / Character / Chat / +Config, LayoutDesc `0x2100002B`) plus the Configure Keyboard screen are +acdream's ONE in-client settings surface (design D1): F11/toolbar open the +authored tab host; `RuntimeCharacterOptionsState` + the 53-id +`CharacterOptionTable` own option storage; retail's wire split ships exactly +(21 auto-save ids → `0x0005` immediate, the rest ride the real `0x01A1` +PlayerModule blob with Apply/logout/480 s flushes, header always `0x460`); +headless bots declare options by name (OP7's live bot-vs-ACE gate PASSED); +OP9 retired the dead F11 `SettingsPanel`/`SettingsVM` surface and the +`GameplaySettings` record outright. OP1/OP2/OP7/OP9 CLOSED through dual/ +combined Opus review. **2026-08-14 re-gate round:** the whole gate-4 fix +batch (#372 both halves, #374, #375, #378–#382, #385) is USER-PASSED; the +OP8 first look filed + same-day-fixed #394/#395/#396 (authored 18px-serif +row-caption font; the retail `GetNameFromKey` key-name pipeline — DAT +tables `0x2300000A`/`0x2300000B`/`0x23000007` via GetDIDByEnum category 4, +OS-localized fallback, register AD-96; the `InitiateBinding` capture- +instruction WAIT dialog) plus the WaitDialog-type-0x19 crash (`2a81e813`, +live-verified no-crash). **STILL OWED: the full §OP3–§OP6 script sections +and §OP8's visual re-check** — script +`docs/research/2026-08-11-campaign-op-test-script.md`, launch with +`ACDREAM_RETAIL_UI=1`. Tail: +#371, #373, AP-198/199/201/202/203. START at +`claude-memory/project_settings_options_digest.md`. + +**Campaign FA — the retail social panel (Fellowship & Allegiance) +(`docs/plans/2026-08-11-fellowship-allegiance-campaign.md`) is +CODE-COMPLETE 2026-08-12.** Retail authors ONE four-tab `gmPanelUI` social +panel (Friends / Allegiance / Fellowship / Squelch, host slot +`0x1000018F`, id 12; F3 = Allegiance, F4 = Fellowship, keyboard-only — +Allegiance is the authored DEFAULT tab), mounted with the OP3 Options-panel +recipe. The Fellowship and Allegiance pages are LIVE end-to-end: real wire +(FA1 repaired the never-called H.2 builders + parsers — retail's FOUR +tree-rejection rules, ELEVEN version gates, the byte-decoded `>=9` size and +the truncated XP-share table), two session-scoped Runtime owners +(`RuntimeFellowshipState`/`RuntimeAllegianceState`, both clear at +generation reset — D2 corrected), and the authored panels through +`LayoutImporter`. Friends/Squelch bind read-only to J4.1's owners. +**The fellowship two-session flow is PROVEN over the live wire** — FA6's +automated bot-vs-ACE gate (`testaccount`/`+Acdream` + `testaccount2`/ +`+Horan`) passed: the recruited bot's OWN `RuntimeFellowshipState` flips +`IsInFellowship`. Six FA slices, each dual-lens Opus reviewed → fix round → +narrow re-review; the reviews caught what tests can't (retail's 4th tree +rule, the D2 reset-lifetime inversion, the D6 server-side invite filter, +a seam-map entry that would have re-introduced a fixed bug). OWED: the +user's connected gates (§FA3-§FA6 of +`docs/research/2026-08-12-campaign-fa-test-script.md`, several +`[TWO-CLIENT]`), and **#384** — the allegiance-swear bot gate is +deferred/disabled because ACE returns NOTHING to the `0x001D` swear at +0.005 m (no confirmation, no tree update, no error; needs ACE-console +disambiguation — the swear CODE is done+reviewed, only its automated +two-session proof is unverified; register AD-87). Tail: #383 (installed- +DAT vs committed-fixture drift, found at FA3). START at +`claude-memory/project_fellowship_allegiance_campaign.md`. + +**2026-08-13/14 gate block — SOCIAL GATES + SECURE TRADE all +USER-PASSED.** The social panel's connected gate rounds closed (border-only +move cursor, amber row selection, wrapped empty-state text, composed +confirmation sentences via the new `DatStringResolver.ResolveTemplate` +StringTable-interleave port, the refused-drop SpewBox notice via the +`InventoryTransactionState.RequestFailed` seam, live friends +Online/Offline through the authored row state machine + the new UiText +per-state string swap). Same block: powerbar mode captions +(jump 'Height' right-aligned per-STATE justify / 'Power'↔'Accuracy' by +combat mode), release-edge airborne jump refusal (supersedes CH round-1's +press-edge report), and **SECURE TRADE SHIPPED + two-client user gate +PASSED 2026-08-14** — gmSecureTradeUI window (LayoutDesc `0x2100000D`), +full `0x1F6`–`0x208` wire, `RuntimeTradeState` as the third sibling +J-owner, both retail open paths, staged-item trading marker +(`ClientObject.TradeState` now live), cancel text. START at +`claude-memory/project_secure_trade.md`; the deferred-Func lesson is +`claude-memory/feedback_resolve_deferred_funcs_per_call.md`. Register: +AD-93/AD-94 filed, AD-85 narrowed, AD-81 amended, AD-89/AD-95 retired. +Filed: #393 (texture-detail options, post-M4). + +**Campaign LA — the alpha launcher (ACTIVE 2026-08-14):** Avalonia +launcher/installer/updater (Windows+Linux) + the retail character- +management screen, driven autonomously under a user-set goal: Fable +plans, Sonnet implements, Opus dual-lens reviews (architectural + +retail-faithful). Spec: +`docs/superpowers/specs/2026-08-14-launcher-campaign-design.md`; plan + +ledger: `docs/plans/2026-08-14-launcher-campaign.md`; START at +`claude-memory/project_launcher_direction.md`. Key recon corrections +already binding: retail's select screen (`gmCharacterManagementUI`) has +NO 3D preview (chargen-only machinery); UI Studio no longer exists +(deleted at Campaign V — ignore stale memory/docs claims otherwise); +App `Program.cs` has no subcommand dispatch (the `--session-config` flag +is additive). +LA0 through LA11's automated scope are review-closed. The launcher composer is now +compiled into both host test suites, and Launcher.Core runs in the portable +Windows/Ubuntu CI closure. The self-contained Avalonia launcher, +transactional two-host plugin lifetime, shared login-command route, +Runtime-owned retail selection state, authored DAT character screen, and +crash-safe verified installer plus atomic cross-platform updater/self-updater +are integrated. Windows group-isolated Headless stop, isolated update fixtures, +strict status/redaction evidence, and the exact Windows/Ubuntu operator script +are landed; the integrated preflight passes 32/32 commands and 14,012 tests / +5 skips. Only the connected/visual/real-DAT user gate remains before shipment. + +**Campaign CC — retail character creation (CLOSED USER-ACCEPTED +2026-08-16).** All seven slices REVIEW-CLOSED; the connected gate ran as +one extended round (findings GF-1..16 + re-tests R2/R3/R4, fix batches +A-G + closeout + two re-test rounds, final build `1.0.2-cc.o`) and +PASSED. **Milestone: the first live character ever created by acdream +against ACE landed mid-round.** The gate round's own harvest hardened +shared surfaces well beyond chargen: authored text margins (P0x23-26), +the authored Unselected/Selected state pair + per-state label color, +un-consumed Type-12 media children (frames/scrollbars client-wide), +single-sprite scrollbar thumbs, UiButton/UiDatElement Tint, the +dialog-always-on-top re-raise (the invisible-modal input blackhole), a +truthful client crash self-report + bounded stderr capture (#405-#407 +fixed, #406 fixed; #408/#409/#410 filed for their own rounds). The full retail creation flow: Create +button (retail's exact `UpdateButtons` roster` — live capture of every player-side - `PhysicsEngine.ResolveWithTransition` call. Each call appends one - JSON Lines record with full inputs, PhysicsBody snapshot before AND - after, plus the `ResolveResult`. Filtered to `IsPlayer` mover flag - — NPC / remote DR calls don't pollute. Pairs with the trajectory - replay harness comparison tests to diff captured vs harness state - per field — the first divergence pinpoints missing apparatus state. - Capture is OFF when the env var is unset (one null-check cost per - call). -- `ACDREAM_DUMP_CELLS=` / `ACDREAM_DUMP_GFXOBJS=` — dump - resolved cell/GfxObj polygon tables as JSON when ids cache. Used - for harness fixture extraction. +Every environment variable and command-line argument the client reads — +what it does, its exact value shape, and **what else it changes about the +run** — is documented in +[`docs/launch-options.md`](docs/launch-options.md). That file is the single +source of truth for every probe we have and how to turn one on, and it is +enforced by `LaunchOptionsDocumentationTests`: a flag without a documented +row fails the build, and so does a documented row whose read site was +deleted. **Any future probe that stays in the code gets its row there in +the same commit — no exceptions.** + +The binding rules: + +- **Every probe and dump is OFF by default.** Nothing that prints, records, + or costs performance may activate without its env var explicitly set + (`=1`). The only default-on flags are retail *behaviors* wearing an + A/B off-switch (`ACDREAM_RETAIL_CHASE`, `ACDREAM_CAMERA_COLLIDE`, + `ACDREAM_CAMERA_ALIGN_SLOPE`, `ACDREAM_RETAIL_CLOSE_DEGRADES` — `=0` + disables); that set is frozen by `LaunchOptionsDocumentationTests` — + never add a default-on diagnostic. +- **Read the side-effects column before any measurement.** Flags that look + inert are not: `ACDREAM_AUTOMATION_ARTIFACT_DIR` also builds a per-frame + diagnostics referee (#432), and `ACDREAM_STREAM_RADIUS` measures a + streaming window production never uses. +- **A temporary probe dies with its investigation.** Add the row when you + add the probe; delete both in the commit that fixes the issue. ### Outbound motion wire format (acdream → ACE) @@ -1478,8 +1645,8 @@ already-running ACE session via the handshake race. ## Reference repos: cross-check the relevant ones -The `references/` tree holds **six** vendored projects (ACE, ACViewer, -WorldBuilder, Chorizite.ACProtocol, holtburger, AC2D). They overlap in +The `references/` tree holds **five** vendored projects (ACE, ACViewer, +WorldBuilder, Chorizite.ACProtocol, holtburger). They overlap in some areas and disagree in others. Before committing to an approach, **cross-reference at least two of them** for the domain you're working in — the per-domain hierarchy in the next section tells you which to @@ -1488,7 +1655,7 @@ the relevant references is almost always the truth. The user has repeatedly had to remind me about this when I narrowly searched one ref and missed obvious answers in another. -The six references: +The five references: - **`references/ACE/`** — ACEmulator server. Authority on the wire protocol (packet framing, ISAAC, game message opcodes, serialization @@ -1538,15 +1705,15 @@ The six references: the message-builder layer. ACE shows what the server expects; holtburger shows what a real client actually sends. -- **`references/AC2D/`** — **C++ AC client emulator.** Oldest reference, - fixed-function OpenGL, but has the **real AC terrain split formula** - (`FSplitNESW` with constants `0x0CCAC033`, `0x421BE3BD`, `0x6C1AC587`, - `0x519B8F25`) which differs from WorldBuilder's physics-path formula. - Also has the complete `0xF61C` movement packet format with flag bits - and the `stMoveInfo` sequence counters. Key lesson from AC2D: it does - NOT do client-side terrain Z — it sends movement keys to the server - and uses the server's authoritative Z. See - `docs/research/2026-04-12-movement-deep-dive.md` for the full analysis. +**AC2D is a retired reference (2026-07-29).** It was a C++ AC client demo +and the sixth entry in this list; it is no longer vendored under +`references/` and must not be re-cloned. Everything we took from it is +already written down and still stands: the terrain split formula +`FSplitNESW` (constants `0x0CCAC033`, `0x421BE3BD`, `0x6C1AC587`, +`0x519B8F25`), the `0xF61C` movement packet layout, and the finding that a +client need not compute terrain Z itself. The historical analysis lives in +`docs/research/2026-04-12-movement-deep-dive.md`; the UI dat-id work it fed +is in `docs/research/retail-ui/`. Cite those, not the repo. ### Reference hierarchy by domain @@ -1571,9 +1738,9 @@ decompiled client code and would have fixed it in minutes. | **EnvCell / dungeon rendering** (cell geometry, portal visibility, collision mesh) | **WorldBuilder `EnvCellRenderManager.cs` + `PortalRenderManager.cs`** | ACME `EnvCellManager.cs` (more complete for collision); ACViewer `Physics/Common/EnvCell.cs` | WB is acdream's geometry base; ACME for collision until ported. | | **Particles / sky** (particle systems, weather, sky particles) | **WorldBuilder `SkyboxRenderManager.cs` + `ParticleEmitterRenderer.cs` + `ParticleBatcher.cs`** | retail decomp | WB is acdream's particle base. | | **Visibility / culling** (frustum, cell visibility) | **WorldBuilder `VisibilityManager.cs` + `Frustum.cs`** | — | WB. | -| **Network protocol** (wire format, packet framing, fragment assembly, ISAAC) | **holtburger** `crates/holtburger-session/` | AC2D `cNetwork.cpp` (simpler, good for cross-check) | ACE shows the server side; holtburger + AC2D show the client side. | -| **Client behavior** (what to send when, login flow, ack pattern, keepalive) | **holtburger** `crates/holtburger-core/src/client/` | AC2D `cNetwork.cpp` + `cInterface.cpp` | holtburger is the most complete; AC2D is simpler but confirmed working. | -| **Movement** (MoveToState format, AutonomousPosition, sequence counters, speed) | **holtburger** `client/movement/` | AC2D `cNetwork.cpp:2592-2664` (0xF61C format) | See `docs/research/2026-04-12-movement-deep-dive.md` for the full cross-reference. | +| **Network protocol** (wire format, packet framing, fragment assembly, ISAAC) | **holtburger** `crates/holtburger-session/` | `docs/research/named-retail/` | ACE shows the server side; holtburger shows the client side. AC2D was the second client-side cross-check here — retired reference; historical analysis remains in `docs/research/2026-04-12-movement-deep-dive.md`. | +| **Client behavior** (what to send when, login flow, ack pattern, keepalive) | **holtburger** `crates/holtburger-core/src/client/` | `docs/research/named-retail/` | holtburger is the most complete. AC2D was the simpler confirmed-working cross-check — retired reference; historical analysis remains in `docs/research/2026-04-12-movement-deep-dive.md`. | +| **Movement** (MoveToState format, AutonomousPosition, sequence counters, speed) | **holtburger** `client/movement/` | `docs/research/named-retail/` | AC2D `cNetwork.cpp:2592-2664` was the `0xF61C` format secondary — retired reference; historical analysis remains in `docs/research/2026-04-12-movement-deep-dive.md`, which carries the packet layout and the terrain-split formula verbatim. | | **Server expectations** (what ACE accepts/rejects, validation thresholds) | **ACE** `Source/ACE.Server/Network/` | — | Only ACE knows what the server actually validates. | | **Silk.NET / .NET 10 idioms** (GL calls, shader setup, VAO patterns) | **WorldBuilder original** | ACME (same stack) | Both use the same backend; original has cleaner isolated examples. | | **Protocol field order** (packed dwords, type prefixes, flag enums) | **Chorizite.ACProtocol** `Types/*.cs` | holtburger (cross-check) | Generated from protocol XML; has accurate field comments. | diff --git a/AcDream.slnx b/AcDream.slnx index 134a5e72..892b0ee3 100644 --- a/AcDream.slnx +++ b/AcDream.slnx @@ -11,20 +11,31 @@ + + + + + + + + + + + @@ -45,7 +56,11 @@ + + + + diff --git a/CLAUDE.md b/CLAUDE.md index 256acf73..90acfba6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -765,6 +765,7 @@ Documentation entry point: [`docs/README.md`](docs/README.md). For canonical state, read in this order: - [`docs/plans/2026-07-29-network-transport-campaign.md`](docs/plans/2026-07-29-network-transport-campaign.md) — Campaign N, the retail reliable-transport port — **CLOSED 2026-07-29, user-accepted** (#260 closed; a real wire loss recovered live during the acceptance session). Still the SSOT for the transport mechanism, the ACE constraint table, and the landmine list — read it (or `claude-memory/project_network_transport_digest.md`) before touching anything under `src/AcDream.Core.Net/`. - [`docs/plans/2026-07-27-vulkan-campaign.md`](docs/plans/2026-07-27-vulkan-campaign.md) — Campaign V, OpenGL → Vulkan — **CLOSED 2026-07-29**; the completed record of the RHI contract, V0–V11 slices, and the GL deletion. Historical reference for `src/AcDream.App/Rendering/`. +- [`docs/ci-and-releases.md`](docs/ci-and-releases.md) — **the Gitea CI/release SSOT (2026-08-19)**: every push to main gates on two self-hosted runners (RARE-win / eriktestLinux) and publishes a Gitea Release the launcher installs from; payloads are release attachments, the `latest` release is the launcher's pointer, old releases are pruned to 5. Load-sensitive tests live in `Lane=Timing` (see `docs/release-gate.md`) — do NOT chase them individually. - [`docs/plans/2026-05-12-milestones.md`](docs/plans/2026-05-12-milestones.md) — milestone targets + freeze list per milestone - [`docs/plans/2026-04-11-roadmap.md`](docs/plans/2026-04-11-roadmap.md) — what's shipped, what's in flight, what's next - [`docs/ISSUES.md`](docs/ISSUES.md) — open + recently closed bugs (tactical) @@ -1554,108 +1555,44 @@ governed by whether the previous shutdown was graceful or forced. ### Test character `+Acdream` at server guid `0x5000000A`. Starts at or near Holtburg. Has -basic stats; `ACDREAM_RUN_SKILL` / `ACDREAM_JUMP_SKILL` env vars (default -200) set the *client-side* skill value used by `PlayerWeenie.InqRunRate` -for local motion prediction. **These are NOT synced to the server** — -ACE's own character data is authoritative for broadcast motion. If you -see a speed/anim mismatch between local and observer views, the fix is -to sync the runSkill from ACE via `UpdateMotion.ForwardSpeed` echo (wired -via `PlayerMovementController.ApplyServerRunRate`) or from -`PlayerDescription (0x0013)`. +basic stats. Run/jump skills arrive FROM the server and drive local motion +prediction (`LiveMovementStatsApplier` → `PlayerMovementController`); the +hardcoded fallbacks before the server speaks are 200 run / 300 jump. The +former `ACDREAM_RUN_SKILL` / `ACDREAM_JUMP_SKILL` client-side overrides no +longer exist — see the Retired section of +[`docs/launch-options.md`](docs/launch-options.md). If you see a speed/anim +mismatch between local and observer views, check the server sync path +(`UpdateMotion.ForwardSpeed` echo via +`PlayerMovementController.ApplyServerRunRate`, or +`PlayerDescription (0x0013)`). ### Diagnostic env vars -- `ACDREAM_DUMP_MOTION=1` — dump every inbound `UpdateMotion` (guid, - stance, cmd, speed) + resulting `SetCycle` call. Massive for remote- - animation debugging. -- `ACDREAM_STREAM_RADIUS=N` — **legacy** streaming-radius override - (`RuntimeOptions.LegacyStreamRadius`). **Default is UNSET**, not 2: the - shipped radii come from the quality preset - (`QualityPreset.High` = NearRadius 4 / FarRadius 12, i.e. a 9×9 Near ring - inside a 25×25 Far window). When set it FORCES `NearRadius = N` and only - ever RAISES `FarRadius` (`SessionPlayerComposition.ComposeCore`), and it is - silently discarded by any later Settings `ApplyQuality` - (`RuntimeSettingsTargets.ApplyQuality` → `ReconfigureRadii`). **Leave it - unset for any measurement or gate run** — with it set you are measuring a - different window than production. Per-axis overrides - `ACDREAM_NEAR_RADIUS` / `ACDREAM_FAR_RADIUS` (`QualitySettings.WithEnvOverrides`) - are the modern spelling. -- `ACDREAM_PROBE_REVEAL_RADIUS=N` — #280 A/B measurement probe - (`StreamingDiagnostics.RevealRadiusOverride`). Forces the outdoor reveal - gate to landblock radius N instead of the derived streaming window, so the - same binary can run a route once with the pre-#280 behaviour (`=1`) and once - without. Not a user setting; not surfaced in Settings; not persisted. - Values below 1 are rejected by the parser: an outdoor acknowledgement with - `RequiredRenderRadius == 0` fails Runtime's `invalid-readiness-shape` - invariant, so `=0` would hang the route it is meant to measure. -- `ACDREAM_NO_AUDIO=1` — suppress OpenAL init for headless / driver- - broken setups. -- `ACDREAM_REMOTE_VEL_DIAG=1` — dump per-tick / per-UM remote motion - diagnostics (`[UM_RAW]`, `[SCFAST]`, `[SCFULL]`, `[SETCYCLE]`, - `[FWD_WIRE]`, `[OMEGA_DIAG]`, `[SEQSTATE]`, `[PARTSDIAG]`, - `[VEL_DIAG]`, `[UPCYCLE]`). Heavy. -- `ACDREAM_PROBE_RESOLVE=1` — one `[resolve]` line per - `PhysicsEngine.ResolveWithTransition` call: input + target + output - position/cell, ok-vs-partial, grounded-in, contact-plane status, - wall normal if hit, **responsible entity guid**, env flag, walkable - polygon valid. Heavy (~30 Hz × every entity). Runtime-toggleable via - the DebugPanel "Diagnostics" section if `ACDREAM_DEVTOOLS=1`. -- `ACDREAM_PROBE_CELL=1` — one `[cell-transit]` line per - `PlayerMovementController.CellId` change: old → new cell, world - position, reason tag (`resolver` / `teleport`). Low volume — only - fires on actual cell crossings. Runtime-toggleable via the same - DebugPanel section. -- `ACDREAM_PROBE_PUSH_BACK=1` — emits three line types per physics - tick: `[push-back]` (per `BSPQuery.AdjustSphereToPlane` call), - `[push-back-disp]` (per `BSPQuery.FindCollisions` dispatch), - `[push-back-cell]` (per `Transition.CheckOtherCells` off-cell hit). - Heavy under motion (~100–500 lines/sec). Pair with retail's cdb - breakpoint set at `tools/cdb/a6-probe.cdb` for the A6.P1 capture - protocol. Runtime-toggleable via the DebugPanel. -- `ACDREAM_PROBE_FLAP=1` — capture probe for indoor visibility - decisions at frame boundaries. Used to converge the U.4c flap fix - (root indoor visibility at player's cell, not eye). -- `ACDREAM_PROBE_STICKY=1` — per-guid sticky-melee timeline: `[sticky]` - lifecycle lines (STICK/UNSTICK/LEASE-EXPIRE/TARGET-status teardown), - per-armed-tick steer lines (signed gap dist, applied delta, heading - delta), `[sticky-snap-skip]` at the suppressed NPC UP-snap site. - Heavy while a pack is stuck (~60 Hz × stuck count). Converged the - #171 residuals (the deep-overlap sign pin AP-82). -- `ACDREAM_PROBE_SUPPORT=1` — **what is holding a body up, and is the - collision geometry where the visual geometry is?** (#337, TEMPORARY). - `[support]`: one line per resolve **for every body, not just the player** - (a corpse falling through geometry is the cheapest control there is on - "movement code vs geometry data"). It samples the outdoor terrain - independently at the body's own out-XY and prints the contact plane's own - height at that same XY, so `support=terrain` / `object` / `none` is a - measurement rather than an inference; `cpSrc=` names the site that wrote - the plane so provenance cross-checks the classification. Edge-eager, - throttled to 4 Hz per body, and emits every 10 cm of vertical movement. - `[geom]`: once per GfxObj near the mover — the object's physics-BSP vertex - cloud against its visual mesh AABB in the same frame, with a verdict - (`coincident` REFUTES "collision isn't where the visual is"; - `no-physics-bsp` / `empty-physics-bsp` / `displaced` / `extent-mismatch` - each name a data defect). `ACDREAM_PROBE_RESOLVE` alone cannot separate - those cases — it carries no plane normal, no plane height, no terrain - sample and no provenance. -- `ACDREAM_WIRE_MESH=1` — upgrades the existing **F2** collision overlay from - a broadphase proxy cylinder to the real physics-BSP polygon edges (cyan) - beside the same objects' visual mesh boxes (magenta) and the terrain - surface (yellow). Settles "visual versus collision" by eye instead of by - log. `ACDREAM_WIRE_RADIUS=` sets the window (default 30). - TEMPORARY, with the #337 probe family. -- `ACDREAM_CAPTURE_RESOLVE=` — live capture of every player-side - `PhysicsEngine.ResolveWithTransition` call. Each call appends one - JSON Lines record with full inputs, PhysicsBody snapshot before AND - after, plus the `ResolveResult`. Filtered to `IsPlayer` mover flag - — NPC / remote DR calls don't pollute. Pairs with the trajectory - replay harness comparison tests to diff captured vs harness state - per field — the first divergence pinpoints missing apparatus state. - Capture is OFF when the env var is unset (one null-check cost per - call). -- `ACDREAM_DUMP_CELLS=` / `ACDREAM_DUMP_GFXOBJS=` — dump - resolved cell/GfxObj polygon tables as JSON when ids cache. Used - for harness fixture extraction. +Every environment variable and command-line argument the client reads — +what it does, its exact value shape, and **what else it changes about the +run** — is documented in +[`docs/launch-options.md`](docs/launch-options.md). That file is the single +source of truth for every probe we have and how to turn one on, and it is +enforced by `LaunchOptionsDocumentationTests`: a flag without a documented +row fails the build, and so does a documented row whose read site was +deleted. **Any future probe that stays in the code gets its row there in +the same commit — no exceptions.** + +The binding rules: + +- **Every probe and dump is OFF by default.** Nothing that prints, records, + or costs performance may activate without its env var explicitly set + (`=1`). The only default-on flags are retail *behaviors* wearing an + A/B off-switch (`ACDREAM_RETAIL_CHASE`, `ACDREAM_CAMERA_COLLIDE`, + `ACDREAM_CAMERA_ALIGN_SLOPE`, `ACDREAM_RETAIL_CLOSE_DEGRADES` — `=0` + disables); that set is frozen by `LaunchOptionsDocumentationTests` — + never add a default-on diagnostic. +- **Read the side-effects column before any measurement.** Flags that look + inert are not: `ACDREAM_AUTOMATION_ARTIFACT_DIR` also builds a per-frame + diagnostics referee (#432), and `ACDREAM_STREAM_RADIUS` measures a + streaming window production never uses. +- **A temporary probe dies with its investigation.** Add the row when you + add the probe; delete both in the commit that fixes the issue. ### Outbound motion wire format (acdream → ACE) diff --git a/README.md b/README.md index d9b668c2..616035ea 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ missing. - A machine-local `acdream.pak` built from those DATs - A running ACE server for connected play; the examples use `127.0.0.1:9000` -- For the graphical client, a driver exposing the mandatory modern OpenGL - capabilities +- For the graphical client, a driver exposing the mandatory Vulkan + capabilities validated at startup The project does not distribute Microsoft/Turbine DAT files or derived prepared packages. @@ -111,10 +111,9 @@ dotnet build AcDream.slnx -c Release dotnet test AcDream.slnx -c Release --no-build ``` -The current baseline is a successful Release build with **8,826 passing tests -and 5 intentional skips**. The build currently reports 17 test-project -warnings tracked by [`#228`](docs/ISSUES.md#228--clean-release-build-emits-17-test-project-warnings); -production compilation has zero errors. +The current CI-filtered Windows baseline is a successful Release build with +**16,151 passing tests and zero failures**; opt-in live, installed-DAT, +prepared-package, manual, timing, and platform-specific lanes run separately. ## Prepare content @@ -127,8 +126,9 @@ dotnet run --project src\AcDream.Bake\AcDream.Bake.csproj -c Release -- ` --out "C:\Games\Asheron's Call\acdream.pak" ``` -A complete package is approximately 30 GB. It is machine-local and must not be -committed. `ACDREAM_PAK_PATH` overrides the default +A complete format-2 package from the standard installed DAT set is about +570 MiB (the former format-1 package was about 30 GB). It is machine-local and +must not be committed. `ACDREAM_PAK_PATH` overrides the default `\acdream.pak`. ## Run the graphical client @@ -141,7 +141,6 @@ $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 ``` @@ -208,7 +207,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=1` | Enable the retained retail gameplay UI | +| `ACDREAM_RETAIL_UI=0` | Disable the retained retail gameplay UI for diagnostics; it is enabled by default | | `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/assets/icons/README.md b/assets/icons/README.md new file mode 100644 index 00000000..9ff56a21 --- /dev/null +++ b/assets/icons/README.md @@ -0,0 +1,93 @@ +# acdream application icons + +Two marks, one family. + +| Mark | Files | Used by | +|---|---|---| +| **Client** — the mosswart head | `acdream-client-*.png`, `acdream-client.ico` | `AcDream.App` (PE icon + runtime window icon) | +| **Launcher** — the ring and crescent | `acdream-launcher-*.png`, `acdream-launcher.ico` | `AcDream.Launcher` (PE icon + Avalonia `Window.Icon`) | + +Each ships PNGs at 16/24/32/48/64/128/256/512/1024 plus a multi-size `.ico` +carrying 16 through 256. + +## Where the art comes from + +**The client mark is the retail mosswart**, not a drawing of one. It is the +actual creature head — `Setup 0x02000B4F` part 14, skin atlas `0x05001E11`, +`ClothingBase 0x10000344` — pulled from `client_portal.dat`, smoothed, lit and +graded. Palette values throughout both marks are sampled from that texture: + +| | | +|---|---| +| `#ACB820` | chartreuse upper skin | +| `#A09800` | mustard belly — the "foul yellow" the lore names | +| `#485010` | deep olive shadow | +| `#F2ECD2` | tusk bone | +| `#AC7438` | ear membrane / hide | + +**The launcher mark is inspired by the Asheron's Call sigil** — a forged ring +enclosing a hooked crescent — rebuilt from measurements of the retail wordmark +and the `acclient.exe` icon resource. It is an original construction in the +same visual language, not a copy of the logo. Its warm field matches the retail +client icon's dark-to-gold interior. + +> **Note on rights.** "Asheron's Call" and its logo are trademarks of their +> owners, and the client mark is rendered from copyrighted game art. Unlike DAT +> content — which stays on the user's own disk — these icons are compiled into +> the shipped binaries. If acdream is ever distributed broadly, both marks +> should be reviewed, and the client mark is the one most likely to want an +> original redraw using these renders as reference. + +## Regenerating + +The launcher mark is fully procedural and rebuilds anywhere: + +```bash +py tools/IconForge/forge.py launcher +``` + +That is byte-for-byte deterministic — it reproduces the committed PNGs exactly, +so an accidental edit is visible as a diff. + +The client mark renders real game geometry, so it needs the installed DATs. +One command extracts both halves — the posed geometry and the surfaces it +references — into `tools/IconForge/work/`: + +```bash +dotnet run --project tools/MosswartArt -- 0x02000B4F 0x10000344 tools/IconForge/work/mosswart_mesh.json 0x09000009 +``` + +The trailing MotionTable id is required. Creatures do not define an upright pose +in `Setup.PlacementFrames`; without it every part stacks on the origin. + +Then: + +```bash +py tools/IconForge/forge.py client +``` + +This is deterministic too — given the same DATs it reproduces the committed +PNGs byte-for-byte. + +Requires Python with `numpy`, `pillow` and `scipy`. + +## How they are wired in + +Neither icon is loaded from disk at runtime. + +- **PE icon** — `` in each `.csproj`, pointing at the `.ico` + here. This is what Explorer and the taskbar shortcut show. +- **Client window icon** — `AcDream.App.Rendering.WindowIconLoader` hands GLFW + four sizes **from the `Load` callback**. That timing is load-bearing: Silk's + `Window.Create` only builds the managed object, and `IWindow.Initialize` is + what creates the native window, so applying an icon any earlier throws + "Window should be initialized". The failure is quiet and misleading — GLFW + falls back to the stock Windows application icon rather than the + executable's, so Explorer shows the mark and the running window does not. + The PNGs are *embedded resources* linked from this directory, so there is one + source of truth for the art and no missing-file case at runtime. + `WindowIconLoaderTests` guards both the resource names, which are otherwise + coupled to `LogicalName` in the csproj by string only, and the call-site + ordering. +- **Launcher window icon** — `AvaloniaResource` linked from here, referenced as + `avares://acdream-launcher/Assets/acdream-launcher.png`. diff --git a/assets/icons/acdream-client-1024.png b/assets/icons/acdream-client-1024.png new file mode 100644 index 00000000..e0534265 Binary files /dev/null and b/assets/icons/acdream-client-1024.png differ diff --git a/assets/icons/acdream-client-128.png b/assets/icons/acdream-client-128.png new file mode 100644 index 00000000..6cbbbb39 Binary files /dev/null and b/assets/icons/acdream-client-128.png differ diff --git a/assets/icons/acdream-client-16.png b/assets/icons/acdream-client-16.png new file mode 100644 index 00000000..df593d31 Binary files /dev/null and b/assets/icons/acdream-client-16.png differ diff --git a/assets/icons/acdream-client-24.png b/assets/icons/acdream-client-24.png new file mode 100644 index 00000000..f8cf4a02 Binary files /dev/null and b/assets/icons/acdream-client-24.png differ diff --git a/assets/icons/acdream-client-256.png b/assets/icons/acdream-client-256.png new file mode 100644 index 00000000..568ecfa2 Binary files /dev/null and b/assets/icons/acdream-client-256.png differ diff --git a/assets/icons/acdream-client-32.png b/assets/icons/acdream-client-32.png new file mode 100644 index 00000000..4f586be1 Binary files /dev/null and b/assets/icons/acdream-client-32.png differ diff --git a/assets/icons/acdream-client-48.png b/assets/icons/acdream-client-48.png new file mode 100644 index 00000000..01e33ecf Binary files /dev/null and b/assets/icons/acdream-client-48.png differ diff --git a/assets/icons/acdream-client-512.png b/assets/icons/acdream-client-512.png new file mode 100644 index 00000000..bd6f2170 Binary files /dev/null and b/assets/icons/acdream-client-512.png differ diff --git a/assets/icons/acdream-client-64.png b/assets/icons/acdream-client-64.png new file mode 100644 index 00000000..de6062ff Binary files /dev/null and b/assets/icons/acdream-client-64.png differ diff --git a/assets/icons/acdream-client.ico b/assets/icons/acdream-client.ico new file mode 100644 index 00000000..264fb28c Binary files /dev/null and b/assets/icons/acdream-client.ico differ diff --git a/assets/icons/acdream-launcher-1024.png b/assets/icons/acdream-launcher-1024.png new file mode 100644 index 00000000..2f2bc52d Binary files /dev/null and b/assets/icons/acdream-launcher-1024.png differ diff --git a/assets/icons/acdream-launcher-128.png b/assets/icons/acdream-launcher-128.png new file mode 100644 index 00000000..b63a0536 Binary files /dev/null and b/assets/icons/acdream-launcher-128.png differ diff --git a/assets/icons/acdream-launcher-16.png b/assets/icons/acdream-launcher-16.png new file mode 100644 index 00000000..01cf16f0 Binary files /dev/null and b/assets/icons/acdream-launcher-16.png differ diff --git a/assets/icons/acdream-launcher-24.png b/assets/icons/acdream-launcher-24.png new file mode 100644 index 00000000..e1343f7e Binary files /dev/null and b/assets/icons/acdream-launcher-24.png differ diff --git a/assets/icons/acdream-launcher-256.png b/assets/icons/acdream-launcher-256.png new file mode 100644 index 00000000..d6c31628 Binary files /dev/null and b/assets/icons/acdream-launcher-256.png differ diff --git a/assets/icons/acdream-launcher-32.png b/assets/icons/acdream-launcher-32.png new file mode 100644 index 00000000..901d21c5 Binary files /dev/null and b/assets/icons/acdream-launcher-32.png differ diff --git a/assets/icons/acdream-launcher-48.png b/assets/icons/acdream-launcher-48.png new file mode 100644 index 00000000..f48f796a Binary files /dev/null and b/assets/icons/acdream-launcher-48.png differ diff --git a/assets/icons/acdream-launcher-512.png b/assets/icons/acdream-launcher-512.png new file mode 100644 index 00000000..e4dd9595 Binary files /dev/null and b/assets/icons/acdream-launcher-512.png differ diff --git a/assets/icons/acdream-launcher-64.png b/assets/icons/acdream-launcher-64.png new file mode 100644 index 00000000..eb31b602 Binary files /dev/null and b/assets/icons/acdream-launcher-64.png differ diff --git a/assets/icons/acdream-launcher.ico b/assets/icons/acdream-launcher.ico new file mode 100644 index 00000000..3a6822a1 Binary files /dev/null and b/assets/icons/acdream-launcher.ico differ diff --git a/docs/ISSUES.md b/docs/ISSUES.md index b38196dd..7744316e 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,9 +24,1910 @@ 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). + +## #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. +**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. + +Running two copies of the exact isolated `app-release23` graphical artifact +against local ACE reproduced the same access violation three times. The +faulting process varied: secondary PID 13688 at 15:01:39, primary PID 15300 at +15:03:35, and fresh primary PID 31412 at 15:08:58. The last occurrence fired +while the fresh primary was still on character selection, before EnterWorld; +the already-in-world secondary survived. Windows Application Error reports +all three as `coreclr.dll` exception `0xC0000005`, fault offset `0x356d4f`. +The managed terminal stack is only: + +```text +Silk.NET.Windowing.WindowExtensions...Run +Silk.NET.Windowing.Internals.ViewImplementationBase.Run +Silk.NET.Windowing.Glfw.GlfwWindow.Run +AcDream.App.Rendering.GameWindow.Run +``` + +This is not #422's rare `0xC0000374` heap corruption during graceful process +exit: #452 happens while both graphical clients are active and reproduces +quickly. It is also not a MossTank/plugin-API, CoreCLR, Vulkan, PAK, or world- +cache failure. + +**Exact root (first-chance cdb proof):** the access violation is in packaged +GLFW's Win32 event pump at `glfw3+0x10681`, not in CoreCLR. During its modifier- +key repair pass `_glfwPollEventsWin32` calls `GetActiveWindow`, then +`GetPropW(hwnd, L"GLFW")`, and dereferences the returned value as this +process's `_GLFWwindow*`. Windows UI automation temporarily joins input queues, +so the primary process can receive the secondary process's HWND. Because every +GLFW process uses the same `GLFW` property name, `GetPropW` succeeds but returns +the secondary process's private pointer. The crashed primary had +`rbx=00000202a7180ab0`; a debugger breakpoint in the surviving secondary +reported its own valid `ACTIVE_GLFW_WINDOW=00000202a7180ab0` — exact pointer +identity across the process boundary. + +**Fix:** `Win32GlfwActiveWindowGuard` patches only `glfw3.dll`'s import-address- +table slot for `USER32!GetActiveWindow`, after the GLFW library is loaded and +before `glfwInit`/window creation. The replacement returns the real HWND only +when `GetWindowThreadProcessId` says it belongs to the current process; +otherwise it returns zero, GLFW's existing safe "nothing to repair" branch. +There is no system-wide hook and no other module is changed. Four focused +tests cover local, foreign, null and unowned HWNDs. + +The isolated `app-release24` live gate launched two graphical clients, both +logged `GLFW foreign-active-window guard installed (#452)`, entered the world, +and remained responsive through 100 rapid forced cross-process activation +switches — the exact prior trigger — plus a 30-minute combined in-world soak. +The local peer API then passed in both directions: the secondary evaluated the +primary heartbeat and returned `+Acdream`. A two-member fellowship gate also +passed with both canonical rosters populated and the secondary returning `2` +from `getfellowshipcount[]`; both processes remained alive and responsive. + +Evidence: + +- `artifacts/live-gates/mosstank-final23-secondary/` +- `artifacts/live-gates/mosstank-final23-secondary2/` +- `artifacts/live-gates/mosstank-final23-primary5/` +- `artifacts/live-gates/i451-cdb-primary-attach/cdb.log` +- `artifacts/live-gates/i451-cdb-secondary/cdb.log` +- `artifacts/live-gates/i451-guard-primary2/` +- `artifacts/live-gates/i451-guard-secondary/` +- 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. +## #451 — Sanctuary cathedral portal seam leaks exterior world and particles + +**Status:** DONE — OWNER-ACCEPTED 2026-08-27 ("Looks good! Gate pass now!"). +**Component:** Vulkan PView / nested building look-ins / landscape alpha ordering. + +At the open-air Sanctuary cathedral seam between `0xF4180104` and +`0xF4180106`, small player or chase-camera movements could make cathedral +halves, their shadows, or floor textures flap; expose the world background, +trees, a nearby building, and waterfall/steam particles through opaque +geometry; or clip the local player in half. Camera zoom alone reproduced the +failure. The final particle repro was +`0xF4180104 [31.111177 57.648911 169.804993]`. + +This was a renderer contract failure, not bad Sanctuary data. The correction: + +- classifies exterior building seeds with retail's `F_EPSILON` instead of the + ordinary 1 cm EnvCell traversal tolerance and retains the exact accepted + `CBldPortal` aperture; +- appends every nested look-in cell's own `portal_view` to the frame clip + buffer, punches only the accepted seed portal, and clips shells plus static + and particle alpha to that view; +- pairs each look-in with only its own exterior building shell and reproduces + retail's per-building alpha barriers; +- keeps dynamic objects whole after the CPU PortalList sphere test, matching + retail `DrawMesh` and preventing the player-slicing regression; +- submits attached and ownerless exterior particles inside `LScape::draw` for + every PView root, eliminating the late post-world replay that let waterfall + alpha repaint already-drawn cathedral cells; and +- treats authored `SeenOutside` open-air cells as atmospheric outdoor cells so + sky/shadow activation no longer flips merely because the camera acquired an + EnvCell root. + +The fix is renderer-wide: no Sanctuary coordinate or asset special case is +present in production. Installed-DAT regressions retain the reported camera +handoff, lateral transition, and exact steam-seam views. The temporary live +emitter/shell trace was removed at closeout. + +**Acceptance:** owner swept both cells, moved across the seam, and zoomed the +camera through the former trigger positions. Cathedral/world flapping, shadow +toggle, player clipping, exterior geometry, and waterfall/particle bleed were +absent in the accepted build. + +## #450 — Fast character re-entry after logout can remain in portal space at `lb 0/0` + +**Status:** DONE — OWNER-ACCEPTED 2026-08-26 in the combined client-parity gate. +**Component:** session reset / streaming-origin retirement / login reveal. + +After Shift+Escape logout to character selection, immediately entering the +character again could leave the client indefinitely in portal space with no +landblocks admitted. The server accepted the second entry; the client title +remained at `lb 0/0`. + +**Root cause/fix:** confirmed logout starts a frame-budgeted retirement of the +old streaming window, but the synchronous session reset ignored its incomplete +result and exposed the fresh Runtime generation. The new world then inherited +the old origin-recenter admission gate. The confirmed-logoff pump now holds the +authored tunnel until old-window retirement converges, then transfers that +completed barrier through the reset callback exactly once. A deterministic +regression proves the character-select handoff cannot execute while retirement +is incomplete and cannot begin a duplicate retirement during reset. + +**Acceptance:** Shift+Escape to character selection, immediately re-enter, and +confirm the destination begins admitting landblocks and exits portal space. +Repeat twice in one process. + +## #449 — Main backpack remains falsely full after an item slot is freed + +**Status:** DONE — OWNER-ACCEPTED 2026-08-26 in the combined client-parity gate. +**Component:** inventory drag acceptance / main-pack capacity. + +With a full backpack, a move into it correctly shows the red reject cursor. +After dropping an item to free a slot, later moves from another pack could +remain rejected. + +**Root cause/fix:** main-pack fullness counted every child of the player, +including side bags, even though retail places side bags in a separate +container-selector list governed by `ContainersCapacity`. Capacity fill, +append placement, and drag acceptance now count only visible loose contents. +A regression starts with two loose items plus a side bag at capacity two, +removes one loose item, and proves the next drag changes from Reject to Accept +with a 50% capacity meter. + +**Acceptance:** fill the main pack, observe one rejected move, drop one loose +item, then move an item from a side pack into the freed main-pack slot. It must +accept immediately without reopening the inventory window. Run with the +combined gate in `docs/research/2026-08-26-combined-client-parity-gate.md`. + +## #448 — Outgoing melee hit messages expose a percentage that retail does not print + +**Status:** DONE — OWNER-ACCEPTED 2026-08-26 in the combined client-parity gate. +**Component:** combat chat / AttackerNotification presentation. + +Successful outgoing melee hits currently print a percentage in chat, for +example `You hit ... for ... damage (54.0%).` The owner reports that this is +not retail behavior and that the percentage should not be shown. + +**Likely seam:** `CombatChatTranslator.HandleDamageDealt` unconditionally +appends `DamageDealt.DamagePercent`; its tests explicitly pin a template taken +from holtburger rather than the named retail client. Recover the exact +AttackerNotification presentation from the named retail decomp/string tables, +then replace the formatter and its tests. Preserve the wire value in combat +state if it has another legitimate consumer; this issue concerns chat output. + +**Acceptance:** ordinary and critical outgoing melee hit lines match retail +wording and punctuation exactly and contain no acdream-added percentage. + +## #447 — `@acecommands` produces blank lines in the chat window + +**Status:** DONE — OWNER-ACCEPTED 2026-08-26 in the combined client-parity gate. +**Component:** ACE server-command responses / chat presentation. + +Running `@acecommands` against the test server produces a series of blank +chat lines instead of the command names and descriptions. The command reaches +ACE, but its multiline response loses its visible text before presentation. + +**Investigation seam:** capture the authoritative response message type and +raw payload, then trace it through the server-command/interface-text parser, +`RuntimeCommunicationState`, and retained chat markup rendering. Do not work +around the defect by printing the static `docs/reference/ace-commands.md` +copy; the live server response must render correctly. + +**Acceptance:** `@acecommands` displays every non-empty server response line +inside retail's retained transcript window with its text intact (newest +complete-line tail when the response itself exceeds the cap), produces no +blank-line spam, and does not regress normal chat or other ACE commands. + +**2026-08-26 fix:** ACE sends the complete command listing as one `0xF7E0` +`ServerMessage` containing embedded newlines. The parser and runtime route +already preserved that payload. The retained transcript budget treated the +whole message as one indivisible log entry, however, so an admin-sized reply +larger than retail's `0x2710`-character cap advanced past the only entry and +rendered nothing. `ChatTranscriptRenderer` now clips an oversized boundary +entry at a newline and keeps its newest complete lines, matching retail's +front-truncation behavior. Ordinary multiline replies below the cap render +every authored line. Parser round-trip, normal multiline, oversized response, +filter, tagged-run, and existing chat regression tests pass. Owner check is in +`docs/research/2026-08-26-combined-client-parity-gate.md`. + +## #446 — Configure Keyboard bindings need an end-to-end retail-parity pass + +**Status:** DONE — OWNER-ACCEPTED 2026-08-26 in the combined client-parity gate, +including connected behavior and persistence. +**Component:** input / Configure Keyboard / binding persistence. + +The owner reports that keyboard binding still does not work reliably or match +retail. Treat this as an end-to-end product gate rather than another isolated +layout fix: display the authored mappings, capture a replacement key or mouse +button, apply the correct retail conflict rules, make the new action fire, +and preserve it across restart. Escape cancellation, Reset/Defaults, scoped +combat bindings, modifier chords, and mouse bindings must also match retail. + +Existing issue #373 is one known concrete defect in this flow: acdream ignores +the DAT `ActionMap.ConflictingMaps` table and can erase valid shared combat +bindings. The fixes recorded under #394-#396 remain pending a complete owner +re-gate and do not establish that binding works end to end. + +**2026-08-26 implementation:** deep audit at +`docs/research/2026-08-26-retail-keyboard-routing-audit.md`. Bare Escape no +longer exits player mode or exposes the orbit/developer bird's-eye camera. It +now follows the complete proven retail ladder: finish jump charge, release +focused UI, stop movement/repeat attack, cancel target mode, clear selection, +then toggle the authored Gameplay Options page. Shift+Escape reaches the real +logout gate. + +All 306 installed ActionMap rows now have distinct live identities and enabled +Configure Keyboard rows. Exact defaults, contexts, activation, DAT conflict +policy, modifier-only and mouse capture, duplicate-chord multicast, explicit +unbinding, dense two-slot insertion, same-row no-op, unsupported-input retry, +priority conflict/non-bindable dialogs with exact DAT text, dirty-only Revert, +Apply/Defaults/OK/Cancel, schema migration, and persistence are implemented. +The complete camera, selection, missile, magic, 87-emote, screenshot/help/ +plugin, quickslot 1–18, panel/chat, and 48 CharacterSettings families reach +concrete consumers. Selection includes retail radar/combat/fellow/vendor/ +environment and session opened-corpse rules. The approved 40 m mouse-wheel +chase zoom remains unchanged and regression-pinned. + +Retail's Load File / Save As path is now live as well: the client parses and +writes the Sept-2013 PFile `.keymap` grammar under +`Documents\Asheron's Call`, remembers the selected profile, presents the +authored type-7 file menu and type-5 filename/overwrite dialogs, loads it on +startup, and rewrites it on graceful shutdown like retail. `keybinds.json` +remains a compatibility mirror for acdream-only commands. AP-202 is retired. + +Automated keyboard-impact evidence is green: App 6,413/6,413, Core +4,713/4,713, Runtime 1,849/1,849, and UI.Abstractions 879/879 (13,854 +tests total). Installed-DAT conformance pins all 306 identities, defaults, the +authored Configure Keyboard mount, and active Load/Save controls. Only the +connected gate below remains. + +**First owner-round findings fixed 2026-08-26:** modifier-only capture now +normalizes LeftShift and consistently raises the retail overwrite prompt when +Move Forward conflicts with Toggle Walk/Run. Regular Enter enters chat without +its raw event immediately submitting the new field; keypad Enter no longer +falls through to the raw chat-focus shortcut. Melee height keys now preserve +the Press→held charge→Release transaction instead of treating the first Hold +tick as release. Map mode transforms both retail's target direction and viewer +offset through the target frame, placing the eye high overhead rather than low +behind the character; the approved mouse-wheel zoom range is unchanged. +Shift+Escape's same-process relog portal stall is tracked and fixed as #450. +Focused App coverage plus the standard Release lane pass. + +**Acceptance:** a connected retail side-by-side covers representative movement, +combat, panel, modifier, and mouse mappings; every rebound action executes, +conflicts match retail, cancellation changes nothing, and applied bindings +survive a fresh client launch. + +## #445 — Stack split errors in inventory; vendor drag ignores selected quantity + +**Status:** DONE — OWNER-ACCEPTED 2026-08-26 in the combined client-parity gate. +**Component:** inventory stack splitting / vendor sell staging / shared split +quantity. + +Two live paths fail after selecting a partial quantity with the stack slider: + +1. Splitting a stack within the inventory produces an error instead of moving + the selected quantity into the destination slot. +2. With a stack of 10 and the slider set to 2, dragging the stack into the + vendor window stages all 10 rather than the selected 2. + +**Expected:** the selected quantity is the single shared value consumed by +inventory split operations and by the vendor drop path; the source retains +the remainder. Capture the exact inventory error text/code during the fix +gate. + +**Investigation seam:** trace `StackSplitQuantityState` from selection/slider +changes through the inventory `SendStackableSplitToContainer` request. The +vendor path currently documents and implements full-stack sell staging in +`VendorUiController.EvaluateSellAcceptability`; compare that claim against +named retail and a retail client gate before changing it, then make the +observed behavior and documentation agree. This is distinct from #313, which +only tracks selection transfer to the newly created split result. + +**2026-08-26 fix:** named retail's enclosing +`VendorSellUI::AcceptDragObject @ 0x004C4F00` disproved the old full-stack-only +comment. A partial vendor drop now sends the exact slider quantity through the +canonical inventory transaction owner, stages the source as retail's temporary +row, and replaces that row in place when the server-created stack with matching +WCID/quantity arrives. A matching failure removes the placeholder. Ordinary +inventory splitting now uses that same owner and computes empty main-pack +placement from visible loose items, excluding side bags that live in retail's +separate selector list. Exact quantity, request lifetime, replacement order, +and side-bag placement are regression-tested. Owner check is in +`docs/research/2026-08-26-combined-client-parity-gate.md`. + +## #444 — Vendor alternate-currency balance stays stale after a successful purchase + +**Status:** DONE — OWNER-ACCEPTED 2026-08-26 in the combined client-parity gate. +**Component:** vendor UI / alternate-currency purchase refresh. + +At a vendor that accepts an alternate currency (observed with Colosseum +Coins), the purchase succeeds and the server removes the currency, but the +vendor window continues to show the pre-purchase holding. Example: the purse +line says "You have 10 Colosseum Coins" before the purchase and still says 10 +afterward. The displayed holding should update immediately after the +authoritative purchase/inventory update. + +**Likely seam:** `VendorUiController.BuildPurseText` and `BuildCostText` read +the vendor-open snapshot `VendorShopProfile.AlternateCurrencyAmount` +directly. `OnObjectMoneyChanged` repaints the text, but the repainted value is +still that latched profile amount rather than the live alternate-currency +holding (or retail's `trade_num - m_last_sale` equivalent). Add a connected +regression for purchase success followed by the refreshed purse and item-cost +text; cover both the Buying tab and Items tab. + +**2026-08-26 fix:** alternate-currency displays and Buy All affordability now +prefer the authoritative sum of player-owned currency stacks. On a successful +Buy/Buy All dispatch, retail's `m_last_sale` subtraction updates the Items and +Buying/Selling purse text immediately; the next matching currency add/update/ +move/remove clears that optimistic subtraction and repaints from canonical +inventory. The vendor snapshot remains only the pre-observation fallback. +Automated coverage pins the immediate 10→8 display and the subsequent +authoritative 8→8 reconciliation. Owner check is in +`docs/research/2026-08-26-combined-client-parity-gate.md`. + +## #443 — Examination/paperdoll private viewport: doll appears only after a delay on first open (was: "renders nothing") + +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 +composites could still be pending when the private pass cleared and published +its target, and Vulkan's two concurrently recorded frames reused that same +offscreen image as both a color attachment and a retained-UI sampled texture. +The 2026-08-26 combined client-parity gate passed every #444–#450 row on the +same Release binary while the paperdoll remained missing, confirming #443 is +an isolated private-viewport defect rather than an inventory transaction, +input, relog, chat, combat-text, or vendor failure. +**Component:** private entity viewports (examination clone, inventory +paperdoll — shared `PrivateEntityViewportRenderer`). +**Filed:** 2026-08-25, AS-GF1 gate-fix session. **Narrowed same day at the +gate's probe round:** with `ACDREAM_PROBE_CREATURE_APPRAISAL_VIEWPORT=1` +(probe since DELETED per the probe-dies rule — recover it with +`git show 65f6f584` if this recurs) the live session showed the render +layer HEALTHY from the first probed frames — nonzero texture-table handle, +34 MeshRefs, sane bounds and camera eye, for both the examination clone +and the inventory paperdoll — while the owner initially saw an empty +(black) pane that later popped in ("I can see the paper doll now, after a +while"), and a subsequent fresh session showed it promptly. Leading +suspect: private-clone MESH residency/upload latency in the shared arena +(the pass records draws only for resident meshes; the probe cannot see +residency). The gate's OTHER symptom (the "black rectangle" band over the +extras rows at small window heights) is the authored scroll-less +clipped-list behavior AS-GF1 ruled retail-correct below, plus the clip +line moving with resize — owner-accepted at the gate. + +**Fix:** `PrivateEntityViewportRenderer.EntitySlot` now uses a two-phase +mesh-residency handoff. A replacement first acquires/pins its complete mesh +ownership set, remains pending while each drawable `MeshRef` crosses the +render-thread upload barrier, then atomically replaces the active entity and +texture-owner generation. While a replacement is pending the renderer keeps +the last completed viewport texture; on first open it publishes no texture so +the authored panel art remains visible instead of exposing a black-cleared +render target. The slot also detects GfxObj-id changes made in place by the +animated appraisal/chargen paths and supersedes stale pending owners without +leaking references. Focused paperdoll, appraisal, draw-order, synthetic-owner, +and new residency tests pass 30/30; the App hermetic lane passes 6,358/6,358. +The owner then live-verified repeated inventory and monster/player assessment +opens against the local ACE test server: "Good. works." + +**2026-08-26 recurrence fix:** the shared renderer now advances and gates the +complete private-entity resource set — mesh upload plus original, palette and +clothing-composite textures — before allocating, clearing, or publishing a +new viewport target. It therefore keeps the previous completed image (or the +authored panel art on first use) until the new doll is actually drawable. +`PaperdollFramePresenter` also builds, redresses and prewarms the inventory +doll while its tab is hidden, so opening the tab no longer starts residency +work from zero. The decisive intermittent fault was the shared render target: +one Vulkan flight slot could clear/write it while the other still sampled it. +`PrivateEntityViewportRenderer` now owns a bounded target, sampler and texture +slot per encountered GPU flight slot, and publishes the current frame's exact +handle. The same correction covers inventory paperdoll, creature appraisal and +character-creation preview viewports. Temporary flight-slot colors proved both +slots render the complete textured doll; all probes were then removed. The +clean Release client passed first open plus two repeated close/reopen cycles on +the local ACE server with no missing frame and no runtime error. Focused App, +Runtime and input tests pass 384/384, including the byte-exact production +SPIR-V oracle; the Release solution builds with zero warnings/errors. + +Owner report at the Campaign AS connected gate: the animated 3-D paperdoll +in the examination window (LayoutDesc `0x2100006B` element `0x10000148`) +worked correctly at baseline `974fe88a` (praised the same session) and was +gone by `87e98395` (ten commits later, AS2-AS5). The same gate also +reported "a reserved black rectangle at the window's bottom" and mid-row +clipping in the character extras list at the window's default (minimum +310x400) size, fixed by dragging the window taller. + +**Exhaustive investigation (this session) found NO code bug in the +Campaign AS diff for either symptom:** + +- Every file the AS2-AS5 window touches + (`AppraisalUiController.cs`, `RetailUiRuntime.cs`, + `CreatureAppraisalRows.cs`, `AllegianceRankTitleTable.cs` (new), + `CharacterIdentityText.cs`, `CharacterSheetProvider.cs`, + `InteractionRetainedUiComposition.cs`, plus two unrelated + mechanical `PublicWeenieFlags`-literal refactors) was read in full + against the pre-Campaign-AS baseline. +- A new hermetic regression test, + `AppraisalUiControllerTests.CharacterResponse_WorstCaseExtrasCombination_DoesNotThrowAndViewportGateStaysOpen`, + applies EVERY AS3+AS4 extras-list addition at once (armor-level trio, + society, allegiance cascade, ratings, all seven configurable extras) — + the combination none of the individual AS3/AS4 tests exercise together — + through the REAL DAT-derived examination layout and REAL row templates. + It proves `Apply`/`ApplyCreature`/`RebuildCreatureStats`/`BuildExtra` + never throw and always leave `ActiveView == Character`, + `CurrentObjectId != 0`, and the viewport's full ancestor-visibility + chain (`creaturePanel` → root) `Visible == true`, even in this worst + case. `RetailCreatureAppraisalFrameView.TryGetVisibleTarget`'s first + three gates (ActiveView, windowFrame visible, viewport visible) are + therefore unaffected. +- The SAME test also proves the extras list's clip-then-wheel-scroll + behavior is correct and unaffected: `UiItemList.OnEvent`'s + `UiEventType.Scroll` handler (pre-existing, unmodified) moves the + shared `Scroll` offset, and the next `LayoutCells()` pass reveals every + row, including the very last one of the 20-row / 400px worst case + against the DAT-authored 87px region. Retail's own LayoutDesc authors + NO scrollbar for this listbox either (`ScrollbarElementId == 0`, + verified against BOTH the committed fixture `tests/AcDream.App.Tests/ + UI/Layout/fixtures/examine_2100006B_100005F2.json` and a fresh + `tools/LayoutDump` read of the live installed DAT — no drift). A + scrollbar-less, wheel-scrollable list clipped to its authored region + until the user scrolls or resizes IS retail's actual, already-correctly- + ported mechanism — not a regression. +- `src/AcDream.App/UI/UiViewport.cs:52` draws NOTHING (not black) when its + `TextureSlot` is unassigned (`if (!Visible || !TextureSlot.IsAssigned) + return;`). The creaturePanel's own full-panel backdrop + (`0x10000141`, DID `0x06004CC2`, ZLevel 100 — the furthest-back layer, + spanning the panel's whole `300x365` rect) is what shows through + wherever nothing else paints over it. This is the most likely explanation + for BOTH the missing paperdoll AND the "black rectangle": if the + viewport's `TextureSlot` never gets assigned, this backdrop is what the + owner is actually seeing, and it is genuinely the SAME defect wearing + two descriptions, not two. +- `CreatureAppraisalPresentation.cs` and `LivePresentationComposition.cs` + (the entire render-time viewport pipeline: `TryGetVisibleTarget`'s + fourth gate `CurrentObjectId`, `TrySynchronize`'s live-entity/mesh + lookup, and the dispatcher/composition gate that constructs the + presenter at all) are byte-for-byte UNCHANGED across the whole + `974fe88a..87e98395` window (`git log -p` for both files is empty). + +**Conclusion after live recurrence:** the temporary probe ruled out every +higher-level gate: the target, clone, 34 MeshRefs, camera and nonzero texture +handle were all healthy while the pane was visibly empty. The shared slot had +published the clone immediately after `IncrementRefCount`, but that operation +only schedules asynchronous preparation/upload. The private pass then cleared +its target to black while `WbDrawDispatcher` skipped every nonresident mesh. +Residency/backlog timing explains both intermittent first-open delay and the +same symptom across inventory and monster/player examination. The probe was +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. +**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). + +A `GC.GetAllocatedBytesForCurrentThread()` zero-allocation assertion in +`tests/AcDream.App.Tests/Rendering/DirectionalShadowCasterFrameTests.cs` +trips intermittently under parallel load — same class as the known +`RuntimeCollisionReportingStateTests.WarmedSteadyContactRefreshDoesNotAllocate` +flake (full-solution parallel load only) and #439's headroom family. +Review isolation evidence (Release): alone (15-test class) passes; App +hermetic lane run 1 = 6,252/1 failed, run 2 (same command) = 6,253/0. +Last touched by `d78ce100` (a render commit). Candidate fixes when picked +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. +**Component:** reveal generation / death teleport / entity hydration + +Owner observed twice (before probes were armed), then could NOT reproduce in a +probed session the same day: + +1. **Run 1:** died, and the death teleport left the client **stuck in portal + space** (the authored tunnel never released). +2. **Run 2:** arrived at the lifestone while the world was **not ready** — + "missing doors, portals and stuff". Note: doors and portal objects are + server-spawned WEENIES, not dat-baked statics + (`claude-memory/feedback_weenie_vs_static.md`) — so this symptom may be + **entity hydration lagging the viewport reveal** (CreateObject stream not + yet drained when the reveal completed), not a composites/terrain gap. + Possibly related to #433 (stale entities near Holtburg after /ls-style + teleports — the flush direction of the same coin). + +**Owner's repro protocol (worked twice pre-probe, then went cold):** log in, +position far from the lifestone, log off, log on again, get killed. The +fresh-login-then-die sequence appears to matter. + +**Probe recipe (armed and documented, ready for the next occurrence):** launch +with `ACDREAM_PROBE_REVEAL=1` + `ACDREAM_PROBE_REVEAL_TIMING=1` and keep the +log. The always-on `[world-reveal]` lines plus `[reveal-timing]` per-edge +attribution distinguish the two shapes directly: symptom 1 = a readiness edge +that never arrives for the death generation; symptom 2 = `complete` firing +with the object stream still behind. + +**Healthy baseline (2026-08-24, `launch-death-repro-3.log`):** login gen and +two portal gens all converge; readiness edges complete in 83-1400 ms; +`totalMs` ≈ 5.5-5.7 s of which the balance after `materialized` is the +authored portal-tunnel presentation, not a stall. + +## #440 — CLOSED: Trained skill did not move to its section (and raise buttons never visually un-ghosted) until the next click + +**Status:** CLOSED 2026-08-24, found by the owner during the CA5 drive. +**Component:** character panel / row refresh + +**Symptom (owner):** training an untrained skill consumed credits (visible +immediately) and the confirmation text appeared, but the skill stayed in +the untrained section; clicking Train AGAIN (which the server rejects — +"Failed to train", no credit change) made it move. + +**Root cause:** the panel's sheet-changed subscription only refreshed the +captured `currentSheet` — per-frame text pulls (credits, values) updated, +but the ROW STRUCTURE (section buckets, selection, raise-button states) +rebuilt only on clicks (`RefreshAfterRaise` ran as the raise's `completed` +callback — synchronously after SEND, before the server's answer). The +second click was simply the first row rebuild after the record landed. +The same gap kept CA4's awaiting-ghost from visually releasing. + +**Fix:** `CharacterStatController.Bind` now returns the data-changed +refresh (`RefreshAfterRaise(null)` — rebuild + reselect + re-evaluate +buttons), and `RetailUiRuntime.MountCharacter`'s subscription invokes it on +every authoritative sheet change — mirroring retail's quality-change +broadcast (`InfoRegion::OnQualityChanged @ 0x004F0EB0` → +`ListenToElementMessage @ 0x004EFBE0`). Pinned by +`DataChangedRefresh_MovesATrainedSkillToItsSection_WithoutAClick`. + +--- + +## #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. +**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 + +**Symptom:** `LossyTransportDecoratorTests.LossySession_FivePercentSeeded_ZeroMessageLoss_Headroom256` +failed once under the full parallel hermetic suite with `Headroom expected 256, actual 0` +— i.e. the seeded loss-recovery simulation ended before the crypto search +window recovered. Passed in isolation immediately after, and the full +suite passed clean on re-run. The test drives a deadline loop over a +seeded end-to-end loss simulation — the load-sensitive shape +`Lane=Timing` exists for, but this test is not marked. Candidate fix: +either mark it `Lane=Timing` or make its quiescence wait +deadline-independent. Decide deliberately; do not just re-run until +green. + +--- + +## #438 — Launcher crash-report bundles (WER dump capture + local bundle, no upload) + +**Status:** OPEN — designed, ready to pick up as a launcher slice +**Severity:** ENHANCEMENT (post-current-queue; owner-approved as upcoming +work 2026-08-24) +**Component:** launcher / crash diagnostics + +**Problem:** the launcher already records a truthful +`exited{reason:"crashed", code}` with bounded stderr (#405–#407), but for +native fail-fasts — #422's `0xC0000374` heap corruption, three sightings, +zero stacks — no evidence exists anywhere: fail-fast bypasses in-process +exception filters BY DESIGN, so .NET's own `DOTNET_DbgEnableMiniDump` +crash handler never fires for this class. Windows' WER LocalDumps is the +OS-level catcher that still does. Today an alpha user can say "it +crashed" but never why. + +**Design (agreed with the owner):** +1. **Launcher owns the WER key, with consent.** First-run opt-in ("save + crash dumps locally") writes + `HKCU\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\AcDream.App.exe` + (HKCU — no elevation): `DumpFolder` inside the launcher's data dir, + `DumpCount` small (e.g. 3), `DumpType=1` (**minidump**, not full — a + minidump carries the faulting stack #422 needs without carrying the + process's whole memory). Uninstall removes the key. +2. **Bundle on next launch.** The launcher already detects the crash exit; + it gathers minidump + client log tail + version + the Vulkan capability + report into one folder and shows "the client crashed last time — the + report is here" with an open-folder button. +3. **NO auto-upload — this is the deliberate line.** Dumps can contain + account names and the plaintext session password in memory; there is no + upload endpoint and no privacy story. The user choosing to send the + folder is consent by construction. Revisit only with real + infrastructure and an explicit consent flow. +4. **Linux:** set `DOTNET_DbgEnableMiniDump=1` / `DOTNET_DbgMiniDumpType` + from the launcher for the non-fail-fast classes; system `core_pattern` + is out of scope. + +**Prior art / constraints:** the project's stance that the CLIENT never +writes registry keys stands — this is launcher-owned, opt-in, per-user. +The owner's dev machine already has the key armed manually (2026-08-24) +for the #422 hunt; this slice productizes that for alpha users, whose +crashes are otherwise invisible telemetry. + +--- + +## #435 — CLOSED: Probe debt: temporary probes outlived their closed investigations or named no owner + +**Status:** CLOSED 2026-08-24 across two passes (`0c5057c9` part 1, +`c1e6e3da` part 2 + the `35454a9f`-adjacent doc correction): 24 probes +deleted, 8 reclassified as standing tools, 1 deleted-and-restored +(`ACDREAM_DUMP_MOVE_TRUTH`, #437 — the canonical soak hard-gates on it). +End state: 137 flags (from 161), 31 temporary probes, every one attributed +to an owning issue or campaign. The default-off invariant is frozen by +`LaunchOptionsDocumentationTests.OnlyTheFourRetailBehaviorFlagsDefaultOn`. +Part-1 record follows. + +**Status (part 1):** The 17 orphaned probes from part 1 are DELETED (2026-08-24) — +3,493 lines removed, flag count 161 → 144, temporary probes 64 → 47. Build +clean; full hermetic suite 15,321 passed / 0 failed (baseline 15,333 minus +the 12 tests whose only subject was a deleted probe). Four files went +entirely: `WalkMissDiagnostic.cs`, `CollisionMeshWireframe.cs` and two +probe-only test files. `LaunchOptionsDocumentationTests` did its job during +the cleanup — it refused the deletion until the doc's rows moved to Retired +and the frozen direct-read counts were lowered (`PhysicsEngine.cs` to zero, +`TransitionTypes.cs` 3 → 2). + +Notable: `TransitionTypes.SetContactPlane` shed its `CallerMemberName` / +`CallerLineNumber` parameters, which existed only for #337's `cpSrc=` +attribution and were explicitly marked "strip with the probe family". No +call site passed them, so no behavior changed. F2's collision overlay +survives and reverts to the proxy-cylinder form, as intended when +`ACDREAM_WIRE_MESH` went. + +**Part 2 (2026-08-24):** traced each of the 14 then-unattributed rows to +its introducing commit and confirmed 8 belonged to closed investigations — +`ACDREAM_A8_DUMP_PV` (Phase A8.F, closed), `ACDREAM_DUMP_CLOTHING` (#37, +DONE 2026-05-11), `ACDREAM_DUMP_EDGE_SLIDE` (#32, CLOSED 2026-08-07), +`ACDREAM_DUMP_LIVE_SPAWNS` (Phase A8, closed), `ACDREAM_DUMP_MOVE_TRUTH` +(#30/#34, DONE 2026-04-29), `ACDREAM_DUMP_STEPUP` (L.2.3d/e/f, closed), +`ACDREAM_DUMP_VENDOR` (vendor campaign, closed 2026-08-08), +`ACDREAM_DUMP_VITALS` (#5, DONE 2026-04-25). All 8 deleted along with their +call sites (`~130` net lines across 21 files), plus `VendorDiagnostics.cs` +(the `ACDREAM_DUMP_VENDOR` owner class, now fully unreferenced) and +`MovementTruthDiagnosticController`'s internals (kept as a permanent +no-op `IMovementTruthDiagnosticSink` implementation — `PlayerModeController`, +`GameWindow`, `LiveEntityNetworkUpdateController`, and +`SessionPlayerComposition` all still construct/wire it, so the DI graph is +unchanged). `docs/launch-options.md` rows moved to Retired; +`LaunchOptionsDocumentationTests`' `DirectReadDebt` lowered for +`PortalVisibilityBuilder.cs` (1→0, dropped), `GameEventWiring.cs` (1→0, +dropped), `PlayerDescriptionParser.cs` (1→0, dropped), +`TransitionTypes.cs` (2→0, dropped), `WorldSession.cs` (3→2). Build clean, +0 warnings; full filtered suite 15,315 passed / 0 failed / 0 skipped (no +`[Fact]`/`[Theory]` was removed — the small delta from part 1's 15,321 +baseline is pre-existing run-to-run count variance, not a test loss). + +**Found during part 2, not fixed (separate from #435's scope):** see #437 +— `tools/run-connected-r6-soak.ps1` sets `ACDREAM_DUMP_MOVE_TRUTH=1` and +greps its own log for `move-truth OUT` lines as part of its automated +movement-verification gate. That signal is now permanently dead (the flag +is a no-op), but the ps1's own text is unchanged so it and its pinning +contract test (`ConnectedWorldSoakRouteContractTests. +LaunchConfigurationIsDisclosedToTheArtifactDirectoryBeforeLaunch`) both +still pass — they assert the ps1's *text*, not that the mechanism it +describes still works. + +**STILL OPEN — the 6 remaining unattributed probes** (`ACDREAM_DUMP_CELLS_DIR`, +`ACDREAM_DUMP_GFXOBJS_DIR`, `ACDREAM_DUMP_SKY`, `ACDREAM_DUMP_STEEP_ROOF`, +`ACDREAM_HIDE_PART`, `ACDREAM_PROBE_CELL` — all deliberately left alone +this pass per the #435 part-2 scope). They name no owning issue, so nothing +records when they are safe to remove. Deleting them on a guess is how a +future investigation loses apparatus it needed. The right next step is +attribution, not deletion: for each, find the commit that introduced it +(`git log -S ACDREAM_PROBE_X`), record the issue in its +`docs/launch-options.md` row, and only then decide. Deliberately deferred. + +**Original report follows.** + +**Status (original):** OPEN +**Severity:** LOW (no runtime defect; hot-path clutter and measurement noise) +**Filed:** 2026-08-24 (measured during the launch-options audit) +**Component:** diagnostics ownership + +**Measurement (2026-08-24, `docs/launch-options.md`):** the client reads 161 +`ACDREAM_*` variables. 64 are temporary probes. They cite 21 distinct +issues, of which **14 are already closed** — 17 probe rows are apparatus +whose investigation ended without the strip: + +| Closed issue | Probes that outlived it | +|---|---| +| #337 | `ACDREAM_PROBE_SUPPORT`, `ACDREAM_WIRE_MESH`, `ACDREAM_WIRE_RADIUS` | +| #119 | `ACDREAM_DUMP_ENTITY`, `ACDREAM_PROBE_VIEWER` | +| #32 | `ACDREAM_PROBE_REMOTE_LANDING` | +| #42 | `ACDREAM_AIRBORNE_DIAG` | +| #63 | `ACDREAM_PROBE_AUTOWALK` | +| #78 | `ACDREAM_PROBE_SHELL` | +| #83 | `ACDREAM_PROBE_WALK_MISS` | +| #105 | `ACDREAM_PROBE_TEXFLUSH` | +| #113 | `ACDREAM_PROBE_PHANTOM` | +| #131 | `ACDREAM_PROBE_OUTSTAGE` | +| #133 | `ACDREAM_PROBE_LIGHT` | +| #171 | `ACDREAM_PROBE_STICKY` | +| #334 | `ACDREAM_PROBE_REACH` | +| #338 | `ACDREAM_PROBE_STEP_HEIGHTS` | + +A further **14 temporary rows name no owning issue at all**, which is worse: +nothing records when they become safe to delete. + +**Why it matters:** each probe leaves a branch on its hot path even unset, +several re-read the environment per call rather than caching +(`ACDREAM_WB_DIAG` on every `Draw()`, `ACDREAM_DUMP_SURFACES` every render +frame until it fires, `ACDREAM_AIRBORNE_DIAG` per airborne resolve), and +the sheer count makes the real diagnostic surface hard to find. This is +also a correctness risk for headless: `HeadlessStaticStateAudit` +reflects over `PhysicsDiagnostics`' probe flags and refuses a multi-session +host when any is set, but it cannot see the probes that live outside that +owner class. + +**Fix shape:** per probe, confirm its issue is closed and no gate script +references it, then delete flag + read sites + doc row together. The +launch-options row is the checklist — `LaunchOptionsDocumentationTests` +fails if a row survives its read site, so the doc cannot drift during the +cleanup. Do NOT bulk-delete: a few (e.g. `ACDREAM_DUMP_ENTITY`) are +consumed by a second, unrelated probe (`ACDREAM_PROBE_OUTSTAGE` reuses its +id list), so deletion order matters. + +--- + +## #436 — CLOSED: Combat refusal text ("No monster target") is silently dropped + +**Status:** CLOSED 2026-08-24, retail-faithfully. Ghidra decompile of +`ClientCombatSystem::ExecuteAttack` (0x0056bb70) settled both open +questions: (1) retail's exact string is **"You must select a valid combat +target before attacking"**, routed via +`ClientSystem::AddTextToScroll(..., 0x1A, true, 0)` — the ClientLocal +SpewBox channel our chat pipeline already owns; (2) retail has ONE message, +not two — attacking while not in melee/missile mode is silent (that path is +unreachable in retail's dispatch), so the invented "Enter melee or missile +combat first" text is deleted rather than rerouted. Implementation: the +string joined `ClientTextRefusals` with its decomp citation; +`CombatFeedbackSlot` gained the sibling `BindOwned` session-lifetime shape; +`SessionPlayerComposition.CompleteSessionPlayer` binds it to +`RuntimeCommunicationState.AddText(ClientLocal)` with session-owned +teardown. Coverage includes a binding-seam test +(`CompleteSessionPlayerBindsCombatFeedbackToTheClientLocalSpewBoxRoute`) +so the slot can never again pass its unit tests while production leaves it +unbound — the exact failure mode that hid this for months. + +**Original report follows.** + +**Status (original):** OPEN +**Severity:** MEDIUM (missing user feedback on a common action) +**Filed:** 2026-08-24 (exposed by #434's dead-code removal) +**Component:** combat / chat presentation + +**Symptom:** `LiveCombatAttackOperations` produces two refusal messages — +`"Enter melee or missile combat first"` and `"No monster target"` — and +hands them to `CombatFeedbackSlot.Show`. Nothing in `src/` ever binds a +target to that slot, so **both messages go nowhere**. Attacking with no +target, or without a combat mode, gives the player no explanation at all. + +**How it got here:** the slot's binding target used to be the developer +`DebugVM`, which Campaign V slice V11 left unreachable (see #434). Nothing +noticed because the drop is silent — `Show` is a null-conditional invoke. +#434 converted the seam to a plain `Action` so it no longer depends +on deleted code, and pinned the current drop-on-the-floor behavior in +`CombatFeedbackSlotTests.AnUnboundSlotDropsItsMessages` so the day it gets a +real binder, that test is what changes. + +**Fix shape:** route the slot to the chat window, where retail puts this +text. Needs the retail oracle first: confirm the exact strings and their +LogTextType/channel (`claude-memory/project_chat_digest.md` has the color +and channel map) rather than inventing wording — retail's own text may +differ from these two placeholder strings. + +--- + +## #437 — CLOSED: The R6 soak's movement-verification signal died with a deleted probe — resolved by RESTORING the probe + +**Status:** CLOSED 2026-08-24, same day, by restoration — not by choosing a +replacement signal. On review, the deletion premise was wrong: +`ACDREAM_DUMP_MOVE_TRUTH` is not a spent investigation probe but +**automation apparatus** — `run-connected-r6-soak.ps1` (the canonical +nine-stop soak) HARD-FAILS every destination when `$moveTruthDelta < 2`, +with a message that would misdirect the next operator ("production +movement did not deliver outbound records" when in truth the diagnostic +was deleted). Under the no-workarounds rule the honest fix is to restore +the mechanism the gate depends on, not to leave the gate broken with an +IOU. Restored in full: `MovementTruthDiagnosticController`, +`RuntimeOptions.DumpMoveTruth` (now carrying a comment naming the soak +dependency), the GameWindow wiring, the RuntimeOptions tests, and the +gate-script allow-list entries. Its `docs/launch-options.md` row moved to +the Automation section with the dependency spelled out. Process lesson, +recorded on #435: "its issue is closed" is NOT sufficient to delete a +probe — grep `tools/` and the contract tests for consumers first (the +original #435 part 1 did this; part 2's dispatch omitted it). + +**Original report follows.** + +**Status (original):** OPEN +**Severity:** LOW (measurement-tooling gap, not a client defect — the gate +still runs and its OTHER checks still verify real behavior) +**Filed:** 2026-08-24 (found while closing #435 part 2) +**Component:** connected-gate tooling (`tools/run-connected-r6-soak.ps1`) + +**Symptom:** `run-connected-r6-soak.ps1` sets `ACDREAM_DUMP_MOVE_TRUTH=1` +before launch, then during each destination's checkpoint asserts the log +contains at least two fresh `move-truth OUT` lines +(`Wait-ForLogPattern $Client $stdoutLog 'move-truth OUT' ...`) and reports +a `$moveTruthDelta` count — this was one of several signals the script uses +to prove the client actually issued outbound movement during the run. +`ACDREAM_DUMP_MOVE_TRUTH` was retired in #435 part 2: +`MovementTruthDiagnosticController` is now a permanent no-op, so +`move-truth OUT` can never appear in the log again. The script still runs +(its forward/jump/combat input-dispatch checks are independent and still +real), but the movement-specific corroboration silently stops meaning +anything — a soak that broke outbound movement entirely would no longer be +caught by this particular check. + +**Why it wasn't fixed inline:** the fix requires choosing a NEW signal to +verify outbound movement happened (e.g. a different existing log line, a +wire-level assertion, or a purpose-built lightweight counter) — a design +decision outside the scope of a probe-deletion pass, not a mechanical +rename. `docs/ISSUES.md`/CLAUDE.md's "no workarounds without approval" rule +applies: inventing a replacement signal without checking it against what +the script's other checks already prove would risk a false sense of +coverage. + +**Corroborating detail:** `ConnectedWorldSoakRouteContractTests. +LaunchConfigurationIsDisclosedToTheArtifactDirectoryBeforeLaunch` +(`tests/AcDream.App.Tests/Diagnostics/ConnectedWorldSoakRouteContractTests.cs`) +asserts `$env:ACDREAM_DUMP_MOVE_TRUTH = '1'` appears in the ps1's source — +a text-content assertion, not a runtime one, so it still passes and gives +no signal that the mechanism died. + +**Fix shape:** decide on a replacement runtime signal for "the client +issued outbound movement" (candidates: an existing non-diagnostic log line +already emitted by the movement pipeline, or a small permanent counter +exposed through an existing owner class), wire it into the soak script's +per-destination checkpoint in place of the `move-truth OUT` grep, and +update the disclosure-contract test to match. + +--- + +## #434 — CLOSED: The DebugPanel/DebugVM developer surface is unreachable, and ~40 doc comments still advertise it as live + +**Status:** CLOSED 2026-08-24. Deleted `DebugPanel.cs` (340 lines), +`DebugVM.cs` (548) and `DebugVMTests.cs` (327) — 1,215 lines. Converted the +one real dependant (`CombatFeedbackSlot`) to a delegate seam, which exposed +#436. Corrected every false "runtime-toggleable via the DebugPanel" claim in +`PhysicsDiagnostics`, `RenderingDiagnostics`, `CameraDiagnostics`, +`PhysicsEngine` and `GameWindow`. Full hermetic suite 15,333 passed / 0 +failed. **Deliberately left alone:** F1's `AcdreamToggleDebugPanel` binding, +which `GameplayInputCommandController` consumes as a documented no-op on +purpose (so the key does not fall through to a lower scope); and the +`DebugVmRenderFactsPublisher` / `DevToolsRuntimeSources` chain, which is +still wired into production composition and needs its own dead-code pass +rather than being dragged into a documentation cleanup. + +**Original report follows.** + +**Status (original):** OPEN +**Severity:** LOW (no runtime defect; a documentation-truth and dead-code problem) +**Filed:** 2026-08-24 (found during the launch-options audit) +**Component:** UI.Abstractions / diagnostics ownership + +**Symptom:** nothing in `src/` ever constructs `DebugPanel` or `DebugVM` +(`src/AcDream.UI.Abstractions/Panels/Debug/`). Their ImGui frontend was +deleted at Campaign V slice V11 — `SettingsDevToolsComposition.cs:13` +says so explicitly. Only `tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs` +instantiates them. Consequences: + +1. The ~35 `ACDREAM_*` reads inside `DebugPanel.cs`/`DebugVM.cs` are + unreachable in production (they only initialize the mirror state of a + panel that never exists). The flags themselves stay live — every one is + also read by its diagnostics owner — so no launch option is lost. +2. **Every "runtime-toggleable via the DebugPanel" claim in + `PhysicsDiagnostics`/`RenderingDiagnostics` XML docs is false.** Those + flags are startup-only today. `docs/launch-options.md` deliberately does + not repeat the claim; the owner-class comments still do. +3. `DebugVM` is still referenced as a TYPE by + `LiveCombatAttackOperations.Bind/Unbind` and + `DebugVmRenderFactsPublisher`, so those bind paths can never receive a + real instance. + +**Why not fixed with the audit:** deleting the pair is a real refactor +(test project churn plus two live type references), not a documentation +edit. Kept separate deliberately. + +**Fix shape:** either delete `DebugPanel`/`DebugVM` with their tests and +the two dead bind seams, or re-host them on the retained retail UI. Then +sweep the owner-class doc comments for "runtime-toggleable" and either +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. +**Severity:** MEDIUM (visibly wrong world state after ordinary travel) +**Filed:** 2026-08-24 (owner report) +**Component:** entity lifecycle / landblock retirement / reveal generation + +**Symptom (owner):** sometimes, after portaling around or using `/ls` +(lifestone recall) while close to Holtburg, monsters and other objects +from OTHER landblocks are visible hanging in the air — as if the old +world's entities were never flushed. Intermittent; "sometimes." + +**Relationship to #432:** NOT its cause — #432 reproduces on a fresh +login parked at town center with no prior travel. But both may share a +residency root (content near Holtburg staying resident/drawn when it +should be retired). The #432 DrawInside attribution probe should also +report WHOSE entities/cells the per-frame cost walks; if dead-generation +leftovers appear there, merge the investigations. + +**Where to look first:** the J6.2 canonical reveal-generation owner +(Runtime owns the sole reveal generation and old-world quiescence — +`docs/plans/2026-07-24-modern-runtime-architecture.md` Slice E/J6.2: +"generation-scoped old-world quiescence"), the entity teardown path at +generation reset (`RuntimeEntityDirectory` tombstones), and the +presentation sidecars (`LiveEntityProjectionStore`) — an entity drawn +without its landblock means the graphical sidecar outlived either its +runtime record or its cell residency. Distinguish: (a) runtime entity +alive but should be dead (server never sent destroy / we dropped it), +(b) runtime dead but presentation sidecar leaked, (c) entity correctly +alive but its OWN landblock geometry retired while inside the Far ring. +"Hanging in the air" + "other landblocks" suggests (b) or (c). + +**Repro lead:** portal arrivals and /ls near Holtburg; intermittent. +Capture: `ACDREAM_PROBE_CELL=1` + entity-ledger counts at the reveal +transition; a screenshot naming one floating guid would pin (a) vs (b) +immediately (F2 overlay shows guids). + +--- + +## #430 — CLOSED: No tooltips on skills and attributes in the character panel + +**Status:** CLOSED 2026-08-24 (found during the CA5 gate follow-up). +Root cause: the TS-85 Batch-B port set runtime `TooltipText` on the +runtime-built rows but never gave them a popup locator, and +`RetailTooltipPresenter.OnTooltipShow` refuses any widget with +`AuthoredTooltipRootElementId == 0` — the tooltip could NEVER mount. +(TS-85's "live-verified on the Character tab" was the OPTIONS panel's +Character tab — authored elements with authored locators.) Fix: rows now +carry the shared popup skin `0x10000395`/`0x21000041` — live-DAT probed as +the ONLY locator pair layout `0x2100002E` references, and the same +inference `UiItemSlot` already ships for runtime-built widgets. Pinned by +`Rows_CarryTheSharedTooltipPopupLocatorAndDescriptionText`. Owner visual +re-check owed at the next session (hover a row, hold the mouse still for +the 0.25 s dwell). +**Severity:** LOW (information affordance missing) +**Filed:** 2026-08-23 (owner report) +**Component:** retail UI / character panel + +**Symptom:** hovering skills and attributes on the character panel shows no +tooltip. Retail shows hover text for these rows (formula/experience detail). +Needs the retail oracle first: dump what retail's gmCharacterUI authors for +these elements (tooltip strings/DAT ids and the hover mechanism) before +implementing — the panel side likely needs a shared hover-tooltip surface +the LayoutDesc importer does not mount yet. + +--- + +## #431 — Raising an attribute does not refresh attribute-derived skills or run speed + +**Status:** PROMOTED to Campaign CA +(`docs/plans/2026-08-24-character-advancement-campaign.md`, 2026-08-24) +after the owner widened the scope to the whole advancement family: vitals +maxima updating on attribute AND direct vital raises, real-time skill +refresh on both attribute and skill raises, run speed responding to +Quickness, attribute-less skills (Salvaging), plus the untested +train/specialize/respec flows. Root cause confirmed at promotion: the +outbound raise actions (0x0044–0x0047) are fully wired — the INBOUND +private attribute/skill update family is parsed nowhere (only the vitals +pair 0x02E7/0x02E9 is), so the recompute trigger never arrives. +**Severity:** MEDIUM (visible stat incoherence during play) +**Filed:** 2026-08-23 (owner report) +**Component:** character state / stat chain / movement speed + +**Symptom:** raising an attribute (e.g. Quickness) leaves dependent skills +unchanged in the panel — Run should rise with Quickness — and the player +does not actually run faster afterwards. + +**Suspects, in order:** +1. Inbound stat-update handling: an attribute raise arrives as its own + private-stat update; if the client does not re-derive formula skills + (attribute-based base + trained credit) from the new attribute, the + panel keeps stale values. Check which 0x02Cx StatMod family messages + ACE sends on attribute raise and which of them we consume (TS-8's + 0x02C2 parse landed in Campaign P — verify the ATTRIBUTE variant and + any skill-recompute fanout). +2. Run-speed application: the `[stat-chain]` log line shows server + movement stats applied (`run=15230 ...`) — confirm whether ACE re-sends + run stats after an attribute raise and whether + `PlayerMovementController.ApplyServerRunRate` re-fires; if ACE only + recomputes on next login, the client-side derivation gap in (1) is the + whole bug for speed too. +3. Panel binding: if Runtime state DOES update but the character panel + row caches its value, it is a binding-refresh bug instead (test the + binding seam, claude-memory/feedback_test_the_binding_seam.md). + +Retail derivation is the oracle: grep named-retail for the skill formula +resolution (SkillTable/attribute-formula) before porting any recompute. + +--- + +## #429 — CLOSED: Periodic hitch in the LOCAL player's own motion while running (~2-3 s cadence) + +**Status:** CLOSED 2026-08-24 — owner-accepted in both presentation modes +("Feels good capped too"). Fixed across three commits: `0330fcd0` +(allocation-exact streamed-mesh completion), `4873c106` (presented +player + chase camera share the object clock), `ad695589` +(allocation-free shadow topology rebuild + churn-frame pipelining). +Final measured state: stall frames 5.8/s → ~0.45/s uncapped (0.49/s +capped), median stall 20.3 → 13.7 ms, camera-vs-player decoherence +~1 m → 0.2–1.2 cm median. Residual content-proportional rebuild cost is +the incremental-topology successor campaign; the town-view latch found +during measurement is #432. +**Severity:** MEDIUM (noticeable during ordinary play) +**Filed:** 2026-08-23 (owner report during the night-sky session) +**Component:** movement / prediction / server reconciliation + +**Symptom:** every ~2-3 seconds while RUNNING forward, the player's own +motion visibly hitches ("small lag, like spike"). Not present when walking +or strafing. Birds and every other world object stay perfectly smooth +through the hitch — so it is NOT a frame-time spike; it is the local +player's position stepping. + +**Owner refinement (2026-08-23, same session):** the hitch persists when +running noticeably slower, and also while running in circles (continuous +turning). Constant-heading-change running makes the AutoPos tracker send +MORE often (heading deltas trigger sends before the interval), so +"hitches in circles too" stays consistent with the correction-loop lead; +"slower run still hitches" weakens a purely speed-magnitude drift story — +the probe run (next probes, item 1) should log correction magnitude vs +speed to separate formula-mismatch from cadence-side effects. + +**Evidence gathered at filing:** +- 45 s `dotnet-counters` capture while reproducing (Atmospheric pack ON): + Gen0 GC every ~6.3 s (cadence mismatch), gen1/gen2 zero, %time-in-GC ~0 + — GC exonerated for the 2-3 s cadence. (Separate finding: the pack path + allocates a steady ~8 MB/s ≈ 130 KB/frame where the base client runs + near zero — worth its own cleanup issue.) +- Pack GPU-timer readback is non-blocking by design (VulkanGpuTimerPool + tolerates NotReady) — not a stall source. +- The owner initially observed "pack off → gone"; after the walk/strafe/ + world-smooth discriminators this is more plausibly a VISIBILITY effect + (lower FPS with the pack makes a 2 s position snap read as a lag spike). + +**Leading hypothesis:** client-predicted run speed vs ACE-authoritative +speed mismatch at high runRate (the test character applies server stats +run=15230 — deep in the ==800-sentinel family, see #266: retail's ==800 vs +ACE's >=800 misread), accumulating drift that the AutoPos/correction loop +snaps back on its cadence. Walking (speed 1.0 both sides) and strafing +accumulate little or no drift — matching the report exactly. + +**MEASURED 2026-08-23 (same session) — the story inverted twice and is now +pinned by data.** A per-frame probe (`PlayerPresentationProbe`, +`ACDREAM_PROBE_PLAYER_PRESENT=`, TEMPORARY, wired in +`WorldRenderFrameBuilder`/`RuntimeWorldFrameEnvironmentPreparation`) +captured `(t, camera, presented player position)` per frame over ~50 s +of running per arm (`artifacts/owner-gate/player-present-429-packON.csv` +/ `-packoff.csv`, ~270 FPS baseline, median frame 3.7 ms): + +- Frame stalls of 15-25 ms (max ~230-260 ms) occur ~1.7/s while moving in + BOTH arms — pack ON 88, pack OFF 83 — the Atmospheric pack does NOT + cause the stalls. The uncapped run reproducing the hitch had already + killed the #235 capped-alias theory. +- The pack changes what a stall LOOKS like: with pack ON, 59 of the 88 + long frames carry a 3x-5x player-position jump (the visible hitch); + with pack OFF only 3 of 83 do — the same stall lands at a pipeline + phase where the presented position has not advanced, so the player + stays visually smooth and the owner never perceived it. +- Right after ON-arm stall clusters, normal-length frames show ~zero + player motion (catch-up artifacts). The prediction/AutoPos-correction + hypothesis is DEAD: position advances exactly proportionally to + elapsed time through the stalls; no snap-back is present in the data. + +**Two separated defects:** +1. BASE CLIENT: periodic 15-25 ms frame stalls (~1.7/s while moving). + GC exonerated (Gen0 at 6.3 s cadence, gen1/2 zero). Suspects: + streaming publication/upload bursts on movement, present-path stalls. + Instrument with a per-stall stack/phase capture, not more theories. +2. PACK FRAME GRAPH: its ordering shifts the stall's position relative + to the physics commit/presented-position sampling, converting silent + stalls into visible player jumps. Establish where the ON-arm long + frames spend their extra time (the pack's CPU stage profiler names + stages) and where the player position is sampled relative to it. + +**ROOT CAUSE FOUND 2026-08-23 (frame-history run, 33,350 frames with stage +attribution — `ACDREAM_FRAME_PROF=1` + `ACDREAM_FRAME_HISTORY`):** of 708 +stall frames (>12 ms), 701 allocate 30-77 MB IN THAT SINGLE FRAME (normal +frames: ~22 KB, p99 94 KB). The time and the allocation sit on the render +path outside the tracked stages. The bursts arrive in ~8-frame clusters +during movement — the streaming MESH COMPLETION path: each frame completes +up to `MaxCompletionsPerFrame` (quality High = 4) newly streamed meshes on +the render thread, and each completion in +`ObjectMeshManager.UploadGfxObjMeshData` (~line 2043) runs LINQ chains — +`TextureBatches.Values.SelectMany(...).Select(b => b.Indices.ToArray()) +.ToArray()`, plus the retained pick-support copies +`CPUPositions = Vertices.Select(v => v.Position).ToArray()` and +`CPUIndices = ...SelectMany(...).SelectMany(...).ToArray()` — megabytes of +enumerator/intermediate-List garbage per mesh, tens of MB per frame. +Gen0 runs 70-145 collections per 5 s during movement (vs ~1/6 s idle). + +**Fix shape (a bounded slice, not a quickie — this is the production mesh +pipeline):** +1. De-LINQ the conversion: direct pre-sized loops for the index batches and + the CPU pick copies (sizes are known up front from the batch counts). +2. Consider byte-budgeted completions (4 huge EnvCell meshes is not the + same frame cost as 4 fence posts) and/or moving the CPU-side conversion + onto the existing mesh-preparation scheduler thread so the render thread + only adopts finished arrays. +3. Allocation-gate test in the I1 style: a completion of a representative + mesh set must allocate near its retained-copy size, not multiples of it. +The pack-ON player-jump phase question remains as the second defect but +becomes mostly moot once the stalls themselves shrink. + +**SITE ATTRIBUTION CORRECTED + FIXED 2026-08-23 (implementation session).** +The frame-history correlation (701/708 stall frames at 30-77 MB) was right; +the SITE was wrong. The baseline CSV's own stage columns refute the +mesh-completion theory: in all 701 alloc-correlated stall frames +`upload_us` is ~3 µs — `WbMeshAdapter.Tick` (which contains the entire +`UploadGfxObjMeshData` completion drain AND the mip flush, inside the +tracked Upload stage) did essentially nothing in those frames. A temporary +per-phase allocation probe (`GC.GetAllocatedBytesForCurrentThread` marks +through the render frame, driven by an automated Holtburg running route) +attributed the allocation exactly: + +- **THE stall allocator: `DirectionalShadowPreparedDraws.Complete`'s + `Array.Sort` comparer** (`WbDrawDispatcher.DirectionalShadows.cs`). + `x.Material.CompareTo(y.Material)` / `x.CullMode.CompareTo(y.CullMode)` + bind to `Enum.CompareTo(object)`, which boxes BOTH operands on every + comparison — measured at a constant **38.88 MB per directional-shadow + topology rebuild** (~4M boxes across the N·log N sort of every prepared + caster draw in the resident window). The topology rebuilds whenever + `RenderDataAvailabilityVersion` moves — i.e. on every streaming-churn + frame while the player runs (the Atmospheric pack's shadow prepass is the + consumer, matching pack-ON visibility; sustained churn windows rebuilt + 260 consecutive frames ≈ 10 GB of garbage in seconds). Everything else + in the rebuild (caster copy, classification loop, grouping) measured + ~0.3 MB — the shadow stack's retained-scratch design was already sound; + two enum comparisons were the whole leak. +- Secondary (the original theory, real but ~7 of 708 baseline frames): + the `UploadGfxObjMeshData` LINQ conversion, 8-14 MB on completion + frames. +- Also observed while probing, pre-existing and bounded, NOT #429: + composite-texture warmup (`TickCompositeTextureCache`, 16/frame budget) + and PView reveal churn allocate ~4-30 MB on teleport/reveal frames. + +**Fix landed (pending owner gate):** +1. The sort comparer compares enums through their underlying integers — + allocation-free, identical ordering. Verified live: the 38.88 MB + rebuild signature is gone (big-alloc frames on the automated running + route: 124 → ~0 shadow-rebuild frames; only the pre-existing + composite/pview/reveal allocations remain). +2. The handoff's de-LINQ of `UploadGfxObjMeshData`: one exact-size + retained `CPUIndices` array now feeds both the pick copies and the + arena upload (`GlobalMeshBuffer.UploadMesh` takes (offset, count) + segments of it); `CPUPositions` fills by direct loop; the + `Sum`/`Any`/`FirstOrDefault` transients are gone. Behavior-preserving: + same bytes staged, same batch order, same retained content. +3. Two I1-style allocation gates: a warmed directional-shadow topology + rebuild must allocate < 2 KB + (`DirectionalShadowPreparedDrawTests.AWarmedTopologyRebuildAllocatesNearZero`), + and a warmed mesh completion must allocate near its retained-copy size + (`MeshPipelineDeviceSeamTests.AWarmedMeshCompletionAllocatesNearItsRetainedCopySize`). +4. The temporary `PlayerPresentationProbe` and the attribution probe are + stripped. + +Session note: one automated probe run (of seven) exited with +0xC0000374 (STATUS_HEAP_CORRUPTION) during graceful close AFTER the route +completed, on a diagnostic build; not reproduced since. Watch for it in +future gate runs. + +Remaining owed: the owner's two-sided acceptance (feel gate + the ~45 s +pack-ON measured run against `artifacts/owner-gate/frame-history-429.csv`). +The pack frame-graph ordering question (defect 2) stays deferred unless +residual hitches survive. + +**OWNER GATE ROUND 1 (2026-08-23, same evening): allocation half PASSED, +felt hitch PERSISTS — residual attributed and a second fix round landed.** +The owner's ~45 s pack-ON drive on the fixed build: alloc-correlated stalls +701 → 5, stall-frame allocation median 39.6 MB → 40 KB, GC pressure gone — +but the micro-freeze feel remained. A time-triggered probe round (print any +frame > 12 ms with per-phase attribution) on an owner-driven clean run +measured the residual exactly: **82 stalls in 47 s (1.75/s — the ORIGINAL +pre-investigation stall rate), median 19.1 ms, clusters every ~1.3-2 s**, +near-zero allocation. Composition per stall frame — a rebuild CASCADE all +triggered by one `RenderDataAvailabilityVersion` bump (streaming publish) +and all paid in the SAME frame: +- `pk:casters` 3.5-14 ms — `DirectionalShadowCasterFrame.Build` copies + + classifies every outdoor projection record; +- `wb:sd-topo` 4-28 ms — `DirectionalShadowPreparedDraws.Complete` + (sort + group), allocation-free after the boxing fix but still the CPU; +- `ws:pview` 4-12 ms — the world draw path's own version-keyed work, + elevated on the same frames. +The owner's "introduced with the night-sky change" hypothesis was tested +directly and REFUTED: the sky default-script segment (`b:skypes`) crossed +1 ms once (2.3 ms) in the whole run. Timing note: the VisualMaster +directional-shadow machinery landed immediately before the night-sky +session where the hitch was first noticed — the sky was the nearest +visible change, the shadow prepass the actual newcomer. + +**Second fix round (in tree, uncommitted): packed-key index sorts.** Both +hot sorts — `DirectionalShadowPreparedDraws.Complete`'s draw sort and +`DirectionalShadowCasterFrame.Build`'s caster sort — previously moved +multi-hundred-byte records through interface comparers. Both now sort +4-byte index arrays against one 64-bit key (draw sort: an order-preserving +packed prefix Material|CullMode|FirstIndex|BaseVertex with exact-comparer +tie-break; caster sort: the traversal `SortKey.Value` directly), then +permute once through retained scratch. Total order preserved everywhere +the arena can reach; 131 directional-shadow tests including both #429 +allocation gates pass. Owner re-drive pending at time of writing. + +**Measurement hygiene note:** an A/B (same launch recipe, same position, +with/without `ACDREAM_UI_PROBE_SCRIPT`, and with an idle one-command +script) proved the harness launch recipe and the script runner are BOTH +innocent of the #432 low-FPS mode — both idle arms run 4.5 ms/17 KB +frames. The mode requires the synthetic route's PATH (through the town +view); owner-driven runs avoid it naturally. + +**OVERNIGHT ROUNDS 2-4 (2026-08-23→24): rebuild cascade cheapened but the +felt hitch is defect 2, now MEASURED as camera/player decoherence.** +Owner drives 2-4 each reported the hitch "unchanged" while every attacked +piece shrank (caster copy+classify+sort ≈ 1.3-2 ms each; the draw sort +round-1 index sort actually REGRESSED — instanced duplicates share one +packed key, so the tie-break full-record comparer became the hot path, +6.5 → 14.6 ms avg, caught by the owner's drive-2 data — round-2 replaced +the 100k-draw sort entirely with O(n) hash-grouping over retained chained +arrays plus an O(g log g) sort of the ~few-thousand DISTINCT group keys; +same emitted product, deterministic). Post-everything, owner-terrain stalls +still ~2.1/s at 27-31 ms median: per-frame `pv:frameview` (scene +frame-view build) + `pv:landscape` + the residual topo loop dominate. +Micro-shaving converges too slowly to clear 12 ms — the FEEL lever is +defect 2. + +**Defect 2 objectively measured** (from the ORIGINAL owner captures +`player-present-429-packON/-packoff.csv`, camera-relative analysis): +pack ON, 50 of 85 long moving frames separate the presented player from +the camera by ~1 m in one frame (18x the normal 5.5 cm relative step); +pack OFF, 7 of 83 at half the size. The felt hitch IS this one-frame +camera/player decoherence — the player lurches on screen while the +camera-anchored world stays smooth. + +**MECHANISM LOCATED (2026-08-24 ~00:10, per-update +`ACDREAM_PROBE_CAMERA_TICK` capture, pack ON, running 16 m/s):** on +long (~27 ms) updates, HALF the samples advance the presented player only +~12-16 cm (a third of elapsed time) while the chase camera steps the full +~40 cm; the other half advance both coherently (~42-53 cm each). The two +run on DIFFERENT clocks: the presented player position is +`ComputeRenderPosition` = lerp(prevQuantum, currQuantum, +pending/MinQuantum) on the retail 30 Hz OBJECT CLOCK (alpha CLAMPS at 1 — +near-quantum-length updates alias against the 33.3 ms quantum and the +presented position under-advances or freezes), while the camera's damping +(`ComputeDampingAlpha(stiffness, dt)` in RetailChaseCamera/ChaseCamera) +integrates WALL-CLOCK dt — on a 27 ms update it closes ~half its +accumulated ~1 m chase lag regardless of the target having barely moved. +Camera and player cross → the lurch. High-FPS updates (pack OFF, ~4 ms) +glide through the quanta, which is why OFF feels smooth with the same +stall count — and why shrinking the stalls below ~quantum length would +also mask it, but the CLOCK MISMATCH is the root cause. + +**DEFECT-2 FIX IMPLEMENTED + MEASURED (2026-08-24 ~00:20, in tree, +uncommitted).** `PlayerMovementController.PresentedDeltaSeconds` now +reports how far the presented position's own clock advanced each tick +(quanta simulated x MinQuantum + clamped-pending delta; wall dt on a +Discarded/teleport batch so the camera snaps along), and +`CameraFrameController` integrates the chase-camera damping with THAT +delta instead of wall dt (manual zoom/pitch stays on wall dt — an input +rate, not target chasing). This RESTORES retail's semantics — the camera +updates on the physics clock via PlayerPhysicsUpdatedCallback +(0x00452d60) — so no divergence-register row: it retires an unregistered +wall-clock deviation. Verified on the automated route, pack ON: +- per long update (28-38 ms): cam/player step ratio 0.75-1.19 (was + 2.6-3.4x with ~27 cm crossing); |cam-player| med 2.2 cm, p90 7 cm, + max 16.7 cm (~12x tighter); +- per frame: typical camera-relative player step 5.5 cm → 0.3 cm; the + baseline's 50-of-85 ~1 m long-frame lurches → 3 frames above 25 cm + (max 31 cm) — better than the old pack-OFF arm the owner perceived as + smooth. +Runtime suite 1,818/0, hermetic App suite 6,082/0. + +**FEEL GATE ROUNDS 2-3 + THE FINAL TWO FIXES (2026-08-24 00:20-01:10).** +Round 2 ("still there") caught that the camera-clock fix alone leaves a +coherent whole-view freeze: with camera and player now in lockstep, the +remaining artifact was the presented position itself under-advancing. +Root cause: `ComputeRenderPosition` normalized its lerp by the FIXED +MinQuantum while the retail clock simulates VARIABLE-length quanta +(everything above MinQuantum in one step, split at MaxQuantum=0.2 s) — a +long host frame fired a >33 ms quantum, alpha reset across the bigger +gap, and presentation froze then replayed fast. Fixed by normalizing by +the actual last-quantum interval (`_lastQuantumSeconds`), with +`PresentedDeltaSeconds` accounting continuous presented time (the camera +consumes the same delta, so both stay coherent by construction). Two +Runtime tests that pinned the old fixed-quantum lerp were updated to the +continuous-rate contract (Update_SubQuantumFrame_..., +Update_LeftoverAboveMinQuantum_... — renamed +...InterpolatesAcrossTheActualQuantumInterval). + +Round 3 landed the owner-approved (A) **shadow rebuild pipelining**: on a +frame where the shadow inputs just changed (streaming churn — the same +frame already pays the frame-view/landscape rebuilds), the caster and +prepared-draws topology rebuilds defer to the next quieter frame, capped +at 2 consecutive deferrals (inside the GPU fence depth, so retained draws +can never reference a released-and-reused arena range). Deferral is +best-effort with hard safety rails: first build, generation change, +transform-journal overflow, and any caster rebuild force the full path +immediately; stale-topology refreshes skip identity-mismatched journal +rows instead of throwing. Implemented across +`DirectionalShadowCasterFrame.Build(allowTopologyRebuild)`, +`WbDrawDispatcher.PrepareDirectionalShadowDraws(allowTopologyRebuild)`, +and the policy in `AtmosphericPostProcessGraph.RenderDirectionalShadows`. + +**Measured outcome (owner feel gate 3, ~210 s drive incl. pack +switching): median stall 20.3 → 13.7 ms; automated route: med 13.9 ms +(was 27-31), max 33, frames >16 ms at 1.4/s. Owner verdict: "almost +gone."** Residual composition (deep marks): frame-view build ~4.7 ms + +early landscape slices ~3.9 ms per churn frame, plus the pipelined shadow +rebuild ~8 ms on its own frame — content-proportional work with no +pathological defect left; further reduction is the incremental-topology +campaign already described above. OWED: the owner's final morning +confirmation, then strip the probe families (RenderFrameAllocProbe + +marks incl. fv:/pl:, PlayerPresentationProbe, CameraTickProbe) and +commit on request. + +**Tree state at pause (uncommitted, on the worktree branch):** four landed +optimizations (enum-boxing comparer fix, upload de-LINQ + arena segment +API, caster-frame index sort, draw hash-grouping) + two allocation-gate +tests; TEMPORARY apparatus still wired: `RenderFrameAllocProbe` (env +`ACDREAM_PROBE_FRAME_ALLOC`, time-or-alloc triggered, 4/s print sampling) +with ~30 phase marks, `PlayerPresentationProbe` +(`ACDREAM_PROBE_PLAYER_PRESENT`), `CameraTickProbe` +(`ACDREAM_PROBE_CAMERA_TICK`). All env-gated, off by default; strip all +three families with the defect-2 fix. Hermetic App suite 6,082/0 (one +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 +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, +scaled by the resident entity count of the whole streaming window. + +**Root cause (three links, each verified):** +1. `ACDREAM_AUTOMATION_ARTIFACT_DIR` (+ retained-UI screenshots) + constructs `CurrentRenderSceneOracle` + (`FrameRootComposition.cs:349`). Ordinary play never constructs it — + the owner was never affected. +2. The oracle's presence as partition observer flips + `LegacyPartitionDiagnosticsEnabled` + (`RetailPViewRenderer.cs`), so the G5-retired legacy + `InteriorEntityPartition` runs EVERY frame with per-entity + fingerprint observation. +3. `CurrentRenderSceneOracle.Complete` sorts the per-frame fingerprints + of every resident entity; the comparer's first key compared enums via + `x.ProjectionClass.CompareTo(y.ProjectionClass)` → + `Enum.CompareTo(object)` → boxes BOTH operands per comparison. The + 3-value enum almost always ties, so the boxing executes on + essentially every one of the sort's ~n·log n comparisons — the #429 + `ad695589` boxing-comparer defect class, second instance. + +**Evidence:** [pview-alloc] per-phase probe (`ACDREAM_PROBE_PVIEW_ALLOC=1`, +TEMPORARY, in `RetailPViewRenderer.DrawInside`) attributes the steady +mode to the partition phase: `part=6199KB` at the login spot +(`0xABB20030`) AND `part=6189KB` at town (`0xA9B4001E`) — near +content-independent because the resident far-window entity total is +similar (~60k) in both areas; this also explains the "latch" (the +resident set, not the view, drives it). Natural experiment across the +2026-08-23/24 runs, same town route: artifact-dir NULL runs +(`d2-packon/packoff`) average 48–67 KB/frame with ~50–74 churn frames +>3 MB out of ~18–20k; artifact-dir SET runs average 2.3–4.6 MB/frame +with thousands. Hermetic repro +(`OracleObservedPartitionAllocationTests.AWarmedObservedPartitionAllocatesNearZero`): +one warmed observed partition of 20,000 entities allocated +**15,876,088 bytes**; with the one-line non-boxing compare +(`((int)x.ProjectionClass).CompareTo((int)y.ProjectionClass)`) it passes +the <64 KB gate. + +**Consequences for past measurements:** any capture taken with +`ACDREAM_AUTOMATION_ARTIFACT_DIR` set carries this ~6 MB/frame + ~14 ms +diagnostic tax. The #429 acceptance data is CLEAN — the owner drives and +the A/B + d2 arms ran with the artifact dir null. + +**Not related to #433:** the stale-entity sighting happens in ordinary +play (no artifact dir), so it cannot share this mechanism. + +**Remaining (with the fix):** the oracle still costs real CPU per frame +(fingerprint walk + sort over ~60k entities) even allocation-free — +acceptable for a diagnostics-only path, but automation-gate FPS numbers +remain diagnostics-loaded; compare like-for-like only. The intermittent +~4.26 MB "post-world diagnostics" satellite (RenderSceneShadow +comparison, same construction condition) was not separately chased — +re-measure after this fix lands and file separately if it survives. + +**Previous (superseded) framing follows for the record:** +**Severity:** MEDIUM (halves frame rate and allocates ~300 MB/s while it holds) +**Filed:** 2026-08-23 (found while measuring the #429 fix; NOT caused by it — +reproduces identically on the pre-fix binary) +**Component:** rendering (untracked render path — attribution not yet done) + +**Symptom:** with the player at/near Holtburg town center (observed at cell +`0xA9B40019`; NOT at `0xA9B40036` a few cells away), every frame allocates a +near-constant ~6.3 MB and costs ~20 ms CPU (~45-50 FPS from a ~270 FPS +baseline), indefinitely, with Gen0 at ~6/s. `update_us` ~1.8 ms and +`upload_us` ~0 — the time and allocation sit in the untracked render path +(same measurement seam as #429). The mode begins the frame the view reaches +the spot (after a `/teleloc` there, or immediately at login when parked +there) and held for 80+ s of continuous running in a loop around town. + +**Evidence:** frame-history CSVs + stdout under the 2026-08-23 session +scratchpad (`frame-history-postfix-224749.csv` — healthy 7 ms/20 KB frames +for 20 s until the teleport, then 6.1-6.3 MB/frame for the rest; +`frame-history-postfix-225154.csv` — the mode active from login onward; +`probe-429-223228.out.log` — the SAME tail on the PRE-#429-fix binary). +The #429 owner baseline (spawn `0xA8B4002A`, running loops near-but-not-in +town) never shows it: normal frames ~22 KB at ~247 FPS. + +**Partial attribution + latch behavior (from the #429 probe runs):** the +per-phase lines that crossed the probe's 8 MB print floor split the mode as +a near-constant **~6.0 MB/frame in the PView draw +(`WorldSceneRenderer` → `DrawInside`)** plus an intermittent ~4.26 MB in +the post-world diagnostics phase. Once triggered it LATCHES: a 70 s +straight-line run ~300+ m away from town held EXACTLY ~6,187 KB/frame the +whole way (the town stays inside the Near ring at that distance, so +whatever content drives it stays resident). Trigger observed at town +center `0xA9B40019` but NOT at `0xA9B40036`, and NOT on the owner's +`0xA8B4002A`-spawn loops. Candidate families, unverified: the town's +buildings entering the PView nearby-building/cell set; an animated static +(the windmill, #426-adjacent) keeping a per-frame path hot. Re-add the +#429 attribution probe (one level deeper, inside DrawInside) and measure — +do NOT guess. + +**Gate caution:** a post-#429 measurement run that strays into this latch +shows every frame as a ~20 ms "stall" at ~6.3 MB — that is THIS issue, not +#429 residue. Compare only non-latched segments (normal frames ~22 KB), or +route away from Holtburg town center. + +**Next probes:** re-add the #429-style per-phase allocation probe ONE +level deeper — inside the PView draw (`WorldSceneRenderer` → +`DrawInside`): per-cell / per-stage bytes with identity (cell id, entity +guid, draw family), so the ~6.0 MB/frame names its owner instead of the +whole pass. Report WHOSE cells/entities the walk touches (also serves +the #433 stale-entity question). Measure at the trigger cell +`0xA9B40019` vs the clean `0xA9B40036`. (An earlier revision of this +entry carried #429's disproven run-rate/UpdatePosition probe list here — +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 +**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 +(`0x02000BA6`, PES `0x33000453`, whose t=0 hooks are a `CallPES` self-loop +(pause 30) plus `CreateParticle 0x320002C2 part=0`). The client boots on the +pre-sync default calendar (PY10 day0, group 16 Rainy); the first TimeSync flips +to the server date and the lightning slot vanishes, and hook executions landed +AFTER `SkyPesFrameController` had already removed the slot's pose. + +**Analysis so far:** `PhysicsScriptRunner.StopAllForEntity` purges owners, +anchors AND `_delayedCalls`, so this cannot loop forever (observed exactly 3, +consistent with hooks already drained into the sink's queue, or an ordering +window between the controller's stop and the runner's tick inside the same +frame). Worth pinning: whether hook dispatch can outlive the owner's stop by +one tick, and whether `part=0` hooks against a root-only synthetic pose (empty +part list) resolve to the root as retail would. Repro: launch while the server +clock is far from the pre-sync default during a Rainy-window fraction; watch +stderr through the first TimeSync. + +**Files:** `src/AcDream.App/Rendering/SkyPesFrameController.cs`; +`src/AcDream.Core/Vfx/PhysicsScriptRunner.cs` (`StopAllForEntity`, tick order); +`src/AcDream.Core/Vfx/ParticleHookSink.cs` (`TryResolveAnchor`, part fallback). + +--- + +## #427 — Hard line below the horizon from altitude: the sky dome was fogged with a 0.2 floor and the world fog range came from the streaming window, not the keyframe + +**Status:** ✅ FIXED 2026-08-23 (owner report, Candeth Keep/Holtburg heights: "a cut off where the sky ends and the world background void begins"). +**Component:** rendering / sky + world fog (retail parity) + +**Root causes (two April-2026 stand-ins, neither registered):** +1. `sky.frag` fogged every non-additive sky layer and clamped the blend with + `SKY_FOG_FLOOR = 0.2` ("mechanism unknown, workaround until pinned", + `97fc1b51`). The dome's rim (a sphere cut 500 m below the camera, radius + 1050 m) was therefore 20 % texture / 80 % fog against a frame cleared to + 100 % fog — a visible seam wherever terrain doesn't cover the rim (altitude, + water, map edge). Retail: `GameSky::Draw @0x00506FF0` draws the sky with + fixed-function fog DISABLED (`SetFFFogEnable(LScape::m_override_enabled ? + 1 : 0)` around the sky draw) — fog touches the dome only under an + AdminEnvirons override; additive layers are never fogged + (`SetFFFogAlphaDisabled(1)` at `D3DPolyRender::SetSurface 0x59c882`). +2. `WorldRenderFrameBuilder` overwrote the UBO's authored fog range with + 0.7 × near radius .. 0.95 × far radius (538..2189 m) at every hour and + weather (`ACDREAM_FOG_START_MULT`/`_END_MULT`). Retail sets + `D3DRS_FOGSTART/FOGEND` straight from the keyframe's `MinWorldFog/ + MaxWorldFog` (`SkyDesc::GetWorldFog @0x00500CE0` → `LScape::UseTime` → + `RenderDeviceD3D::SetFFFogProperties @0x005A2F70`, no draw-distance + scaling; `Render::zfar` is a constant 4000 m). Authored Sunny: day + 150–2400 m, dawn/dusk 90–800, night 0–400; Rainy day 150–1500. + +**Fix:** sky pass `ApplyFog = environOverrideActive && !additive`, no floor; +the frame builder leaves `SceneLightingUbo.Build`'s authored `FogParams.xy` +alone; the two env multipliers are deleted from `RuntimeOptions`. Guard: +`SkyFogRuleTests` (source-level; the sky renderer has no hermetic harness); +`sky.frag.spv` re-pinned in `VulkanShaderManifestTests` with the reason. +**Visible consequence (owner gate owed):** night and rain fog are now +retail's much shorter ranges; the dome's horizon tint is the authored +texture alone. The streaming window (≥ 2.1 km from the player) still sits +at/beyond every authored fog end except Sunny day's 2400 m, where the far +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 +0x010010CE, 30.4N 28.2E). +**Component:** content extraction (`MeshExtractor`/`GfxObjMesh`) + draw-time +classification (`WbDrawDispatcher`) + +**Symptom:** the windmill axle's 8 polygons (all `Stippling.NoPos` + +`SurfaceType.Base1Solid`) extracted to a 0-vertex mesh — +`[up-null] 0x010010CE produced a 0-vertex mesh`. Not an isolated case: EVERY +flat-coloured (untextured) polygon on EVERY GfxObj client-wide extracted to +nothing, because `PrepareGfxObjMeshData`/`GfxObjMesh.Build` gated emission of +a polygon's positive side on `!Stippling.HasFlag(StipplingType.NoPos)`. + +**Root cause:** `StipplingType.NoPos` (`NO_POS_UVS = 0x4`, +`docs/research/named-retail/acclient.h:7380-7388`) means "this side has no +texture coordinates" — true of every solid-colour polygon, since nothing +samples them — NOT "there is no positive face". The extraction code read it +as the latter and silently dropped the polygon entirely. Retail's +`D3DPolyRender::DrawMesh` (@0x0059d4a0, +`docs/research/named-retail/acclient_2013_pseudo_c.txt` ~line 426048) draws +an untextured subset (`(surface->type & 6) == 0`, i.e. neither +`BASE1_IMAGE` nor `BASE1_CLIPMAP`) on an ORDINARY object exactly like a +textured one; the ONLY retail cases that skip an untextured subset are a +BUILDING SHELL (`RenderDeviceD3D::DrawBuilding` @0x0059f2a0 sets +`ObjBuildingOrBuildingPart = 1`) and an EnvCell interior +(`RenderDeviceD3D::DrawEnvCell` @0x0059f170, `arg4 = 1`). The earlier #119 +investigation's "retail's skipNoTexture never draws them either" conclusion +was itself wrong — that only happened to hold for #119's two specific +GfxObjs because retail's per-model draw call passes `arg4` from the caller's +own context, not because untextured subsets are universally skipped (see the +#119 amendment below). + +**Fix:** `MeshExtractor.PrepareGfxObjMeshData` and `GfxObjMesh.Build` now +emit a polygon's positive side whenever `PosSurface` is a valid index, +regardless of NoPos; a NoPos polygon with no UVs on the wire falls back to +UV index 0 / zero texcoords (the pre-existing fallback path, unchanged). +`RetailUntexturedSurfacePolicy.IsUntextured(SurfaceType)` +(`src/AcDream.Core/Meshing/RetailUntexturedSurfacePolicy.cs`) is the ONE +place that now answers "is this surface textured", built from the surface's +own `Type` flags (`Base1Image`/`Base1ClipMap`) instead of the polygon's +Stippling — `MeshExtractor`'s `isSolid`/`TextureKey.IsSolid` now uses it +(previously `isSolid = NoPos || Base1Solid`, which also mis-classified a +NEG-side batch by the POS-side's NoPos flag). `RetailUntexturedSubsetPolicy +.Draws(isBuildingShell, isUntextured)` in the same file is the shared +draw-time predicate wired into `WbDrawDispatcher.ClassifyBatches`, +`.PackedOracle.ClassifyPackedBatches`, and +`.DirectionalShadows.AddDirectionalShadowBatches` — the ONE thing that +still skips an untextured subset is a building-shell entity, matching +retail's `DrawBuilding` gate; the shadow caster and receiver agree by +construction. `CellMesh.cs` and `MeshExtractor.PrepareCellStructMeshData` +(EnvCell/cell-wall geometry) deliberately KEEP their existing NoPos-gated +skip — retail's `DrawEnvCell` really does skip untextured cell subsets, and +the NoPos flag remains an approximation of that rule rather than a bug (see +register row AP-234). + +**Verification:** `Issue119UpNullGfxObjDumpTests` (Lane=InstalledDat) reran +against the installed DAT post-fix: #119's own two objects (0x010002B4, 9/9 +polygons; 0x010008A8, 1/1 polygon — both all-NoPos+Base1Solid) now gate +`DRAWS` on every polygon instead of producing a 0-vertex mesh. + +**Pak version:** `PakFormat.CurrentBakeToolVersion` 4→5 (also +`LauncherInstallRecordStore.CurrentBakeToolVersion`, kept in lockstep) — a +pak baked by an older tool is missing every untextured face and MUST be +regenerated; no bake was run as part of this fix (out of scope for a code +commit — the next scheduled bake picks it up via the version bump forcing a +rebuild). + +## #425 — Options Apply "Atmospheric rendering" fell back to the default path and stayed locked out: Low's 64 MiB resident budget did not scale with resolution + +**Status:** ✅ FIXED 2026-08-23 (found at the owner's VM3/VM6 gate launch). +**Component:** rendering / render packs (Campaign AR budget contract + activation memo) + +**Symptom:** live Holtburg, 2560×1440 fullscreen; Options → Atmospheric rendering → +Apply → the panel snapped back to "acdream default". Log +(`artifacts/owner-gate/launch2.log`): first Apply `Render pack preset 'low' +needs 67368164 resident GPU bytes after materializing its scene-dependent +shadow command buffers; the active pack budget is 67108864 bytes`, then every +later Apply `This pack selection already failed for the current registration +and will not be retried.` + +**Two root causes:** (1) a preset's `MaxResidentGpuBytes` (Low 64 / Medium 128 / +High 256 MiB) was applied as an absolute ceiling at any resolution, but the +pack's resident set is dominated by screen-sized images (12 B/px HDR+depth plus +ray/bloom intermediates): Low's targets are 25 MB at 1080p and 44 MB at 1440p, +and live Holtburg's shadow command buffers took the total to 64.25 MiB — 0.4 % +over. Medium would have failed the same way at 4K. The ceilings were only ever +validated at 1080p. (2) The controller's failure memo keyed the user's explicit +Apply and automatic re-activation identically, so one failed attempt locked +that selection out until restart even after the cause (resolution, scene) +changed. + +**Fix:** `RenderPackResidentBudget.Effective` — the declared figure is the 1080p +ceiling, scaled by the viewport's pixel-count ratio (never below 1) and still +capped by the hardware's `MaxPackResidentBytes`; used by both pack graphs and +mirrored in `tools/run-atmospheric-performance-matrix.ps1` so the tool judges +1440p/4K rows by the same rule. `RenderPackController.Request(selection, +explicitUserChoice: true)` clears the memo for that selection; the settings +binding passes it for every display edge (the user's Apply, including a +resolution change); startup keeps the memo. Tests: +`RenderPackResidentBudgetTests`, `An_explicit_user_request_retries_a_selection_that_failed_earlier`, +the extended `Selection_binding_activates_only_at_boundary_and_persists_failed_fallback`, +and the matrix contract test. Not a workaround: the ceiling now means what the +budget table implied ("at 1080p"), and a deliberate user action is allowed to +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). +**Component:** rendering / frame orchestration (host minimised-window guard) + +**Symptom:** exclusive fullscreen 2560×1440, High pack, alt-tab to the desktop → +`Unhandled exception. System.ArgumentOutOfRangeException: Activation extent must be +positive. (Parameter 'Width')` at `RenderPackController.ApplyAtFrameBoundary` +← `VulkanWorldScenePhase.Render` ← `GameWindow.OnRender`. Log: +`artifacts/owner-gate/launch.err`. + +**Root cause:** GLFW auto-iconifies an exclusive-fullscreen window on focus +loss. `GameWindow.OnRender` guards minimised windows through +`VulkanGraphicsContext.PrepareFrame()`, but that only consults a *pending* +recreate / swapchain existence — for the one frame between iconify and the +present that reports OUT_OF_DATE, `_window.Size` already reads 0×0 while +the swapchain is still "created", so a zero-area `RenderFrameInput` went +down the full pipeline. Pre-campaign the retail path tolerated a 0×0 +viewport silently; Campaign AR's pack controller (correctly) refuses a +zero activation extent, turning the latent zero-area frame into a crash. + +**Fix:** `RenderFrameOrchestrator.Render` returns +`RenderFrameOutcome.ZeroArea` (flag `SkippedZeroArea`) BEFORE +`BeginFrame` when either viewport dimension is ≤ 0 — no GPU frame, phase, +measurement, diagnostics or recovery runs — and `GameWindow.OnRender` skips +`NoteFrameClosed` for such a frame. Test: +`RenderFrameOrchestratorTests.ZeroAreaViewport_SkipsTheFrameBeforeAnyGpuWork` +(four extents). Not a workaround: a window with no area has nothing to +render, and the rule now lives at the one seam every frame passes through. + +## #423 — Atmospheric ray/shadow/volumetric day-group policy keys on the raw day-group index, not the DAT-classified WeatherKind + +**Status:** OPEN — filed 2026-08-23 at Campaign VM VM6 (spot-check); pre-existing from Campaign AR. +**Component:** rendering / render packs — atmosphere policy + +**Description:** `BuiltInAtmosphericRenderPack.AtmospherePolicy()` declares +`ActiveDayGroupMultiplier(0, 1.0), (1, 0.35), (2, 0.20)` and +`AtmosphericPostProcessGraph.EvaluateDayGroupPolicy` applies it to sun-ray, +directional-shadow and volumetric strength by the raw `activeDayGroup` +INDEX, with an undocumented assumed meaning (0 = brightest … 2 = dimmest). +Dereth's day-group index carries no weather meaning; the codebase classifies +the group's DAT name into `AcDream.Core.World.WeatherKind` (Clear/Overcast/ +Rain/Snow/Storm, `WeatherState.cs:198-232`), which `DirectionalShadowQuality` +already keys on. VM6's foliage wind was corrected to `WeatherKind` at +`6cc5e183`; the ray/shadow/volumetric multiplier was left as-is because the +owner accepted its look at the Stage-1 gate and re-keying changes it. + +**What to do:** re-declare as `WeatherKind`-keyed points (same shape as +`FoliageWindByWeather`), choose the per-kind multipliers at an owner visual +gate, and retire `ActiveDayGroupMultiplier`. Validator must reject unknown +kinds. Same class as the VM6 fix; do not guess the values. + +## #422 — Intermittent heap-corruption exit (0xC0000374) at process exit after an offline capture (pack on OR off) + +**Status:** OPEN — filed 2026-08-22 at Campaign VM VM3; **characterised at VM7 (2026-08-23) — rare, pack-INDEPENDENT, exit-time, not yet caught with a stack.** Facts: (1) it fired once more, on the **retail (pack-off)** row of `tools/run-atmospheric-performance-matrix.ps1` at 1920×1080 / 45 s warm-up / uncapped on `621a0edf` — the first launch after a fresh build — so "retail/off never reproduced it" is withdrawn and the title's "pack-on" is wrong: the exit is in the common teardown. (2) It did not fire in 16 runs with cdb attached (High, 720p, 12 s), 24 runs launched under cdb's debug heap (High, 720p, 12 s), 6 runs under the debug heap with the exact matrix recipe, or 10 plain runs with the exact recipe and a forced non-incremental rebuild before run 1 (`tools/i422/loop-debugheap.ps1`, `tools/i422/loop-plain.ps1`) — 1 in ~57 offline runs today, ~2 %. (3) The fail-fast leaves NO Application event-log entry and NO WER report on this machine (WerSvc is in its normal on-demand state, nothing disabled), so there is no dump to read; a per-user `HKCU\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\AcDream.App.exe` key (`DumpType=2`, `DumpFolder`) is the one-time user action that turns the next occurrence into a full dump — the project does not set registry keys itself. (4) The pre-campaign binary (`6c79d35c` + VM0 patches) CANNOT be tested with this tool: it predates the gate's in-process close verb, so every run ends in the gate's forced kill and never reaches the graceful-exit path where the fault lives (10/10 "automation close timed out") — whether the fault predates Campaign AR is therefore unknown, not disproven. Evidence: `docs/research/evidence/vm7/i422-*.txt`, `artifacts/vm7-matrix/uncapped-retail-1920x1080/` (the crashing run's log — managed shutdown complete, `MossTank disabled` last). **Owner decision 2026-08-23: accepted as carried; Campaign VM merged with it open.** Next step when it recurs: the LocalDumps key above, then `tools/i422/loop-plain.ps1` / `loop-debugheap.ps1`. **Recurred 2026-08-24:** the #432 attribution run (`probe-432-094728`, connected live session, graceful WM_CLOSE, managed shutdown complete) exited `-1073740940` — third sighting, first on a CONNECTED (non-offline-capture) run; still no dump (the LocalDumps key remains unset). One earlier #429-round sighting was also at graceful close (~1 in 10 diagnostic runs that day). +**Component:** rendering / render packs (Campaign AR) — native teardown + +**Description:** `tools/run-offline-pixel-gate.ps1 -RenderPackPreset high` (shipped +High defaults: shadows, rays, volumetrics, bloom all active) captured its +screenshot normally, the managed shutdown ran to completion (`MossTank +disabled` is the last log line, stderr empty), and the process then exited +with `-1073740940` = `STATUS_HEAP_CORRUPTION`. **1 occurrence in 8 runs** of +the identical command on the same binary (`51178f7c`); the other 7 exited 0, +including 4 runs with `ACDREAM_DEVTOOLS=1` (Vulkan validation layers) that +reported no validation message at all. `retail/off` and `high` with every +effect neutral (no shadow/ray/volumetric work) have never reproduced it. + +Campaign VM changed no native lifetime code (VM1 added a ring-section bind; +VM3/VM5 are shader/uniform changes), so this is most likely a Campaign AR +teardown race (a pack image/buffer/pipeline or directional depth target +destroyed while still referenced, or a double free) that the recording-RHI +convergence fixtures cannot see and that a 1-in-8 rate hides from the six +connected lifecycle processes. + +**What to do:** reproduce under the Windows debug heap / Application Verifier +(`gflags /p /enable AcDream.App.exe /full`) or with `VK_LAYER_KHRONOS_validation` +plus `VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION`, looping the +offline High capture until it fires, and capture the crash stack. Fix the root +cause — no try/catch, no "skip teardown" guard. Evidence: `artifacts/vm3/ +high-default/` (the crashing run's log; no crash dump was configured). + +## #421 — Directional-shadow pass uploads its own transform buffer instead of binding the main pass's instance SSBO + +**Status:** OPEN — filed 2026-08-22 at Campaign VM VM5 (post-M7 / with GPU culling). +**Component:** rendering / render packs (Campaign AR Tier 2) + +**Description:** `DirectionalShadowTransformBufferSet` +(`src/AcDream.App/Rendering/DirectionalShadowTransformBufferSet.cs`) +re-composes every caster's world matrix from the same +`MeshRef.PartTransform x LocalToWorld` inputs the main pass uses (same +`WbDrawDispatcher.ComposePartWorldMatrix`, so there is no second pose — +Campaign AR constraint 5 is honoured in spirit) and uploads them into a +second GPU buffer each frame for the animated subset. This is where the +65,536-matrix binding ceiling regression lived (fixed in Campaign AR +Stage 1). Cost is measured small (0.11–0.21 ms CPU incremental on the +reference matrix) but it is a duplicate of the N.5 instance SSBO the main +pass already binds. + +**What to do:** when the planned GPU-culling step for shadow cascades +lands (Campaign AR plan, "If that is too expensive, the next permitted +step is GPU culling"), bind the main pass's instance transform SSBO for +the caster pass instead of maintaining `DirectionalShadowTransformBufferSet`, +and delete the set. Not before — the shared buffer's lifetime/flight +rules differ (main pass ring vs retained caster topology) and unifying +them without the culling redesign would just move the ceiling. +Reference: Campaign AR review F7, +`docs/research/2026-08-22-campaign-ar-review.md`. + +## #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. +**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: +`started` → `connected` → `characterList` (2 characters) → +`exited code 1 "crashed"`, with `client.err.log` carrying + +``` +Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'key') + at System.Collections.Generic.Dictionary`2.FindValue(TKey key) + at AcDream.App.UI.UiButton.OnDraw(UiRenderContext ctx) +``` + +**Root cause:** `UiButton`'s constructor allocated the per-face-segment +media-state array as `new string[n]`, leaving every element **null**, while +its single-face sibling `_faceMediaState` was correctly seeded to `""` +(DirectState). `NextMediaState` returns `current` UNCHANGED on three of its +four arms — including "the committed state is authored but its media array is +empty", which is retail's own keep-playing-the-previous-media rule. So on a +multi-segment button whose committed state carries no media, the null +survived the first `SyncMediaStates` and reached +`ElementInfo.StateMedia.TryGetValue(null)`, throwing mid-paint and taking the +process down. + +**Fix:** `Array.Fill(_segmentMediaStates, "")` at construction — the segment +array now starts on base media exactly like `_faceMediaState`, which is what +the surrounding comment already claimed the media machine did. + +**Regression test:** +`UiButtonTests.MultiSegmentFace_CommittedStateWithoutMedia_DrawsInsteadOfThrowing`. +Verified by reverting the one-line fix: the test throws `ArgumentNullException` +from `UiButton.ActiveFile`, the same frame as the live crash. + +**Note for whoever tidies this file:** the crash was found while +investigating Campaign LU item 4 ("launching the selected character doesn't +work"). It is why nothing worked — the client reached character select and +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:** OPEN (filed 2026-08-17, user screenshot + FOV experiment). +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 silhouette against black, scaling with the Config FOV slider (barely visible at minimum FOV); a brief ring flash remains at exit even at @@ -124,9 +2025,74 @@ interior there ⇒ our pipeline, rings there ⇒ shared dat interpretation); (4) a retail side-by-side screenshot for ground truth (brightness included). Fix only against that evidence. +**2026-08-25 apparatus closeout:** step (1) is implemented as +`ACDREAM_PROBE_TUNNEL_FREEZE=1` (authored frame 72) or `=N` (frame 2–120). +The diagnostic withholds the TAS_TUNNEL transition even after world-ready, +then stops the tunnel sequence and roll at the requested frame and emits one +`[tunnel-freeze] ... state=held` marker. It intentionally prevents placement, +reveal, and login completion until cancellation/process exit; it is a capture +tool, not a shipping behavior or a performance mode. Step (2)'s frozen +RenderDoc capture (`portal-frozen-frame1178.rdc`) confirmed the authored finite +mesh and production portal pass were actually drawing; it did not support a +cull/material/asset substitution. Step (3) was attempted, but ACViewer was not +a trustworthy renderer oracle for this path and was discarded rather than used +as fix evidence. Step (4)'s direct retail capture showed the radial tunnel +field swapping straight to the destination world, with no small centered disk. + +**Root cause and final fix (2026-08-25):** the exit defect was a two-part +presentation-boundary error, not tunnel content. First, our outgoing viewport +remained eligible through retail animation-table level 1022 even though paired +initial 40 fps captures appeared to establish level 1013 as the last sample +whose radial field covered the measured center rings; level 1016 and later +exposed a small finite-mesh disk. `TeleportAnimSequencer` initially retired the +outgoing viewport after quantized level 1013. Second, +`LocalPlayerTeleportPresentation.Tick` +published the `WorldFadeIn` terminal projection while the tunnel could still be +visible, and both teleport/login event handlers called the synchronous +destination-release suffix before `ExitTunnel`. A slow suffix could therefore +hold the invalid tunnel/projection combination on screen. The presentation now +hides portal space before publishing any snapshot whose `ShowTunnel` is false, +and both handlers enforce retail's order: hide portal, reveal/release world, +then play `Sound_UI_ExitPortal`. The later `ExitTunnel` call remains idempotent. + +**Acceptance evidence:** the 40 fps frame-sequence verifier fails the pre-fix +owner-pak capture on the exact disk (`radial coverage=0.053`). Three consecutive +fixed launches, using `artifacts/owner-gate/acdream-v5.pak` with the installed +DATs and the same ACE login route, swap directly to world and pass at minimum +tail coverage `0.800`, `0.747`, and `0.736`. Focused regressions cover the 1001 +table boundary, tunnel-retire-before-view-plane publication, and both +teleport/login host-call orders. No portal mesh, shader, sampler, lighting, or +camera-position change is part of the fix. + +**16:9 perimeter correction (2026-08-25):** the center-ring verifier missed +the same mesh boundary crossing the sides of a full-width viewport. The owner +capture and the automated 1280x720 sequence agree: level 1001 is the last +fully covered tunnel sample, level 1006 begins exposing the outer perimeter, +and the old level-1013 handoff can hold the complete faceted rim while the +world viewport is installed. The sequencer cutoff is therefore 1001, still in +the retail quantized table domain, and the gate now measures an unobstructed +left-edge rail in addition to center rings. This changes only which authored +tunnel sample is held for the atomic swap; it does not enlarge or repaint the +mesh. + +**Residual found by the owner:** the direct swap still leaves a large blue +lower-viewport region during the first `WorldFadeIn` samples. This is not a +missing tunnel texture and the title's `lb 0/0` is not a terrain-residency +count. The destination is resident; its finite terrain is projected with +`M22=0.001` and `znear=0.1`, so lower-screen ground rays meet the terrain before +the Vulkan near plane and only a thin horizon strip survives. The legacy retail +landscape visibly supplies coverage at this singular endpoint. The modern +renderer adaptation scales the near plane with view-plane distance only while +the world viewport owns `WorldFadeOut`/`WorldFadeIn`; tunnel and ordinary-world +near planes remain unchanged. The lower-viewport coverage gate passes the +owner-pak live capture, and the owner accepted the final in-game transition on +2026-08-25 as "Perfect!". + ## #418 — Login world load takes ~27 s: publication advances at a flat 32 blocks/s -**Status:** IN-PROGRESS 2026-08-17 — producer half landed (this commit's +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -289,9 +2255,24 @@ attributed to the cold render-thread barrier); portal-hold gate-ready render-thread upload/registration phase (t≈1–8 s, concurrent) — budgets are exonerated three times over.** +**2026-08-25 attribution checkpoint:** `ACDREAM_PROBE_REVEAL_TIMING=1` now +pairs each reveal timing run with low-frequency `[reveal-resource]` snapshots +at begin, readiness edges, one-second progress intervals, and summary. The +snapshots borrow the canonical render owners and report mesh/atlas residency, +global and per-frame upload counts/bytes, buffer/texture/copy work, staging +high-water, mesh-arena capacity/migration, prepared-package probe/read results, +composite backlog, CPU mesh cache, and managed/committed/tracked GPU memory. +Use it with `ACDREAM_FRAME_PROF=1` and +`ACDREAM_FRAME_HISTORY=`; the next cold login comparison can now +distinguish decode/cache fill, upload/staging, +registration/composite debt, arena growth, and process-memory growth without +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:** ✅ FIXED 2026-08-17 (logout-audio round; fix + tests in the same +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -317,7 +2298,9 @@ 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:** ✅ FIXED 2026-08-17 (same round as #414; fix + tests in the same +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -350,7 +2333,9 @@ 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:** ✅ FIXED 2026-08-17 (same round as #414/#416; fix in the same +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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: @@ -369,7 +2354,9 @@ 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:** ✅ FIXED 2026-08-17 (entry/exit presentation follow-up; fix + +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -394,12 +2381,13 @@ contract. ## #413 — House tab shows no content (owned-house display, Display* line builders unported) -**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. +**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. **What's shipped (this session, on top of Batch C's mount + parser groundwork).** @@ -480,26 +2468,20 @@ groundwork).** already use (a plain `(layoutId, elementId) -> UiElement` resolve+build, nothing map-specific about it despite the binding's name). -**What remains open — item 3, the entire surviving scope:** +**Final item 3 resolution (2026-08-28):** 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`). 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. + 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. **Reference:** `docs/research/2026-08-17-map-house-recon.md` (the recon); `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (this session's owner, @@ -509,8 +2491,7 @@ 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 test — CLOSED for the houseless case, LIVE-VERIFIED, still the -target for the owned-house case.** The House tab, on a fresh `+Acdream` +**Acceptance.** 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, @@ -529,8 +2510,11 @@ 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`). Still owed: the owned-house case -once item 3 lands. +(`[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. ## #412 — Options panel Config tab content escapes the window frame (footer mid-panel, rows drawing below the window's bottom edge) @@ -723,7 +2707,22 @@ porting. ## #410 — Client-wide VJustify (vertical text justification) enum mapping + unauthored default are wrong (retail default is Top, not Center) -**Status:** OPEN +**HJustify sibling evidence (2026-08-24, CA5 tooltip re-check):** the SAME +wrong-default class exists horizontally — `ElementInfo.HJustify` defaults +to Center while retail's unauthored default is Left. Observed live: the +tooltip popup text (whose authored skin sets no justification) rendered +centered where retail left-aligns; point-fixed in +`RetailTooltipPresenter.ApplyTooltipText` (the chat transcript carries the +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. **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 @@ -804,7 +2803,9 @@ 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:** 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:** 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. **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 @@ -1193,9 +3194,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 — General importer-wide honor of dat property 0x3B (Invisible) is unshipped (1,083 elements client-wide) +## #408 — FIXED: General importer-wide honor of dat property 0x3B (Invisible) -**Status:** OPEN +**Status:** FIXED 2026-08-28 **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): @@ -1252,6 +3253,23 @@ 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) @@ -1378,22 +3396,19 @@ launch: `started → connected → characterList`, graceful close. ## #404 — ChargenSkillScoreResolver duplicates ChargenTableReader's own SkillTable read -**Status:** OPEN (post-CC cleanup follow-up) +**Status:** FIXED 2026-08-28 **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 takes its OWN independent read of +`ChargenSkillScoreResolver`'s constructor took 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) — 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 +(`ChargenTableReader.cs:41` names the id, `:61` reads it). 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 @@ -1415,9 +3430,25 @@ 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:** OPEN (post-CC consolidation follow-up) +**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. **Severity:** LOW **Filed:** 2026-08-15 (Campaign CC slice CC6b-PRE review fix round, F5) **Component:** `src/AcDream.Core/Physics/RetailAnimationCyclePlayback.cs`, @@ -1450,13 +3481,18 @@ change to any currently-animated NPC. ## #402 — Flaky test: Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate -**Status:** OPEN (flake, not a regression) +**Status:** FIXED 2026-08-18 by `dfc841b7`; ledger reconciled 2026-08-28. **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`). @@ -1480,7 +3516,12 @@ intermittently failing on this test. ## #401 — RetailUi should default ON (opt-out), not per-path forced -**Status:** OPEN (product-default decision) +**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. **Severity:** MEDIUM (recurrence risk) **Filed:** 2026-08-15 (Campaign LA gate-round-2 batch review, F2) **Component:** `src/AcDream.App/RuntimeOptions.cs` @@ -1506,7 +3547,8 @@ opted out; the forcing is gone; docs updated. ## #400 — Character select: Credits button is ghosted; retail opens gmCreditsUI -**Status:** OPEN (post-LA polish) +**Status:** FIXED 2026-08-28 — the authored credits screen and retail scroll/ +return state machine are mounted from the installed DATs. **Severity:** LOW **Filed:** 2026-08-15 (Campaign LA gate round 2, char-select findings batch) **Component:** `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` @@ -1514,14 +3556,25 @@ opted out; the forcing is gone; docs updated. Retail's character-management screen routes the Credits button (`0x100003A3`, listbox-base offset 6 in `gmCharacterManagementUI::ListenToElementMessage @0x004ed5a0`) to -`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. +`QueueUIMode(0x10000005)` → `gmCreditsUI` (`Register @0x004e7500`) — a +scrolling credits screen. -**Acceptance:** Credits opens the ported retail credits screen and -returns to character select; button re-enabled. +**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. ## #399 — Launcher: no test ever constructs MainWindow, so code-behind defects reach the user gate @@ -1624,9 +3677,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:** IN-PROGRESS — the isolated process-group implementation and real -Windows fixtures are complete; the LA11 connected acceptance row remains -required before closure. +**Status:** CLOSED 2026-08-28 — the isolated process-group implementation, +real Windows fixtures, and connected Windows supervisor acceptance gate all +pass. **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") @@ -1654,17 +3707,26 @@ 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. -**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. +**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`. ## #396 — Configure Keyboard: no capture-instruction dialog on a mapping-button click -**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 +**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 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. @@ -1687,7 +3749,9 @@ cancel would race it). ## #395 — Configure Keyboard: key captions show raw enum spellings, not retail's localized key names -**Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's re-gate. +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -1705,7 +3769,9 @@ 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:** ROOT-CAUSED + FIXED (this commit) — pending the user's re-gate. +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -1721,43 +3787,31 @@ and applied to the caption `UiText`. Probe evidence: `KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings` (env-gated, kept). -## #393 — Texture detail options: retail's "High Resolution Textures" toggle + Landscape/Environment TextureDetail mip-skip +## #393 — Texture detail options / high-resolution DAT handling -**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). +**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. ## #392 — A refused/failed fullscreen enter leaves `fullscreen: true` persisted against a windowed client -**Status:** OPEN — filed 2026-08-13 from the #376/#388 blast review (M4). +**Status:** DONE — 2026-08-22. `IRuntimeDisplayWindowTarget` now returns +the observed native fullscreen post-condition. Both startup and live-save +controllers reconcile that result back through their own storage boundary, +so a refused/failed enter immediately restores `fullscreen: false` in the +retained Config row and `settings.json`; a failed leave likewise preserves +the true native state. Focused startup, target, persistence, and observer +tests pin the result seam and the requested→applied publication contract. + +**Original filing:** The save path persists the Full Screen flag BEFORE the apply runs; when the state-aware apply then refuses (mode not offered / catalog absent) or the native switch fails, the client stays windowed while settings.json and @@ -1951,30 +4005,42 @@ 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 would size the popup to content, our shipped 6-row window may diverge +## #386 — Vendor category dropdown: authored ListBox is edge-docked — retail sizes the popup to content -**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. +**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. ## #385 — Options-panel dropdowns: gold left-aligned text + fixed 6-row popup (retail: white, centered, size-to-content) @@ -1999,8 +4065,11 @@ is #376/#377) — no change. ## #384 — FA6 allegiance-swear bot gate: ACE returns no response to 0x001D swear (no confirmation/0x0020/error) -**Status:** OPEN — filed 2026-08-12 at Campaign FA slice FA6. The two-bot -headless fellowship/allegiance connected gate +**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 (`src/AcDream.Headless/Policies/HeadlessBotPolicy.cs`, `FellowshipAllegianceLeaderBotPolicy`/`FellowshipAllegianceRecruitBotPolicy`) ran live against local ACE (`127.0.0.1:9000`, `testaccount`/`+Acdream` as @@ -2072,7 +4141,10 @@ the flag is off. ## #383 — Installed-DAT vs committed-fixture drift: regeneration produces large diffs in existing UI fixtures -**Status:** OPEN — filed 2026-08-12 at Campaign FA slice FA3. Running the +**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 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 @@ -2338,7 +4410,11 @@ cap and open a real bordered, scrollable popup on click.** ## #377 — Startup CRASH (0xC0000005 in Glfw.GetVideoMode) when settings.json has `fullscreen: true` -**Status:** OPEN, NOT REPRODUCIBLE on current code (2026-08-13, display +**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 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 @@ -2487,8 +4563,7 @@ switching stays #376. ## #373 — Configure Keyboard: DAT `ActionMap.ConflictingMaps` not consulted — the combat cluster raises false conflict prompts -**Status:** OPEN — filed 2026-08-11 at Campaign OP slice OP8's re-review -round 2 (R1's scope boundary). +**Status:** DONE 2026-08-26 — fixed as the first #446 keyboard-parity slice. The DAT ActionMap (DID `0x26000000`) carries a `ConflictingMaps` table retail's `UIOption_ActionKeyMap` consults when deciding whether two rows @@ -2505,17 +4580,22 @@ new action to one) prompts "overwrite N bindings?" where retail prompts for fewer or none. Accepting the prompt then strips retail-default bindings that should have survived. -The OP8 round-2 fix already excluded store-only rows (`MappedAction is -null`) from the conflict universe — those cannot collide because they -never reach the InputDispatcher — but retail-mapped cross-context -sharing needs the real table. **Fix:** parse `ConflictingMaps` in +The OP8 round-2 fix originally excluded store-only rows (`MappedAction is +null`) from the conflict universe. Campaign KB later mapped and enabled every +one of the 306 installed rows, eliminating that tier; retail cross-context +sharing still needs the real table. **Fix:** parse `ConflictingMaps` in `RetailActionMap` (the reader already round-trips the field — `RetailActionMapReaderTests` constructs it), and make `FindConflicts` consult it: two rows sharing a chord conflict only if their contexts' ConflictingMaps entries say so. Conformance-test against the combat cluster's authored defaults (five keys, multi-row each, zero prompts on -a no-op rebind). The gate script's §OP8 warns the user off treating the -false prompts as new breakage until this lands. +a no-op rebind). + +**Fix landed:** `RetailActionMapSnapshot` now owns the copied DAT conflict +sets and `KeyboardConfigController.FindConflicts` consults them before +offering reassignment. Hermetic tests pin permitted cross-combat sharing and +declared cross-map conflicts; an installed-DAT test pins that melee, missile, +and magic are pairwise non-conflicting. Same-context conflicts remain active. ## #372 — Options panel: Character/Chat/Config tabs render BLANK on screen and most Gameplay buttons do nothing (connected-gate failure) @@ -2639,7 +4719,16 @@ symptom. ## #360 — @allegiance/@house management dispatchers only port their simple subcommands -**Status:** OPEN — filed 2026-08-09, Campaign CH slice CH4; corrected +**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 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 @ @@ -2693,16 +4782,19 @@ slice CH4). ## #361 — @day / @log / @render pure-local commands recognized in help only, not executed -**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. +**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. **Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH, slice CH4). @@ -2954,7 +5046,10 @@ 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:** OPEN — filed 2026-08-10 during #368's fix verification. +**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. **Symptom:** with #368 fixed (one dedicated update thread, thread migration provably gone — the new affinity test pins it), the `jump-probe` @@ -2988,8 +5083,11 @@ 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:** OPEN — filed 2026-08-10, Campaign CH slice CH6b (register row -AP-188). The floating chat window LayoutDesc (`0x2100005B`) authors no +**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 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 @@ -3019,9 +5117,12 @@ 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:** 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` +**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` 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 @@ -3029,10 +5130,8 @@ 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. **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. +widget instead of being silently swallowed. The later controller work completed +the behavioral half described above. **Where:** `src/AcDream.App/UI/Layout/ChatWindowController.cs` (behavior, still missing); `src/AcDream.App/UI/Layout/LayoutImporter.cs` @@ -3193,8 +5292,11 @@ this session's hard constraints excluded client launches. ## #359 — 0x019E PlayerKilled line prints to participants — retail suppresses it -**Status:** OPEN — filed 2026-08-09 at the CH1 Opus review. Pre-existing (not -introduced by CH1); candidate for CH4/CH5. +**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. **Symptom:** `ChatLog.OnPlayerKilled` (`src/AcDream.Core/Chat/ChatLog.cs`) always appends the death message for every recipient of the `0x019E` @@ -3438,7 +5540,10 @@ wrap threshold is the authored PIXEL width, not a character count. ## #352 — Vendor range-watcher cylinder metric: discriminating unit test deferred -**Status:** OPEN (filed 2026-08-08). The EnforceRange cylinder-gap fix +**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 (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 @@ -3470,7 +5575,8 @@ configs. ## #350 — Render-shadow ledger overflow after 2h42m: lifetime int counters in a never-reset accumulator -**Status:** FIXED IN TREE 2026-08-08 (pending clean-room + landing). +**Status:** CLOSED 2026-08-28 — the production lifetime counters are `long`; +the old pending-landing label is stale. **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 @@ -3496,7 +5602,9 @@ 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:** FIX IN TREE (2026-08-08) pending the vendor-gate relaunch. +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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`, @@ -3868,9 +5976,10 @@ reasoned-from-source diagnoses. ## #346 — `PortalProjectionTests.ProjectToClipLease_ReusesPooledWorkWithoutResultArrays` is a SIXTH load-sensitive flake -**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. +**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. **Filed:** 2026-08-08, observed during #344's suite runs. **2026-08-16 recurrence (Campaign #409 tooltip review-fix round).** The Opus @@ -3964,8 +6073,9 @@ 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:** OPEN — HIGH priority for the next physics session; the fix itself -is byte-proven, the BLOCKER is that the measurement chain contradicted itself. +**Status:** CLOSED 2026-08-28 — the relanded fix passed its ten-run, +bit-identical gate; the contradictory historical measurement is retained only +as evidence. **Filed:** 2026-08-07 (overnight), at the S4 landing split. Retail's `adjust_offset` safety push-out uses the BARE sphere radius in both @@ -4138,26 +6248,31 @@ 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:** 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. +**Status:** CLOSED 2026-08-28 — the current assertion compares the old-model +and new-model values rather than the same expression to itself. **Filed:** 2026-08-07. --- ## #340 — `StreamingWorkBudgetTests.DestinationAndEmptyUnloadPriorityNeverBypassPublicationBudget` is a FIFTH load-sensitive flake -**Status:** OPEN. LOW. +**Status:** FIXED 2026-08-18 by `dfc841b7`; ledger reconciled 2026-08-28. 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: @@ -4221,8 +6336,7 @@ separately below. ## #339 (original) — Stuck in portal space: the destination reveal generation never becomes ready -**Status:** OPEN — observed live 2026-08-07, evidence captured. **Not chased**; -user directed it be fixed later. +**Historical status:** OPEN — observed live 2026-08-07, evidence captured. **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). @@ -4414,7 +6528,9 @@ 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:** FIXED 2026-08-06 by #333's fix — the query-site broadphase reach +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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`, @@ -4568,7 +6684,9 @@ 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:** FIXED 2026-08-06 — **the filter is deleted, not re-centred.** +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -4969,7 +7087,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:** OPEN +**Status:** FIXED 2026-08-28 **Severity:** MEDIUM (no observed symptom; reachability against ACE is unmeasured — see below. The behaviour when reached is four simultaneous divergences, not one.) @@ -5045,6 +7163,14 @@ 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 @@ -5109,12 +7235,23 @@ 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:** OPEN -**Severity:** LOW today (no observed symptom — see below); the correctness -question is real and unresolved +**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 **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: @@ -5223,7 +7360,8 @@ 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:** OPEN +**Status:** CLOSED AS TEST DEBT 2026-08-28 — no current product defect is +established; any desired extra composition coverage belongs in the test plan. **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 @@ -5282,7 +7420,13 @@ shadow registry specifically. ## #317 — `TryCommitAuthoritativeVelocity`'s call site has no established retail basis -**Status:** OPEN +**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. **Severity:** LOW (tracking only; no known observable defect) **Filed:** 2026-08-04, C4 route 5 (projectile authoritative placement) round-2 review, MINOR (c) @@ -5333,7 +7477,7 @@ carried over from the route 4b-3 round-2 reviews. Evidence: ## #313 — `DeclareValid`'s `SetSelectedObject` split-recovery is not ported -**Status:** OPEN +**Status:** FIXED 2026-08-28 **Severity:** LOW (selection UX, not placement) **Filed:** 2026-08-04 **Component:** UI / inventory / selection @@ -5371,6 +7515,12 @@ 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` @@ -5499,7 +7649,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:** **DEFERRED as an ACCEPTED DIVERGENCE — user decision 2026-08-06.** +**Status:** CLOSED AS ACCEPTED DIVERGENCE 2026-08-28 — 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. @@ -5654,7 +7804,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:** OPEN +**Status:** FIXED 2026-08-28 **Severity:** HIGH **Filed:** 2026-08-04 **Component:** physics / streaming @@ -5690,9 +7840,22 @@ 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:** OPEN +**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. **Severity:** LOW (perf, not correctness) **Filed:** 2026-08-04 **Component:** physics / headless @@ -6829,7 +8992,14 @@ it. Do #297 FIRST — #298 depends on it. `#153` closed 2026-07-30 on the AD-30 hold + arrival StopCompletely + canonical outbound + reveal-barrier evidence chain). TS-50/TS-51/TS-53 are tracked in the divergence register. -- **Deferred visual fidelity:** `#226` retail landscape detail overlay. +- **Resolved visual fidelity (2026-08-21):** `#226` implements retail's + building/EnvCell detail overlay through the existing Building Detail + Textures preference. The reachable retail `ChangeRegion` caller disables + landscape detail, so no separate landscape-detail item remains queued. + The same Track A closeout ports retail's incident-face-averaged shared + terrain vertex normals without changing positions, indices, or collision; + terrain subdivision was rejected because quantized source samples cannot + recover detail and the retail-correct normal interpolation is now present. - **Deferred frame-pacing fidelity:** `#235`, capped/RDP jump presentation aliases the retail 30 Hz object clock; uncapped Release presentation is smooth and physics, collision, and wire state remain correct. @@ -6932,7 +9102,8 @@ for a follow-up that did not exist. Both now cite #322. ## #274 — Restricted/barred-house entry needs a connected retail comparison -**Status:** OPEN — explicitly deferred by the user on 2026-07-31 +**Status:** CLOSED 2026-08-28 — this is a deferred comparison/gate request, +not an observed client failure. **Severity:** LOW (validation debt; no confirmed failure) **Filed:** 2026-07-31 **Component:** physics / EnvCell entry restrictions @@ -6951,7 +9122,9 @@ character access state, and result before closing. ## #273 — ACDream can squeeze through tight world gaps that block retail -**Status:** OPEN — live mismatch confirmed 2026-07-31; exact location/capture +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN — live mismatch confirmed 2026-07-31; exact location/capture still required **Severity:** MEDIUM (world traversal differs from retail) **Filed:** 2026-07-31 @@ -7165,8 +9338,9 @@ row refresh after the final retained-UI invalidation correction. ## #267 — Vitae does not update the character panel's skills/attributes display -**Status:** IMPLEMENTED 2026-07-30 (`cf2605fa`, merged) — closure pends -the user visual check. Retail finding: primary attributes are +**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 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. @@ -7222,8 +9396,9 @@ ACE's >= reading" warning. ## #265 — Steep-slope response set: uphill-jump bounce, roof slides lost, edge wedge (TS-4 removal fallout — REVERTED) -**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) +**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) 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, @@ -7319,7 +9494,9 @@ model). ## #263 — Drudge Scrying Orb still occludes its particles after the composite-translucency fix -**Status:** OPEN (deferred by user 2026-07-29) +**Status:** CLOSED 2026-08-28 — owner-confirmed after validity audit. + +**Previous status:** OPEN (deferred by user 2026-07-29) **Severity:** LOW (single known item; "very subtle" per user) **Component:** rendering / world translucency / particles @@ -7368,7 +9545,9 @@ regressing the #225 lifestone/candle compositing. ## #264 — Water semantics: WATER_CONTACT_TS consumer + two unverified swim behaviors -**Status:** OPEN (filed 2026-07-30, Campaign P Slice P4 AP-10 closeout) +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -7416,7 +9595,8 @@ sub-item resolves. ## #262 — Run-on-the-spot at first login: no displacement until a recall reset -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner reports the first-login movement issue +is solved. **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 @@ -7513,7 +9693,10 @@ scenario 11 (20 fresh logins) provides the structured re-test. ## #261 — Wire LinkStatusSnapshot.PacketLossPercentage from retail's formula -**Status:** OPEN +**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. **Severity:** LOW **Filed:** 2026-07-29 **Component:** net (link-status presentation) @@ -7616,8 +9799,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:** OPEN — environment fault, not a product defect; recorded so the next -reader does not bisect the tree for it +**Status:** CLOSED 2026-08-28 — confirmed machine/driver environment fault, +not an acdream product defect; retained only as diagnostic history. **Severity:** HIGH while it lasts (the client cannot start at all) **Filed:** 2026-07-29 **Component:** host machine / AMD driver / Win32 WSI @@ -7667,7 +9850,8 @@ self-differential is still available once a window can be created. ## #258 — Developer panels have no host after V11 deleted ImGui -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner classified the deleted developer-panel +host request as void; it is not desired product scope. **Severity:** MEDIUM (developer capability regression; no player-facing effect) **Filed:** 2026-07-29 **Component:** developer tooling / retained UI @@ -7870,7 +10054,8 @@ afterwards. **The test was not modified.** ## #256 — Server-spawned objects go invisible after repeated portal runs -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — superseded by #260; its discriminator found no +missing-object drift and the transport-loss mechanism was fixed elsewhere. **Severity:** HIGH (world objects invisible but interactive; live-server observed) **Filed:** 2026-07-28 **Component:** live-entity render publication / streaming (backend attribution pending) @@ -7947,7 +10132,9 @@ CSVs, difference maps, `churn-soak.json`). ## #257 — Working set balloons to ~1.5 GB over a live portal-churn session -**Status:** OPEN +**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. **Severity:** HIGH (memory; live-server observed) **Filed:** 2026-07-28 **Component:** GPU/streaming resource lifetime (backend attribution pending) @@ -8022,7 +10209,9 @@ re-run with walked portal transits rather than `/teleloc`. Artifacts: ## #253 — Attribute/skill icons: not centered in their cells, and fully opaque -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** LOW (visual fidelity; user-observed) **Filed:** 2026-07-28 **Component:** retail UI / character sheet (D.2b) @@ -8127,7 +10316,8 @@ its only `CameraDiagnostics` writer, but four classes there call ## #251 — glClientWaitSync returned 0 and crashed the render loop, once in nine connected runs -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — obsolete OpenGL-path report; the production +renderer is Vulkan-only and no `glClientWaitSync` path remains. **Severity:** MEDIUM (one observed occurrence; kills the process when it fires) **Filed:** 2026-07-28 **Component:** rendering / GL frame-flight fences @@ -8184,7 +10374,9 @@ 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:** OPEN +**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. **Severity:** MEDIUM (undermines every "tests green" gate) **Filed:** 2026-07-27 **Component:** tests / allocation assertions @@ -8321,8 +10513,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:** OPEN -**Severity:** MEDIUM +**Status:** FIXED 2026-08-28 +**Severity:** CLOSED **Filed:** 2026-07-27 **Component:** rendering / GPU resource lifetime @@ -8652,7 +10844,9 @@ then its description after one retail paragraph break, without the bogus ## #235 — Capped/RDP jump presentation aliases the 30 Hz object clock -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-confirmed after validity audit. + +**Previous status:** OPEN **Severity:** LOW **Filed:** 2026-07-23 **Component:** local animation / render interpolation / frame pacing @@ -8847,11 +11041,20 @@ reports decoded bytes, budget, and evictions. ## #241 — InteriorEntityPartition never uses its per-landblock AABBs to cull -**Status:** OPEN +**Status:** FIXED 2026-08-28 **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` @@ -8874,7 +11077,8 @@ Slice H-a / G. ## #242 — Static publication rebuilds a third dictionary and re-sorts per completion attempt -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — current publication prepares the replacement +snapshot once and reuses it across completion retries. **Severity:** LOW **Filed:** 2026-07-24 **Component:** streaming @@ -9117,7 +11321,8 @@ from crossing into a new world. ## #228 — Clean Release build emits 17 test-project warnings -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — disproved by a clean current Release build with +zero warnings and zero errors. **Severity:** LOW **Filed:** 2026-07-20 **Component:** tests / build hygiene @@ -9150,48 +11355,87 @@ the full 6,558-pass / 5-skip suite remains green. --- -## #226 — Retail landscape detail-texture overlay is not rendered +## #226 — Retail building/EnvCell detail-texture overlay is not rendered -**Status:** OPEN — deferred visual fidelity; the user-visible tiling regression -in #155 is fixed +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 +[`docs/research/2026-08-22-vm2-retail-detail-path-cdb.md`](research/2026-08-22-vm2-retail-detail-path-cdb.md)). **Severity:** LOW **Filed:** 2026-07-20 -**Component:** rendering / terrain material +**Component:** rendering / building and environment materials -**Description:** Retail can overlay a high-frequency landscape detail texture, -faded by viewer distance and gated by the Environment Detail Textures setting. -acdream now repeats every base/overlay/road surface at its authored -`TerrainTex.TexTiling`, which fixed the stretched/blurry symptom in #155, but -does not yet render this separate optional detail layer. +**Description:** Retail overlays a category-scoped detail texture on building +shells and interior/EnvCell geometry, faded by viewer distance and gated by +the Building Detail Textures preference. acdream's existing “Building +Detail Textures” checkbox persisted that preference but previously had no +renderer consumer. Outdoor landscape detail is forced off by the reachable +Sept-2013 retail preference caller and is not this issue's user-visible target. **Root cause / status:** The earlier #155 investigation conflated two retail mechanisms. `bb5acab9` ported the behavior that produced the observed mismatch: `TexMerge::CopyAndTile`/`Merge` pass each source's authored base tiling into the -terrain composition. The still-missing detail pass is a distinct -`LScape::GenerateDetailSurfaces`/`ACRender::landPolyDraw` path. The first -experimental detail-array implementation sampled the wrong neutral/data -contract and was reverted rather than shipping a darkened ground. TS-52 records -the current divergence. +terrain composition. #226 now resolves Dereth category 1/2 detail surfaces, +uploads their authored texture/tiling with retail wrap/linear sampling, and +replays building and EnvCell built-mesh subsets with retail's single-pass +detail combine (VM1/VM2, 2026-08-22): `SRCALPHA + INVSRCALPHA` compositing +`lerp(base * diffuse, detail.rgb, detail.a * diffuseAlpha)`, matching the +`bCanDoSinglePassDetailing = 1` path real hardware runs — not the two-pass +`DESTCOLOR + INVSRCALPHA` fallback the port originally reproduced. This +includes opaque, ClipMap, straight-alpha, +additive, and inverse-alpha material subsets; transparent base/detail commands +remain adjacent in acdream's authoritative shared alpha order with depth writes +disabled (retail bypasses delayed alpha while detail is installed; retaining +the accepted queue is the registered bounded ordering seam). The existing +persisted checkbox is read at draw time. Opaque object replay is restricted to +coalesced command runs containing a building, with mixed commands filtered per +instance in the shader. Its depth-equal, non-A2C overlay inherits the exact +per-sample coverage written by the opaque/A2C base, including ClipMap edges. +Ordinary objects and landscape remain excluded; the base pass is untouched +when the option is off. The first experimental +landscape array used the wrong target, topology, neutral point, and blend and +was reverted rather than shipping a darkened ground. **Files:** `src/AcDream.App/Rendering/TerrainAtlas.cs`; -`src/AcDream.App/Rendering/TerrainModernRenderer.cs`; -`src/AcDream.App/Rendering/Shaders/terrain_modern.frag`. +`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs`; +`src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs`; +`src/AcDream.App/Rendering/Shaders/mesh_detail.vert`; +`src/AcDream.App/Rendering/Shaders/mesh_detail.frag`. **Research:** `docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md` -covers the now-shipped base contract. The detail symbols cited above must be -distilled into a dedicated pseudocode note as the first #226 implementation -step; the reverted experiment remains available in git history. +covers the already-shipped base contract. The dedicated, corrected detail +contract is `docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md`; +its evidence source is +`docs/research/2026-08-21-terrain-and-atmospheric-rendering-findings.md`. -**Acceptance:** With retail Environment Detail Textures enabled, close ground -shows the same high-frequency detail and distance fade without changing base -color/brightness. Disabling it produces the already-accepted authored base -tiling. +**Acceptance:** Toggling the existing “Building Detail Textures” checkbox +visibly changes nearby building and interior surfaces without a restart. +There is no distance fade (VM1/VM2: retail's `get_alpha_for_z` is unreachable +for built meshes; attenuation is the sampler's linear mip chain), and the +single-pass combine darkens the live category texture by roughly 10% on +mid-tones rather than brightening it. Disabling it submits no detail replay +and preserves the already-accepted base render. Landscape, ordinary objects, +physics, and collision remain unchanged. +The automated gates cover the setting gate, data/blend/fade contract, +built-mesh subset eligibility, opaque command filtering and A2C coverage, +transparent depth/order seam, Vulkan descriptor +binding and total/per-stage storage-descriptor limits, shader artifacts, and +build. The connected Facility Hub A/B/A gate applied the real Config checkbox +on -> off -> restored-on: nearby static walls/floor changed immediately, the +restored frame returned to the original-on image (right-wall RGB MAE 2.132 +on/off versus 0.007 on/restored), the persisted preference was observed false +during B and restored true, and logout was ACE-confirmed graceful. --- ## #225 — Scene particles overpaint translucent world objects -**Status:** IN-PROGRESS — implementation/reviews and connected stress gate pass; final visual gate pending +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -9746,7 +11990,8 @@ right-aligned Total Experience/progress, and Unassigned Experience all updated. ## #213 — Retail client commands were sent to ACE as chat text -**Status:** IN-PROGRESS — command-family gate passed except two corrective fixes, pending re-gate +**Status:** CLOSED 2026-08-28 — retail commands route through the dedicated +client-command controller; the pending re-gate label was stale. **Severity:** MEDIUM **Component:** retained UI / chat commands / net @@ -9808,7 +12053,8 @@ in-world SmartBox readout. ## #212 — Toolbar shortcut numbers turn gray in physical combat -**Status:** IN-PROGRESS — implementation complete 2026-07-13, pending user gate +**Status:** CLOSED 2026-08-28 — implementation and regression coverage are +present; the pending-gate label was stale. **Severity:** LOW **Component:** retained UI / toolbar @@ -9915,7 +12161,9 @@ combat mode, and the jump bar still fills and hides normally. ## #209 — Retail jump power bar missing -**Status:** IN-PROGRESS — implementation complete 2026-07-13, pending live visual gate +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** IN-PROGRESS — implementation complete 2026-07-13, pending live visual gate **Severity:** MEDIUM **Component:** retained UI / movement @@ -9945,7 +12193,9 @@ the jump starts. Repeat after moving/resizing the bar and after relogging. ## #208 — Combat bar appears after logging in peacefully -**Status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate **Severity:** MEDIUM **Component:** retained UI / combat lifecycle @@ -10107,9 +12357,11 @@ 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:** OPEN — filed 2026-07-10. `post-M2`, LOW priority. Surfaced during the #193 heap analysis. +**Status:** CLOSED 2026-08-28 — `BeginFrame` prunes old instance groups and +current regression coverage pins the bounded lifetime. **Severity:** LOW — bounded, not a crash risk (unlike #193 which was the real OOM). **Component:** render — `WbDrawDispatcher._groups` (`Dictionary`, WbDrawDispatcher.cs:236). @@ -10117,7 +12369,9 @@ in the Release client. ## #193 — Client OOMs after extended play (~50 min) — FIXED -**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:** 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.) **Severity:** was MEDIUM→HIGH (crashed after extended sessions). RESOLVED. **Component:** render — `WbDrawDispatcher` per-frame instance-group reset. @@ -10130,7 +12384,9 @@ in the Release client. **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:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -10400,7 +12656,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:** OPEN +**Status:** CLOSED 2026-08-28 — owner-confirmed during ledger cleanup. **Severity:** LOW **Filed:** 2026-07-07 **Component:** rendering / scenery / streaming @@ -10424,7 +12680,9 @@ 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:** VELOCITY-MODEL REBUILD SHIPPED (Slices 1+2, `8bb8b204`→`54d56229`) — +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -10515,7 +12773,9 @@ 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:** OPEN — mechanism PINNED from live evidence; the knife-edge test not yet identified +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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) @@ -10633,7 +12893,8 @@ 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:** 🟡 BOTH FIXES SHIPPED 2026-07-06 + LOG-VERIFIED; user gate pends on #181 +**Status:** CLOSED 2026-08-28 — both fixes shipped and were log-verified; the +remaining visual was tracked separately in now-closed #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 @@ -10695,7 +12956,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:** OPEN +**Status:** CLOSED — USER-ACCEPTED 2026-08-28 **Severity:** LOW-MEDIUM (correctness/perf debt; 2× shell fragment load) **Filed:** 2026-07-06 **Component:** render — EnvCellRenderer MDI draw @@ -10716,6 +12977,26 @@ 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) @@ -10912,7 +13193,9 @@ 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:** 🟡 lighting fix SHIPPED + verified; #180 (camera strobe) BOTH halves +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -10931,7 +13214,9 @@ 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:** OPEN — root cause CONFIRMED; fix DEFERRED to the A7 +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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, @@ -10961,7 +13246,9 @@ 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:** 🟡 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:** 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). **FIX:** `ShadowShapeBuilder.FromSetup` gains a `partPoseOverride` (BSP part shapes only; CylSphere/Sphere unchanged); `RegisterServerEntityCollision` derives it via `GameWindow.MotionTableDefaultPose` — the wire MotionTableId's @@ -11023,7 +13310,9 @@ unregressed (door apparatus green). ## #174 — Door Use dies after the first jump: the RemoveLinkAnimations seam stripped animations without retail's queue drain -**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:** 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). **FIX:** the `MotionInterpreter.RemoveLinkAnimations` seam is retail `CPhysicsObj::RemoveLinkAnimations` 0x0050fe20 — a tailcall to `CPartArray::HandleEnterWorld` 0x00517d70 → `MotionTableManager:: @@ -11129,7 +13418,9 @@ 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:** 🟡 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:** 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). **Severity:** MEDIUM (remote-motion fidelity indoors; lands visibly late) **Filed:** 2026-07-05 **Component:** physics — remote dead-reckoning collision response @@ -11171,7 +13462,9 @@ unchanged. ## #172 — Town-network portal platform blocks instead of stepping up (CCylSphere family was never ported) -**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:** 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). **Severity:** HIGH (blocks dungeon access — gates the whole #137 repro) **Filed:** 2026-07-05 **Component:** physics — CylSphere object collision response @@ -11536,7 +13829,9 @@ actually steps at (Player_Move.cs / Creature GetRunRate usage). ## #165 — Remote entities penetrate walls ("swallowed a bit") before stopping -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -11619,7 +13914,9 @@ wall from acdream, matching the retail-observer view side-by-side. ## #166 — Slope-landing glide + bounce absent (retail "sled" on downhill jumps) -**Status:** FIX IMPLEMENTED 2026-07-30 (this session) — closure pends the +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -11795,7 +14092,9 @@ R5/R6 touches the action list. **Where:** ## #202 — Port the portal String-table lookup for WeenieError / UseDone text -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** LOW **Filed:** 2026-07-03 **Component:** ui / net @@ -11810,7 +14109,8 @@ R5/R6 touches the action list. **Where:** ## #195 — Retail chat ChatVM lacks Fps/Position providers — /framerate and /loc degrade -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — obsolete architecture; the duplicate +ChatVM/provider shape is gone and local commands use the dedicated route. **Severity:** MEDIUM **Filed:** 2026-07-02 **Component:** ui @@ -11913,7 +14213,8 @@ states `0x28` and `0x29`. ## #199 — Port the server-authoritative character raise flow -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — Campaign CA replaced optimistic mutation with +the authoritative one-request-in-flight response flow. **Severity:** MEDIUM **Filed:** 2026-07-02 **Component:** ui / core @@ -11932,7 +14233,8 @@ states `0x28` and `0x29`. ## #200 — Migrate remaining retail-window mounts (vitals/chat/toolbar/inventory + MockupDesktop) to RetailWindowFrame -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — stale two-stack migration list; its inline +mounts and MockupDesktop targets no longer describe current composition. **Severity:** LOW **Filed:** 2026-07-02 **Component:** ui @@ -11977,7 +14279,9 @@ BCL-only. ## #158 — Character window — deferred polish -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** LOW **Filed:** 2026-06-26 **Component:** ui @@ -12012,9 +14316,11 @@ field through `TerrainAtlas`, uploads a layer-indexed table, and applies it in the modern shader while leaving cell-scale alpha masks unchanged. The user confirmed the outdoor textures now match the expected scale. -The optional high-frequency Environment Detail Textures pass is a different -retail mechanism. It remains deferred under #226/TS-52 and does not keep this -fixed user-visible regression open. +The high-frequency detail pass is a different retail mechanism. #226 completed +its reachable user-visible target on 2026-08-21: building shells and EnvCell +geometry. Retail's reachable `ChangeRegion` caller passes zero landscape-detail +surfaces, so the former TS-52 landscape premise is retired and does not keep +this fixed user-visible regression open. **Files:** `src/AcDream.App/Rendering/TerrainAtlas.cs`; `src/AcDream.App/Rendering/TerrainModernRenderer.cs`; @@ -12031,7 +14337,9 @@ passed in the connected visual gate. ## #151 — Far-town (Arwic) collision broken at login: terrain barely grounds + city/perimeter walls never block -**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:** 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). **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) @@ -12208,7 +14516,9 @@ collision working. Also re-checks **#138**. ## #148 — Status-bar backpack icon should toggle the inventory window (stateful open/closed) -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** MEDIUM **Filed:** 2026-06-22 @@ -12241,7 +14551,9 @@ D.2b inventory visual gate. ## #146 — D.2b inventory capacity-bar visual polish -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** LOW **Filed:** 2026-06-22 @@ -12326,7 +14638,9 @@ See divergence register **AP-59**. ## #144 — Empty item-slot press+drag+release still emits a Click -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** LOW **Filed:** 2026-06-20 **Component:** ui — D.2b drag-drop spine (B.1) @@ -12390,7 +14704,9 @@ See divergence register **AP-59**. ## #139 — D.2b retail UI polish: chat buttons -**Status:** OPEN (narrowed 2026-08-09 — the chat-text-colors half CLOSED, see below) +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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) @@ -12749,7 +15065,9 @@ neighbour load/unload churn). ## #104 — Scene VFX particles not clipped to the PView visible cell set -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** LOW **Filed:** 2026-06-02 **Component:** render, vfx @@ -12786,7 +15104,9 @@ 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:** PARTIALLY RESOLVED (Phase U.2a, 2026-05-30, commit `d880775`) +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -13044,7 +15364,9 @@ Retail oracle for cell-id hysteresis: `acclient_2013_pseudo_c.txt:308742-308783` ## #94 — Held items project spotlight on walls -**Status:** OPEN — **UNBLOCKED 2026-07-11.** Retail ParentEvent/CreateObject child +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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. @@ -13586,7 +15908,8 @@ divergence and update commit messages / code comments to match. ## #73 — Retail-message centralization plan — per-feature string sweeps -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — retained as an engineering policy, not a +concrete client defect. **Severity:** LOW (per-feature work, not infrastructure) **Filed:** 2026-05-16 **Component:** ui / retail messages @@ -13815,7 +16138,9 @@ doors work after the heartbeat bump. ## #64 — Local-player pickup animation does not render -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-confirmed after validity audit. + +**Previous 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 @@ -14055,7 +16380,10 @@ for full evidence and rationale. ## #55 — Static-entity slow path reports ~1.45M `meshMissing` per 5s at r4 standstill -**Status:** OPEN +**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. **Severity:** LOW (no visible regression — affects a diagnostic counter, not rendered output) **Filed:** 2026-05-11 **Component:** rendering / `WbDrawDispatcher` static-entity classification path @@ -14695,7 +17023,9 @@ the local terrain normal, not the actor's facing. ## #41 — Residual sub-decimeter blips on observed player remotes (M3 baseline) -**Status:** FIX IMPLEMENTED 2026-07-17 — pending acdream-observer visual gate +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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) @@ -15753,7 +18083,7 @@ additional flash-shader work). **Description:** Lightning/storm sky visuals still do not match retail. A 2026-04-28 named-retail recheck disproved the prior assumption that `SkyObject.PesObjectId` drives sky-render flash particles: `SkyDesc::GetSky` copies the field into `CelestialPosition.pes_id`, but `GameSky::CreateDeletePhysicsObjects`, `GameSky::MakeObject`, and `GameSky::UseTime` never read it. -**Root cause / status:** Open again. The sky-PES path is non-retail and must stay disabled for normal rendering. The remaining mismatch likely lives in the sky/weather mesh material path, the lightning/fog flash path, or another weather subsystem outside `GameSky`; do not reintroduce per-SkyObject PES playback without new decompile evidence. +**Root cause / status:** CORRECTED 2026-08-23 — the "must stay disabled" ban is lifted by new decompile evidence: retail DOES play the sky carriers' PES, via the Setup `DefaultScript` → `CPhysicsObj::makeObject` default-script path (never via the `pes_id` column, which remains dead — that half of the April finding stands). Sky default-script playback is production as of the C.1.5c port (`SkyPesFrameController`, research `2026-08-23-sky-default-script-port.md`). The lightning-flash carriers (`0x02000BA6` → `0x33000453`, windows 0.03–0.19 / 0.40–0.50 / 0.91–0.98 on Rainy groups) now fire through it; whether the flash PRESENTATION matches retail (sky-wide crossfade vs a sprite) still needs its own storm-window gate. **Files:** - `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — sky/weather mesh draw, material state, pre/post split @@ -15775,7 +18105,8 @@ additional flash-shader work). ## #3 — Client clock drifts from retail after ~10 minutes (periodic TimeSync missing) -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — periodic TimeSync is parsed and routed into +the world clock; the old missing-clock premise is no longer current. **Severity:** MEDIUM **Filed:** 2026-04-25 **Component:** net / sky @@ -15800,11 +18131,32 @@ additional flash-shader work). ## #28 — Aurora ("northern lights") effect not rendered -**Status:** OPEN +**Status:** DONE — connected gate USER-PASSED 2026-08-23 (commit `18fce7bb`). +The accepted residual: the aurora reads somewhat more intense in acdream than +retail — the emitters are verbatim; the delta is the linear-light compositing +(+ optional Atmospheric-pack bloom) brightening faint additive glows over a +dark sky vs retail's gamma-space add. Accepted by the user ("gate pass for +now. Might reopen it!") — if reopened, the principled fix is a gamma-faithful +compensation on ADDITIVE particle contributions, never an aurora-only factor. **Severity:** LOW (aesthetic feature-parity) **Filed:** 2026-04-26 **Component:** sky / vfx +**2026-08-23 resolution (the C.1.5c mechanism, found):** the sky PES ids ride +the carrier Setups' own `DefaultScript` (byte-equal to the dead `pes_id` +column) and retail plays them through the ordinary default-script machinery — +`GameSky::MakeObject @0x00506EE0` → `CPhysicsObj::makeObject @0x00513970` +(`state |= 0x80000`) → `CPhysicsObj::animate_static_object @0x00513DF0`. The +aurora carrier (`0x02000714` → PES `0x330007DB`) is present in ALL 20 day +groups around the clock; visibility is purely "faint additive glows over a +dark sky", pulsing on the emitters' 6.7/15/55-min rebirth cycles. The April +"colored wash" experiment was run inside a Rainy day group's lightning window +— the flash/thunder PES at the camera anchor, not the aurora, made the wash. +`SkyPesFrameController` is now the production owner (no env flag), with +retail's slot-identity persistence and the `calc_draw_frame @0x0050DFA0` +facing law in the particle renderer. Full chain: +`docs/research/2026-08-23-sky-default-script-port.md`; register AD-112. + **Description:** Retail renders a dynamic colored "light play" effect in the sky during certain Rainy/Cloudy DayGroup time windows. The user describes it as aurora-borealis-style. acdream renders no comparable effect. **Root cause / status:** Open again. The prior root cause was wrong: `CelestialPosition.pes_id` exists in the retail header and is populated by `SkyDesc::GetSky`, but named retail `GameSky` code does not read it during sky object creation, update, or draw. A 2026-04-28 C.1 experiment that played those PES ids produced colored blobs/wash that did not match retail's broad aurora-like rays, and the path is now debug-only behind `ACDREAM_ENABLE_SKY_PES=1`. @@ -16208,7 +18560,9 @@ flip-a → []; flip-b → [0x100]. ## #114 — Indoor PView shell-clip regions are not draw-quality (clip scoped to outdoor roots) -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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`) @@ -16243,7 +18597,9 @@ clip against the accumulated portal view (`planeMask=0xffffffff` :427922). ## #115 — Camera feels draggy/jittery vs retail when turning in cramped interiors -**Status:** OPEN +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous status:** OPEN **Severity:** LOW-MEDIUM (feel; no geometry errors reported) **Filed:** 2026-06-11 (user, same gate session) **Component:** camera (collision sweep / smoothing) @@ -16260,7 +18616,9 @@ 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:** OPEN (narrowed further, 2026-07-30) — **shape-2 CLOSED** +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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 @@ -16525,6 +18883,18 @@ failing step pins which candidate fires. **Filed:** 2026-06-11 (T5 comprehensive gate, user items 9+13) **Component:** render — mesh upload / content inclusion +**AMENDED 2026-08-23 (#426):** the "Retail's skipNoTexture never draws them +either" conclusion below is WRONG as a general rule — retail only skips an +untextured (solid-colour) subset on a BUILDING SHELL or inside an EnvCell; +an ORDINARY object's untextured polygons DO draw, and the extraction was +dropping every one of them client-wide via a NoPos misread (fixed by #426). +Post-fix, `Issue119UpNullGfxObjDumpTests` shows BOTH of this entry's +GfxObjs now gate DRAWS on every polygon instead of extracting to nothing — +whether that geometry is actually visible on screen (i.e. whether either +object is a building-shell part, which would still skip it at draw time) +is unverified and NOT the same question as the extraction-level "no draw" +claim this entry made in 2026-06-12. + **RESOLUTION (2026-06-12) — three root causes, fixed in sequence, each pinned by the ACDREAM_DUMP_ENTITY decisive probe (`3cf6bcc`):** 1. **`2163308` — Tier-1 cross-entity batch serving** (the broken stairs + @@ -16655,7 +19025,9 @@ 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:** FIXED 2026-06-11 (`dede7e4`) — pending re-gate (watch for +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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) @@ -16708,7 +19080,9 @@ 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:** FIXED 2026-06-11 — pending re-gate +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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) @@ -16935,7 +19309,9 @@ staircase entity's per-frame draw decision. ## #129 — Doors/doorways leak through terrain and houses from over a landblock away -**Status:** FIX SHIPPED — awaiting user visual gate +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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) @@ -16972,7 +19348,10 @@ 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:** FIX 2 SHIPPED — awaiting user visual re-gate +**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. **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) @@ -17100,6 +19479,13 @@ DrawDynamicsParticles only sees dynamics-last cone survivors. **Gate:** stand inside, look out the doorway at the town portal — the swirl renders through the door. +**2026-08-27 ordering correction (#451):** the old "once per frame after the +look-ins" placement was sufficient for this gate but did not preserve the +installed `outside_view` and could repaint an already-drawn open-air building. +Ownerless emitters now submit once per outside-view slice with that slice's +clip slot. When building look-ins exist they enter the pre-building alpha +barrier; otherwise they drain at the end of `LScape::draw`. + --- ## #132 — Candle flame disappears when the through-opening background is behind it @@ -17140,6 +19526,13 @@ against interiors). The owner-id filter carries over; cell-pass and dynamics-pass emitters keep their own passes (owners never in the outdoor-static set → no double-draw). +**2026-08-27 correction (#451):** the post-frame placement fixed this narrow +flame-overpaint case but was too late for nested open-air cathedral cells: an +exterior waterfall could repaint their completed opaque floor. Outdoor-static +and ownerless particles now submit inside `LScape::draw` under the exact +outside-view clip slot, with retail's pre-building/per-building alpha barriers; +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. @@ -18759,7 +21152,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:** OPEN +**Status:** FIXED 2026-08-28 **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. @@ -18812,9 +21205,17 @@ 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:** FIX IMPLEMENTED, awaiting the connected acceptance gate (§7 of the +**Status:** CLOSED 2026-08-28 — owner-directed ledger cleanup. + +**Previous 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), @@ -18935,7 +21336,9 @@ Full analysis, with the headless test's structural immunity explained: ## #321 — `DatSoundCacheTests` concurrent-decode-dedup fails under full-suite load -**Status:** OPEN +**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. **Severity:** LOW (test-only so far; no production symptom observed) **Filed:** 2026-08-05 **Component:** content / audio cache @@ -18969,7 +21372,13 @@ strictly worse than a red test. ## #322 — Two callers compute the same two pre-placement flags from the same two inputs -**Status:** OPEN +**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. **Severity:** LOW (internal refactor debt; NOT a retail divergence) **Filed:** 2026-08-05 (C5b review, finding S1 — the successor #275 closed without filing) @@ -19016,7 +21425,9 @@ 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:** OPEN +**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. **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/README.md b/docs/README.md index 328d201e..71558c70 100644 --- a/docs/README.md +++ b/docs/README.md @@ -82,6 +82,16 @@ document in the same change; do not leave both claims standing. - [`superpowers/specs/`](superpowers/specs/) and [`superpowers/plans/`](superpowers/plans/) are per-slice design and execution records. Completed plans remain historical. +- [`ci-and-releases.md`](ci-and-releases.md) is the SSOT for the Gitea CI + pipeline, the self-hosted runners, and how alpha releases are published. + Load-sensitive tests live in `Lane=Timing`; see + [`release-gate.md`](release-gate.md) before adding to it. +- [`launch-options.md`](launch-options.md) is the SSOT for every environment + variable and command-line argument the client reads, including what each one + changes about the run beyond its obvious effect. Read the side-effects column + before trusting any measurement. Enforced by + `LaunchOptionsDocumentationTests`: a flag without a row fails the build, and + so does a row whose read site was deleted. - [`audit/`](audit/) contains completion and conformance audits. - [`reference/ace-commands.md`](reference/ace-commands.md) preserves the local ACE server's complete in-game command catalog and points to the authoritative diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index 184e26e4..544d92d3 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -137,6 +137,68 @@ loads none). The headless adapter projects entity snapshots on demand from the canonical Runtime view, subscribes to Runtime's ordered events, and borrows the exact Runtime selection owner; it does not mirror gameplay state. +Graphical plugin panels are first-class retained windows. A plugin calls +`IUiRegistry.AddPanel` with a BCL-only `PluginPanelDescriptor`; Core's scoped +host authenticates the owner from the loaded manifest, and App derives the +stable identity `plugin:{pluginId}:{windowId}`. The host, not the plugin, owns +window geometry, z-order, persisted visibility, minimize/restore chrome, and +the shared right-edge plugin shelf. Minimizing only hides the presentation: +the plugin session, event subscriptions, automation policy, and binding object +remain live. Legacy `AddMarkupPanel` registrations are enriched into the same +first-class path, so API-v1 plugins keep working without a second lifecycle. +The markup vocabulary includes nested groups plus retained tab, toggle, +slider, editable-field, and retail-menu controls. Fields bind live +`Action` change/submit callbacks and menus bind an +`IEnumerable` plus selection callback, so plugin-owned profile/rule +editors stay behind the BCL contract instead of importing App widgets. These +are presentation bindings only and never become parallel gameplay owners. + +Durable plugin data uses the BCL-only `IPluginHost.Storage` contract. Core's +manifest-authenticated scoped host prefixes every logical key with the loaded +plugin id; graphical composition writes atomically beneath the per-user config +root (`plugins/{pluginId}/...`). Plugins receive neither another plugin's +namespace nor a machine-specific path. Hosts without durable storage expose +`NoOpPluginStorage` and report the capability unavailable. +The additive `List(prefix)` operation enumerates only keys inside that same +authenticated namespace, allowing plugins to discover explicit import/export +files without receiving a filesystem path or crossing plugin ownership. + +`IPluginHost.Automation` is the additive gameplay-automation projection. Its +character, spell, magic, chat, combat, equipment, item, loot, fellowship, +enchantment-observation, and navigation +groups contain BCL-only immutable snapshots plus attempt-style commands; the +graphical implementation borrows the exact `GameRuntime` +character/action/entity/object/vendor/fellowship owners. Item projections also +carry Virindi's stable ObjectClass plus ordered ObjDesc subpalette samples; +the graphical host resolves each representative RGB directly from portal DAT +using VTank's sample-index formula. MossTank owns all +macro policy (buff planning, target rules, selection scoring, corpse policy, +loot-rule ordering and action timing). In particular, `ICombatAutomation` +does not create a plugin combat model: each hostile capture is a detached +point-in-time projection of `RuntimeHostileTargetQuery`, and physical commands +enter the canonical `RuntimeCombatModeState` / `RuntimeCombatAttackState` +press-charge-release state machine. Item and loot commands similarly enter +App's one `ItemInteractionController`: appraisal, use/apply, pickup, +move/split/merge/drop/give, retail 0x027D salvage, and current-vendor sale +reuse the same readiness checks, reservations, wire sends, and authoritative +completion/object-table signals as retained retail UI. Plugins never hold an +optimistic inventory or vendor shadow. Navigation similarly projects live and +server-accepted position, portal/object state, and semantic movement levels; +the App host applies those levels through Runtime's one command interpreter. +Route sequencing, steering cones, follow breadcrumbs, checkpoint policy, +door/lockpick decisions and portal retry behavior remain plugin-owned. The +shared enchantment-observation group is deliberately a confirmed-cast timer +ledger rather than another authoritative spellbook: the host records successful +local duration casts and cooperating plugins can report their own confirmed +casts, matching VTank's `LogSpellCast` contract. It resets at session detach; +dispel/debuff policy remains plugin-owned. The +inert default remains +`NoOpAutomationSurface`, preserving one plugin code path on hosts without a +live gameplay session. +`ICharacterInfo.Name` projects the canonical local `ClientObject.Name` (empty +when unavailable) solely for per-character plugin profile scoping; it does not +introduce a second identity owner. + Core `SelectionState` is the sole selected-object owner for world, radar, inventory, paperdoll, toolbar, use/examine consumers, and plugins; `IPluginHost.Selection` exposes that same state and retail-style old/new callback. @@ -332,7 +394,19 @@ src/ Installation/ -> portable four-DAT validation, Windows retail path discovery, versioned JSONL bake-process orchestration, and atomic SHA/size/tool-version - install-record verification and recovery; one + install-record verification and recovery; + startup discovery begins only after the desktop + window opens, and exceptional whole-pak hashing + reports its long-read status in that window; + content recipes resolve through one compiled + None/Overlay/FullRebuild/Verify migration ledger; + bounded changes build one cumulative filtered + overlay and publish `pak/content.current.json`, + while full rebuilds bake beside the live base and + swap only after candidate verification; a tiny + `pak/content.client-pending` gate survives a + crash/restart until the active client is + confirmed compatible; one OS-handle lease serializes recovery/install per DataDirectory; a second OS-held publication lock plus durable per-transaction nonce makes @@ -362,7 +436,9 @@ src/ is never persisted, and permits HTTP only for a loopback fixture; production remains pinned HTTPS ViewModels/ -> thin MVVM projection over Launcher.Core, - including the first-run DAT/bake wizard and + including the first-run DAT/bake wizard, explicit + world-data work confirmation (kind, reason, + free-space guidance, progress/cancellation), and nonfatal startup/manual update state, actions, progress, cancellation, rollback, and errors -> references Launcher.Core only (Platform transitively); it never owns @@ -372,6 +448,28 @@ src/ -> Linux launcher/probe/headless flows remain portable; graphical-client actions are explicitly disabled until Modern Runtime Slice L resumes + Prepared-content launch contract + -> `install.json` remains the strict backward-compatible base-pak authority + -> format 2 / recipe 6 is the current full-package contract: globally + shared texture payloads, independent adaptive blob compression, and + source-native BC1/2/3 for surfaces that require no pixel-local edit + -> format changes always use an explicit confirmed full rebuild; the + launcher preflights 2 GiB free space, shows long-work progress, validates + the candidate beside the active package, and promotes atomically + -> the prepared texture LRU is bounded to 64 MiB / 1,024 entries; GPU atlas + ownership remains in App and the whole-file mmap is virtual, not a + committed-copy cache + -> optional `pak/content.current.json` binds one cumulative overlay to the + base SHA; there is never an unbounded overlay chain + -> `pak/content.client-pending` prevents newly migrated content from + becoming launchable before the matching client check/install succeeds, + including across launcher restart + -> launcher session config carries base + optional overlay paths and both + recipe identities only for layered launches + -> App and Headless construct one `LayeredPreparedAssetSource`; overlay + Missing falls through to base, while overlay Corrupt is authoritative + for both render and collision reads + AcDream.Headless/ Linux/Windows no-window production host Program.cs -> CLI entry only Configuration/ -> strict versioned process/session config @@ -393,6 +491,10 @@ src/ IGameState.cs -> done IEvents.cs -> done ISelectionService.cs -> done + IPluginStorage.cs -> manifest-scoped durable text profiles + Automation.cs -> character/spell/magic/chat automation groups + CombatAutomation.cs -> hostile snapshots + retail combat attempts + EnchantmentAutomation.cs -> shared confirmed duration-cast timer ledger AcDream.App/ Layer 1 + Layer 4 wiring Platform/ diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index b842aa53..a7716216 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-07-31 +# Retail Divergence Register — current through 2026-08-28 **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, @@ -37,10 +37,11 @@ accepted-divergence entries (#96, #49, #50). --- -## 1. Intentional architecture (IA) — 20 active rows (IA-23 filed 2026-08-17 at the night-round review fix round (F8) — the House tab's not-yet-expired purchase-restriction line renders .NET's culture-default `DateTime.ToString()` where retail renders the C runtime's `strftime("%c", localtime(...))`, a different formatting engine producing a different-shaped (but equivalent-intent) date string; IA-22 filed 2026-08-13 — the #391 user-directed modern-only curated resolution list + desktop-mode default, replacing retail's full adapter enumeration + authored 800x600 default) +## 1. Intentional architecture (IA) — 23 active rows (IA-26 filed 2026-08-23 — the pack-gated procedural night sky replacing the stretched DAT star layer; IA-25 filed 2026-08-22 for Campaign VM VM6's opt-in weather-driven foliage wind — a render-only vertex displacement keyed by the DAT-classified `WeatherKind`, not the raw day-group index, with no authored retail wind direction to read; IA-24 filed 2026-08-22 for Campaign AR's opt-in real-time sun/moon directional shadows; IA-23 filed 2026-08-17 at the night-round review fix round (F8) — the House tab's not-yet-expired purchase-restriction line renders .NET's culture-default `DateTime.ToString()` where retail renders the C runtime's `strftime("%c", localtime(...))`, a different formatting engine producing a different-shaped (but equivalent-intent) date string; IA-22 filed 2026-08-13 — the #391 user-directed modern-only curated resolution list + desktop-mode default, replacing retail's full adapter enumeration + authored 800x600 default) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| +| IA-26 | **Filed 2026-08-23, user-directed ("I want the night sky to look very good").** When the Atmospheric render pack is the active runtime, the DAT star layer (GfxObj `0x010015EF`, identical across all 20 Dereth day groups — a small texture stretched over a 10-poly dome cap) renders instead as `sky.frag`'s fully procedural night sky: hash-derived stars on a cube-face grid in three density tiers plus sparse diffraction-spiked standouts, sized in SCREEN pixels via derivatives (crisp at any resolution/FOV — the stretched-texture flaw this exists to remove), over a 0.4–1.3% cool background mottle. The art direction is the user-approved 2026-08-23 generator recipe. The draw is forced onto the additive pipeline; the day/night fade derives from the star layer's own retail lighting product so timing matches the authored keyframes. Retail default (pack inactive) is byte-untouched. | `src/AcDream.App/Rendering/Shaders/sky.frag` (the `uParamA` branch); `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` / `.Rhi.cs`; `src/AcDream.App/Composition/FrameRootComposition.cs` | Pack-gated opt-in like IA-24/IA-25; `EnhancedNightSkyRuleTests` pins the gate, the exact star-layer id, the forced-additive draw, and the pack-runtime wiring. | A future region whose star layer uses a different GfxObj id keeps its retail stars even with the pack active (the swap simply never fires); anyone comparing acdream-with-pack to retail at night will see a deliberately different (denser, crisper) starfield. | `EnhancedNightSkyRuleTests`; user gate 2026-08-23; gen_starfield2.py (scratchpad, seed 11) | | IA-1 | Contact-plane pre-seed on grounded movers (**#96 ACCEPTED** per ISSUES.md) — retail's `CTransition::init` clears `contact_plane_valid`; we seed from the body's previous-frame plane | `src/AcDream.Core/Physics/PhysicsEngine.cs:919` | Removing it broke last-step stair `step_up` (`892019b`, reverted); seed propagates the body's *real current* plane, behavior matched retail in the A6.P3 gates | A stale pre-seeded plane lets `AdjustOffset` project sub-step 1 onto a plane retail wouldn't have yet — wrong slope motion / step-up acceptance right after leaving a surface | `CTransition::init`, pc:272547 family | | IA-2 | Lateral self-heal beyond retail's keep-curr: when no candidate contains the sphere, try `FindVisibleChildCell` over the claim's stab-list before keeping the claim | `src/AcDream.Core/Physics/CellTransit.cs:912` | Reuses the recovery retail's own `AdjustPosition` performs (:280028 stab-list mode), applied at the `find_cell_list` site to heal near-miss claims without a doorway crossing | In containment-gap geometry, membership flips to a neighbouring room where retail keeps curr — wrong render root / collision cell at gap positions | `find_cell_list` keep-curr pc:308788-308825; `find_visible_child_cell` :311444 | | IA-3 | **NARROWED 2026-07-17 — `get_state_velocity` may prefer a nonzero dat cycle velocity (`MotionData.Velocity × speedMod`) over the decompiled constant.** Production grounded player/remote translation no longer consumes this value; both use the literal CSequence root Frame. The accessor remains observable for jump launch and headless/test fallbacks | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`get_state_velocity`); grounded owners in `PlayerMovementController` / `RemotePhysicsUpdater` | Installed Humanoid Walk/Run MotionData velocity is zero, so the retail constants remain the jump/fallback result. A nonzero exotic/modded cycle may override them, preserving the earlier adapter contract without affecting ordinary grounded motion | Jump horizontal speed for an exotic MotionTable with nonzero authored velocity can differ from the retail binary's constants | `CMotionInterp::get_state_velocity` 0x00527D50; `CPhysicsObj::UpdatePositionInternal` 0x00512C30 | @@ -61,10 +62,12 @@ accepted-divergence entries (#96, #49, #50). | IA-21 | When ACE sends player BoolProperty `68` (`SpellComponentsRequired`) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. | `src/AcDream.App/Spells/SpellComponentRequirementService.cs` | A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | `ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90`; `ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50`; `CSpellBase::InqScarabOnlyFormula @ 0x00597050` | | IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction. **Amended 2026-08-16 (#407, Campaign CC gate round 1):** the dropdown now offers `DisplayModeCatalog.WindowedResolutions` — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a WINDOWED pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost no modes (the live RDP display exposed only 1920x1080 + the 2056x1290 desktop, starving the dropdown). The original "an offered mode is supported by construction" invariant now holds for the FULLSCREEN half only: the fullscreen apply still validates against the hardware `Resolutions` list plus `GlfwDisplayModeSwitcher`'s monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388; the #392 apply-result seam is that family's open follow-up) — "Graphics mode not supported" crashes remain unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 | | IA-23 | **Filed 2026-08-17 at the night-round review fix round (F8).** `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s not-yet-expired branch renders `"You may buy another landscape house at " + strftime("%c", localtime(timestamp + 0x278d00)) + ". This restriction does not apply to apartments."` — byte-decoded from raw pushed literals at `@0x004a3265`/`@0x004a321d`/`@0x004a3235` (all three text pieces confirmed; a prior filing had wrongly called this "unrecoverable"). This port renders the SAME three pieces, in the same order, with the same expiry-timestamp math, but formats the middle date/time piece with .NET's culture-default `DateTime.ToString()` (no explicit format string) rather than the C runtime's `strftime("%c", ...)` — the two engines do not share a format table, so the RENDERED SHAPE of the date/time differs (e.g. .NET's short numeric date+time vs the CRT's `Ddd Mon DD HH:MM:SS YYYY`-style locale string) even though both express "the process's own locale's full date+time" and use the SAME underlying instant (local time, matching retail's `localtime()`). | `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (`Recompute`'s not-expired branch) | Both are "whatever the process locale says" full date+time strings; no game-logic reads or parses this text back, it is pure chat-scroll presentation, so a differently-shaped (but equally legible) date string carries no functional risk | A retail-side-by-side visual comparison will show a differently formatted date/time (not a byte-identical `strftime("%c")` reproduction) — cosmetic only | `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`; `strftime`/`localtime` CRT calls at `@0x004a322c`/`@0x004a3216` | +| IA-24 | **Filed 2026-08-22, Campaign AR.** An explicitly selected atmospheric render pack adds cascaded real-time directional shadows from terrain, trees, buildings, players, monsters, and other retained outdoor casters. The one shadow direction follows the visible authored sun, then the dominant haloed moon (`0x01001F6A`), then the secondary moon (`0x01001F67`); a moon supplies direction only while colour/energy remains retail's single interpolated `SkyTimeOfDay.DirColor × DirBright` channel. **While shadows render, the pack's receiver vertex shaders (`mesh_atmospheric.vert`, `terrain_atmospheric.vert`) also take the outdoor directional LIGHTING direction from that same celestial source instead of retail's authored `uLights[0]` direction, so the lit term agrees with the shadow direction; whenever the shadow gate is closed (night, user strength 0, portal cover, indoor) the flag bit is clear and both shaders fall back to the plain pipeline's authored-light expression (Campaign VM VM6 round 5, `754d59d9`).** Retail renders none of these real-time object-shadow maps and does not expose a second moon light. | `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`; `src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs`; pack-only receiver shaders; evidence `docs/research/2026-08-22-dereth-celestial-shadow-sources.md` | This is the user-requested headline graphics enhancement and is strictly opt-in. The retail path remains the default and authoritative fallback; pack-off does not build/select shadow work or change `SceneLighting`. One selected source reuses one cascade array, so moon support does not multiply shadow resources. | Pack-on output intentionally differs from retail. A wrong celestial identity/transform or stale source transition would visibly misalign shadows from the sky; pack-off output changing would violate the campaign's primary safety contract. | `SkyDesc::GetLighting @0x00500A80`; `GameSky::UseTime @0x005075B0`; installed Region `0x13000000`; cited research note | +| IA-25 | **Filed 2026-08-22, Campaign VM VM6.** An explicitly selected atmospheric render pack sways procedural-scenery foliage (trees/bushes — entity ids in the `0x8XXYYIII` `ProceduralSceneryIdAllocator` namespace) in `mesh_atmospheric.vert` and the four `directional_shadow_world_*` caster vertex shaders, driven by a weather-table lean/branch/flutter vertex displacement (`foliage_wind.glsl`, `FoliageWindModel` CPU mirror) whose mean/gust strength is looked up per DAT-classified `AcDream.Core.World.WeatherKind` (Clear/Overcast/Rain/Snow/Storm — the same classification `WeatherState.cs` already derives from the active day group's authored name, not the day group's raw index, which carries no weather meaning by itself) and eases toward its target over `WeatherSystem.TransitionSeconds` (10 s) so a weather change never snaps. Retail's fixed-function renderer applies no per-vertex wind displacement to any scenery mesh — Dereth's trees are static geometry. Wind direction (`wind-direction-degrees`, default 225°) is a plain pack default: there is no authored retail wind direction to read (no wind data exists in retail at all). Render-only: `WorldPicker` picks the undisplaced mesh, so a swaying leaf can be up to `lean + branch` metres from its pick volume at the moment of a click; foliage subsets are cosmetic scenery, not interactable in retail either. | `src/AcDream.App/Rendering/Shaders/foliage_wind.glsl`; `src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs`; `src/AcDream.App/Rendering/Packs/FoliageWindModel.cs`; `src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs` (`ResolveFoliageWind`); `src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs` (wind settings + `FoliageWindByWeather`) | Explicitly opt-in graphics enhancement — the retail path (`mesh_modern`, `terrain_modern`, `mesh_detail`) never reads `BatchData.flags` bits 1/2 and is pixel-identical with the pack off. The classification never touches Runtime/Core physics — the collision BSP is the trunk, and picking against the undisplaced mesh has no gameplay consequence since foliage is not interactable. | Pack-on output intentionally differs from retail (moving foliage where retail has none). A wrong classification bit would sway a non-foliage object or leave a real tree still; a caster/receiver clock or amplitude mismatch would visibly misalign a leaf's shadow from the leaf itself. Pack-off output changing would violate the campaign's primary safety contract. | None — retail applies no vertex wind displacement to any geometry; `ProceduralSceneryIdAllocator` (top-nibble-0x8 entity-id namespace, existing acdream mechanism, not retail) | --- -## 2. Adaptation (AD) — 85 active rows (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- +## 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- -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 @@ -108,6 +111,9 @@ readiness/requeue adaptation. See | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| +| AD-115 | **Filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16), classification: intentional.** `AppraisalUiController.BuildCharacterTitleDisplay` composes examination element `0x10000151` (Profession/title): when Int 261 `CharacterTitleId` is absent/unresolvable AND String 5 `Template` is also absent, it returns an empty string, and `ClearCreatureText` has already blanked the element for this `ApplyCreature` call, so the element stays cleared. Retail never clears `0x10000150`/`0x10000151`/`0x10000152` anywhere — neither `CharExamineUI::Show @0x004AB5D0` nor `BasicCreatureExamineUI::Init @0x004AB9C0` writes an empty string to those elements — so in this exact case retail would keep showing the PREVIOUS assessed target's title text on screen instead of clearing it. | `src/AcDream.App/UI/Layout/AppraisalUiController.cs` (`BuildCharacterTitleDisplay`, `ClearCreatureText`) | Deliberate improvement over retail's quirk: a stale leftover title from a prior target reads as more confusing/wrong to a player than a blank line for the current one; review F16 (2026-08-25) accepted the clear-on-no-source behavior as intentional. | None expected — this is a deliberate, reviewed divergence, not a game-feel regression; a future retail-faithfulness audit assuming `0x10000151` always mirrors retail's persistent stale-text behavior would be surprised to see it clear instead when the current target's title can't be resolved. | `CharExamineUI::Show @0x004AB5D0`; `BasicCreatureExamineUI::Init @0x004AB9C0` | +| AD-114 | **Filed 2026-08-25 at Campaign AS slice AS2, owner-ruled 2026-08-25 (verbatim "we animate it, and I like it").** acdream's examination-window preview (`CreatureAppraisalFramePresenter` / `RetailCreatureAppraisalCloneFactory`) shares the assessed target's already-resolved live MeshRefs and re-synchronizes them every frame, so the preview clone plays the SAME current animated pose the live target is actually doing right now (attack, cast, run, idle, ...). Retail's `BasicCreatureExamineUI::Init @0x004AB9C0` instead clones the selected physics object ONCE, fixes its heading at 191.367905°, and lets its own private `CreatureMode` animate that clone independently — decoupled from whatever the live target is currently doing. | `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs` (`CreatureAppraisalFramePresenter`, `RetailCreatureAppraisalCloneFactory`) | Explicit owner direction, 2026-08-25 (`docs/plans/2026-08-25-assess-window-parity-campaign.md`: "The animated 3D paperdoll is an INTENTIONAL acdream deviation... Keep it"), noted alongside the owner's own observation that retail's static-clone colors are buggy — porting the decoupled-motion clone would not even be a faithfulness win here. | None expected — a deliberate, user-approved visual improvement over retail's decoupled clone motion, not a game-feel divergence; a future faithfulness audit assuming the preview mirrors retail's independent `CreatureMode` cycle would be surprised to see it track the live target's pose instead. | `BasicCreatureExamineUI::Init @0x004AB9C0`; `docs/plans/2026-08-25-assess-window-parity-campaign.md` | +| AD-112 | **Filed 2026-08-23 with the sky default-script port (issues #28/#2, phase C.1.5c).** Retail plays the sky carriers' aurora/lightning/thunder PES by creating real `CPhysicsObj`s in the viewer-centered sky cells (`GameSky::MakeObject @0x00506EE0` → `CPhysicsObj::makeObject @0x00513970`; the Setup's `DefaultScript` marks `state \|= 0x80000` and `CPhysicsObj::animate_static_object @0x00513DF0` ticks `ScriptManager::UpdateScripts` + the ParticleManager). acdream plays the same scripts through `PhysicsScriptRunner` synthetic owners anchored at the camera (`SkyPesFrameController`), pass-routed by `props & 1` into the existing SkyPreScene/SkyPostScene particle draws — no sky-cell physics objects exist. Slot persistence keys on (index, gfx id, properties), the `CreateDeletePhysicsObjects @0x005073C0` identity law. The heading/rotation → pose axis mapping uses `UnitY` where retail's `GameSky::CalcFrame @0x00506F80` runs `set_heading` + `grotate`; every current PES carrier authors 0°/0° so the axis is unexercised. | `src/AcDream.App/Rendering/SkyPesFrameController.cs`; `src/AcDream.Core/World/SkyDescLoader.cs` (`SkyObjectData.DefaultScriptId`) | Retail's sky cells are camera-centered, so a camera-anchored world-space owner is the same geometry; the script/emitter engines are the production ones shared with entity effects; `SkyPesFrameControllerTests` pins the identity/persistence/window lifecycle. | A sky carrier authored with nonzero heading/rotation angles would orbit around the wrong axis; a PES whose hooks depend on real part frames (part_index targeting) would find only the synthetic owner's root pose. | `GameSky::MakeObject @0x00506EE0`; `GameSky::CreateDeletePhysicsObjects @0x005073C0`; `CPhysicsObj::animate_static_object @0x00513DF0`; `docs/research/2026-08-23-sky-default-script-port.md`; `SkyPesFrameControllerTests` | | AD-111 | **Filed 2026-08-17 at the systemic escape-normalization round (commit 967b9c57).** The appraisal report's WIRE-string shaping (`ItemAppraisalTextLayout.Shape`) converts a literal two-character ` ` in server-sent fragment text (long description, use text) into a real line break. Retail does NOT: `ItemExamineUI::AddItemInfo @ 0x004AC050` hands wire text straight to `UIElement_Text::AppendTextWithFont` with no `StringTableMetaLanguage::UnescapeString` pass (that decode belongs to `StringInfo` resolution — DAT/authored strings — which the same round ported to `DatStringResolver`/`RetailStringEscapes` as the single source decode), so retail renders a wire backslash-n literally. Pre-existing behavior documented as wire-domain at the same round (it shipped inside the user-accepted Slice-3 assessment surface); the sibling inscription path (`IndicatorDetailText.Shape`) was returned to retail-verbatim in the same commit. | `src/AcDream.App/UI/Layout/ItemAppraisalReport.cs` (`ItemAppraisalTextLayout.Shape`'s domain-commented replace) | Accommodates literal " " sequences appearing in ACE database strings; server strings carrying REAL line-break characters flow through the same split either way, so the replace only ever fires on content retail would render with a visible backslash-n. | A wire string legitimately containing the two characters backslash+n (a file path, ASCII art in a description) renders with a spurious line break where retail shows it literally. | `ItemExamineUI::AddItemInfo @ 0x004AC050`; `UIElement_Text::AppendTextWithFont` (direct append, no unescape); `StringTableMetaLanguage::UnescapeString @ 0x0067BDC0` (the decode retail applies ONLY at StringInfo resolution) | @@ -180,7 +186,7 @@ readiness/requeue adaptation. See | AD-75 | **Filed 2026-08-11 at Campaign OP slice OP3 (D5).** Urgent Assistance (`0x10000206`) and Report Abuse (`0x10000207`) never call `ShellExecuteA` against `http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic` — the endpoint is dead in 2026. Each button instead ALWAYS emits its own byte-verified retail failure body (the `ShellExecuteA`-failure `MessageBoxA` text, `(Error code %d)` dropped since no real Win32 error ever occurs, the URL kept verbatim) through the interface-text seam (`RetailLogTextType.ClientLocal`) instead of a native `MessageBoxA` popup. | `src/AcDream.Core/Chat/OptionsPanelText.cs` (`UrgentAssistanceUnavailable`/`ReportAbuseUnavailable`); `src/AcDream.App/UI/Layout/OptionsPanelController.cs` (button wiring) | The URL genuinely does not resolve to a live Turbine support endpoint; attempting `ShellExecuteA` would open a browser to a dead page rather than usefully fail. The retained failure TEXT is retail's own (byte-verified), just always shown instead of conditionally on a real launch failure, and routed to acdream's existing interface-text channel rather than a modal OS dialog (retail's own EoR-era mechanism has no acdream analogue for a one-off native `MessageBoxA`). | If Turbine ever revives the endpoint, both buttons would still short-circuit instead of opening it — a silent staleness, not a crash. | `gmGameplayOptionsUI::ListenToElementMessage @0x0049E110`; `ShellExecuteA` call sites `0x0049E154`/`0x0049E1F0`; research doc `2026-08-10-keyboard-config-and-gameplay-tab.md` §4.1/§4.2 | | AD-76 | **Filed 2026-08-11 at Campaign OP slice OP3 (D5).** In-Game Help Files (`0x10000205`) is authored and clickable but has no handler — clicking it does nothing visible. | `src/AcDream.App/UI/Layout/OptionsPanelController.cs` (button wiring — no callback bound) | Retail's own `KeyStone::OpenHelp` loads a third-party embedded help viewer (`plugins\ACHelpPlugin.dll` via `keystone.dll`) that acdream does not have and cannot port (no DAT-resident help content, no source). Retail ITSELF fails silently with the plugin absent (`KeyStone::m_fnAC2HelpPluginExecute` unresolved) — mirroring that as an inert button is the faithful behavior for "the asset is missing", not an invented stub screen. | A user clicking In-Game Help Files gets no feedback at all, same as retail with the plugin missing — indistinguishable from a dead button unless they already expect the asset-missing case. | `KeyStone::OpenHelp @0x00557010`; `KeyStone::Init @0x00556CF0` (the unresolved plugin function pointer); research doc `2026-08-10-keyboard-config-and-gameplay-tab.md` §4.5 | | AD-77 | **Filed 2026-08-11 at the Campaign OP OP3 review-fix round (dual-review S4/MUST-FIX 2 — the plan's §5 "out of scope" list explicitly delegated this ruling to the OP3 review).** Retail exposes TWO `gmPanelUI` host variants for the same panel stack — a floating host (`0x2100006E`, `gmFloatyPanelUI`) and a docked host (`0x21000017`) — so a retail user can dock the Options panel (and every other `gmPanelUI` sibling) into a fixed screen position instead of leaving it freely floating. acdream mounts every main panel through `RetailWindowFrame.Mount` + `RetailPanelUiController.RegisterMainPanel` against the floating host ONLY; no code path resolves or mounts `0x21000017` at all. | `src/AcDream.App/UI/RetailUiRuntime.cs` (every `Mount*`/`RegisterMainPanel` call site for a `gmPanelUI` sibling — Character/Inventory/Spellbook/Effects/the four indicator-detail panels/Options); `src/AcDream.App/UI/Layout/RetailWindowFrame.cs` | This predates OP3 — every `gmPanelUI` sibling has shipped floating-only since its own slice landed; OP3 did not introduce the gap, it just added a tenth panel to an already-floating-only cohort. The plan explicitly scoped filing the row to "whichever slice's review deems it a divergence" rather than blocking any one panel's slice on building a docked-host variant no prior panel has either. | A user who expects to dock the Options panel (or any other main panel) the way retail allows cannot — every `gmPanelUI` sibling is floating-only in acdream, client-wide, not an Options-specific gap. | research doc `2026-08-10-options-panel-structure.md` §10.1 (docked/floating host pair); `docs/plans/2026-08-10-options-panel-campaign.md` §5 | -| AD-78 | **Filed 2026-08-11, user-directed (verbatim: "mark all options that are not implemented now, so I can clearly see what is not implemented"), gate 2 of Campaign OP's follow-up.** Retail dims nothing on any Options-panel row or Configure-Keyboard action row — every retail row drives its own real consumer by construction, so retail has no "does this actually do anything" ambiguity to signal. acdream, by contrast, ships a large honest store-only set (AP-198/AP-199/AP-200/AP-203, TS-73/TS-74/TS-75/TS-76/TS-77/TS-78/TS-79/TS-80, and the Character-tab Group A/D rows) that persist and, where auto-save, send the wire bit, but drive nothing observable client-side. Per explicit user direction, every such row's CAPTION now renders in a shared neutral grey (`UiRenderContext.StoreOnlyCaptionColor`, `(0.5,0.5,0.5,1)` — the SAME value the existing disabled/ghosted convention already used, `UiMenu.TextColorGhosted`) instead of its normal white/DAT-authored color, while the row itself stays fully interactive (click/drag/persist exactly as before — only the caption's paint color changes). No invented marker text is added anywhere (the project's "no user-visible strings outside the DAT" rule stands); the dim IS the marker. **[FA4 fix-round addendum, 2026-08-12 — blast SHOULD-FIX 1 + mechanism SF-8/SF-9: this row's own count had drifted stale THROUGH two campaigns (FA4's D7 un-dim landed 31, but this row still read the pre-FA4 "35"; the fix round then reverted three of FA4's four un-dims — see below — landing at 34). The Character-tab count is now 34 of 50 dimmed / 16 live.]** | `src/AcDream.App/UI/UiRenderContext.cs` (`StoreOnlyCaptionColor`, the one shared constant); `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (21 of 27 rows dimmed — `ApplyLabelAndTooltip`/`SetLabelText`'s `storeOnly` parameter, threaded from each `BindXxxSection` call site); `src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs` (**34 of 50 rows dimmed** — `RowSpec.StoreOnly`, derived per-row in the class doc's table, cross-checked against actual shipped consumers rather than the research doc alone. FA4 D7 originally un-dimmed 4 rows — `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests`/`FellowshipShareXP`/`FellowshipShareLoot` — landing at 31. The FA4 FIX ROUND, 2026-08-12, reverted THREE of those four back to dimmed: `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests` per the corrected plan D6 (retail's client reads neither option bit on the fellowship-invite path — both are pure server-side filters with no client consumer, exactly like the two allegiance bits that were always meant to parallel them; the client-side auto-respond interceptor that was their claimed consumer, `RetailUiRuntime.TryAutoRespondToFellowshipInvite`, is deleted outright), and `FellowshipShareLoot` per mechanism review SF-8 (its claimed "second checkbox surface" consumer never actually reads the stored value back — a second EDITOR of a value is not a CONSUMER of it). Only `FellowshipShareXP` survives as genuinely live (the fellowship Create flow reads it as the sent `shareXP` bit) — net ONE row un-dimmed from the pre-FA4 baseline, not four.); `src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (`BuildActionRow` dims a row when `RetailActionIdentityTable.TryResolve` fails, i.e. `MappedAction` is null — AP-203's set); `src/AcDream.App/UI/Layout/ChatOptionsPageController.cs` (audited, zero dimmed rows — every row already has a live consumer). | Explicit, unambiguous user direction (this session, gate 2) overriding the earlier per-slice register rows' silence on presentation; the four controllers' own conformance tests (`ConfigOptionsPageControllerTests.CaptionDimming_MatchesTheStoreOnlySetExactly`, `CharacterOptionsPageControllerTests.StoreOnlyRows_MatchTheDerivationTableExactly` + `Bind_AppliesDimmedCaptionColor_ForStoreOnlyRows_AndWhiteForLiveRows`, `KeyboardConfigControllerTests.UnmappedRows_DimTheirCaption_MappedRowsStayWhite`) pin the exact dimmed set so a future consumer landing without also flipping its row's literal fails the build, not just the eye. | A reviewer comparing a byte-exact retail screenshot to acdream will see caption colors retail never has — this row exists precisely so that divergence is understood as intentional, not a bug. If a row's dim/live classification in the four cited tables ever drifts from its ACTUAL consumer state (a landed consumer whose row was never un-dimmed, or a regressed consumer whose row was never re-dimmed), the caption becomes misleading in the OPPOSITE direction it was built to prevent — treat any report of "this dimmed row visibly does something" or "this live-looking row does nothing" as a real defect, not a rendering nit (see the gate script's own note). **The FA4 fix round is itself an instance of this exact risk materializing** — the register row lagged two code-side count changes across one campaign before this addendum caught up. This row retires only when acdream reaches full retail parity (zero store-only rows remaining), at which point the convention itself — not just its content — should be deleted. | None (acdream-only divergence; retail has no store-only rows to compare against) — `docs/research/2026-08-10-character-options-map.md` §7.1 (Group A/B/C/D split); `docs/research/2026-08-11-campaign-op-test-script.md` (per-tab store-only enumerations this row's dimmed set matches) | +| AD-78 | **Filed 2026-08-11, user-directed (verbatim: "mark all options that are not implemented now, so I can clearly see what is not implemented"), gate 2 of Campaign OP's follow-up.** Retail dims nothing on any Options-panel row or Configure-Keyboard action row — every retail row drives its own real consumer by construction, so retail has no "does this actually do anything" ambiguity to signal. acdream, by contrast, ships a large honest store-only set (AP-198/AP-199/AP-200/AP-203, TS-73/TS-74/TS-75/TS-76/TS-77/TS-78/TS-79/TS-80, and the Character-tab Group A/D rows) that persist and, where auto-save, send the wire bit, but drive nothing observable client-side. Per explicit user direction, every such row's CAPTION now renders in a shared neutral grey (`UiRenderContext.StoreOnlyCaptionColor`, `(0.5,0.5,0.5,1)` — the SAME value the existing disabled/ghosted convention already used, `UiMenu.TextColorGhosted`) instead of its normal white/DAT-authored color, while the row itself stays fully interactive (click/drag/persist exactly as before — only the caption's paint color changes). No invented marker text is added anywhere (the project's "no user-visible strings outside the DAT" rule stands); the dim IS the marker. **[#226 addendum, 2026-08-21: Building Detail Textures gained a live renderer consumer and is no longer dimmed; Config is now 20 of 27 dimmed.]** **[FA4 fix-round addendum, 2026-08-12 — blast SHOULD-FIX 1 + mechanism SF-8/SF-9: this row's own count had drifted stale THROUGH two campaigns (FA4's D7 un-dim landed 31, but this row still read the pre-FA4 "35"; the fix round then reverted three of FA4's four un-dims — see below — landing at 34). The Character-tab count is now 34 of 50 dimmed / 16 live.]** | `src/AcDream.App/UI/UiRenderContext.cs` (`StoreOnlyCaptionColor`, the one shared constant); `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (20 of 27 rows dimmed — `ApplyLabelAndTooltip`/`SetLabelText`'s `storeOnly` parameter, threaded from each `BindXxxSection` call site); `src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs` (**34 of 50 rows dimmed** — `RowSpec.StoreOnly`, derived per-row in the class doc's table, cross-checked against actual shipped consumers rather than the research doc alone. FA4 D7 originally un-dimmed 4 rows — `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests`/`FellowshipShareXP`/`FellowshipShareLoot` — landing at 31. The FA4 FIX ROUND, 2026-08-12, reverted THREE of those four back to dimmed: `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests` per the corrected plan D6 (retail's client reads neither option bit on the fellowship-invite path — both are pure server-side filters with no client consumer, exactly like the two allegiance bits that were always meant to parallel them; the client-side auto-respond interceptor that was their claimed consumer, `RetailUiRuntime.TryAutoRespondToFellowshipInvite`, is deleted outright), and `FellowshipShareLoot` per mechanism review SF-8 (its claimed "second checkbox surface" consumer never actually reads the stored value back — a second EDITOR of a value is not a CONSUMER of it). Only `FellowshipShareXP` survives as genuinely live (the fellowship Create flow reads it as the sent `shareXP` bit) — net ONE row un-dimmed from the pre-FA4 baseline, not four.); `src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (`BuildActionRow` dims a row when `RetailActionIdentityTable.TryResolve` fails, i.e. `MappedAction` is null — AP-203's set); `src/AcDream.App/UI/Layout/ChatOptionsPageController.cs` (audited, zero dimmed rows — every row already has a live consumer). | Explicit, unambiguous user direction (this session, gate 2) overriding the earlier per-slice register rows' silence on presentation; the four controllers' own conformance tests (`ConfigOptionsPageControllerTests.CaptionDimming_MatchesTheStoreOnlySetExactly`, `CharacterOptionsPageControllerTests.StoreOnlyRows_MatchTheDerivationTableExactly` + `Bind_AppliesDimmedCaptionColor_ForStoreOnlyRows_AndWhiteForLiveRows`, `KeyboardConfigControllerTests.UnmappedRows_DimTheirCaption_MappedRowsStayWhite`) pin the exact dimmed set so a future consumer landing without also flipping its row's literal fails the build, not just the eye. | A reviewer comparing a byte-exact retail screenshot to acdream will see caption colors retail never has — this row exists precisely so that divergence is understood as intentional, not a bug. If a row's dim/live classification in the four cited tables ever drifts from its ACTUAL consumer state (a landed consumer whose row was never un-dimmed, or a regressed consumer whose row was never re-dimmed), the caption becomes misleading in the OPPOSITE direction it was built to prevent — treat any report of "this dimmed row visibly does something" or "this live-looking row does nothing" as a real defect, not a rendering nit (see the gate script's own note). **The FA4 fix round is itself an instance of this exact risk materializing** — the register row lagged two code-side count changes across one campaign before this addendum caught up. This row retires only when acdream reaches full retail parity (zero store-only rows remaining), at which point the convention itself — not just its content — should be deleted. | None (acdream-only divergence; retail has no store-only rows to compare against) — `docs/research/2026-08-10-character-options-map.md` §7.1 (Group A/B/C/D split); `docs/research/2026-08-11-campaign-op-test-script.md` (per-tab store-only enumerations this row's dimmed set matches) | | AD-79 | **MOSTLY RETIRED 2026-08-13 (user-ordered social completion batch):** Friends Add/Remove/Appear-Offline and Squelch add-character/add-account/remove are LIVE (the wire beneath had existed end-to-end since J4.1/FA1 — docs/research/2026-08-13-social-wire-completion.md §4; the panel now publishes the same Runtime commands). REMAINING scope: the Friends "Send Tell" button (`0x10000516`) only, which needs the chat-tell seam. **Original filing — 2026-08-12 at Campaign FA slice FA3, D1 (the plan's "Friends + Squelch pages bind READ-ONLY... their mutation actions are wired only if their wire is already served by ACE and trivially pinnable in-slice — otherwise the action buttons are honest INERT" decision).** The social panel's Friends page authors three buttons (Add/Remove Friend-shaped, `0x10000514`/`0x10000515`/`0x10000516`) plus an "Appear Offline"-shaped checkbox (`0x1000052C`); the Squelch page authors three buttons (`0x10000547`/`0x1000054B`/`0x1000054C`). All seven are built, laid out, and clickable exactly as authored, but carry no click handler — no Friends add/remove/appear-offline wire and no Squelch add/remove/clear wire is implemented this campaign. `gmFriendsUI`/`gmSquelchUI` were also outside lane A/B/C/D's own decompiled scope (only Fellowship/Allegiance were researched), so their real button semantics and wire opcodes are not yet established either — this row covers BOTH "not wired" and "not yet researched." | `src/AcDream.App/UI/Layout/SocialFriendsPageController.cs`; `src/AcDream.App/UI/Layout/SocialSquelchPageController.cs` (both classes' own doc comments cite this row) | FA3 is the panel SHELL slice; D1 sets the bar for which Friends/Squelch actions get wired in-slice at "trivially pinnable," which none of these seven meet without their own wire research. `SocialPanelControllerTests.FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler` pins the INERT contract so a future consumer landing without also removing this row's citation fails nothing silently — the row is the only signal until a follow-up slice wires real handlers. | A user clicking Add/Remove Friend, Appear Offline, or any Squelch button in acdream sees no effect and no feedback — indistinguishable from a dead control unless they already expect the gap. The Friends/Squelch LISTS themselves are live (bound read-only to `RuntimeCommunicationState.Friends`/`.Squelch`) — only the mutation controls are inert. | None (no retail decomp anchor — `gmFriendsUI`/`gmSquelchUI` are outside this campaign's researched scope); `docs/research/2026-08-11-fa-panel-structure.md` §10 (coordinator addendum, the panel discovery that first surfaced these two pages); `docs/plans/2026-08-11-fellowship-allegiance-campaign.md` D1 | | AD-80 | **Filed 2026-08-12 at Campaign FA slice FA4, D5.** The fellowship page's per-fellow percentage text renders retail's own byte-decoded XP-share table verbatim (1.0/.75/.6/.55/.5/.45/.4/.35/.3111111/.28, default 0.0 — `docs/research/2026-08-11-fa-fellowship-wire.md` §7.2, byte-decoded from the PDB-paired binary because both available decompilers folded the function to a constant). The currently-targeted ACE server computes the ACTUAL distributed XP from a DIFFERENT table (`.3` at 9 fellows instead of `.3111111`, no explicit 10-fellow row, and a wrong out-of-range default of `1.0` instead of `0.0` — `Fellowship.cs:604-632`, lane B §4.3). So a full (9-member) or over-full-in-retail's-table (10-member) fellowship's displayed percentage will not exactly match the XP ACE actually grants. This is a divergence between ACE and RETAIL, not between acdream and retail — acdream's client-side display is retail-faithful — but it is filed here because it is directly user-visible through this panel and a tester comparing "panel says 31.1%" against "server granted 30%" is measuring ACE's bug, not acdream's port. | `src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs` (`EvenSplitPercentTable`, `FormatStatsText`) | The client-side table is byte-verified against the retail binary; re-deriving it to match ACE's (wrong) numbers would make acdream disagree with a REAL retail client observing the same fellowship, which is the opposite of this project's goal. | A tester with a 9- or 10-member fellowship on ACE sees a panel percentage that does not exactly match the XP bonus they actually receive; below 9 members the two agree exactly. The proportional (non-even-split) branch has a SEPARATE, narrower gap: acdream has not ported an `ExperienceToRaiseLevel`-equivalent table, so that branch omits the percentage entirely (level only) rather than computing a wrong number — see AD-81's citation of the same method. | `FellowshipSystem::GetEvenSplitXPPctg @0x005B9BA0` (lane B §7.2); ACE `Fellowship.cs:604-632`; `docs/research/2026-08-11-fa-fellowship-wire.md` §4.3 | | AD-81 | **Filed 2026-08-12 at Campaign FA slice FA4.** Two retail text-composition primitives the fellowship page's mechanism needs are not ported, so this controller renders their CONTENT as plain numeric composites instead of retail's exact resolved sentence, never invented English: (1) **`StringInfo` variable substitution** — every row field beyond the bare name is a retail `StringInfo` template with embedded variables (`ID_Fellowship_FellowStats` + `ID_Level`/`ID_Experience`; the three `…Status` fields + `ID_Cur`/`ID_Max` — `docs/research/2026-08-11-fa-panel-structure.md` §3.1/§4.1), resolved at runtime through `StringInfo::InqString` → `StringTableMetaLanguage::UnescapeString`, a cross-cutting UI-string engine acdream has never ported (the SAME gap the pre-Campaign-OP Character window recorded, `docs/research/2026-06-25-character-window-faithful-spec.md`: "NOT yet ported — current controller uses canonical AC labels"); this controller instead renders `"{level} {pct}%"` and `"{cur}/{max}"` — the retail-authored NUMBERS, without retail's surrounding words. **AMENDED 2026-08-13:** the no-metalanguage fragment/variable interleave of `StringTable::GetString @0x004300D0` IS now ported as `DatStringResolver.ResolveTemplate` (the AD-85 dialog narrowing), so VERIFIED-token-free templates can resolve exactly; this row's remaining scope is the meta-token engine (`StringTableMetaLanguage::RenderString @0x004302B1` + `StripMetaLetters`) the multi-variable stats templates may need, plus `FormatName`. (2) **`ACCharGenData::FormatName`** — retail's Create flow canonicalizes the typed fellowship name and writes the formatted text back into the entry box before sending (lane B §2.2/§6.2); acdream sends the raw typed text verbatim. Neither gap affects the WIRE — the `0x00A2` builder's `str16L` field is unaffected either way; only the client-side PRESENTATION differs. | `src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs` (`UpdateRow`, `FormatStatsText`, `SetVitals`, the create-button `OnClick`) | Porting `StringTableMetaLanguage` is a cross-cutting UI-string-engine prerequisite, not a fellowship-specific task, and guessing its token syntax without decoding `StringInfo::InqString` would risk silently-wrong substitution rather than an honestly-numeric fallback — exactly the guessing CLAUDE.md's workflow forbids. `FormatName`'s capitalization/character rules are a separate chargen algorithm with no fellowship-specific anchor read yet. | A user sees "12 31%" / "140/140" instead of retail's full sentence, and a typed fellowship name keeps whatever casing/spacing the player typed instead of retail's canonicalized form. The underlying DATA (level, percentage, cur/max, the name itself) is correct in every case — only the surrounding words/formatting are absent. | `StringInfo::InqString @0x0042e490` → `StringTableMetaLanguage::UnescapeString` (unresolved — not yet decoded); `gmFellowshipUI::CreateFellowship @0x0048F730` (the `ACCharGenData::FormatName` call, lane B §2.2); `docs/research/2026-06-25-character-window-faithful-spec.md` (the identical prior finding for the Character window) | @@ -190,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 | **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-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-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 | @@ -205,19 +211,57 @@ readiness/requeue adaptation. See | AD-100 | **Filed 2026-08-15 at the Campaign CC CC2 review, finding F2 (unrequested `0xF643` handling).** When a `0xF643` (`CharGenVerificationResponse`) arrives with NO outstanding create/restore request, acdream DROPS the message with a once-per-session stderr log. Retail has no such gate: `Handle_CharGenVerificationResponse @0x0055E8B0` processes whatever arrives, discriminating create-vs-restore by its OWN persistent verification state (case 1 branches on `GetVerificationState() == PENDING` → new `CharacterIdentity` + `AddIdentity`, else unpacks into the existing identity at `slot`) — an unsolicited reply would be applied against whatever that state happens to be. acdream's transport-level latch (`PendingCharGenVerificationRequest`) is the equivalent discriminator, but when it is `None` there is no state to apply the reply against, so the honest move is drop-and-log rather than guessing a family. | `src/AcDream.Core.Net/WorldSession.cs` (the `CharGenVerificationResponse.ResponseOpcode` arm in `ProcessDatagram`; `_loggedUnexpectedCharGenVerificationResponse`) | Processing an unsolicited reply requires retail's persistent chargen verification state, which lives in CC3's Runtime owner, not the transport. Until then a reply with no outstanding request is either a server bug or a latch-lifecycle bug on our side — surfacing it in the log beats silently misrouting it to an arbitrary event. Pinned by `WorldSessionCharacterCreationTests.ResponseWithNoOutstandingRequest_IsDroppedAndNeverMisattributed`. | A server that sends a spontaneous/duplicate `0xF643` (ACE can double-send NameInUse — see the CC2 review's F3 note) has its second copy dropped here, where retail would re-process it. If CC3's verification gate ever needs retail's re-process semantics, this drop must move behind that owner's state. | `Handle_CharGenVerificationResponse @0x0055E8B0`; `CharGenState::GetVerificationState`; CC2 review F2 (2026-08-15) | | AD-102 | **Filed 2026-08-15 at Campaign CC slice CC4 (the Heritage page's Viamontian button and the Town page's Sanamar button).** Retail gates BOTH controls behind `CPlayerSystem::AccountHasThroneOfDestiny`: `gmCGHeritagePage::ListenToElementMessage @ 0x00483860` shows `MakeToDWarningDialog` instead of selecting Viamontian (element `0x100003c3`) for a non-ToD account, and `gmCGTownPage::ListenToElementMessage @ 0x0047c480` does the same for Sanamar (element `0x1000040b`, `startArea` index 3 — also the reason `CharGenState::RandomizeStartArea`'s ToD-aware `RandInt(3 or 4)` bound exists). acdream's `ChargenOptions` (CC1) carries no account/DLC-ownership signal anywhere in the model, so both controls ship WITHOUT the gate — every installed heritage/town in `Options.HeritagesById`/`Options.StarterAreas` is always selectable, matching what a ToD-owning account would see. | `src/AcDream.App/UI/Layout/CharacterCreationHeritagePage.cs` (`HeritageByButtonId[0x100003C3u]`); `src/AcDream.App/UI/Layout/CharacterCreationTownPage.cs` (`StartAreaByButtonId[0x1000040Bu]`, `Randomize`) | ACE's server-side `CharacterCreate` handler never checks ToD ownership either (the field is purely a retail-client UI gate), so accepting the selection unconditionally never produces a request the emulator would reject; adding an account-ownership model to CC1's DAT-only `ChargenOptions` is out of this slice's scope and would need its own design (where does the "ToD owned" bit come from — account service, launcher config, a new env flag?). | None observable against ACE. A future retail-parity gate that specifically checks "does a non-ToD account get warned off Viamontian/Sanamar" will fail until an account-ownership signal exists to gate on. | `gmCGHeritagePage::ListenToElementMessage @ 0x00483860`; `gmCGTownPage::ListenToElementMessage @ 0x0047c480`; `gmCGTownPage::SetTown @ 0x0047c360`; `CharGenState::RandomizeStartArea` (DoRandom case 4, `RandInt(hasToD ? 4 : 3)`) | | AD-99 | **Filed 2026-08-15 at Campaign LA gate round 2 finding 1 (character-select Exit button).** On a confirmed Exit, acdream closes the client through the existing graceful window-close path (`d.Window.Close`, the same seam `GameplayInputCommandController`'s in-world Escape fallback already uses) instead of retail's real post-confirm behavior: `RecvNotice_CloseDialog`'s case-1 arm queues UI mode `0x10000009`, which `gmEpilogueUI::Register` claims — a brief epilogue/farewell screen — before the process actually terminates. The confirmation dialog itself (`MakeConfirmExitDialog`, its exact `ID_CharacterManagement_ConfirmExit` text, and the `m_confirmExitDialogContext != 0` re-entry guard) IS ported faithfully; only the post-confirm destination differs, the same shape as AD-74's Options-panel exit. | `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` (`RequestExit`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`CharacterSelectionRuntimeBindings.RequestExit`); `src/AcDream.App/Composition/InteractionRetainedUiComposition.cs` (`d.Window.Close` binding) | acdream has no `gmEpilogueUI` port (out of scope this round); reusing the ONE existing graceful-shutdown seam keeps `disconnected`/`exited` status events firing through `GameWindow.OnClosing` → `CompleteShutdown` rather than inventing a second shutdown path, per explicit direction for this finding. | A user confirming Exit sees the window close immediately instead of retail's brief epilogue screen; a future feature wanting to reproduce that screen (or an intermediate "logged off, returned to character select" state) has no seam yet — same gap class as AD-44. | `gmCharacterManagementUI::MakeConfirmExitDialog @0x004ed250`; `RecvNotice_CloseDialog @0x004ed760` case 1; `gmEpilogueUI::Register(0x10000009)` @0x0047a680; `gmCharacterManagementUI::OnAction @0x004ed410` (Escape key, unported — button-only this round) | +| AD-113 | **Filed 2026-08-25 at Campaign CT slice CT-GF1 (client-wide retained-UI ancestor clip).** Porting retail's `UIRegion::DrawHere @0x0069FA30` ancestor-clip intersection (an element's screen rect is intersected against the FULL inherited clip-rect chain and the subtree is skipped when the intersection is empty — the `var_24` gate @0x0069FB8E) as `UiElement.ClipsChildren`'s new client-wide default (true, threaded through the pre-existing `UiRenderContext.PushClip`/`PopClip`) needed one deliberate opt-out: retail spawns a menu's dropdown popup as a SEPARATE top-level region (`UIElement_Menu::MakePopup`), clipped only by the screen, while acdream's `UiMenu` draws its popup INLINE from the owning button in a second traversal (`OnDrawOverlay`, pre-existing, "regardless of this element's position in the tree" by its own doc comment). Without an escape, the new ancestor clip would wrongly cut off a popup that legitimately extends outside its own (possibly short) owning window — e.g. a channel dropdown opened upward past a short chat window's top edge. `UiElement.ExpandsClipForPopup` (default false) resets the accumulated clip to the full CANVAS rect (0,0,ScreenSize) — SCREEN-clipped, not truly unbounded, matching retail's own popup region (`UIElement_Menu::MakePopup` spawns a top-level region bounded by the screen) — for exactly the `OnDrawOverlay` call of an opted-in element (`UiRenderContext.PushClipUnbounded`, sharing the existing clip stack; corrected from an earlier `null`/unbounded clip at the CT-GF1 fix round); `UiMenu` overrides it true, paired with `ClipsChildren => false` so its own out-of-bounds `OnHitTest` union (the popup occupies `ly < 0` or `ly >= Height` depending on open direction) stays reachable through the same early-bounds gate that now defaults on for every other element. | `src/AcDream.App/UI/UiElement.cs` (`ClipsChildren`, `ExpandsClipForPopup`, `DrawOverlays`); `src/AcDream.App/UI/UiRenderContext.cs` (`PushClipUnbounded`); `src/AcDream.App/UI/UiMenu.cs` (the two overrides) | The popup is the ONLY overlay-drawing widget in the tree today (grep-confirmed: exactly one `OnDrawOverlay` override client-wide), and it already renders on top of the whole UI by construction (the overlay pass beats even rect backgrounds), so exempting it from the ancestor clip matches its existing "regardless of tree position" contract rather than introducing new behavior. | A future `OnDrawOverlay` override that is NOT a screen-anchored popup (e.g. an in-place highlight meant to stay window-clipped) would silently escape every ancestor's clip if it left `ExpandsClipForPopup` at its default; the opt-in default direction makes that the exception rather than the rule, but a widget that WANTS window-clipped overlay content has no dedicated seam beyond simply not overriding the escape. | `UIRegion::DrawHere @0x0069FA30`; `UIElement_Menu::MakePopup`; the register's own AP-201 retirement note (the FIRST `ClipsChildren`/`PushClip` port, for `UiScrollablePanel`'s viewport) | --- -## 3. Documented approximation (AP) — 161 active rows (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-204~~ RETIRED 2026-08-11 at the OP8 rework — the silent-auto-reassign narrowing it recorded is fixed by a real `RetailDialogFactory` confirm-before-reassign dialog; see its retirement note below. AP-203/AP-202 filed 2026-08-11 at Campaign OP slice OP8 (Configure Keyboard) remain active — AP-202 records D4's `.keymap`-file-interchange narrowing (`keybinds.json` only), AP-203 records that roughly half of the DAT ActionMap's 306 user-bindable rows (82 of 87 Emotes, all 48 CharacterSettings hotkeys, all 10 CameraAlternateControls rows per the M2 de-alias fix, and assorted UI/Combat odds) render/bind/persist on the Configure Keyboard screen with no live acdream gameplay consumer yet; 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) +## 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. +**AP-161 correction and narrowing (2026-08-26, #444/#445):** the old F6 +full-stack conclusion and the row's `m_last_sale` conclusion are superseded. +`VendorSellUI::AcceptDragObject @ 0x004C4F00` splits the live selected +quantity, temporarily stages the source, then substitutes the new matching +WCID/quantity object; acdream now ports that flow through the canonical +inventory owner. `gmVendorUI::BuySingleItem @ 0x004C2820` and Buy All both +assign the purchase value to `m_last_sale`; purse/cost/affordability now +subtract it immediately and reconcile to authoritative owned-currency +objects. Those two residuals are closed. The closed-dropdown arrow-cap +cosmetic is AP-161's only live item; the long row below remains as historical +research chronology and must be read through this correction. + +**Vendor double-click correction (2026-08-26):** direct reading of +`gmVendorUI::HandleMousePresses @ 0x004C40D0` disproved the earlier +absence-of-symbol inference embedded in AP-161 and AP-171. Retail directly +buys a browse row on double-click and removes staged Buying/Selling rows in +that same mouse dispatcher. AP-171 is retired; the old AP-161 sentence saying +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` | +| AP-234 | **Filed 2026-08-23 at the #426 solid-face extraction fix.** Cell-wall (EnvCell/CellStruct) geometry approximates retail's "skip untextured subsets inside a cell interior" with the polygon's own `Stippling.NoPos` flag rather than resolving the Surface's own `Type` (`Base1Image`/`Base1ClipMap`) before the per-polygon draw decision — the same NoPos-vs-surface-type conflation #426 fixed for ordinary GfxObj extraction (`PrepareGfxObjMeshData`/`GfxObjMesh.Build`), deliberately LEFT in place here | `src/AcDream.Core/Meshing/CellMesh.cs:45`; `src/AcDream.Content/MeshExtractor.cs`'s `PrepareCellStructMeshData` `hasPos` gate carries the identical rule | Cells are the one retail context that genuinely skips untextured subsets (`DrawEnvCell`), so approximating "untextured" with NoPos is directionally correct for the common case — a solid-colour polygon always carries NoPos since it has no UVs to carry; resolving Surface.Type first would need a per-polygon dat lookup this code doesn't currently perform before the emit/skip decision | A textured polygon whose author left NoPos set (no positive UVs authored despite a real texture) would be wrongly skipped as if untextured, or an untextured polygon whose author left NoPos unset would wrongly draw — either edge case shows as a cell wall gaining or losing a face relative to retail | `RenderDeviceD3D::DrawEnvCell` @0x0059f170 → `D3DPolyRender::DrawMesh(..., arg4=1)`; `RetailUntexturedSurfacePolicy`/`RetailUntexturedSubsetPolicy` (`src/AcDream.Core/Meshing/RetailUntexturedSurfacePolicy.cs`) | +| 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 | **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-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-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` | @@ -227,14 +271,14 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-206 | **Filed 2026-08-11 at Campaign OP gate 4 (#382).** `UiButton.TrySetRetailState`'s DirectStateId branch now requires REAL `""`-keyed media (`HasStateMedia("")`) before accepting a DirectState transition; a `_mediaInfo.States` entry that exists ONLY as a property bag (every button carries one, holding ToggleBehavior/RolloverEnabled/etc regardless of whether it authors blank media) no longer counts. A reference-identity-verified live-DAT probe found the chat window's four floating-window indicator buttons (`0x10000522`-`0x10000525`) resolve their own correct `ActiveState="Normal"` at construction, then get blanked to `""` moments later in the SAME `LayoutImporter.Build` call: the indicator column's backing panel (`0x10000600`) authors `PassToChildren=true` on its own empty DirectState (confirmed live: `States[0xFFFFFFFF].PassToChildren == true`), and `LayoutImporter.BuildWidget`'s post-attach state reapply (needed so retained PassToChildren TABS get their authored Open/Closed child media) cascades that DirectState to every `IUiDatStateful` child — including these already-correctly-resolved buttons. Retail's own decompiled `UIElement::SetState @0x00464e70` commits its `m_curStateDesc`/`m_state` unconditionally once `ElementDesc::AccessStateDesc` finds ANY StateDesc (media or not) and does the exact same blind per-child cascade; retail avoids this exact bug purely through construction TIMING — `UIElement::Initialize`'s `SetState(m_defaultState)` call is the SECOND operation in the function, before any child-tree construction, so a PassToChildren cascade fired during import always iterates zero children in retail. Our port's `LayoutImporter.BuildWidget` deliberately reapplies AFTER children are attached (the opposite order), so this literal 1:1 state-machine port needed a compensating guard rather than a full reapply-ordering rewrite (out of scope for this fix; `CharacterStatController`'s own three-chrome-children PassToChildren cascade depends on the current ordering and is left untouched). | `src/AcDream.App/UI/UiButton.cs` (`TrySetRetailState`'s `stateId == UiStateInfo.DirectStateId` branch) | Scoped to `UiButton` only — `UiDatElement.TrySetRetailState`'s parallel DirectStateId branch (and the cascade mechanism itself) are UNCHANGED, so every existing PassToChildren consumer keeps its current behavior; the fix only stops an UNRELATED ancestor's cascade from overriding a button's OWN already-resolved, independently authored state with an empty one it never asked for. | If a future button is EVER meant to render literally blank at rest via a cascaded DirectState with no authored `""` media, this guard would reject that transition (falls back to its previous `ActiveState`) — no such button is known to exist today; `UiButtonTests.DirectStateTransition_WithRealMedia_StillSucceeds` documents that an AUTHORED blank state still works. | `UIElement::SetState @0x00464e70` (cascade + unconditional commit); `UIElement::Initialize @0x00462c90` (SetState call precedes child construction) — both in `docs/research/named-retail/acclient_2013_pseudo_c.txt` | | AP-205 | **Filed 2026-08-11 at Campaign OP gate 4 (#381).** The Apply/Reset/Defaults footer on the Character/Chat/Config tabs draws an opaque, borderless backing field (`UiSolidSpriteFill`, tiling `RetailChromeSprites.CenterFill` — the SAME panel-background sprite the Options window's own `UiNineSlicePanel` chrome already tiles behind everything) behind the three buttons. A live-DAT probe (scratch console app against `DatCollectionAdapter`, 2026-08-11) found retail authors NO such element: each page root (`0x100001F9`/`0x100001FF`/`0x1000050A`) has EXACTLY five children — the row ListBox, its scrollbar, and the three physical buttons — with zero direct-state media on the root itself. Scrolled row content therefore bled through visibly between/behind the buttons before this fix. | `src/AcDream.App/UI/UiSolidSpriteFill.cs`; `src/AcDream.App/UI/Layout/OptionsPanelController.cs` (`AddFooterBacking`) | Reusing the SAME sprite the rest of the window's chrome already draws keeps the synthesized field visually indistinguishable from an authored one rather than inventing a new color; the field is `ClickThrough=true` and z-ordered strictly behind every other child, so it cannot intercept input or occlude the buttons themselves. | A reviewer comparing a byte-exact retail screenshot to acdream will see one extra opaque rect retail never authors — cosmetically invisible (it exactly matches the surrounding chrome), so the only observable difference IS the fix (content no longer bleeding through). If a future page's footer strip ever needs a DIFFERENT background (a themed panel, a translucent tab), this hardcoded `CenterFill` reuse would need revisiting. | Live-DAT probe, 2026-08-11 (page-root child-count/direct-state-media dump against `client_local_English.dat`, LayoutDescs `0x21000028`/`0x21000029`/`0x2100005C`) — no retail element to cite since none exists | | ~~AP-201~~ | **RETIRED 2026-08-11 at the Campaign OP gate-3 fix round (closes #371).** UiScrollablePanel now marks ClipsChildren=true (the draw walk and hit-test both route through UiRenderContext.PushClip, which existed by retirement time) and its cull predicate keeps any INTERSECTING row visible - a straddling row renders its visible slice instead of vanishing whole. The user-observed symptom this row predicted (the Chat tab per-window filter blocks reading as MISSING at the default scroll offset, gate 3) is the exact acceptance evidence. Original filing follows for the record: filed at the OP5 review-fix round (S2), predates OP5 but was made user-visible by it. `UiTemplateListBox`'s internal row viewport (`UiScrollablePanel.LayoutScrollableChildren`) culls a child WHOLE — `child.Visible = top >= -0.5f && top + child.Height <= Height + 0.5f` — rather than clipping the visible portion of a row that straddles the viewport edge, because the UI renderer has no scissor stack. Retail's own `UIElement_ListBox`/scroll-region rendering clips partially-visible rows at the pixel boundary, same as any native scroll view. Every row in this viewport was 8-36px until Campaign OP slice OP5 added five self-sized filter blocks (12x20=240px / 13x20=260px, AP-195) to the Chat tab's ~560px viewport; a 240-260px block straddling the viewport edge at a given scroll offset now disappears ENTIRELY (a visible "pop") instead of clipping, where the pre-OP5 8-36px rows made the same all-or-nothing cull read as ordinary row-granular scrolling. | `src/AcDream.App/UI/UiScrollablePanel.cs:69` (the cull predicate); consumed by `src/AcDream.App/UI/UiTemplateListBox.cs` (`Viewport`) — the Character/Chat/Config Options-panel tabs and any other controller-built row list sharing this viewport | A scissor stack does not exist anywhere in the retained-UI renderer yet (class's own doc comment, `UiScrollablePanel.cs:8-12`, predates this row); whole-row culling is a correct, cheap stand-in for every list whose rows are small relative to the viewport, which was true for every consumer before OP5. | A tall block (any future row taller than roughly the viewport's own height, not just OP5's filter blocks) can vanish completely for a range of scroll offsets instead of showing a partial view — the OP5 gate script's own step 2 documents the exact symptom so it is not mistaken for a self-sizing regression (`docs/research/2026-08-11-campaign-op-test-script.md`). Scrolling further always restores the block whole; no data or state is lost, only the presentation pops. | No scissor-stack retail oracle needed — this is a stand-in for ordinary native clip-rect rendering every GUI toolkit (including retail's own) provides; issue #371 tracks adding a real per-row clip rect to `UiScrollablePanel` | -| AP-202 | **Filed 2026-08-11 at Campaign OP slice OP8 (D4).** Configure Keyboard persists every rebind to `keybinds.json` only. Retail's own storage is a `\Asheron's Call\.keymap` text file (`CInputManager_WIN32::SaveKeyMap @0x00686C20`, `PFileParser`), with Load-File/Save-As buttons for NAMED keymap profiles and a `keymap` key in `UserPreferences.ini` selecting which one loads at startup (research doc §5.7). D4 chose the existing, tested `keybinds.json` schema over building a second `PFileParser`-compatible text codec + named-profile management; this row's the Load File/Save As buttons on the Configure Keyboard screen (`0x10000027`/`0x10000029`) are wired but INERT. | `src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (`WireScreenButtons`'s Load/Save-As no-op); `src/AcDream.UI.Abstractions/Input/KeyBindings.cs` (`SaveToFile`/`LoadOrDefault`) | `keybinds.json` already round-trips every retail action this screen can bind (identity table + the DAT-defaults conformance test), so the ONLY capability lost is exchanging `.keymap` files with a real retail client or another acdream install by named profile — a real feature gap, not a correctness gap. | A user who expects to export/import a named `.keymap` profile (e.g. to share a control layout with a retail-client friend) cannot; every rebind still works and persists locally. | `docs/research/2026-08-10-keyboard-config-and-gameplay-tab.md` §5.7-§5.8; `CInputManager_WIN32::SaveKeyMap @0x00686C20`; `gmKeyboardUI::SaveKeymap @0x004DCF90` | -| AP-203 | **Filed 2026-08-11 at Campaign OP slice OP8.** Of the DAT ActionMap's 306 user-bindable rows, `RetailActionIdentityTable` (`src/AcDream.UI.Abstractions/Input/RetailActionIdentityTable.cs`) resolves roughly half to a live acdream `InputAction`; the rest render, bind, conflict-check, and persist (via `RetailUnmappedKeyBindings`, a sibling `*-unmapped.json` file) exactly like any other row, but have no live gameplay consumer to dispatch through. The two largest classes: 82 of 87 Emote rows (only Cry/Laugh/Cheer/Wave/PointState dispatch an animation today — acdream has no general emote-animation player), and all 48 CharacterSettings hotkey rows (ctx `0x10000008` — these are hotkeys for the SAME `PlayerOption`/`CharacterOptions` preference bits OP1's `CharacterOptionTable` and OP4's Character-tab checkboxes already model; wiring "press this key, flip that same server-synced bit" is a real feature, a hotkey-to-option-toggle dispatcher, that does not exist anywhere in acdream yet). Smaller residuals: Spell Slot 10-12, Quickslot 10-13 (both hit a PRE-EXISTING `InputAction` enum gap this slice did not introduce), and roughly twenty UI-panel-toggle rows for panels acdream has no analog for (Vitae/Link Status/House/Map/Character Info/the two Magic panels/...). | `src/AcDream.UI.Abstractions/Input/RetailActionIdentityTable.cs` (class doc has the full accounting); `src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (`CurrentForUnmapped`/`SetForUnmapped`) | Guessing a mapping for an ambiguous row risks silently misrouting a rebind to the wrong gameplay action (worse than an honest "not wired yet" — the identity table's own class doc states this directly); every mapping that WAS added was cross-verified two ways (label match + DAT-default-vs-`KeyBindings.RetailDefaults()` byte match, see `RetailActionIdentityRoundTripTests`). | A user rebinds e.g. an emote or a CharacterSettings hotkey on the Configure Keyboard screen and the binding persists but has no observable in-game effect — matches retail's OWN screen shape (the row exists, is bindable) while honestly lacking retail's gameplay behavior behind it. ADDENDUM (2026-08-11, OP8 re-review round 2): this row's scope EXPLICITLY includes the ten CameraAlternateControls (InputMap 0x6) rows the M2 de-alias narrowed to store-only — a case the generic wording understated because their SIBLING rows (InputMap 0x5, the same verbs) ARE live on the same screen: the 0x6 rows display their DAT-default arrow keys (display-only seeding), persist user edits, and drive nothing; only the 0x5 scheme reaches the InputDispatcher. Store-only rows are also EXCLUDED from the conflict universe (they cannot actually collide) — mapped cross-context sharing remains ISSUES #373. | `docs/research/2026-08-10-keyboard-config-and-gameplay-tab.md` §5.1-§5.3; live-DAT probe 2026-08-11 (306-row/six-ActionClass accounting, `RetailActionMapReaderTests`) | -| ~~AP-204~~ | **RETIRED 2026-08-11 at the OP8 rework (M3, combined review).** Originally filed for two narrowings: (1) silent auto-reassign on a cross-row conflict instead of retail's modal `OpenOverwriteBindingDialog`, and (2) OK/Cancel wired as left-click instead of retail's right-click-release gesture. (1) is FIXED — `KeyboardConfigController.BeginSlotCapture` now opens a real confirm dialog through `RetailDialogFactory.MakeConfirmation` (the SAME seam `GameplayConfirmationController` uses) BEFORE reassigning, listing every conflicting row (N-way), and only applies on accept; decline leaves every row untouched. (2) is NOT fixed and does not warrant its own row: it is authored-input-only with zero observable difference to a user (retail's own right-click-release on just this pair of buttons carries no distinguishing visual cue either, and every other Campaign OP button already uses left-click) — noted as a code comment at the OK/Cancel wiring site instead of a register row, matching this register's convention of reserving rows for divergences that could produce an observable symptom. | `src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (`FindConflicts`/`BeginSlotCapture`; `WireScreenButtons`'s OK/Cancel `OnClick` comment); `src/AcDream.App/UI/RetailUiRuntime.cs` (`MountKeyboardConfig`'s `ConfirmOverwrite` wiring) | — | — | `docs/research/2026-08-10-keyboard-config-and-gameplay-tab.md` §5.4 (`UIOption_ActionKeyMap::KeyHitHandler @0x00489570`, `OpenOverwriteBindingDialog @0x00488BF0`, `OpenCantOverwriteBindingDialog @0x00489300`) and §5.5 (OK/Cancel `idMessage 0x19` gesture) | +| ~~AP-202~~ | ~~**Keyboard .keymap file import/export remains intentionally deferred.**~~ **RETIRED 2026-08-26 by #446.** Load File and Save As now use retail's authored type-7/type-5 dialogs and the Sept-2013 PFile grammar; profiles live under `Documents\Asheron's Call`, the selected profile loads at startup and rewrites on graceful shutdown, overwrite/read-only handling is live, and all 306 user-bindable identities round-trip. `%LOCALAPPDATA%\acdream\keybinds.json` remains only the host-command compatibility mirror. | ✅ RETIRED — codec, profile-store, controller, and installed-DAT gates landed. | +| ~~AP-203~~ | ~~**Configure Keyboard exposed rows without production consumers.**~~ **RETIRED 2026-08-26 by #446.** All 306 installed-DAT ActionMap rows resolve to distinct `InputAction` identities and concrete production consumers; the compatibility sibling store is retained only for unknown future-DAT rows. The installed-DAT identity, default, mount, and routing gates pin 306/306. | ✅ RETIRED — evidence: `docs/research/2026-08-26-retail-keyboard-routing-audit.md`. | +| ~~AP-204~~ | ~~**Configure Keyboard capture/conflict behavior diverged from retail.**~~ **RETIRED and corrected 2026-08-26 by #446.** Capture instructions, unsupported-input retry, same-row no-op, dense two-slot insertion, exact priority conflict/non-bindable dialogs, overwrite behavior, dirty-only Revert state, Defaults, Apply/OK, Cancel, and persistence now follow the named retail routines. `idMessage 0x19, dwParam1=7` is the authored button action/release event; the decomp supplies no right-click evidence. | ✅ RETIRED — named-retail conformance and controller tests landed. | | AP-194 | `CharacterOptionTable`'s `ClientDefault` column (what the Character tab's Defaults button restores) disagrees with the raw constructor default word for three ids: `ConfirmVolatileRareUse` (`0x2D`), `ShowHelm` (`0x2F`), and `ShowCloak` (`0x32`) are all ON in retail's constructor default `CharacterOptions2 = 0x00948700` (`PlayerModule::PlayerModule @0x005D51F0`, byte-verified literal write) but report default-OFF via `PlayerModule::GetDefaultOptionValue @0x005D2A30`, whose own per-option table stops at id `0x2A` and returns `false` for everything past it. This is retail's OWN behavior, reproduced deliberately — the Defaults button does not reproduce a fresh `PlayerModule`. **CONFIRMED 2026-08-11 at Campaign OP slice OP4**: `CharacterOptionsPageController` seeds every `BoolOptionRow`'s default directly from this column (`EveryRow_DefaultValue_MatchesCharacterOptionTableClientDefault`, `tests/AcDream.App.Tests/UI/Layout/CharacterOptionsPageControllerTests.cs`); the directive below was followed, not re-litigated. OP4 also independently traced retail's OWN mechanism for the Character tab specifically — `UIOption_Checkbox::SetPlayerOption @0x00486e80` (pseudo-C line 147375) sets `m_default` directly from `GetDefaultOptionValue`, confirming this column (not the separate `DBPropertyCollection`/`InqDefaultGameplayOptionProperty` mechanism that governs the Chat/Config tabs' `m_propName`-bound rows) is the correct and ONLY source for this tab. | `src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs` (`ClientDefault` column; see the type's XML doc); `src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs` | Byte-verified at both addresses (wire research §2.5 for the constructor literals, §8.2 for `GetDefaultOptionValue`'s own table and bounds check) — this is not a guess, it is retail's documented quirk. "Fixing" it to match the constructor default would make acdream's Defaults button MORE correct than retail's own, which is the opposite of this project's goal. | A future OP-campaign slice (OP4, the Character tab's Defaults button) must consult THIS column, not the constructor default word, or a future reader may "fix" this back and silently diverge from retail. | `PlayerModule::GetDefaultOptionValue @0x005D2A30`; `UIOption_Checkbox::SetPlayerOption @0x00486e80` (N-4 anchor-column correction, OP4 review-fix round 2026-08-11 — was mislabeled `PlayerModule::SetPlayerOption`, same address, wrong class); `PlayerModule::PlayerModule @0x005D51F0`; `docs/research/2026-08-10-set-character-options-wire.md` §8.2 | | AP-193 | Character option id `0x34` (`ListenToPKDeathMessages` / "Listen to PK death messages") is mapped to `CharacterOptions2` bit `0x02000000` and modeled as a batched (non-auto-save) option purely on ACE's own enum — the id does not exist in the 2013 EoR PDB (`PlayerOption` there terminates at `TotalNumberOfPlayerOptions_PlayerOption = 0x34`), so neither the mask nor its `IsAutoSaveOption`/`GetDefaultOptionValue` classification is byte-verifiable against our binary. | `src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs` (`HearPkDeathMessages` row) | The user's retail memory (and ACE's own `CharacterOption` enum) both carry this option; shipping wire+store coverage for it is strictly better than omitting the row the Character tab's screenshots show, and ACE never actually reads the bit server-side (`PlayerFactory.cs:659-660` — "possibly was added to Defaults post PDB we have"), so a wrong id/mask/auto-save guess here has zero server-observable consequence either way. | If the final EoR client's real id/mask/auto-save classification ever surfaces (a later PDB, or a byte-level trace against a 2015+ binary), this row's values may be wrong and need correcting — until then treat them as ACE-sourced, not retail-verified. | ACE `PlayerFactory.cs:659-660`, `CharacterOptions2.cs` (`ListenToPKDeathMessages = 0x02000000`); `named-retail/acclient.h:4162-4218` (2013 `PlayerOption` terminates at `0x34`); `docs/research/2026-08-10-set-character-options-wire.md` §8.1 | | ~~AP-196~~ | **RETIRED 2026-08-11 at Campaign OP slice OP9.** Filed at the OP4 review-fix round (MUST-FIX 3/blast M2) recording that OP4's Group-C re-point deleted only three of the eight re-pointed `GameplaySettings` fields (`AutoTarget`/`AutoRepeatAttack`/`ViewCombatTarget`), leaving `VividTargetingIndicator`/`CoordinatesOnRadar`/`LockUI`/`AcceptLootPermits`/`ToggleRun` behind as WRITE-BEHIND `settings.json` persistence/draft mirrors of the now-authoritative server bit (plus a "two writable copies" default-source change, ADDENDUM historical only). OP9 verified all remaining `GameplaySettings` members — those five plus `ShowTooltips`/`SideBySideVitals`/`SpellDuration`/`AllowGive`/`ShowHelm`/`ShowCloak`/`AdvancedCombatUI`/`UseMouseTurning`, 13 total — already had a live server-bit home in `RuntimeCharacterOptionsState` (11 as OP4 Character-tab rows through `CharacterOptionTable`/`CharacterOptionsPageController`; `LockUI` through `/lockui` + the PlayerDescription `SetUiLocked` convergence, deliberately not a Character-tab row; `UseMouseTurning` through the Gameplay-tab mouse-macro button + the Config tab's Use-Mouse-Turning row — OP9 review NIT 6's channel-attribution correction) and deleted the `GameplaySettings` record outright — the type, the `SettingsStore.LoadGameplay`/`SaveGameplay` plumbing, and `RuntimeSettingsController`'s `Gameplay` property/`SetAcceptLootPermits` write-behind method — closing the "two writable copies" gap for good: there is no longer a second store to diverge from server truth. | `src/AcDream.App/Settings/RuntimeSettingsController.cs`; `src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs`; `src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs` | — | — | `docs/research/2026-08-10-character-options-map.md` §7.1/§7.2 (Group C re-point directive); `CharacterOptionTable.cs` | | AP-197 | **Filed 2026-08-11 at the OP4 review-fix round (SF-1/S4).** "Display Timestamps" hardcodes retail's `PlayerModule` constructor-default format string `"%#H:%M:%S "` rather than reading the PER-CHARACTER override `GenericQualitiesData::InqString(m_pPlayerOptionsData, 1, &m_TimeStampFormat)` carries when the wire's `GenericQualitiesData` string-key `1` is populated — acdream's `PlayerDescription` parser reads and discards that field (wire research doc: "timestamp string (`0x80`) \| read, discarded \| ❌ \| never sent"). | `src/AcDream.Core/Chat/ChatLog.cs` (`FormatTimestampPrefix`); parser site cited at `docs/research/2026-08-10-set-character-options-wire.md:647` | The 2013 client's own constructor default is the only format any fresh/default character would ever show — retail ships no options-panel control that authors a custom one — so hardcoding the one value every real player sees is a safe, honest approximation until a consumer needs the per-character override. | A character whose account somehow carries a non-default persisted timestamp format (a modded/legacy server, or a hypothetical later retail patch exposing a UI for it) sees acdream render the DEFAULT format instead of their stored one — cosmetic only (still a valid H:MM:SS-shaped timestamp), never a wire or data-loss risk. | `PlayerModule::PlayerModule @0x005D51F0` (ctor default literal); `GenericQualitiesData::InqString` call site (wire doc §3.3); `docs/research/2026-08-10-set-character-options-wire.md` U6 | -| AP-198 | **Filed 2026-08-11 at Campaign OP slice OP6; CORRECTED at the OP6 rework round (2026-08-11, review N1/S2) — the row count was ALWAYS ten (this row's own enumeration always listed ten items); the commit message that said "nine" was the error, now reconciled, and `Render_ScreenBrightness` no longer overloads `Gamma`.** The Config tab's "Graphics Options" + "Rendering Quality Options" sections author ten rows with no acdream renderer consumer: `Render_ScreenBrightness` (its OWN `DisplaySettings.ScreenBrightness` field, range [-1,1] default 0 — NOT the pre-existing `Gamma` multiplier, which has a different unit system and its own live legacy Settings-panel consumer; no gamma-correction pass exists for either), `Render_AutomaticDegrades`, `Render_GraphicsPerformance`, `Render_DegradeDistance`, `Render_LandscapeTextureDetail`, `Render_EnvironmentTextureDetail`, `Render_TextureFiltering`, `Render_LandscapeDrawDistance`, `Render_BuildingDetailTextures`, `Render_MultiPassAlpha`. acdream's world renderer is Vulkan driven by ONE aggregate `QualitySettings`/`QualityPreset` (near/far streaming radii, anisotropic level, alpha-to-coverage, completion budget) — there is no per-feature texture-detail/degrade-distance knob for any of these ten rows to drive. Each round-trips faithfully through `DisplaySettings`/`SettingsStore` and shows retail's own row/label/range (where applicable), with zero observable render effect. **Sub-note, `Render_LandscapeDrawDistance` specifically:** its retail default (`gmConfigUI::InitOptions @0x0049E70D`, `SetDefaultValue(8)`) does not index its own 6-entry `UIPreferences::SetEnumChoices` array (`ID_Graphics_Value_VeryLow`..`Extreme`, `gmClient::InitUIPreferences @0x004041b7`) — reproduced faithfully as an opaque `int` (`DisplaySettings.LandscapeDrawDistance`), not guessed into a clamped index; the Config-tab menu simply shows no highlighted selection at the default. | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (`BindGraphicsSection`/`BindRenderingQualitySection`) | Building ten dead per-feature render knobs into a Vulkan renderer that has no analogous per-feature toggles would be pure UI theater with no correctness payoff; persisting them faithfully keeps the panel honest (every row is clickable, nothing crashes, nothing silently discards a user's choice) while the register makes the "no effect" fact auditable rather than a silent gap a future report would have to re-discover. | A user who changes any of these ten Config-tab controls sees no visual change and, for `LandscapeDrawDistance` specifically, may see no highlighted menu item even after Defaults — both are the CONTRACTED behaviour for this row, not a bug. | `gmConfigUI::InitOptions @0x0049E400`; `gmClient::InitUIPreferences @0x004035b0` (`UIPreferences::AttachPreference`/`SetEnumChoices` calls); `src/AcDream.App/Settings/RuntimeSettingsController.cs` (`QualitySettings`/`ReapplyQualityPreset`) | +| AP-198 | **Filed 2026-08-11 at Campaign OP slice OP6; CORRECTED at the OP6 rework round (2026-08-11, review N1/S2); NARROWED 2026-08-21 by #226.** The Config tab's "Graphics Options" + "Rendering Quality Options" sections author nine rows that still have no acdream renderer consumer: `Render_ScreenBrightness` (its OWN `DisplaySettings.ScreenBrightness` field, range [-1,1] default 0 — NOT the pre-existing `Gamma` multiplier, which has a different unit system and its own live legacy Settings-panel consumer; no gamma-correction pass exists for either), `Render_AutomaticDegrades`, `Render_GraphicsPerformance`, `Render_DegradeDistance`, `Render_LandscapeTextureDetail`, `Render_EnvironmentTextureDetail`, `Render_TextureFiltering`, `Render_LandscapeDrawDistance`, `Render_MultiPassAlpha`. #226 removed `Render_BuildingDetailTextures` from this row: the existing checkbox now directly gates the retail building/EnvCell detail replay and is no longer caption-dimmed as store-only. acdream's remaining world-quality controls are Vulkan driven by ONE aggregate `QualitySettings`/`QualityPreset` (near/far streaming radii, anisotropic level, alpha-to-coverage, completion budget) — there is no per-feature texture-detail/degrade-distance knob for the nine residual rows to drive. Each residual round-trips faithfully through `DisplaySettings`/`SettingsStore` and shows retail's own row/label/range (where applicable), with zero observable render effect. **Sub-note, `Render_LandscapeDrawDistance` specifically:** its retail default (`gmConfigUI::InitOptions @0x0049E70D`, `SetDefaultValue(8)`) does not index its own 6-entry `UIPreferences::SetEnumChoices` array (`ID_Graphics_Value_VeryLow`..`Extreme`, `gmClient::InitUIPreferences @0x004041b7`) — reproduced faithfully as an opaque `int` (`DisplaySettings.LandscapeDrawDistance`), not guessed into a clamped index; the Config-tab menu simply shows no highlighted selection at the default. | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (`BindGraphicsSection`/`BindRenderingQualitySection`) | Building the nine residual dead per-feature render knobs into a Vulkan renderer that has no analogous controls would be pure UI theater with no correctness payoff; persisting them faithfully keeps the panel honest while the register makes the "no effect" fact auditable. | A user who changes any of these nine residual Config-tab controls sees no visual change and, for `LandscapeDrawDistance` specifically, may see no highlighted menu item even after Defaults — both are the CONTRACTED behaviour for this row, not a bug. Building Detail Textures is explicitly outside this residual and must visibly change eligible building/EnvCell surfaces. | `gmConfigUI::InitOptions @0x0049E400`; `gmClient::InitUIPreferences @0x004035b0` (`UIPreferences::AttachPreference`/`SetEnumChoices` calls); `src/AcDream.App/Settings/RuntimeSettingsController.cs` (`QualitySettings`/`ReapplyQualityPreset`); `docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md` | | AP-199 | **Filed 2026-08-11 at Campaign OP slice OP6; CORRECTED at the OP6 rework round (2026-08-11, review M2) — the field names and the "gating to zero when disabled" wording were describing an INVERTED, muted-by-default bug, not the shipped behaviour.** The Config tab's "Sound Options" section authors three rows with no acdream consumer: `Sound_SoundFeatures` (Stereo/Mono menu — acdream's OpenAL backend has no channel-count toggle), the Interface Sound toggle+slider trio (`Sound_InterfaceSoundDisabled`/`Sound_InterfaceSoundVolume` — AP-174 already documents this as retail's OWN dead knob, "registered and then never read... interface sounds are scaled by the EFFECT knob"; acdream matches that exact behaviour rather than building a working Interface bus), and `Sound_PlaySoundOnlyWhenActive` (no window-focus-based audio mute subsystem exists). All three round-trip faithfully through the new `AudioSettings.SoundFeatures`/`InterfaceEnabled`/`InterfaceVolume`/`PlaySoundOnlyWhenActive` fields. The Sound and Ambient trios' own toggle+slider pairs are NOT covered by this row — `SfxEnabled`/`AmbientEnabled`/`Sfx`/`Ambient` are LIVE (`RuntimeSettingsController.SaveAudio` now pushes into `OpenAlAudioEngine` on every change; the effective volume is zero only when the corresponding `*Enabled` flag is false — retail's own `SoundManager::effect_sounds_enabled`/`ambient_sounds_enabled` statics default to enabled, so a fresh profile is audible, not muted). | `src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (`BindSoundSection`) | Matches the SAME reasoning AP-174 already established for the Interface knob specifically; Sound Features and Play-Only-When-Active are honest new store-only rows with no existing or planned acdream subsystem to bind (stereo/mono output selection and window-focus audio gating are both out of this campaign's scope). | A user who changes any of these three Config-tab controls sees/hears no change — the CONTRACTED behaviour, matching retail's own Interface-knob precedent for two of the three. | `gmClient::InitUIPreferences @0x004035b0` (`AttachPreference(&Sound_SoundFeatures, ...)`/`&Sound_InterfaceSoundDisabled`/`&Sound_InterfaceSoundVolume`/`&Sound_PlaySoundOnlyWhenActive`); AP-174 (Interface-knob precedent); `SoundManager::InitPrefs @0x005503F0` (`UserPreferences::RegisterPreference` binding the enabled-sense statics) | | AP-200 | **Filed 2026-08-11 at Campaign OP slice OP6.** The Config tab's "UI Options" section authors `UI_ChatFontFace`/`UI_ChatFontSize` menu rows (retail Windows TrueType face name / a Tiny-Small-Medium-Large-XLarge size-tier enum). These are DELIBERATELY separate NEW fields (`ChatSettings.ChatFontFace`/`ChatFontSizeIndex`) rather than reusing the existing LIVE `ChatSettings.FontSize` (a 10..20pt float acdream's chat panel already renders with) — there is no verified index-to-point mapping from retail's five-tier enum to that float range, and acdream's text rendering has no arbitrary system-font-face swap capability (DAT-baked/bitmap fonts only, not OS TrueType files). Store-only round-trip; `FontSize` is untouched by these two rows. | `src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (`BindUiSection`) | Inventing a size-index-to-point mapping without retail evidence would risk silently overwriting `FontSize`'s own already-live, user-visible behaviour with a guessed value; keeping the two concepts separate is the honest choice until a byte-verified mapping (or a font-face-swap capability) exists. | A user who changes either Config-tab font control sees no chat-panel rendering change; the SEPARATE, pre-existing font-size control (wherever acdream currently exposes `ChatSettings.FontSize`) remains the only live one. | `gmClient::InitUIPreferences @0x0040387b`/`@0x00403a1a` (`AttachPreference(&UI_ChatFontFace, ...)`/`&UI_ChatFontSize`, `SetEnumChoices` choice arrays "Arial"/"Tiny".."XLarge") | | AP-172 | **Filed 2026-08-08 (#354 fix — spell-bar drag reorder).** Retail removes a lifted favorite from `PlayerModule` (+ UI list + wire) the instant a drag starts and the remaining shortcuts visibly slide left to close the gap for the rest of the gesture (`RecvNotice_ItemListBeginDrag` → `RemoveSpellFromMenu`, live). acdream's controller performs the same PlayerModule/wire removal at drag-begin but DEFERS the whole favorite-list's visual rebuild until the drag concludes (drop or off-bar release) — the lifted cell's icon stays visible in its old slot and siblings do not slide until release, instead of reflowing continuously through the gesture. `DropFavorite` compensates by porting retail's own `AddFavorite`-side index adjustment (decrement the target index by one when the lifted item's original index was before it) against the now-intentionally-stale sibling numbering, so the FINAL landed position is byte-identical to retail's in every case exercised (`DragFavoriteOntoAnotherSlot_ThroughTheRealPointerPipeline_ReordersAndSyncsWire`). **NARROWED + CORRECTED 2026-08-08 (drop-ring change).** Correction: this row originally claimed empty-tail-slot drops were "already-live-count-relative and are untouched" — false. The #354 `-1` adjustment sat inside `DropFavorite`, which the empty-cell path also calls, so its live-count-clamped (post-lift-numbered) index was double-corrected: lifting a non-last favorite onto the empty tail landed it second-to-last instead of last. `FavoriteDropIndex` is now THE one landing computation and applies retail's rule exactly — the `-1` is gated on the lifted spell's pre-lift-numbered removal site (retail's `RemoveSpellFromMenu`-return-gated decrement @0x004C7157), which for a live-numbered empty-tail target is retail's `RemoveSpellFromMenu == -1` no-adjustment case (test `SpellFavoriteDrag_DroppedOnTheEmptyTail_AppendsAtTheEnd` fails against the double-correcting code). Narrowing: the mid-drag presentation now includes retail's authored drag-over Accept ring — `SpellCastSubMenu::OnItemListDragOver` @0x004C5990 setting the per-cell authored DragAccept child (element 0x1000045A, `UIElement_UIItem::PostInit` @0x004E1870) to `ItemSlot_DragOver_Accept` (UIStateId 0x10000040 → authored art 0x060011F9) on the hovered cell while a spell drag is live, cleared on leave/drop (`UiCatalogSlot.DragOverAcceptance` → `UiItemSlot.DrawDragAcceptOverlay`), with the ring and the drop sharing `FavoriteDropIndex` so the ring cannot promise a different landing. | `src/AcDream.App/UI/Layout/SpellcastingUiController.cs` (`BeginFavoriteDrag`, `EndFavoriteDrag`, `DropFavorite`, `Tick` — the `_favoriteDragActive` gate) | `UiRoot`'s subtree-removal safety net (`ClearSubtreeOwnership`, `UiRoot.cs:240-247`) cancels any in-flight drag whose source widget is destroyed, and `Rebuild()` tears down and recreates every cell in the list (`UiItemList.Flush` → `RemoveChild` per cell) rather than incrementally diffing. Left unguarded, the press-time removal's `SpellbookChanged` event would let the very next per-frame `Tick()` (production drives this unconditionally via `RetailUiRuntime.Tick`) destroy the cell driving the gesture and silently cancel the reorder before the user could complete the drop — this was the reported bug. Deferring the rebuild for the gesture's duration is the minimal fix that does not touch the shared `UiRoot` drag machinery every other panel (toolbar/inventory/vendor/paperdoll) also depends on. | A future rewrite that makes `Rebuild()` an incremental per-cell diff (add/remove/reflow one cell) instead of flush-and-recreate-all would make this deferral unnecessary and should retire this row along with it — until then, a player watching their OWN spell bar mid-drag sees the vacated slot's icon linger and siblings snap into place only on release, rather than reflowing live as retail does — and one ring consequence of that frozen bar: when dragging rightward past the source, the Accept ring's SCREEN slot sits one cell right of where the icon finally lands (retail's live-reflowed bar makes them coincide); the ring is on the correct CELL in both — the spell lands immediately before that cell's spell, retail's exact insert-before semantic. No effect on final position, the wire pair sent, or any other panel; cross-window spellbook→favorite drops are live-count-relative and untouched (the empty-tail claim this sentence used to carry was corrected 2026-08-08 — see the Divergence column). | `gmSpellcastingUI::RecvNotice_ItemListBeginDrag` @0x004C7360 (`SpellCastSubMenu::RemoveSpellFromMenu`, immediate live-list removal at lift); `SpellCastSubMenu::AddFavorite` @0x004C7060 (`RemoveSpellFromMenu`'s return value gating the `-1`-if-lifted-before-target `m_numSpells` adjustment before `ItemList_InsertSpellShortcut`); `PlayerModule::AddSpellFavorite` @0x005D43E0 (`InsertPos`); `PlayerModule::RemoveSpellFavorite` @0x005D4910 | @@ -320,7 +364,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-129 | **NARROWED 2026-07-30 (P4 Opus review fix) — `CanMoveInto`/`IsAllowedIn` are now ported and fed; two narrow gaps remain.** `ObjectInfo.CheckEntryRestrictions` resolves the cell's `RestrictionObj` via `PhysicsEngine.Objects` (a `ClientObjectTable`, acdream's `GetObjectA` equivalent) and evaluates the real owner IID / `HouseRestrictionRecord` (open flag, allegiance monarch, guest table) fed from CreateObject's `HouseOwner`/`HouseRestrictions`/`Monarch` PWD-tail fields and live `House_UpdateRestrictions (0x0248)` refreshes — see `RestrictionObjPrevalenceInspectionTests` (103,766 of 729,888 installed EnvCells, 1,293 landblocks, carry a baked `RestrictionObj`; this is the whole housing estate, not a rare case, which is why the OLD unconditional-fail-closed row was upgraded to FIX-FIRST rather than shipped). Remaining gaps: (1) `House_UpdateRestrictions`'s `Sequence` byte is parsed but not used for staleness/reordering rejection — a lost-then-late UDP delivery could transiently apply an older restriction snapshot over a newer one (low-probability; the next full CreateObject or another update self-corrects). (2) Outdoor `CLandCell` restriction (`LandblockInfo.RestrictionTables`, a separate per-landblock packed hash table) remains entirely unported — unaffected by this fix, since the gate only reads the indoor/EnvCell `CellPhysics.RestrictionObj` field. `HouseData (0x0225)`/`HouseStatus (0x0226)` and the guest-management opcode family (`House_AddPermanentGuest`, `House_UpdateHAR`, etc.) remain unparsed but are NOT consulted by this entry gate (they carry rent/ownership-transfer UI data, not the owner-iid/guest-list pair `CanMoveInto` needs) — noted for future house-UI work, not a residual of this row. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`ObjectInfo.CheckEntryRestrictions`); `src/AcDream.Core/Physics/PhysicsEngine.cs` (`Objects`); `src/AcDream.Core/Items/{ClientObject,ClientObjectTable,HouseRestrictions}.cs`; `src/AcDream.Core.Net/{Messages/CreateObject.cs,Messages/GameEvents.cs,GameEventWiring.cs}`; `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs` (production wiring) | A reordered `House_UpdateRestrictions` pair could transiently apply the older snapshot; self-corrects on the next update or CreateObject. An outdoor restricted cell (if that content ever exists) is not gated at all. | `ACCWeenieObject::CanMoveInto` 0x0058da40 (pc:407982-408056); `RestrictionDB::IsAllowedIn` 0x005ae8f0 (pc:444493-444516); `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Types/RestrictionDB.generated.cs`; `references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventHouseUpdateRestrictions.cs` | | AP-72 | **Cursor art falls back to OS standard cursors when dat resolution fails** — retail always renders MediaDescCursor / EnumIDMap-resolved dat cursor art; acdream's `RetailCursorManager.Apply` falls back to Silk `StandardCursor` (IBeam/crosshair/not-allowed/…) when the EnumIDMap chain or RenderSurface decode fails, and `RetailCursorResolver`/`RetailCursorManager` permanently negative-cache the failed enum/surface id for the session. | `src/AcDream.App/Rendering/RetailCursorManager.cs:47` (`ApplyStandard`), `RetailCursorResolver.cs:47` (negative cache) | Fallback triggers only when the dat lacks the asset — nominal EoR dats always resolve the 0x27/0x28/0x29 chain; an OS cursor keeps the UI usable rather than showing nothing. | A dat-read or decode regression silently shows OS-native cursors instead of surfacing an error — masked failure class; check the `[D.2b]` cursor log lines before suspecting art. | `ClientUISystem::UpdateCursorState` 0x00564630 | | AP-74 | **UseDone WeenieError text comes from a hardcoded subset map, not the portal String tables** — retail resolves the 0x01C7 UseDone error code through the client String tables into the canonical line ("You are not trained in healing!"); acdream's `WeenieErrorText.For` hardcodes the handful of codes the current use/heal flows produce (0x001D/0x04EB/0x04FC/0x04FE, texts phrased after the ACE enum names) with a generic code-carrying fallback. | `src/AcDream.Core.Net/Messages/WeenieErrorText.cs` | Every refusal is now visible; only unmapped wording deviates, and those lines retain the raw code. Retire by porting the String-table lookup (#202). | An unmapped WeenieError shows a generic line instead of retail's exact sentence | retail String-table error lookup; ACE `WeenieError.cs` values | -| AP-73 | **Character raises mutate optimistically, contrary to retail's server-authoritative flow** — after sending RaiseAttribute/RaiseVital/RaiseSkill/TrainSkill, `CharacterSheetProvider.ApplyLocalRaise` immediately bumps ranks and debits XP/credits. Named retail permits one request in flight, ghosts the clicked button, and waits for an authoritative quality-change element message before changing displayed state (**#199**). | `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` | ACE usually accepts client-affordable raises, so its later property echoes conceal the incorrect prediction; Wave 8 removes local mutation and owns one awaiting request | A rejected/reordered raise can display invented state until a later full refresh, and repeated clicks can create multiple speculative spends | `gmAttributeUI`/`gmSkillUI` raise and quality-change paths, pinned in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` | +| AP-73 | **NARROWED 2026-08-24 (Campaign CA CA4)** — the optimistic mutation this row filed is DELETED: `ApplyLocalRaise` and the six LocalPlayerState optimistic apply/debit methods are gone, and the raise flow now matches the pinned retail mechanism (one request in flight, raise controls ghost while awaiting, displayed state changes only when the authoritative quality records land — which CA2's inbound 0x02E3/0x02DD parsers now deliver; release on any quality-change event mirrors `gmStatManagementUI::ListenToElementMessage @ 0x004EFBE0`). REMAINING OPEN POINT (the reason this row narrows instead of retiring): retail's release behavior on a rejection that produces NO quality change is statically unverifiable (the pseudocode doc's own §5 caveat), and ACE sends chat-only for a failed Raise* and NOTHING for a rejected RaiseSkill/TrainSkill — acdream holds the gate until the panel remounts (retail's per-instance flag lifetime), which may differ from retail's live behavior. Verify at the CA5 connected gate (deliberately provoke the vital raise-10-with-1-affordable client bug ACE's own comment documents). | `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` (`HandleRaiseRequest`/`ReleaseAwaitingRaise`); `src/AcDream.App/UI/Layout/CharacterStatController.cs` (AwaitingRaise ghosting) | A silently-rejected request leaves the raise controls ghosted until the panel is reopened | Raise buttons stuck ghosted after a failed raise until panel close/reopen — visible only on server-rejected requests | `gmStatManagementUI @ 0x004F03F0`; `gmAttributeUI::RaiseSelection @ 0x0049D020`; `gmSkillUI::RaiseSelection @ 0x0049C8C0`; `InfoRegion::OnQualityChanged @ 0x004F0EB0`; `ListenToElementMessage @ 0x004EFBE0`; pinned in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` §5 | --- | AP-75 | **NARROWED 2026-07-19 — adapter-boundary `adjust_motion` only.** `SetCycle` remaps TurnLeft/SideStepLeft/WalkBackward to their mirror command with negated speed before dispatch. Retail performs that normalization in `CMotionInterp`; GameWindow's local-player adapter can still pass raw ids directly | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap) | Preserves raw local callers until every caller enters through `MotionInterpreter`; literal DAT velocity and omega now flow through CSequence's complete Frame | A future caller that already normalizes a raw left/back command but still passes the original id can be adjusted twice | `CMotionInterp::adjust_motion` @305343; retire with the remaining local caller unification | @@ -349,8 +393,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-105 | **PARTIAL 2026-07-13** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`), and both chat backends share typed client-command routing plus one retained `ChatVM` for reply state. Retained chat still lacks complete tab/filter/unread, social availability, incoming squelch enforcement, and focus-opacity behavior. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, command ownership, and outer maximize geometry work; later chat work consolidates the remaining presentation/filter state | Tabs are no-ops, squelched lines can still render, contextual social actions are absent, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods | | ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` | | AP-108 | Paperdoll/AutoWield still omit the remaining missile/held restrictions and corrupt-mask branch of full `AutoWieldIsLegal`, dual-wield/off-hand rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **AutoWear legality retired from this row 2026-07-23:** inventory activation and paperdoll drops now apply the retail clothing-priority/location blocker lookup and exact `"You must remove your %s to wear that"` system notice. **Primary replacement retired 2026-07-14; Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, live doll, AutoWear conflict reporting, and primary weapon/incompatible shield/mismatched ammo blocker sequencing work in peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWieldIsLegal @ 0x0055ED60`; `CPlayerSystem::AutoWearIsLegal @ 0x0055EF40`; `CPlayerSystem::AutoWield @ 0x00560A60`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` | -| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` | -| AP-110 | **NARROWED 2026-08-09 (Slice 5.4, vendor browse panel) — "vendor" retired from the absent-panels list; see AP-161 for the precise successor (Buy/Sell transaction UI, Slice 6).** Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination has its independent authored floaty layout, inscription transaction, retail creature stat/rating/animated-preview presentation, default selection-follow, authored local spell subview with appropriate-formula component state, and the full EoR item-report dispatch: appraisal-only unknowns; exact equipment-set/rating/tinkering/weapon/armor/caster/requirement/XP/healer/rare prose and intentional blank section rows; ordinary/enchantment DAT spell descriptions; live material-decorated appropriate titles plus DAT material and creature names; expiry, decorated material/gem descriptions; and portal/PK restrictions with authored item colors. It still lacks item-object preview, player-dependent effective shield projection, live cooldown-remaining projection, localized augmentation-cost `StringInfo`, exhaustive character detail regions, and exact creature appraisal FontInfo-list selection. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs`; `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs`; `src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, the Slice 5.4 vendor "Items" browse panel (category-filtered stock list, retail's quantity-correct pricing — `ItemHolder::GetObjectSplitSize`'s split-exempt mask, not a flat per-unit price), and the core examination request/presentation/inscription/creature-preview/item-report loop cover the active loops; the residual examination mechanisms require live player/enchantment/localization state or object-preview ownership rather than fabricated content | Item assessments omit only the listed live/localized/preview projections; enchanted/incomplete creature appraisal rows use the normal authored font until the exact FontInfo list is bound; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0`; `gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0`; `gmExaminationUI::ExamineSpell @ 0x004B6900`; `SpellExamineUI::ExamineSpell @ 0x004B6210`; `AttributeInfoRegion::Update @ 0x004F1D90`; `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ACCWeenieObject::GetObjectName @ 0x0058E6E0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `ItemExamineUI::AddItemInfo @ 0x004AC050`; `ItemExamineUI::Appraisal_ShowCapacity @ 0x004B2680`; `ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140`; `ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0`; `ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10`; `ItemExamineUI::Appraisal_ShowDescription @ 0x004B6990`; `MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500`; `ItemExamineUI::SetInscription @ 0x004AE2F0`; `CM_Writing::Event_SetInscription @ 0x006A98B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog | +| AP-109 | **NARROWED FURTHER 2026-08-25 (Campaign AS slice AS5) — the 17-function heritage×gender `AllegianceSystem::GetTitle @0x005B8DD0` table and `AllegianceData::GetFullName @0x005B6950` are now ported VERBATIM** (`AllegianceRankTitleTable`, `src/AcDream.App/UI/Layout/AllegianceRankTitleTable.cs` — every one of the 17 `Get*Title` functions transcribed string-for-string from the decomp, including the PE-byte-recovered data-literal indirections in the Sho/Gearknight/Tumerok tables) **and wired to BOTH windows this row named as open**: the examination window's title bar (`AppraisalUiController.BuildCharacterTitleBarName`, called from the `character` branch of `ApplyCreature` — rank/heritage/gender read LIVE off the APPRAISAL bundle, `props.GetInt(0x1E)`/`0xBC`/`0x71`, ruling R8, never `RuntimeAllegianceState`) and the character panel's NAME line (`CharacterSheetProvider.BuildSheet`, the exact `props.GetInt(0x1Eu)` read this row's own CT4 text already prescribed as the correct future port). Both call sites were independently re-verified against the decomp at this slice: `CharExamineUI::SetAppraiseInfo`'s local `AllegianceData` (BN name `var_a8`, proven by its `CAllegianceData::CAllegianceData`/`~AllegianceData` ctor/dtor pair) never shows an explicit field WRITE for `_rank`/`_hg`/`_gender` — a Binary Ninja struct-flattening artifact, not a missing read — while `gmStatManagementUI::UpdateCharacterInfo` shows the same three `CBaseQualities::InqInt(0x71/0xbc/0x1e)` calls as plain, unambiguous locals, independently confirming the property ids this row's CT4 text already named. This closes the row's stated risk (a ranked character's Name line showing plain-name-only). **NOT closed by this slice, and the reason this row survives NARROWED rather than RETIRED:** the CT4 narrowing's own `FormatXp` caveat, immediately below — the Luminance pair's number formatting remains a `.ToString("N0", CultureInfo.InvariantCulture)` approximation of retail's `XPToString`→`GetNumberFormatA` Win32 call, unverified on an exotic negative/overflow input. That caveat is now this row's ONLY open item.** **NARROWED FURTHER 2026-08-25 at the Campaign CT4 fix round — the luminance pair's TEXT is now bound and the PK classification now reads the live PWD bits, closing both out of this row.** `CharacterSheetProvider.PkStatusText` classifies off `ClientObject.PublicWeenieBitfield` bits `0x20` (IsPK) / `0x02000000` (IsPKLite) — the exact `ACCWeenieObject::IsPK @0x0058c8b0` / `IsPKLite @0x0058c8a0` PWD-bitfield reads, ported already at `PlayerKillerStatusBitfield.Apply` (#297) — instead of the CT4-landed bitwise test against raw PropertyInt 134 (a non-retail mapping: PropertyInt 134 carries ACE's own `PlayerKillerStatus` enum values, not the PWD bit layout). `CharacterStatController`'s luminance pair (`0x100005C5`/`0x100005C6`) now binds real text: caption `"Luminance:"` (UTF-16, PE-byte-recovered from the `gmStatManagementUI` vftable-adjacent data region at `@0x007c3dd4`) and value `" / "` (narrow `"%s / %s"` format, PE-byte-recovered at `@0x007c3dcc`, args in that order per `UpdateExperience`'s call sequence — `ExperienceSystem::XPToString(AvailableLuminance, ...)` then `XPToString(MaximumLuminance, ...)`), both numbers formatted through the same shared `FormatXp` helper the Total XP / XP-to-level fields use (`.ToString("N0", CultureInfo.InvariantCulture)` — the C# equivalent of retail's `XPToString`→`GetNumberFormatA` locale-grouped-decimal call; not a byte-identical Win32 port, so an exotic edge case, e.g. negative/overflow, is this row's own residual sliver if one is ever found). The hide path is retail's own `UIElement_Text::ClearAllText` (`@0x004f0e31`/`@0x004f0e3c` — empties `LinesProvider` content, leaves layout) rather than `Visible = false`. (CT3's Titles-page narrowing, restored here verbatim after CT4's edit compressed it to a bare pointer phrase, still stands:) **CT3's narrowing (2026-08-24), verbatim:** `CharacterTitlesController` binds the Titles page (`gmCharacterTitleUI`, LayoutDesc `0x2100002E` element `0x10000539`) through the standard `UiTemplateListBox`/`UiScrollbar`/`UiButton` classes — no bespoke widgets: the earned-titles list sorted by resolved display string (`FindSortedInsertPosition @0x0049A760`), the current display-title text (`Refresh @0x0049abc0`, including its hardcoded `"Unknown"` fallback, refreshed on both the table-replace and display-change notices), row selection using the row template's own authored Highlight state (the same `InfoRegion::SetState(6)` mechanism CT1 confirmed for the stat rows), the "Set as Display Title" button's Ghosted-unless-a-differing-selection gate (`UpdateButtons @0x0049A500`, CORRECTED direction per the CT campaign plan's CT1 fix round — no selection is the Ghosted case), and the `TitleSet (0x002C)` wire send through CT2's `RuntimeCharacterTitleState`/`IRuntimeCharacterCommands.SetTitle` (no local mutation). Campaign CT slice CT4 (2026-08-24) then put the header identity block live: `CharacterStatController`'s Name/Heritage/PkStatus/Level labels use `LabelAuthoredColor` (the widget's own DAT-set `DefaultColor`/Outline, matching CT1's live-DAT pin — the former hardcoded `Body`/`Gold` runtime constants are deleted); the heritage line appends CT2/CT3's resolved display title VERBATIM (`CharacterIdentityText.StatHeaderLine`, CT4-fix-round-corrected 2026-08-25 to stop stripping a leading "The " — retail `AppendText`s the resolved title unmodified at `@0x004f0990`, and 26 real ACE `CharacterTitle` entries begin with "The"); the level shows `"%d"`-formatted `InqInt(0x19)` or the literal `"???"` when absent (both PE-recovered). One item remains open, registered rather than silently dropped: the NAME line ships the PLAIN-NAME case only — retail's allegiance-rank prefix (`AllegianceData::GetFullName @0x005b6950` → `AllegianceSystem::GetTitle @0x005b8dd0`) needs a ~170-string, **17-function** [CORRECTED 2026-08-25 from CT4's original 22-function/~200-string estimate — `GetTitle`'s own dispatch switch (`@0x005b8dd0`) was read directly: Gearknight and Tumerok author only a MALE `Get*Title` function, reused for both genders' dispatch branches, and Lugian authors only a FEMALE one, reused for both — 11 heritages produce 17 functions, not 22 (2 each for Aluvian/Gharu'ndim/Sho/Viamontian/Shadowbound/Empyrean/Undead, 1 each for Gearknight/Tumerok/Lugian); Olthoi/OlthoiAcid (heritage ids 12/13) have no title function at all — `GetTitle`'s own range check `(heritage-1) <= 0xa` (unsigned) excludes them, and heritage id `0xa` (Penumbraen) aliases to the Shadowbound functions] heritage×gender title table (verbatim in the decomp, e.g. `GetAluvianMaleTitle @0x005b7bc0`'s "Yeoman"/"Baronet"/.../"High King") that CT4 judged out of "reasonable size" for this slice. The RANK value is PropertyInt `0x1E` (`AllegianceRank`) read LIVE off the qualities bundle (`CBaseQualities::InqInt(qualities, 0x1e)` — ACE actively pushes this property on every allegiance-rank change) [CORRECTED 2026-08-25 — CT4's original text claimed `RuntimeAllegianceState` "already carries the local player's own rank," conflating this row's context with `SocialAllegiancePageController`'s OWN, DIFFERENT, already-documented substitution (that controller has no qualities-bundle access, so it renders `RuntimeAllegianceSnapshot.Rank` — same `0x0020 AllegianceUpdate` wire message, numerically equivalent in every observed case — as its own accepted stand-in). `CharacterSheetProvider.BuildSheet` already reads every other header property straight off `props.GetInt(...)` from the qualities-equivalent `PropertyBundle`, so the correct future port reads `props.GetInt(0x1Eu)` directly, not `RuntimeAllegianceState` — only the STRING table is missing, not the data]. | `src/AcDream.App/UI/Layout/CharacterStatController.cs` (`LabelAuthoredColor`, `RefreshLuminanceVisibility`, `FormatXp`); `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` (`PkStatusText`); `src/AcDream.Core/Items/ClientObject.cs` (`PlayerKillerStatusBitfield`); `src/AcDream.App/UI/Layout/CharacterSheet.cs`; `src/AcDream.App/UI/Layout/CharacterIdentityText.cs`; `src/AcDream.App/Composition/InteractionRetainedUiComposition.cs`; `src/AcDream.App/UI/Layout/CharacterTitlesController.cs` (Titles page, CT3, unchanged); `src/AcDream.App/UI/Layout/AllegianceRankTitleTable.cs` (AS5, new — the 17-function title table + `GetFullName` port); `src/AcDream.App/UI/Layout/AppraisalUiController.cs` (AS5, examination window title-bar overwrite) | Attributes/skills core output and the Titles-page binding seam are user-accepted; evidence for the header-identity block is synthetic-layout binding tests plus a small number of InstalledDat string/DID pins (the three PK strings, the gender/heritage EnumMapper chain at AP-235); AS5 adds hermetic golden-value conformance tests for all 17 `Get*Title` functions (`AllegianceRankTitleTableTests`) plus fixture-layout binding tests for both the examination title bar and the character panel name line — still not a connected/live gate | The title-table gap is CLOSED (was: a ranked-allegiance character's Name line showing plain name only). The row's ONLY remaining risk: an exotic negative/overflow Luminance value could format differently than retail's byte-exact `GetNumberFormatA` — `FormatXp`'s `.ToString("N0", CultureInfo.InvariantCulture)` is a documented approximation, not a byte-identical Win32 port, and this has never been observed or reproduced | `gmStatManagementUI::UpdateCharacterInfo @ 0x004F0770`; `UpdatePKStatus @ 0x004F00A0`; `UpdateExperience @ 0x004F0A70`; `UIElement_Text::ClearAllText @ 0x004F0E31`/`0x004F0E3C`; `ACCWeenieObject::IsPK @ 0x0058C8B0`; `IsPKLite @ 0x0058C8A0`; `CharExamineUI::SetAppraiseInfo @ 0x004B45F0`; `AllegianceData::GetFullName @ 0x005B6950`; `AllegianceSystem::GetTitle @ 0x005B8DD0` | +| AP-110 | **NARROWED 2026-08-25 (Campaign AS, the assess/examination-window retail-parity campaign) — "exhaustive character detail regions" retired from the still-lacks list.** AS2 ported the player header identity block (composed gender+heritage, current display title, PK status from the local weenie's PWD bits, allegiance name); AS3 ported the per-bodypart armor-level trio (with the `*` unenchantable sentinel), the ratings-family spacer discipline, and the unconditional `* = Unenchantable` legend; AS4 ported the remaining extras-list rows — society/faction (rank bands, local-vs-target faction color rule), the Monarch/Patron/Followers cascade, and the seven configurable extras (Fellowship, Arrived in Dereth, Time in Dereth, Chess Rank, Fishing Skill, Deaths, Titles Earned) — closing the character-path extras list end to end. See `docs/research/2026-08-25-campaign-as-ground-truth.md` for the full row-by-row decomp citations; the row's OTHER residuals (item-object preview, effective shield projection, cooldown-remaining, augmentation-cost `StringInfo`, creature FontInfo-list selection) are untouched by this campaign and remain open below. **NARROWED 2026-08-09 (Slice 5.4, vendor browse panel) — "vendor" retired from the absent-panels list; see AP-161 for the precise successor (Buy/Sell transaction UI, Slice 6).** Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination has its independent authored floaty layout, inscription transaction, retail creature stat/rating/animated-preview presentation, default selection-follow, authored local spell subview with appropriate-formula component state, and the full EoR item-report dispatch: appraisal-only unknowns; exact equipment-set/rating/tinkering/weapon/armor/caster/requirement/XP/healer/rare prose and intentional blank section rows; ordinary/enchantment DAT spell descriptions; live material-decorated appropriate titles plus DAT material and creature names; expiry, decorated material/gem descriptions; and portal/PK restrictions with authored item colors. It still lacks item-object preview, player-dependent effective shield projection, live cooldown-remaining projection, localized augmentation-cost `StringInfo`, and exact creature appraisal FontInfo-list selection. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs`; `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs`; `src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, the Slice 5.4 vendor "Items" browse panel (category-filtered stock list, retail's quantity-correct pricing — `ItemHolder::GetObjectSplitSize`'s split-exempt mask, not a flat per-unit price), and the core examination request/presentation/inscription/creature-preview/item-report loop cover the active loops; the residual examination mechanisms require live player/enchantment/localization state or object-preview ownership rather than fabricated content | Item assessments omit only the listed live/localized/preview projections; enchanted/incomplete creature appraisal rows use the normal authored font until the exact FontInfo list is bound; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `CharExamineUI::SetAppraiseInfo @ 0x004B45F0`; `CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0`; `gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0`; `gmExaminationUI::ExamineSpell @ 0x004B6900`; `SpellExamineUI::ExamineSpell @ 0x004B6210`; `AttributeInfoRegion::Update @ 0x004F1D90`; `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ACCWeenieObject::GetObjectName @ 0x0058E6E0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `ItemExamineUI::AddItemInfo @ 0x004AC050`; `ItemExamineUI::Appraisal_ShowCapacity @ 0x004B2680`; `ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140`; `ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0`; `ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10`; `ItemExamineUI::Appraisal_ShowDescription @ 0x004B6990`; `MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500`; `ItemExamineUI::SetInscription @ 0x004AE2F0`; `CM_Writing::Event_SetInscription @ 0x006A98B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog | | AP-161 | **REVIEW CORRECTIONS 2026-08-09 (Opus review of `92ea3977`, findings F1-F13):** thirteen further fixes, mostly bug-fixes-to-already-claimed-behavior rather than new divergences, so no new AP row is filed for most of them; the exceptions are called out below. F1 ports Buy All's four retail pre-send guards (pyreal/alt-currency affordability, container-slot/item-slot capacity) — see AP-162's narrowing. F2 corrects `AddToBuyList` from upsert to retail's actual ACCUMULATE-with-5000-cap semantics and ports `RemoveFromShop`'s shop-row hide/restore as staging consumes limited vendor supply. F3 corrects `VendorSellAcceptability`'s too-valuable branch to the byte-verified bitwise-complement form (`(~(itemTypeMask >> 16)) & 4`), exempting `PromissoryNote` items. F4 wires `BF_RETAINED` end to end, RETIRING AP-164 below. F5 ports `UpdateDragOver`'s auto-switch-to-Selling-on-hover. F6 corrects sell staging to always record the FULL stack (never the live split slider) and ports `SellSingleItem`'s partial-stack refusal plus its literal amount-1 send. F7 corrects the X-close confirmation string's missing trailing question mark. F8 disposes a live confirmation dialog on session Close/Reset. F9 repaints the Buying/Selling strips' own selection highlight on every selection change, not just a staging change. F10 unstages a sell entry that leaves `ClientObjectTable` and a buy entry whose shop row is retired, the latter with retail's exact notice. F11 reorders `RequestUse`'s eligibility check ahead of `BeginApproach` so an ineligible far target no longer speculatively approaches. F13 makes Sell Item act on the global selection unconditionally, matching retail — a prior version of this port required a staged entry first. New approximations this pass introduced are filed as AP-167 (`SellSingleItem`'s non-empty-container refusal branch not ported) and AP-168 (Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container`). **NARROWED 2026-08-09 (Slice 6b/6c, staging+sell arc) — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES.** `VendorUiController` now fully wires both tabs: Buying (`Add to List`/`Buy Item`/`Buy All`/`Clear Item`/`Clear List`, backed by `VendorStagingList`) and Selling (drag-to-sell via `IItemListDragHandler`, `VendorSellAcceptability`'s port of `VendorProfile::InqAcceptability`, `Sell Item`/`Sell All`/`Clear Item`/`Clear List`), plus the X-close staging confirmation dialog (`RetailDialogFactory`, the exact retail string recovered from the decompiled binary's data segment at `0x007b5bd8`). Sell (`0x0060`) is wired end to end (`VendorRequests.BuildSell`/`WorldSession.SendSell`/`ItemInteractionController.TrySell`). Three narrow residuals from this pass are filed separately rather than folded in here: `InqAcceptability`'s non-sellable bitfield check is unmodeled (AP-164), the Buying tab's stackable-removal-amount test substitutes `VendorShopItem.DescStackSize` for retail's `_maxStackSize` (AP-165), and the Buying/Selling tabs' own per-row/purse count text plus the cross-panel "pending sell" inventory highlight are not wired (AP-166). The two PRE-EXISTING residuals below (dropdown arrow-cap glyph, alt-currency `m_last_sale` simplification) are UNCHANGED by this pass — see the ORIGINAL text below for their citations. **REVIEW CORRECTIONS 2026-08-09 (Opus review of `97cf8738`, findings F1-F9):** none of these are NEW divergences from retail — they are bug fixes that make this row's own claims actually true, so no new AP row is filed for them. F2 fixed the priced/named quantity freezing at a selection-time seed while the Buy button separately read the LIVE slider — both now share one `ResolveBuyQuantity` computation, so the displayed price always equals what a purchase actually charges (retail: `gmVendorUI::RecvNotice_StackSliderChanged` re-runs the SAME display update on every slider change, `pc:203262-203278`). F6 corrected an unauthored "preserve the prior selection if it survives the filter" rule to retail's actual UNCONDITIONAL reselect-to-first-item on every rebuild this controller reaches (`VendorItemsUI::UpdateItemsList`'s notify=1 path, `pc:201180-201184`, confirmed reached by a fresh open AND a same-vendor refresh via `VendorItemsUI::OpenVendor`'s unconditional `SetSelectedItem(...,1)`, `pc:201022`). F7 ported `BuySingleItem`'s stack-size-1 quantity clamp (`pc:201674-201681`) so a stale slider value left over from a previously-selected, DIFFERENT stackable item cannot leak into a non-stack purchase. F8 is recorded inline below, where it corrects this row's own stale claim about the Add-to-List button. **NARROWED 2026-08-09 (Slice 6.1-6.3, buy arc) — TWO of the four consciously-deferred residuals below CLOSE.** Private per-panel selection is GONE: `SelectionState` gains a `Vendor` change source (`SelectionChangeSource.Vendor`) and is now the AUTHORITY — row clicks, the F4 auto-select-first-item fallback, and right-click examine all call `SelectionState.Select`/`Clear`; `VendorUiController` is a CONSUMER (`OnSelectionTransition`) exactly like every sibling panel, matching retail's global `ACCWeenieObject::selectedID`. The examine gap (F7c) is GONE too: `VendorShopItemMaterializer` (`src/AcDream.Runtime/Gameplay/VendorShopItemMaterializer.cs`, Slice 6.1) registers every `ApproachVendor` shop item into `ClientObjectTable` (guid, `ContainerId = vendorGuid`, merge-upserted via the ordinary `Ingest` path, retired on session Close/Reset/vendor-replace via the SAME `VendorState.Changed` subscription) so `AppraisalUiController.Apply`'s lookup now succeeds; `VendorUiController.ExamineItem` wires `UiItemList.ExamineItemRequested` to `ItemInteractionController.ExamineSelectedOrEnterMode`, mirroring `ExternalContainerController`. **Double-click-to-buy was investigated (research doc `docs/research/2026-08-08-slice6-vendor-transactions-research.md` §B.2) and confirmed ABSENT from retail** — no `gmVendorUI::CheckForDoubleClick`/`VendorItemsUI::CheckForDoubleClick` symbol exists anywhere in the 18,366-function named table, unlike sibling panels (`gmContractsUI::CheckForDoubleClick`, `gmPageListUI::CheckForDoubleClick`) that DO have one; acdream intentionally does NOT add a double-click shortcut — a user request for it as a deliberate acdream-only UX addition would need its own AP row, per CLAUDE.md's no-invented-mechanisms discipline. The remaining two residuals (dropdown arrow-cap glyph, alt-currency `m_last_sale` simplification) are UNCHANGED — see below. New approximations this pass introduced are filed separately as AP-162 (no client-side Buy pre-check) and AP-163 (shop-item guid-collision policy). **Original REWRITTEN text follows, retained for the two still-open residuals:** `VendorUiController` mounts LayoutDesc `0x21000012`/root `0x100000B7` and fully wires only the authored "Items" tab (`0x100000B9` — `VendorItemsUI`: category-filtered browse list with retail's quantity-correct pricing, `ItemHolder::GetObjectSplitSize`'s `0xDC41CB0` split-exempt mask ported locally rather than a flat per-unit price). The other two authored tabs render and switch pages (so the layout looks complete) but are otherwise INERT: "Buying" (`0x100000BA`, `VendorBuyUI` — staged-purchase review/confirm, buttons `0x100000C9`/`CA`/`CB`/`CC`) and "Selling" (`0x100000BB`, `VendorSellUI` — staged-sale review/confirm, buttons `0x100000D2`/`D3`/`D4`/`D5`) have no data binding at all. The "Items" page's own `Buy` button (`0x100000C2`) correctly enables/disables with selection (`UiButton.Enabled`, retail `SetState(1)`/`SetState(0xd)`) and Slice 6.3 wires it to a real immediate single-item purchase (`gmVendorUI::BuySingleItem`, `pc:201661` — `VendorRequests.BuildBuy`/`WorldSession.SendBuy`, opcode `0x005F`). **Review correction 2026-08-09 (F8):** `Add to List` (`0x100000C3`, staging) does NOT enable/disable with selection — it is PERMANENTLY disabled (`VendorUiController.SetActionButtonsEnabled`), because it has no wired `OnClick` at all; an enabled-but-dead button is a worse affordance than a disabled one, so it stays disabled until the "Buying" tab's staging list is actually implemented. The Buy opcode exists on the wire now; Sell (`0x0060`) does not. `VendorProfile::InqAcceptability` (sell-eligibility filtering) is unread — moot without a sell UI. Two divergences remain of the four the F1-F8 fix pass originally recorded — the other two (private per-panel selection, unwired shop-item examine) CLOSED at Slice 6.1/6.2, see the NARROWED note above: (1) the closed-dropdown button face reuses the row template's own two sprites (`0x060012B3` normal/`0x060012B4` open) through `UiMenu`'s existing single-texture 3-slice `DrawButtonFace` instead of retail's authored two-piece label+arrow-cap assembly (label `0x1000034D` + a separate 17x19 arrow cap `0x1000034E` with its own `0x060012B1`/`0x060012B2` states) — a cosmetic gap only; the popup panel and its rows render with the exact authored geometry and sprites; (2) the alt-currency "you have" holding reads `VendorShopProfile.AlternateCurrencyAmount` directly instead of tracking retail's `gmVendorUI.m_last_sale` purchase debit — moot until a sell path exists to actually debit it, since `m_last_sale` only changes on a completed SALE (retail's own `m_last_sale == 0` case, `pc:204091`/`OpenVendor`'s `this->m_last_sale = 0` reset at `pc:203790`/`203800`); Slice 6.3's buy path does not touch `m_last_sale` either (retail's own buy flow never writes it), so this residual is unaffected by the buy arc landing. The "Buying"/"Selling" staging tabs (`VendorBuyUI`/`VendorSellUI`) and the full Sell wire remain unwired — unchanged Slice 6b/6c territory per contract decision 6, not a residual of THIS row. | `src/AcDream.App/UI/Layout/VendorUiController.cs`; `src/AcDream.Core/Items/VendorState.cs`; `src/AcDream.Core.Net/GameEventWiring.cs`; `src/AcDream.App/UI/RetailUiRuntime.cs` | Slice 6 (`docs/plans/2026-07-23-world-interaction-completion.md`) owns the authoritative buy/sell transaction command, quantity/stack-split selection, drag-to-sell consumption, and `InqAcceptability`-gated sell UI — Slice 5.4's charter was browse only. Buy (6.3), the global `ACCWeenieObject::selectedID` coupling (6.2), and shop-item `ClientObjectTable` registration (6.1) are now DONE, landing exactly the seam this row's original filing fenced off; drag-to-sell consumption and `InqAcceptability`-gated sell UI remain Slice 6b/6c territory. | A player can browse, select, examine, and BUY (Slice 6.3) — the only remaining unbuilt transaction is Sell. Clicking "Buying"/"Selling" still shows an empty page with no error or explanation, matching "present but does nothing" rather than a disabled/hidden affordance. The dropdown's closed-state button face is missing its separate arrow-cap glyph — a minor visual gap, not a functional one; the open popup itself is pixel-faithful to the authored template. | `gmVendorUI::OpenVendor` pc:203650 (`m_itemsUI`/`m_buyUI`/`m_sellUI` construction, `PostInit` pc:199906, `m_last_sale` reset pc:203790/203800); `VendorBuyUI::VendorBuyUI` pc:199717; `VendorSellUI::VendorSellUI` pc:199753; `VendorProfile::InqAcceptability` pc:484768-484797; `UIElement_Menu::MakePopup` pc:120705-120764, `::Initialize` pc:120789-120828; `VendorItemsUI::UpdateItemsUI` pc:202539-202820; `VendorItemsUI::UpdateItemsList` pc:201029-201190; `ItemHolder::GetObjectSplitSize` pc:401465-401477; `gmToolbarUI::HandleSelectionChanged` pc:198740-198790 (mask `0xDC41CB0` at pc:198784); `ACCWeenieObject::GetObjectName` pc:409056-409132; `docs/research/2026-08-08-slice5-vendor-browse-research.md` §B.4, §D | | AP-162 | **NARROWED 2026-08-09 (Opus review of `92ea3977`, finding F1) — the "Buy All" half of this row CLOSES.** `VendorUiController.BuyAllButtonPressed` now ports all four of retail's client-side pre-send guards (pyreal affordability `pc:204017`, alt-currency affordability `pc:204032`, container-slot capacity `pc:204053`, item-slot capacity `pc:204067`) — see `ComputeBuyTransactionValue`/`ComputeBuySlotsNeeded`/`CountPlayerContents`, each guard returning with staging fully intact and retail's own exact notice string (`"You don't have enough money"` at `0x007b57b4`, `"You must empty some slots in your backpack first"` at `0x007b5750`, both byte-recovered). The container-vs-item slot CLASSIFICATION this port uses (`ItemType.Container` instead of retail's bitfield/capacity test) is its own new, narrower approximation — filed separately as AP-168 rather than folded in here. Only `TryBuy`'s single-item Buy path (Items tab's own Buy button, and the Buying tab's "Buy Item") remains WITHOUT a client-side pre-check — the risk/oracle columns below now describe that one remaining case, not both. **EXTENDED 2026-08-09 (Slice 6b) — the same omission now also covers "Buy All".** `ItemInteractionController.TryBuyAll` (the batched-send path `VendorUiController.BuyAllButtonPressed` calls) sends unconditionally too, without porting retail's `pc:204017/204032/204053/204067` affordability/pack-capacity pre-checks for the MULTI-item case either — the same latency-not-correctness tradeoff this row already documents for the single-item path, extended rather than duplicated into a second row; retiring this row should port both the single- and batched-send pre-checks together. **Filed 2026-08-09, Slice 6.3 (buy wire + button).** Retail's `BuySingleItem` (`pc:201661`) performs TWO client-side pre-checks before ever sending `CM_Vendor::Event_Buy`: (a) an affordability check against `this->m_totalValue` (pyreal) or `shopVendorProfile->trade_num - m_last_sale` (alt-currency), showing a LOCAL string via `ECM_UI::SendNotice_DisplayStringInfo` and returning without sending anything on failure (`pc:201686-201717`); (b) a pack/container-capacity pre-check (`pc:201730-201746`) mirroring the server's own check. acdream's `ItemInteractionController.TryBuy` sends unconditionally once the shared use/inventory gate is free — no client-side affordability or capacity check runs before dispatch. Every refused purchase pays a full round-trip (send → server rejects → `UseDone`/`GameEventInventoryServerSaveFailed`) instead of failing instantly and silently client-side. **Swept 2026-08-09 (F4 review fix):** `TryBuy` now also checks whether `sendBuy` actually reached a live, in-world session before marking the reservation dispatched — an orthogonal reservation-leak bug fix (no session ever produced a stray permanent busy-lock), not an affordability/capacity check; this row's scope and residual are unchanged. | `src/AcDream.App/UI/ItemInteractionController.cs` (`TryBuy`) | The research doc's own open question 1 (`docs/research/2026-08-08-slice6-vendor-transactions-research.md`) recommends deferring this: the server is authoritative either way (ACE re-validates both affordability and capacity server-side — `Vendor.BuyItems_ValidateTransaction`, `Vendor.cs:431-571`), so omitting the client pre-check is a LATENCY/UX gap, not a correctness one — a refused purchase still fails cleanly, just one round-trip later than retail. | A player attempting to buy something they cannot afford or have no room for sees the failure arrive after a network round-trip instead of instantly; against a well-behaved ACE server no purchase can succeed that retail's pre-check would have blocked, so no transaction outcome differs — only its latency. Retiring this row means porting `BuySingleItem`'s two pre-check branches (`pc:201686-201746`) into `TryBuy` before dispatch. | `gmVendorUI::BuySingleItem` pc:201661/0x004C2820 (affordability pc:201686-201717, capacity pc:201730-201746); `Vendor.BuyItems_ValidateTransaction` (`references/ACE/Source/ACE.Server/WorldObjects/Vendor.cs:431-571`); `docs/research/2026-08-08-slice6-vendor-transactions-research.md` §D point 4, Open question 1 | | AP-163 | **REVIEW CORRECTION 2026-08-09 (Opus review of `97cf8738`, finding F1):** this row's ownership discipline is now COMPLETE on both halves, not just the add-time collision guard described below. The retire pass (`OnVendorTransition`'s loop over guids missing from the new `ApproachVendor` snapshot) previously deleted ANY such guid unconditionally — a plain bug, not a documented divergence, since buying a UNIQUE vendor item re-containers that SAME guid into the buyer's own pack (`Player_Commerce.cs:86-108`) BEFORE the post-buy refresh that drops it from the shop's own list arrives; the old retire pass would have stripped the just-purchased item straight back out of the buyer's inventory. **The exact rule now enforced:** each owned guid remembers the vendor id it was registered under (`Dictionary`, guid -> vendorId), and the retire pass calls `ClientObjectTable.Remove` ONLY when the live object's CURRENT `ContainerId` still equals that recorded vendor id; when it differs (or the object is already gone), the tracking entry is dropped silently and the object itself is left completely untouched — the SAME skip-not-clobber discipline the add-time collision guard below already used, now applied symmetrically on the way out. This is a bug fix, not a new divergence, and does not change this row's still-open scope: retail's actual `ClientObjMaintSystem`/`CObjectMaint` collision behavior on a guid collision remains untraced. **Filed 2026-08-09, Slice 6.1 (shop-item materialization).** `VendorShopItemMaterializer` registers each `ApproachVendor` shop item into `ClientObjectTable` keyed by its own server guid. ACE's `UniqueItemsForSale` (`Vendor.cs:34,638`) can list the EXACT guid a player last held (an item sold to this vendor keeps its original guid), so a guid collision against an existing, differently-owned `ClientObjectTable` entry is a real, if rare, possibility. No retail behavior for this exact case was traced (retail's `ClientObjMaintSystem`/`CObjectMaint` guid-keyed registration internals were not decompiled for this pass). acdream's policy is a conscious, conservative default: a guid this materializer did NOT itself add to the table on a previous cycle is treated as owned by something else and is left completely untouched — never overwritten, never later removed by this class. | `src/AcDream.Runtime/Gameplay/VendorShopItemMaterializer.cs` (`OnVendorTransition`'s collision guard) | Skip-not-clobber is the safe default absent a traced retail mechanism: silently reparenting a live entity's or another container's item into the vendor's `ContainerId` would corrupt real ownership state (equipment tracking, burden, radar) for a guid this code does not own, which is strictly worse than a single shop row's status-bar/appraisal projection staying blank. The vendor list itself is unaffected either way — `VendorUiController` reads display fields straight off `VendorShopItem`, never through `ClientObjectTable`. | If retail's actual behavior differs (e.g. it always overwrites, or a real `UniqueItemsForSale` collision is more common than assumed), the one colliding shop row's status-bar/appraisal projection stays stale/blank instead of showing the vendor listing — a narrow, single-row display gap, never a corrupted non-vendor object. Retiring this row requires tracing retail's `ClientObjMaintSystem` registration behavior on a guid collision, which was out of scope for this pass. | No direct retail citation traced this pass — `Vendor.cs:34,638` (`UniqueItemsForSale`, ACE) establishes the collision is POSSIBLE, not what retail does about it; `docs/research/2026-08-08-slice6-vendor-transactions-research.md` (task brief: "study how ACE guids vendor stock and state your collision policy with evidence") | @@ -361,7 +405,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-168 | **NARROWED 2026-08-08 (grand-gate finding G1) — the player's-OWN-pack half (`CountPlayerContents`) is FIXED; only the shop-stock half (`ComputeBuySlotsNeeded`) remains approximated.** Live testing surfaced the risk this row already predicted: "Buy All" false-blocked a container purchase while the player visibly had free container slots. Root cause was NOT the theoretical corner case originally described here — it was that the old dual-heuristic (`ItemType.Container` bit OR nonzero `ItemsCapacity`/`ContainersCapacity`) could over-classify an ordinary non-container object as an occupied container slot, undercounting free space. `CountPlayerContents` now reads `ClientObject.ContainerTypeHint` first — retail's actual wire `ContainerProperties` (`Item_ServerSaysContainId` 0x0022's `ContainerType`; also carried by `ContentProfile`/`PlayerDescription`'s per-entry container-kind byte), already threaded onto every owned object by `InitializeInventoryManifest`/`ApplyConfirmedServerMove`/`ReplaceContents` and already used for this identical question by `ClientObjectTable.IsContainerListMember` — falling back to `ItemType.Container` alone (the capacity-field legs were dropped) only for the rare object that never received a hint. This matches retail's real `_itemsList`/`_containersList` bucketing (`ACCWeenieObject::GetNumContainedItems`/`GetNumContainedContainers` @0x0058beb0/0x0058bec0 just report already-bucketed `IDList` lengths; the bucketing happens once, at insert time, in `ServerSaysContainID` @0x0058be40, from that same wire field) rather than reconstructing it from the item's own type/capacity fields. Original text: **Filed 2026-08-09, Opus review of `92ea3977`, finding F1 (Buy All's client pre-send capacity guard).** Retail's `gmVendorUI::InqListSlotCount` (`pc:200038-200065`, `0x004c0c10`) classifies each staged item as needing a CONTAINER slot or an ITEM slot by testing a bitfield bit (a decompiler string-misattribution artifact not yet decoded) ORed with the item's own nonzero `_itemsCapacity`/`_containersCapacity`. `VendorUiController.ComputeBuySlotsNeeded`/`CountPlayerContents` approximate this with `(item.ItemType & ItemType.Container) != 0` instead — correct for the ordinary case (an authored backpack/pouch DOES carry the `Container` type bit) but not byte-identical for the theoretical case of a non-`Container`-typed item that still authors nonzero pack/side capacities (or vice versa, a `Container`-typed item with zero capacity of its own, e.g. a locked/sealed decorative chest never meant to be carried). | `src/AcDream.App/UI/Layout/VendorUiController.cs` (`ComputeBuySlotsNeeded`, `CountPlayerContents`) | `VendorShopItem`'s wire shape (Slice 5's deliberately narrow browse-scope subset) genuinely does not carry `PublicWeenieBitfield`/`ItemsCapacity`/`ContainersCapacity`/`ContainerProperties` the way `ClientObject` does for an ordinary `CreateObject`/membership-sourced item, so `ComputeBuySlotsNeeded` (the shop-stock side, staged-but-not-yet-owned items) cannot read a wire-truth hint the way the fixed `CountPlayerContents` (the already-owned side) now does; extending the DTO was out of scope for this fix. The server remains authoritative and re-validates real pack-space regardless (`Vendor.BuyItems_ValidateTransaction`, `Vendor.cs:431-571`) — the residual failure mode stays UX/latency, not correctness. | A vendor selling a `Container`-typed item with zero authored capacity (rare/decorative) would still be misclassified as needing a container slot instead of an item slot, or vice versa for a non-`Container`-typed item that DOES author capacity (also rare) — the pre-check could still reject a purchase retail's own guard would have allowed, or allow one retail would have blocked, purely on the CLIENT side for the SHOP-STOCK item being bought; the player's-OWN-pack accounting that drives the free-slot count is no longer the source of that risk. | `gmVendorUI::InqListSlotCount` `pc:200038-200065`/`0x004c0c10`; `ACCWeenieObject::GetNumContainedItems`/`GetNumContainedContainers` `0x0058beb0`/`0x0058bec0`; `ACCWeenieObject::ServerSaysContainID` `0x0058be40`; `docs/research/2026-08-08-slice6b-vendor-completion-research.md` | | AP-169 | **Filed 2026-08-08, grand-gate finding G2 (vendor toolbar split-slider absent live). CORRECTED 2026-08-08 (re-gate finding R1). CORRECTED AGAIN 2026-08-08 (live vendor-diag evidence) — both earlier stories mis-identified the operand; this row now records the third and evidence-pinned shape.** The G2 fix fell back to the packed ItemProfile supply-count dword (unusable: a standard listing has UNLIMITED stock, `-1`). The R1 fix preferred the wire `PublicWeenieDesc::_stackSize` (`VendorShopItem.DescStackSize`) on the claim that ACE never populates it for a browse row — the live vendor-diag run REFUTED that claim: ACE serializes `descStackSize=1` for EVERY browse row (`[vendor-diag] ApproachVendor wire-item[...] descStackSize=1 stackSizeMax=100`), so desc-first resolved every vendor stack to 1 and the split bar never appeared (`ApplySelection ... failingPredicate=stackSize<=1u stackSize=1`). The named decomp settles what retail actually reads at its VENDOR-owned quantity sites: `pwd._maxStackSize` DIRECTLY — `VendorItemsUI::UpdateItemsList` (`0x004c1ea0`, `pc:201085-201133`) displays each browse row's quantity as `min(remaining, _maxStackSize)` (plain `_maxStackSize` for an unlimited listing, via `VendorSubUI::SetObjectStackSize`); `gmVendorUI::InqListSlotCount` (`0x004c0c10`, `pc:200052`) classifies rows on `pwd._maxStackSize <= 1`; the Buy cases (`gmVendorUI::HandleButtonClicks` `0x100000c9` @`pc:203996` / `0x100000cb` @`pc:204086`) gate the stackable-buy path on `pwd._maxStackSize > 1`. `VendorSplitPolicy.ResolveAuthoredStackSize(descStackSize, maxStackSize)` is therefore **max-first** (desc fallback, then 1), consumed only by the vendor-owned paths (`VendorShopItemMaterializer.ToWeenieData`, `VendorUiController.ResolveBuyQuantity`); player-inventory stacks never route through it. Matches the live retail screenshot ("1000 Prismatic Tapers", ceiling 1000 = the taper's authored max stack size). The toolbar-side `gmToolbarUI::HandleSelectionChanged` does read `pwd._stackSize` (`pc:198688`/`198744`/`198774`/`198791`) — on a REAL retail server the two agree for a browse row (the vendor UI stamps the displayed stack from `_maxStackSize`); against ACE (desc always 1) the `_maxStackSize` operand is the one that carries retail's meaning. | `src/AcDream.Runtime/Gameplay/VendorShopItemMaterializer.cs` (`ToWeenieData`); `src/AcDream.Core/Items/VendorSplitPolicy.cs` (`ResolveAuthoredStackSize`); `src/AcDream.App/UI/Layout/VendorUiController.cs` (`ResolveBuyQuantity`) | This is an ACE-server-constraint adaptation on the toolbar leg only: retail's vendor UI reads `_maxStackSize` literally (ported as-is); the toolbar seed's `_stackSize` read is satisfied through the materialized `ClientObject.StackSize`, which this resolution stamps from `_maxStackSize` exactly as retail's own `UpdateItemsList` stamps the displayed stack — not an arbitrary substitute. | A vendor stocking a bounded but non-unit quantity shows a ceiling of `min` semantics only on a real retail server; against ACE the client-side slider ceiling is the authored max stack size, not the bounded stock count — the server remains authoritative and rejects an over-large Buy regardless (a latency/UX gap, not a correctness one — see AP-162). If ACE ever starts serializing a REAL per-listing `_stackSize` (not the constant 1), the max-first preference would hide it; the desc fallback fires only when no authored ceiling exists. | `VendorItemsUI::UpdateItemsList` `0x004c1ea0` `pc:201029-201133`; `gmVendorUI::InqListSlotCount` `0x004c0c10` `pc:200052`; `gmVendorUI::HandleButtonClicks` `pc:203996`/`204086`; `gmToolbarUI::HandleSelectionChanged` `pc:198688-198791`; live vendor-diag wire capture + live retail screenshot (2026-08-08) | | AP-170 | **Filed 2026-08-08, grand-gate finding G3 (out-of-range vendor Use lost silently).** Retail's `ItemHolder::UseObject @ 0x00588A80` has no client-side range check and sends Use immediately regardless of distance — this port's ORIGINAL `RequestUse` faithfully mirrored that shape. Live testing against the user's local ACE server showed it does not hold: walking to a vendor and using it from out of range plays the vendor's cosmetic greeting (a distance-only reaction, independent of Use) but never opens the shop panel — `ApproachVendor` never arrives. ACE's `Player.HandleActionUseItem` (`references/ACE/Source/ACE.Server/WorldObjects/Player_Use.cs:176-215`) explains why: an out-of-range target routes through `CreateMoveToChain(item, (success) => TryUseItem(item, success))` (`Player_Move.cs:37-96`), which polls every 0.1s for the player to reach `WithinUseRadius` and only then calls `ActOnUse` — it does not teleport or server-move the player; it waits for the CLIENT's own walk to land, and a Use that arrives before that poll ever starts observing an in-range player is simply never followed by the vendor's `ApproachVendor` send (`Vendor.ActOnUse`'s own doc comment: "the player will have been commanded to move using `DoMoveTo` before `ActOnUse` is called... it should be assumed that the player is within range" — a precondition our immediate send violated). `SelectionInteractionController.RequestUse` now arms the out-of-range case on the SAME arrival-gated shape `SendPickup`'s close-range (turn-only) branch already used (`RuntimeInteractionTransactionState.TryArmPostArrivalUse`/`TryResolveUseApproachCompletion`, mirroring `TryArmPostArrivalPickup`/`TryResolveApproachCompletion` field-for-field) — the wire Use dispatches only once the local approach naturally completes. An already-in-range Use (a turn at most, or no approach concept applies) is unaffected and still sends immediately, matching ACE's own "already within use distance" synchronous callback. | `src/AcDream.App/Interaction/SelectionInteractionController.cs` (`RequestUse`, `HandleApproachCompletion`, `HandleUseApproachCompletion`, `CancelPendingApproach`, `OnEntityHidden`, `OnEntityRemoved`); `src/AcDream.Runtime/Gameplay/RuntimeInteractionTransactionState.cs` (`RuntimePendingUse`, `TryArmPostArrivalUse`, `TryResolveUseApproachCompletion`, `TryCancelPendingUse`) | This is an ACE-server-constraint adaptation, not a retail redesign: retail's REAL server walks the player itself before the target's `ActOnUse` ever sees the request, so the client's immediate send never races anything there. ACE does not do this for a player-initiated Use — it only polls and waits — so arming on arrival is required for correctness against the only server this port can test against, not a stylistic preference. | An interaction path that still calls `TryDispatchUse` directly without going through `RequestUse`'s approach gate (none identified at this fix) would keep the original race. The armed reservation is a live busy-count reference until arrival/cancellation resolves it; `ResetCore` releases it unconditionally on any reset/dispose so a teardown that runs without a preceding `CancelPendingApproach()` (e.g. a headless/no-window host with no `SelectionInteractionController`) cannot leak it. | `ItemHolder::UseObject` `0x00588A80`; `Player.HandleActionUseItem` `Player_Use.cs:176-215`; `Player.CreateMoveToChain`/`MoveToChain` `Player_Move.cs:37-153`; `Vendor.ActOnUse` `Vendor.cs:223-266` | -| AP-171 | **Filed 2026-08-08 (user-approved modernization).** Double-clicking a vendor shop item buys it (select + the Buy button's exact quantity/price path). Retail has NO double-click-to-buy — the full named function table was swept at the Slice 6 research and the user chose the addition explicitly after being told. | `src/AcDream.App/UI/Layout/VendorUiController.cs` (shop cell DoubleClicked) | Deliberate QoL divergence, user-directed; trivially removable. | None — additive input affordance; the single-click and Buy-button paths are unchanged. | User direction 2026-08-08 ("When I double click, I should buy it") | +| ~~AP-171~~ | **RETIRED 2026-08-26 — the original filing was false.** Direct named-retail evidence in `gmVendorUI::HandleMousePresses @ 0x004C40D0` calls `BuySingleItem` from the Items-list double-click branch. Browse-row double-click purchase is retail behavior, not an acdream modernization. | `src/AcDream.App/UI/Layout/VendorUiController.cs` (shop cell DoubleClicked) | — | — | `gmVendorUI::HandleMousePresses @ 0x004C40D0`; `docs/research/2026-08-26-retail-inventory-interaction-audit.md` | | AP-173 | **Filed 2026-08-08 (Campaign A slice A2).** Retail pans with `IDirectSoundBuffer::SetPan`, which attenuates ONE output channel by \|pan\| decibels — so full deflection is a 15 dB inter-channel level difference, never full separation. OpenAL exposes no per-channel gain for a mono source, so acdream expresses the same pan as a source-relative AZIMUTH (`MaxPanAzimuthDegrees = 30`, scaled by pan/15) and lets OpenAL's constant-power panner turn it into channel gains. Everything about the pan's SHAPE is retail's and byte-verified: the value is `(int)(-15·sin(Δbearing))` in whole decibels from retail's compass convention, it is forced to dead centre when `(int)distance < 5`, it distinguishes neither front from back nor elevation, and it is frozen for the voice's lifetime. Only the mapping from a 15 dB channel difference to an azimuth under OpenAL's own pan law is approximate. | `src/AcDream.App/Audio/OpenAlAudioEngine.cs` (`MaxPanAzimuthDegrees`, `ApplyPan`) | The exact alternative is to pre-mix a stereo buffer per (wave, pan) pair, which multiplies AL buffer memory by up to the 31 distinct pan values and would fight the 48 MiB LRU; OpenAL's stereo pan law is also driver-dependent, so a measured mapping would not be portable. The audible quantity (inter-channel difference) is preserved in shape and bounded in magnitude. | Stereo image at full deflection may be somewhat wider or narrower than retail's 15 dB; direction and the centre deadzone are correct. Sounds are never hard-panned to silence in one ear the way an uncompressed azimuth would do. | `SoundManager::PlaySoundInternal @ 0x00550170`; `SoundBuf::Play` SetPan call; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` §1 (pan decode) | | AP-174 | **Volume-knob taxonomy differs from retail's, filed 2026-08-08 (Campaign A slice A2).** Retail has exactly three float knobs — `effect_sound_volume`, `ambient_sound_volume`, `interface_sound_volume` — **no master and no music knob**, and the interface one is registered and then never read (interface sounds are scaled by the EFFECT knob). acdream keeps an extra `MasterVolume` on top of `SfxVolume`, which A2 folds into the mixer's single master multiply (`EffectMaster = MasterVolume * SfxVolume`) rather than publishing as an AL listener gain — so the −50 dB no-allocate floor, the audible radius, and the whole-decibel quantisation all move with the slider the way they would if retail had one. `MusicVolume` is dead (retail has no music system at all; slice A6 deletes it) and `AmbientVolume` is unread until slice A5 wires the ambient path. No Interface knob exists yet; slice A4 adds the UI bus and will scale it by the effect knob, matching retail's dead-knob behaviour rather than implementing a working one. | `src/AcDream.App/Audio/OpenAlAudioEngine.cs` (`EffectMaster`); `src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs` | A master slider is a modern nicety users expect and costs nothing once it is inside the one retail multiply; implementing retail's dead interface knob as a working control would be a divergence in the other direction, so it stays dead. | At Master 1.0 (the default) behaviour is bit-identical to a retail single-knob mix. Below 1.0 the mix is quieter than retail's would be at the same effect setting, because retail has no such knob to turn down. | `SoundManager::InitPrefs @ 0x005503F0`; `SoundManager::GetAttenuation @ 0x00550020`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` §3 D11/D13 | | ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` | @@ -395,7 +439,6 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-139 | **Filed 2026-08-04 (Bug B).** The remote tick clears its InterpolationManager queue on the LANDING edge — retail’s own `set_on_walkable(1)` transition, the same edge HitGround fires from. Retail has no such clear on a ground or contact edge: its only queue teardown outside a completed walk is `PositionManager::StopInterpolating` from `CPhysicsObj::teleport_hook` @0x00514EFD and the `InterpolationManager::UseTime` @0x00555f20 stall/autonomy blips. The clear is carried over unchanged in intent from the deleted hand-rolled landing block (#184, 2026-07-07), which hung it on a hand-rolled `Airborne && IsOnGround && Velocity.Z <= 0` test that also fired on a steep (non-walkable) contact; Bug B re-derived the edge without changing the behaviour it was written for | `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs` (the SetPositionInternal commit block); the packet-side twin lives in `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition`, the player-remote landing snap) | A contact-free arc never enqueues — route 4a's airborne no-op writes nothing at all — so anything still queued when the body lands is a pre-arc waypoint, and the first catch-up after touchdown would otherwise walk the body backward toward it | A remote that regains contact while a legitimately fresh waypoint is queued loses one correction and re-acquires it on the next accepted Position (~5-10 Hz). A body that repeatedly loses and regains contact (a bounce chain down a rough face) clears the queue once per bounce. Retire when the arc itself feeds the queue, at which point the pre-arc waypoints are no longer stale | `CPhysicsObj::teleport_hook @ 0x00514ED0` (`StopInterpolating` @0x00514EFD); `InterpolationManager::UseTime @ 0x00555f20`; `CPhysicsObj::SetPositionInternal @ 0x00515330` | | AP-181 | **Filed 2026-08-09 (Campaign CH slice CH3, side-channel gate); corrected 2026-08-09 at the CH3 Opus review (S6) — the original text named only the spam throttle and wrongly credited `RouteLegacyChannel` with porting gates it has no code for.** Retail's `SendTurbineChat @0x0057db10` runs TWO local pre-send refusals acdream has no port for, in this order: `IsMessageSafe(text)` first (a silent drop — no wire send, no local text at all), then, only if that passes, the per-account spam throttle `IsMessageSpam()` (→ "You must wait %ds before communicating again!"). acdream's `TurbineChatMembershipGate`/`RouteTurbineChat` port the Turbine-unavailable and Hear-option gates that run BEFORE both checks in retail's own function (§4.2) and stop there — neither `IsMessageSafe` nor `IsMessageSpam` exists anywhere in acdream. `RouteLegacyChannel` is the unrelated legacy 0x0147 `ChatChannel` pipeline and has no equivalent of either check in retail OR acdream — it was never the site these two gates belonged to. | `src/AcDream.Runtime/Gameplay/TurbineChatMembershipGate.cs`; `src/AcDream.App/Net/LiveSessionCommandRouter.cs` (`RouteTurbineChat`) | The user's target server (local ACE) leaves `chat_requires_account_15days`/`chat_requires_player_level` etc. at their disabled defaults (research doc §3.6) and has no observed rate-limit or unsafe-content complaint; porting a client-side throttle/safety check with no server-side counterpart to validate against risks inventing a threshold retail didn't use. | A future connected gate against a server that DOES rate-limit chat, or a deliberately unsafe test string, would see every send attempted rather than refused after the first — cosmetic only, since ACE's own server-side handling (if any) still governs what actually reaches other players. | `ClientCommunicationSystem::SendTurbineChat @0x0057db10` (`IsMessageSafe`/`IsMessageSpam` branches); research doc `docs/research/2026-08-09-chat-side-channels-vs-ace.md` §4.2 | | AP-182 | **Filed 2026-08-09 (Campaign CH slice CH4); corrected 2026-08-09 at the CH4 REJECT-review (nit 11).** `@title ` is wired to a pure no-op — `LiveSessionRuntimeFactory`'s `SetChatTitle` binding is `_ => { }`; the requested title is neither stored nor consumed anywhere (the original filing's "stores the value locally" claim was false). This matches retail's own silent success (no confirmation text was recovered at the `DoTitle` success site, so a no-visible-effect accept is exactly as faithful as a stored-but-unread value would be). Also omitted: `DoTitle`'s three local failure messages — no title given, "You must provide a new title for the window."; length over 99 characters, "Window title length cannot exceed 100 characters."; and wrong source window (`m_idCurrentCommandSource` 1 or 8), "This command must be issued from a popup chat window." — acdream's catalog validator (`ClientCommandId.SetChatTitle`, `AnyArguments`) accepts any argument shape and never raises any of the three. `src/AcDream.App/Net/LiveSessionRuntimeFactory.cs` (`SetChatTitle`) | Retail's chat window presumably re-renders its title bar text; acdream's chat window has no title bar at all under the current retained-UI import, so there is nothing to visually diverge from yet | Once a titled chat-window chrome is built, `@title` needs to be re-wired to it — today it is a pure no-op, and the three failure messages above are silently absent | `ClientCommunicationSystem::DoTitle @ 0x0057A640` | -| AP-185 | **Filed 2026-08-10 (Campaign CH slice CH6a — retail chat-window layout + 8-grip resize).** The main chat window's 8 cosmetic `_Locked` border-art twins (`0x10000693`-`0x1000069A`) are retail's `PlayerModule::LockUI`-driven alternate skin — `gmFloatyMainChatUI::UpdateLockedStatus @0x004D23D0` swaps them in for the 8 live Resizebar/Dragbar grips (`0x1000069B`-`0x100006A2`) when the UI is locked, and swaps them back out when unlocked. `ChatWindowController.Bind` always hides the twins and always shows the live set — i.e. it renders only retail's UNLOCKED skin, regardless of `UiRoot.UiLocked`. `src/AcDream.App/UI/Layout/ChatWindowController.cs` (`LockedTwinIds`) | `UiRoot.UiLocked` already gates the underlying move/resize INTERACTION generically and correctly in both states (locked ⇒ no move, no resize, regardless of which border art is drawn); the two art sets occupy identical rects, so always showing the interactive-grip skin is a cosmetic simplification, not a functional one, and the default matches `UiLocked`'s own `false` default | A user who locks the UI (`PlayerModule::LockUI`) sees the interactive-grip chat-window border art unchanged instead of retail's inert locked variant — cosmetic only; the window still correctly refuses to move or resize while locked | `gmFloatyMainChatUI::UpdateLockedStatus @0x004D23D0`; `PlayerModule::LockUI`; `docs/research/2026-08-09-chat-retail-window-shell.md` §1.6 | | AP-187 | **Filed 2026-08-10 (Campaign CH slice CH6b — floating chat windows). BROADENED 2026-08-11 at Campaign OP slice OP5 (Chat tab): the divergence now covers the MAIN chat window's filter too (`ChatSettings.ChatWindowMainFilter`), and the write path is no longer mount-time-seed-only — the retail Options panel's Chat tab (`ChatOptionsPageController`, five `UiCheckboxBitfield64` blocks) is now a LIVE editing surface for all five windows' filters, writing `ChatWindowState.SetFilter` directly and persisting on every change via `RetailUiRuntime.SaveChatWindowFilters`, closing that method's own former "worth tightening to auto-save-on-change once a live settings surface exists" note.** The five chat windows' (main + four floating) text-type filters (`AcDream.Core.Chat.ChatWindowState`, retail's `0x1000007F` per-window option) persist only in local `settings.json` (`ChatSettings.ChatWindowMainFilter`/`ChatWindow1Filter`..`ChatWindow4Filter`, `SettingsStore.LoadChat`/`SaveChat`). Retail's authoritative store for this same data is the per-window option array (`0x1000008C`) packed inside the character-scoped `GameplayOptions` blob, which ACE stores and echoes as opaque bytes without parsing (window-shell research doc §4.1/§4.4); acdream has no reader or writer for that blob (CH3 already deleted one malformed attempt at the outbound `SetCharacterOptions 0x01A1` builder — `SocialActions.cs`). Geometry and open/visible state for these same windows do NOT need a row of their own: they persist through the pre-existing generic `RetailWindowLayoutPersistence` path (X/Y/W/H/visible/collapsed/maximized per window name), which is retail's OWN local-file mechanism too (`gmGamePlayUI::SaveScreenLayout`/`LoadScreenLayout`, window-shell research doc §4.3) — only the filter mask lacks any such local-file precedent in retail and is acdream's own addition to make the feature usable before a `0x1000008C` wire slice lands. `src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs`; `src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs` (`LoadChat`/`SaveChat`/`BuildChatObject`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`MountChat`, `MountFloatingChatWindows`, `SaveChatWindowFilters`); `src/AcDream.App/UI/Layout/ChatOptionsPageController.cs` | CH6a/CH6b's own port-shape recommendation (window-shell research doc §6.1) explicitly chose local persistence first and deferred the `0x1000008B`/`0x1000008C` wire to a dedicated CH6f slice, citing CH3's deleted malformed builder as the reason not to rush it | A character's floating-window filter customization does not travel to a different acdream install, and would not round-trip through a retail client sharing the same character (retail would see acdream's local-only values as unset, falling back to its own `PostInit` defaults) — cosmetic/preference-only, no gameplay effect | `PlayerModule::GetChatOptionStructure @0x005D5300`; `PlayerModule::InqChatWindowOption/SetChatWindowOption @0x005D5540/:70`; `docs/research/2026-08-09-chat-retail-window-shell.md` §4.1/§4.4/§6.1; `docs/plans/2026-08-09-chat-parity-campaign.md` (CH6f row) | | AP-188 | **Filed 2026-08-10 (Campaign CH slice CH6b — floating chat windows).** A floating chat window's chat entry always sends on the `Say` channel (`FloatingChatWindowController.Bind`'s `OnSubmit` hardcodes `ChatChannelKind.Say`). The floaty LayoutDesc (`0x2100005B`) authors no talk-focus menu (window-shell research doc §2.2 — only the main window's layout has one, element `0x10000014`), so there is no visible channel picker on a floaty window either way, matching retail's authored UI exactly. What is UNVERIFIED is whether retail's actual SEND path for a floaty window's typed message reads a per-window channel or the single globally-current talk-focus channel/target the main window's menu (or the last-selected/last-speakable-target state `gmMainChatUI::UseTime @0x004CDB20` tracks) last set — if the latter, a real retail floaty window would send on whatever channel the player most recently picked from the MAIN window, not always `Say`. Confirming this requires tracing `gmCCommunicationSystem`'s send-command path from a floaty `ChatInterface` instance, not yet done. Filed as ISSUES.md #369. `src/AcDream.App/UI/Layout/FloatingChatWindowController.cs` (`Bind`, the `OnSubmit` wiring) | Building genuine cross-window shared-channel state (reading `ChatWindowController`'s private `_activeChannel` from four independent sibling controllers, or promoting it to a shared owner) is a real design decision outside this slice's explicit scope (task items 1-6 do not ask for cross-window channel sharing); `Say` is retail's own default channel and the safest fixed value absent confirmation | If retail's actual mechanism is "send on the currently-selected global channel," a user who selects e.g. Fellowship from the main window's talk-focus menu and then types into a floaty window would see it sent as Fellowship in retail but as Say in acdream — no data loss (the message still sends), only channel-selection mismatch | `gmMainChatUI::InitTalkFocusMenu @0x004CDC50`; `gmMainChatUI::UseTime @0x004CDB20`; `docs/research/2026-08-09-chat-retail-window-shell.md` §2.2 | | AP-189 | **Filed 2026-08-10 at the CH6a/b REJECT-review rework (SHOULD-FIX 5, `docs/research/2026-08-10-ch6ab-review-findings.md`).** Retail keeps a PER-`ChatInterface` `m_chatLog`, truncated at 10,000 lines (`ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4711` → `TruncateChatLog`) — each of the five windows (main + 4 floaty) owns its OWN 10,000-line backlog, and a closed window keeps accumulating into its own log because `gmFloatyMainChatUI::SetVisible @0x004CE9B0` never unregisters the handler. acdream instead shares ONE canonical `ChatLog` capped at 500 entries (`RuntimeCommunicationState`'s ctor, `maximumChatEntries: 500`) with a 200-line display tail every window filters from (`InteractionRetainedUiComposition.cs:564`'s `displayLimit: 200` feeding `ChatVM.RecentLinesDetailed`; `ChatWindowState.ShouldDisplay` does the per-window filtering). The accumulate-while-closed and independent-per-window-scroll BEHAVIORS both fall out correctly from this shared-log shape, but the EFFECTIVE per-window scrollback DEPTH differs from retail's: a window whose filter accepts only a rare message type (e.g. a Fellowship-only floaty) sees only the fellowship lines that happen to still be inside the shared log's last 200-of-500 lines, not up to 10,000 like retail's own per-window log. `src/AcDream.Core/Chat/ChatLog.cs` (`_maxEntries`); `src/AcDream.App/Composition/InteractionRetainedUiComposition.cs:564` (`displayLimit: 200`); `src/AcDream.App/UI/Layout/ChatWindowController.cs`/`FloatingChatWindowController.cs` (`GetTranscriptLines`) | A single shared canonical log matches acdream's Slice-J "one canonical transcript, many filtered presentations" pattern and keeps memory bounded regardless of how many windows are open; 500 shared entries covers many minutes of typical mixed-channel play, and both retail-observable BEHAVIORS this row could have broken (closed-window accumulation, independent per-window scroll position) are reproduced correctly — only the numeric DEPTH ceiling differs | In a busy mixed-channel session (heavy General/Trade traffic), a rarely-used channel (Fellowship, a Turbine room) can scroll out of the shared 500-entry window long before a floaty window filtered to just that channel would have neared retail's 10,000-line depth — a user who opens that floaty window after a long session sees a much shorter backlog than retail would show for the same play session | `ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640`/`TruncateChatLog @0x004F4711`; `gmFloatyMainChatUI::SetVisible @0x004CE9B0`; `docs/research/2026-08-09-chat-retail-window-shell.md` §1.2 | @@ -413,11 +456,13 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | 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) — 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) +## 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) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| -| TS-85 | **Filed 2026-08-16 at #409 (client-wide retail tooltip system); REWRITTEN at the same-day F3 review round; NARROWED again at the same-day live-failure round.** LIVE-FAILURE-ROUND NARROWING: the `m_TTText` READ side is now ported — `RetailTooltipPresenter.ResolveTooltipText` consults `UiElement.GetTooltipText()` (this port's `m_TTText`) BEFORE the authored `P0x49`, exactly as `StartTooltipAtMouse @0x00460DA3`/`@0x00460DDF` orders them, and the `P0x48`-absent fallback to the element's own layout (`@0x00460E7E`) is ported through `UiElement.SourceLayoutDid`. That lit up every acdream surface whose controller ALREADY writes runtime tooltip text (the four Options tabs, Configure Keyboard, the social pages) — live-verified 2026-08-16 on the Character tab. What remains deferred is the WRITE side at the retail `SetTooltip` call sites acdream has no analog for yet, enumerated below. Two sub-mechanisms of retail's tooltip system are unported. **(1) The `m_TTText`/`SetTooltip` runtime-text family (headed by the `P0xD0` truncated-text auto-tooltip):** the ORIGINAL filing argued this port's gap was "retail's dynamic `InqProperty(0x49)` override" — that framing is false. `UIElement::InqProperty @0x004638D0`, the BASE implementation every element uses unless its own class overrides the virtual, reads exactly the same authored property bags (`m_instanceProperties`, `m_curStateDesc`, `m_desc`) this port's `ElementReader` already walks generically — so an element with no literal `P0x49` gets NOTHING from retail's own default `InqProperty` either. The REAL second text source is the element's cached `m_TTText` field, set ONLY by the explicit, non-dat `UIElement::SetTooltip` call (`UIElement::StartTooltipAtMouse @0x00460D70` prefers `m_TTText` over the `InqProperty` fallback whenever it is non-empty). `SetTooltip` has 15+ known game-code call sites (Options rows `@0x00485E65`, chargen `@0x00481981`, the paperdoll endowment icon `@0x004C63A1`, the spellcast button `@0x004C6FE8`/`@0x004C6AAE`, and more), headed by the highest-volume one: `UIElement_Text::RecalculateTruncation @0x00466F80`, gated on authored `P0xD0` — an overflowing single/wrapped line calls `SetTooltip(this, ownText) @0x00467064` + sets enable bit 5 `@0x00467076`; a line that now fits calls `ClearTooltip @0x00467064`/clears the bit `@0x00466ff9`. `RecalculateTruncation`'s own truncation-POSITION computation (the rest of the function, `@0x004670a1` onward) walks a `GlyphList` per-line-position model (`FindCompleteLineFromY`/`FindPosFromLineAndPixels`/`FindPixelsFromPos`) this port's `UiText` has no equivalent of — `UiText` clips visually via a scissor rect (`DrawClippedText`'s `PushClip`) with no tracked "does this line overflow" state at all, so porting the auto-tooltip trigger requires building that state first. Sized as genuinely disproportionate for a single fix-round commit alongside F1-F2/F4-F11 and deferred here rather than shipped as a partial/unverified stub. A live-DAT sweep found 187 of the 430 elements authoring at least one tooltip-trigger property have NO literal `P0x49` `StringInfo` text; the live-failure round re-measured that set and found every one of the 187 authors BOTH popup-locator ids (`P0x47`+`P0x48`) — i.e. they are runtime-`SetTooltip` targets by construction, waiting only for text. **F12 correction (night-round review, 2026-08-17): this is 17 sites, not 15** — the original tally dropped `gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF` (mentioned two sentences below as its own closed row) and undercounted by one more besides. The 17 `SetTooltip` call sites, enumerated from the decomp at the live-failure round, split into: PORTED (an acdream controller already writes the text, and the presenter now reads it) — the Options rows `@0x00485E65`/`@0x00484803`/`@0x00487053`, chargen skills `@0x00481981`, the radar `@0x004D9605`; PORTED 2026-08-16 (hover-feedback completion round, docs/ISSUES.md #409/#411): inventory/shortcut item hover `UIElement_UIItem::UpdateTooltip @0x004E1CB0` — `UiItemSlot` now hardcodes the catalog's uniform popup locator (`P0x47=0x10000395`/`P0x48=0x21000041`, live-DAT-confirmed uniform across all 47 UIItem-type catalog prototypes, `TooltipLiveDatTests.UiItemCatalog_EveryPrototype_SharesTheSamePopupLocator`) and a `TooltipTextResolve` delegate wired at every physical-item construction site (inventory, external container, paperdoll — closing the separate `gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF` row below too, vendor, secure trade, toolbar), backed by the new `ClientObject.GetTooltipDisplayName()` (NAME_APPROPRIATE + the `"%d %s"` stack-count prefix, matching the decomp exactly); and the SmartBox found-object world-hover tooltip `UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @0x004E5AD0` (`@0x004E5D74`/`@0x004E5DFB`) — `RetailTooltipPresenter.UpdateWorldHoverTooltip` ports its `PlayerModule::ShowTooltips`-gated, `GetAppropriateName`-only (no stack prefix — a real, decomp-confirmed asymmetry vs. the item-cell case) trigger — **TIMING CORRECTED at the 2026-08-17 morning gate round (user finding: retail world tooltips "lag"; ours popped instantly): the original "edge-fired (no dwell)" reading was a misread — the notice's immediate `StartTooltipAtMouse @0x004E5DFB` sits inside `if (s_pInstance->m_dragElement != 0)` (`@0x004E5D8E`; `m_dragElement` is a real, distinct PDB field in `acclient.h`'s `UIElementManager`, separate from the `m_pTooltipElement` family), so the immediate mount is DRAG-ONLY; the ordinary hover path merely STAGES the name (`SetTooltip @0x004E5D74` + `|= 0x20`) and the display rides `CheckTooltip @0x0045B6E0`'s mouse-idle dwell (`m_lastMouseMoveTime` stamped on EVERY move `@0x0045e736` + `m_tooltipDelay` 0.25 s `@0x0045f75d`), with found-object changes under an idle mouse swapping the popup same-frame via `SetTooltip`'s own text-change teardown (`@0x004617FF` → `ResetTooltip @0x0045C360` tail-calling `CheckTooltip`) and the 10 s `m_tooltipDuration` expiry requiring a fresh mouse move before re-arming (`SwitchMouseOver(null) @0x0045b7b2`) — all now ported, including the drag-immediate branch**, reusing the SAME popup locator since an exhaustive DAT sweep found `UIElement_SmartBoxWrapper` (class `0x10000030`) has no authored `ElementDesc` anywhere installed (`TooltipLiveDatTests.SmartBoxWrapper_HasNoAuthoredElementDesc_AnywhereInstalled`) — the popup-skin choice is therefore the best-evidenced inference, not a measured value, and is called out here as such. **BATCH B (2026-08-17) CLOSED the spellcasting and character-panel rows of this list. BATCH C (2026-08-17, Map/House toolbar panel) CLOSES THE LAST REMAINING ITEM: `gmMapUI::AddMapNote @0x004A1C51`'s 53 town-hotspot tooltips are now ported via `MapPageController.BuildTownMarkers` (`src/AcDream.App/UI/Layout/MapPageController.cs`), setting `UiButton.TooltipText` (retail's RUNTIME `m_TTText`/`SetTooltip` mechanism, not the DAT-authored `P0x49` path an earlier same-day cut mistakenly used and which never rendered live during verification); literal town names from `MapLocations.cs` (a verbatim port of `s_rgLocations`), not a DAT string-table lookup, matching `AddMapNote`'s own `StringInfo::SetLiteralValue` call. CORRECTED at the same-day morning gate round (user finding 3 — the retail screenshot's green hover highlight + special-font parchment tooltip): Batch C's "the town-marker template authors no locator of its own" claim was WRONG — the template (`0x100001F0` in `0x21000026`, `MapNoteLiveDatTests`) authors its OWN `P0x47=0x10000398`/`P0x48=0x21000041` (the fourth popup skin, whose incorporated text child `0x10000396` fonts `0x40000015` where the other three skins font `0x40000002`), a zero per-element delay `P0x50=0.0`, `P0x4B` TooltipOn, and `P0x13` RolloverEnabled with PassToChildren `Normal`/`Normal_rollover` states flipping the highlight child `0x100001F1`'s per-state `P0x3B` (the green `0x06004CC9` frame, byte-decoded A=FF R=00 G=FF B=00); the hardcoded shared-skin override was removed (the built marker's authored locator wins) and the rollover highlight + per-state-`P0x3B` + button PassToChildren cascade are now ported (`UiButton.CascadeStateToChildren`, `UiDatElement.TrySetRetailState`'s 0x3B honor). Sub-mechanism (1)'s `SetTooltip`-call-site enumeration is 16 of 17 known sites PORTED — `UIElement_Text::RecalculateTruncation @0x00466F80` (the headline, highest-volume site named at the top of sub-mechanism (1)) remains the ONE open item, exactly as this row's own sub-mechanism (1) text above already scoped it out (its own "Sized as genuinely disproportionate... deferred here" note). The prior "all 15 known sites accounted for" close (F12 correction, night-round review) was wrong twice over: the count is 17, not 15, and RecalculateTruncation was never actually ported — it was always the one deliberately-deferred item, not a closed one.** Batch B audit findings: the endowment icon `@0x004C63A1`, favorite `@0x004C7206`, and submenu `@0x004C67D8` sites turned out to be ALREADY CORRECT — all three are `UiCatalogSlot`-based and the pre-existing `Label`-driven `GetTooltipText` already carried retail's exact text (`SpellCastSubMenu::AddFavorite @0x004C7060`/`UpdateFromPlayerModule @0x004C6570` both build a single-arg `Formatted` PStringBase — plain spell name, no wrapper — for the favorite-bar/submenu case; `gmSpellcastingUI::UpdateEndowmentIcon @0x004C6120` mirrors the cast button's confirmed `"%s (%hs)"` literal at `data_7b64d8` — item name, then spell name in parens — via the identical two-value narrow/wide prep sequence, one address away in the same function family). The cast button `@0x004C6FE8`/`ClearTooltip @0x004C6AAE` was the one real gap (`UiButton` had no tooltip wiring at all): now ported via `SpellcastingUiController.UpdateCastAvailability`/`ComputeEndowmentCastState`, sourced from `gmSpellcastingUI::UpdateCastButtonTooltip @0x004C6A30`. Fully verified literal text: the no-selection states (`"Select a spell to cast"` @ `data_7b64ec`, `"You have no spells ready to cast"` @ `data_7b6520`) and the complete endowment-item branch (`"USE the %s"` @ `data_7b64c0`, `" on %s"` @ `data_7b6464`, `"You must select a target for the %s"` @ `data_7b6478`; `ItemUses::IsUseable_SelfTarget @0x004fcd30` is exactly `ItemUseability.AllowsSelfTarget`). NOT ported: the endowment branch's incompatible-target sub-state (`"You must select an appropriate\ntarget for the %s"` @ `data_7b6400`, gated by `ItemHolder::TargetCompatibleWithObject @0x00587520` — a ~400-line function with its own chat-message side effects, out of scope for a tooltip batch; a present target is optimistically treated as compatible, same text as the confirmed-compatible case). **CORRECTED at the night-round review (F3/F4, 2026-08-17): the plain-spell branch's wording is NOT unrecoverable — the "genuine `gmNoticeHandler` vtable SLOTS" claim above was itself the artifact. `PStringBase::sprintf`'s second argument at those three call sites is a raw pushed literal (a plain `push 0x7bXXXX; call sprintf`); Binary Ninja's pseudo-C rendering of that operand as `&gmSpellcastingUI::\`vftable'.RecvNotice_XXX` was a spurious symbol match, not the true operand — a direct capstone disassembly of the raw bytes at `0x4c6e48`/`0x4c6ea4`/`0x4c6f18`/`0x4c6f5d` resolves the actual constants: `"CAST %hs"` @0x7b63a4 (untargeted/self-cast at `0x4c6f35`, and targeted+compatible at `0x4c6e57` — both enabled, the latter appending `" on %s"` @0x7b6464 with the target's name), `"You must select an appropriate target for %hs"` @0x7b6348 (targeted+incompatible, disabled), `"You must select a target for %hs"` @0x7b63b8 (no target, disabled); `%hs` is the spell's own name in all four call sites (`CSpellBase::InqName`, the same call `0x5bbee0` throughout). Now ported: `RuntimeSpellCastState.EvaluateCastGate` (the four-state gate) + `SpellcastingUiController.ComputeSpellCastState`. Also corrected the endowment branch's "USE the %s" operand: it was NOT the bare item name (F4) — the vararg to `"USE the %s"`/`"You must select a target for the %s"`/the still-unported incompatible-target string is the SAME composed `"%s (%hs)"` string (item name, spell name) built once at `@0x004c6bb6-ef` from format literal `data_7b64d8`, byte-confirmed by all three sprintf call sites (`0x4c6c7f`/`0x4c6ca4`/`0x4c6d46`) reading the identical `[esp+0x18]` slot — now ported via `SpellcastingUiController.ComposeEndowmentName`.** The character-panel `AttributeInfoRegion @0x004F1617` / `Attribute2ndInfoRegion @0x004F1777` / `SkillInfoRegion @0x004F222F` constructors are now fully ported through a new `UiClickablePanel.TooltipText` (the same settable-string seam as `UiButton.TooltipText`): the six hardcoded attribute descriptions (`SkillSystem::InqAttributeDescription @0x005c8e30`) and three hardcoded, pair-shared vitals descriptions (`SkillSystem::InqAttribute2ndDescription @0x005c8f70`) were byte-decoded from the retail string pool (the pseudo-C dump truncates them with "…"); skill tooltips compose `SkillInfoRegion::GetTooltip @0x004f1fe0`'s exact `"\n" + formula + description` (ported verbatim, including the confirmed lack of any separator between the formula and description text) from the ALREADY-DAT-parsed `DatReaderWriter.Types.SkillBase.Description`/`.Formula` fields (portal `0x0E000004`, the same resource `CharacterSheetProvider.SkillTable` already reads for skill names/costs) rather than hand-transcribed literals — no guessing was needed for the ~30+ skill description strings. The formula-to-text algorithm itself (`SkillSystem::InqSkillFormula @0x005c89b0`, e.g. producing `"( (Strength + Coordination) / 2 )"`) was fully recovered by byte-decoding six short literal fragments (`data_7e7930`/`7e7934`/`7e7940`/`7e7950`/`7e7954`/`797584`) the pseudo-C dump left completely unlabeled — they sit between two `gmSpellcastingUI` vtable declarations and Binary Ninja's type inference never recognized them as strings, so the raw hex had to be read directly as narrow ASCII (confirmed against the function's own directly-visible `" / %u"` and `"(%u x %s)"` literals, which needed no such recovery). Retail's runtime sites also SET the `P0x4B` on-bit themselves (`__bitfield164 |= 0x20`, eight sites) — the port models that as "runtime text present implies tooltip-on", so only the authored-text path consults the authored bit. **(2) The per-element wrap-width override:** `UIElement_Text::InqSizewMargins @0x00469660`'s `UITS_MAX_WIDTH` branch checks `GetAttribute_Int(this, 0x3D, ...)` before falling back to `RenderDevice::GetDisplayWidth()`; `RetailTooltipPresenter.ApplyTooltipText` always wraps at `UiRoot.EffectiveCanvasSize.X` (the confirmed fallback) and never checks for a `P0x3D` override — the live-DAT sweep found zero tooltip-bearing elements author one. | `src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs` (`ResolveTooltipText`'s runtime-then-authored order; `ApplyTooltipText`'s wrap-width literal; `UpdateWorldHoverTooltip`/`TryBuildAndMountPopup` — the world-hover half added 2026-08-16); `src/AcDream.App/UI/Layout/ElementReader.cs` (`ElementInfo.TooltipText`'s own doc comment carries the same F3 correction); `src/AcDream.App/UI/UiItemSlot.cs` (`TooltipTextResolve`, `GetTooltipText`, the hardcoded popup-locator constants); `src/AcDream.Core/Items/ClientObject.cs` (`GetTooltipDisplayName`); `src/AcDream.App/UI/CursorFeedbackController.cs` (the related #411 found-cursor fix, same round — see that row); Batch B (2026-08-17) additions: `src/AcDream.App/UI/UiPanel.cs` (`UiClickablePanel.TooltipText`/`GetTooltipText`); `src/AcDream.App/UI/Layout/SpellcastingUiController.cs` (`UpdateCastAvailability`, `ComputeEndowmentCastState`); night-round review (F3/F4, 2026-08-17) additions: `src/AcDream.App/UI/Layout/SpellcastingUiController.cs` (`ComputeSpellCastState`, `ComposeEndowmentName`); `src/AcDream.Runtime/Gameplay/RuntimeSpellCastState.cs` (`EvaluateCastGate`, `SpellCastGate`); `src/AcDream.App/Net/RetailSkillFormula.cs` (`AttributeName`, `FormatFormula`, `BuildTooltip`); `src/AcDream.App/UI/Layout/CharacterSheet.cs` (`CharacterSkill.TooltipText`); `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` (`BuildLiveCharacterSkills`'s tooltip compose); `src/AcDream.App/UI/Layout/CharacterStatController.cs` (`AttributeDescriptions`, `Attribute2ndDescriptions`, `BuildAttributeRows`/`BuildSkillRows` row wiring) | The 243 elements WITH literal text — the "core" case #409 ships — cover every hover-text scenario the investigation's own landmark checks exercised (main-game-UI Appearance-page rotate/color/spin hints, etc.). F9 correction: "243 with literal text" is not automatically "243 showable" — `RetailTooltipPresenter.OnTooltipShow`'s real gate is the FULL conjunction of `TooltipEnabled` (P0x4B) AND non-null text (P0x49) AND both popup-locator ids (P0x47/P0x48) — so this was measured, not assumed: `TooltipLiveDatTests.ClientWideSweep_FindsKnownLandmarksAndAFloorCount`'s `Showable` column finds the intersection is exactly 243, i.e. every element authoring literal text also authors the other three properties together (they are evidently authored as one group in practice). The P0x3D sweep found zero authoring elements, so the unconditional display-width fallback is not an approximation for any element that exists today. | If a future DAT revision adds a `P0xD0`-truncated text element, a game-code `SetTooltip` caller, or authors a `P0x3D` override, it silently shows no tooltip / wraps at the wrong width instead of erroring — indistinguishable from "the element authors no tooltip at all" without re-running the sweep AND separately auditing which of the 187 no-literal-text elements would actually truncate at their authored width. | `UIElement::InqProperty @0x004638D0` (base authored-bag read, NOT a dynamic override); `UIElement::StartTooltipAtMouse @0x00460D70` (`m_TTText`-vs-`InqProperty` preference order); `UIElement_Text::RecalculateTruncation @0x00466F80` (`P0xD0` gate, `SetTooltip`/`ClearTooltip` sites); `UIElement_Text::InqSizewMargins @0x00469660` (`UITS_MAX_WIDTH` branch, `GetAttribute_Int(this, 0x3D, ...)`); Batch B (2026-08-17): `gmSpellcastingUI::UpdateCastButtonTooltip @0x004C6A30` (cast-button state machine); `gmSpellcastingUI::UpdateEndowmentIcon @0x004C6120` (endowment-icon `"%s (%hs)"` confirmation); `SpellCastSubMenu::AddFavorite @0x004C7060` / `UpdateFromPlayerModule @0x004C6570` (favorite/submenu plain-name confirmation); `ItemUses::IsUseable_SelfTarget @0x004fcd30`; `AttributeInfoRegion::AttributeInfoRegion @0x004f1530` / `Attribute2ndInfoRegion::Attribute2ndInfoRegion @0x004f1680` / `SkillInfoRegion::SkillInfoRegion @0x004f2140` / `SkillInfoRegion::GetTooltip @0x004f1fe0`; `SkillSystem::InqAttributeName @0x005c8d90` / `InqAttributeDescription @0x005c8e30` / `InqAttribute2ndName @0x005c8ed0` / `InqAttribute2ndDescription @0x005c8f70` / `InqSkillFormula @0x005c89b0` | +| TS-85 | **Filed 2026-08-16 at #409 (client-wide retail tooltip system); REWRITTEN at the same-day F3 review round; NARROWED again at the same-day live-failure round.** LIVE-FAILURE-ROUND NARROWING: the `m_TTText` READ side is now ported — `RetailTooltipPresenter.ResolveTooltipText` consults `UiElement.GetTooltipText()` (this port's `m_TTText`) BEFORE the authored `P0x49`, exactly as `StartTooltipAtMouse @0x00460DA3`/`@0x00460DDF` orders them, and the `P0x48`-absent fallback to the element's own layout (`@0x00460E7E`) is ported through `UiElement.SourceLayoutDid`. That lit up every acdream surface whose controller ALREADY writes runtime tooltip text (the four Options tabs, Configure Keyboard, the social pages) — live-verified 2026-08-16 on the Character tab. What remains deferred is the WRITE side at the retail `SetTooltip` call sites acdream has no analog for yet, enumerated below. Two sub-mechanisms of retail's tooltip system are unported. **(1) The `m_TTText`/`SetTooltip` runtime-text family (headed by the `P0xD0` truncated-text auto-tooltip):** the ORIGINAL filing argued this port's gap was "retail's dynamic `InqProperty(0x49)` override" — that framing is false. `UIElement::InqProperty @0x004638D0`, the BASE implementation every element uses unless its own class overrides the virtual, reads exactly the same authored property bags (`m_instanceProperties`, `m_curStateDesc`, `m_desc`) this port's `ElementReader` already walks generically — so an element with no literal `P0x49` gets NOTHING from retail's own default `InqProperty` either. The REAL second text source is the element's cached `m_TTText` field, set ONLY by the explicit, non-dat `UIElement::SetTooltip` call (`UIElement::StartTooltipAtMouse @0x00460D70` prefers `m_TTText` over the `InqProperty` fallback whenever it is non-empty). `SetTooltip` has 15+ known game-code call sites (Options rows `@0x00485E65`, chargen `@0x00481981`, the paperdoll endowment icon `@0x004C63A1`, the spellcast button `@0x004C6FE8`/`@0x004C6AAE`, and more), headed by the highest-volume one: `UIElement_Text::RecalculateTruncation @0x00466F80`, gated on authored `P0xD0` — an overflowing single/wrapped line calls `SetTooltip(this, ownText) @0x00467064` + sets enable bit 5 `@0x00467076`; a line that now fits calls `ClearTooltip @0x00467064`/clears the bit `@0x00466ff9`. `RecalculateTruncation`'s own truncation-POSITION computation (the rest of the function, `@0x004670a1` onward) walks a `GlyphList` per-line-position model (`FindCompleteLineFromY`/`FindPosFromLineAndPixels`/`FindPixelsFromPos`) this port's `UiText` has no equivalent of — `UiText` clips visually via a scissor rect (`DrawClippedText`'s `PushClip`) with no tracked "does this line overflow" state at all, so porting the auto-tooltip trigger requires building that state first. Sized as genuinely disproportionate for a single fix-round commit alongside F1-F2/F4-F11 and deferred here rather than shipped as a partial/unverified stub. A live-DAT sweep found 187 of the 430 elements authoring at least one tooltip-trigger property have NO literal `P0x49` `StringInfo` text; the live-failure round re-measured that set and found every one of the 187 authors BOTH popup-locator ids (`P0x47`+`P0x48`) — i.e. they are runtime-`SetTooltip` targets by construction, waiting only for text. **F12 correction (night-round review, 2026-08-17): this is 17 sites, not 15** — the original tally dropped `gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF` (mentioned two sentences below as its own closed row) and undercounted by one more besides. The 17 `SetTooltip` call sites, enumerated from the decomp at the live-failure round, split into: PORTED (an acdream controller already writes the text, and the presenter now reads it) — the Options rows `@0x00485E65`/`@0x00484803`/`@0x00487053`, chargen skills `@0x00481981`, the radar `@0x004D9605`; PORTED 2026-08-16 (hover-feedback completion round, docs/ISSUES.md #409/#411): inventory/shortcut item hover `UIElement_UIItem::UpdateTooltip @0x004E1CB0` — `UiItemSlot` now hardcodes the catalog's uniform popup locator (`P0x47=0x10000395`/`P0x48=0x21000041`, live-DAT-confirmed uniform across all 47 UIItem-type catalog prototypes, `TooltipLiveDatTests.UiItemCatalog_EveryPrototype_SharesTheSamePopupLocator`) and a `TooltipTextResolve` delegate wired at every physical-item construction site (inventory, external container, paperdoll — closing the separate `gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF` row below too, vendor, secure trade, toolbar), backed by the new `ClientObject.GetTooltipDisplayName()` (NAME_APPROPRIATE + the `"%d %s"` stack-count prefix, matching the decomp exactly); and the SmartBox found-object world-hover tooltip `UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @0x004E5AD0` (`@0x004E5D74`/`@0x004E5DFB`) — `RetailTooltipPresenter.UpdateWorldHoverTooltip` ports its `PlayerModule::ShowTooltips`-gated, `GetAppropriateName`-only (no stack prefix — a real, decomp-confirmed asymmetry vs. the item-cell case) trigger — **TIMING CORRECTED at the 2026-08-17 morning gate round (user finding: retail world tooltips "lag"; ours popped instantly): the original "edge-fired (no dwell)" reading was a misread — the notice's immediate `StartTooltipAtMouse @0x004E5DFB` sits inside `if (s_pInstance->m_dragElement != 0)` (`@0x004E5D8E`; `m_dragElement` is a real, distinct PDB field in `acclient.h`'s `UIElementManager`, separate from the `m_pTooltipElement` family), so the immediate mount is DRAG-ONLY; the ordinary hover path merely STAGES the name (`SetTooltip @0x004E5D74` + `|= 0x20`) and the display rides `CheckTooltip @0x0045B6E0`'s mouse-idle dwell (`m_lastMouseMoveTime` stamped on EVERY move `@0x0045e736` + `m_tooltipDelay` 0.25 s `@0x0045f75d`), with found-object changes under an idle mouse swapping the popup same-frame via `SetTooltip`'s own text-change teardown (`@0x004617FF` → `ResetTooltip @0x0045C360` tail-calling `CheckTooltip`) and the 10 s `m_tooltipDuration` expiry requiring a fresh mouse move before re-arming (`SwitchMouseOver(null) @0x0045b7b2`) — all now ported, including the drag-immediate branch**, reusing the SAME popup locator since an exhaustive DAT sweep found `UIElement_SmartBoxWrapper` (class `0x10000030`) has no authored `ElementDesc` anywhere installed (`TooltipLiveDatTests.SmartBoxWrapper_HasNoAuthoredElementDesc_AnywhereInstalled`) — the popup-skin choice is therefore the best-evidenced inference, not a measured value, and is called out here as such. **BATCH B (2026-08-17) CLOSED the spellcasting and character-panel rows of this list. BATCH C (2026-08-17, Map/House toolbar panel) CLOSES THE LAST REMAINING ITEM: `gmMapUI::AddMapNote @0x004A1C51`'s 53 town-hotspot tooltips are now ported via `MapPageController.BuildTownMarkers` (`src/AcDream.App/UI/Layout/MapPageController.cs`), setting `UiButton.TooltipText` (retail's RUNTIME `m_TTText`/`SetTooltip` mechanism, not the DAT-authored `P0x49` path an earlier same-day cut mistakenly used and which never rendered live during verification); literal town names from `MapLocations.cs` (a verbatim port of `s_rgLocations`), not a DAT string-table lookup, matching `AddMapNote`'s own `StringInfo::SetLiteralValue` call. CORRECTED at the same-day morning gate round (user finding 3 — the retail screenshot's green hover highlight + special-font parchment tooltip): Batch C's "the town-marker template authors no locator of its own" claim was WRONG — the template (`0x100001F0` in `0x21000026`, `MapNoteLiveDatTests`) authors its OWN `P0x47=0x10000398`/`P0x48=0x21000041` (the fourth popup skin, whose incorporated text child `0x10000396` fonts `0x40000015` where the other three skins font `0x40000002`), a zero per-element delay `P0x50=0.0`, `P0x4B` TooltipOn, and `P0x13` RolloverEnabled with PassToChildren `Normal`/`Normal_rollover` states flipping the highlight child `0x100001F1`'s per-state `P0x3B` (the green `0x06004CC9` frame, byte-decoded A=FF R=00 G=FF B=00); the hardcoded shared-skin override was removed (the built marker's authored locator wins) and the rollover highlight + per-state-`P0x3B` + button PassToChildren cascade are now ported (`UiButton.CascadeStateToChildren`, `UiDatElement.TrySetRetailState`'s 0x3B honor). Sub-mechanism (1)'s `SetTooltip`-call-site enumeration is 16 of 17 known sites PORTED — `UIElement_Text::RecalculateTruncation @0x00466F80` (the headline, highest-volume site named at the top of sub-mechanism (1)) remains the ONE open item, exactly as this row's own sub-mechanism (1) text above already scoped it out (its own "Sized as genuinely disproportionate... deferred here" note). The prior "all 15 known sites accounted for" close (F12 correction, night-round review) was wrong twice over: the count is 17, not 15, and RecalculateTruncation was never actually ported — it was always the one deliberately-deferred item, not a closed one.** Batch B audit findings: the endowment icon `@0x004C63A1`, favorite `@0x004C7206`, and submenu `@0x004C67D8` sites turned out to be ALREADY CORRECT — all three are `UiCatalogSlot`-based and the pre-existing `Label`-driven `GetTooltipText` already carried retail's exact text (`SpellCastSubMenu::AddFavorite @0x004C7060`/`UpdateFromPlayerModule @0x004C6570` both build a single-arg `Formatted` PStringBase — plain spell name, no wrapper — for the favorite-bar/submenu case; `gmSpellcastingUI::UpdateEndowmentIcon @0x004C6120` mirrors the cast button's confirmed `"%s (%hs)"` literal at `data_7b64d8` — item name, then spell name in parens — via the identical two-value narrow/wide prep sequence, one address away in the same function family). The cast button `@0x004C6FE8`/`ClearTooltip @0x004C6AAE` was the one real gap (`UiButton` had no tooltip wiring at all): now ported via `SpellcastingUiController.UpdateCastAvailability`/`ComputeEndowmentCastState`, sourced from `gmSpellcastingUI::UpdateCastButtonTooltip @0x004C6A30`. Fully verified literal text: the no-selection states (`"Select a spell to cast"` @ `data_7b64ec`, `"You have no spells ready to cast"` @ `data_7b6520`) and the complete endowment-item branch (`"USE the %s"` @ `data_7b64c0`, `" on %s"` @ `data_7b6464`, `"You must select a target for the %s"` @ `data_7b6478`; `ItemUses::IsUseable_SelfTarget @0x004fcd30` is exactly `ItemUseability.AllowsSelfTarget`). NOT ported: the endowment branch's incompatible-target sub-state (`"You must select an appropriate\ntarget for the %s"` @ `data_7b6400`, gated by `ItemHolder::TargetCompatibleWithObject @0x00587520` — a ~400-line function with its own chat-message side effects, out of scope for a tooltip batch; a present target is optimistically treated as compatible, same text as the confirmed-compatible case). **CORRECTED at the night-round review (F3/F4, 2026-08-17): the plain-spell branch's wording is NOT unrecoverable — the "genuine `gmNoticeHandler` vtable SLOTS" claim above was itself the artifact. `PStringBase::sprintf`'s second argument at those three call sites is a raw pushed literal (a plain `push 0x7bXXXX; call sprintf`); Binary Ninja's pseudo-C rendering of that operand as `&gmSpellcastingUI::\`vftable'.RecvNotice_XXX` was a spurious symbol match, not the true operand — a direct capstone disassembly of the raw bytes at `0x4c6e48`/`0x4c6ea4`/`0x4c6f18`/`0x4c6f5d` resolves the actual constants: `"CAST %hs"` @0x7b63a4 (untargeted/self-cast at `0x4c6f35`, and targeted+compatible at `0x4c6e57` — both enabled, the latter appending `" on %s"` @0x7b6464 with the target's name), `"You must select an appropriate target for %hs"` @0x7b6348 (targeted+incompatible, disabled), `"You must select a target for %hs"` @0x7b63b8 (no target, disabled); `%hs` is the spell's own name in all four call sites (`CSpellBase::InqName`, the same call `0x5bbee0` throughout). Now ported: `RuntimeSpellCastState.EvaluateCastGate` (the four-state gate) + `SpellcastingUiController.ComputeSpellCastState`. Also corrected the endowment branch's "USE the %s" operand: it was NOT the bare item name (F4) — the vararg to `"USE the %s"`/`"You must select a target for the %s"`/the still-unported incompatible-target string is the SAME composed `"%s (%hs)"` string (item name, spell name) built once at `@0x004c6bb6-ef` from format literal `data_7b64d8`, byte-confirmed by all three sprintf call sites (`0x4c6c7f`/`0x4c6ca4`/`0x4c6d46`) reading the identical `[esp+0x18]` slot — now ported via `SpellcastingUiController.ComposeEndowmentName`.** **CA5-GATE CORRECTIONS (2026-08-24, owner retail-render oracle): (a) `SkillInfoRegion::GetTooltip`'s compose is `formula + " +" + description` — the row below has the operand order backwards (" +"+formula, "no separator"); the operator+ left operand is the InqSkillFormula output, and retail renders formula-first-line/description-below. (b) The "grow further if vertical scroll overflow" branch this row calls "a structural no-op" is NOT one — it is retail's SECOND sizing pass (`StartTooltip @0x0045DE90`: measure-wrap at max width → clamped root resize → `RecalculateGlyphList` RE-WRAP at the final clamped width → height growth for the extra lines), which is what makes long tooltips multi-line; both now ported in `RetailTooltipPresenter.ApplyTooltipText`/`RetailSkillFormula.BuildTooltip`.** **#430 CORRECTION (2026-08-24): the character-panel port below set the TEXT but omitted the popup LOCATOR on the runtime-built rows, so these tooltips never mounted until the CA5-adjacent fix gave rows the shared 0x10000395/0x21000041 skin (live-DAT probed; UiItemSlot precedent).** The character-panel `AttributeInfoRegion @0x004F1617` / `Attribute2ndInfoRegion @0x004F1777` / `SkillInfoRegion @0x004F222F` constructors are now fully ported through a new `UiClickablePanel.TooltipText` (the same settable-string seam as `UiButton.TooltipText`): the six hardcoded attribute descriptions (`SkillSystem::InqAttributeDescription @0x005c8e30`) and three hardcoded, pair-shared vitals descriptions (`SkillSystem::InqAttribute2ndDescription @0x005c8f70`) were byte-decoded from the retail string pool (the pseudo-C dump truncates them with "…"); skill tooltips compose `SkillInfoRegion::GetTooltip @0x004f1fe0`'s exact `"\n" + formula + description` (ported verbatim, including the confirmed lack of any separator between the formula and description text) from the ALREADY-DAT-parsed `DatReaderWriter.Types.SkillBase.Description`/`.Formula` fields (portal `0x0E000004`, the same resource `CharacterSheetProvider.SkillTable` already reads for skill names/costs) rather than hand-transcribed literals — no guessing was needed for the ~30+ skill description strings. The formula-to-text algorithm itself (`SkillSystem::InqSkillFormula @0x005c89b0`, e.g. producing `"( (Strength + Coordination) / 2 )"`) was fully recovered by byte-decoding six short literal fragments (`data_7e7930`/`7e7934`/`7e7940`/`7e7950`/`7e7954`/`797584`) the pseudo-C dump left completely unlabeled — they sit between two `gmSpellcastingUI` vtable declarations and Binary Ninja's type inference never recognized them as strings, so the raw hex had to be read directly as narrow ASCII (confirmed against the function's own directly-visible `" / %u"` and `"(%u x %s)"` literals, which needed no such recovery). Retail's runtime sites also SET the `P0x4B` on-bit themselves (`__bitfield164 |= 0x20`, eight sites) — the port models that as "runtime text present implies tooltip-on", so only the authored-text path consults the authored bit. **(2) The per-element wrap-width override:** `UIElement_Text::InqSizewMargins @0x00469660`'s `UITS_MAX_WIDTH` branch checks `GetAttribute_Int(this, 0x3D, ...)` before falling back to `RenderDevice::GetDisplayWidth()`; `RetailTooltipPresenter.ApplyTooltipText` always wraps at `UiRoot.EffectiveCanvasSize.X` (the confirmed fallback) and never checks for a `P0x3D` override — the live-DAT sweep found zero tooltip-bearing elements author one. | `src/AcDream.App/UI/Layout/RetailTooltipPresenter.cs` (`ResolveTooltipText`'s runtime-then-authored order; `ApplyTooltipText`'s wrap-width literal; `UpdateWorldHoverTooltip`/`TryBuildAndMountPopup` — the world-hover half added 2026-08-16); `src/AcDream.App/UI/Layout/ElementReader.cs` (`ElementInfo.TooltipText`'s own doc comment carries the same F3 correction); `src/AcDream.App/UI/UiItemSlot.cs` (`TooltipTextResolve`, `GetTooltipText`, the hardcoded popup-locator constants); `src/AcDream.Core/Items/ClientObject.cs` (`GetTooltipDisplayName`); `src/AcDream.App/UI/CursorFeedbackController.cs` (the related #411 found-cursor fix, same round — see that row); Batch B (2026-08-17) additions: `src/AcDream.App/UI/UiPanel.cs` (`UiClickablePanel.TooltipText`/`GetTooltipText`); `src/AcDream.App/UI/Layout/SpellcastingUiController.cs` (`UpdateCastAvailability`, `ComputeEndowmentCastState`); night-round review (F3/F4, 2026-08-17) additions: `src/AcDream.App/UI/Layout/SpellcastingUiController.cs` (`ComputeSpellCastState`, `ComposeEndowmentName`); `src/AcDream.Runtime/Gameplay/RuntimeSpellCastState.cs` (`EvaluateCastGate`, `SpellCastGate`); `src/AcDream.App/Net/RetailSkillFormula.cs` (`AttributeName`, `FormatFormula`, `BuildTooltip`); `src/AcDream.App/UI/Layout/CharacterSheet.cs` (`CharacterSkill.TooltipText`); `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` (`BuildLiveCharacterSkills`'s tooltip compose); `src/AcDream.App/UI/Layout/CharacterStatController.cs` (`AttributeDescriptions`, `Attribute2ndDescriptions`, `BuildAttributeRows`/`BuildSkillRows` row wiring) | The 243 elements WITH literal text — the "core" case #409 ships — cover every hover-text scenario the investigation's own landmark checks exercised (main-game-UI Appearance-page rotate/color/spin hints, etc.). F9 correction: "243 with literal text" is not automatically "243 showable" — `RetailTooltipPresenter.OnTooltipShow`'s real gate is the FULL conjunction of `TooltipEnabled` (P0x4B) AND non-null text (P0x49) AND both popup-locator ids (P0x47/P0x48) — so this was measured, not assumed: `TooltipLiveDatTests.ClientWideSweep_FindsKnownLandmarksAndAFloorCount`'s `Showable` column finds the intersection is exactly 243, i.e. every element authoring literal text also authors the other three properties together (they are evidently authored as one group in practice). The P0x3D sweep found zero authoring elements, so the unconditional display-width fallback is not an approximation for any element that exists today. | If a future DAT revision adds a `P0xD0`-truncated text element, a game-code `SetTooltip` caller, or authors a `P0x3D` override, it silently shows no tooltip / wraps at the wrong width instead of erroring — indistinguishable from "the element authors no tooltip at all" without re-running the sweep AND separately auditing which of the 187 no-literal-text elements would actually truncate at their authored width. | `UIElement::InqProperty @0x004638D0` (base authored-bag read, NOT a dynamic override); `UIElement::StartTooltipAtMouse @0x00460D70` (`m_TTText`-vs-`InqProperty` preference order); `UIElement_Text::RecalculateTruncation @0x00466F80` (`P0xD0` gate, `SetTooltip`/`ClearTooltip` sites); `UIElement_Text::InqSizewMargins @0x00469660` (`UITS_MAX_WIDTH` branch, `GetAttribute_Int(this, 0x3D, ...)`); Batch B (2026-08-17): `gmSpellcastingUI::UpdateCastButtonTooltip @0x004C6A30` (cast-button state machine); `gmSpellcastingUI::UpdateEndowmentIcon @0x004C6120` (endowment-icon `"%s (%hs)"` confirmation); `SpellCastSubMenu::AddFavorite @0x004C7060` / `UpdateFromPlayerModule @0x004C6570` (favorite/submenu plain-name confirmation); `ItemUses::IsUseable_SelfTarget @0x004fcd30`; `AttributeInfoRegion::AttributeInfoRegion @0x004f1530` / `Attribute2ndInfoRegion::Attribute2ndInfoRegion @0x004f1680` / `SkillInfoRegion::SkillInfoRegion @0x004f2140` / `SkillInfoRegion::GetTooltip @0x004f1fe0`; `SkillSystem::InqAttributeName @0x005c8d90` / `InqAttributeDescription @0x005c8e30` / `InqAttribute2ndName @0x005c8ed0` / `InqAttribute2ndDescription @0x005c8f70` / `InqSkillFormula @0x005c89b0` | | TS-84 | Chargen 3D preview (Campaign CC slice CC6a foundation): `ChargenClothingTable`'s composer skips retail's ~8-branch Setup-id substitution chain (`ClothingTable::BuildObjDesc @ 0x005A7900`'s Umbraen/Penumbraen/Undead/Anakshay fallback) when a garment's `ClothingBaseEffects` has no entry for the resolved body Setup. MEASURED (not assumed) against the installed EoR dat across all 26 heritage/gender combinations via `ChargenAppearanceCatalogInstalledDatTests`, with the measurement now PINNED by a real assertion rather than diagnostic-only output (review fix round F7): the 9 standard heritages whose UI actually shows clothing controls resolve every default gear choice with zero coverage gaps. Undead is a real gap — its default gear choices (both genders) have NO base-effect entry on **ALL FOUR clothing slots — headgear, trousers, shirt, AND footwear** (not the three-slot "headgear/trousers/footwear" this row originally understated, with a self-contradicting "4 of 4 non-shirt slots" aside — corrected at the review fix round F2) — for Undead's own live body Setup (male 0x02001A9C / female 0x02001AA0), because that Setup is one of the skeleton/zombie variants the un-ported chain exists to redirect. The four measured missing clothing-table ids are identical on both genders and in a fixed order: `0x10000009, 0x100000F9, 0x10000001, 0x10000007` (Headgear, Trousers, Shirt, Footwear — the factory's own composition order). Gear Knight and both Olthoi variants also show gaps under a synthetic "select every offered option" sweep, but retail hides the clothing controls entirely for those three heritages (`gmCGAppearancePage::Update @ 0x0047E8F0`'s `m_pClothesButton->SetVisible(0)` branches for `mHeritageGroup == 6` and `== 0xc \|\| == 0xd`), so a real chargen selection never reaches them — not a live gap. | `src/AcDream.Core/CharGen/ChargenClothingTable.cs`; `src/AcDream.Core/CharGen/ChargenAppearanceFactory.cs` (`ComposeClothingSlot`) | CC6a is explicitly the rendering-foundation slice (index→ObjDesc factory + static-pose offscreen renderer, no page mount yet); porting the ~8-branch substitution chain is bounded follow-up work once CC6b wires real clothing-slot UI, not a blocker for the foundation deliverable — and the installed-DAT test proves the gap is narrow (one heritage, all four of ITS slots) rather than pervasive. | Undead's default clothing preview renders the bare body mesh for ALL FOUR slots — headgear, trousers, shirt, AND footwear (no clothing part/texture override applied on any of them, though the dye subpalette contribution — gated on a DIFFERENT lookup — is unaffected) — until the chain, or an equivalent per-heritage default-clothing-Setup map, is ported. | `ClothingTable::BuildObjDesc @ 0x005A7900` (Umbraen/Penumbraen/Undead/Anakshay Setup-substitution branches); `gmCGAppearancePage::Update @ 0x0047E8F0` (clothes-button visibility gate); `tests/AcDream.Content.Tests/CharGen/ChargenAppearanceCatalogInstalledDatTests.cs` | | TS-73 | **NARROWED 2026-08-11 at Campaign OP slice OP4.** `RuntimeCharacterOptionsState.TrySetOption`'s port of `CPlayerModule::OnChanged @0x0059A8E0`'s local side-effect switch (step 2) still covers only the two `PlayerModule`-state-mutating cases (`case 2`/`case 0x12` fellowship mutual exclusion) — that part is unchanged. Of the four presentation-binding cases, TWO are now closed: `0x07 ViewCombatTarget` (re-pointed `ICombatGameplaySettingsSource` reads `RuntimeCharacterOptionsState` live — `CharacterOptionCombatSettingsSource`, `src/AcDream.App/Combat/LiveCombatAttackOperations.cs`) and `0x30 DisableDistanceFog` (`WeatherSystem.DisableDistanceFogSource`, a poll bound once in `GameWindow.cs`, forces `FogMode.Off` in `WeatherSystem.Snapshot`) — NEITHER lives inside `TrySetOption`'s own switch; both are separate App-layer poll bindings, so the literal claim in this row's title ("this Runtime-only seam can reach") stays true, but the user-observable symptom is fixed for these two ids. The remaining two, `0x04 DisableMostWeatherEffects` and `0x05 PersistentAtDay`, stay open — see TS-6 (weather-particle subsystem not yet located) and TS-75 (day/night force) respectively; this row no longer duplicates either. | `src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs` (`RuntimeCharacterOptionsState.TrySetOption`) | The remaining two options are correctly scoped to their OWN pre-existing/new rows (TS-6, TS-75) rather than re-litigated here. | Toggling `DisableMostWeatherEffects`/`PersistentAtDay` writes the bit and dirties/auto-saves it correctly, but produces NONE of retail's immediate local presentation change (weather doesn't stop, day/night doesn't force) — see TS-6/TS-75 for why. `ViewCombatTarget`/`DisableDistanceFog` are retired from this row's risk: both now behave correctly. | `CPlayerModule::OnChanged @0x0059A8E0`; `docs/research/2026-08-10-character-options-map.md` §1.5 | | TS-75 | "Always Daylight Outdoors" (`PlayerOption PersistentAtDay`, `CPlayerModule::OnChanged` case `0x05` → `LScape::SetDay(value)`) has no acdream consumer. The campaign plan's own Group-B binding table cites `RuntimeWorldEnvironmentDefinition.ForcedDayGroupIndex` as the target seam — **that citation is a mechanism mismatch, corrected here**: `ForcedDayGroupIndex` selects which WEATHER-VARIETY day-group (`RuntimeWorldDayGroupDefinition`, e.g. a Clear/Overcast/Rain/Snow/Storm pick) is always chosen — the SAME deterministic-per-day-RNG mechanism `WeatherSystem`'s own roll uses (see TS-6) — NOT retail's time-of-day day/night force. No acdream mechanism currently overrides the sky cycle's TIME to stay in daytime lighting; wiring this option correctly needs that mechanism built first, not just a poll into the wrong field. | `src/AcDream.Runtime/World/RuntimeWorldEnvironmentState.cs` (`RuntimeWorldEnvironmentDefinition.ForcedDayGroupIndex` — NOT the right target); no current consumer exists | Filed rather than silently wired to the wrong field — a poll into `ForcedDayGroupIndex` would have SILENTLY changed the character's weather-variety odds instead of forcing daytime, an incorrect fix masquerading as a correct one (CLAUDE.md's "no workarounds" rule). | Toggling the option writes the bit and dirties/auto-saves it correctly, but night still falls normally — no observable daylight-forcing behavior. | `CPlayerModule::OnChanged @0x0059A8E0` case 5; `LScape::SetDay` (not yet located in the decomp) | @@ -457,7 +502,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | TS-49 | Hidden-object availability is bridged through `TargetManager.NotifyVoyeurOfEventAndClear(ExitWorld)` because acdream has not ported retail's DetectionManager. Retail `CObjCell::hide_object` sends `LeftDetection` to detection voyeurs; acdream instead withholds Hidden hosts from ordinary `GetObjectA` relationship creation and uses the existing non-Ok target update to tear down MoveTo/Sticky consumers and clear watched-role subscriptions while preserving the hidden object's own watcher role. | `src/AcDream.App/Physics/EntityPhysicsHost.cs` (`NotifyHidden`); `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs` (`ResolvePhysicsHost`); `src/AcDream.Core/Physics/Motion/TargetManager.cs` (`NotifyVoyeurOfEventAndClear`) | The current movement consumers already share TargetManager's status fan-out; the bridge prevents pursuit of an unavailable object without inventing a second partial detection database. | Plugins or future systems listening specifically for retail detection enter/leave events receive no `LeftDetection`; only movement/sticky target consumers observe the equivalent availability loss. | `CObjCell::hide_object @ 0x0052BE30`; retire by porting DetectionManager/CObjCell detection-voyeur delivery and routing Hidden through `LeftDetection` | | TS-50 | `AnimationDone` executes semantically at each owner's retail `CPhysicsObj::process_hooks` boundary, but all other animation hooks are retained in `AnimationHookFrameQueue` until final root/part/equipped-child pose publication. Retail executes the complete hook stream before transition and the Target/Movement/PartArray/Position manager tail because its current CPartArray pose already exists in-place. Static owners correctly reach `process_hooks` only after their root, parts, and children are current. | `src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs`; `src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs`; shared frame drain in `src/AcDream.App/Update/LiveObjectFrameController.cs` (`LiveEffectFrameController`) | The modern renderer publishes immutable effect-pose snapshots after all root/child composition; deferred visual sinks avoid attaching particles/lights/audio to the previous pose. Semantic `AnimationDone` is split out and exact, so motion completion and manager behavior are not delayed. Pose-owner lifetime tokens prevent deferred hooks from crossing delete/local-ID reuse. | A non-AnimationDone hook with same-quantum semantic consequences (notably `CallPES`, default-script chaining, audio/particle creation relative to a transition) runs later than retail and can observe post-tail state or start one render frame late. | `CPhysicsObj::process_hooks @ 0x00511550`; `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by publishing the current per-object/child pose before hook routing or splitting semantic and presentation sinks without changing authored hook order | | TS-51 | Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each ordinary object's ParticleManager then ScriptManager inside every admitted `UpdateObjectInternal` quantum; `animate_static_object` instead advances that static owner's ScriptManager then ParticleManager and only then `process_hooks`, using its whole admitted elapsed interval. acdream's shared tail is Particle → Script after static hook capture. | `src/AcDream.App/Update/LiveObjectFrameController.cs` (`LiveObjectFrameController` + `LiveEffectFrameController` shared `_particles.Tick` / `_scripts.Tick` tail); `src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs` | The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and ordinary manager order faithful without pretending the shared tails have per-owner timing or static-tail order. Splitting ownership safely requires a later effect-lifetime slice. | A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static hooks can route before their script/particle managers and static default scripts/particles use render elapsed in Particle → Script order rather than `animate_static_object` elapsed/discard and Script → Particle → hooks timing. | `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum/order | -| TS-52 | The terrain shader applies retail-authored base/overlay/road `TerrainTex.TexTiling` but omits the separate Environment Detail Textures pass and its viewer-distance fade (**#226**). | `src/AcDream.App/Rendering/TerrainAtlas.cs`; `src/AcDream.App/Rendering/TerrainModernRenderer.cs`; `src/AcDream.App/Rendering/Shaders/terrain_modern.frag` | `bb5acab9` fixed the user-visible stretched/blurry regression by porting the distinct base-tiling contract. An earlier experimental detail array darkened the whole ground because its source/neutral blend contract was wrong, so it was correctly reverted rather than guessed into production. | With retail's Environment Detail Textures preference enabled, close terrain lacks the extra high-frequency modulation/fade even though authored base texture scale is correct. | `LScape::GenerateDetailSurfaces` / `SetDetailTexturing @ 0x00506B40`; `ACRender::landPolyDraw @ 0x006B6450..0x006B6525`; issue #226 | +| ~~TS-52~~ | **RETIRED 2026-08-21 (#226); re-ported 2026-08-22 (Campaign VM VM1/VM2).** The row's landscape premise was wrong: the reachable Sept-2013 `ChangeRegion` caller passes zero LANDSCAPE detail surfaces and enables the building/environment categories. acdream now resolves those authored category detail textures and tiling, and replays eligible building-shell and EnvCell subsets with retail's **single-pass** detail combine — `SRCALPHA + INVSRCALPHA` compositing `lerp(base·diffuse, detail.rgb, detail.a·diffuseAlpha)` — which VM2's live cdb read proved is the path real hardware runs (`m_caps.bCanDoSinglePassDetailing = 1`), not the two-pass `DESTCOLOR + INVSRCALPHA` fallback this row originally described. There is no viewer-depth fade: retail's `get_alpha_for_z` is unreachable for built meshes (`noFadeDetail = 1`); attenuation is the sampler's linear mip chain. acdream consumes the existing Building Detail Textures preference. The earlier experimental landscape array remains correctly reverted; there is no missing user-visible landscape pass to track. | `src/AcDream.App/Rendering/TerrainAtlas.cs`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs`; `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs`; `src/AcDream.App/Rendering/Shaders/mesh_detail.vert`; `src/AcDream.App/Rendering/Shaders/mesh_detail.frag` | Retired on measured caller/category evidence and the connected on/off/restored visual gate; blend/fade re-ported on VM2's live cdb evidence. | None; disabling the preference submits no detail replay, while enabling it visibly changes nearby building/EnvCell surfaces (a mild darkening, not the earlier-assumed brightening). Landscape remains unchanged, matching the reachable retail caller. | `docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md`; `docs/research/2026-08-22-vm2-retail-detail-path-cdb.md`; `LScape::ChangeRegion`; `SmartBox::SetDetailTexturing`; issue #226 | | TS-53 | acdream advances retained UI time on the draw seam and local teleport/UI-camera presentation after its SmartBox-shaped object → inbound network → CommandInterpreter barrier. Retail `Client::UseTime` calls `UIElementManager::UseTime` first, whose global time message reaches `gmSmartBoxUI::UseTime`, and publishes player-camera work from the physics/player callback rather than one post-network camera tail. Slices 6–7 preserve the accepted host order as ownership-only extractions. | `src/AcDream.App/Update/UpdateFrameOrchestrator.cs` (post-live-frame teleport/camera phases); `src/AcDream.App/Rendering/PrivatePresentationRenderer.cs` (`RetainedGameplayUiFrame.Render`); `docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`; `docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md` | Current retained UI, portal transit, reveal, camera, and connected movement traces are accepted; changing cross-subsystem host order while extracting ownership would combine a behavior change with the structural cutover. | Retained UI, teleport, and camera presentation can observe same-frame object/inbound/player state one host update earlier or later than retail at transition boundaries; a future exact host-order port must prove UI, input, reveal, and camera consequences together. | `Client::UseTime @ 0x00411C40`; `UIElementManager::UseTime`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; `CPhysics::UseTime @ 0x00509950`; retire only with a focused host-order port and connected portal/camera comparison | | ~~TS-54~~ | **RETIRED 2026-08-08 (Campaign A slice A4).** The AdminEnvirons stingers now play. `UiSoundController.PlayEnvironCue` maps the change type through `EnvironSoundCueMap` — an EXPLICIT table read case-by-case out of `CPlayerSystem::Handle_Admin__Environs` @ `0x0055DE20` (`0x0055E0C6..0x0055E2C7`), not an offset: codes `0x65..0x72` sit 0x11 below their SoundType but `0x73`/`0x74` have no case at all, so `0x75` lands on `UI_Squeal` (0x84) where arithmetic would give 0x86, and the switch ends at `0x7B`/`UI_Thunder6` with no `0x7C` case. All 21 cases are pinned by conformance tests. The bank itself is no longer a blocker either: the UI sound table's DID is resolved by walking the dats' EnumIDMap chain (`UiSoundTableResolver`, master → slot-7 map → `0x2000004B`), which is how retail finds it — `GetUISoundTable` holds no literal. | retired | — | — | `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20`; `SoundManager::PlaySoundFromCenter @ 0x00550950`; `ClientUISystem::GetUISoundTable @ 0x00563FB0`; `docs/research/2026-08-08-audio-retail-music-absence.md` §5 | | TS-55 | AdminEnvirons fog values remain a color-only `WeatherSystem.Override` approximation. Retail values 1..5 install authored ambient color/level plus fog color/max; value 6 also forces transition/min/max and blanks radar; Clear restores all override fields and radar; `0x270F` installs a separate authored override. | `src/AcDream.App/World/WorldEnvironmentController.cs` (`ApplyAdminEnvirons`); `src/AcDream.Core/World/WeatherState.cs` (`EnvironOverrideColor`) | Preserves the already accepted enum bridge while Slice 8 moves ownership; porting the complete environment/radar presentation is a separate behavior change requiring focused visual gates. | Forced-fog hue, density, scene ambient, and radar blanking differ from retail; `0x270F` is ignored. | `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20` (`0x0055DE2B..0x0055E344`) | @@ -472,8 +517,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | 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 | **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-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-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` | @@ -491,6 +536,14 @@ equivalence argument (promote to AD/AP) or a fix. | UN-4 | GfxObj double-sided/negative-surface handling keeps WB's legacy logic (cull-mode double-siding, no reversed-winding duplicate, different neg-surface predicate) while the CellStruct path follows the retail-cited `ConstructMesh` reading | `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:1059` (CellStruct contrast :1396-1410) | No recorded justification on the GfxObj side — it is the unmodified WB extraction; the retail citation was added only to the CellStruct path | GfxObj models retail draws via duplicated-reversed-winding get wrong back-face lighting (normals not inverted) or missing/extra negative faces — dark or absent faces from behind | `D3DPolyRender::ConstructMesh` 0x0059dfa0 | | UN-6 | Fixed 200 ms sleep between ConnectRequest and ConnectResponse; retail inserts no delay. Annotated only as "with 200ms race delay"; the 2026-06-04 audit flagged it, the follow-up refuted "forbidden workaround" but wrote no fuller rationale back | `src/AcDream.Core.Net/WorldSession.cs:484` | Presumed ACE port+1 listener race guard — four words, no citation | Every login eats a flat 200 ms; if the race needs longer on a loaded server, the handshake fails intermittently (ConnectResponse ignored → CharacterList never arrives, exit-29 shape) with no retry — a timing constant masking an unconfirmed root cause | (none recorded) | | UN-7 | Outdoor OBJECT point lighting uses `calc_point_light` (wrap/norm + per-channel cap, `~1/d²`) for ALL meshes including static buildings, but retail's object path is unconfirmed — `config_hardware_light` (0x0059ad30) sets D3D-FF point lights (`Diffuse=color×intensity`, `Attenuation=(0,1,0)`⇒`1/d`, `Range=falloff×1.5`, `material.diffuse=white`) yet that math would blow walls WHITE while retail stays DIM, so static buildings may instead use the `SetStaticLightingVertexColors` bake. Model + the brightness-scaling factor both UNRESOLVED (issue #140 / Fix D) | `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (`pointContribution`); `src/AcDream.Core/Lighting/LightManager.cs` (`SelectForObject`) | Fix A/B ported calc_point_light + per-object selection for objects without confirming retail uses that model for static buildings; cdb captured the D3D-FF path but it contradicts the observed dim result | Outdoor buildings blow out warm near torches (the #140 meeting-hall symptom); whichever model is wrong, the object torch contribution is too strong | `config_hardware_light` 0x0059ad30; `SetStaticLightingVertexColors` 0x0059cfe0; `rangeAdjust=1.5` 0x00820cc4 — see docs/research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md | +| CT-1 | Transcript truncation uses ONE character threshold (10,000) where retail uses two — it beheads to ~7,500 (`0x1D4C`) on passing 10,000 (`0x2710`), so its buffer oscillates between the two. acdream also cuts at whole LINES rather than searching for a newline near a byte offset | `src/AcDream.App/UI/Layout/ChatTranscriptRenderer.cs` (`MaxTranscriptCharacters`, `FirstLineWithinBudget`) | Retail's hysteresis exists to avoid re-trimming an ACCUMULATING buffer on every append; we rebuild the visible list from the log each time, so there is nothing to damp and a second threshold would only make the oldest visible line jump around as messages arrive. Whole-line cutting is what retail's newline preference is trying to achieve — our unit already is the line | acdream shows up to ~2,500 characters more scrollback than retail at the moment retail has just trimmed. Visible only as a slightly longer history; no state, wire or memory effect (ChatLog's own entry cap still bounds the model) | `ChatInterface::TruncateChatLog @0x004F4290`; threshold read at `RecvNotice_DisplayFinalStringInfo @0x004F4640` | +| CT-2 | No client-side chat word filtering. Retail runs every transcript line through a taboo table when the `FilterLanguage` option is on and SUBSTITUTES matches; acdream performs no substitution at all. The option itself is kept and still stores/ships its bit to the server exactly as retail does | `src/AcDream.Core.Net/GameEventWiring.cs` (no filter in the AddText path); option at `src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs` | DELIBERATE PRODUCT DECISION by the user, 2026-08-21: "I do not want any censoring." Not an oversight and not a porting gap | A player who enables FilterLanguage expecting retail's behaviour sees unfiltered text. No state, wire or server-visible effect — the option bit is still sent, so anything the SERVER gates on it behaves normally | `PlayerModule::FilterLanguage` + `TabooTableAdaptor::CheckCensorsW @0x00682A30` inside `ClientSystem::AddTextToScroll @0x00563C50`; matching at `TabooTable::CreateCheckString @0x00681570` / `StringMatchesFilter @0x00681600` | +| CT-3 | A media `Pause` step holds for its `MinDuration`; retail authors a min AND a max and acdream ignores the max. Every sequence measured so far sets them equal, so nothing shipped is affected | `src/AcDream.App/UI/Layout/UiMediaSequence.cs` (`Sample`, the `Pause` case) | Whether the range means a random hold, a ramp, or a min-with-a-frame-budget ceiling is NOT determinable from the decomp, and picking one would be a guess dressed as a port. Using the min is the one reading that is right in every interpretation for the equal-valued case we can actually observe | A sequence authoring min != max would animate faster than retail. None does in the elements dumped so far; if one is found, the reading has to be measured before it is implemented | `MediaDescPause` in the LayoutDesc dat; playback at `UIElement::AnimateMedia` | +| CT-4 | A media `Jump`/`State` step with a probability below 1 FALLS THROUGH rather than branching; retail rolls for it | `src/AcDream.App/UI/Layout/UiMediaSequence.cs` (`Sample`) | The roll's distribution and its re-roll cadence (per visit? per state entry?) are not in the decomp. Falling through is the conservative direction: a sequence that ends early stops animating, where treating it as certain would animate forever and could pin a state that never hands off | A probabilistic sequence plays its deterministic tail instead of its branch. The chat indicator authors p=1 throughout, so it is exact there | `MediaDescJump{Probability}` / `MediaDescState{Probability}` in the LayoutDesc dat | +| CT-5 | A bare `@log` filename lands in the client's own log directory (`ApplicationPathSet.LogsDirectory`), not the install directory retail names ("a log file named Aclog.txt in your Asheron's Call directory"). Rooted paths are honoured verbatim, as retail's `fopen` would | `src/AcDream.App/Net/LiveSessionRuntimeFactory.cs` (`_chatLogDirectory`); `src/AcDream.Core/Chat/ChatSessionLog.cs` | acdream's launcher replaces the install directory atomically on update, so a log written there is wiped by the next update or blocks it outright. Retail had no updater with that property. The client's own data directory is the equivalent that survives | A player following retail-era instructions looks for the file next to the executable and does not find it. The `/log` reply names the file, not the directory, so the path is discoverable only from this row and the code | `ClientCommunicationSystem::StartCopyOutputToFile @0x0057C8A0`; help text at `DoSetOutputHelp @0x0057A950` | +| CT-6 | The `@log` file records the composed line WITHOUT retail's inline text-tag markup. Retail's `fprintf` runs before glyph parsing, so its logs contain literal `` markers around tagged names | `src/AcDream.App/UI/ChatTranscriptLogWriter.cs` | acdream never puts markup in the line: `ChatVM` carries tags as SPANS beside the text (CT-A2/A3), so there is no markup at that seam to preserve. Reconstructing it purely to write it to a file would be inventing a string the client does not otherwise produce | A log diffed against a retail-era log differs on tagged lines — acdream's are the clean ones. No in-client effect | `ClientSystem::AddTextToScroll` write at `@0x00563E5B`, upstream of `UIElement_Text::InqGlyphs @0x00468EA0` | +| QJ-1 | The per-character journal file lives in the client's own data directory (`{data}/journal/Journal-{server}-{character}.txt`), not beside the executable where retail's sits | `src/AcDream.App/UI/JournalPersistence.cs`; path composed in `InteractionRetainedUiComposition` | Identical reasoning to CT-5: acdream's launcher replaces the install directory atomically on update, so a journal written there is destroyed by the next update. The file NAME follows retail's own `"%s%s-%s-%s.txt"` pattern exactly | A player migrating a retail journal must copy the file rather than find it picked up in place. No in-client effect | `gmJournalUI::LoadPages @0x00496AC0` / `SavePages @0x00497270` | +| QJ-2 | Authored button property `0x0D` is ignored. Retail's `UIElement_Button::UpdateState_ @0x00471CF0` reads it and selects the Ghosted visual state when set; acdream reads it for neither input nor appearance | `src/AcDream.App/UI/UiButton.cs` (constructor) | It CANNOT mean input-disabled: measured across every installed layout, 85 elements author `0x0D` and all 85 author it TRUE, never False, and no panel clears it (the only `SetAttribute_Bool(.., 0xd, ..)` sites are chargen appearance, the keymap option and the barber). Reading it as "disabled" made every Journal-panel button visible-but-unclickable. Nor can it be a pure ghosted LOOK: the same 85 include live buttons (New, Record, Start, Delete, Reset) alongside inert column headers, so one appearance cannot suit both | If `0x0D` turns out to drive appearance, the affected elements render un-ghosted where retail greys them — 85 elements, mostly column headers. No input or state effect | `UIElement_Button::OnSetAttribute @0x00471F40` case 0; `UpdateState_ @0x00471CFC` | --- diff --git a/docs/architecture/worldbuilder-inventory.md b/docs/architecture/worldbuilder-inventory.md index a449b22b..b3d5ec59 100644 --- a/docs/architecture/worldbuilder-inventory.md +++ b/docs/architecture/worldbuilder-inventory.md @@ -22,7 +22,7 @@ particles), and uses the same Silk.NET + .NET stack we target. our tree (see CLAUDE.md for the full breakdown): - `src/AcDream.Core/Rendering/Wb/` — pure helpers (no GL): `TerrainUtils`, `TerrainEntry`, `RegionInfo`, `SceneryHelpers`, `TextureHelpers`. -- `src/AcDream.App/Rendering/Wb/` — GL infrastructure + mesh pipeline: +- `src/AcDream.App/Rendering/Wb/` — Vulkan/RHI infrastructure + mesh pipeline: `ObjectMeshManager`, `WbMeshAdapter`, `WbDrawDispatcher`, texture cache, shader infra, EnvCell/portal/scenery/terrain-blending pipeline classes. @@ -42,7 +42,7 @@ non-render content plus explicit bake/equivalence tooling. **MP1a (2026-07-05): CPU mesh-extraction half moved to `AcDream.Content`.** The GL-free portion of the former `ObjectMeshManager` — dat read → polygon -walk → vertex/index build → inline BCn/palette texture decode → +walk → vertex/index build → palette/conditional BCn texture decode → `ObjectMeshData` — is now `MeshExtractor` in a new `src/AcDream.Content/` assembly (no Silk.NET dependency), so the MP1b bake tool can run the exact same extraction code offline without an OpenGL context. This was a @@ -84,7 +84,7 @@ behavior change, no divergence-register row. survive, as they always did). The MP1b bake tool passes its own collector. - **Stays in `src/AcDream.App/Rendering/Wb/`:** `ObjectMeshManager` (the - staged-queue/worker-pool/Dispose-quiesce lifecycle and all GL upload; + staged-queue/worker-pool/Dispose-quiesce lifecycle and all Vulkan/RHI upload; production workers now consume `IPreparedAssetSource`), `ObjectRenderData`/`ObjectRenderBatch` (hold a GL `TextureAtlasManager` field), `TextureAtlasManager`, @@ -120,9 +120,8 @@ EnvCell, Surface, palette, and texture graphs during portals. The validated machine-local `acdream.pak` is opened through Content's `IPreparedAssetSource`; typed GfxObj and EnvCell requests deserialize immutable `ObjectMeshData` while retaining the existing App worker, staging, render-thread -upload, cache, ownership, and shutdown contracts. The original -format-1/bake-tool-3 render payload persists exact batch translucency so App -does not reconstruct a +upload, cache, ownership, and shutdown contracts. The prepared render payload +persists exact batch translucency so App does not reconstruct a `GfxObjMesh` for metadata. Setup activation uses the package TOC as an explicit type-presence index before reading valid Setup records through the bounded DAT cache. `DatPreparedAssetSource` and `MeshExtractor` remain explicit @@ -132,8 +131,39 @@ bake/equivalence/UI-Studio tools, not a production fallback. Portal → HighRes installed-DAT gates are recorded in `docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md`. -**Slice I3 prepared collision extension (2026-07-25).** The package remains -format 1 and retains mesh type values 1–3; bake-tool 4 appends typed GfxObj, +**Launcher cumulative-overlay extension (2026-08-25).** Production still has +no live-DAT fallback and consumes the same prepared-payload contracts. For a +bounded recipe migration, App and Headless may receive one complete base pak +plus one cumulative filtered pak through `LayeredPreparedAssetSource`. The +overlay is probed first: Missing falls through to the base, while a present but +corrupt render or collision payload remains authoritative corruption. Both +mapped owners share one composite lifetime and there is never an overlay +chain. The launcher binds the overlay to the base digest in the optional +`pak/content.current.json` sidecar; format/global extraction migrations retain +the explicit full-rebuild path. A tiny `pak/content.client-pending` marker +keeps either result non-launchable until the matching client is confirmed, +including across a crash/restart. Design and gates: +`docs/plans/2026-08-25-launcher-content-stabilization.md`. + +**PAK v2 resource closeout (2026-08-27).** Format 2 / bake recipe 6 retains +the fixed header and sorted random-access TOC while moving texture arrays into +one globally shared type-8 payload partition and independently applying +adaptive Brotli compression with raw fallback. Unedited DXT1/3/5 surfaces +retain exact DAT BC blocks through Vulkan upload; clip maps and authored +translucency continue through the RGBA edit path. The reader bounds decoded +blobs and retains shared texture arrays in a 64 MiB / 1,024-entry LRU. The +complete installed package is 597,229,424 bytes versus 29,908,271,024 bytes +for format 1; four- and nine-worker bakes have identical SHA-256. The +authoritative connected before/after route reduced heavy-route working set by +48.0%, private bytes by 26.1%, and prepared-mesh GPU bytes by 25.9% with equal +or faster matching reveal/frame percentiles. Format changes remain mandatory +launcher-confirmed full rebuilds with beside-active validation and atomic +promotion; overlays never cross a format boundary. Design and evidence: +`docs/plans/2026-08-27-pak-v2-resource-campaign.md`. + +**Slice I3 prepared collision extension (2026-07-25).** At its introduction, +the package remained format 1 and retained mesh type values 1–3; bake-tool 4 +appended typed GfxObj, Setup, CellStruct, and EnvCell-topology collision payloads. Core owns the immutable flat records and deterministic raw-DAT flattener. Content owns the strict little-endian codec and `IPreparedCollisionSource`. diff --git a/docs/ci-and-releases.md b/docs/ci-and-releases.md new file mode 100644 index 00000000..0155696f --- /dev/null +++ b/docs/ci-and-releases.md @@ -0,0 +1,150 @@ +# Continuous integration and alpha releases (Gitea) + +Single source of truth for how acdream builds, gates, and ships alpha builds. +Landed 2026-08-19. Companion to [`release-gate.md`](release-gate.md), which +owns the *local* bounded gate. + +## What happens on a push to main + +``` +git push origin main + │ + ├─ windows-gate (RARE-win) build + full lane-filtered suite + ├─ linux-portable (eriktestLinux) portable closure, Linux lanes + │ + └─ release (needs BOTH green) publish a Gitea Release + + republish the `latest` pointer +``` + +Workflow: [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml). Docs-only +pushes (docs/, the memory trees, markdown) skip the pipeline entirely — no +test can fail on them and a run costs ~7 minutes plus a 121 MB release. A red gate +cannot publish: `release` uses `needs:`, not a `workflow_run` trigger, whose +Forgejo support is unreliable. + +## Why Gitea and not GitHub + +GitHub Actions is **billing-blocked** on this account ("recent account payments +have failed"), and the repo is private, so hosted runners consume paid minutes. +Forgejo ships **no hosted runners at all**, so Actions there requires +self-hosted ones — which are free on both platforms. The same two machines can +serve GitHub later by registering a second agent; only the workflow's +`runs-on` labels change. + +## The runners + +| | Windows | Linux | +|---|---|---| +| Host | `RARE` (10.6.0.3) | `eriktestLinux` (10.0.0.202) | +| Agent | `act_runner` 0.2.13 | `forgejo-runner` 13.0.0 | +| Persistence | Scheduled task `ForgejoRunner`, at logon of `acbot` | systemd `forgejo-runner`, `Restart=always` | +| Labels | `windows`, `windows-latest`, `windows-x64` | `ubuntu-latest`, `ubuntu`, `linux`, `ubuntu-slim` | +| Execution | host mode (`:host`) — no Docker on either box | host mode | + +Both **poll outbound** over HTTPS. Gitea never connects to them, so no inbound +ports, no port forwarding, and no static IP; they work behind NAT. The runner +does not have to live next to the Gitea container (which runs on `bluesnake`, +a host we have no shell on). + +`forgejo-runner` publishes **no Windows binary in any release**, which is why +Windows uses Gitea's `act_runner`. Forgejo speaks the same Actions protocol. + +### Prerequisites on a runner + +- **.NET SDK in the `global.json` band** — currently `10.0.3xx`. `10.0.400` is a + different feature band and `rollForward: latestPatch` rejects it. +- **Node.js** — `actions/checkout` and `actions/upload-artifact` are JavaScript + actions. Docker images normally supply Node; in host mode the machine must. +- **Git**, and outbound HTTPS to `git.snakedesert.se`. +- **PowerShell 7** on Windows (`pwsh`); `tools/*.ps1` require it. + +## Releases + +Everything about distribution lives under **Releases** — nothing in git. A build +is ~120 MB, so payloads are release attachments; and the pointer the launcher +polls is itself a release asset, so there is no payload branch, no bot commit on +`main`, and no push that could retrigger the pipeline. + +``` +Release 0.1.0-build. <- the actual build + client-win-x64.zip AcDream.App.exe + acdream-headless.exe + launcher-win-x64.zip acdream-launcher.exe + acdream-bake.exe + manifest.json + +Release latest <- pointer, replaced every publish + manifest.json names the version above and its asset URLs +``` + +The launcher polls the pointer at a URL that never changes +(`ReleaseManifestClient.ProductionManifestUri`): + +``` +https://git.snakedesert.se/erik/acdream/releases/download/latest/manifest.json +``` + +A pointer is needed because **Forgejo has no `/releases/latest/download/` +route** (verified: 404) — unlike GitHub, there is no built-in stable URL for +"the newest release". Publishing it recreates the `latest` tag each time, which +means deleting the old release *and* its tag; the tag outlives its release and +would otherwise block recreation. + +The newest **5** versioned releases are kept and older ones are pruned with +their tags. Each build is ~121 MB of attachments, so retaining every one grew +the server by that much per push — 5 builds had already reached 606 MB. Five is +enough to grab a previous build or bisect a regression while staying bounded. +The `latest` pointer is never pruned; it is the feed, not a build. + +`tools/publish-bin.ps1 -BaseUrl ` builds the payloads; CI +passes the tag's asset base. Running it locally is for inspection only — +publishing is CI's job. + +### Verifying a release + +```powershell +dotnet test tests/AcDream.Launcher.Core.Tests --filter Lane=Live +``` + +`LiveGiteaReleaseInstallTests` installs the advertised client from the real feed +through the production updater — real SHA-256/size verification, extraction, and +atomic activation — then asserts both hosts resolve out of the activated +directory and `current.json` names the installed version. + +## Landmines + +Each of these cost a red pipeline; none was a config typo. Two rows record a +fix that was tried and **disproved** — read those before repeating it. + +| Symptom | Cause | +|---|---| +| `Cannot find: node in PATH` | JS actions need Node on the host in `:host` mode | +| `actions/setup-dotnet` never resolves | `data.forgejo.org` does not mirror it (404). `checkout` and `upload-artifact` **are** mirrored. Self-hosted runners carry the SDK anyway | +| Job "failed" while dotnet processes still run | `run-release-gate.ps1` redirects children to log files, so the step goes silent; Forgejo fails a non-reporting task as a zombie. CI runs `dotnet test` directly so output streams | +| ~40 tests fail on formatted numbers | Runner's `HKCU` locale was `en-SE` (comma decimal): expected `"update:0.25"`, got `"update:0,25"`. `Set-Culture` does **not** reach a scheduled task without a loaded profile — set the registry directly | +| `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` as the locale fix | Too blunt — it breaks tests that legitimately construct a culture. Fix the machine locale instead | +| `FileNotFoundException: client_cell_1.dat` | DAT-dependent tests missing `[Trait("Lane", "InstalledDat")]`. Build machines have no DATs | +| Timing-sensitive test fails only under load | It belongs in `Lane=Timing` (see [`release-gate.md`](release-gate.md)). Do **not** chase these individually: four separate fixes each surfaced a different member of the same family, and serializing `Core.Net` to fix Linux regressed Windows from 1000 passed in 7 s to 999/1000 in 17 s | +| Avalonia "calling thread cannot access this object" in cleanup | `MainWindowViewTests` needs a real desktop session and is `Lane=Manual`. Measured: PASSES on a dev desktop and on the CI Windows box over SSH; FAILS under `act_runner` and on Linux. Serializing the assembly does **not** fix it (tried via `xunit.runner.json` and a compiled-in `CollectionBehavior` attribute), and de-async-ing the test actively causes the failure. The stack shows a compositor being **constructed** during teardown — it is the headless session lifecycle, not parallelism | + +## Do not leave load on a runner + +A stress/diagnostic run left going on a runner competes with CI for the same +machine and makes every job slower and more likely to trip a load-sensitive +test — the exact failures you would then be trying to diagnose. Kill background +work before trusting a timing result: + +```powershell +Get-Process dotnet -ErrorAction SilentlyContinue | Stop-Process -Force # Windows +pkill -9 dotnet # Linux +``` + +Leave `act_runner` / `forgejo-runner` itself alone; killing those unregisters +nothing but stops the machine picking up jobs until it restarts. + +## Culture note + +The `en-SE` discovery is worth remembering beyond CI: config files, numeric +parsing, and the wire are all culture-safe (`System.Text.Json` is invariant by +spec, every `float/double.TryParse` passes `CultureInfo.InvariantCulture`, and +the protocol is binary). Only **diagnostic strings** format with the current +culture, so a European player sees `local=(8,00; 191,00)` in an F3 dump. The +client installs and runs correctly in both the US and Europe. diff --git a/docs/launch-options.md b/docs/launch-options.md new file mode 100644 index 00000000..9decd203 --- /dev/null +++ b/docs/launch-options.md @@ -0,0 +1,384 @@ +# acdream launch options — operator reference + +Every environment variable and command-line argument the acdream client +reads, what it does, and **what else it changes about the run**. + +**This is an operator's reference, not user documentation.** Players never +set these: the launcher owns installation and login, and the in-client +Options panel (F11) owns settings. If a flag here looks like something a +player would want, that is a signal it belongs in the Options panel, not a +signal to document it better. + +## How to use this document + +- **Running the client for yourself?** Read *Production launch* and stop. +- **Taking a measurement?** Read *Production launch*, then read the + *Side effects* column of every flag you are about to set. A flag that + changes what you are measuring is the normal case, not the exception. +- **Adding a flag?** Add its row in the same commit. `LaunchOptionsDocumentationTests` + fails the build otherwise — in both directions, so deleting a read site + without deleting its row fails too. + +### Why the side-effects column exists + +Two flags in this list were believed to be inert and were not: + +- `ACDREAM_AUTOMATION_ARTIFACT_DIR` reads like an output path. It also + constructs a per-frame diagnostics referee that re-enabled a retired + render pass, costing ~6 MB and ~14 ms **every frame** — three days of + performance measurements were silently taxed before anyone noticed + ([#432](ISSUES.md)). +- `ACDREAM_STREAM_RADIUS` reads like a radius knob. It forces the near + radius, only ever *raises* the far radius, and is then silently + discarded by any later quality apply — so a measurement taken with it + set is measuring a window production never uses. + +Assume a flag has a side effect until its row says otherwise. + +## Conventions + +- **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`, + `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 + 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 + one whose test is "is the variable present"). +- **Default** is the behavior when the variable is unset. +- **Kind** is one of: + +| Kind | Meaning | +|---|---| +| `production` | Ordinary configuration; safe in a real run. | +| `measurement` | Profiling/instrumentation. Read the side effects before trusting numbers taken with it on. | +| `automation` | Drives scripted runs; usually implies extra machinery. | +| `permanent-probe` | A diagnostic toggle owned by a subsystem's diagnostics class. Expected to persist. | +| `temporary-probe` | Tied to an open investigation. Deleted with its issue — never build tooling on one. | +| `deprecated` | Superseded. Do not use for new work. | + +--- + +## Production launch + +The canonical connected launch against a local ACE server. PowerShell, +because the DAT path contains an apostrophe: + +```powershell +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +$env:ACDREAM_LIVE = "1" +$env:ACDREAM_TEST_HOST = "127.0.0.1" +$env:ACDREAM_TEST_PORT = "9000" +$env:ACDREAM_TEST_USER = "testaccount" +$env:ACDREAM_TEST_PASS = "testpassword" +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_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` | +| `ACDREAM_CAMERA_COLLIDE` | `=0` disables (anything else/unset = on) | selects whether the chase camera sweeps a 0.3 m collision sphere from head-pivot to eye and stops at the first wall (retail spring-arm) | alters camera position every frame (camera can clip into geometry when disabled); startup-only | true (on) | `CameraDiagnostics.CollideCamera` | +| `ACDREAM_CONFIG_DIR` | `=` | Overrides the resolved config-root directory (`settings.json`, `keybinds.json`) | none beyond redirecting config I/O | Windows: `%APPDATA%\acdream`; Linux: `$XDG_CONFIG_HOME/acdream` or `~/.config/acdream` | `ApplicationPathSet.Resolve` (`ApplicationPathSet.cs:79`), via `IApplicationPathEnvironment` seam | +| `ACDREAM_DATA_DIR` | `=` | Overrides the resolved data-root directory (logs, screenshots, plugins) | none beyond redirecting data I/O | Windows: `%LOCALAPPDATA%\acdream`; Linux: `$XDG_DATA_HOME/acdream` or `~/.local/share/acdream` | `ApplicationPathSet.Resolve` (`ApplicationPathSet.cs:81`), via `IApplicationPathEnvironment` seam | +| `ACDREAM_DAT_DIR` | `=` | Fallback dat-directory when no positional argument is given. App: single read at `Program.cs:58`. Cli: read independently per-subcommand (each subcommand does `args.ElementAtOrDefault(N) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")`) plus once more for the default (no-subcommand) asset-inventory mode at line 152. | Two of the four `Program.cs` line numbers in the raw grep (91, 135) are **not reads** — they're the literal string `ACDREAM_DAT_DIR` inside `Log.Error` usage-text messages, not `GetEnvironmentVariable` calls. Only line 58 is a real read in `AcDream.App`. | none — hard usage error (exit 2) if unset and no positional arg | `Program.cs:58` (App); `Cli/Program.cs:24,35,47,59,71,84,113,125,137,152` (every Cli subcommand) | +| `ACDREAM_DISPLAY_PROTOCOL` | `="auto"` / `"x11"` / `"wayland"` (case-insensitive, trimmed); any other value throws `InvalidOperationException` at startup | Linux-only: forces the GLFW 3.4 platform-init hint (X11 vs Wayland vs auto) before any window is created; ignored entirely on Windows (always `Windows` protocol) | An invalid value is fatal at startup (throws before any window exists), not a silent fallback | unset → auto-detected from `XDG_SESSION_TYPE`/`WAYLAND_DISPLAY`/`DISPLAY`, falling back to GLFW `Automatic` | `GraphicalWindowBackendSelection.Resolve` (`GraphicalWindowBackendSelection.cs:26-58`) | +| `ACDREAM_FAR_RADIUS` | `=` | Overrides preset's `FarRadius` (outer streaming/reveal window, landblocks) | Enlarging changes streaming memory budget and what's resident/rendered — CLAUDE.md: leave unset for measurement/gate runs (same family as legacy `ACDREAM_STREAM_RADIUS`) | preset's `FarRadius` (Low=5, Medium=8, High=12, Ultra=15) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:47`) | +| `ACDREAM_LIVE` | `=1` (exactly the literal string `"1"`) | Core switch: connect to a live ACE server instead of running offline/no-connect. | The 4 non-`RuntimeOptions.cs` line numbers in the raw grep are **all comments or log-message text**, not reads — `SessionStartComposition.cs:39` is inside the string `"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping"`; `Program.cs:126` is inside a `--session-config` override log line; `GameWindow.cs:614,627` are doc comments. The only actual parse is `RuntimeOptions.cs:141`. Requires `ACDREAM_TEST_USER`/`ACDREAM_TEST_PASS` too (`HasLiveCredentials`) or the session silently reports `MissingCredentials` and skips. Forced to effectively-on (LiveMode=true) unconditionally by `--session-config` launches regardless of this var. | `false` | `RuntimeOptions.LiveMode` → `SessionStartComposition.cs` (log text only), `Program.cs:126` (log text only), `GameWindow.cs:614,627` (comments only), consumed for real via `RuntimeOptions.HasLiveCredentials` and `WorldSession`/`GameRuntime` session-start gating | +| `ACDREAM_MAX_COMPLETIONS_PER_FRAME` | `=` | Overrides preset's per-frame streaming-completion throughput cap | Directly changes the streaming admission budget measured by perf/completion gates — do not vary during a measurement run | preset's value (Low=2, Medium=3, High=4, Ultra=6) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:59`) | +| `ACDREAM_MSAA_SAMPLES` | `=` (0/2/4/8) | Overrides preset's MSAA sample count | Changes GPU multisample anti-aliasing (visual + GPU-cost change) | preset's `MsaaSamples` (Low=0, Medium=2, High/Ultra=4) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:48`) | +| `ACDREAM_NEAR_RADIUS` | `=` | Overrides preset's `NearRadius` (inner streaming ring radius) | Changes the streaming/render window size — CLAUDE.md: leave unset for measurement/gate runs | preset's `NearRadius` (Low=2, Medium=3, High=4, Ultra=5) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:46`) | +| `ACDREAM_NO_AUDIO` | `=1` | Suppresses OpenAL device init entirely (headless / driver-broken machines). | Fail-open design: even without this flag, a missing/broken OpenAL driver already makes `IsAvailable=false` and all `Play*` calls no-ops — this flag is the explicit, deliberate version of the same fallback. When set, `LiveSessionWorldRuntime.WorldAudio` is `null` and logout-audio reset/resume steps no-op. | `false` | `RuntimeOptions.NoAudio` → `GameWindow.cs:1430` → `ContentEffectsAudioCompositionPhase` → `OpenAlAudioEngine.cs` (fail-open doc), `LiveSessionRuntimeFactory.cs:71` (`WorldAudio` gate) | +| `ACDREAM_PAK_PATH` | `=` | Overrides the path to the prepared machine-local asset bake (`acdream.pak`) that production world-mesh streaming reads through `IPreparedAssetSource`. | none beyond the obvious | unset → `/acdream.pak` | `RuntimeOptions.PreparedAssetPath` → `ContentEffectsAudioComposition.cs:53,372,379`, `GameWindow.cs:1420` | +| `ACDREAM_PLUGIN_TAGS` | comma-separated tags (maximum 128 tags, 128 characters each) | Advertises machine-local role/group tags through the plugin peer-discovery API, for UtilityBelt-compatible expressions such as client selection by tag. Values are trimmed and deduplicated case-insensitively. | Writes the tags into the bounded local peer heartbeat document while a character is in world; no network traffic leaves the machine. | unset → no tags | `RuntimeOptions.PluginTags` → `AppAutomationSurface` / `LocalPluginPeerRegistry` | +| `ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB` | `=` (`>0`, else default; overflow-checked) | Byte ceiling for the retail alpha (translucency) draw queue's scratch buffer | Shrinking below production working set changes translucency-queue eviction/reflow behavior — not comparable to a default-budget perf run | 16 MiB | `ResidencyBudgetOptions.Parse` (`ResidencyBudgetOptions.cs:88-89`), flows through `RuntimeOptions.ResidencyBudgets`→`AlphaScratchBudgetProfile.Create`→`RetailAlphaQueue` ctor (`GameWindow.cs:721-725`) | +| `ACDREAM_RESIDENCY_ANIMATION_ENTRIES` | `=` (`>0`, else default) | Entry-count ceiling for the retained animation-data cache | Changes cache eviction cadence for animation data — a perf/memory measurement under a non-default value is not representative | 512 | `ResidencyBudgetOptions.Parse` (`:82-84`) | +| `ACDREAM_RESIDENCY_ANIMATION_MIB` | `=` (`>0`, else default) | Byte ceiling for the retained animation-data cache | Same cache-behavior-change caveat as the entries variant | 64 MiB | `ResidencyBudgetOptions.Parse` (`:79-81`) | +| `ACDREAM_RESIDENCY_AUDIO_MIB` | `=` (`>0`, else default) | Byte ceiling for the retained audio-buffer cache | Shrinking can force more frequent audio buffer re-decode/eviction | 32 MiB | `ResidencyBudgetOptions.Parse` (`:85-87`), consumed by `ContentEffectsAudioComposition.cs` | +| `ACDREAM_RESIDENCY_COMPOSITE_PHYSICAL_MIB` | `=` (`>0`, else default) | Byte ceiling for physically-resident composite (character palette/texture) GPU memory | Changes composite-texture eviction pressure — not representative of production if varied during a measurement run | 128 MiB | `ResidencyBudgetOptions.Parse` (`:67-69`), consumed by `TextureCache.cs` | +| `ACDREAM_RESIDENCY_COMPOSITE_UNOWNED_MIB` | `=` (`>0`, else default) | Byte ceiling for unowned/retained (not currently referenced) composite textures kept for reuse | Same eviction-pressure caveat | 64 MiB | `ResidencyBudgetOptions.Parse` (`:70-72`), consumed by `TextureCache.cs` | +| `ACDREAM_RESIDENCY_MESH_GPU_MIB` | `=` (`>0`, else default) | Byte ceiling for GPU-resident object mesh data | The single largest residency budget (1024 MiB default) — shrinking it directly forces more mesh re-upload/eviction; do not vary during an FPS/GPU-memory measurement run | 1024 MiB | `ResidencyBudgetOptions.Parse` (`:49-51`), consumed by `ObjectMeshManager.cs`/`WbDrawDispatcher.cs` | +| `ACDREAM_RESIDENCY_MESH_STAGING_ENTRIES` | `=` (`>0`, else default) | Entry-count ceiling for the mesh upload staging cache | Changes staging-buffer churn/eviction cadence | 256 | `ResidencyBudgetOptions.Parse` (`:64-66`) | +| `ACDREAM_RESIDENCY_MESH_STAGING_MIB` | `=` (`>0`, else default) | Byte ceiling for the mesh upload staging cache | Same staging-churn caveat | 128 MiB | `ResidencyBudgetOptions.Parse` (`:61-63`) | +| `ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES` | `=` (`>0`, else default) | Entry-count ceiling for unowned (retained-for-reuse) object mesh entries | Changes mesh-cache eviction cadence | 50 | `ResidencyBudgetOptions.Parse` (`:52-54`) | +| `ACDREAM_RESIDENCY_PREPARED_MESH_ENTRIES` | `=` (`>0`, else default) | Entry-count ceiling for the CPU-side "prepared mesh" cache (post-classification, pre-upload) | Changes eviction cadence for prepared-mesh CPU memory | 100 | `ResidencyBudgetOptions.Parse` (`:58-60`) | +| `ACDREAM_RESIDENCY_PREPARED_MESH_MIB` | `=` (`>0`, else default) | Byte ceiling for the CPU-side prepared-mesh cache | Same eviction-cadence caveat | 128 MiB | `ResidencyBudgetOptions.Parse` (`:55-57`) | +| `ACDREAM_RESIDENCY_STANDALONE_UNOWNED_ENTRIES` | `=` (`>0`, else default) | Entry-count ceiling for unowned standalone (non-composite) texture entries | Changes standalone-texture eviction cadence | 256 | `ResidencyBudgetOptions.Parse` (`:76-78`), consumed by `TextureCache.cs` | +| `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_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`) | +| `ACDREAM_TEST_USER` | `=` | ACE account name for live-mode connect. | none | `null` (empty → `HasLiveCredentials` false) | `RuntimeOptions.LiveUser` (`RuntimeOptions.cs:144`) | +| `ACDREAM_VULKAN_DEVICE` | `=` (decimal index) or `=` (case-insensitive device-name match) | Overrides automatic Vulkan physical-device selection (normally: discrete > integrated > virtual > CPU, tie-broken by device-local heap size) — for multi-GPU machines. | A bare-digits value is matched as an index ONLY (never falls through to substring match) specifically because digits like `"7"` are substrings of real device names ("AMD Radeon RX 9070 XT") — a fallback would silently select the wrong device by coincidence. An override matching nothing falls back to the automatic choice (does not fail startup) and records why in the capability report. | `null` → automatic ranked choice | `RuntimeOptions.VulkanDeviceOverride` → `VulkanPhysicalDeviceSelection.Choose` (`VulkanPhysicalDeviceSelection.cs:54-100`), consumed at `VulkanGraphicsContext.cs:207,322` | + +## Command-line arguments + +### `AcDream.App` + +| Arg | What it does | Side effects | +|---|---|---| +| `` (positional) | Dat directory; outranks `ACDREAM_DAT_DIR`. | Not read at all once `--session-config` is present. | +| `--session-config ` | The launcher's launch path: endpoint, account, credential reference, character selector, status file, plugins, login commands. | **Overrides `ACDREAM_LIVE` and every `ACDREAM_TEST_*`** (logged at startup). Diagnostic flags stay env-controlled. Missing value is a startup error. | + +### `AcDream.Headless` + +Its usage banner matches the parser exactly. `validate` loads and checks a +config without connecting; `run` connects. + +| Arg | What it does | Side effects | +|---|---|---| +| `validate` \| `run` (positional) | Selects the mode; must be the first argument. | Anything else is a parse error. | +| `--config ` | The versioned headless session-configuration document. Required. | — | +| `--config-dir` / `--data-dir` / `--cache-dir` `` | Override each portable path root. | Merged over the config document's own `process.paths`; the command line wins. | +| `-user` / `--user`, `-password` / `--password` | Direct single-session credentials, bypassing the config's credential source. | Plaintext in the process command line — prefer the config's credential reference. | +| `--help` / `-h` (or no args) | Prints usage, exits 0. | — | + +### `AcDream.Launcher` + +| Arg | What it does | Side effects | +|---|---|---| +| `--verify-publish` | Packaging smoke probe: parses arguments and exits 0 without opening a display or resolving user paths. | — | +| `--config-dir` / `--data-dir` / `--cache-dir` `` | Override each path root. | **All three or none** — supplying a subset is an error. Must be absolute. | +| `--update-manifest-uri ` | Points the self-updater at a different release manifest (test-feed seam). | Must be `https://` (or loopback `http://`). Changes where updates come from — do not point a real install at a test feed. | +| `--acdream-self-update-helper-v1`, `--acdream-self-update-confirm-v1` | Internal re-exec markers for the self-update handoff. | Not user-facing; never pass these by hand. | + +### `AcDream.Cli` + +A dat-dump and measurement tool dispatched by a positional subcommand +(`args[0]`); no `--flag` options. Most subcommands take a dat directory and +fall back to `ACDREAM_DAT_DIR`. + +- **Measurement:** `summarize-frame-history `, + `compare-screenshots [channelTolerance=2] [maxDifferentFraction=0.001] [mask.png]`, + `probe `. +- **Dat inspection:** no subcommand (asset-type inventory), `dump-vitals-bars`, + `dump-vitals-layout [0xLayoutId]`, `list-ui-layouts [0xRootType]`, + `dump-sprite-sheet <0xId,...>`, `dump-font-atlas [0xFontId] [sample] [outBase]`, + `dump-edges <0xId>`, `export-ui-sprite <0xId> [out.png]`. +- **Mockup rendering:** `render-vitals-mockup [out.png]`, `mock-selbar [out.png]`, + `crop `. + +## Measurement and profiling + +| Flag | Value | What it does | Side effects | Default | Read by | +|---|---|---|---|---|---| +| `ACDREAM_CAPTURE_RESOLVE` | `=` | appends one JSON-Lines record (full before/after `PhysicsBody` snapshot) per player-side `ResolveWithTransition` call, filtered to `IsPlayer` movers | real per-tick allocation (snapshot object graph + `System.Text.Json` serialize) and buffered file I/O (`AutoFlush=false`) for the local player only; will skew any perf measurement of local-player physics while active; feeds `CellarUpTrajectoryReplayTests` fixtures | unset (off) | `AcDream.Core.Physics.PhysicsResolveCapture` (`CapturePath`) | +| `ACDREAM_COLLISION_SHADOW_DIR` | `=` | output directory for Slice I5 graph/flat collision-shadow mismatch artifacts | only takes effect when `ACDREAM_COLLISION_SHADOW_EVERY>0`; directory creation + file writes on mismatch | `/.test-out/collision-shadow` | `PhysicsDiagnostics.CollisionShadowArtifactDirectory` | +| `ACDREAM_COLLISION_SHADOW_EVERY` | `=` | when >0 and the cache is constructed with `requirePreparedCollision:false`, arms a `CollisionShadowVerifier` that re-runs the graph-vs-flat collision referee every Nth traversal entry (`PhysicsDataCache` ctor) | extra CPU on sampled ticks + mismatch-artifact file I/O; graph path stays authoritative regardless of mismatch (doc-asserted, not independently verified here) — does not change production physics results, but does add work when active | `0` (disabled) | `PhysicsDiagnostics.CollisionShadowSampleEvery` (parsed via `ParsePositiveInt`, non-positive → 0) | +| `ACDREAM_DAY_GROUP` | `=` | Forces Dereth's day-group (weather preset) selection instead of the retail hash-based pick, "useful for visually A/B-testing each weather preset against retail" (own doc comment). | **Dead second read**: the `SkyDescLoader.cs:252` raw read only feeds `SelectDayGroupIndex`, which is only called from `ActiveDayGroup(double)` and the `DefaultDayGroup` property — and grepping all of `src/` finds **zero production call sites** for either. That whole path is unreachable; only the typed `RuntimeOptions.ForcedDayGroupIndex` → Runtime path is live. Bounds differ too: the typed path only checks `>= 0` (`TryParseNonNegativeInt`) and Runtime clamps out-of-range to `null`; the dead Core-layer path checks `forced >= 0 && forced < DayGroups.Count` directly. `SkyState.cs:400,403` are doc-comment mentions only, not reads. | unset → normal server/date-driven hash selection | `RuntimeOptions.ForcedDayGroupIndex` (typed) → `GameWindow.cs:718` → `WorldEnvironmentController` → `RuntimeWorldEnvironmentState` (Runtime, live path); **also** raw `Environment.GetEnvironmentVariable` at `SkyDescLoader.cs:252` (Core layer, separate parse) | +| `ACDREAM_DISABLE_TIER1_CACHE` | `="1"` (ordinal exact match; anything else = enabled) | A/B diagnostic that forces **every** static (non-animated) entity through the slow per-entity classification path, bypassing the Tier-1 classification cache (`#53`) | Materially changes per-frame CPU cost for entity classification — a perf/FPS measurement taken with this set is NOT representative of production and must not be compared against a normal run | unset (cache enabled) | `WbDrawDispatcher` ctor field `_tier1CacheDisabled` (`WbDrawDispatcher.cs:473-474`) | +| `ACDREAM_FRAME_HISTORY` | `=` | opts into a per-frame CSV history capture (frame idx, timestamps, per-stage CPU us, GPU us, alloc bytes) alongside the aggregated 5 s `[frame-prof]` report | allocates a `List` with ~131,072-record (~9 MiB) initial capacity, growing further for longer captures (~72 B/record, ~43 MB/hour at 165 fps) held in memory for the whole run; CSV write happens ONLY at `Dispose`/shutdown (no frame-thread I/O); only takes effect while `ACDREAM_FRAME_PROF` is ALSO on | unset (off) | `RenderingDiagnostics.FrameHistoryPath` / `AcDream.App.Diagnostics.FrameProfiler` | +| `ACDREAM_ORBIT_DISTANCE_METERS` | `=`, must be finite and `>0` | Diagnostic-only initial distance for the offline orbit camera, so deterministic renderer acceptance captures land inside a finite shadow reach. | Own doc comment: "used by deterministic renderer acceptance captures." Rejects non-finite/non-positive values silently (parses to `null`, camera default used). | `null` (unset) → normal camera default | `RuntimeOptions.InitialOrbitDistanceMeters` → `GameWindow.cs:1412` → offline orbit-camera composition | +| `ACDREAM_ORBIT_PITCH_DEGREES` | `=`, clamped `[-89, 89]` | Diagnostic-only initial orbit camera elevation. | Values outside `[-89,89]` or non-finite are silently rejected (→ `null`, default kept) rather than clamped. | `null` | `RuntimeOptions.InitialOrbitPitchDegrees` → `GameWindow.cs:1414` | +| `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_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`) | +| `ACDREAM_STREAM_WORK_ENTITY_OPS` | `=` (`>0`, else default) | Per-frame ceiling on entity-cursor operations (small ops, e.g. one dictionary/index write each) on the update thread | A/B-measurement-only family. Doc comment: elapsed-time ceiling (`ACDREAM_STREAM_WORK_MS`) remains the authoritative CPU guard — this is a secondary cap, deliberately loose (leaves >90% of the time budget unused at default) | 4,096 | `StreamingWorkBudgetOptions.Parse` (`:62-64`) | +| `ACDREAM_STREAM_WORK_GL_RETIRE_OPS` | `=` (`>0`, else default) | Per-frame ceiling on GL/GPU resource-retirement operations on the update thread | A/B-measurement-only family; changes retirement cadence, which changes when GPU memory is actually reclaimed | 64 | `StreamingWorkBudgetOptions.Parse` (`:68-70`) | +| `ACDREAM_STREAM_WORK_GPU_MIB` | `=` (`>0`, else default) | Per-frame ceiling on GPU upload bytes on the update thread | A/B-measurement-only family; directly changes per-frame upload throughput | 8 MiB | `StreamingWorkBudgetOptions.Parse` (`:65-67`) | +| `ACDREAM_STREAM_WORK_HOLD_DEST_MS` | `=` (`>0` and finite, else default `8.0`) | Absolute (not quality-scaled) time ceiling for destination-lane work during a portal/login hold; never shrinks a profile whose own ceiling is already ≥ this value | Explicitly documented as "NOT a user setting... exists for A/B measurement only, matching the rest of the `ACDREAM_STREAM_WORK_*` family" — do not set outside a deliberate hold-latency A/B comparison | 8.0 ms | `StreamingWorkBudgetOptions.Parse` (`:74-76`); `HoldDestinationCeilingMilliseconds` widens the frame meter via `StreamingWorkBudget.WidenForDestinationHold` while a destination reservation hides the world behind the authored tunnel (#418) | +| `ACDREAM_STREAM_WORK_MS` | `=` (`>0` and finite, else default) | Per-frame elapsed-time ceiling for update-thread streaming work — "the authoritative CPU guard" per the entity-ops comment | A/B-measurement-only family; this is the primary per-frame time budget for streaming — changing it changes both perceived streaming latency and measured frame cost | 2.0 ms | `StreamingWorkBudgetOptions.Parse` (`:53-55`) | +| `ACDREAM_UNCAPPED_RENDER` | `=1` | Removes the normal VSync/refresh-rate software pacer, so the render loop runs as fast as the GPU/CPU allow. | Own doc comment (`RuntimeOptions.cs:147-150`): "Normal presentation is always bounded by VSync or a refresh-rate software pacer. This explicit diagnostic is the sole way to measure truly uncapped renderer throughput." Not representative of what a real player experiences — exists purely for throughput measurement. | `false` → VSync/pacer-bounded | `RuntimeOptions.UncappedRendering` → `GameWindow.cs:765` → `DisplayFramePacingController`; also `VulkanBringUpHost.cs:75` | +| `ACDREAM_WB_DIAG` | `=1` (raw `string.Equals` ordinal compare) | (a) `GameWindow`: gates the `[FRAME-DIAG]` render-thread entity-upload-distribution report; (b) `WbDrawDispatcher`: gates `BeginRhiTimer`/`SampleRhiTimers`, wrapping the opaque/detail/transparent draw passes in extra Vulkan GPU timer-scope queries and periodically logging a `[WB-DIAG]` CPU/GPU median/p95 report | adds extra per-pass GPU timestamp queries every frame while on — genuine measurement overhead; NOT read through `RenderingDiagnostics` or any diagnostics-owner class, unlike every other flag in this set — flag for whitelisting (see Notes #2); the flag's supposed interaction with `ACDREAM_FRAME_PROF`'s GPU query is stale documentation (see Notes #1) | unset (off) | read directly at `WbDrawDispatcher.cs:2061-2064` (every `Draw()`/`BeginEntityDispatch` call, i.e. effectively per frame, NOT cached) and cached once as a readonly field at `GameWindow.cs:153-156` | +| `ACDREAM_WORLD_TIME` | `=`, accepted only in `[0, 1)` | Campaign V slice V7 instrument-determinism pin: freezes the Dereth day fraction (and therefore sun direction, sky keyframe, and every lit surface) instead of following the server clock. | Outranks BOTH the server `TimeSync` clock and the `/time` slash command's `SetDebugTime` (which is deliberately transient — the next `TimeSync` clears it); this pin does not clear. Distinct axis from `ACDREAM_DAY_GROUP` (day-group/weather-preset selection) and `ACDREAM_SKY_PHASE_SECONDS` (cloud scroll + foliage wind) — the calendar DATE still advances, only the intra-day fraction freezes. Anything outside `[0,1)` (including negative, unparseable, or unset) leaves the server clock alone entirely — no partial/clamped behavior. | `null` → server clock | `RuntimeOptions.PinnedWorldDayFraction` → `GameWindow.cs:720` → `WorldEnvironmentController` → `Runtime.WorldTime.PinnedDayFraction` | + +## Automation + +A scripted route run adds three things at once — a session config so the +client self-selects a character, a route script, and an artifact directory: + +```powershell +$env:ACDREAM_UI_PROBE_SCRIPT = "$scratch\route.txt" +$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = "$scratch\artifacts" +$env:ACDREAM_FRAME_PROF = "1" +$env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv" +& $exe --session-config "$scratch\session.json" +``` + +**Two traps this recipe exists to document:** + +1. **Without `--session-config`, the client stops at character select** and + the route never runs. The session JSON supplies the endpoint, account, + and a character `index` for auto-selection. +2. **`ACDREAM_AUTOMATION_ARTIFACT_DIR` is not free.** It constructs the + render-scene oracle, which fingerprints every resident entity every + frame. The allocation cost was fixed in + [#432](ISSUES.md), but the CPU walk remains — automation-run frame + rates are diagnostics-loaded and must only be compared against other + automation runs, never against a plain run. Some route verbs + (`wait world-*`) additionally do nothing unless this is set. + +| Flag | Value | What it does | Side effects | Default | Read by | +|---|---|---|---|---|---| +| `ACDREAM_AUTOMATION_ARTIFACT_DIR` | `=` | Output directory for the retail-UI automation probe's checkpoint JSON + screenshot PNG artifacts; gates whether the full `WorldLifecycleAutomationController` (checkpoint/screenshot/render-pack-automation capable) is composed at all vs. the cheaper facts-only `WorldRevealFactsAutomationRuntime` fallback (`wait world-ready/visible` verbs work either way per issue #415's fix; checkpoint/screenshot verbs report "requires ACDREAM_AUTOMATION_ARTIFACT_DIR" without it). | **Known #432 surprise, confirmed still live**: `FrameRootComposition.cs:349-353` — `AutomationArtifactDirectory is not null` (together with `RetainedUi?.Screenshots is not null`) unconditionally constructs a `CurrentRenderSceneOracle` **and** a `RenderSceneShadowComparisonController` — a per-frame diagnostics referee — regardless of whether any checkpoint/screenshot is ever actually requested that session. Merely setting this var for its "just an output path" purpose pays the per-frame comparison cost for the whole run. | unset (null) → facts-only automation runtime, no per-frame referee constructed | `RuntimeOptions.AutomationArtifactDirectory` → `FrameRootComposition.cs:351,543-627`, `WorldLifecycleAutomationController.cs`, `RetailUiAutomationScriptRunner.cs:108` | +| `ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER` | `=1` | Forces the graphical host to use the persisted display resolution as the *initial* size of a **borderless** window at creation, so the OS window manager cannot clamp a decorated window to the desktop work area — needed for pixel-exact automated screenshot comparison. | Changes window chrome (borderless) at startup — a visible difference from an ordinary launch, not just an internal measurement knob. | `false` → normal decorated window | `RuntimeOptions.ExactAutomationFramebuffer` → `GameWindow.cs:852` (`CreateStartupWindowOptions`) | +| `ACDREAM_BAKE_PUBLISH_NONCE_V1` | `=<32-hex GUID "N" format>` | Launcher-to-bake-child authorization token: when present and valid, the bake child takes a cross-process publish file lock + writes an authorization file before atomic publication (serializes with launcher recovery) | If present but fails `IsValidNonce` (not a 32-char Guid "N"), throws `InvalidOperationException` and aborts the bake. When absent, bake runs unguarded (standalone mode). Never set this manually outside the launcher's own child-process spawn. | unset (standalone unguarded bake) | `BakePublicationGuardPaths.cs:12`, read by `BakePublicationGuard.AcquireIfRequested` (`AcDream.Bake/BakePublicationGuard.cs:18`); set by `BakeProcessRunner.cs:150/162` | +| `ACDREAM_NET_DROP_DIR` | `="out"`/`"in"`/anything-else (incl. unset) → `Both` (case-insensitive) | Selects which direction(s) — outbound, inbound, or both — the deterministic loss-injection decorator drops | Only takes effect when `ACDREAM_NET_DROP_PCT>0` (decorator is structurally absent otherwise). Drives real datagram loss on the live connection — the injection point for `tools/run-connected-loss-gate.ps1`. Never set during a normal/measurement run. | `Both` | `NetDiagnostics.NetDropDir` (`NetDiagnostics.cs:88-90,98-104`), consumed by `LossyTransportDecorator.WrapIfConfigured` (`Transport/LossyTransportDecorator.cs:21-22`), also read at `WorldSession.cs:901-907` (comment only) | +| `ACDREAM_NET_DROP_PCT` | `=` (out-of-range or unparsable → 0) | Percent chance (post-handshake-arming, per droppable datagram) that the deterministic `LossyTransportDecorator` drops a packet in the configured direction(s) | **Fault injection.** `>0` wraps the real socket transport in a packet-dropping decorator for the whole session — genuinely breaks/delays delivery to exercise N1-N4 reliable-transport recovery. At 0 the decorator is never constructed (zero structural cost). Must be 0/unset for any normal run or non-loss-gate measurement. | `0` (off, decorator absent) | `NetDiagnostics.NetDropPercent` (`NetDiagnostics.cs:60-69,92-96`), consumed by `LossyTransportDecorator.WrapIfConfigured` (`:21`), wired at `WorldSession.cs:901-907` | +| `ACDREAM_NET_DROP_SEED` | `=` (unparsable → `1`) | PRNG seed for the loss decorator (outbound seeded with `seed`, inbound with `~seed`) — same seed reproduces an identical drop pattern | Only matters when `ACDREAM_NET_DROP_PCT>0`; makes fault injection deterministic/reproducible for the connected loss gate | `1` | `NetDiagnostics.NetDropSeed` (`NetDiagnostics.cs:75-82`), consumed by `LossyTransportDecorator` (`:21-22`) | +| `ACDREAM_OPEN_CHARGEN` | `=1` | Campaign CC slice CC4 interim env/test-only seam: opens the character-creation screen (`gmCharGenMainUI`) automatically once Runtime's chargen view goes active, bypassing the real retail Create-Character-button transition. Fires once per mount (`_openOnStartConsumed` latch). | Own doc comment explicitly calls this "interim env/test-only" — Campaign CC (closed 2026-08-16, user-accepted) later wired the real Create button with its roster<55-slot ghost gate, so this flag is now a bypass of that gate for automation/testing rather than the only way in. | `false` | `RuntimeOptions.OpenCharacterCreationOnStart` → `CharacterCreationUiController.cs:21,524-530` | +| `ACDREAM_UI_PROBE_DUMP` | `=1` | Enables the retail-UI automation probe's diagnostic dump path and feeds `RetailUiProbeBindings`/`RetailUiAutomationScriptRunner`. Also part of `RuntimeOptions.UiProbeEnabled` (`UiProbeDump \ | \ | UiProbeScript is set`). | `RuntimeOptions.UiProbeDump` → `LivePresentationComposition.cs:1465-1495`, `InteractionRetainedUiComposition.cs:1092-1100` | +| `ACDREAM_UI_PROBE_SCRIPT` | `=` | Path to a script file the `RetailUiAutomationScriptRunner` executes against the retained UI (pointer/semantic-input command playback) for scripted UI regression testing. | Also flips `RuntimeOptions.UiProbeEnabled` true even without `ACDREAM_UI_PROBE_DUMP=1`. | `null` | `RuntimeOptions.UiProbeScript` → `InteractionRetainedUiComposition.cs:1094` | +| `ACDREAM_VULKAN_FORCE_UNSUPPORTED` | `=` (case-insensitive property name, e.g. `MultiDrawIndirect`) | Test knob (Slice V5): clears one named required Vulkan feature from the capability record to synthetically fail the gate, so the `NotSupportedException` → exit-code-4 → report path can be exercised on hardware that actually supports everything. | Deliberately breaks Vulkan startup when set to a matched feature name — this is a "make it fail on purpose" gate-testing flag, never appropriate for a normal or measurement run. | `null` → real capabilities used unmodified | `RuntimeOptions.VulkanForcedUnsupportedFeature` → `VulkanCapabilityRecord.Without` (`VulkanCapabilityRecord.cs:113-119`), consumed at `VulkanGraphicsContext.cs:339` | +| `ACDREAM_VULKAN_PROBE` | `=1` | Runs the standalone Vulkan capability-probe/bring-up harness (opens its own window, runs the capability gate, presents synthetic V6c/V6d verification scenes, captures one screenshot) **instead of** the real client composition host, then exits. | This flag ALONE gates entry (`GameWindow.cs:828`); the former `ACDREAM_RENDER_BACKEND=vulkan` co-requisite died with the OpenGL backend (its class doc was corrected 2026-08-24). | `false` → normal composition host | `RuntimeOptions.VulkanCapabilityProbe` → `GameWindow.cs:828` → `VulkanBringUpHost` | +| `ACDREAM_VULKAN_PROBE_FRAMES` | `=` (non-negative) | Bounds the bring-up probe harness to N presented frames so it can run unattended in CI, instead of presenting until a human closes the window. | The frame budget never cuts a pending screenshot capture short — the loop stays open until the screenshot has been attempted even past the budget, so an unattended run's whole product (a PNG) is guaranteed. Zero (unset/unparseable/explicit `0`) keeps the interactive wait-for-close behavior. | `0` → interactive (wait for window close) | `RuntimeOptions.VulkanCapabilityProbeFrames` → `VulkanBringUpHost.cs:141-249` | +| `ACDREAM_DUMP_MOVE_TRUTH` | `=1` | Emits one `move-truth OUT` line per outbound movement record (MoveToState / AutonomousPosition): local resolved position vs the wire position/cell, ground contact, velocity (`MovementTruthDiagnosticController`). | **Automation apparatus, NOT a spent probe** — the canonical nine-stop soak (`tools/run-connected-r6-soak.ps1`) hard-gates on ≥2 of these lines per destination as its proof that production input produced outbound movement traffic; deleting it fails the soak at every stop (#437, deleted-and-restored 2026-08-24). Print volume follows the outbound send cadence. | off | `RuntimeOptions.DumpMoveTruth` → `GameWindow.cs` → `MovementTruthDiagnosticController` | + +## Permanent diagnostics + +| Flag | Value | What it does | Side effects | Default | Read by | +|---|---|---|---|---|---| +| `ACDREAM_CAPTURE_PLAYER_QUANTA` | `=` (any non-whitespace path) | Opt-in JSON-Lines trace of every admitted player physics quantum (position/orientation/velocity/contact-plane snapshots at each stage boundary of `CPhysicsObj::UpdateObjectInternal`) | Appends+flushes one JSON line per physics quantum to the file (real file I/O on the physics tick when enabled); disabled path costs one static string null/empty check, no allocation. Read once into a mutable static property (settable via `ResetForTest`) rather than a typed options object. | unset (disabled, zero-alloc) | `PlayerPhysicsQuantumCapture` static class (`AcDream.Runtime/Gameplay/PlayerPhysicsQuantumCapture.cs:22`) | +| `ACDREAM_DUMP_MOTION` | `=1` | prints `UM`/`[UM_STALE]`/`[MOTIONDONE]`/`VU.land`/raw-hex wire dump lines tracing inbound `UpdateMotion` handling, remote ground-contact edges, and motion-done callbacks (bug-a/#32 stuck-cast subthread is temporary; core trace is long-lived) | print-only, but the raw-site reads in `LiveEntityNetworkUpdateController.cs` and `UpdateMotion.cs` fire on EVERY inbound motion/UM event (not cached) — `Environment.GetEnvironmentVariable` call per packet even when off; `UpdateMotion.cs`'s branch additionally builds a `StringBuilder` hex dump when on. Rule-5 violation (raw reads outside a diagnostics-owner class) at 5+ call sites | off | THREE independent readers: `PhysicsDiagnostics.DumpMotionEnabled` (owner, appears unconsumed — see Notes), `AnimationPresentationDiagnostics.FromEnvironment()` (App owner record, cached once at startup, consumed by `LiveEntityAnimationPresenter`), and raw `Environment.GetEnvironmentVariable` reads scattered across `LiveEntityNetworkUpdateController.cs` (4 sites) + `Core.Net/Messages/UpdateMotion.cs:163` + `Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:630` | +| `ACDREAM_DUMP_PLAYSCRIPT` | `="1"` (ordinal) | Traces PhysicsScript playback: missing/empty script resolution, malformed `StartTime` entries, and other `[pes]`-prefixed hook-dispatch events | print-only (`Console.WriteLine`) at all 4 use sites (`:85-86,136,300,328`) | unset (off) | `PhysicsScriptRunner.DiagEnabled` (`PhysicsScriptRunner.cs:61-62`) — per-instance settable property seeded from the env var, not a shared static diagnostics-owner class | +| `ACDREAM_DUMP_SURFACES` | `="1"` (ordinal) | One-shot (per session) surface-format histogram dump for the atlas-opportunity audit — fires once after `_dumpFrameCounter>=600` OnRender ticks AND `_uploadMetadata.Count>=100` uploaded textures; writes to the host diagnostics directory | Doc comment claims "Zero cost when off" but `_uploadMetadata[name]=(w,h,fmt)` (`TextureCache.cs:1042`) is written **unconditionally on every texture upload regardless of the flag** — real (small) always-on dictionary-write cost. `TickSurfaceHistogramDumpIfEnabled` also re-reads `Environment.GetEnvironmentVariable` every OnRender frame (not cached) until the one-shot fires. Dump-write failures are caught and logged to stderr, not fatal. | unset (off) | `TextureCache` (`TextureCache.cs:102-113` fields, gate at `TextureCache.cs:802-812`, dump at `TextureCache.cs:814-829`), Phase N.6 slice 1 | +| `ACDREAM_FRAME_PROF` | `=1` | master toggle for the frame profiler: CPU frame time, GPU time samples, per-stage CPU attribution, per-frame alloc/GC, `[frame-prof]` report every ~5 s (doc: "permanent apparatus ... do not strip with session probes") | when on, samples `GC.GetAllocatedBytesForCurrentThread()` and stage-scope timing every frame (cheap, by design); its own XML doc claims a GPU-query self-disable tied to `ACDREAM_WB_DIAG=1` that `FrameProfiler.cs` says no longer exists — see Notes #1; startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.FrameProfEnabled` / `FrameProfiler` | +| `ACDREAM_PROBE_ENVCELL` | `=1` | emits one `[envcells]` line per indoor frame: `CellsRendered`/`TrianglesDrawn` + ourBldgs/otherBldgs/filter counts (phase a8 relic; its own render pass was removed but the probe was kept) | print-only; implicitly turned on whenever `ACDREAM_PROBE_VIS` is on (getter is `_probeEnvCellEnabled \ | \ | `RenderingDiagnostics.ProbeEnvCellEnabled` (backing field OR'd with `ProbeVisibilityEnabled`) | +| `ACDREAM_PROBE_INDOOR_ALL` | `=1` | master switch that reads as AND / writes as cascade across Walk, Lookup, Upload, Xform, Cull | print-only (every underlying probe is print-only); startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.IndoorAll` (cascades to the 5 flags below) | +| `ACDREAM_PROBE_INDOOR_CULL` | `=1` (also set by `ACDREAM_PROBE_INDOOR_ALL=1`) | emits `[indoor-cull]` per culled cell entity with cull reason (visibleCellIds-miss / frustum / landblock) | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorCullEnabled` | +| `ACDREAM_PROBE_INDOOR_LOOKUP` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-lookup]` per visible cell entity/sec: render-data hit/miss, IsSetup, parts-hit/parts-miss tallies | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorLookupEnabled` | +| `ACDREAM_PROBE_INDOOR_UPLOAD` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-upload]` requested/completed lines per EnvCell id at `WbMeshAdapter`'s staged-drain time | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorUploadEnabled` | +| `ACDREAM_PROBE_INDOOR_WALK` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-walk]` per visible cell entity/sec: world position, parent cell, landblock/AABB-visible flags, "drew" flag | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorWalkEnabled` | +| `ACDREAM_PROBE_INDOOR_XFORM` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-xform]` per visible cell entity/sec: cell-geometry SetupPart's composed world-matrix translation | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorXformEnabled` | +| `ACDREAM_PROBE_LOGIN_FRAMES` | `="1"` | Per-completed-frame login/portal-wormhole presentation classification (`world`/`tunnel`/`black`/`void`); logs `[login-frames]` on each classification transition | print-only. "Not a user setting; not in RuntimeOptions; not persisted" (doc comment). | unset (off) | `RenderPresentationDiagnostics.ProbeLoginFrames` (`LoginPresentationFrameProbe.cs:28-29`), consumed by `LoginPresentationFrameProbe.Process` | +| `ACDREAM_PROBE_NET` | `="1"` | Emits `[net-out]` (per outbound reliable message), `[net-tick]` (1 Hz WorldSession.Tick summary incl. reliable-transport rates), `[net-final]` (cumulative stats at Dispose), and `[cmd-gate]` (generation-gated command rejections) | print-only. Doc comment: "the counters themselves increment unconditionally in `TransportStats`; only the string work is gated" — i.e. the underlying stats tracking has a small always-on cost independent of this flag, but this flag itself gates only string/console formatting. | unset (off) | `NetDiagnostics.ProbeNet` (`NetDiagnostics.cs:56-57`), issue #260 probe family | +| `ACDREAM_PROBE_RESOLVE` | `=1` | gates one structured `[resolve]` line per `PhysicsEngine.ResolveWithTransition` call (in/target/out position+cell, ok-vs-partial, grounded/contact status, wall normal, walkable-polygon validity, responsible entity) (l.2a slice 1, general-purpose resolver probe) | print-only, ~30 Hz per moving entity while on | off | `PhysicsDiagnostics.ProbeResolveEnabled` | +| `ACDREAM_PROBE_REVEAL` | `="1"` | While a reveal destination's composite warmup is incomplete, emits one `[composite-warmup]` line/second: pending queue depth, scan state, upload-budget gate, first few unresolved GfxObj ids | print-only | unset (off) | `NetDiagnostics.ProbeReveal` (`NetDiagnostics.cs:115-116`), issue #260 | +| `ACDREAM_PROBE_REVEAL_TIMING` | `="1"` | Wall-clock attribution of each login/portal reveal hold: `[reveal-timing]` lines for `begin`/first-true readiness edges (render/composites/collision/gate/materialized), 1 Hz progress, and one `SUMMARY` line at viewport reveal; paired low-frequency `[reveal-resource]` snapshots report mesh staging/uploads/arena state, prepared-asset activity, composite warmup/uploads, managed memory, and tracked GPU residency | print-only; the probe object and render-resource sampler are not constructed when unset. When enabled, canonical resource owners are sampled only at begin, readiness edges, 1 Hz progress, and summary—not every frame. Use with `ACDREAM_FRAME_PROF=1` / `ACDREAM_FRAME_HISTORY` for per-frame CPU/GPU/alloc timing. | unset (off) | `StreamingDiagnostics.ProbeRevealTiming`, `RevealTimingProbe`, `RuntimeRenderFrameResourceDiagnosticsSource`, `PublicationTimingProbe` | +| `ACDREAM_PROBE_TUNNEL_FREEZE` | `=1` or `=N` | #419 RenderDoc apparatus: holds the teleport state in stable `Tunnel` after destination readiness and freezes the portal-space animation/roll at frame 72 (`=1`) or an explicit frame 2–120 (`=N`); emits one `[tunnel-freeze]` line with the actual frame and retail Setup/animation ids | **behavior-changing diagnostic:** placement, world viewport reveal, and LoginComplete are intentionally withheld until transition cancellation/process exit. For static visual inspection only; never use in a performance or lifecycle measurement. | unset (off) | `StreamingDiagnostics.TunnelFreezeFrame`; consumed by `LocalPlayerTeleportPresentation` and `PortalTunnelPresentation` | +| `ACDREAM_PROBE_SOUND_WIRE` | `="1"` | One line per inbound server Sound event (`0xF750`) and per wire-sound play decision, with the drop reason when nothing plays — used to determine whether missing interior soundscapes are server- or client-side | print-only, consumed at `AudioHookSink.cs:159` and `EntityEffectController.cs:123` | unset (off) | `AudioDiagnostics.ProbeWireSoundsEnabled` (`AudioDiagnostics.cs:20-21`) | +| `ACDREAM_PROBE_USEABILITY_FALLBACK` | `=1` | gates a per-call log of `IsUseableTarget` calls that take the null-useability fallback path (creature/door/lifestone passes) (measures a real ace-vs-retail data gap, not a bug investigation) | print-only; measures how often ACE ships entities without `_useability` set | off | `PhysicsDiagnostics.ProbeUseabilityFallbackEnabled` | +| `ACDREAM_PROBE_VIS` | `=1` | emits `[vis]` line on root-cell CHANGE: visible cell ids, OutsideView poly/plane counts, per-cell plane counts, scissor-fallback count (phase u.2d repurposed the flag; its DebugPanel mirror is unreachable — #434) | print-only; ALSO implicitly enables the separate `ACDREAM_PROBE_ENVCELL` probe (its getter ORs with this flag — see Notes #3); startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.ProbeVisibilityEnabled` | +| `ACDREAM_REMOTE_VEL_DIAG` | `=1` | prints per-UM/per-tick remote-velocity and animation-cycle diagnostic lines; `Runtime/Physics/RemoteMotion.cs` carries diagnostic-only fields (`PrevServerPos`, `PrevServerPosTime`, `MaxRootMotionSpeedSinceLastUP`, `LastOmegaDiagLogTime`) unconditionally on every remote — small fixed per-instance memory regardless of the flag, not gated (long-lived remote-velocity/animation diagnostic, commit a.1) | print-only, but the raw-site reads in `LiveEntityNetworkUpdateController.cs` fire on every UM/tick even when off (rule-5 violation, `Environment.GetEnvironmentVariable` call per event, 6+ call sites) | off | THREE readers: `AnimationPresentationDiagnostics.FromEnvironment()` (App owner record, cached at startup, consumed by `LiveEntityAnimationPresenter` for `[SEQSTATE]`/`[CURRNODE]`/other part-diagnostic lines, throttled to 1/sec/entity) + raw `Environment.GetEnvironmentVariable` reads scattered across `LiveEntityNetworkUpdateController.cs` (6+ sites: `[UM_RAW]`, `[FWD_WIRE]`, `[VEL_DIAG]`, `[UPCYCLE_SRC]`, `[UM_STALE]`) + `RemoteServerControlledVelocityCycle.cs:68` (`[UPCYCLE]`) | +| `ACDREAM_DUMP_CELLS` | `=` | one-shot JSON dump of any cached EnvCell whose id matches the list, to `ProbeDumpCellsPath` (issue #98 fixture capture) — Standing fixture-extraction tooling (A6.P3/#98 lineage) for the physics replay harness; roundtrip-tested. Not investigation-scoped. | file I/O once per matching cell id (no-op on repeat); fixture-generation tool, not a perf-neutral no-op when ids are listed | off/unset | `PhysicsDiagnostics.ProbeDumpCellIds` (`ParseHexIdList`) | +| `ACDREAM_DUMP_CELLS_DIR` | `=` | overrides the output directory for `ACDREAM_DUMP_CELLS` — Companion output-directory knob for ACDREAM_DUMP_CELLS. | print/file-path only; no effect unless `ACDREAM_DUMP_CELLS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpCellsPath` | +| `ACDREAM_DUMP_GFXOBJS` | `=` | one-shot JSON dump of any cached GfxObj's polygon table + BSP root metadata matching the list, to `ProbeDumpGfxObjsPath` (issue #98 fixture capture) — Standing fixture-extraction tooling (A6.P3/#98 lineage), pair of DUMP_CELLS. | file I/O once per matching id (no-op on repeat) | off/unset | `PhysicsDiagnostics.ProbeDumpGfxObjIds` (`ParseHexIdList`) | +| `ACDREAM_DUMP_GFXOBJS_DIR` | `=` | overrides the output directory for `ACDREAM_DUMP_GFXOBJS` — Companion output-directory knob for ACDREAM_DUMP_GFXOBJS. | print/file-path only; no effect unless `ACDREAM_DUMP_GFXOBJS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpGfxObjsPath` | +| `ACDREAM_DUMP_SKY` | `=1` | Print-only: dumps decoded `SkyDesc` raw values on region load (`SkyDescLoader.cs`) and per-GfxObj `Surface.Type`/translucency flags on first upload (`SkyRenderer.cs`), plus gates a `TimeSync` console diagnostic in `GameWindow`. Built to resolve specific open questions about retail sky units and GfxObjReplace timing (2026-04-23 research), now answered but the dumps remain wired. — Generic sky-keyframe isolation dump (introduced with the phase-1 tint revert); a tool, not a bug probe. | Three independent reads of the SAME env var, only one of which (`RuntimeOptions.DumpSky`) goes through the typed options object; the other two are raw scattered reads (see Notes). `SkyRenderer.cs:582`'s raw read is in the App layer and has no architectural excuse for bypassing `RuntimeOptions` — `_options.DumpSky` was already available to that composition. `print-only` in all three sites. | off/unset | `RuntimeOptions.DumpSky` (typed) → `GameWindow.cs:704` (`TimeSyncDiagnostic`); **also** two independent raw `Environment.GetEnvironmentVariable` reads at `SkyDescLoader.cs:392` (Core) and `SkyRenderer.cs:582` (App) | +| `ACDREAM_DUMP_STEEP_ROOF` | `=1` | gates `[steep-roof] KILL-VELOCITY-APPLIED` in `PhysicsEngine.ResolveWithTransition` when retail's `kill_velocity` zeroes body velocity on steep-slope impact, plus per-frame plane-normal traces in `TransitionTypes`/`PlayerMovementController` — KEEP: observes LIVE divergence-register row AD-56 (the plumb-fall freeze on steep-but-walkable polys, restored 2026-08-07). The only runtime lens on that active divergence; delete only with the AD-56 row itself. | print-only | off/unset | `PhysicsDiagnostics.DumpSteepRoofEnabled` | +| `ACDREAM_HIDE_PART` | `=` | Hides one mesh part by index on entities with ≥10 parts (humanoids) — a debugging aid for equipment/clothing part-visibility issues. — Generic model-part isolation tool (issue #37 lineage but general-purpose since); a tool, not a bug probe. | Real (visible) behavior change, not print-only, but scoped to a single diagnostic index and off by default. | off/unset | `RuntimeOptions.HidePartIndex` → `LivePresentationComposition.cs:608` → `LiveEntityAnimationPresenter.cs:21,38,243` | +| `ACDREAM_PROBE_CELL` | `=1` | gates one `[cell-transit]` line per `PlayerMovementController.CellId` change (old→new cell, position, reason tag) — Standing cell-transit tracer (L.2a slice 1), pair of the permanent ACDREAM_PROBE_RESOLVE; recurs in every membership investigation. | print-only; low volume (only on actual cell crossings) | off/unset | `PhysicsDiagnostics.ProbeCellEnabled` | + +## Temporary probes + +Each row names the issue that owns it. **A temporary probe is deleted in +the same commit as its investigation's fix** — if you find one here whose +issue is closed, the strip was missed; delete both. + +> **Probe debt, measured 2026-08-24:** 64 temporary probes existed, citing 21 +> distinct issues with 14 already closed. [#435](ISSUES.md) part 1 stripped +> the 17 rows whose investigation had ended without the strip — see the +> Retired section below for their removal record — leaving 47. Part 2 +> traced each of the (then-)14 unattributed rows to its introducing commit +> and stripped the 7 that belonged to closed investigations +> (`ACDREAM_A8_DUMP_PV`/Phase A8, `ACDREAM_DUMP_CLOTHING`/#37, +> `ACDREAM_DUMP_EDGE_SLIDE`/#32, `ACDREAM_DUMP_LIVE_SPAWNS`/Phase A8, +> `ACDREAM_DUMP_STEPUP`/L.2.3d-f, `ACDREAM_DUMP_VENDOR`/the vendor +> campaign, `ACDREAM_DUMP_VITALS`/#5). An eighth, +> `ACDREAM_DUMP_MOVE_TRUTH`, was deleted and then RESTORED the same day: +> it turned out to be automation apparatus, not a probe — the canonical +> nine-stop soak hard-gates on its output (see its row under Automation; +> #437 is the record). The rest of the attributed rows were reclassified +> into Permanent diagnostics as standing tools rather than investigation +> probes, leaving **31 temporary probes, every one attributed to an owning +> issue or campaign**. Each still costs a branch on its hot path even when +> unset, and a handful re-read the environment per frame rather than +> caching (see their side-effects column). + +| Flag | Owning investigation | Value | What it does | Side effects | Read by | +|---|---|---|---|---|---| +| `ACDREAM_CLIP_DEBUG` | #176 | `=1` | forces the EnvCell SHELL pass to map every instance to clip slot 0 (no-clip) instead of its cell's portal-slice region | ALTERS RENDERED OUTPUT: shells draw whole/unclipped instead of trimmed — a visual isolation mode, not a log-only probe; no DebugPanel mirror | `RenderingDiagnostics.ClipDebugNoShellTrim` | +| `ACDREAM_DUMP_APPEARANCE` | #5 | `="1"` | Logs every `0xF625` ObjDescEvent + `0xF7DB` UpdateObject with body length, target guid, hex preview — used to debug remote-player appearance asymmetry | print-only (`Console.WriteLine`) | `WorldSession` static field `DumpAppearanceEnabled` (`WorldSession.cs:792-793`), raw scattered read, issue #5 diagnostic | +| `ACDREAM_DUMP_OPCODES` | #5 | `="1"` | Logs first occurrence of each genuinely-unhandled inbound opcode (deduped by opcode) | print-only. Must stay the LAST else-if in the dispatch chain per comment (else it would intercept handled opcodes) — currently correct. | `WorldSession` static field `DumpOpcodesEnabled` (`WorldSession.cs:788-789`, consumed `WorldSession.cs:2391-2398`), issue #5 diagnostic. Also mirrored (display-only, non-functional) via `DebugPanel.cs:241`/`DebugVM.cs:227`. | +| `ACDREAM_DUMP_SCENERY_Z` | #48 | `=1` | Per-spawn Z-placement diagnostic for procedural scenery (trees/bushes/rocks), added for issue #48 (the "trees-in-sky" bug). | **NOT print-only** — this is a real behavior fork, not just added logging. `LandblockBuildFactory.cs:167-178`: when the flag is on, the streaming worker calls a **separate, duplicate scenery-building method** (`BuildSceneryEntitiesForStreaming`, a full parallel reimplementation of GfxObj/Setup mesh resolution + placement inline in this file) instead of production's `LandblockPhysicsContentBuilder.HydrateProceduralScenery`. Any visual/measurement run taken with this flag set is exercising a different scenery-placement code path than production, which can drift from it silently. | `RuntimeOptions.DumpSceneryZ` → `SessionPlayerComposition.cs:280` → `LandblockBuildFactory.cs:23,42,168,335` | +| `ACDREAM_DUMP_TRANSIT_FAIL` | #345 | `=1` | buffers per-tick `[transit-fail-insert]`/`[transit-fail-stepup]`/`[transit-fail-walk]`/`[transit-fail-adjust]` trace lines into a `[ThreadStatic]` list and flushes them to console ONLY when a tick requested nonzero XY movement but delivered zero (self-selecting "stuck tick" predicate) | print-only, zero allocation when off (flag checked before touching any buffer per its own doc); buffer/list allocation only on ticks that are already stuck | `PhysicsDiagnostics.DumpTransitFailEnabled` | +| `ACDREAM_LIGHT_DEBUG` | #176 | `=` (`int.TryParse`; unset/invalid → 0) | shader isolation mode uploaded as `uLightDebug` by `EnvCellRenderer` + `WbDrawDispatcher`: 0=off, 1=ambient-only vertex lighting, 2=kill dynamic point lights, 3=raw vLit visualization (texture ignored) | ALTERS RENDERED OUTPUT directly every draw pass (changes fragment-shader lighting/texturing) — not a log probe; no DebugPanel mirror | `RenderingDiagnostics.LightDebugMode` | +| `ACDREAM_PROBE_BUILDING` | l.2d slice 1 | `=1` | gates the multi-line `[resolve-bldg]` BSP-shadow-hit trace in `TransitionTypes.FindObjCollisions`, one-time `[entity-source]` registration logs in `GameWindow`, `[door-cycle]` UM dispatch trail, and a one-shot `[setstate-hex]` wire dump of the first `SetState` (0xF74B) packet in `WorldSession` | print-only; also un-gates the `PhysicsDiagnostics.LastBspHitPoly` diagnostic side-channel (a static field write in `BSPQuery`/`FlatBspQuery`, read back by the `[resolve-bldg]` line) — no gameplay effect, but an extra static-field write per BSP hit while on; heavy output (one multi-line entry per BSP hit per physics tick) | `PhysicsDiagnostics.ProbeBuildingEnabled` | +| `ACDREAM_PROBE_CELLSET` | a6.p5 | `=1` | gates `PhysicsDiagnostics.LogCellSetBuild`, one `[cellset-build]` line per `BuildCellSetAndPickContaining` call (seed cell, sphere XY, candidate list) from `CellTransit.cs:1468` | print-only; builds a `StringBuilder` of the candidate id list only when the flag is on | `PhysicsDiagnostics.ProbeCellSetEnabled` | +| `ACDREAM_PROBE_CELL_CACHE` | indoor walking phase d | `=1` | gates one `[cell-cache]` line per EnvCell first-cached in `PhysicsDataCache.CacheCellStruct` (poly counts, BSP root structure) | print-only; fires at most once per EnvCell (cache is no-op after first population); no DebugPanel mirror | `PhysicsDiagnostics.ProbeCellCacheEnabled` | +| `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` | +| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` | +| `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` | +| `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B | +| `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` | +| `ACDREAM_PROBE_GLSTATE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[gl-state]` line: depth/blend/cull/scissor/viewport/draw-FBO/color-mask/`glGetError` snapshot | print-only per its docstring; the actual state-snapshot/comparison call site lives outside `RenderingDiagnostics.cs` and was outside this pass's cited read sites | `RenderingDiagnostics.ProbeGlStateEnabled` | +| `ACDREAM_PROBE_INDOOR_BSP` | indoor walking phase 1 / cellar-lip wedge | `=1` | gates `[indoor-bsp]` (per `BSPQuery.FindCollisions` indoor call), `[neg-poly]` (near-miss polygon detail in `BSPQuery`), and `[stepdown-decide]` (step-down accept/reject inputs in `TransitionTypes`) trace lines | print-only; also un-gates the `LastBspHitPoly` diagnostic side-channel write (same as `ACDREAM_PROBE_BUILDING`) | `PhysicsDiagnostics.ProbeIndoorBspEnabled` | +| `ACDREAM_PROBE_INDOOR_LIGHT` | #176/#177 discriminator, a7.l1 | `=1` | rate-limited (1 Hz) `[indoor-light]` line from `LightManager.BuildPointLightSnapshot`: point-light pool set composition (pool/cellLess/registered/capped/byCell histogram) | print-only, explicitly "inert unless set" per the call-site comment (LightManager.cs:368-370); no DebugPanel mirror | `RenderingDiagnostics.ProbeIndoorLightEnabled` | +| `ACDREAM_PROBE_JUMP` | campaign ch round 2 | `=1` | gates the `[jump]` line in `PlayerMovementController.ReportJumpRefusal`, printed UNCONDITIONALLY (even when `OnInterfaceText` is null) to distinguish "branch never fired" from "branch fired, callback dropped it" | print-only; `Headless/Policies/HeadlessBotPolicy.cs`'s `JumpProbeHeadlessBotPolicy` doc comment references this flag as a companion but does not itself read it — it is a headless bot behavior meant to be run alongside `ACDREAM_PROBE_JUMP=1`, not a second consumer | `PhysicsDiagnostics.ProbeJumpEnabled` | +| `ACDREAM_PROBE_LOCAL_TELEPORT` | c4 route 3 d-t8 | `=1` | gates one `[local-tp]` line per local-player portal-arrival attempt (committed AND refused) from `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController.LogPortalArrivalAttempt` — the single Runtime chokepoint both graphical and headless hosts share | print-only; dual-host parity evidence (same line shape from both hosts) | `PhysicsDiagnostics.ProbeLocalTeleportEnabled` | +| `ACDREAM_PROBE_PARK` | issue #309 | `=1` | gates `[park]`/`[park-restore]` lines when a `RuntimeSetPositionState` placement parks or a cancelled park's withdrawal is rolled back | print-only, low volume (parks are rare); in a MULTI-session headless host, `HeadlessStaticStateAudit.ValidateProcessIsolation` THROWS `HeadlessConfigurationException` at startup if this (or any other process-global `Probe*`/`Dump*` boolean, `CollisionShadowSampleEvery`, or `PhysicsResolveCapture`) is enabled — refusal is waived only when `sessionCount==1` (logs loudly and proceeds instead) | `PhysicsDiagnostics.ProbeParkEnabled` | +| `ACDREAM_PROBE_PLACEMENT_FAIL` | issue #98 | `=1` | gates one `[place-fail]` line per Path-1 (Placement/Ethereal) `Collided` return in `BSPQuery.FindCollisions`, plus one per `Transition.DoStepDown` placement-insert rejection | print-only; low volume (fires only on actual rejection) | `PhysicsDiagnostics.ProbePlacementFailEnabled` | +| `ACDREAM_PROBE_POLY_DUMP` | a6.p3 slice 4, issue #98 | `=1` | gates one `[poly-dump]` line (full polygon geometry: cell, poly index, sides, plane, all vertices) per `AdjustSphereToPlane` push-back call | print-only; HEAVY output (one full-geometry dump per push-back call) — doc explicitly says "use briefly, then turn off" | `PhysicsDiagnostics.ProbePolyDumpEnabled` | +| `ACDREAM_PROBE_PORTAL_CHURN` | "throwaway apparatus — strip once the bound ships" | `=1` | one `[portal-churn]` summary per `PortalVisibilityBuilder.Build` call: per-cell pop/re-pop counts, re-enqueue totals, reciprocal-clip pre→post region growth | print-only | `RenderingDiagnostics.ProbePortalChurnEnabled` | +| `ACDREAM_PROBE_PUSH_BACK` | phase a6.p1 | `=1` | gates `[push-back]` (`BSPQuery.AdjustSphereToPlane`), `[push-back-disp]` (`BSPQuery.FindCollisions` 6-path dispatcher), `[push-back-cell]` (`Transition.CheckOtherCells` multi-cell BSP) lines | print-only; the `DebugVM.cs:380` "runtime mirror" is dead code — `DebugVM`/`DebugPanel` (`AcDream.UI.Abstractions/Panels/Debug/`) are never instantiated anywhere in `src/` (the ImGui frontend they required was removed at Campaign V slice V11); only the startup env var takes effect | `PhysicsDiagnostics.ProbePushBackEnabled` | +| `ACDREAM_PROBE_PVINPUT` | "throwaway apparatus — strip once the jitter source is pinned" | `=1` | one `[pv-input]` line/frame with 6-dp-precision `PortalVisibilityBuilder.Build` inputs (camera eye, player position, VP elements) + resulting flood-cell count; deliberately runs WITHOUT the heavier `[flap]` probe so the log stays diffable | print-only | `RenderingDiagnostics.ProbePvInputEnabled` | +| `ACDREAM_PROBE_REMOTE_SLIDE` | bug b, temporary — strip once two-client roof capture lands | `=1` OR `=` | gates `[remote-slide-up]`/`[remote-slide-vec]`/`[remote-slide-snap]`/`[remote-slide-enq]` lines across `LiveEntityNetworkUpdateController`, `InterpolationManager`, `RuntimeRemotePhysicsUpdater`, `RuntimeRemoteSteadyStatePosition` tracing two candidate remote-slide "blip" producers | print-only; `BeginRemoteSlideAttribution`/GUID-stamping calls are UNCONDITIONAL at several call sites (self-guard is internal), so a `[ThreadStatic]` field write happens on every remote tick regardless of the flag (cheap, non-allocating); a GUID allow-list narrows output to specific entities for a readable two-client capture | `PhysicsDiagnostics.ProbeRemoteSlideEnabled` + `ProbeRemoteSlideGuids` (raw string parsed via `ParseHexIdList` unless it's the literal `"1"`) | +| `ACDREAM_PROBE_REMOTE_TELEPORT` | c4 route 4b-3, temporary | `=1` | gates one `[remote-teleport]` line per routed remote teleport arm in `LiveEntityNetworkUpdateController.ApplyRemoteContactRouting` | print-only; a 2026-08-04 fix moved the enabled-check to the CALL SITE because the probe's internal self-guard did not prevent `teleportStatus.ToString()` from being evaluated/allocated on every teleport regardless of flag state — now properly guarded | `PhysicsDiagnostics.ProbeRemoteTeleportEnabled` | +| `ACDREAM_PROBE_SEAMDRAW` | #176, "throwaway apparatus" | `"1"`/`"true"`/blank → default #176 Facility Hub cell set (7 fixed hex ids); otherwise comma-separated hex cell-id list | change-deduped + 2 s-heartbeat `[seam-cell]`/`[seam-snap]`/`[seam-ent]`/`[seam-mask]` lines from `EnvCellRenderer.Render` and `WbDrawDispatcher` describing per-instance transforms and resolved light-set identities at target cells | print-only | `RenderingDiagnostics.ProbeSeamDrawEnabled` / `SeamDrawTargetCells` | +| `ACDREAM_PROBE_STEP_WALK` | a6.p3 issue #98 | `=1` | gates `[step-walk]` lines at select points in the transition sub-step loop and step-down probe (requested vs adjusted offset, sphere positions, contact planes, walkable flags) | print-only; no DebugPanel mirror | `PhysicsDiagnostics.ProbeStepWalkEnabled` | +| `ACDREAM_PROBE_SWEPT` | phase w stage 0 | `=1` | gates one `[cell-swept]` line per `ResolveWithTransition` call comparing the transition's swept cell vs the legacy static `ResolveCellId` path | print-only | `PhysicsDiagnostics.ProbeSweptEnabled` | +| `ACDREAM_PROBE_TELEPORT` | 2026-06-22, "removable diagnostic" | `=1` | gates `[tp-probe]` lines (`LogTeleport`) at AIM/ENQ/BUILD/APPLY/PLACED teleport-pipeline events across `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController`, with cross-thread monotonic timestamps | print-only | `PhysicsDiagnostics.ProbeTeleportEnabled` | +## Deprecated + +| Flag | Value | What it does | Side effects | Default | Read by | +|---|---|---|---|---|---| +| `ACDREAM_DEVTOOLS` | `=1` | logs a one-time "ImGui dev UI removed" notice; the only remaining functional consumer is `VulkanGraphicsContext.cs:184` (`enableOptionalExtensions: _options.DevTools`, selects optional Vulkan validation/debug-utils extensions) | real effect: turns on Vulkan validation/debug-utils extensions (can change perf and can surface validation-layer errors that don't occur when off) — NOT measurement-neutral for a perf gate; `GameWindow.DevToolsEnabled` is a hardcoded `false` const (dead — no ImGui dev UI exists to gate); `DevToolsInputCaptureSource(bool enabled)` explicitly discards its `enabled` ctor arg (`_ = enabled;`) — dead parameter, always reports `WantCaptureKeyboard=false` | off | `RuntimeOptions.DevTools` (typed, `Program.cs`/`RuntimeOptions.Parse`) | +| `ACDREAM_STREAM_RADIUS` | `=` (non-negative) | Legacy override for the streaming near/far radii, applied on top of the quality-preset's radii at session-start composition. | **CLAUDE.md explicitly documents this as "legacy" and warns against using it for measurement.** Confirmed in code (`SessionPlayerComposition.cs:256-259`): `nearRadius = legacyRadius; farRadius = Math.Max(legacyRadius, farRadius)` — it FORCES `NearRadius` and only ever RAISES (never lowers) `FarRadius`. It is set once at session-start composition and is **silently discarded** by any later Settings quality change: `RuntimeSettingsController.ApplyQuality` → `RuntimeSettingsTargets.ApplyQuality` → `StreamingController.ReconfigureRadii` recomputes radii straight from the quality preset with no knowledge of this override. A measurement/gate run taken with this set is measuring a different streaming window than production and than any run that later touches Settings. | `null` → quality-preset radii unmodified (production default: High preset, Near 4 / Far 12) | `RuntimeOptions.LegacyStreamRadius` → `SessionPlayerComposition.cs:254-268` | + +--- + + + +## Retired + +Flags that no longer exist, kept only so a stale script or an old research +document does not send someone hunting. Rows below this marker are exempt +from the "must still exist" check. + +| Flag | Retired | Replacement | +|---|---|---| +| `ACDREAM_RUN_SKILL` | Client-side run-skill override for local motion prediction. Skills now arrive from the server (`LiveMovementStatsApplier`); the hardcoded fallback is 200. | none — server-authoritative | +| `ACDREAM_JUMP_SKILL` | As above. The fallback is 300, not the 200 that CLAUDE.md advertised. | none — server-authoritative | +| `ACDREAM_RENDER_BACKEND` | Selected the GL-vs-Vulkan backend. Campaign V deleted the OpenGL backend; Vulkan is the only one. Two comments still named it as a live co-requisite until 2026-08-24. | none | +| `ACDREAM_ANIM_SPEED_SCALE` | Animation-speed multiplier from the pre-retail-sequencer era; died with the 1.248x factor. | none | +| `ACDREAM_A8_AUDIT` | Phase A8 EnvCell batch/cull audit dump. Its only caller never existed; `EnvCellRenderer.CollectCellAuditLines` was unreachable and was deleted 2026-08-24. | `ACDREAM_PROBE_ENVCELL` | +| `ACDREAM_AIRBORNE_DIAG` | #42 airborne-sweep `[SWEEP]`/`[SWEEP-OBJ]` XY-drift trace. Investigation closed; stripped 2026-08-24 (#435) along with its 16 siblings below. | none | +| `ACDREAM_DUMP_ENTITY` | #119 tower-staircase HYDRATE/DRAW/WALK-REJECT entity watchlist. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_AUTOWALK` | Issue #63 server-initiated auto-walk trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_LIGHT` | #133 A7 dungeon-lighting `[light]`/`[light-detail]` trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_OUTSTAGE` | #131 outside-stage dynamics routing trace (also the `ACDREAM_DUMP_ENTITY` `[outstage-own]` watchlist consumer). Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_PHANTOM` | #113 phantom-shell/phantom-objs draw-mechanism trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_REACH` | #334 broadphase candidate-disposition trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_REMOTE_LANDING` | Bug A / issue #32 remote ground-contact landing trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_SHELL` | #78 cell-shell opaque-pass render trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_STEP_HEIGHTS` | Issue #338 step-up/step-down height provenance trace (including its unconditional once-per-process `AnnounceStepHeightProbeOnce` self-report). Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_STICKY` | R5-V3 issue #171 sticky-melee lifecycle/steer trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_SUPPORT` | Issue #337 `[support]`/`[geom]` collision-vs-visual classifier trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_TEXFLUSH` | #105 white-indoor-textures staged-upload trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_VIEWER` | #119-residual viewer/flood capture (tower-ascent replay). Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_PROBE_WALK_MISS` | Issue #83 indoor walkable-plane miss trace. Investigation closed; stripped 2026-08-24 (#435). | none | +| `ACDREAM_WIRE_MESH` | Issue #337 F2 overlay upgrade to real physics-BSP polygon edges. Investigation closed; stripped 2026-08-24 (#435) — F2 reverted to its proxy-cylinder overlay. | none | +| `ACDREAM_WIRE_RADIUS` | Companion radius knob for `ACDREAM_WIRE_MESH`. Stripped alongside it 2026-08-24 (#435). | none | +| `ACDREAM_A8_DUMP_PV` | Phase A8.F portal-frame visual-gate triage dump (camera-cell portal census + EXIT-PROJ/EXIT-CLIP/EXIT trace in `PortalVisibilityBuilder.Build`). Phase A8 closed; stripped 2026-08-24 (#435 part 2). | none | +| `ACDREAM_DUMP_CLOTHING` | Issue #37 humanoid-coat clothing/part-swap trace. #37 closed 2026-05-11; stripped 2026-08-24 (#435 part 2). | none | +| `ACDREAM_DUMP_EDGE_SLIDE` | Issue #32 L.2c edge-slide/cliff-slide branch trace (five `edge-slide:` lines). #32 closed 2026-08-07; stripped 2026-08-24 (#435 part 2). | none | +| `ACDREAM_DUMP_LIVE_SPAWNS` | Phase A8 indoor-visibility batch live-spawn/DROP trace. Phase A8 closed; stripped 2026-08-24 (#435 part 2). | none | +| `ACDREAM_DUMP_STEPUP` | L.2.3d/e/f step-up `stepup: enter/SUCCESS/FAILED` trace. Investigation closed; stripped 2026-08-24 (#435 part 2) — its content is still covered by the separate `[transit-fail-stepup]` line under `ACDREAM_DUMP_TRANSIT_FAIL`. | `ACDREAM_DUMP_TRANSIT_FAIL` | +| `ACDREAM_DUMP_VENDOR` | `[vendor-diag]` trace (~25 call sites) for two vendor-approach/split-stack regressions. Vendor campaign closed 2026-08-08; stripped 2026-08-24 (#435 part 2) along with its owner class `VendorDiagnostics.cs`. | none | +| `ACDREAM_DUMP_VITALS` | Issue #5 `PrivateUpdateVital`/`PlayerDescription`/parse-failure trace across 4 sites. #5 closed 2026-04-25; stripped 2026-08-24 (#435 part 2). | none | diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 4f2cc180..ea586a03 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -979,7 +979,7 @@ Research: R7 + R10 + R11 + UI slice 05. - **✓ SHIPPED — H.1 — Chat window.** UI panel + all 6 wire opcodes (Channel, Tell, System, HearSpeech, HearRangedSpeech, TurbineChat). Wire layer + panel + outbound input + holtburger inbound parity + combat translator all shipped across I.1-I.7 on 2026-04-25. Targets `AcDream.UI.Abstractions`; will be reskinned when D.2b's custom retail-look toolkit lands. - **H.2 — Allegiance.** Tree model + XP pass-up math + 5 allegiance chat channels + MOTD. See `r11-allegiance.md`. -- **H.3 — Emote scripts + quests + dialogs.** 122 EmoteType × 39 Trigger mini-VM. Contract tracker UI. NPC dialog rendered via chat with `` markup. See `r10-quest-dialogs.md`. +- **H.3 — Emote scripts + quests + dialogs.** **Client scope COMPLETE 2026-08-21** (Campaigns QT + QJ, both user-accepted). The "122 EmoteType × 39 Trigger mini-VM" in this line describes the SERVER's job: per `r10-quest-dialogs.md` §1.3 the retail client stores no quest flag, evaluates no emote, and is never told a flag changed. It learns about quests three ways — dialogue strings the server already formatted, generic error toasts, and the contract tracker. The first two shipped earlier; the tracker, plus the Journal notebook and its index, shipped as the three-tab Journal panel (`RetailPanelCatalog.Journal` = 25). Start at `claude-memory/project_quest_journal_panel.md`. - **H.4 — Character creation.** `0xE000002 CharGen` dat + 13 heritages + templates + appearance picker + preview renderer. See `r07-character-creation.md`. **Acceptance:** create a character from scratch, talk to an NPC, get + complete a quest, gain XP that passes up to the patron. @@ -2070,6 +2070,10 @@ Native macOS graphical support is not committed by this track. The current mandatory renderer requires modern OpenGL capabilities beyond Apple's native OpenGL ceiling; revisit macOS only if a supported graphics backend is chosen. +**Future / unscheduled — Campaign AR:** the opt-in [Atmospheric Rendering / Shader Packs campaign](2026-08-21-atmospheric-rendering.md) makes moving authored sun-and-moon directional shadows from trees, monsters, players, and buildings its Tier-2 headline while preserving acdream's current retail-faithful renderer as the default and leaving physics, collision, gameplay, and network behavior unchanged; the project owner assigned Campaign AR on 2026-08-22 without displacing active M4 gameplay work. The [celestial source contract](../research/2026-08-22-dereth-celestial-shadow-sources.md) selects sun, dominant moon, then secondary moon by rendered direction while retaining AC's single authored directional-energy channel; sun rays and volumetrics remain sun-only. The previously referenced #268 + TS-8 package is complete and retired. Stage 1's automated correctness, performance, lifetime, locked-restore, Release, evidence, documentation, and project-owner live gates completed on 2026-08-22 after the opt-in exposure correction. The [Stage 2 connected and closeout gate](../research/2026-08-22-atmospheric-stage2-connected-gate.md) now closes every available machine-local ACE, dense A/B, long-lifetime, graphical-package, shader, Release, and complete-test row. The implemented two-client gate proved that ACE rejects two concurrent characters on the one available account and is ready for separate observer credentials without recording secrets. A distinct-account remote-player row, unavailable physical GPU classes, and final project-owner pack-off/pack-on acceptance remain external gates; Campaign AR is not yet declared shipped. + +**SHIPPED 2026-08-23 — Campaign VM (VisualMaster):** the [VisualMaster campaign](2026-08-22-visualmaster-campaign.md) closed the [Campaign AR review](../research/2026-08-22-campaign-ar-review.md) findings with evidence: VM0 proved the pack-off path is the pre-campaign renderer (pixel identity on static content, CPU −13 %, allocation −25×); VM2 cdb-read retail's live detail path (single-pass texture stages, the z-fade dead for built meshes) and VM1 re-ported #226 to it; VM4 made the documents truthful (real hardware darkens, the fallback brightened); VM5 de-banded the volumetrics (#421 filed); VM3 put the post stack in linear light with a numerically neutral preset; VM6 added weather-driven foliage wind behind the render pack (scenery-only, cutout/trunk motions, casters follow, independent of the shadow gate) through five Opus review rounds and a repeat-floor pixel apparatus ([note](../research/2026-08-23-vm6-foliage-wind-pixel-proof.md)). VM7's automated gates ran 2026-08-23 on `99d5b2a6`: release gate 15,283/0/0, connected lifecycle/reconnect route PASS pack-off and pack-on, final pack-off vs `base+normals` robust diff 5–10 px, #422 not reproducible in 40 instrumented runs. The owner's VM3 (linear light) and VM6 (wind) live gates passed 2026-08-23 at 2560×1440 with the High pack; two defects found during those gates were fixed the same session (#424 alt-tab zero-area frame crash, #425 resolution-blind resident budget + activation lockout). #422 (rare heap-corruption exit at process close, pack-independent) is carried open by the owner's decision. Merged to main as a fast-forward of 54 commits. + --- ## Cross-cutting work tracked in parallel @@ -2115,7 +2119,7 @@ OpenGL ceiling; revisit macOS only if a supported graphics backend is chosen. | Sliding along buildings / walls feels wrong | **Phase L.2c + L.2d** | | Roof edge / cliff / precipice blocks or slides wrong | **Phase L.2c** | | Crossing outdoor cell seams reports the wrong cell | **Phase L.2e** | -| Can't talk to NPCs | Basic select/use/give interaction works; full emote conversation/dialog systems remain **Phase H.3** | +| Can't talk to NPCs | NPC dialogue works (user-confirmed 2026-08-21); the emote VM behind it is the SERVER's, not ours | | Can't open a door | **FIXED** ✓ — object-use, animation, fading hooks, and collision transitions shipped | | Portals render as a rotating black disk | **FIXED** ✓ — DAT particles/effects and portal-space presentation shipped | | Chimneys have no smoke | **Phase E.3 SHIPPED** ✓ | @@ -2135,6 +2139,6 @@ OpenGL ceiling; revisit macOS only if a supported graphics backend is chosen. | No character creation — must use ACE admin | **Phase H.4** | | Sky is a flat color | **Phase G.1** (shipped; F7 cycles time, F10 cycles weather) | | Can't join allegiance | **Phase H.2** | -| No quest tracker | **Phase H.3** | +| ~~No quest tracker~~ | **SHIPPED 2026-08-21** — the Journal panel's Contracts tab (Campaign QT) | If you see something not on this list, add it here and assign a phase. diff --git a/docs/plans/2026-04-24-ui-framework.md b/docs/plans/2026-04-24-ui-framework.md index a216406e..0a82d8a8 100644 --- a/docs/plans/2026-04-24-ui-framework.md +++ b/docs/plans/2026-04-24-ui-framework.md @@ -184,17 +184,28 @@ panel through `IPanelRenderer`. ## Plugin UI API -The shipped plugin-facing gameplay UI contract is -`AcDream.Plugin.Abstractions.IUiRegistry.AddMarkupPanel`: a plugin provides -KSML-style markup and a binding object; the host builds it into the retained -`UiRoot` tree. `IPanel`/`IPanelRenderer` remains a first-party developer-panel -contract and is intentionally not referenced by `Plugin.Abstractions`. +The shipped plugin-facing gameplay UI contract is the additive BCL-only +`AcDream.Plugin.Abstractions.IUiRegistry.AddPanel`: a plugin provides a stable +window id/title/icon descriptor, KSML-style markup, and a binding object; the +host builds it into the retained `UiRoot` tree. The API-v1 +`AddMarkupPanel` member remains source/binary compatible and is enriched into +the same first-class window route by the scoped host. `IPanel`/ +`IPanelRenderer` remains a historical first-party developer-panel contract and +is intentionally not referenced by `Plugin.Abstractions`. -This makes plugin gameplay panels independent of ImGui while allowing them to -share the retained input, window, and DAT-sprite runtime. Registrations made -before the GL host exists are buffered. In builds where retail UI is disabled, -they remain registered but have no gameplay surface; the long-term release -configuration enables retained gameplay UI. +This makes plugin gameplay panels presentation-assembly independent while +allowing them to share the retained input, window, and DAT-sprite runtime. +Registrations made before the graphical host exists are buffered. The host +assigns `plugin:{pluginId}:{windowId}`, registers every panel with the common +window manager, persists its geometry/visibility, and exposes it through the +shared plugin sidepanel. Hiding/minimizing a panel does not dispose or pause the +plugin. No-window hosts retain the plugin session but expose the no-op UI +capability. + +The retained markup vocabulary includes panels, nested groups, labels, +buttons, meters, tabs, lamp-style toggles, and scalar sliders. Controls bind to +BCL-visible properties/actions on the plugin binding object; visible controls +must correspond to real behavior, never placeholders that report success. The following was the original pre-D.2b proposal and remains historical context, not the shipped plugin contract: @@ -255,7 +266,8 @@ walk around / take damage / regen. ### Sprint 3 — Plugin API hardening (superseded shape) - Document the `IPanel` contract. -- The shipped route is `IUiRegistry.AddMarkupPanel`, not plugin-owned +- The shipped route is `IUiRegistry.AddPanel` (with `AddMarkupPanel` as the + compatible legacy entry), not plugin-owned `IPanel` implementations. - Confirm plugins can subscribe to game events and expose retained markup bindings without referencing App or ImGui assemblies. diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index 6947bfae..903f5875 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -503,6 +503,12 @@ 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 @@ -2591,6 +2597,10 @@ 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/plans/2026-08-09-chat-parity-campaign.md b/docs/plans/2026-08-09-chat-parity-campaign.md index 954aef58..fcf3dd12 100644 --- a/docs/plans/2026-08-09-chat-parity-campaign.md +++ b/docs/plans/2026-08-09-chat-parity-campaign.md @@ -129,9 +129,9 @@ implementer per slice against a pinned contract (per dissolved with the `RetailWindowChrome.Imported` mount (0x2100006F's own border art IS the window chrome — no nine-slice wrapper, no content crop, frame==content) using the DAT's real - minH=100/maxH=2000/minW=300/maxW=2000. Register row AP-185 files the - one accepted simplification (the `_Locked` cosmetic border-art swap on - `UiLocked` is not ported; the live grip skin shows unconditionally). + minH=100/maxH=2000/minW=300/maxW=2000. The originally deferred AP-185 + `_Locked` cosmetic border-art swap was subsequently ported through the + shared registered-window lock presenter on 2026-08-20. - **CH6b — floating windows 1–4.** Mount `0x2100005B` ×4 as always-resident children per `gmGamePlayUI::SetupChildren @0x004E9EC0` (ids 0x10000505/0x1000050E/0x1000050F/0x10000510); diff --git a/docs/plans/2026-08-10-options-panel-campaign.md b/docs/plans/2026-08-10-options-panel-campaign.md index 31591d10..14458996 100644 --- a/docs/plans/2026-08-10-options-panel-campaign.md +++ b/docs/plans/2026-08-10-options-panel-campaign.md @@ -94,8 +94,9 @@ DAT-authored values. register row for the ACE-sourced 2013-unverifiable mapping. - **D4 — Configure Keyboard is the campaign's rebind screen** (it is the ONLY rebind screen — D1). Port `gmKeyboardUI`'s shape and DAT ActionMap - data (lane D Option C) but persist to `keybinds.json`; retail `.keymap` - file interchange is a register-row deferral. + data (lane D Option C). **Superseded 2026-08-26 by #446:** named retail + `.keymap` Load File / Save As/startup/shutdown persistence now ships; + `keybinds.json` remains only the host-command compatibility mirror. - **D5 — dead-endpoint buttons short-circuit to their own retail failure strings.** Urgent Assistance / Report Abuse open a defunct `support.turbine.com` URL in retail; acdream skips the browser launch and @@ -360,8 +361,8 @@ modal capture; right-click erases; N-way cross-map conflicts + the non-user-bindable refusal per lane D §5; Save/Cancel; Reset-to-defaults reloads the DAT maps. Persistence: `keybinds.json` (D4). -**Register rows:** `.keymap` file interchange not implemented (D4); any -retail column/behaviour consciously narrowed. +**Register rows:** any retail column/behaviour consciously narrowed. The +former D4 `.keymap` deferral was retired by #446 on 2026-08-26. **Gate:** connected — rebind a movement key, conflict prompt on a taken chord, persistence across relaunch, reset restores retail defaults. @@ -394,7 +395,8 @@ chord, persistence across relaunch, reset restores retail defaults. `2026-08-09-chat-retail-window-shell.md` §6.3's register row. - A pre-world character-select flow (D6 adapts; its register row carries the future work). -- Retail `.keymap` file read/write (D4 register row). +- None for retail `.keymap` file read/write; #446 implemented it on + 2026-08-26 and retired AP-202. - The `0x21000017` docked `gmPanelUI` host variant — acdream ships the floating host only (register row in OP3 if the review deems it a divergence; retail exposes both). diff --git a/docs/plans/2026-08-19-launcher-usability-campaign.md b/docs/plans/2026-08-19-launcher-usability-campaign.md new file mode 100644 index 00000000..2d1b9dd6 --- /dev/null +++ b/docs/plans/2026-08-19-launcher-usability-campaign.md @@ -0,0 +1,334 @@ +# Campaign LU — launcher usability + +**Status: CLOSED USER-ACCEPTED 2026-08-19/20.** Ten slices — the six planned +plus four the gate rounds added — shipped through CI and accepted live. + +**Gate results, in the user's words:** the update flow "works, it updates as it +should"; the launcher self-update round "pass"; the client's exit back to the +character selector "pass". + +| slice | commit | what it fixed | +|---|---|---| +| (blocker) #420 client crash | `a34e8f2a` | character select killed the client mid-paint | +| LU1 instant startup | `00d12782` | 29.9 s → 0.89 s, measured on the real 27.9 GiB pak | +| LU2/LU3 one update question | `a01ff426` | six buttons → Update / Not now, self-restarting | +| LU4 Setup complete | `0a2defb6` | setup ends with a dialog, not a finished progress bar | +| LU5/LU6 Play + sessions | `09305be6` | one Play per character; rows say who is playing | +| (cross-cutting) locale | `6a15dd06`, `955c6180` | retail text stopped following the machine's locale | +| headless CLI + LU7 | `2bff44a9` | headless and character refresh had never run at all | +| LU8 roster + fold | `18bbd377` | logging in IS the refresh; Play above the fold | +| LU9/LU10 stop + logout | `6ab5d8ce` | 30 s graceful stop, ACE hold, logout lands on select | +| verification-cache limit | `7037681a` | the ZFS finding below | + +Full solution under the release-gate filter: **14,375 passed, 0 failed, +0 skipped**, and identical under `sv-SE`, `tr-TR`, `ar-SA` and `de-DE`. + +--- + +## What the gate rounds found that the plan did not + +Four of the ten slices did not exist when this plan was written. Each came from +the user running the thing, and each was a defect the automated suite could not +have surfaced: + +**Headless and character refresh had never worked, once.** The launcher spawned +`acdream-headless --config `; the host reads `arguments[0]` as its command +and accepts only `validate` or `run`. Every launcher-started headless session +and every roster refresh died on its first instruction with "Invalid command" +and exit 64 — visible only as a code in a status file. A whole campaign's gates +missed it because they drove the headless host through its CLI directly, never +through the launcher's spec. `LauncherHeadlessCommandLineContractTests` now +feeds the launcher's real argument vector to the host's real parser. + +**Refresh was harmful as well as broken.** It opened a second connection to an +account purely to read the roster, which the server treats as a new login — so +using it while playing disconnected you. It was also redundant: every ordinary +login already carries the roster, and the orchestrator already folds it in. + +**Stop was the crash.** The UI gave the client five seconds before killing it, +which is not enough to send a logout, await the acknowledgement, and tear down a +mapped 28 GB world. So Stop routinely produced exactly the ungraceful exit that +leaves the server holding the account. + +**Play was below the fold.** The buttons existed; the plugins/login-commands +form pushed them past the bottom of the scroll area. Reported, correctly, as +"there is no headless or gui option". + +## Findings worth keeping + +**The verification cache cannot see a same-size, same-timestamp change.** Run +174 failed on a test asserting it could. Measured on the runner: `/tmp` is ZFS, +and 141 of 200 same-size rewrites produced an identical mtime. NTFS's 100 ns +resolution is the only reason it never showed on Windows. The contract is now +two true statements — startup catches a corruption whose write time moves, and +a forced full verification catches one that preserves both — instead of one +that is false on some filesystems. Verify files is the forced path. + +**Testing the launcher does not test your source.** The launcher runs the +INSTALLED client from the version store, so a client-side fix cannot be gated +until CI publishes it. A void-world screenshot was read as "the fix failed" when +the installed build was 63 minutes older than the fix. + +**A locally built launcher cannot test self-update.** Its stamped version is +`1.0.0`, which sorts above every `0.1.0-build.*` the feed publishes, so it is +never offered an update. Publishing one with a deliberately low +`InformationalVersion` is what made that path testable at all. + +**Goal** + +> The launcher opens without a long wait. On startup it asks whether to +> update the launcher or the client, and restarts itself after a launcher +> update; the old update flow is gone. First-run setup ends with a success +> popup that returns you to the launcher on OK. A selected character +> launches directly. The sessions frame shows account, character (or Char +> Select) and whether they are in game — not the launch mode. + +**Why now.** Campaign LA shipped a launcher that is *correct* — atomic +installs, verified artifacts, session barriers, rollback — and *not +usable*. The user's verdict, twice: "way too complex", "too complex for +sending it to my friends". This campaign changes the surface a person +touches. It does not weaken what happens underneath. + +**Acceptance for the whole campaign** is the user's own walkthrough: +download `launcher-win-x64.zip` from the `latest` release, unzip, run, +install, play — without being told anything. + +--- + +## LU1 — the launcher opens immediately + +**Measured problem.** [App.axaml.cs:57](../../src/AcDream.Launcher/App.axaml.cs) +blocks the UI thread on `installer.LoadExistingAsync().GetAwaiter().GetResult()` +before the window is constructed. That reaches +`LauncherInstallRecordStore.VerifyFileAsync`, which computes a full SHA-256 +of the installed package. + +Measured on the user's machine 2026-08-19: + +| fact | value | +|---|---| +| `%LOCALAPPDATA%\acdream\pak\acdream.pak` | 29,908,271,024 bytes (27.9 GiB) | +| full SHA-256 | **24.1 s** at 1.16 GB/s | +| digest vs `install.json` | identical (`fee8595d…`) | + +So the startup cost is 24 s of disk read to re-confirm something that was +already true. A friend does not see it only because they have no package +installed yet — verification short-circuits at "nothing installed". It +will hit them the moment first-run setup finishes. + +**Change.** Startup verification becomes size + last-write-time against +the record. The full hash keeps running where it is cheap and meaningful: +at install, after an update installs a new package, and behind an explicit +**Verify files** button (the Steam shape). + +The cheap facts live in a **sidecar** (`install.verification.json`), not as a +new field on the install record. `LauncherInstallRecordStore` reads +`install.json` with `JsonUnmappedMemberHandling.Disallow`, so a new field +there would make an *older* launcher build reject the record outright and +demand a 28 GB re-bake after a rollback. An unknown sidecar file is simply +ignored by older builds, so the change is compatible in both directions. +An install with no sidecar yet pays one full hash and then writes it. + +**Acceptance** + +- Window visible in under 2 s with the 27.9 GiB package installed. +- Truncating or touching the package still blocks launch with a clear reason. +- **Verify files** reproduces the full check and reports pass/fail. +- The install and update paths still hash in full — unchanged. + +--- + +## LU2 — one update question, asked once, at startup + +**Change.** On start the launcher checks the feed once. If the launcher or +the client is behind, it shows **one** dialog naming what is out of date and +offering **Update** / **Not now**. Nothing else. + +- Launcher first when the feed's `minimumLauncherVersion` demands it, or + when only the launcher is behind: install, then **restart into the new + version** (`LauncherSelfUpdateBootstrap` already owns this handoff). +- Client otherwise: install, close the dialog, back at the launcher. +- Nothing to do: no dialog at all. The launcher just opens. + +**Acceptance** — three observed cases: up to date (silent), client behind +(one dialog → play), launcher behind (one dialog → relaunched on the new +version, confirmed by the version it reports). + +--- + +## LU3 — delete the old update surface + +The current prompt offers six buttons — Check again, Rollback client, +Stage launcher, Install client, Cancel, Close — plus a version table and a +restart-required banner. That is the flow being removed, along with the +"Check for updates" header button and the `LauncherUpdateViewModel` paths +only it reached. + +**What stays:** everything in `AcDream.Launcher.Core/Updates/` that makes +an update safe — manifest validation, bounded verified download, safe ZIP +extraction, versioned install with an atomic `current.json` switch, the +session barrier, and rollback as a *capability*. The complexity the user +objects to is the panel, not the safety beneath it. + +**Open — needs one confirmation before code is deleted:** rollback has no +place in the new single-question flow. It can move behind a small +"Advanced" affordance or leave the UI entirely (staying available as Core +API + tests). I will show the exact deletion list and ask before removing +it. + +**Acceptance** — exactly one update entry point in the UI; tests covering +deleted view-model behavior are removed with the code, never skipped. + +--- + +## LU4 — "Setup complete" ends first-run setup + +**Change.** When the bake publishes and the install record verifies, the +wizard shows a modal: setup succeeded, what was built, **OK**. OK closes +the wizard and returns to the launcher with the "Client setup required" +banner gone and launching enabled. + +**Acceptance** — a real first-run bake shows it exactly once on success; +cancellation and failure paths keep their existing error/status reporting +and must **not** show it. + +--- + +## LU5 — pressing Play on a character launches that character + +**Reproduce before changing anything.** The plumbing already exists end to +end: `LauncherOrchestrator.LaunchAsync` clones the character with the +*requested* mode (`CloneCharacter(character, mode)`), +`SessionConfigComposer.BuildSelector` emits an id selector (falling back to +name), and `RuntimeOptions.MapCharacterSelector` maps it into the App host. +A defect somewhere in a chain that reads correct is exactly the case this +project has repeatedly lost time to by guessing. + +Two candidates to separate by observation, not argument: + +1. The launch button is gated off by a capability reason, so the click + never becomes a session. +2. The selector reaches the client but the roster match fails, so character + select stays on screen — which is what "you can just select different + chars" describes. + +**Change.** One obvious **Play** per character that enters the world as +that character, plus the deliberate "Character select" path kept separate. +Three near-identical launch buttons is itself part of the complaint. + +**Acceptance** — select a character, press Play, arrive in the world as +that character with no character-select screen in between. + +--- + +## LU6 — the sessions frame says who is playing + +Today each row reads `server / account / character`, then `Mode` +(Gui/GuiSelect/Headless/Probe), then `State`, then a raw status string. +The launch mode is launcher bookkeeping and means nothing to a player. + +**Change.** Each row shows the account, the character — or **Character +select** when no character was chosen — and one plain status word derived +from the host's own status stream: + +`Starting` → `Character select` → `In game` → `Stopped` / `Failed` + +Errors keep their own line. Stop keeps its button. Character-refresh +(probe) rows stay distinguishable from play sessions. + +**Acceptance** — launching a character shows account + name + **In game** +once in world; a character-select launch shows **Character select** until a +character is entered. + +--- + +## Non-goals + +- No change to download verification, atomic install, or the session barrier. +- No change to credential handling (plaintext profile remains the user's decision). +- No change to Linux graphical gating (Slice L stays parked). + +## Working rules for this campaign + +- One slice per commit, `dotnet build` + `dotnet test` green before each. +- Push to main; CI gates on both runners and publishes the release the + launcher itself updates from — so every slice is testable by the user + through the shipped path within a few minutes. +- LU3's deletions and LU5's root cause get shown to the user before they + land. + +--- + +# Implementation notes (recon 2026-08-19, before any code) + +These were read out of the tree, not assumed. They exist so each slice +starts from the mechanism that is already there instead of re-deriving it. + +## The self-update restart chain already exists end to end (LU2) + +`LauncherUpdater.StageLauncherAsync` stages a verified payload and writes a +plan. On the next ordinary startup `LauncherSelfUpdateBootstrap.HandleAsync` +takes the exclusive lease, sees `SelfUpdatePlanState.Staged`, and spawns the +STAGED launcher in helper mode. `RunHelperAsync` waits for the parent PID to +exit, applies the replacement, starts the updated launcher with +`--acdream-self-update-confirm-v1`, and waits for the confirmation receipt. + +So "restart after a launcher update" needs no new update machinery. What it +needs is one seam: after staging succeeds, start the staged helper against +the CURRENT process and shut down. Extract the existing staged-plan branch of +`HandleAsync` into a callable entry point and reuse it — do not duplicate it, +and do not restart by launching a second copy of the launcher and hoping the +bootstrap picks the plan up, which races the exclusive lease against the +process that is still shutting down. + +## The orchestrator already knows "in game" (LU6) + +`LauncherActivityState` has `InWorld`, and the orchestrator already sets it +from `EnteredWorldStatusEvent`, which carries the real `CharacterId` and +`CharacterName` from the host. Today that identity is written into a status +STRING (`"In world as X."`) and thrown away. + +LU6 promotes it: the entered-world event updates the activity's character +name so a character-select launch can show who is actually being played, and +the row renders one word derived from `LauncherActivityState` rather than the +raw enum plus the launch mode: + +| state | row shows | +|---|---| +| `Starting`, `Running` | Starting | +| `Connected` | Character select | +| `InWorld` | In game | +| `Disconnected`, `Stopping` | Stopping | +| `Exited`, `Cancelled` | Stopped | +| `Failed` | Failed | + +`LauncherActivityKind.Probe` rows stay visually distinct (they are a +character refresh, not a play session). + +## First-run completion has an exact point (LU4) + +`FirstRunInstallerViewModel.StartAsync` succeeds at the line that calls +`_onInstalled(result.Record)` and sets `Phase = LauncherInstallPhase.Completed`. +That is where the success dialog belongs — after the record is published, so +the launcher behind it is already in its launch-enabled state when the user +presses OK. The cancelled and failed branches immediately below it must not +reach it. + +## The launcher side of "launch this character" reads correct (LU5) + +Confirmed by reading, so the live repro can skip re-checking these: + +- `LauncherOrchestrator.LaunchAsync` -> `CloneCharacter(character, mode)` + overrides the profile's saved `LaunchMode` with the mode the button asked + for, so the stored default cannot leak into an explicit launch. +- `SessionConfigComposer.Compose` builds a selector for every mode except + `GuiSelect`, preferring a parsed non-zero id over the name. +- `SessionPlayerComposition` passes the selector into + `LiveSessionConnectOptions` with `AwaitCharacterSelection: selector is null`, + and `InteractionRetainedUiComposition` binds the character-selection UI only + when the selector is null. + +The user's stored profiles all carry `launchMode: "guiSelect"` (the default), +and every cached character has a real id. So the defect is NOT a missing id +and NOT the saved default overriding the click. Reproduce live before +changing anything. diff --git a/docs/plans/2026-08-21-atmospheric-rendering.md b/docs/plans/2026-08-21-atmospheric-rendering.md new file mode 100644 index 00000000..e8ac1a0b --- /dev/null +++ b/docs/plans/2026-08-21-atmospheric-rendering.md @@ -0,0 +1,977 @@ +# Campaign AR — Atmospheric Rendering / Shader Packs + +**Date:** 2026-08-21 +**Status:** MACHINE-LOCAL STAGE 2 COMPLETE — the authored sun-and-moon +shadow-source extension, Stage 1 owner gate, connected ACE matrix, dense A/B, +long lifetime, graphical package lifecycle, shader, locked-restore, Release, +and complete-test gates pass. A distinct-account second-client remote-player +row, unavailable physical GPU classes, and final project-owner pack-off/pack-on +acceptance remain external closeout gates; the campaign is not yet declared +shipped. +**Phase id:** **Campaign AR** — assigned by the project owner on 2026-08-22 +**Scheduling:** originally held for the post-M7 rendering-polish pass; the +project owner explicitly authorized implementation on 2026-08-21. This +owner-directed campaign is now Campaign AR and does not displace the active M4 +gameplay work. The previously referenced #268 + TS-8 stat-chain package is +already complete and retired, so it is no longer a scheduling dependency. + +## Goal + +Add an opt-in enhanced-graphics system whose headline feature is **real-time +directional shadows cast by trees, monsters, players, and buildings as +Dereth's authored sun and moons move across the sky**. The same system can add +bloom, filmic tonemapping, colour grading, vignette, sun rays, and later +volumetric shafts, with useful quality levels on weak through high-end +hardware. Sun rays and volumetric shafts remain sun-only effects; the approved +moon scope applies to Tier 2 directional shadows. + +Campaign AR executes in two stages. Stage 1 fixed the dense-scene transform +ceiling and shadow quality, completed authored sun/dominant-moon/secondary-moon +source selection, and finished every automated gate that did not require the +project owner's physical-display judgment. The owner accepted the subsequent +live visual/performance round on 2026-08-22 after the default Atmospheric +exposure was corrected from 1.0 to 0.80. The machine-local Stage 2 rows are now +complete. Remaining closeout is limited to a second connected remote player on +a distinct ACE account, unavailable physical-hardware rows, and final owner +acceptance; it does not add another renderer feature tier. + +Stage 1's dense-scene regression is pinned by the connected failures already +captured on 2026-08-22: Atmospheric fell back at 68,395, 67,581, and even +65,538 combined world matrices against the old 65,536-matrix binding ceiling. +The corrected connected launch must exceed that historical workload without +persisting acdream-default fallback or splitting the authoritative pose data. + +The enhancement is a shader pack, not a rewrite of acdream's renderer or AC's +art. **acdream's current retail-faithful renderer** remains the default, +authoritative path. References below to the “default” or “retail-faithful” path +always mean acdream—not the original retail executable. + +The evidence and constraints for this design are recorded in the +[terrain and atmospheric rendering findings](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md), +especially [the measured renderer baseline](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#5-renderer-state-relevant-to-atmospheric-work) +and [the requested tier model](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#6-wanted-work--atmospheric-rendering-user-stated). +The approved celestial identity, priority, transform, and direction-versus- +energy contract are pinned by the +[Dereth celestial shadow-source research](../research/2026-08-22-dereth-celestial-shadow-sources.md). + +## Opt-in contract + +1. **acdream's current renderer is the default.** With no pack selected, the current render graph, + shaders, render targets, submissions, lighting, colours, and screenshots + remain authoritative. No enhancement resource or pass is created. +2. **Selection is explicit.** Installing a pack does not enable it. The user + selects one pack and one quality preset in Display settings. `acdream + default (retail-faithful)` is always present and cannot be removed. +3. **One pack owns the enhancement graph.** Packs do not stack. This prevents + ambiguous pass ordering, incompatible HDR conventions, and unbounded GPU + cost. +4. **The renderer owns the RHI.** A pack declares assets, semantic pass hooks, + capabilities, resources, and quality variants. It never receives Vulkan + handles or mutates the authoritative scene, streaming, gameplay, or + physics owners. +5. **Failure returns to acdream's default renderer.** Unsupported capabilities, + malformed assets, shader/pipeline candidate-creation failure, or an invalid + pass graph disables the complete pack and restores acdream's default path + with a visible reason. A half-enabled pack is never rendered. A terminal + `VK_ERROR_DEVICE_LOST` cannot render either path on the lost device; it tears + down that renderer/device lifetime, and retail remains authoritative while a + fresh renderer/device is constructed and the pack is validated again. +6. **Divergence is honest.** Enhanced screenshots are intentionally not retail + parity evidence. The default path remains the comparison oracle and the + enhancement choice is recorded in diagnostics and screenshot metadata. +7. **No scheduling claim.** Rendering phases stay frozen until the M7 polish + pass unless the project owner explicitly reprioritizes this work. + +## Capability tiers + +The costs below are **planning estimates**, not measurements. They are +incremental GPU p50 targets for a representative discrete GPU at 1920x1080; +every slice must replace them with physical-hardware measurements. Tier-1 +pixel effects scale with output resolution, so 4K has roughly four times the +1080p fragment workload. Shadow-map cost depends more on caster count, map +resolution, and cascade count than on output resolution. + +| Tier | Contents | Prerequisite | Rough incremental GPU cost at 1080p | +|---|---|---|---:| +| acdream default | Current authoritative retail-faithful rendering | Current mandatory Vulkan/RHI capabilities | 0 ms | +| 0 | **Weather-driven foliage wind** (Campaign VM VM6): procedural-scenery trees/bushes lean, branch-swing, and flutter; their directional-shadow casters apply the identical displacement so the shadow moves with the leaf | Shader ABI v2 (`AtmosphericFrame` gains `uAtmosphereClockWind`/`uAtmosphereWindAmplitude`); the existing `ProceduralSceneryIdAllocator` entity-id namespace for classification | ~0.05 ms (near-zero; ~25 ALU per displaced vertex, zero CPU cost, zero extra draw submissions — an early-out `uint` AND gates every non-foliage vertex) | +| 1 | Bloom, ACES filmic tonemap, colour grade, vignette | Main-world colour intermediate and fullscreen passes | 0.35–0.80 ms | +| 1 | Screen-space sun rays (crepuscular) | Authored sun screen position plus an occlusion mask; **no shadow maps** | 0.20–0.50 ms | +| 2 | **Moving authored sun-and-moon cascaded directional shadows from trees, monsters, players, and houses/buildings** | A second scene pass, selected-celestial view/projection matrices, sampled depth maps, caster pipeline variants | 1.50–3.00 ms | +| 2+ | Sun-only volumetric light shafts | Reuse Tier-2 shadow infrastructure only while the selected source is the authored sun, plus authored weather | 0.15–0.40 ms | +| Later | SSAO and water reflections | Scene depth plus normal inputs and separate designs | Not budgeted here | +| Out | True PBR | AC lacks authored per-texture normal/roughness/metalness maps | Not planned | + +Tier numbers express prerequisites, not a forced bundle. A pack may offer +Tier 1 without shadows. Tier 2 always includes the complete shadow-caster +classes; weak-hardware presets reduce range, cascade count, and resolution +rather than silently dropping monsters, trees, or buildings. + +**Foliage wind (Tier 0, Campaign VM VM6).** Deliberately numbered below Tier +1: it needs no fullscreen pass, no colour intermediate, and no shadow +infrastructure of its own — only the shader ABI v2 clock/wind block and the +per-batch classification bits `BatchData.flags` already carries. It composes +with every other tier (Tier 2's shadow casters read the same displacement so +a swaying tree's shadow tracks it) and, uniquely among these tiers, is cheap +enough that a future default-on consideration is plausible; today it ships +opt-in with the rest of the atmospheric pack, gated by `wind-enabled`. + +## Tier 2 headline: Dereth's moving authored sun-and-moon shadows + +The [measured renderer state](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#5-renderer-state-relevant-to-atmospheric-work) +already supplies retail's single directional colour/energy channel from +`SkyStateProvider`. Tier 2 augments it with the visible authored celestial +positions documented in the +[Dereth celestial shadow-source research](../research/2026-08-22-dereth-celestial-shadow-sources.md). +The camera-relative cascaded map selects, in order, the visible above-horizon +sun (`0x01001348`), dominant haloed moon (`0x01001F6A`), or secondary moon +(`0x01001F67`). The selected mesh's exact rendered transform supplies shadow +direction; retail's one interpolated `DirColor * DirBright` channel supplies +colour/energy. Moon texture brightness and mesh luminosity never manufacture a +second world light. As those authored bodies move, tree branches, monsters, +players, houses, and other eligible world geometry cast correspondingly moving +shadows. + +This source selection is an explicit opt-in pack enhancement. It is not a +claim that the retail executable rendered real-time moon shadows, and it does +not alter acdream's default retail-faithful scene lighting. Screen-space sun +rays and volumetric shafts continue to use only the authored sun; they do not +switch to either moon. + +The required behavior is: + +- Terrain and opaque world geometry receive shadows. Terrain, buildings, + statics, procedural scenery, the local player, remote players, and creatures + cast them when resident and visible to the main outdoor world. +- Foliage and other cutout materials use an alpha-sampling shadow fragment + shader. An empty depth fragment shader would turn each tree plane into a + solid rectangular shadow. +- Animated casters reuse the exact per-part transforms already published in + the N.5 SSBO. The shadow pass must not create a second animation pose or + gameplay entity projection. +- Cascades follow the camera and are texel-stabilized. Their reach is clamped + to the resident two-tier streaming window; the pack does not extend world + streaming or issue speculative loads. +- The celestial directional-shadow pass is outdoor-only. Dungeon and EnvCell lighting remains + authored per-cell lighting. Entering an interior retires or idles outdoor + shadow work without leaving stale maps on screen. +- Shadow direction follows the selected visible above-horizon sun, dominant + moon, or secondary moon. The authored directional colour/energy remains + `DirColor * DirBright`; active day/weather pack policy may soften or reduce + it without inventing a second celestial clock, light-energy channel, or + weather system. A time with no eligible above-horizon body has no + directional shadow; night is not itself a disable condition. +- Transparent blend materials do not cast an opaque silhouette by default. + Only existing opaque and cutout classifications participate until a + material-specific transparent-shadow contract is designed. + +The pass reuses the retained resident scene. It must not run PView, portal +traversal, or per-object CPU visibility classification once per cascade. +Initially, each cascade draws the bounded resident caster set through the +existing batched/MDI ownership. If that is too expensive, the next permitted +step is GPU culling—not repeated CPU culling or per-object submissions. + +## Pack API surface sketch + +The public declarations belong in the BCL-only +`AcDream.Plugin.Abstractions` assembly. The graphical App supplies the +implementation and translates the declarations to the Vulkan RHI. Headless +hosts expose no render-pack registry and never load pack assets. + +This is an API shape, not code committed by this design: + +```csharp +public interface IRenderPackPlugin +{ + void Register(IRenderPackRegistry registry); +} + +public interface IRenderPackRegistry +{ + IDisposable Register(RenderPackDescriptor descriptor, IRenderPackAssets assets); +} + +public interface IRenderPackAssets +{ + Stream OpenRead(string assetKey); +} + +public sealed record RenderPackDescriptor( + string Id, + string DisplayName, + Version PackVersion, + int PackApiVersion, + RenderPackTier HighestTier, + IReadOnlyList RequiredCapabilities, + IReadOnlyList OptionalCapabilities, + IReadOnlyList Resources, + IReadOnlyList Passes, + IReadOnlyList SceneReplays, + IReadOnlyList PipelineVariants, + IReadOnlyList QualityPresets, + IReadOnlyList Settings, + AtmospherePolicyDeclaration? AtmospherePolicy); +``` + +A pack declares: + +- a stable ID, display name, pack version, and pack-API version; +- its highest tier and a human-readable feature summary; +- mandatory and optional GPU capabilities and per-preset limits; +- shader assets and fixed renderer semantic inputs, including world colour, + scene depth, optional normals, selected celestial shadow direction/energy, + sun direction/screen position for sun-only effects, active weather, camera + matrices, shadow-caster transforms, and frame time; +- intermediate images/buffers by relative or absolute extent, format class, + usage, lifetime, and estimated bytes; +- passes at renderer-owned hooks such as `ShadowDepthBeforeWorld`, + `AtmosphereBeforeToneMap`, `ToneMap`, and + `AfterToneMapBeforePrivateViewports`; +- renderer-owned scene replays such as `OutdoorDirectionalShadowCasters`, with + requested cascade views and existing caster/material classes + (`Terrain`, `OpaqueWorld`, `AlphaCutoutWorld`, `AnimatedOpaque`, and + `AnimatedAlphaCutout`); the renderer resolves those classes from its + retained scene and records their existing batched draws; +- fixed pipeline variants for shadow-caster depth and main-world shadow + receivers. A variant names its base semantic (`Terrain`, `WorldMesh`, or + `EnvCell`), shader asset, compatible material classes, and declared inputs + such as cascade matrices, directional depth maps, and sampler state; it does + not replace visibility, batching, mesh ownership, or draw submission code; +- quality presets, user-visible settings with bounded ranges, and declared + incremental GPU/VRAM budgets; and +- an atmosphere policy: explicit directional-source/sun-elevation response + curves and a mapping from AC's categorical `activeDayGroup` values to effect + multipliers. These values live in the visible pack declaration, not as hidden + renderer constants; AC remains the owner of celestial position, directional + energy, and weather state. + +The renderer—not the pack—defines descriptor layouts, validates SPIR-V and +resource declarations, resolves semantic scene-replay and pipeline-variant +requests, builds pipelines, schedules barriers, owns frame-flight and teardown, +and supplies immutable frame inputs. Packs cannot add arbitrary draw callbacks, +read gameplay owners, submit command buffers, retain borrowed frame views, or +address resources outside their registration. The built-in pack's Tier-2 +caster pass and receiver shaders must be expressible entirely through these +same public declarations. + +### Selection and fail-safe lifecycle + +1. Discover manifests and descriptors without creating GPU objects. +2. Show compatibility and estimated cost in Display settings. Unsupported + packs remain visible with the exact missing capability; they cannot be + selected. +3. On explicit selection, validate the whole descriptor, all assets, resource + ceilings, hooks, and shader interfaces; then build a complete candidate + pipeline set off to the side. +4. Atomically activate the candidate only after every required object exists. + Until then acdream's default path continues rendering. +5. Persist `pack id + pack version + preset`, never a positional index. If the + pack disappears or becomes incompatible, select `acdream default` and + retain the failure notice. +6. On runtime validation or post-recreation candidate failure, withdraw all + pack passes/resources at a frame boundary and resume acdream's default + renderer. Do not repeatedly retry a failing pack during the session. +7. Unload and reconnect use the normal render-generation and GPU-flight + retirement rules. No pack object may retain a world generation, scene + entity, or collectible plugin load context. + +In this campaign, **device recreation** means disposing the complete old +renderer, Vulkan context, and device, then constructing a fresh context/device, +re-probing capabilities, and validating selection again with retail active +until the candidate is complete. It does **not** mean live, in-process recovery +from `VK_ERROR_DEVICE_LOST`; device loss remains terminal to that renderer and +device lifetime. + +The built-in Atmospheric Rendering pack should be the first consumer of this +same API. It must not receive private renderer shortcuts that third-party packs +cannot express. + +The public v1 authoring surface, manifest schema, shader semantic bindings, +failure guidance, validator command, and external no-op sample are indexed by +the [render-pack SDK](../render-packs/README.md). + +## Frame-graph placement + +With the pack off, the frozen retail graph is unchanged. With a pack selected, +the renderer builds a separate enhancement graph: + +1. Update the existing immutable world frame, including authored sky objects, + the retail directional colour/energy channel, and weather. +2. Outdoors, select the visible above-horizon sun/dominant moon/secondary moon + direction and render Tier-2 cascaded shadow depth from the resident caster + set. +3. Render the main world to the pack's world-colour intermediate, using + pack-selected pipeline variants to sample the shadow map where requested. +4. Preserve the established PView, punch/seal depth discipline, shared-alpha + ordering, particle ordering, and world transparency boundaries. +5. Generate screen-space sun occlusion/rays or, only while the shadow source is + the sun, shadow-map volumetrics. +6. Composite rays/shafts **before tonemapping**, so bloom sees them and the + filmic curve rolls them off instead of clipping them. +7. Apply tonemap, colour grade, and vignette to the main world image. +8. Continue with private portal/paperdoll/appraisal viewports and retained UI + on their existing path. They are not accidentally post-processed with the + main world. + +## Delivery slices and acceptance + +The slice labels below are local to this document. They are not phase IDs. + +### Pre-moon checkpoint, Stage 1 acceptance, and Stage 2 start — 2026-08-22 + +Before the approved moon extension, all seven local slices (0–6) had production +implementations in the current worktree. A source-identical isolated clean +snapshot closed that sun-only reference-GPU physical matrix, and one physical +integrated-AMD Auto safe-fallback row was also present. Those artifacts remain +valid evidence for the exact binaries and sun-only scope they measured; they +are not moon-alignment, source-transition, current-worktree, or final user- +acceptance evidence. + +Stage 1's automated implementation and validation are complete. The authored +sun/dominant-moon/secondary-moon resolver and its direction-versus-energy +handoff are present and covered without inferring physical quality from unit +tests, screenshots, or historical sun-only rows. The subsequent live ACE round +covered the owner-reported shadow visibility/configuration, temporal +pixelation/shimmer, frame-pacing/desktop responsiveness, selection/fullscreen +regressions, and matched indoor/outdoor exposure. After the exposure correction +the owner accepted the live result. The exact evidence and limits are recorded +in the [Stage 1 live-gate report](../research/2026-08-22-atmospheric-stage1-live-gate.md). + +Current authored-celestial Stage 1 automated gate (2026-08-22): + +- The shader compiler reports **24/24** Vulkan shader pairs ready. Incremental + regeneration expands only pack includes and preserves all 18 pre-campaign + retail SPIR-V artifacts byte-for-byte; the exact SHA-256 oracle and complete + source-manifest checks pass. All selected-celestial binding-6 modules expose + the 336-byte ABI v1 layout, including source kind at offset 320. +- Focused Release validation passes **344/344** App renderer tests, **30/30** + standalone SDK/pack-validator tests, **14/14** Core sky-loader tests, and + **48/48** MossTank tests. Both external Tier-2 samples embed and validate the + current selected-celestial shader ABI without App or Vulkan dependencies. +- The repository's forced locked restore passes. The complete Release solution, + including all source, tests, tools, and SDK samples, builds with **0 warnings + and 0 errors** after that restore. +- The repository-owned fresh-process hermetic gate passes + **14,928/14,928** tests with zero skips or failures across 14 assemblies + **under the repository's hermetic lane filter** (Manual/Timing/Live/ + InstalledDat/Diagnostic lanes excluded; a raw `dotnet test AcDream.slnx` + reports those lanes as skips/failures by design — VM4 correction); + `AcDream.App.Tests` contributes **5,823/5,823**. Evidence is under + `artifacts/atmospheric-rendering/stage1-moon-release-gate/`. +- The App total includes the 9,500-caster 256-frame zero-managed-allocation + steady-state fixture, warmed CPU/GPU sampling allocation gates, the complete + 12-cycle Low/Medium/High/retail/resize/failure/recovery/frame-flight/ + generation convergence fixture, and independent renderer/context/device + recreation. These prove the non-physical performance and lifetime contracts; + they do not claim physical frame pacing or image quality. +- A final path audit finds no source changes under `src/AcDream.Runtime`, no + physics or collision changes, and no changes to the retail GLSL sources or + tracked retail SPIR-V binaries. Pack-off production integration remains the + strict authoritative-path oracle. + +The command-level record and evidence boundary are in the +[Stage 1 automated gate report](../research/2026-08-22-atmospheric-stage1-automated-gate.md). + +Recorded pre-moon automated checkpoint (not a current moon-scope completion +claim): + +- The repository-owned fresh-process Release test stage passes + **14,880/14,880** tests with zero skips or failures across 14 assemblies + (hermetic lane filter, as above). + `AcDream.App.Tests` contributes **5,783/5,783**; campaign-focused App cases + cover descriptor/asset/SPIR-V + validation, pack-off/no-op invariants, atomic asynchronous candidate swaps, + runtime fallback, declared settings, Tier-1/Tier-2/Tier-2+ graph execution, + all headline caster classes, topology caching, exact animated transforms, + Low/Medium/High/Auto policy, diagnostics, and the pack UI. +- Headless plugin-session tests pass **6/6**, including rejection of a + render-pack-only request before its DLL is loaded. +- The SDK validator suite passes **26/26** and builds/validates the external + `AcDream.RenderPacks.NoOp`, `AcDream.RenderPacks.AtmosphericTier2`, and + `AcDream.RenderPacks.ShadowsOnlyTier2` samples without App or Vulkan + references. +- The production catalog is revisioned rather than frozen at startup. The same + composed controller/UI observes external registration, withdrawal, and + corrected re-registration; an active withdrawn pack retires at the next + frame boundary, persists retail fallback, releases its asset/context owners, + and does not retry the removed registration. Runtime package admission now + matches the SDK: exactly one public constructible render-pack entry point and + at least one live registration, with transactional rollback for malformed, + multiple, internal, zero-registration, and partially failing packages. +- The retained 9,500-caster warmed-frame fixture performs no second-frame + scene-index copy, topology rebuild, sort, or classification and allocates + zero managed bytes. Animated-static, live-dynamic, and equipped-child root + and part transforms refresh through cached IDs/slots with exact float bits. +- Render and screenshot diagnostics now publish exact accepted counts for + terrain commands, outdoor statics, buildings, animated statics, local + players, remote players, non-player creatures, other live dynamics, and + equipped children. These labels stop at the authoritative evidence boundary: + static DAT publication does not distinguish a tree from other outdoor + scenery, and create-object render metadata does not distinguish a hostile + monster from a non-hostile NPC creature. Diagnostics therefore report + `OutdoorStatics` and `NonPlayerCreatures`; they never infer tree or monster + identity from a mesh or ID. +- The recording-RHI long-cycle gate repeatedly crosses Low, Medium, High, and + retail selection; resize; injected candidate failure and explicit recovery; + both frame-flight slots; render-generation replacement; and final renderer + disposal. Pack resources, pipeline-format leases, texture slots, retained + transforms, receiver candidates, and registrations converge exactly. A + second fixture proves that device recreation is old-renderer/context/device + teardown followed by an independent fresh device and activation generation. +- Therefore the deterministic lifecycle implementation, recording-RHI + convergence, and fresh-device recreation definition are locally closed. The + executable connected route and its contract assertions are implemented for + select/disable/re-enable, exact resize, authored time/weather changes, and + fresh-process recreation, but a contract-tested route is not connected-world + evidence; its ACE-backed execution and artifacts remain open below. +- The complete Release solution, including all three SDK samples, the + validator, shader compiler/generated manifest, and repository tools, builds + with **0 warnings and 0 errors**. This historical checkpoint could not repeat + locked restore because that managed workspace denied NuGet access to the + user-profile `NuGet.Config`; the later Stage 1 moon gate and final Stage 2 + gate supersede that limitation and both report forced locked restore green. + +The corresponding durable source/test entry points are: + +- public contracts and SDK: + `src/AcDream.Plugin.Abstractions/Rendering/`, `docs/render-packs/`, + `tools/RenderPackValidator/`, `samples/AcDream.RenderPacks.*`, and + `tests/AcDream.RenderPackValidator.Tests/`; +- activation, compatibility, Auto, diagnostics, and built-in graph: + `src/AcDream.App/Rendering/Packs/` and + `tests/AcDream.App.Tests/Rendering/Packs/`; +- moving authored-celestial cascades, casters, receivers, and retained topology: + `src/AcDream.App/Rendering/DirectionalShadow*.cs`, + `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`, + `src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs`, + `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.DirectionalShadows.cs`, and + `tests/AcDream.App.Tests/Rendering/DirectionalShadow*` plus + `tests/AcDream.App.Tests/Rendering/Packs/AuthoredCelestialShadowSourceResolverTests.cs`; +- retained Display UI and headless boundary: + `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs`, + `tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs`, and + `tests/AcDream.Headless.Tests/HeadlessPluginSessionTests.cs`. + +Machine-local offline/physical evidence currently present under +`artifacts/atmospheric-rendering/` is gate evidence, but it is not a substitute +for connected-world or project-owner acceptance: + +- `smoke-retail-720p/` records the pack-off `retail/off` path with zero pack + resources, casters, cascades, or classification calls; +- `accept-shadow-morning-200m/`, `accept-shadow-afternoon-200m/`, and + `accept-shadow-morning-close/` contain fixed-camera moving-sun shadow + screenshots plus metadata for 9,498 resident casters and three cascades; + these pre-moon captures do not prove moon alignment or source transitions; +- `volumetric-valid-camera-500m/` contains a High-preset volumetric diagnostic + capture; it is not a performance acceptance row; +- `matrix-clean-snapshot-dense-linear-v20/` is the current complete AMD Radeon + RX 9070 XT physical matrix: **30/30 rows pass** across + retail/Low/Medium/High/Auto, 1080p/1440p/4K, and capped/uncapped pacing. The + actual worktree's 4,397 source files were copied and hash-verified into an + isolated clean snapshot at commit `4876c970`; the Release App product version + names that exact commit, source and binary identities match, and tracked + status is empty. Its 18 active enhanced rows each own exact 2,048-sample + CPU/receiver/GPU windows, 9,498 casters, and zero warmed classification calls. + Six rows are retail and six 4K Low/Medium/Auto rows are accepted + resource-unavailable fail-safe outcomes; +- `matrix-final-v16-exact-auto/` and the two `current-low-1080p-*-v20/` + captures retain the optimization's predecessor/reference trail; the clean + snapshot matrix above supersedes them as current reference-adapter evidence; +- all six retail rows record zero pack resources, passes, casters, cascades, + draws, or dispatches. The six unavailable 4K Low/Medium/Auto rows likewise + record zero pack work and pass their strict paired-default framebuffer + comparisons instead of rendering a half-enabled graph; +- `igpu-auto-safe-fallback-v7-paired/` records physical Auto behavior on the + integrated **AMD Radeon(TM) Graphics** adapter (Vulkan 1.4.315, driver + 2.0.353). After 180 stable Low samples, Auto failed safe with the exact reason + `GPU p99 19.308 ms (budget 3.000 ms), CPU p99 0.534 ms (budget 0.500 ms), + resident GPU bytes 41648404 (budget 67108864)`. The published state is + `retail/off`, has zero pack resources or work, and retains that visible + reason. Its comparison against the paired time-matched retail artifact + `igpu-retail-current-v6-time-matched/`, using `sky-mask.png`, differs in only + **56 / 1,536,000 compared pixels**, a **0.003645833% (0.00365%) sky-masked + pixel difference**, below the 0.1% gate. This closes physical weak-adapter + safe fallback, not active Low performance on that adapter or the remaining + GPU matrix; +- `compare-retained-transform.json` records the retained-transform image + comparison used by the 9,500-caster steady-state gate. + +Stage 1 project-owner gate: + +- **PASS — accepted by the project owner on 2026-08-22.** The acceptance closes + Stage 1's physical-display and desktop-performance stop. It authorizes Stage + 2; it is not a substitute for Stage 2's connected scenario, long-lifetime, + package lifecycle, additional physical-GPU, or final pack-off/pack-on rows. + +Stage 2 machine-local closeout result (2026-08-22): + +The exact commands, commits, metrics, artifacts, and evidence limits are in the +[Stage 2 connected and closeout report](../research/2026-08-22-atmospheric-stage2-connected-gate.md). + +1. **PASS, except the external second-client row.** The connected graphical + route covered moving local players, known monster encounters + reported under the authoritative `NonPlayerCreatures` category, and equipped + children; landblock publication/demotion; clear, overcast and rain; + outdoor/interior/dungeon transitions; portal travel and reconnect. Capture + the implemented select/disable/re-enable, resize, authored sun/moon source + transitions and weather, and + fresh-process renderer/context/device-recreation assertions against a real + ACE session, proving exact resource convergence and no stale maps/owners. + The implemented two-client gate proved the observer login and movement, but + ACE rejected the concurrent primary login when both used the available + account. The gate now requires dedicated observer environment credentials, + keeps them out of its artifacts, and permits independent character indexes; + a distinct-account connected run must close the authoritative nonzero + `RemotePlayers` row. +2. **EXTERNAL HARDWARE GATE.** Repeat the current clean-source RX 9070 XT + pack-off/Low/Medium/High/Auto × 1080p/1440p/4K × capped/uncapped matrix on + every other supported physical GPU class. The integrated-AMD Auto-to-retail + artifact proves weak-adapter safe fallback only; it does not prove active Low + or the complete matrix on that adapter. Automated weak-GPU fixtures, one + fallback row, and one high-end reference adapter do not prove the remaining + physical rows. +3. **FINAL OWNER GATE.** Complete the visual matrix for Tier-1 neutral values and private-view/UI + isolation; rays at dawn/noon/dusk, behind-camera and occluded states; + foliage cutouts; moving animated shadows under sun and moon; indoor gating; + source-transition continuity; temporal pixelation/shimmer; bias/cascade seam + review; sun-only volumetric weather/occluder behavior; and pack-off + restoration. +4. **PASS.** External install/select/update/remove/fail/recover flows passed in + six fresh connected graphical processes. The nine-stop Medium lifetime route + passed with graceful shutdown, as did the deterministic 12-cycle and + fresh-device convergence fixtures. +5. **FINAL OWNER GATE.** Obtain explicit project-owner acceptance of the final pack-off and pack-on + visual/performance matrix before changing this document to shipped. + +### Slice 0 — Contract, capability probe, and acdream-default no-op + +**Implementation:** complete. The BCL-only v1 ABI, live revisioned plugin +discovery/catalog, retained Display selection, compatibility/cost summaries, +strict SDK-equivalent entry admission, asynchronous candidate preparation, +frame-boundary activation/withdrawal/fallback, registration-scoped no-retry, +stable diagnostics, no-op sample, and headless exclusion are present and +automated. The checked-in default-path oracle and RX 9070 XT physical pack-off +rows pass; connected lifetime and graphical package evidence now pass. The +remaining supported physical GPU classes stay open. + +Define the versioned BCL-only descriptor/registry, manifest fields, semantic +bindings, pack discovery, Display selection, diagnostics, and atomic +activation/fallback transaction. Implement a no-op conformance pack only. + +**Acceptance:** `acdream default (retail-faithful)` remains selected on clean and upgraded +installs; pack discovery allocates no GPU resources; the disabled run has the +same pass list, pipeline set, draw/dispatch counts, deterministic framebuffer +digests, and resource ledger as the pre-campaign baseline; malformed, +unsupported, missing, and shader-invalid fixtures all report one precise +reason and render acdream's default path without partial resources or retry loops; headless +hosts load no render assemblies or pack assets. + +### Slice 1 — Tier-1 world-colour and filmic stack + +**Implementation:** complete. The pack-owned main-world target, bloom chain, +ACES filmic pass, colour grade, vignette, declared neutral settings, resize +recreation, and private-viewport/UI placement are implemented and automated. +The RX 9070 XT 1080p/1440p/4K physical budget rows pass. Project-owner visual +acceptance and the remaining supported physical GPU classes stay open. + +Add the main-world intermediate and implement bloom, ACES filmic tonemapping, +colour grade, and vignette through the pack API. Supply half/quarter-resolution +variants and preserve private viewports/UI. + +**Acceptance:** every effect can be independently set to its neutral value; +the preset is deterministic across resize/recreate; UI, paperdoll, portal, and +appraisal surfaces retain their accepted colours; 1080p/1440p/4K captures show +no clipping, haloing at the world/UI edge, stale frame, or resource leak; the +slice meets its preset GPU/VRAM budget. + +Campaign VM VM3 (2026-08-22) moved the post stack to linear light: every +world/ray/volumetric read is decoded with the 2.2 display assumption; the +bloom intermediate is already linear (decoded once at extraction, so +downstream blur/composite reads need no further decode). Tonemap/grade/ +vignette run in linear and the result is re-encoded; the post stack's +neutral settings are a numerical identity (measured on a real frame at +Campaign VM VM3: ≤1 LSB on 99.99 % of pixels; 95 foliage-silhouette pixels +— 58 isolated — differ by up to 73 LSB from cutout-edge rasterization between +two separate client runs, none on any surface). + +### Slice 2 — Tier-1 screen-space sun rays + +**Implementation:** complete. Authored sun projection, the screen-space +occlusion mask, declared sun/day-group/weather policy, pre-tonemap ray +composition, and deterministic disabled gates are implemented and automated. +Connected time/weather transitions pass; physical-display acceptance for +dawn/noon/dusk, behind-camera, occlusion, and edge-flicker behavior remains in +the final owner visual matrix. + +Project the existing authored sun position, build a screen-space occlusion +mask, and composite weather-driven crepuscular rays before tonemapping. This +slice deliberately has no shadow-map dependency. + +**Acceptance:** clear dawn/dusk produces visible raking rays, noon makes them +vanish, overcast/rain mutes them, the sun behind the camera or fully occluded +produces none, and camera edges do not streak or flicker. The pack descriptor's +sun-elevation and `activeDayGroup` policy deterministically produces those +states without a second weather/clock owner. The exact same scene with Tier 1 +disabled returns to the Slice-0 digest from acdream's default +retail-faithful renderer. + +### Slice 3 — Tier-2 moving authored sun-and-moon dynamic shadows + +**Implementation:** Stage 1 automated and project-owner live gates complete. +Camera-relative stabilized cascades, +outdoor gating, opaque and alpha-cutout casters, terrain/world receivers, +headline caster membership, exact current animated transforms, bounded +resident replay, GPU-flight ownership, and cached topology/dynamic-transform +refresh were present at the sun-only checkpoint. The approved authored +sun/dominant-moon/secondary-moon resolver and direction-versus-energy handoff +are now present, and focused plus complete fresh-process automated validation +pass. The project owner's 2026-08-22 live round accepted source alignment, +temporal stability, desktop responsiveness, and the final exposure correction. +Per-class diagnostics cover +terrain commands, outdoor +statics, buildings, animated statics, local/remote players, non-player +creatures, other live dynamics, and equipped children without inventing tree +or hostile-monster identity. Fixed-camera morning/afternoon artifacts are not +moon evidence; the live-gate result and its exact boundary are recorded in the +[Stage 1 live-gate report](../research/2026-08-22-atmospheric-stage1-live-gate.md). +The machine-local Stage 2 connected and closeout matrix passes. The external +second-client, additional-GPU, and final owner rows above remain open. + +This is the campaign's headline slice. Add camera-relative cascades, opaque +and alpha-cutout caster variants, animated SSBO transforms, shadow receivers, +texel stabilization, outdoor gating, and weather/authored-directional-energy +control. Select the exact rendered direction of the visible above-horizon sun, +dominant haloed moon, or secondary moon according to the +[celestial source contract](../research/2026-08-22-dereth-celestial-shadow-sources.md), +while retaining retail's one `DirColor * DirBright` energy channel. Trees, +monsters, players, houses/buildings, terrain, and ordinary outdoor statics +participate through existing scene ownership. + +**Acceptance:** in fixed-camera and live dawn/noon/dusk/night captures, shadows +align with and change direction/length under Dereth's authored sun and selected +dominant/secondary moon; overlap and no-source transitions are stable and do +not snap to an unrelated body. Walking players and monsters cast and +self-shadow from their current animated poses; foliage casts leaf/branch +cutouts rather than rectangles; houses and procedural trees retain shadows +through landblock publication/demotion without popping outside the chosen +cascade transition tolerance; indoor/dungeon captures have no outdoor +celestial directional shadow; portal/reconnect/device recreation leaves zero +stale maps or owners. Acne, Peter-panning, cascade seams, distant depth-bias +leaks, temporal pixelation/shimmer, and desktop performance pass the live user +gate and subsequent Stage 2 matrix; the dense-Arwic CPU submission and GPU +budgets pass. + +### Slice 4 — Quality scaling and automatic compatibility + +**Implementation:** complete. Low/Medium/High declarations, capability and +memory admission, preset cost summaries, retained Display controls, the +built-in Automatic checkbox, hysteretic Auto, diagnostics, asynchronous +off-side candidate preparation, and atomic stable-boundary swaps are present +and automated. The source-identical clean-snapshot RX 9070 XT matrix passes all +30 current rows after the Low dense-pose CPU optimization. The integrated-AMD +physical row proves that persistently over-budget Low returns Auto atomically to +retail with a visible reason and a paired-retail framebuffer match. Active Low +on that adapter and additional supported-adapter matrices remain open. + +Land the Low/Medium/High presets, memory ceilings, capability-based preset +availability, stable cascade fitting, resize handling, and optional hysteretic +Auto selection. Auto may change resolution/range only at a stable frame +boundary and must expose its current choice. + +**Acceptance:** every supported preset retains all headline caster classes; +weak-hardware fixtures select a valid lower preset or fail safely to acdream's +default renderer; +changing preset cannot leak, stall the render thread, invalidate streaming, or +leave mixed-resolution resources; the quality/performance table is populated +with measured physical-hardware results. + +### Slice 5 — Tier-2+ volumetric shafts + +**Implementation:** complete for the recorded sun-only scope. The declared +volumetric pass reuses directional-shadow depth only when the selected source +is the authored sun, consumes authored sun/weather/indoor inputs, composites +before tonemapping, and has independent quality/step settings and automated +failure gates. The reference-GPU low-sun 2,048-sample enabled/neutral A/B passes +its incremental cost target. Moon selection does not enable moon shafts. +Connected weather/lifecycle transitions pass; physical-display +occluder/weather acceptance in the final owner matrix and additional physical +GPU classes remain open. + +Reuse the directional shadow map for world-space light shafts only while its +source is the authored sun. Drive density, strength, and colour from authored +sun/weather inputs and composite before tonemapping. A selected moon produces +directional shadows but no rays or volumetric shafts. + +**Acceptance:** shafts respect terrain, trees, houses, and moving creatures; +clear low sun is strongest, overcast and indoor scenes are muted/off; disabling +shafts leaves Tier-2 shadow output unchanged; the incremental cost stays within +the Tier-2+ budget. + +### Slice 6 — Pack SDK and campaign closeout + +**Implementation:** SDK deliverables complete and the Stage 1 project-owner +gate passed on 2026-08-22. The v1 +manifest schema, semantic binding table, compatibility/failure guide, +validator, built-in pack, and three buildable external samples are present and +automated. Connected graphical package lifecycle and long-run convergence pass. +The distinct-account second-client remote-player row, remaining physical GPU +classes, and final project-owner acceptance remain open; the 30-row RX 9070 XT +reference matrix is complete. + +Publish the manifest/schema, semantic binding table, sample no-op pack, +Atmospheric pack, compatibility diagnostics, authoring/validation tool, and +failure-handling guidance. Run the full automated, connected, physical-display, +performance, lifetime, portal, and screenshot matrix. + +**Acceptance:** a clean external sample builds without App or Vulkan +references; install/select/update/remove/fail/recover flows work; pack-off +evidence from acdream's default retail-faithful renderer remains authoritative +and unchanged; all resource ledgers +converge after long play, reconnect, portal travel, pack disable, and device +recreation; the project owner accepts the visual matrix before the campaign is +declared shipped. + +## Performance budget and measurement + +The [measured pre-campaign baseline](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#performance-baseline-and-the-binding-constraint) +is **519.7 FPS with CPU/GPU p50 of 1.869/1.096 ms**, and dense towns are +CPU-submission-bound. Fullscreen work may occupy currently idle GPU time, but +it is not treated as free. Shadow cascades must protect the CPU submission +path. + +| Preset | Incremental GPU p50 / p99 at 1080p | Incremental render-CPU p50 / p99 | Pack-owned resident GPU memory **at 1080p** (scales with pixel count — `RenderPackResidentBudget.Effective`, #425) | +|---|---:|---:|---:| +| Low | ≤ 2.0 / 3.0 ms | ≤ 0.15 / 0.50 ms | ≤ 64 MiB | +| Medium | ≤ 3.25 / 4.50 ms | ≤ 0.25 / 0.75 ms | ≤ 128 MiB | +| High | ≤ 4.50 / 6.00 ms | ≤ 0.35 / 1.00 ms | ≤ 256 MiB | + +The current physical reference matrix is +`artifacts/atmospheric-rendering/matrix-clean-snapshot-dense-linear-v20/` on an +AMD Radeon RX 9070 XT (Vulkan 1.4.349, driver 2.0.395). The source-identical +isolated commit and Release binary both identify `4876c970`; tracked source +status is empty. All 30 rows pass. At capped 1080p, the exact 2,048-sample +windows are: + +| Selection | Incremental render-CPU p50 / p99 | Inclusive GPU p50 / p99 | Resident pack memory | +|---|---:|---:|---:| +| Low | 0.108 / 0.164 ms | 0.900 / 1.002 ms | 39.719 MiB | +| Medium | 0.116 / 0.144 ms | 1.001 / 1.023 ms | 73.590 MiB | +| High | 0.121 / 0.145 ms | 1.240 / 1.273 ms | 113.556 MiB | +| Auto (settled High) | 0.122 / 0.146 ms | 1.211 / 1.222 ms | 113.556 MiB | + +Every uncapped active row also passes. Across capped and uncapped active rows, +the maximum measured p50/p99 and resident memory are: Low 0.108/0.164 ms CPU, +1.070/1.080 ms GPU, 60.868 MiB; Medium 0.117/0.194 ms CPU, 1.152/1.163 ms GPU, +103.583 MiB; High 0.121/0.159 ms CPU, 2.237/2.252 ms GPU, 238.141 MiB; and Auto +0.122/0.166 ms CPU, 1.482/1.494 ms GPU, 145.856 MiB. All six retail rows record +zero pack resources/work. At 4K, Low needs 131,302,400 bytes and Medium/initial +Auto need 198,440,960 bytes, so those six capped/uncapped rows correctly report +`ResourceUnavailable`, create no pack resources/work, and pass their strict +paired-default framebuffer comparisons. This closes the current reference +adapter, not the connected receiver A/B route or other supported/weak physical +GPU classes. + +Tier 2+'s separate low-sun physical A/B is recorded in +`artifacts/atmospheric-rendering/volumetric-performance-ab-1080p.json`. Both +runs pin High, 1080p uncapped, clear weather, 16.667° sun elevation, the same +50 m / 180° / 10° camera, 9,498 casters, four cascades, and exact 2,048-sample +windows. Enabling one volumetric draw over the neutral-strength run adds +**0.189 ms GPU p50 / 0.219 ms p99**, **0.009 ms CPU p50 / 0.007 ms p99**, and +4,147,200 resident bytes. The measured GPU p50 passes the Tier-2+ ≤0.40 ms +reference target; connected weather/occluder behavior and other adapters still +require their own rows. + +acdream's default path has a stricter gate: zero new enhancement passes, +images, buffers, submissions, or shader variants, with CPU/GPU deltas within +the existing run-to-run noise envelope and deterministic reference captures +unchanged. `NoOpRenderPackProductionIntegrationTests` pins the pass list, +pipeline set, draw/dispatch tuple, framebuffer SHA-256, and resource ledger of a +**2x2 synthetic, one-draw composition fixture on a recording device that does +not rasterize** — it proves the controller arm adds nothing to that shape, not +that the production frame is unchanged. The matrix's six physical retail rows +record zero pack work at all three resolutions and both pacing modes. The +actual pack-off pixel and production-performance invariance against the +pre-campaign build `6c79d35c` was established later by Campaign VM slice VM0 +([report](../research/2026-08-22-vm0-default-path-invariance.md), corrected +2026-08-22). + +Measurement protocol: + +- Use existing asynchronous GPU timestamps and frame diagnostics. Never add a + `glFinish`/device-idle-style measurement fence to the frame loop. +- Runtime Auto compares the declared incremental CPU budget with pack-added + target-preparation, shadow, post, and volumetric recording only. The complete + enhanced main-world receiver recording is retained separately as an absolute + CPU diagnostic; it is not itself an incremental delta. GPU accounting remains + conservatively inclusive of the complete enhanced receiver pass and every + resolved pack pass, exactly once after asynchronous resolution. Identical + pack-off/on runs remain the authority for the final receiver CPU delta and + the complete physical incremental A/B result. +- Run capped and uncapped Release builds; record CPU/GPU p50, p95, and p99, + FPS, draw/dispatch submissions, shadow-caster count, cascade draw count, + transient/retained GPU bytes, and process working/private memory. +- Compare pack off, Low, Medium, and High with identical camera paths, render + resolution, active day group, authored celestial/time keyframe, entity set, + and warmed residency. +- Cover pinned dense Arwic, a foliage-heavy outdoor route, a building cluster, + moving-player/monster combat, dawn/noon/dusk/night plus sun/moon/no-source + transitions, clear/overcast/rain, a dungeon, portal travel, resize, reconnect, + and a long lifetime run. +- Measure 1920x1080, 2560x1440, and 3840x2160 on each supported physical GPU + class. Report—not hide—unavailable presets. +- No cascade may rerun CPU PView/portal traversal or issue per-object draws. + The pass records CPU classification calls and submission counts so this is + an enforced gate, not an architectural hope. +- Pipeline creation and pack validation occur before atomic activation. Normal + play may not hitch on first shadow, weather, caster, or quality use. + +## Quality scaling for weak hardware + +These are starting envelopes to validate, not asset or world guarantees. +Distances are metres and are always clamped to current resident world data. + +| Setting | Bloom/rays | Directional shadows | Volumetric shafts | Approx. depth-map memory at 32-bit depth | +|---|---|---|---|---:| +| Off / acdream default | Off | Off | Off | 0 MiB | +| Low | Quarter resolution | 2 × 768² cascades, about 72 m maximum reach | Off by default | 4.5 MiB | +| Medium | Half resolution | 3 × 1536² cascades, about 144 m maximum reach | Quarter resolution | 27 MiB | +| High | Half/full as measured | 4 × 2048² cascades, about 240 m maximum reach | Half resolution | 64 MiB | + +Additional scaling rules: + +- Prefer reducing cascade count, shadow resolution, reach, bloom/ray + resolution, and sample count before removing a feature's semantic + correctness. +- Preserve alpha-tested foliage and animated transforms at every shadow + quality. A cheaper preset may look softer or end sooner; it may not turn a + tree into a rectangle or freeze a monster's shadow. +- Clamp resource dimensions and bytes before allocation. A capability probe + that cannot support Low disables the pack and explains why. +- Preset availability uses the selected Vulkan adapter's probed 2-D image and + array-layer limits. Optional pack memory receives at most one eighth of its + device-local heap, capped at 256 MiB resident and 512 MiB transient; Auto + starts at Low when Medium is unavailable and acdream's default remains the fallback if + Low cannot fit. +- Optional Auto quality uses long hysteresis and stable frame-boundary swaps; + it never oscillates cascade layouts frame to frame. If Low stays over its + declared runtime GPU/CPU/resident budgets for 180 stable samples, Auto + retires the complete pack and returns to acdream's default with the measured + and declared limits in the visible failure reason. +- 4K defaults may choose lower post-process resolution because Tier 1 pays + approximately four times the 1080p pixel workload. + +## Constraints and traps + +This list carries forward every item in the findings' measured +[shadow-specific constraints](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#shadow-specific-constraints) +and adds the current renderer's ownership and lifecycle boundaries. + +1. **The renderer is the shipped pass-based Vulkan RHI.** Design against + `IGpuDevice` / `IGpuFrame` / `IGpuPassEncoder` and explicit pass/pipeline + descriptions. Do not revive an OpenGL backend or build a parallel renderer. +2. **The current PView graph is authoritative.** Shadow and atmosphere passes + consume its retained scene; they do not introduce a competing visibility + owner or change punch/seal, shared-alpha, particle, or private-viewport + ordering. +3. **CPU submission is the limiting dimension.** Reusing the full bounded + resident caster set is preferable to CPU-reculling it per cascade. GPU + culling is the only planned escalation. +4. **Alpha-tested foliage needs sampling and discard.** Reusing the existing + empty `portal_depth` fragment shader would cast solid tree rectangles. +5. **Animated casters use the existing N.5 SSBO transforms.** A second pose, + animation tick, or entity owner is forbidden. +6. **Indoors has no outdoor celestial directional shadow.** Dungeon/EnvCell + authored ambient and local lighting wins; outdoor sun/moon directional + shadows, sun rays, and sun shafts are gated off. +7. **Cascades are camera-relative and streaming-bounded.** They may not use a + fixed Dereth-wide extent, request landblocks, retain retired generations, or + draw stale portal destinations. +8. **Depth bias is specified in meaningful eye/world units.** A constant NDC + bias spans approximately `b*d²/near` metres of eye depth at distance and can + recreate issue #129's door-shaped holes through hills. Bias, normal offset, + cascade projection, near/far fitting, and reversed-depth conventions must be + tested together at near and far ranges. +9. **“Shadow” is an overloaded project term.** Existing `shadow_objects` and + `CPhysicsObj::add_shadows_to_cells` are collision registration, not light + shadows. New names use `DirectionalShadowMap`, `ShadowCaster`, or + `CelestialDirectionalShadow`; never generic `ShadowObject`. +10. **Authored celestial position, directional energy, and weather are + inputs.** Do not invent another celestial clock, light-energy channel, + weather state, or hard-coded dawn/noon schedule. The directional map uses + the visible above-horizon sun/dominant moon/secondary moon's exact rendered + direction but retail's single interpolated `DirColor * DirBright` colour/ + energy channel, per the + [celestial source research](../research/2026-08-22-dereth-celestial-shadow-sources.md). + Rays and shafts remain sun-only and use the pack's declared sun-elevation + curve and categorical `activeDayGroup` mapping. The decomp evidence proves + the category reaches the frame, not an authored numeric ray intensity, so + the enhancement mapping must remain explicit pack policy. +11. **Atmosphere ordering is deliberate.** Rays/shafts composite before + tonemapping; retained UI and private viewports remain outside main-world + post-processing. +12. **Transparency remains ordered.** The pack cannot flatten the retail + world-alpha queue into an unordered shadow/post pass. Truly translucent + surfaces cast no opaque shadow until separately designed. +13. **Generation and GPU-flight lifetimes remain exact.** Pack images, + descriptors, and pipelines retire through existing fences and converge on + disable, resize, portal, reconnect, reset, failure, and device recreation. +14. **4K is a distinct performance row.** Tier-1 effects scale with pixels; + passing at 1080p is not evidence for 4K. +15. **Do not repeat closed investigations.** High-res DAT precedence is not + dropping overrides, AC detail textures are colour/alpha rather than normal + maps, and the engine's historical DOT3 capability does not turn those + assets into PBR inputs; these points are already falsified in the findings. +16. **Caster evidence must not exceed source identity.** Diagnostics separately + count terrain commands, outdoor statics, buildings, animated statics, + local/remote players, non-player creatures, other live dynamics, and + equipped children. Outdoor statics include trees but have no authoritative + tree discriminator; non-player creatures include monsters but have no + render-only hostile-monster-versus-NPC discriminator. Visual/connected + acceptance must name those limits instead of fabricating narrower counts. + +## What this does NOT do + +- It does **not** change acdream's default retail-faithful rendering path, its + expected output, or its authority in fidelity tests. +- The Atmospheric shader pack does **not** own or depend on #226 detail + texturing or the terrain-normal parity correction. Those remain separate + Track A ports even though the project owner authorized their implementation + in the same worktree. +- It does **not** add PBR or fabricate normal, roughness, metalness, or material + maps that AC's assets do not contain. +- It does **not** change terrain vertices, collision triangles, walkability, + slope response, physics shadow lists, movement, projectiles, or any Runtime + physics/collision owner. +- It does **not** change gameplay rules/state, network messages or ordering, or + any Runtime gameplay/network owner. +- It does **not** extend view distance, streaming radius, landblock residency, + or PView visibility to find more shadow casters. +- It does **not** add an indoor sun or replace authored EnvCell/local lights. +- It does **not** turn moon texture brightness or mesh luminosity into another + world-light energy channel, and it does not produce moon rays or moon shafts. +- It does **not** post-process retained UI or silently restyle private + paperdoll, appraisal, or portal viewports. +- It does **not** promise that every pack or quality preset runs on unsupported + hardware; compatibility failure is explicit and safely returns to acdream's default renderer. +- Campaign AR does **not** outrank active M4 gameplay work. The project owner's + explicit reprioritization authorizes this campaign without changing M4's + milestone priority; #268 + TS-8 are already complete and retired. + +## Completion gate + +The requirement ledger and the exact remaining external rows are consolidated +in the [Campaign AR completion audit](../research/2026-08-22-atmospheric-campaign-completion-audit.md). + +The design, implementation, Stage 1 owner gate, and every available +machine-local Stage 2 connected, performance, lifetime, package, shader, build, +and complete-test gate are complete. The campaign becomes **shipped** only +after the distinct-account second-client remote-player row, remaining physical- +hardware rows, and final visual gates above pass and the project owner accepts +both sides of the final matrix: + +- **pack off:** unchanged output, performance, ownership, and lifecycle from + acdream's authoritative default retail-faithful renderer; and +- **pack on:** moving authored sun-and-moon directional shadows from trees, + monsters, players, and buildings; sun-only rays/shafts; scalable atmosphere, + safe compatibility fallback, measured budgets, and clean long-lived resource + convergence. diff --git a/docs/plans/2026-08-21-chat-text-tag-campaign.md b/docs/plans/2026-08-21-chat-text-tag-campaign.md new file mode 100644 index 00000000..d81c7871 --- /dev/null +++ b/docs/plans/2026-08-21-chat-text-tag-campaign.md @@ -0,0 +1,199 @@ +# Campaign CT — complete chat parity (system + GUI) + +**Status:** Groups A, B, C and D COMPLETE 2026-08-21, each user-gated. +CT-B4 landed 2026-08-21 after the research block turned out to rest on a wrong +premise (see the slice). One item deliberately not shipped: CT-B3 (word +filtering — dropped by user direction, register row CT-2). + +**Carried forward:** ~~multi-frame state media~~ **DONE 2026-08-21.** The +importer now keeps the whole authored sequence and `UiMediaSequence` plays it. +Measuring the real data (`LayoutDump --media 0x1000048C`) corrected the +behaviour as well as enabling it: the indicator blinks three times over three +seconds and then hands off to `Ghosted`, hiding itself. Retail's is a transient +attention-flash, not a badge that stays lit until you scroll down. Register +rows CT-3, CT-4. + +**Goal, set by the user 2026-08-21: complete retail parity for the chat +system AND the chat GUI.** Not "fix the green name" — that was the symptom +that started the review. The bar is that a retail player sitting down in front +of acdream's chat window finds nothing missing and nothing behaving +differently. + +Campaign CH (2026-08-09, closed user-accepted) landed colours, side channels, +the 152-verb command registry, the window shell and verbatim `/help`. CT is +the pass that closes what CH did not reach. + +Research notes (all 2026-08-21): `chat-texttag-model.md`, +`chat-tagged-name-composition.md`, `chat-tag-click-dispatch.md`, +`retail-chat-window-ui.md`, `acdream-text-stack-audit.md`, +`acdream-chat-ui-audit.md`. + +## Definition of done + +1. Every retail chat behaviour is either implemented, or has a divergence- + register row saying why not. +2. Every user-visible chat surface has a test that would catch its regression. +3. The chat digest and `docs/ISSUES.md` describe reality (both are stale today). + +## What the review established + +### The green clickable name is a TEXT-STACK gap, not a chat gap + +The client sprintfs literal markup into the line — +`{name}<\Tell> says, "{text}"`, closing marker a +literal backslash — and `UIElement_Text::InqGlyphs @0x00468EA0` parses the +brackets while appending, calling `TextTagFactory::MakeTag @0x00478480`. Tags +attach **per glyph**; a "run" is emergent (adjacent glyphs with equal tag +pointers). A glyph takes the tag colour (property `0x1D`) only when a tag is +open AND its `m_type == 0x10000001`, else the line colour (`0x1B`). Only +senders with a GUID in `0x50000001..0x6FFFFFFF` are tagged. + +Colour **measured** from the installed dats (`LayoutDump --colors`), chat +`0x2100006F` / transcript `0x10000011`: `P0x1B` = RGB(204,204,204), +`P0x1D` = **RGB(0,178,0)**. The tag colour is per-ELEMENT and authored, while +the line colour on that same element comes from the runtime chat table — +filing "tag green" into the LogTextType table would put it in the wrong place. + +Click: `UIElement_Text::MouseUp @0x004694F0` → `DeterminePositionFromXY +@0x004688F0` → `GlyphList::InqGlyph @0x00473430` → virtual `HandleClick` at +tag-vtable `+0x14` → `gmMainChatUI::RecvNotice_TextTag_IIDStringClick +@0x004CCE10` → `ChatInterface::StartTell @0x004F41F0`, which writes +`"@tell {Name}, "`, takes focus, and shows the entry bar. Clicking a name +always opens a TELL — fellowship, allegiance, patron/vassal and named-channel +lines all embed the same markup. No hover effect. + +### Our side is closer than feared + +`UiText` **already** draws multi-coloured runs (`TextRun`/`RunsProvider`, used +by the character stat panel); it is gated to `OneLine == true`. The draw path +needs no renderer work — arbitrary pen X, substring measurement — and +`UiText.HitChar` already resolves a click to (line, column). The blocker is +that `ChatVM.RecentLinesDetailed()` drops `Sender`/`SenderGuid` one step before +the renderer, though `ChatEntry` carries them the whole way. + +### The command registry is already at parity + +All 13 verbs the CH3 research note lists as MISSING were closed by CH4 and +verified present 2026-08-21 (`cg`, `soc`, `o`, `co-vassals`, `fellows`, +`group`, `party`, `vassal`, `ab`, `guild`, `ct`, `clfg`, `crp`). `/g` correctly +resolves to Fellowship, confirmed against the live retail client. That note's +"acdream status" columns are stale and now carry a correction banner. + +## Slices + +### Group A — the tagged-text capability (strict chain, A1→A5) + +Nothing is user-visible until A4. + +- **CT-A1** Multi-line text elements carry coloured runs. Additive; the ~50 + files using `Line` are untouched. No behaviour change. +- **CT-A2** Parse the tag markup into runs with a tag payload, including + retail's rule that an unparseable bracket closes the open tag. Pure, unit- + testable, no UI. +- **CT-A3** Stop flattening: carry sender name + guid through `ChatVM` into + spans, and compose retail's markup in the speech handlers behind the + player-GUID-range gate. +- **CT-A4** Apply the authored `0x1D` tag colour when a tag is open and its + type matches. **Names turn green.** +- **CT-A5** Sub-line hit-testing and `StartTell`. **Names become clickable.** + +### Group B — chat SYSTEM behaviours + +- **CT-B1** Bound the transcript: 10,000 chars, trim to ~7,500 preferring a + newline boundary (`TruncateChatLog @0x004F4290`). Today it grows for the life + of the session — a slow leak, not only a fidelity gap. +- **CT-B2** Text-replacement macros: typing `/r `, `/t `, `/tell ` rewrites the + input to `@tell {LastTeller}, ` on the space keypress + (`HandleTextReplacements @0x004F50D0`). The commands already work; the + visible expansion does not exist. +- **CT-B3** ~~`FilterLanguage` word filtering~~ — **DROPPED by user direction + (2026-08-21): "I do not want any censoring."** acdream keeps the option + itself, which still stores and ships its bit to the server exactly as retail + does, but performs no client-side substitution. Registered as CT-2 rather + than left as an implicit gap, since it IS a knowing departure from retail + (`PlayerModule::FilterLanguage` + `TabooTableAdaptor::CheckCensorsW + @0x00682A30` inside `AddTextToScroll`). + + Worth keeping on record, because the attempt established two things that + would otherwise be rediscovered if this is ever revisited: + + 1. The table's dat id is not readable from the decomp — `CheckCensorsW` + reaches it through `DBObj::GetByEnum` with the arguments elided by Binary + Ninja. The portal master enum map (`0x25000000`) has 22 categories, with + category 3 (`0x0E010001`, `0x0E010002`) and the single-entry categories 8 + and 11 the plausible candidates. + 2. Chorizite.DatReaderWriter declares a `TabooTable` type but does NOT decode + it — only `DBObjType` and `HeaderFlags`. The format would have to be + decoded here first. + + And the matching is an algorithm, not a word list: + `TabooTable::CreateCheckString @0x00681570` normalises a candidate before + `StringMatchesFilter @0x00681600` compares it, which is how retail catches + obfuscated spellings. +- **CT-B4** ~~The plain-text session chat log~~ **DONE — and the premise was + wrong.** There is no automatic session log to have a path for. Retail's + `@log` is a COMMAND: `ClientCommunicationSystem::DoSetOutput @0x0057E4F0` + takes a filename, `StartCopyOutputToFile @0x0057C8A0` does the + `fopen(name, "a+")`, and running it again with no argument closes it. So + "path and rotation UNKNOWN" was asking a question the design does not have: + the player names the file, and there is no rotation because it appends + forever. + + The path question that DOES exist — where a bare name lands — is answered by + retail's own help text, which the CH4 help table already carried verbatim + without anyone reading it: "a log file named Aclog.txt **in your Asheron's + Call directory**". acdream cannot use the install directory (the launcher + replaces it atomically on update), so a bare name lands in the client's own + log directory. Rooted paths are honoured verbatim. Register row CT-5. + + Landed with it: the verb registered in the catalog (it had a help entry + since CH4 but no catalog entry, so `/log` printed help and did nothing), + retail's `.txt`-for-extensionless rule, all five reply strings byte-decoded + from the paired binary, and the writer attached at OPEN so only text after + the command is copied. The line logged is the composed display line with the + shared timestamp, because retail's `fprintf` sits inside `AddTextToScroll` + — downstream of composition, upstream of glyph layout. + +### Group C — chat GUI + +- **CT-C1** Auto-scroll vs unread: retail samples "was at bottom" BEFORE the + line lands; if you had scrolled up it leaves you there and lights the unread + indicator (`0x1000048C`), which scrolls to bottom and clears on click. +- **CT-C2** Escape in the chat input is a complete no-op — `UiField` has no + `Escape` case, and a focused field also suppresses the input dispatcher's + fallback, so there is no clear, no defocus and no hotkey passthrough. +- **CT-C3** Option-gated timestamp prefix (`%#H:%M:%S `, colour index `0x0C`, + grey), gated on `PlayerModule::DisplayTimeStamps()`. +- **CT-C4** Input-bar editing parity: clipboard and selection paths + (Ctrl+C/X/V, shift-selection) work but are untested; `ToggleMaximize` and the + floating-window Close button have zero coverage. **DEFERRED** — pure test + coverage over behaviour the audit confirmed already works, so it changes + nothing a user can see. Worth doing; not worth blocking the campaign on. + +### Group D — hygiene + +- **CT-D1** ~~Delete the dead ImGui-era `ChatPanel`~~ **DONE.** Verified never + constructed in `src/`, then removed with its three panel-only test files. + `ChatVMCombatTests` was KEPT — three of its four tests are real `ChatVM` + coverage; only the one `ChatPanel` render test went. +- **CT-D2** ~~Reconcile the chat digest and `docs/ISSUES.md`~~ **DONE.** + `docs/ISSUES.md` turned out to be ACCURATE already — #358 and #363 are + recorded CLOSED there. Only the chat digest's "Open" section was stale, and + it is corrected: genuinely open are #359, #360, #361, #366. The digest also + gained a Campaign CT section and three new DO-NOT-RETRY rows. + +## Research still owed before the affected slices + +- The tag-type roster behind `m_type == 0x10000001` — only "Tell" is + confirmed; the full set lives in the DAT `EnumMapper` category `0x18`. + Blocks nothing in Group A, but decides whether other tag shapes exist. +- Whether retail's transcript supports text selection distinctly from the + entry field (blocks CT-C4's scope). +- Whether a chat-specific sound cue exists — a grep came back empty, which is + weak evidence, not proof of absence. + +## Deliberately NOT in scope + +Item links and the other three tag shapes (`DID`, `IID`, `IIDEnum`). They have +no listener in the retail build we target, so porting them would be inventing +behaviour. CT-A5's dispatch is generic, so they cost nothing to add later. diff --git a/docs/plans/2026-08-21-contract-tracker-campaign.md b/docs/plans/2026-08-21-contract-tracker-campaign.md new file mode 100644 index 00000000..7fa6bbe2 --- /dev/null +++ b/docs/plans/2026-08-21-contract-tracker-campaign.md @@ -0,0 +1,200 @@ +# Campaign QT — the contract tracker (H.3's client half) + +**Status:** CLOSED USER-ACCEPTED 2026-08-21. All six slices landed and the +connected gate passed, together with Campaign QJ's two tabs. + +**Why now.** M4's demo scenario is "talk to an NPC, accept a quest, ... complete +the quest." Everything in that sentence works today EXCEPT the player's ability +to see what they have accepted. NPC dialogue, emote text, soul emotes, tells and +the quest-failure strings all render; Campaign CT (closed 2026-08-21) added the +`` markup those dialog lines carry. What is missing is the +only STRUCTURED view of quest state a retail client ever gets. + +**What H.3 is not.** The roadmap line reads "122 EmoteType × 39 Trigger +mini-VM", which describes the SERVER's job. Per `r10-quest-dialogs.md` §1.3 the +retail client never stores a quest flag, never evaluates an emote, and is never +told a flag changed. It learns about quests three ways: dialog strings the +server already formatted, generic error toasts, and the contract tracker. Two of +the three ship. So H.3's remaining client scope is this campaign, and the emote +VM is explicitly out of it. + +## Measured ground truth + +### The panel + +`LayoutDump --find 0x1000004B` (the `UIElement::RegisterElementClass` id from +`gmContractsUI::Register @0x00499C80` — registration keys on the element's +**Type**, not its id) finds the class in six layouts. `0x21000069` holds it as a +standalone 300x500 root (`0x100005CD`); the rest embed it at 300x575 inside +window chrome. + +Authored children of `0x100005CD`: + +| Element | Type | Rect | Reading | +|---|---|---|---| +| `0x100005CE` | 1 | 8,8 80x18 | header button | +| `0x100005D6` | 1 | 160,8 80x18 | header button | +| `0x100005CF` | 5 | 8,30 270x298 | the contract list | +| `0x100005D0` | 11 | 278,30 16x298 | its scrollbar | +| `0x100005D8`/`0x100005DF` | 12 | y=332 | label / value | +| `0x100005D9`/`0x100005E0` | 12 | y=352 | label / value | +| `0x100005DA`/`0x100005E1` | 12 | y=372 | label / value | +| `0x100005DB`/`0x100005E2` | 12 | y=392 | label / value | +| `0x100005DE` | 12 | 8,418 270x52 | description block | +| `0x100005DD`, `0x100005E3`, `0x100005DC` | 12/12/1 | y=468 | button row | + +### The wire + +Both opcodes are already NAMED in `GameEventType.cs` and nothing parses them — +the bytes arrive and are dropped. + +`0x0315 SendClientContractTracker` — one tracker plus two flags: + +``` +uint32 Version +uint32 ContractId +uint32 Stage +double TimeWhenDone +double TimeWhenRepeats +uint32 DeleteContract (bool widened) +uint32 SetAsDisplayContract (bool widened) +``` + +`0x0314 SendClientContractTrackerTable` — a full replacement, as a packable +hash table (`PackableHashTable` in the decomp +at `0x00497C10`): the familiar `u16 count` / `u16 numBuckets` header, then +`u32 key` + the 28-byte tracker per entry. NO trailing flags on this path. + +Source: `ContractTrackerExtensions.Write`, `GameEventSendClientContractTracker`, +`ContractManager.Write` in ACE; cross-checked against the retail decomp's own +`PackableHashTable` instantiations. + +`ContractStage`: `1` Available, `2` InProgress, `3` DoneOrPendingRepeat, +`4 + n` ProgressCounter with n steps done. + +### `gmContractsUI::FillProgressString @0x00498DE0` — the one real algorithm + +Recovered whole. The x87 compares are the standard `fcom` + `sahf` pattern; +`(status & 0x41) != 0` tests C0|C3, i.e. **<= 0**. + +``` +stage 1 -> "Available" +stage 2 -> "In Progress" +stage 3: + if TimeWhenRepeats <= 0 + -> QuestflagRepeatTime empty ? "Done" : "Available" + remaining = TimeWhenRepeats - (now - timeOfServerUpdate) + if remaining <= 0 -> "Available" + else -> "Done (" + DeltaTimeToString(remaining) + " to Repeat)" +stage >= 4: + if DescriptionProgress empty -> "In Progress" + else -> sprintf(DescriptionProgress, stage - 4) +``` + +Three things a reimplementation would get wrong: + +1. **`TimeWhenDone` is never read.** Only `TimeWhenRepeats` drives the text. +2. **`timeOfServerUpdate` is not on the wire.** The client stamps arrival and + counts down from its own clock, so the countdown has to be anchored at parse + time, not recomputed from the server value each frame. +3. **`DescriptionProgress` is a printf format** taking one integer, `stage - 4`. + It is not a literal string. + +### It is not a "contract panel" — it is tab 1 of the JOURNAL panel + +Measured from the installed dats. Host `0x2100006E`, `gmPanelUI` slot +`0x10000559`, whose own authored `0x10000029` is **`0x19` = 25** — the same +slot-key recipe `RetailPanelCatalog` already uses for Options (10), the social +panel (12) and Map/House (16). Three tabs: + +| Tab | Caption | Page | Page type | +|---|---|---|---| +| `0x100005D3` | **Contracts** | `0x100005D4` | `0x1000004B` = `gmContractsUI` | +| `0x10000560` | **Journal** | `0x10000563` | `0x10000048` — notes: "Title:", "Notes:", "First" | +| `0x10000561` | **Page List** | — | — | + +`0x10000562` (type 1, at 276,0) is the panel's own corner button. + +Only the Contracts tab is in scope. The Journal notes page and Page List are +their own feature and are NOT part of Campaign QT — mounting the panel with two +dead tabs is the expected intermediate state, not a defect. + +### The contracts page, resolved + +Authored text read out of the dats (`LayoutDump --props`, which now resolves +`StringInfo` rather than printing the type name): + +| Element | Role | +|---|---| +| `0x100005CE` / `0x100005D6` | list column headers — "Contract" / "Status" | +| `0x100005CF` (type 5) | the list, scrollbar `0x100005D0` via property `0x72` | +| `0x100005D1` / `0x100005D2` | per-ROW children: contract name / progress text | +| `0x100005D8` → `0x100005DF` | "Status:" → value | +| `0x100005D9` → `0x100005E0` | "Contact:" → value | +| `0x100005DA` → `0x100005E1` | "Contact Location:" → value | +| `0x100005DB` → `0x100005E2` | "Quest Location:" → value | +| `0x100005DE` | description block (270x52, wrapping) | +| `0x100005DD` → `0x100005E3` | "Timed:" → value | +| `0x100005DC` | "Abandon" button | + +`gmContractsUI::RefreshContractListbox @0x00499830` walks the tracker list and, +per row, sets `0x100005D1` from the contract's name and `0x100005D2` from +`FillProgressString`, caching the result back into the row. The list is a +`UiTemplateListBox` here — the same widget OP2 built for the Options panel — so +the page is binding rather than new widget work. + +## Slices + +- **QT1 — wire.** Typed records + parsers for `0x0314`/`0x0315`, arrival stamp + included. Pure; no UI, no state ownership. +- **QT2 — dat.** Read `ContractTable`/`Contract` (name, description, progress + description, NPC names, the three positions). Nothing reads it today; the + only reference in the tree counts them in a CLI diagnostic. +- **QT3 — state.** `RuntimeContractState` as a session-scoped J4-style owner: + full replace, single add/update, delete, and the display-contract selection. + Clears at generation reset. +- **QT4 — the progress string.** Port `FillProgressString` + the retail + `DeltaTimeToString` it calls. Table-driven tests over every stage arm. +- **QT5 — the panel.** Register slot 25 in `RetailPanelCatalog`, mount the + Journal panel by the OP3/FA recipe, and bind the Contracts page: rows from + `IRuntimeContractView` x `ContractCatalog`, progress from QT4, selection + driving the detail pane. The other two tabs mount empty. +- **QT6 — open/close.** The open path (no toolbar button authors slot 25, so + it is keyboard or menu — to be measured the way FA's F3/F4 was), plus the + plugin-visible read surface from `r10-quest-dialogs.md` §11.6. + +### Landed + +QT1 `ab3934e2` (wire), QT3 `f629ce7f` (state + routing), QT2/QT4 `ef6b7310` +(catalog + progress string), QT5/QT6 (the panel and its open path). + +**The open path needed no new keybind.** Toolbar button `0x1000055A` authors +`0x10000029 = 0x19` and has been in `ToolbarController.PanelButtonIds` since +the toolbar was ported — it simply had no panel registered behind it, so +clicking it did nothing. Registering slot 25 completed a wiring that was +already three-quarters present. + +**The plugin surface** (`r10-quest-dialogs.md` §11.6's contract half) ships as +`IGameState.Contracts`, projected through `ContractPluginProjection` — a +pull-through view of the canonical tracker, never a mirror. Both hosts +implement it; the headless one carries the numeric fields without the authored +text, since a bot has no dat access. The rest of §11.6 (chat stream, tells, +give, use, confirmations) is other features and stays out of Campaign QT. + +### Owed + +- The connected user gate: accept a quest against live ACE, open the Journal + panel, confirm the list, the progress column and a repeat countdown. +- ~~The Abandon button is deliberately unwired~~ **WIRED 2026-08-21.** The + claim that it had no wire message was wrong: it is game action `0x0316` + carrying one contract id, and ACE answers with the `0x0315` delete QT3 + already handles. Nothing is removed locally, so a refused abandon leaves the + quest visibly intact. +- The Journal notes page and Page List tabs mount inert, by design. + +## Definition of done + +1. Accepting a quest against live ACE shows it in the panel; completing it + updates the stage; a repeatable one shows its countdown. +2. Every ported algorithm cites its retail address. +3. Every slice has a test that would catch its regression. diff --git a/docs/plans/2026-08-21-journal-campaign.md b/docs/plans/2026-08-21-journal-campaign.md new file mode 100644 index 00000000..0f1d9ddd --- /dev/null +++ b/docs/plans/2026-08-21-journal-campaign.md @@ -0,0 +1,121 @@ +# Campaign QJ — the Journal and Page List tabs + +**Status:** CLOSED USER-ACCEPTED 2026-08-21. All five slices landed and the +connected gate passed. + +The gate took four rounds, and every defect it found was the same mistake in a +different place — an element bound as the wrong thing, or a binding never +tested: + +1. **Button property `0x0D`** read as "starts disabled", which killed every + button on the panel (register QJ-2). +2. **The location readout** is authored EDITABLE, so it is a `UiField`; bound + as `UiText` it silently discarded every write — the value reached the model + and the file and never the screen. +3. **Handlers deferred their redraw** to the next frame's `Tick` where retail + redraws at the click. +4. **The timer's unit labels** stayed visible behind the running readout, + because retail's `ShowEditableTimer` toggles each box AND its label. + +Round 3's "Record does nothing" turned out not to be a defect at all: the +character was indoors, where retail's own `gid_to_lcoord` fails and nothing is +recorded. Faithful, and now commented so it does not read as a gap. + +The durable outcome is `JournalPanelLiveBindTests` — see +`claude-memory/feedback_test_the_binding_seam.md`. Completes the panel Campaign QT mounted: QT +shipped the Contracts tab and left the other two inert by design. + +**Scope:** retail's `gmJournalUI` (element type `0x10000048`, page +`0x10000563`) and `gmPageListUI` (type `0x10000049`, page `0x10000564`). + +## What this actually is + +A **per-character notebook**, entirely client-side. No wire, no server +involvement, no dat content — the player writes the pages. Each page carries a +label, a title, free-form notes, a recorded LOCATION, and a countdown TIMER. +The Page List tab is a searchable index over those pages. + +Nothing about it depends on quests; it shares the panel with Contracts and +nothing else. That it is called "Journal" while the panel is also called +"Journal" is retail's own naming, not a mistake here. + +## Measured ground truth + +### The file format + +`gmJournalUI::SavePages @0x00497270` / `LoadPages @0x00496AC0`. A plain tagged +text file, `fopen` mode `w+`. Both call sites pass the literal prefix +`"Journal"`; the path template is `%s%s-%s-%s.txt`, i.e. +`{dir}Journal-{server}-{character}.txt`. + +``` + begins a page (a file that does not open with one is refused) + %d page number + %s label (authored max length 16) + %s title (32) + %s notes (2048) + %d timer days + %d timer hours + %d timer minutes + %f recorded location + %f +