Compare commits
No commits in common. "main" and "claude/git-sync-status-5fb1d2" have entirely different histories.
main
...
claude/git
|
|
@ -1,227 +0,0 @@
|
||||||
# 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)"
|
|
||||||
}
|
|
||||||
3
.github/workflows/copilot-setup-steps.yml
vendored
|
|
@ -3,6 +3,9 @@ name: "Copilot Setup Steps"
|
||||||
# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server
|
# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent
|
# The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent
|
||||||
|
|
|
||||||
145
.github/workflows/headless-portability.yml
vendored
|
|
@ -1,6 +1,48 @@
|
||||||
name: Headless portability
|
name: Headless portability
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/headless-portability.yml"
|
||||||
|
- "AcDream.slnx"
|
||||||
|
- "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.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/**"
|
||||||
|
- "tools/ShaderCompiler/**"
|
||||||
|
- "tools/compile-shaders.ps1"
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/headless-portability.yml"
|
||||||
|
- "AcDream.slnx"
|
||||||
|
- "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.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/**"
|
||||||
|
- "tools/ShaderCompiler/**"
|
||||||
|
- "tools/compile-shaders.ps1"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -18,13 +60,13 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install pinned .NET SDK
|
- name: Install .NET 10
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
global-json-file: global.json
|
dotnet-version: "10.0.x"
|
||||||
|
|
||||||
# No apt step here on purpose. This job's whole claim is that the closure
|
# No apt step here on purpose. This job's whole claim is that the closure
|
||||||
# below is presentation-free: it builds Bake, Plugin.Abstractions, Core,
|
# below is presentation-free: it builds Plugin.Abstractions, Core,
|
||||||
# Core.Net, Content, Runtime and Headless, runs their tests, and invokes
|
# Core.Net, Content, Runtime and Headless, runs their tests, and invokes
|
||||||
# the Headless CLI. Nothing in it opens a display, links GL, or calls
|
# the Headless CLI. Nothing in it opens a display, links GL, or calls
|
||||||
# xvfb-run, so an "install the graphical smoke dependencies" step here was
|
# xvfb-run, so an "install the graphical smoke dependencies" step here was
|
||||||
|
|
@ -36,9 +78,6 @@ jobs:
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$projects = @(
|
$projects = @(
|
||||||
"src/AcDream.Platform/AcDream.Platform.csproj",
|
|
||||||
"src/AcDream.Launcher.Core/AcDream.Launcher.Core.csproj",
|
|
||||||
"src/AcDream.Bake/AcDream.Bake.csproj",
|
|
||||||
"src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj",
|
"src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj",
|
||||||
"src/AcDream.Core/AcDream.Core.csproj",
|
"src/AcDream.Core/AcDream.Core.csproj",
|
||||||
"src/AcDream.Core.Net/AcDream.Core.Net.csproj",
|
"src/AcDream.Core.Net/AcDream.Core.Net.csproj",
|
||||||
|
|
@ -59,9 +98,6 @@ jobs:
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$projects = @(
|
$projects = @(
|
||||||
"tests/AcDream.Platform.Tests/AcDream.Platform.Tests.csproj",
|
|
||||||
"tests/AcDream.Launcher.Core.Tests/AcDream.Launcher.Core.Tests.csproj",
|
|
||||||
"tests/AcDream.Bake.Tests/AcDream.Bake.Tests.csproj",
|
|
||||||
"tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj",
|
"tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj",
|
||||||
"tests/AcDream.Content.Tests/AcDream.Content.Tests.csproj",
|
"tests/AcDream.Content.Tests/AcDream.Content.Tests.csproj",
|
||||||
"tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj",
|
"tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj",
|
||||||
|
|
@ -81,89 +117,6 @@ jobs:
|
||||||
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- validate --config headless-k0.json
|
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- validate --config headless-k0.json
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
|
||||||
- name: Verify Linux headless host executable permission
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
test -x src/AcDream.Headless/bin/Release/net10.0/acdream-headless
|
|
||||||
|
|
||||||
portable-launcher:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest, ubuntu-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Install pinned .NET SDK
|
|
||||||
uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
|
||||||
global-json-file: global.json
|
|
||||||
|
|
||||||
- name: Build and test the portable launcher
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
dotnet build src/AcDream.Launcher/AcDream.Launcher.csproj -c Release
|
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
||||||
dotnet test tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj -c Release
|
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
||||||
|
|
||||||
- name: Publish the self-contained launcher distribution
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$rid = if ($IsWindows) { "win-x64" } else { "linux-x64" }
|
|
||||||
dotnet publish src/AcDream.Launcher/AcDream.Launcher.csproj `
|
|
||||||
-c Release `
|
|
||||||
-r $rid `
|
|
||||||
-o "artifacts/acdream-launcher-$rid"
|
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
||||||
|
|
||||||
- name: Verify self-contained Windows launcher and bake artifacts
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$root = "artifacts/acdream-launcher-win-x64"
|
|
||||||
if (-not (Test-Path -LiteralPath "$root/acdream-launcher.exe" -PathType Leaf)) { throw "launcher executable missing" }
|
|
||||||
if (-not (Test-Path -LiteralPath "$root/acdream-bake.exe" -PathType Leaf)) { throw "bake executable missing" }
|
|
||||||
if (Test-Path -LiteralPath "$root/acdream-launcher.dll") { throw "launcher is not single-file" }
|
|
||||||
if (Test-Path -LiteralPath "$root/acdream-bake.dll") { throw "bake is not single-file" }
|
|
||||||
$env:DOTNET_ROOT = "Z:\definitely-not-installed"
|
|
||||||
$env:DOTNET_ROOT_X64 = "Z:\definitely-not-installed"
|
|
||||||
$env:DOTNET_MULTILEVEL_LOOKUP = "0"
|
|
||||||
& "$root/acdream-launcher.exe" --verify-publish
|
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
||||||
& "$root/acdream-bake.exe" --help
|
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
||||||
|
|
||||||
- name: Verify self-contained Linux launcher and bake artifacts
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
root=artifacts/acdream-launcher-linux-x64
|
|
||||||
self_contained=$(dotnet msbuild \
|
|
||||||
src/AcDream.Launcher/AcDream.Launcher.csproj \
|
|
||||||
-nologo \
|
|
||||||
-property:RuntimeIdentifier=linux-x64 \
|
|
||||||
-getProperty:SelfContained | tr -d '\r\n ')
|
|
||||||
test "$self_contained" = true
|
|
||||||
test -x "$root/acdream-launcher"
|
|
||||||
test -x "$root/acdream-bake"
|
|
||||||
test ! -f "$root/acdream-launcher.dll"
|
|
||||||
test ! -f "$root/acdream-bake.dll"
|
|
||||||
DOTNET_ROOT=/definitely-not-installed \
|
|
||||||
DOTNET_ROOT_X64=/definitely-not-installed \
|
|
||||||
DOTNET_MULTILEVEL_LOOKUP=0 \
|
|
||||||
"$root/acdream-launcher" --verify-publish
|
|
||||||
DOTNET_ROOT=/definitely-not-installed \
|
|
||||||
DOTNET_ROOT_X64=/definitely-not-installed \
|
|
||||||
DOTNET_MULTILEVEL_LOOKUP=0 \
|
|
||||||
"$root/acdream-bake" --help
|
|
||||||
|
|
||||||
linux-graphical:
|
linux-graphical:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|
@ -171,10 +124,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install pinned .NET SDK
|
- name: Install .NET 10
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
global-json-file: global.json
|
dotnet-version: "10.0.x"
|
||||||
|
|
||||||
- name: Build and publish Linux graphical client
|
- name: Build and publish Linux graphical client
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
@ -264,10 +217,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install pinned .NET SDK
|
- name: Install .NET 10
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
global-json-file: global.json
|
dotnet-version: "10.0.x"
|
||||||
|
|
||||||
- name: Install lavapipe, the Vulkan loader and Xvfb
|
- name: Install lavapipe, the Vulkan loader and Xvfb
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
||||||
4
.github/workflows/hygiene-assessment.lock.yml
generated
vendored
|
|
@ -49,6 +49,9 @@
|
||||||
|
|
||||||
name: "acdream Hygiene Assessment"
|
name: "acdream Hygiene Assessment"
|
||||||
on:
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "54 4 * * *"
|
||||||
|
# Friendly format: daily (scattered)
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
@ -1345,3 +1348,4 @@ jobs:
|
||||||
/tmp/gh-aw/safe-output-items.jsonl
|
/tmp/gh-aw/safe-output-items.jsonl
|
||||||
/tmp/gh-aw/temporary-id-map.json
|
/tmp/gh-aw/temporary-id-map.json
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
|
|
||||||
35
.github/workflows/release-gate.yml
vendored
|
|
@ -1,35 +0,0 @@
|
||||||
name: Complete Release gate
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
complete-release:
|
|
||||||
name: Complete Release suite (Windows)
|
|
||||||
runs-on: windows-latest
|
|
||||||
timeout-minutes: 45
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Install pinned .NET SDK
|
|
||||||
uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
|
||||||
global-json-file: global.json
|
|
||||||
|
|
||||||
- name: Run complete bounded Release gate
|
|
||||||
shell: pwsh
|
|
||||||
run: ./tools/run-release-gate.ps1
|
|
||||||
|
|
||||||
- name: Upload Release gate evidence
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-gate-${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
path: artifacts/release-gate/
|
|
||||||
10
.gitignore
vendored
|
|
@ -2,11 +2,6 @@
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
out/
|
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
|
# Rider / VS
|
||||||
.idea/
|
.idea/
|
||||||
|
|
@ -113,8 +108,3 @@ studio-shots/
|
||||||
|
|
||||||
# Campaign V capture/evidence output - session-local, never tracked (423 MB lesson, 2026-07-29)
|
# Campaign V capture/evidence output - session-local, never tracked (423 MB lesson, 2026-07-29)
|
||||||
artifacts/
|
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/
|
|
||||||
|
|
|
||||||
38
AGENTS.md
|
|
@ -45,14 +45,13 @@ in `src/AcDream.*` references it as a project dependency.
|
||||||
`TextureCache`, `GlobalMeshBuffer`, shader infrastructure, and the
|
`TextureCache`, `GlobalMeshBuffer`, shader infrastructure, and the
|
||||||
EnvCell/portal/scenery/terrain-blending pipeline classes.
|
EnvCell/portal/scenery/terrain-blending pipeline classes.
|
||||||
|
|
||||||
**Modern rendering path is MANDATORY.** The N.5 ship amendment deleted
|
**Modern rendering path is MANDATORY** as of the N.5 ship amendment.
|
||||||
`WbFoundationFlag`, `InstancedMeshRenderer`, and `StaticMeshRenderer`;
|
`WbFoundationFlag`, `InstancedMeshRenderer`, and `StaticMeshRenderer`
|
||||||
Campaign V (`docs/plans/2026-07-27-vulkan-campaign.md`, closed
|
are deleted. Missing `GL_ARB_bindless_texture` or
|
||||||
2026-07-29) then ported the renderer to Vulkan behind the RHI contract
|
`GL_ARB_shader_draw_parameters` throws `NotSupportedException` at
|
||||||
and deleted the OpenGL backend outright — `AcDream.App` references only
|
startup. There is no legacy fallback. Engineering cribs (WbMeshAdapter
|
||||||
`Silk.NET.Vulkan`. There is no legacy fallback. Engineering cribs
|
seams, N.5 SSBO layout, translucency model, gotchas) live in
|
||||||
(WbMeshAdapter seams, N.5 SSBO layout, translucency model, gotchas)
|
`memory/reference_modern_rendering_pipeline.md`.
|
||||||
live in `memory/reference_modern_rendering_pipeline.md`.
|
|
||||||
|
|
||||||
Before re-implementing any AC-specific rendering or dat-handling
|
Before re-implementing any AC-specific rendering or dat-handling
|
||||||
algorithm, **read `docs/architecture/worldbuilder-inventory.md` FIRST**.
|
algorithm, **read `docs/architecture/worldbuilder-inventory.md` FIRST**.
|
||||||
|
|
@ -79,20 +78,17 @@ and `~/.claude/projects/.../memory/` (the latter is browsable in
|
||||||
Obsidian via the `claude-memory/` junction in the repo root; see
|
Obsidian via the `claude-memory/` junction in the repo root; see
|
||||||
`memory/reference_obsidian_vault.md`).
|
`memory/reference_obsidian_vault.md`).
|
||||||
|
|
||||||
**UI strategy:** one presentation stack — the retained retail
|
**UI strategy:** two coexisting presentation stacks over shared state,
|
||||||
`UiHost`/`UiRoot` tree in `AcDream.App/UI`, imported from LayoutDesc/DAT
|
ViewModels, events, and commands. ImGui.NET +
|
||||||
assets and bound by focused controllers over shared state, ViewModels,
|
`Silk.NET.OpenGL.Extensions.ImGui` is the permanent
|
||||||
events, and commands (the ViewModels/commands seam from the earlier
|
`ACDREAM_DEVTOOLS=1` developer stack using `IPanel`/`IPanelRenderer`.
|
||||||
two-stack era remains the stable boundary between state and
|
Retail gameplay UI is the independent retained `UiHost`/`UiRoot` tree in
|
||||||
presentation). The ImGui.NET developer-tools frontend
|
`AcDream.App/UI`, imported from LayoutDesc/DAT assets and bound by focused
|
||||||
(`AcDream.UI.ImGui`) and the OpenGL backend it required were deleted at
|
controllers. The stable cross-stack seam is ViewModels/commands, not a backend
|
||||||
Campaign V slice V11 (`docs/plans/2026-07-27-vulkan-campaign.md`);
|
swap. `TextRenderer` + `BitmapFont` also serve D.6 world-space HUD elements
|
||||||
`ACDREAM_DEVTOOLS=1` now only selects the optional Vulkan
|
where ImGui cannot reach the 3D scene. Plugin gameplay UI uses the BCL-only
|
||||||
validation/debug-utils extensions (see the flag's log line in
|
|
||||||
`Program.cs`). `TextRenderer` + `BitmapFont` serve D.6 world-space HUD
|
|
||||||
elements in the 3D scene. Plugin gameplay UI uses the BCL-only
|
|
||||||
`AcDream.Plugin.Abstractions.IUiRegistry.AddMarkupPanel` contract; plugins
|
`AcDream.Plugin.Abstractions.IUiRegistry.AddMarkupPanel` contract; plugins
|
||||||
never import App namespaces. Full design:
|
never import App or ImGui namespaces. Full design:
|
||||||
[`docs/plans/2026-04-24-ui-framework.md`](docs/plans/2026-04-24-ui-framework.md).
|
[`docs/plans/2026-04-24-ui-framework.md`](docs/plans/2026-04-24-ui-framework.md).
|
||||||
Memory cribs: `claude-memory/project_chat_pipeline.md` (chat pipeline as of
|
Memory cribs: `claude-memory/project_chat_pipeline.md` (chat pipeline as of
|
||||||
Phase I), `claude-memory/project_input_pipeline.md` (input pipeline as of
|
Phase I), `claude-memory/project_input_pipeline.md` (input pipeline as of
|
||||||
|
|
|
||||||
25
AcDream.slnx
|
|
@ -7,30 +7,13 @@
|
||||||
<Project Path="src/AcDream.Core/AcDream.Core.csproj" />
|
<Project Path="src/AcDream.Core/AcDream.Core.csproj" />
|
||||||
<Project Path="src/AcDream.Core.Net/AcDream.Core.Net.csproj" />
|
<Project Path="src/AcDream.Core.Net/AcDream.Core.Net.csproj" />
|
||||||
<Project Path="src/AcDream.Headless/AcDream.Headless.csproj" />
|
<Project Path="src/AcDream.Headless/AcDream.Headless.csproj" />
|
||||||
<Project Path="src/AcDream.Launcher/AcDream.Launcher.csproj" />
|
|
||||||
<Project Path="src/AcDream.Launcher.Core/AcDream.Launcher.Core.csproj" />
|
|
||||||
<Project Path="src/AcDream.Platform/AcDream.Platform.csproj" />
|
|
||||||
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
|
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
|
||||||
<Project Path="src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj" />
|
|
||||||
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
|
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
|
||||||
<Project Path="src/AcDream.Runtime/AcDream.Runtime.csproj" />
|
<Project Path="src/AcDream.Runtime/AcDream.Runtime.csproj" />
|
||||||
<Project Path="src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj" />
|
<Project Path="src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tools/">
|
<Folder Name="/tools/">
|
||||||
<Project Path="tools/A8CellAudit/A8CellAudit.csproj" />
|
|
||||||
<Project Path="tools/dump-keymap/dump-keymap.csproj" />
|
|
||||||
<Project Path="tools/MosswartArt/MosswartArt.csproj" />
|
|
||||||
<Project Path="tools/PesChainAudit/PesChainAudit.csproj" />
|
|
||||||
<Project Path="tools/ProjectileVfxAudit/ProjectileVfxAudit.csproj" />
|
|
||||||
<Project Path="tools/RainMeshProbe/RainMeshProbe.csproj" />
|
|
||||||
<Project Path="tools/RetailTimeProbe/RetailTimeProbe.csproj" />
|
<Project Path="tools/RetailTimeProbe/RetailTimeProbe.csproj" />
|
||||||
<Project Path="tools/SetupInspect/SetupInspect.csproj" />
|
|
||||||
<Project Path="tools/ShaderCompiler/ShaderCompiler.csproj" />
|
|
||||||
<Project Path="tools/SkyObjectInspect/SkyObjectInspect.csproj" />
|
|
||||||
<Project Path="tools/StarsProbe/StarsProbe.csproj" />
|
|
||||||
<Project Path="tools/TextureDump/TextureDump.csproj" />
|
|
||||||
<Project Path="tools/WeatherEnumerator/WeatherEnumerator.csproj" />
|
|
||||||
<Project Path="tools/WeatherSetupProbe/WeatherSetupProbe.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/AcDream.App.Tests/AcDream.App.Tests.csproj" />
|
<Project Path="tests/AcDream.App.Tests/AcDream.App.Tests.csproj" />
|
||||||
|
|
@ -41,14 +24,6 @@
|
||||||
<Project Path="tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj" />
|
<Project Path="tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj" />
|
||||||
<Project Path="tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj" />
|
<Project Path="tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj" />
|
||||||
<Project Path="tests/AcDream.Headless.Tests/AcDream.Headless.Tests.csproj" />
|
<Project Path="tests/AcDream.Headless.Tests/AcDream.Headless.Tests.csproj" />
|
||||||
<Project Path="tests/AcDream.Launcher.Core.Tests.Fixtures.InstallLeaseHolder/AcDream.Launcher.Core.Tests.Fixtures.InstallLeaseHolder.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Launcher.Core.Tests.Fixtures.ConsoleSignalChild/AcDream.Launcher.Core.Tests.Fixtures.ConsoleSignalChild.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Launcher.Core.Tests.Fixtures.ConsolelessSupervisorParent/AcDream.Launcher.Core.Tests.Fixtures.ConsolelessSupervisorParent.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Launcher.Core.Tests/AcDream.Launcher.Core.Tests.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Platform.Tests/AcDream.Platform.Tests.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Plugins.MossTank.Tests/AcDream.Plugins.MossTank.Tests.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Plugin.Tests.Fixtures.HostPlugin/AcDream.Plugin.Tests.Fixtures.HostPlugin.csproj" />
|
|
||||||
<Project Path="tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj" />
|
<Project Path="tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj" />
|
||||||
<Project Path="tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj" />
|
<Project Path="tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
|
|
||||||
281
CLAUDE.md
|
|
@ -43,14 +43,13 @@ in `src/AcDream.*` references it as a project dependency.
|
||||||
`TextureCache`, `GlobalMeshBuffer`, shader infrastructure, and the
|
`TextureCache`, `GlobalMeshBuffer`, shader infrastructure, and the
|
||||||
EnvCell/portal/scenery/terrain-blending pipeline classes.
|
EnvCell/portal/scenery/terrain-blending pipeline classes.
|
||||||
|
|
||||||
**Modern rendering path is MANDATORY.** The N.5 ship amendment deleted
|
**Modern rendering path is MANDATORY** as of the N.5 ship amendment.
|
||||||
`WbFoundationFlag`, `InstancedMeshRenderer`, and `StaticMeshRenderer`;
|
`WbFoundationFlag`, `InstancedMeshRenderer`, and `StaticMeshRenderer`
|
||||||
Campaign V (`docs/plans/2026-07-27-vulkan-campaign.md`, closed
|
are deleted. Missing `GL_ARB_bindless_texture` or
|
||||||
2026-07-29) then ported the renderer to Vulkan behind the RHI contract
|
`GL_ARB_shader_draw_parameters` throws `NotSupportedException` at
|
||||||
and deleted the OpenGL backend outright — `AcDream.App` references only
|
startup. There is no legacy fallback. Engineering cribs (WbMeshAdapter
|
||||||
`Silk.NET.Vulkan`. There is no legacy fallback. Engineering cribs
|
seams, N.5 SSBO layout, translucency model, gotchas) live in
|
||||||
(WbMeshAdapter seams, N.5 SSBO layout, translucency model, gotchas)
|
`memory/reference_modern_rendering_pipeline.md`.
|
||||||
live in `memory/reference_modern_rendering_pipeline.md`.
|
|
||||||
|
|
||||||
Before re-implementing any AC-specific rendering or dat-handling
|
Before re-implementing any AC-specific rendering or dat-handling
|
||||||
algorithm, **read `docs/architecture/worldbuilder-inventory.md` FIRST**.
|
algorithm, **read `docs/architecture/worldbuilder-inventory.md` FIRST**.
|
||||||
|
|
@ -77,20 +76,17 @@ and `~/.claude/projects/.../memory/` (the latter is browsable in
|
||||||
Obsidian via the `claude-memory/` junction in the repo root; see
|
Obsidian via the `claude-memory/` junction in the repo root; see
|
||||||
`memory/reference_obsidian_vault.md`).
|
`memory/reference_obsidian_vault.md`).
|
||||||
|
|
||||||
**UI strategy:** one presentation stack — the retained retail
|
**UI strategy:** two coexisting presentation stacks over shared state,
|
||||||
`UiHost`/`UiRoot` tree in `AcDream.App/UI`, imported from LayoutDesc/DAT
|
ViewModels, events, and commands. ImGui.NET +
|
||||||
assets and bound by focused controllers over shared state, ViewModels,
|
`Silk.NET.OpenGL.Extensions.ImGui` is the permanent
|
||||||
events, and commands (the ViewModels/commands seam from the earlier
|
`ACDREAM_DEVTOOLS=1` developer stack using `IPanel`/`IPanelRenderer`.
|
||||||
two-stack era remains the stable boundary between state and
|
Retail gameplay UI is the independent retained `UiHost`/`UiRoot` tree in
|
||||||
presentation). The ImGui.NET developer-tools frontend
|
`AcDream.App/UI`, imported from LayoutDesc/DAT assets and bound by focused
|
||||||
(`AcDream.UI.ImGui`) and the OpenGL backend it required were deleted at
|
controllers. The stable cross-stack seam is ViewModels/commands, not a backend
|
||||||
Campaign V slice V11 (`docs/plans/2026-07-27-vulkan-campaign.md`);
|
swap. `TextRenderer` + `BitmapFont` also serve D.6 world-space HUD elements
|
||||||
`ACDREAM_DEVTOOLS=1` now only selects the optional Vulkan
|
where ImGui cannot reach the 3D scene. Plugin gameplay UI uses the BCL-only
|
||||||
validation/debug-utils extensions (see the flag's log line in
|
|
||||||
`Program.cs`). `TextRenderer` + `BitmapFont` serve D.6 world-space HUD
|
|
||||||
elements in the 3D scene. Plugin gameplay UI uses the BCL-only
|
|
||||||
`AcDream.Plugin.Abstractions.IUiRegistry.AddMarkupPanel` contract; plugins
|
`AcDream.Plugin.Abstractions.IUiRegistry.AddMarkupPanel` contract; plugins
|
||||||
never import App namespaces. Full design:
|
never import App or ImGui namespaces. Full design:
|
||||||
[`docs/plans/2026-04-24-ui-framework.md`](docs/plans/2026-04-24-ui-framework.md).
|
[`docs/plans/2026-04-24-ui-framework.md`](docs/plans/2026-04-24-ui-framework.md).
|
||||||
Memory cribs: `claude-memory/project_chat_pipeline.md` (chat pipeline as of
|
Memory cribs: `claude-memory/project_chat_pipeline.md` (chat pipeline as of
|
||||||
Phase I), `claude-memory/project_input_pipeline.md` (input pipeline as of
|
Phase I), `claude-memory/project_input_pipeline.md` (input pipeline as of
|
||||||
|
|
@ -130,194 +126,8 @@ user-accepted, including exact response flags, independent examination
|
||||||
window, inscription transaction, complete creature/item/spell reports,
|
window, inscription transaction, complete creature/item/spell reports,
|
||||||
favorite-spell press/right-click behavior, modern scarab/prismatic formula,
|
favorite-spell press/right-click behavior, modern scarab/prismatic formula,
|
||||||
DAT component icons, foreground stacking, and authored 310 x 400 extent.
|
DAT component icons, foreground stacking, and authored 310 x 400 extent.
|
||||||
Slice 4 equipped-child world picking passed its two-client Coldeve gate and
|
Resume at Slice 4 equipped-child world picking, then vendor browse and
|
||||||
was user-accepted 2026-07-29. **Slices 5 and 6 (the complete vendor
|
authoritative transactions.
|
||||||
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<slots ghost gate) →
|
|
||||||
`gmCharGenMainUI`'s six-page flow (Heritage / Profession / Skills /
|
|
||||||
Appearance with live 3D preview / Town / Summary with its own zoomed-out
|
|
||||||
viewport) → byte-exact 0xF656 with the 55-slot invariant → complete
|
|
||||||
0xF643 handling (roster append + retail log-straight-in; every rejection
|
|
||||||
dialog, incl. the corrected ground truth that retail shows NameDBDown
|
|
||||||
for Pending/Undef — the plan's original "retail swallows it" was
|
|
||||||
DISPROVEN at CC5's review) → the §LA1 `characterCreated`/`creationFailed`
|
|
||||||
launcher status cycle. `RandomizeCharacter` + sub-primitives are ported
|
|
||||||
(retail's ctor-time open-roll incl. the gender-flip quirk; humans-only
|
|
||||||
random heritage ids 1-4 — a real retail quirk). Plan + ledger:
|
|
||||||
`docs/plans/2026-08-15-character-creation-campaign.md`; connected gate
|
|
||||||
script: `docs/research/2026-08-16-campaign-cc-test-script.md` (launch:
|
|
||||||
launcher flow, or `ACDREAM_RETAIL_UI=1` + `ACDREAM_OPEN_CHARGEN=1`);
|
|
||||||
START at `claude-memory/project_character_creation_campaign_handoff.md`.
|
|
||||||
Register churn: AP-214/AP-225/TS-82/AD-101 retired; AP-211 updated;
|
|
||||||
AP-212 narrowed; AP-215–AP-229 filed (AP-221 one-shot preview binding,
|
|
||||||
AP-222 spin-highlight no-op, AP-229 stacked-screens-vs-retail-teardown
|
|
||||||
are the ones a gate tester will meet). Known-flake set now also names
|
|
||||||
`RuntimeCollisionReportingStateTests.WarmedSteadyContactRefreshDoesNotAllocate`
|
|
||||||
(full-solution parallel load only). Suites at `2176ba76`: full solution
|
|
||||||
14,426 / 4 skips, App 5257/3, Runtime 1735/0, Launcher.Core 324/0.
|
|
||||||
|
|
||||||
**Placement cutover — C4 COMPLETE 2026-08-05, merged to main.** Every
|
|
||||||
placement route now runs through the canonical residence + continuation-
|
|
||||||
executor owner. Routes landed this session: 4b-3 remote teleport/cell-less
|
|
||||||
(`6dc7ba51`), 6 drops (`1b484937`, zero production lines), 5 projectile
|
|
||||||
(`36255af0`), 7 child-cell propagation (`cd3129e9`), 3 portal
|
|
||||||
(`e0f96a55`), plus the `OnPosition` dual-tail collapse (`edc911b0`) that
|
|
||||||
retired the duplication behind three separate defects. Suite 11,027 →
|
|
||||||
**11,090 passed / 4 skipped / 0 failed**. Connected gates: routes 3, 6 and 7
|
|
||||||
user-passed 2026-08-05 with probe evidence; route 7's is THIN (one
|
|
||||||
`cause=propagate`) and 4b-3's `cause=cellless` case remains unrun with an
|
|
||||||
UNESTABLISHED trigger — route 7 invalidated its recorded recipe.
|
|
||||||
**C5 COMPLETE — the placement campaign is FULLY CLOSED (`addb5657`,
|
|
||||||
2026-08-07).** C5a deleted the legacy resolver outright and retired
|
|
||||||
AP-1/AP-145 (closing #318); C5b closed #275 and filed AP-147/AP-148; C5c's
|
|
||||||
closeout passed its 11,196-test automated gate and the owed connected-gate
|
|
||||||
batch USER-PASSED 2026-08-07. #280's portal-prefetch fix and its dual
|
|
||||||
review also landed (AP-149/150/151), and AP-22 retired 2026-08-06. Start
|
|
||||||
any new placement work at `claude-memory/project_placement_cutover_closed.md`
|
|
||||||
(probes deliberately NOT stripped; start at #331).
|
|
||||||
|
|
||||||
**Read `docs/research/2026-08-05-c4-closeout-handoff.md` before any
|
|
||||||
placement work.** Its seven process findings remain binding. The two that
|
|
||||||
cost the most that campaign: a contract asserting a mechanism that does not
|
|
||||||
exist caused three separate defects, and inferring a fact you can observe
|
|
||||||
made one fix strictly worse than the bug it replaced — it removed the
|
|
||||||
invariant failure while leaving the bug.
|
|
||||||
|
|
||||||
**Modern Runtime/performance status:** Slices A–K of
|
**Modern Runtime/performance status:** Slices A–K of
|
||||||
`docs/plans/2026-07-24-modern-runtime-architecture.md` are complete. Slice L is
|
`docs/plans/2026-07-24-modern-runtime-architecture.md` are complete. Slice L is
|
||||||
|
|
@ -750,10 +560,9 @@ The capped/RDP jump-presentation cadence alias is deferred as issue #235:
|
||||||
uncapped Release presentation is smooth, while physics, collision, and wire
|
uncapped Release presentation is smooth, while physics, collision, and wire
|
||||||
truth remain correct.
|
truth remain correct.
|
||||||
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
|
||||||
`docs/architecture/code-structure.md`. **Carried:** #116 (Campaign P P2),
|
`docs/architecture/code-structure.md`. **Carried:** #153, #116, remaining
|
||||||
remaining R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice L,
|
R6 ownership cleanup, TS-50/TS-51/TS-53, Modern Runtime Slice L, and #225's
|
||||||
and #225's lifestone/particle alpha visual gate. #153 closed 2026-07-30
|
lifestone/particle alpha visual gate.
|
||||||
(Campaign P P5 ledger evidence chain).
|
|
||||||
|
|
||||||
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
Start structural work at `memory/project_gamewindow_decomposition.md` and
|
||||||
`docs/architecture/code-structure.md`; start
|
`docs/architecture/code-structure.md`; start
|
||||||
|
|
@ -765,7 +574,6 @@ Documentation entry point: [`docs/README.md`](docs/README.md).
|
||||||
For canonical state, read in this order:
|
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-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/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-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/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)
|
- [`docs/ISSUES.md`](docs/ISSUES.md) — open + recently closed bugs (tactical)
|
||||||
|
|
@ -1569,26 +1377,8 @@ via `PlayerMovementController.ApplyServerRunRate`) or from
|
||||||
- `ACDREAM_DUMP_MOTION=1` — dump every inbound `UpdateMotion` (guid,
|
- `ACDREAM_DUMP_MOTION=1` — dump every inbound `UpdateMotion` (guid,
|
||||||
stance, cmd, speed) + resulting `SetCycle` call. Massive for remote-
|
stance, cmd, speed) + resulting `SetCycle` call. Massive for remote-
|
||||||
animation debugging.
|
animation debugging.
|
||||||
- `ACDREAM_STREAM_RADIUS=N` — **legacy** streaming-radius override
|
- `ACDREAM_STREAM_RADIUS=N` — tune landblock visible-window radius
|
||||||
(`RuntimeOptions.LegacyStreamRadius`). **Default is UNSET**, not 2: the
|
(default 2 = 5×5).
|
||||||
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-
|
- `ACDREAM_NO_AUDIO=1` — suppress OpenAL init for headless / driver-
|
||||||
broken setups.
|
broken setups.
|
||||||
- `ACDREAM_REMOTE_VEL_DIAG=1` — dump per-tick / per-UM remote motion
|
- `ACDREAM_REMOTE_VEL_DIAG=1` — dump per-tick / per-UM remote motion
|
||||||
|
|
@ -1622,29 +1412,6 @@ via `PlayerMovementController.ApplyServerRunRate`) or from
|
||||||
delta), `[sticky-snap-skip]` at the suppressed NPC UP-snap site.
|
delta), `[sticky-snap-skip]` at the suppressed NPC UP-snap site.
|
||||||
Heavy while a pack is stuck (~60 Hz × stuck count). Converged the
|
Heavy while a pack is stuck (~60 Hz × stuck count). Converged the
|
||||||
#171 residuals (the deep-overlap sign pin AP-82).
|
#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=<metres>` sets the window (default 30).
|
|
||||||
TEMPORARY, with the #337 probe family.
|
|
||||||
- `ACDREAM_CAPTURE_RESOLVE=<path>` — live capture of every player-side
|
- `ACDREAM_CAPTURE_RESOLVE=<path>` — live capture of every player-side
|
||||||
`PhysicsEngine.ResolveWithTransition` call. Each call appends one
|
`PhysicsEngine.ResolveWithTransition` call. Each call appends one
|
||||||
JSON Lines record with full inputs, PhysicsBody snapshot before AND
|
JSON Lines record with full inputs, PhysicsBody snapshot before AND
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<!-- Repository-wide language and warning policy. Project files only override
|
|
||||||
these values when a target has a documented, target-specific need. -->
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<AnalysisLevel>latest</AnalysisLevel>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Deterministic>true</Deterministic>
|
|
||||||
|
|
||||||
<!-- Use custom names for every graph. A conventional packages.lock.json
|
|
||||||
always overrides NuGetLockFilePath, which makes neutral and RID locks
|
|
||||||
impossible to keep side by side. -->
|
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
|
||||||
<NuGetLockFilePath Condition="'$(RuntimeIdentifier)' == ''">$(MSBuildProjectDirectory)/packages.neutral.lock.json</NuGetLockFilePath>
|
|
||||||
<NuGetLockFilePath Condition="'$(RuntimeIdentifier)' != ''">$(MSBuildProjectDirectory)/packages.$(RuntimeIdentifier).lock.json</NuGetLockFilePath>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageVersion Include="Arch" Version="2.1.0" />
|
|
||||||
<PackageVersion Include="Avalonia" Version="12.1.1" />
|
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
|
|
||||||
<PackageVersion Include="Avalonia.Headless.XUnit" Version="12.1.1" />
|
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.1" />
|
|
||||||
<PackageVersion Include="BCnEncoder.Net" Version="2.2.1" />
|
|
||||||
<PackageVersion Include="BCnEncoder.Net.ImageSharp" Version="1.1.2" />
|
|
||||||
<PackageVersion Include="Chorizite.Core" Version="0.0.18" />
|
|
||||||
<PackageVersion Include="Chorizite.DatReaderWriter" Version="2.1.7" />
|
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
|
||||||
<PackageVersion Include="Serilog" Version="4.0.2" />
|
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.Input" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.OpenAL" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.OpenAL.Extensions.Creative" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.OpenAL.Extensions.EXT" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.OpenAL.Soft.Native" Version="1.23.1" />
|
|
||||||
<PackageVersion Include="Silk.NET.Shaderc" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
<PackageVersion Include="StbImageSharp" Version="2.30.16" />
|
|
||||||
<PackageVersion Include="StbTrueTypeSharp" Version="1.26.12" />
|
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
|
||||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
10
NuGet.Config
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<packageSources>
|
|
||||||
<clear />
|
|
||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
|
||||||
</packageSources>
|
|
||||||
<fallbackPackageFolders>
|
|
||||||
<clear />
|
|
||||||
</fallbackPackageFolders>
|
|
||||||
</configuration>
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
# 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** — `<ApplicationIcon>` 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`.
|
|
||||||
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 699 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 397 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 819 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 86 KiB |
8533
docs/ISSUES.md
|
|
@ -82,10 +82,6 @@ document in the same change; do not leave both claims standing.
|
||||||
- [`superpowers/specs/`](superpowers/specs/) and
|
- [`superpowers/specs/`](superpowers/specs/) and
|
||||||
[`superpowers/plans/`](superpowers/plans/) are per-slice design and execution
|
[`superpowers/plans/`](superpowers/plans/) are per-slice design and execution
|
||||||
records. Completed plans remain historical.
|
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.
|
|
||||||
- [`audit/`](audit/) contains completion and conformance audits.
|
- [`audit/`](audit/) contains completion and conformance audits.
|
||||||
- [`reference/ace-commands.md`](reference/ace-commands.md) preserves the local
|
- [`reference/ace-commands.md`](reference/ace-commands.md) preserves the local
|
||||||
ACE server's complete in-game command catalog and points to the authoritative
|
ACE server's complete in-game command catalog and points to the authoritative
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ stack. Full history and the corrected contract live in
|
||||||
│ LayoutDesc/DAT → UiRoot retained widgets + controllers │
|
│ LayoutDesc/DAT → UiRoot retained widgets + controllers │
|
||||||
├─────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────┤
|
||||||
│ SHARED CONTRACTS │
|
│ SHARED CONTRACTS │
|
||||||
│ ViewModels, input actions, state/event and command seams │
|
│ ViewModels, commands, input actions, state/event services │
|
||||||
│ ► one model and mutation path, one presentation projection │
|
│ ► one model and mutation path, one presentation projection │
|
||||||
├─────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────┤
|
||||||
│ Game state + events (unchanged) │
|
│ Game state + events (unchanged) │
|
||||||
|
|
@ -100,44 +100,27 @@ stack. Full history and the corrected contract live in
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
`AcDream.UI.Abstractions` — the `IPanel`/`IPanelRenderer` contract and the
|
`AcDream.UI.Abstractions` — the `IPanel`/`IPanelRenderer` contract, the
|
||||||
ViewModels — **survives intact**. It was always
|
ViewModels and the commands — **survives intact**. It was always
|
||||||
backend-agnostic, which is exactly what Code Structure Rule 3 was written to
|
backend-agnostic, which is exactly what Code Structure Rule 3 was written to
|
||||||
protect, and it is what a future developer-panel host would bind to. Only the
|
protect, and it is what a future developer-panel host would bind to. Only the
|
||||||
ImGui *backend* was deleted. `ACDREAM_DEVTOOLS=1` still selects Vulkan's
|
ImGui *backend* was deleted. `ACDREAM_DEVTOOLS=1` still selects Vulkan's
|
||||||
debug-utils extensions and now logs that the developer UI is gone; replacing it
|
debug-utils extensions and now logs that the developer UI is gone; replacing it
|
||||||
is issue **#258**, deliberately unscheduled.
|
is issue **#258**, deliberately unscheduled.
|
||||||
|
|
||||||
`AcDream.UI.Abstractions` owns backend-neutral ViewModels, input, and the
|
`AcDream.UI.Abstractions` owns backend-neutral ViewModels, commands, input,
|
||||||
`IPanel`/`IPanelRenderer` devtools contract. `AcDream.App/UI` owns the
|
and the `IPanel`/`IPanelRenderer` devtools contract. `AcDream.App/UI` owns the
|
||||||
retained gameplay tree, LayoutDesc importer, window runtime, and panel
|
retained gameplay tree, LayoutDesc importer, window runtime, and panel
|
||||||
controllers. Neither presentation stack owns independent game-state truth.
|
controllers. Neither presentation stack owns independent game-state truth.
|
||||||
Chat submission follows the same rule: `AcDream.Runtime/Chat` owns the shared
|
Chat submission follows the same rule: both presentation stacks enter the
|
||||||
parser, retail command/channel/help catalogs, `ChatCommandRouter`, command bus,
|
shared `ChatCommandRouter`, which emits distinct backend-neutral intents for a
|
||||||
and its four backend-neutral records (`ExecuteClientCommandCmd`,
|
retail client command (`ExecuteClientCommandCmd`), an ACE-owned command
|
||||||
`SendServerCommandCmd`, `SendChatCmd`, and `SendRawChannelCmd`). Its only
|
(`SendServerCommandCmd`), or ordinary chat (`SendChatCmd`). App-layer
|
||||||
presentation callback is the four-member `IChatCommandFeedback`; retained
|
handlers and controllers translate those intents to `WorldSession`; panels
|
||||||
`ChatVM` implements that seam. Both App and Headless bind the same
|
never inspect or construct wire messages.
|
||||||
`LiveChatCommandRoute` to the active `WorldSession` send delegates and exact
|
|
||||||
`RuntimeCommunicationState`/`RuntimeCharacterState` children. Panels never
|
|
||||||
inspect or construct wire messages, and Runtime has no UI or App dependency.
|
|
||||||
Configured login commands enter that identical parser/router only after the
|
|
||||||
generation's `enteredWorld` edge, in order, once per generation. The shared
|
|
||||||
generation-aware sequence cancels on replacement, applies the configured
|
|
||||||
inter-command delay, and reports each isolated failure without aborting the
|
|
||||||
session or plugin lifetime.
|
|
||||||
Plugins register retained gameplay markup through the BCL-only
|
Plugins register retained gameplay markup through the BCL-only
|
||||||
`AcDream.Plugin.Abstractions.IUiRegistry`; they do not import App or
|
`AcDream.Plugin.Abstractions.IUiRegistry`; they do not import App or
|
||||||
presentation assemblies. `IPluginHost.HasUi` is the explicit capability edge:
|
presentation assemblies. Core `SelectionState` is the sole selected-object owner for world,
|
||||||
the graphical host supplies its retained registry, while no-window hosts
|
|
||||||
return `false` and the BCL-only `NoOpUiRegistry`, which retains no plugin
|
|
||||||
binding. Both hosts use Core's session-scoped discovery/lifetime orchestrator
|
|
||||||
and the same config allow-list semantics (absent loads all; explicit empty
|
|
||||||
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.
|
|
||||||
|
|
||||||
Core `SelectionState` is the sole selected-object owner for world,
|
|
||||||
radar, inventory, paperdoll, toolbar, use/examine consumers, and plugins;
|
radar, inventory, paperdoll, toolbar, use/examine consumers, and plugins;
|
||||||
`IPluginHost.Selection` exposes that same state and retail-style old/new callback.
|
`IPluginHost.Selection` exposes that same state and retail-style old/new callback.
|
||||||
Temporary pointer modes are separate App orchestration in `InteractionState` and
|
Temporary pointer modes are separate App orchestration in `InteractionState` and
|
||||||
|
|
@ -165,16 +148,6 @@ window registration, plugin mounts, cursor feedback, layout persistence, and the
|
||||||
retained tick/draw/restore/dispose paths. Panel-specific construction must not
|
retained tick/draw/restore/dispose paths. Panel-specific construction must not
|
||||||
move back into `GameWindow.OnLoad`.
|
move back into `GameWindow.OnLoad`.
|
||||||
|
|
||||||
The graphical no-selector launch projects Runtime's sole
|
|
||||||
`RuntimeCharacterSelectionState` through the retained character-management root
|
|
||||||
resolved from DAT enum table 5 (`0x10000005` -> `0x21000004`, selected root
|
|
||||||
`0x1000039A`). App borrows the view and routes generation-capturing typed
|
|
||||||
commands; it owns no roster, highlight, operation, error, or lifecycle mirror.
|
|
||||||
The authored screen is a flat ListBox and buttons, with the shared retail dialog
|
|
||||||
catalog for confirmation, wait, and error presentation. It contains no viewport
|
|
||||||
or character preview. Explicit-selector graphical launches and no-window hosts
|
|
||||||
do not mount this presentation.
|
|
||||||
|
|
||||||
Magic follows the same boundary. Core `Spellbook` is the one learned/favorite/
|
Magic follows the same boundary. Core `Spellbook` is the one learned/favorite/
|
||||||
desired/enchantment state projection; Core.Net owns exact manifest and live
|
desired/enchantment state projection; Core.Net owns exact manifest and live
|
||||||
message parsing; Runtime `RuntimeActionState.SpellCast` owns validated cast
|
message parsing; Runtime `RuntimeActionState.SpellCast` owns validated cast
|
||||||
|
|
@ -201,9 +174,6 @@ parallel window-lifecycle map.
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
AcDream.Core/ Layer 2-4: no Vulkan, no Silk.NET, pure logic
|
AcDream.Core/ Layer 2-4: no Vulkan, no Silk.NET, pure logic
|
||||||
Plugins/
|
|
||||||
PluginSession.cs -> shared per-host allow-list, failure isolation,
|
|
||||||
status outcome, and collectible ALC lifetime
|
|
||||||
Physics/
|
Physics/
|
||||||
PhysicsBody.cs -> body state / integration foundation (done)
|
PhysicsBody.cs -> body state / integration foundation (done)
|
||||||
CollisionPrimitives.cs -> retail primitive helpers (partial, active)
|
CollisionPrimitives.cs -> retail primitive helpers (partial, active)
|
||||||
|
|
@ -249,29 +219,14 @@ src/
|
||||||
generation + teardown
|
generation + teardown
|
||||||
Session/ -> J2 canonical session lifetime, ordered
|
Session/ -> J2 canonical session lifetime, ordered
|
||||||
inbound routing + retryable teardown
|
inbound routing + retryable teardown
|
||||||
RuntimeCharacterSelectionState.cs -> sole generation-scoped pre-world
|
|
||||||
roster/highlight/delete/restore/error owner;
|
|
||||||
borrowed view + ordered deltas + typed commands
|
|
||||||
Entities/
|
Entities/
|
||||||
RuntimeEntityDirectory.cs -> sole GUID/incarnation/local-ID authority
|
RuntimeEntityDirectory.cs -> sole GUID/incarnation/local-ID authority
|
||||||
RuntimeEntityRecord.cs -> presentation-free accepted entity state
|
RuntimeEntityRecord.cs -> presentation-free accepted entity state
|
||||||
RuntimeEntityObjectLifetime.cs -> one entity/object lifetime root
|
RuntimeEntityObjectLifetime.cs -> one entity/object lifetime root
|
||||||
RuntimeEntityObjectEventStream.cs -> canonical ordered entity/object deltas
|
RuntimeEntityObjectEventStream.cs -> canonical ordered entity/object deltas
|
||||||
RuntimeEntityObjectViews.cs -> direct allocation-free borrowed views
|
RuntimeEntityObjectViews.cs -> direct allocation-free borrowed views
|
||||||
InboundPhysicsStateController.cs -> retail timestamp/snapshot authority,
|
InboundPhysicsStateController.cs -> retail timestamp/snapshot authority
|
||||||
including gate-only dormant acceptance
|
ParentAttachmentState.cs -> generation-exact parent relations
|
||||||
ParentAttachmentState.cs -> generation-exact parent relations plus raw
|
|
||||||
missing-parent Create admission
|
|
||||||
RuntimeInitialCreateAdmissionFreezer.cs -> immutable parser-payload copy
|
|
||||||
boundary for dormant initial placement
|
|
||||||
RuntimeInitialCreateResidenceState.cs -> exact-incarnation initial
|
|
||||||
placement lease and accepted mixed-update FIFO
|
|
||||||
RuntimeInitialCreateContinuationExecutor.cs -> retry-idempotent
|
|
||||||
adoption + retail Create tail + strict-order
|
|
||||||
FIFO/replay execution over the residence
|
|
||||||
Chat/ -> LA6 parser/router/catalog and four command
|
|
||||||
intents; shared live route + generation-scoped
|
|
||||||
configured-login sequence for both hosts
|
|
||||||
Gameplay/
|
Gameplay/
|
||||||
RuntimeCommunicationState.cs -> one chat/social owner + ordered stream
|
RuntimeCommunicationState.cs -> one chat/social owner + ordered stream
|
||||||
RuntimeInventoryState.cs -> exact object-table borrower + inventory
|
RuntimeInventoryState.cs -> exact object-table borrower + inventory
|
||||||
|
|
@ -286,15 +241,6 @@ src/
|
||||||
Physics/
|
Physics/
|
||||||
RuntimePhysicsState.cs -> per-session engine/cache/scratch/shadows,
|
RuntimePhysicsState.cs -> per-session engine/cache/scratch/shadows,
|
||||||
collision receipts, bodies/hosts/worksets
|
collision receipts, bodies/hosts/worksets
|
||||||
RuntimeCollisionReportingState.cs -> exact-key retail collision table,
|
|
||||||
environment latch, ordered callbacks, and
|
|
||||||
SetPosition report-result ownership
|
|
||||||
RuntimeSetPositionState.cs -> exact placement/lost-cell operations,
|
|
||||||
authored mover retention, ordered host
|
|
||||||
receipts, and collision-generation wake
|
|
||||||
RuntimePlacementProjectionChannel.cs -> generation-gated public host
|
|
||||||
observation/retry/exact-ack seam over the
|
|
||||||
one Runtime SetPosition receipt owner
|
|
||||||
RuntimeRemotePhysicsUpdater.cs -> presentation-free remote simulation
|
RuntimeRemotePhysicsUpdater.cs -> presentation-free remote simulation
|
||||||
RuntimeOrdinaryPhysicsUpdater.cs -> presentation-free object simulation
|
RuntimeOrdinaryPhysicsUpdater.cs -> presentation-free object simulation
|
||||||
RuntimeProjectile.cs -> canonical projectile component/prediction owner
|
RuntimeProjectile.cs -> canonical projectile component/prediction owner
|
||||||
|
|
@ -302,83 +248,19 @@ src/
|
||||||
World/
|
World/
|
||||||
RuntimeWorldEnvironmentState.cs -> canonical calendar/time/weather owner
|
RuntimeWorldEnvironmentState.cs -> canonical calendar/time/weather owner
|
||||||
RuntimeWorldTransitState.cs -> canonical reveal generation/readiness owner
|
RuntimeWorldTransitState.cs -> canonical reveal generation/readiness owner
|
||||||
|
Platform/
|
||||||
|
ApplicationPathSet.cs -> shared BCL-only XDG/Windows config, data,
|
||||||
|
cache, plugin, screenshot, and diagnostic paths
|
||||||
RuntimeGenerationReset.cs -> one retryable canonical-generation reset
|
RuntimeGenerationReset.cs -> one retryable canonical-generation reset
|
||||||
-> Slice J complete; graphical and no-window hosts share one GameRuntime
|
-> Slice J complete; graphical and no-window hosts share one GameRuntime
|
||||||
-> may reference Core, Core.Net, Content, Plugin.Abstractions, and
|
-> may reference Core, Core.Net, Content, and Plugin.Abstractions only
|
||||||
Platform only
|
|
||||||
-> must never reference App, UI, Silk.NET, OpenAL, or Arch
|
-> must never reference App, UI, Silk.NET, OpenAL, or Arch
|
||||||
|
|
||||||
AcDream.Platform/ BCL-only portable path contract (Campaign LA LA0)
|
|
||||||
ApplicationPathSet.cs -> shared XDG/Windows config, data, cache,
|
|
||||||
plugin, screenshot, and diagnostic paths
|
|
||||||
BakePublicationGuardPaths.cs
|
|
||||||
-> shared launcher/Bake environment nonce and
|
|
||||||
adjacent publication lock/token naming contract
|
|
||||||
-> zero project/package references (guarded by
|
|
||||||
tests/AcDream.Platform.Tests/PlatformDependencyBoundaryTests.cs);
|
|
||||||
Runtime and App reference it directly; Headless reaches it
|
|
||||||
transitively through Runtime (K0 guard: Headless declares exactly
|
|
||||||
one project reference)
|
|
||||||
|
|
||||||
AcDream.Launcher.Core/ BCL-only launcher state/orchestration owner
|
|
||||||
Profiles/ -> sole credential/profile document + CRUD owner
|
|
||||||
Launching/ -> config composition and supervised process seams;
|
|
||||||
Windows console hosts are no-shell, redirected-
|
|
||||||
stdin process-group leaders receiving targeted
|
|
||||||
CTRL_BREAK, while Linux hosts receive SIGINT
|
|
||||||
Status/ -> incremental host-status parsing/tailing
|
|
||||||
Orchestration/ -> immutable UI snapshots, typed actions,
|
|
||||||
capability gates, and running-session lifetime
|
|
||||||
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
|
|
||||||
OS-handle lease serializes recovery/install per
|
|
||||||
DataDirectory; a second OS-held publication
|
|
||||||
lock plus durable per-transaction nonce makes
|
|
||||||
late orphan Bake children irrevocably stale
|
|
||||||
before recovery, while already-authorized
|
|
||||||
promotion completes before recovery; only exact
|
|
||||||
adjacent
|
|
||||||
`.<pak>.acdream-bake.<guid:N>.tmp` files are
|
|
||||||
transaction-owned crash residue
|
|
||||||
Updates/ -> pinned GitHub manifest + strict SemVer/RID
|
|
||||||
authority, bounded verified streaming download,
|
|
||||||
hardened ZIP extraction, immutable
|
|
||||||
`app/<version>/` installs, atomic `current.json`
|
|
||||||
activation/rollback, and durable next-start
|
|
||||||
launcher self-update journal; one OS-handle
|
|
||||||
shared-session/exclusive-update barrier spans
|
|
||||||
every launcher process
|
|
||||||
-> references Platform only; no Avalonia or game-host dependency
|
|
||||||
|
|
||||||
AcDream.Launcher/ Avalonia 12 Windows/Linux desktop shell
|
|
||||||
Startup/Program -> one immutable process-local option graph before
|
|
||||||
owner construction; config/data/cache require
|
|
||||||
three absolute normalized roots and one exact
|
|
||||||
`ApplicationPathSet` reaches profiles, installer,
|
|
||||||
versions/updater, sessions, cache, orchestration
|
|
||||||
-> manifest override reaches only update composition,
|
|
||||||
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
|
|
||||||
nonfatal startup/manual update state, actions,
|
|
||||||
progress, cancellation, rollback, and errors
|
|
||||||
-> references Launcher.Core only (Platform transitively); it never owns
|
|
||||||
a second profile, process, status, or credential state graph
|
|
||||||
-> every per-RID publish composes the separately published self-contained
|
|
||||||
`acdream-bake` executable beside the launcher without a project edge
|
|
||||||
-> Linux launcher/probe/headless flows remain portable; graphical-client
|
|
||||||
actions are explicitly disabled until Modern Runtime Slice L resumes
|
|
||||||
|
|
||||||
AcDream.Headless/ Linux/Windows no-window production host
|
AcDream.Headless/ Linux/Windows no-window production host
|
||||||
Program.cs -> CLI entry only
|
Program.cs -> CLI entry only
|
||||||
Configuration/ -> strict versioned process/session config
|
Configuration/ -> strict versioned process/session config
|
||||||
Credentials/ -> redacted env/stdin/owner-only-file providers
|
Credentials/ -> redacted env/stdin/owner-only-file providers
|
||||||
Hosting/ -> one GameRuntime/session/lease/policy lifetime
|
Hosting/ -> one GameRuntime/session/lease/policy lifetime
|
||||||
Plugins/ -> no-window IPluginHost borrowing Runtime/Core;
|
|
||||||
BCL no-op UI and per-session plugin lifetime
|
|
||||||
Policies/ -> typed Runtime-view/command consumers
|
Policies/ -> typed Runtime-view/command consumers
|
||||||
-> references Runtime only; no presentation/backend package
|
-> references Runtime only; no presentation/backend package
|
||||||
-> Slice K complete: portable single/multi-session production host,
|
-> Slice K complete: portable single/multi-session production host,
|
||||||
|
|
@ -389,7 +271,6 @@ src/
|
||||||
AcDream.Plugin.Abstractions/ Layer 5: plugin interfaces
|
AcDream.Plugin.Abstractions/ Layer 5: plugin interfaces
|
||||||
IAcDreamPlugin.cs -> done
|
IAcDreamPlugin.cs -> done
|
||||||
IPluginHost.cs -> done
|
IPluginHost.cs -> done
|
||||||
IUiRegistry.cs -> capability-aware retained/no-op UI contract
|
|
||||||
IGameState.cs -> done
|
IGameState.cs -> done
|
||||||
IEvents.cs -> done
|
IEvents.cs -> done
|
||||||
ISelectionService.cs -> done
|
ISelectionService.cs -> done
|
||||||
|
|
@ -412,7 +293,9 @@ src/
|
||||||
RemotePhysicsUpdater.cs -> ordinary/Hidden remote narrow-tick integration
|
RemotePhysicsUpdater.cs -> ordinary/Hidden remote narrow-tick integration
|
||||||
LiveEntityOrdinaryPhysicsUpdater.cs -> manager-less body Transition commits
|
LiveEntityOrdinaryPhysicsUpdater.cs -> manager-less body Transition commits
|
||||||
RemoteInboundMotionDispatcher.cs -> animation-optional retail UM funnel
|
RemoteInboundMotionDispatcher.cs -> animation-optional retail UM funnel
|
||||||
RemoteTeleportHook.cs -> ordered retail teleport teardown seam (teleport_hook port; C4 route 4b-3 runs it from LiveEntityNetworkUpdateController's teleport arm dispatch, through RuntimeRemotePlacementDriveController)
|
RemoteTeleportController.cs -> incarnation-scoped loaded/pending placement owner
|
||||||
|
RemoteTeleportHook.cs -> ordered retail teleport teardown seam
|
||||||
|
RemoteTeleportPlacement.cs -> collision-seated SetPosition transition commit
|
||||||
World/
|
World/
|
||||||
LiveEntityRuntime.cs -> exact-key App projection/lifecycle host
|
LiveEntityRuntime.cs -> exact-key App projection/lifecycle host
|
||||||
LiveEntityProjectionStore.cs -> materialized sidecars by RuntimeEntityKey
|
LiveEntityProjectionStore.cs -> materialized sidecars by RuntimeEntityKey
|
||||||
|
|
@ -444,16 +327,8 @@ src/
|
||||||
PlayerMovementController.cs -> active movement driver
|
PlayerMovementController.cs -> active movement driver
|
||||||
Plugins/
|
Plugins/
|
||||||
AppPluginHost.cs -> done
|
AppPluginHost.cs -> done
|
||||||
GraphicalPluginSession.cs -> thin shared-session/root/status adapter
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The 4B2 production SetPosition routes and shared local-controller body remain
|
|
||||||
dormant. Runtime now owns the exact collision table, environment latch, and
|
|
||||||
report-result semantics needed by that cutover. Activation still waits for
|
|
||||||
exact authored mover preparation, presentation-only rebucketing,
|
|
||||||
placement-prefix quiescence, and an atomic Runtime body/controller publication
|
|
||||||
transaction to land as one reviewed cutover.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Movement And Collision Architecture
|
## Movement And Collision Architecture
|
||||||
|
|
@ -566,26 +441,30 @@ What exists and is active:
|
||||||
and every routed hook. A delete/local-ID reuse during capture or during an
|
and every routed hook. A delete/local-ID reuse during capture or during an
|
||||||
earlier hook can never advance the displaced sequencer or send the old
|
earlier hook can never advance the displaced sequencer or send the old
|
||||||
owner's remaining sound, particle, or light hooks to its replacement.
|
owner's remaining sound, particle, or light hooks to its replacement.
|
||||||
- **C4 route 4b-3 (2026-08-04) deleted `RemoteTeleportController` /
|
- `RemoteTeleportController` owns the placement half of a fresh remote
|
||||||
`RemoteTeleportPlacement` / `RemoteShadowPlacementSynchronizer` outright** —
|
teleport after `RemoteTeleportHook` has torn down movement/target state. It
|
||||||
605 + 85 + 49 lines of App-layer incarnation-scoped placement machinery,
|
collision-seats loaded destinations through `RemoteTeleportPlacement`; an
|
||||||
replaced by routing the remote teleport/cell-less classification through
|
unloaded destination retains one generation- and PositionSequence-scoped
|
||||||
the SAME canonical `RuntimeRemotePlacementDriveController` the far snap
|
pending placement and resolves the latest accepted frame when that same
|
||||||
(C4 route 4b-2) already uses (`ApplyAcceptedRemoteTeleport`, sharing
|
projection becomes visible. It neither owns GUID identity nor reconstructs
|
||||||
`StoresAcceptedDestination`/`StoreAcceptedDestinationPose`). `teleport_hook`
|
an entity. A placement failure after hydration restores the captured source
|
||||||
(@0x00514ED0) still runs first, via `RemoteTeleportHook` invoked from
|
frame/cell/contact rather than leaving a visible collisionless projection;
|
||||||
`LiveEntityNetworkUpdateController`'s teleport-arm dispatch (both the
|
source-resident shadows restore immediately, while an unloaded source
|
||||||
player-guid and NPC-guid branches share one `RunRemoteArmTail` helper for
|
delegates one incarnation-scoped restore to
|
||||||
the routing-decision/currency/constraint-arm sequence — see the C4 4b-3
|
`LiveEntityPresentationController`, shared with Hidden/UnHide. A newer
|
||||||
fix round, 2026-08-04, for why the two branches were unified there after
|
placement transfers that marker into an explicit active-placement generation
|
||||||
independently drifting). There is no separate "loaded vs pending
|
before rebucketing even while Hidden. That generation suppresses every
|
||||||
destination" placement path anymore: an unresolved destination collision
|
intervening Hidden/UnHide and projection restore until the controller reaches
|
||||||
generation retains a preparation-stage retry inside the SAME drive
|
a stable result, then it either restores after collision seating, re-defers
|
||||||
controller that far snap already retries through, not a second incarnation-
|
its rollback source, or hands a Hidden result back for UnHide. The typed
|
||||||
scoped machine, and `_activePlacementOwners`'s Hidden/UnHide visibility-edge
|
`ILiveEntityRemotePlacementRuntime` seam permits a same-incarnation wrapper
|
||||||
protection is gone with its only writer chain — the synchronous, single-
|
rebind only around the canonical body; pending placement adopts that wrapper
|
||||||
frame teleport commit removes the multi-frame window that protection
|
before hydration and cannot silently lose ownership. Clearing a motion or
|
||||||
existed for.
|
projectile component retains the incarnation's body/contract identity until
|
||||||
|
logical teardown. The production wrapper exposes an immutable body, while
|
||||||
|
hydration defensively validates arbitrary implementations against the record
|
||||||
|
and rolls the retained body back on mismatch. Runtime binding snapshots the
|
||||||
|
interface Body getter once for validation, assignment, and state mutation.
|
||||||
`GpuWorldState`
|
`GpuWorldState`
|
||||||
rebuckets atomically and commits spatial visibility before draining its
|
rebuckets atomically and commits spatial visibility before draining its
|
||||||
transition FIFO; `LiveEntityRuntime` rejects delayed duplicate edges. A
|
transition FIFO; `LiveEntityRuntime` rejects delayed duplicate edges. A
|
||||||
|
|
@ -606,60 +485,8 @@ What exists and is active:
|
||||||
collision assets loaded from the validated prepared package. Production
|
collision assets loaded from the validated prepared package. Production
|
||||||
retains no parsed DAT collision graph; graph construction is restricted to
|
retains no parsed DAT collision graph; graph construction is restricted to
|
||||||
bake/equivalence tools and explicit test oracles.
|
bake/equivalence tools and explicit test oracles.
|
||||||
- Landblock collision activation is generation-owned by
|
- `ShadowObjectRegistry` gives movement a broadphase over nearby objects and
|
||||||
`RuntimePhysicsState`. Graphical and no-window hosts populate a private
|
buildings.
|
||||||
`PreparedLandblockCollisionGeneration` over bounded cursors; its cache,
|
|
||||||
`CellGraph`, engine landblock, buildings, static shadows, and retained-owner
|
|
||||||
refloods are never visible through the borrowed live engine. Retained owners
|
|
||||||
comprise every non-suspended dynamic or adjacent-root static touching the
|
|
||||||
target prefix (including a withdrawn repair marker); target-root statics come
|
|
||||||
from the authored replacement. Runtime mutation-gates their exact capture,
|
|
||||||
refreshes each through the host work meter, and builds every cache/graph/
|
|
||||||
shadow replacement through one-work-unit seal cursors. Stable per-prefix
|
|
||||||
owner slots replace the former registry-global mutation gate. One Runtime-
|
|
||||||
scoped versioned journal coalesces repeated live mutations by owner instead
|
|
||||||
of copying the owner into every draft on every event. Each draft reconciles
|
|
||||||
only the latest exact state for owners changed during its lifetime, one owner
|
|
||||||
per seal step, so unrelated or continuously moving owners cannot restart the
|
|
||||||
target cursors. Once discovered, a relevant owner receives exact subscribed
|
|
||||||
updates without restoring global fanout. First entry to or departure from a
|
|
||||||
target after the global slot cursor has passed is routed through the owner's
|
|
||||||
changed prefix to that one matching draft. During topology construction a
|
|
||||||
visited unrelated owner receives only a cheap coalesced dirty notification;
|
|
||||||
its exact mirror is deferred to one metered seal unit. Once the topology seal
|
|
||||||
exists, observed owners temporarily write through exactly until activation,
|
|
||||||
so the finite pre-seal queue drains even when several unrelated owners move
|
|
||||||
continuously. Production activates in that same update-thread call. Slots older than a
|
|
||||||
newer draft's captured root are superseded at the tail rather than reused
|
|
||||||
behind live cursors; new drafts start at their captured suffix, obsolete
|
|
||||||
slots compact incrementally, and the journal clears with the last draft.
|
|
||||||
Empty prefix containers are reclaimed under GUID churn; seal cursors retain
|
|
||||||
their captured slot lists.
|
|
||||||
Cache, CellGraph, engine, and shadow topology share one complete off-side
|
|
||||||
`CollisionWorldState`. Admission captures the current root reference in O(1)
|
|
||||||
and materializes the non-target leaves through the same one-work-unit frame
|
|
||||||
meter; a dense resident world is never cloned synchronously. After an older
|
|
||||||
preparation commits, its exact landblock delta queues into every later draft
|
|
||||||
and drains one cache, graph, landblock, or owner leaf per seal step. A later
|
|
||||||
demotion or withdrawal cancels matching queued/active rebases and tombstones
|
|
||||||
that prefix in unfinished source scans, then retires one owner/cache/graph/
|
|
||||||
outdoor leaf per seal step from growable retirement storage. Commit rechecks
|
|
||||||
both retirement and rebase state after sealing, so retired topology cannot
|
|
||||||
return or cause a drafts-times-world-size update spike.
|
|
||||||
The host immediately performs the zero-work root transfer in the same update-
|
|
||||||
thread call that completes final reconciliation, so continuous unrelated
|
|
||||||
movement cannot manufacture a required quiet frame between seal and commit.
|
|
||||||
Deterministic preparation order prevents a later draft from exposing early,
|
|
||||||
inheriting cancelled topology, or overwriting a committed prefix. Final activation is one
|
|
||||||
zero-allocation volatile root transfer that preserves public facade identity,
|
|
||||||
revokes staging, and then emits `CollisionGenerationCommitted`. Cancellation disposes only the named
|
|
||||||
staging generation and never withdraws the previous active world. Thus
|
|
||||||
readers see the complete old generation or complete new generation, never a
|
|
||||||
mixed cell/cache/shadow world.
|
|
||||||
- `ShadowObjectRegistry` gives movement a per-cell broadphase over nearby
|
|
||||||
objects and buildings. Streaming reflood is structurally part of the Runtime
|
|
||||||
collision-generation commit; there is no independent post-publication
|
|
||||||
reflood suffix.
|
|
||||||
- `TerrainSurface` uses triangle-aware terrain contact; older "bilinear terrain
|
- `TerrainSurface` uses triangle-aware terrain contact; older "bilinear terrain
|
||||||
Z" descriptions are historical B.3 language, not current architecture.
|
Z" descriptions are historical B.3 language, not current architecture.
|
||||||
|
|
||||||
|
|
@ -696,37 +523,6 @@ server-GUID/incarnation map, Runtime local-ID allocation/reverse lookup,
|
||||||
accepted snapshots and timestamp gates, parent state, session/operation
|
accepted snapshots and timestamp gates, parent state, session/operation
|
||||||
versions, and exact tombstones. `RuntimeEntityRecord` is presentation-free.
|
versions, and exact tombstones. `RuntimeEntityRecord` is presentation-free.
|
||||||
|
|
||||||
Initial world placement has one deliberate dormant exception to ordinary
|
|
||||||
snapshot publication. While an exact `RuntimeInitialCreateResidenceState`
|
|
||||||
lease is waiting for its first canonical placement, retail timestamp gates may
|
|
||||||
accept later same-incarnation Create, ObjDesc, Parent, Pickup, Position,
|
|
||||||
Movement, State, and Vector packets, but neither the canonical record, public
|
|
||||||
accepted snapshot, event stream, nor presentation changes. Runtime retains
|
|
||||||
deep-frozen typed actions in one monotonic arrival-ordered FIFO under the exact
|
|
||||||
entity key. A Create whose parent is not yet addressable is retained even
|
|
||||||
earlier as a complete raw packet, before child timestamp admission, and is
|
|
||||||
guarded by a non-reused admission token. Delete, generation replacement,
|
|
||||||
reset, GUID reuse, and reentrant teardown discard only the matching ownership.
|
|
||||||
The admission checkpoint (`30012361`) intentionally stopped before executing
|
|
||||||
the FIFO. The continuation executor (`5db3de3c`,
|
|
||||||
`RuntimeInitialCreateContinuationExecutor`) completes the mechanism: one
|
|
||||||
synchronous, retry-idempotent `Execute` transaction adopts the acknowledged
|
|
||||||
initial placement exactly once, emits the local player's after-enter-world
|
|
||||||
hook request, replays deferred missing-parent raw Creates and queued parent
|
|
||||||
relations by parent GUID (whole-bucket detach, FIFO dispatch,
|
|
||||||
cancellation-aware restore windows), and drains the mixed FIFO strictly by
|
|
||||||
sequence — classifying each retained Position at execution time with live
|
|
||||||
inputs and driving authored placements through the canonical
|
|
||||||
`RuntimeSetPositionState` lifecycle with retryable yields. Apply bodies are
|
|
||||||
shared with the legacy fused inbound paths through gate-less instance seams
|
|
||||||
that keep the one snapshot store in lockstep; same-incarnation Create
|
|
||||||
envelopes apply atomically with buffered publication; every abandonment path
|
|
||||||
retires the residence and converges the combined ownership ledger. The
|
|
||||||
executor has NO production caller yet — graphical and no-window Create still
|
|
||||||
use legacy `RegisterEntity` — and the next checkpoint must switch both
|
|
||||||
production routes onto this owner rather than create another snapshot or
|
|
||||||
placement path.
|
|
||||||
|
|
||||||
`LiveEntityRuntime` is the App projection/lifecycle host.
|
`LiveEntityRuntime` is the App projection/lifecycle host.
|
||||||
`RegisterLiveEntity` first creates or refreshes canonical Runtime state without
|
`RegisterLiveEntity` first creates or refreshes canonical Runtime state without
|
||||||
an App record. `MaterializeLiveEntity` claims the Runtime local ID and creates
|
an App record. `MaterializeLiveEntity` claims the Runtime local ID and creates
|
||||||
|
|
|
||||||
|
|
@ -120,14 +120,6 @@ ViewModel or command had to change, because none of them had ever imported
|
||||||
writes against `IPanelRenderer`; a renderer implementation translates those
|
writes against `IPanelRenderer`; a renderer implementation translates those
|
||||||
calls at runtime. Plugin-facing UI follows the same rule.
|
calls at runtime. Plugin-facing UI follows the same rule.
|
||||||
|
|
||||||
The shared chat parser/router/catalog and its four command intents live in
|
|
||||||
`AcDream.Runtime/Chat`, not in a panel or App. `AcDream.UI.Abstractions`
|
|
||||||
references Runtime so retained `ChatVM` can implement the narrow
|
|
||||||
`IChatCommandFeedback` seam and its existing panel input can call the shared
|
|
||||||
router. That dependency does not permit panels to import App, windowing,
|
|
||||||
rendering, audio, or another presentation backend; Runtime itself remains
|
|
||||||
presentation-independent and its dependency guards enforce that boundary.
|
|
||||||
|
|
||||||
**Status:** there is currently no `IPanelRenderer` implementation in the tree —
|
**Status:** there is currently no `IPanelRenderer` implementation in the tree —
|
||||||
the ImGui one went with V11 and the replacement is issue **#258**. The contract
|
the ImGui one went with V11 and the replacement is issue **#258**. The contract
|
||||||
is kept rather than deleted precisely because this rule proved its worth; a new
|
is kept rather than deleted precisely because this rule proved its worth; a new
|
||||||
|
|
@ -216,12 +208,8 @@ documentation, not an exhaustive allowlist):
|
||||||
- `DevToolsFramePresenter -> DevToolsPanelSet -> panel/ViewModel bindings` for
|
- `DevToolsFramePresenter -> DevToolsPanelSet -> panel/ViewModel bindings` for
|
||||||
the optional developer UI.
|
the optional developer UI.
|
||||||
- `WorldRenderFrameBuilder -> RuntimeWorldFrameSettingsPreview ->
|
- `WorldRenderFrameBuilder -> RuntimeWorldFrameSettingsPreview ->
|
||||||
IRuntimeSettingsPreviewSource -> RuntimeSettingsController` for the settings
|
IRuntimeSettingsPreviewSource -> RuntimeSettingsController -> optional
|
||||||
snapshot read before world drawing. (The `SettingsVM` draft-preview tail of
|
SettingsVM` for the live settings draft preview applied before world drawing.
|
||||||
this seam was retired at Campaign OP slice OP9 — the preview source now
|
|
||||||
mirrors the committed Display/Audio snapshot directly; the retail Options
|
|
||||||
panel applies its edits live through `SaveDisplay`/`SaveAudio` instead of a
|
|
||||||
draft layer.)
|
|
||||||
- `LocalPlayerPortalViewport -> LocalPlayerTeleportController ->
|
- `LocalPlayerPortalViewport -> LocalPlayerTeleportController ->
|
||||||
GameplayInputFrameController -> InputDispatcher.Fired -> GameWindow` for the
|
GameplayInputFrameController -> InputDispatcher.Fired -> GameWindow` for the
|
||||||
canonical portal/input lifetime and the host's input-action subscription.
|
canonical portal/input lifetime and the host's input-action subscription.
|
||||||
|
|
@ -274,7 +262,9 @@ src/AcDream.App/
|
||||||
│ ├── DeferredLiveEntityMotionRuntimeBindings.cs # fail-fast construction-order bridge
|
│ ├── DeferredLiveEntityMotionRuntimeBindings.cs # fail-fast construction-order bridge
|
||||||
│ ├── LiveEntityShadowPublisher.cs # authoritative exact-owner/residency collision gate
|
│ ├── LiveEntityShadowPublisher.cs # authoritative exact-owner/residency collision gate
|
||||||
│ ├── RemoteInboundMotionDispatcher.cs # shared animated/headless UpdateMotion funnel
|
│ ├── RemoteInboundMotionDispatcher.cs # shared animated/headless UpdateMotion funnel
|
||||||
│ └── RemoteTeleportHook.cs # ordered retail teleport teardown actions (teleport_hook port; C4 4b-3 deleted RemoteTeleportController/RemoteTeleportPlacement/RemoteShadowPlacementSynchronizer — the teleport arm now routes through RuntimeRemotePlacementDriveController, the same canonical placement owner the far snap uses)
|
│ ├── RemoteTeleportController.cs # loaded/pending teleport placement ownership
|
||||||
|
│ ├── RemoteTeleportHook.cs # ordered retail teleport teardown actions
|
||||||
|
│ └── RemoteTeleportPlacement.cs # collision-seated SetPosition transition commit
|
||||||
├── World/
|
├── World/
|
||||||
│ ├── InboundPhysicsStateController.cs # timestamps + accepted spawn snapshots
|
│ ├── InboundPhysicsStateController.cs # timestamps + accepted spawn snapshots
|
||||||
│ ├── LiveEntityRuntime.cs # shipped: logical lifetime + ServerGuid↔entity.Id translation
|
│ ├── LiveEntityRuntime.cs # shipped: logical lifetime + ServerGuid↔entity.Id translation
|
||||||
|
|
@ -427,19 +417,28 @@ radar/status targeting, effects, and audio remain closed until reveal. This
|
||||||
distinction is part of the Slice E connected gate, not an alternate live-entity
|
distinction is part of the Slice E connected gate, not an alternate live-entity
|
||||||
lifetime.
|
lifetime.
|
||||||
|
|
||||||
**C4 route 4b-3 (2026-08-04) deleted `Physics/RemoteTeleportController` and
|
Remote teleport placement is bounded in `Physics/RemoteTeleportController`,
|
||||||
`RemoteTeleportPlacement` outright** (605 + 85 lines, plus
|
not `GameWindow`: it retains at most one pending request per materialized
|
||||||
`RemoteShadowPlacementSynchronizer`, 49 lines). Remote teleport placement is
|
incarnation, scopes it by the live generation and accepted PositionSequence,
|
||||||
no longer a separate App-layer incarnation-scoped machine — it is the SAME
|
and asks `RemoteTeleportPlacement` to collision-seat the current body when the
|
||||||
canonical `RuntimeRemotePlacementDriveController` route 4b-2's far snap
|
destination projection is available. `GameWindow` supplies lifecycle and
|
||||||
already uses, dispatched via `ApplyAcceptedRemoteTeleport`, which shares
|
shadow-sync callbacks only; canonical identity remains in
|
||||||
`StoresAcceptedDestination`/`StoreAcceptedDestinationPose` unchanged.
|
`RuntimeEntityDirectory`, and App placement retains the exact projection key.
|
||||||
`teleport_hook` (@0x00514ED0) still runs first — `RemoteTeleportHook`,
|
Failed hydration restores the captured source and delegates an
|
||||||
invoked from `LiveEntityNetworkUpdateController`'s teleport-arm dispatch —
|
incarnation-scoped shadow restore to `LiveEntityPresentationController` while
|
||||||
and its `report_collision_end(this, 1)` action now routes through
|
that source is unloaded, so Hidden/UnHide and teleport never become competing
|
||||||
`RuntimeCollisionReportingState.LeaveWorld` (the existing, exact port of
|
restore owners. A newer placement transfers that restore into an explicit
|
||||||
that retail call) rather than `ShadowObjectRegistry.Suspend`, which ports a
|
generation-scoped active-placement state before its rebucket visibility edge
|
||||||
DIFFERENT retail function `teleport_hook` never calls. `GpuWorldState`
|
even while Hidden. All intervening Hidden/UnHide and projection edges defer to
|
||||||
|
that owner until stable success or rollback completes; only then can it restore,
|
||||||
|
re-defer the source, or hand a Hidden result back for UnHide. The
|
||||||
|
`ILiveEntityRemotePlacementRuntime` seam keeps the complete cell/contact
|
||||||
|
handoff available across same-body runtime-wrapper replacement; replacing the
|
||||||
|
canonical body or dropping the placement contract within one incarnation is
|
||||||
|
rejected even after an operational component clear. `RemoteMotion.Body` is
|
||||||
|
constructor-owned; hydration compares pending/current wrappers directly to the
|
||||||
|
record body rather than trusting wrapper-to-wrapper equality. Binding reads an
|
||||||
|
interface Body getter once and reuses that snapshot. `GpuWorldState`
|
||||||
performs remove+place as one spatial rebucket,
|
performs remove+place as one spatial rebucket,
|
||||||
then commits and serially drains visibility edges; `LiveEntityRuntime` filters
|
then commits and serially drains visibility edges; `LiveEntityRuntime` filters
|
||||||
delayed duplicates. A rollback inside an observer cannot race the outer
|
delayed duplicates. A rollback inside an observer cannot race the outer
|
||||||
|
|
@ -475,7 +474,7 @@ useful ordering seam, but its ownership status is **partial**.
|
||||||
| Area | Status | Current truth |
|
| Area | Status | Current truth |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Startup options | **Complete** | `RuntimeOptions` owns startup configuration (`eda936dc`). Remaining direct environment reads are legacy runtime diagnostics, not startup configuration. |
|
| Startup options | **Complete** | `RuntimeOptions` owns startup configuration (`eda936dc`). Remaining direct environment reads are legacy runtime diagnostics, not startup configuration. |
|
||||||
| Network session | **Complete Runtime ownership** | `LiveSessionController` owns the sole `WorldSession` generation and resolve/create/Connect/pre-world selection/EnterWorld/Tick/stop/reconnect/disposal transaction. Its `RuntimeCharacterSelectionState` owns the full active roster (including greyed entries and retained wire slots), highlight, delete confirmation, restore/delete/error state, generation/lifecycle, borrowed view, ordered deltas, and typed commands. A selector-free graphical launch pauses on that owner; explicit and headless selection retain the established fallback. Runtime route owners preserve exact inbound/outbound ordering and retryable teardown. App supplies immutable options, graphical/domain callbacks, and borrowed projections—no mirrored session, selection state, or reset plan. |
|
| Network session | **Complete Runtime ownership** | `RuntimeLiveSessionController` owns the sole `WorldSession` generation and resolve/create/Connect/selection/EnterWorld/Tick/stop/reconnect/disposal transaction. Runtime route owners preserve exact inbound/outbound ordering and retryable teardown. App supplies immutable options, graphical/domain callbacks, and one borrowed inertable UI command projection—no mirrored session or reset plan (`75930787`). |
|
||||||
| World environment | **J6.1 complete Runtime ownership** | `RuntimeWorldEnvironmentState` owns the instance-scoped Dereth calendar, synchronized clock, weather progression/state, selected day group, AdminEnvirons state, and typed debug overrides. App converts immutable DAT sky definitions once and projects the borrowed Runtime snapshot into rendering; no process-global Region origin or second App clock/weather owner remains (`902076c0`). TS-54/TS-55 register the remaining centered UI sound and full fog/ambient/radar behavior gaps. |
|
| World environment | **J6.1 complete Runtime ownership** | `RuntimeWorldEnvironmentState` owns the instance-scoped Dereth calendar, synchronized clock, weather progression/state, selected day group, AdminEnvirons state, and typed debug overrides. App converts immutable DAT sky definitions once and projects the borrowed Runtime snapshot into rendering; no process-global Region origin or second App clock/weather owner remains (`902076c0`). TS-54/TS-55 register the remaining centered UI sound and full fog/ambient/radar behavior gaps. |
|
||||||
| Live identity/lifetime | **J3 complete** | `RuntimeEntityObjectLifetime` owns the sole `RuntimeEntityDirectory`, live `ClientObjectTable`, direct views, and ordered entity/object stream. The directory owns canonical GUID/incarnation/local-ID identity, accepted snapshots/timestamps, parent state, operation versions, and tombstones. `LiveEntityProjectionStore` owns App graphical sidecars by exact `RuntimeEntityKey`; hydration, presentation components, `GpuWorldState` residence/visibility, and retryable teardown preserve that key without another authority. Exact receipts precede fallible callbacks, re-entrant commits drain synchronously in sequence, and stable reset/disposal must converge the complete ledger to zero (`f46ddb5c`, `420e5eea`, `e937cc36`, `5ef8b537`, `ce3ac310`, `119b7c11`). |
|
| Live identity/lifetime | **J3 complete** | `RuntimeEntityObjectLifetime` owns the sole `RuntimeEntityDirectory`, live `ClientObjectTable`, direct views, and ordered entity/object stream. The directory owns canonical GUID/incarnation/local-ID identity, accepted snapshots/timestamps, parent state, operation versions, and tombstones. `LiveEntityProjectionStore` owns App graphical sidecars by exact `RuntimeEntityKey`; hydration, presentation components, `GpuWorldState` residence/visibility, and retryable teardown preserve that key without another authority. Exact receipts precede fallible callbacks, re-entrant commits drain synchronously in sequence, and stable reset/disposal must converge the complete ledger to zero (`f46ddb5c`, `420e5eea`, `e937cc36`, `5ef8b537`, `ce3ac310`, `119b7c11`). |
|
||||||
| Inbound/object-frame order | **Complete App orchestration** | `UpdateFrameOrchestrator` owns the complete typed host phase graph; `RetailInboundEventDispatcher`, `RetailLiveFrameCoordinator`, `LiveObjectFrameController`, `LiveSpatialPresentationReconciler`, streaming/input/teleport/player-mode/camera owners preserve the accepted order. `GameWindow.OnUpdate` is one profiler-scoped handoff (`e91f3102`). |
|
| Inbound/object-frame order | **Complete App orchestration** | `UpdateFrameOrchestrator` owns the complete typed host phase graph; `RetailInboundEventDispatcher`, `RetailLiveFrameCoordinator`, `LiveObjectFrameController`, `LiveSpatialPresentationReconciler`, streaming/input/teleport/player-mode/camera owners preserve the accepted order. `GameWindow.OnUpdate` is one profiler-scoped handoff (`e91f3102`). |
|
||||||
|
|
|
||||||
|
|
@ -178,20 +178,6 @@ package schema, bake, DAT reader, collision formula, or render portal graph
|
||||||
changed. Evidence:
|
changed. Evidence:
|
||||||
`docs/research/2026-07-26-prepared-indoor-transit-regression.md`.
|
`docs/research/2026-07-26-prepared-indoor-transit-regression.md`.
|
||||||
|
|
||||||
**Cell availability semantics (2026-07-31, corrected after full-catalog
|
|
||||||
audit).** Raw and prepared CellStruct publication retains a `CellPhysics`
|
|
||||||
record when the physics root is empty but requires a valid containment root.
|
|
||||||
The installed 729,888-record raw and prepared catalogs contain zero rootless
|
|
||||||
containment payloads. A malformed null/-1 root is quarantined atomically; the
|
|
||||||
recursive inside base case applies only to a missing positive child below a
|
|
||||||
valid root. Registration-side outdoor floods still add outside cells but skip
|
|
||||||
transit when the active CLandCell is unavailable, and every later outdoor
|
|
||||||
candidate independently requires its own visible landcell before building
|
|
||||||
transit. The existing reflood retries after terrain/cell hydration. Both raw
|
|
||||||
and prepared point-in-cell paths preserve retail's zero-portals guard. No
|
|
||||||
package schema or DAT reader changed.
|
|
||||||
Evidence: `docs/research/2026-07-31-cell-availability-semantics.md`.
|
|
||||||
|
|
||||||
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
|
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
|
||||||
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
|
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
|
||||||
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
|
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
|
||||||
|
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
# 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.<yyyyMMddHHmm> <- 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 <release asset base>` 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.
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# acdream — strategic roadmap
|
# acdream — strategic roadmap
|
||||||
|
|
||||||
**Status:** Living document. Updated 2026-08-14. **M3 landed; M4 is active.** M3's retail casting/UI, R6 locomotion/collision/projectile/teleport/radar rebaseline, deterministic fresh-login/portal world lifecycle, and final two-client portal observer flow are user-gated. All eight slices of the behavior-preserving ownership campaign in [`docs/architecture/code-structure.md`](../architecture/code-structure.md), their automated closeout, and the user's connected visual matrix are complete. Modern Runtime J3 canonical entity/object lifetime and J4 gameplay-state ownership are closed at `89e6b207`; J5.1 canonical selection/combat/target-mode ownership is closed at `b298f99f`, J5.2 interaction transactions at `f5f7b417`, J5.3 combat/magic intent at `20df9d15`, J5.4 local movement/outbound cadence at `aa3f4a60`, J5.5 per-session physics/remote simulation at `7e6033d0`, J5.6 projectile simulation at `2aee3356`, and J5.7 combined simulation closeout at `cdee7a4b`. J6.1 world-environment ownership is closed at `902076c0`; J6.2 canonical reveal generation and typed destination readiness is closed at `a6860d55` plus `acb845d8`; J6.3 exact F751/Position destination correlation is closed at `6a063a27`; J6.4 exact graphical-host acknowledgement and owner cleanup is closed at `18d17d8b`. J7's one graphical `GameRuntime` root is closed at `ce41efb9`, including the user's 2026-07-27 exact post-cutover visual acceptance. J8 closed Slice J at `a9a822f2` with one shared graphical/no-window root and generation-reset transaction. Slice K Linux headless/multi-session work is closed. K0's tested no-presentation Windows/Linux boundary closed at `aada8a37`, K1's portable single-session host at `f8cb840f`, K2's deterministic scheduler and shared bot API at `7e8acb74` plus `38e83640`, and K3's shared-content/isolation plus connected observer gate at `3f340125`. K4 closed through `776482da`: 1/5/10/30-root isolation and two-hour simulated endurance, death/randomized cancellation, committed resource ceilings, ten minutes of exact native Linux two-account connected sampling, ACE-confirmed graceful logout, and zero-debt Runtime/content convergence all pass. Slice L Linux graphical/platform work is parked at its L1 implementation checkpoint by user direction on 2026-07-27. Issue #225's lifestone/particle alpha comparison remains a separate rendering visual gate.
|
**Status:** Living document. Updated 2026-07-27. **M3 landed; M4 is active.** M3's retail casting/UI, R6 locomotion/collision/projectile/teleport/radar rebaseline, deterministic fresh-login/portal world lifecycle, and final two-client portal observer flow are user-gated. All eight slices of the behavior-preserving ownership campaign in [`docs/architecture/code-structure.md`](../architecture/code-structure.md), their automated closeout, and the user's connected visual matrix are complete. Modern Runtime J3 canonical entity/object lifetime and J4 gameplay-state ownership are closed at `89e6b207`; J5.1 canonical selection/combat/target-mode ownership is closed at `b298f99f`, J5.2 interaction transactions at `f5f7b417`, J5.3 combat/magic intent at `20df9d15`, J5.4 local movement/outbound cadence at `aa3f4a60`, J5.5 per-session physics/remote simulation at `7e6033d0`, J5.6 projectile simulation at `2aee3356`, and J5.7 combined simulation closeout at `cdee7a4b`. J6.1 world-environment ownership is closed at `902076c0`; J6.2 canonical reveal generation and typed destination readiness is closed at `a6860d55` plus `acb845d8`; J6.3 exact F751/Position destination correlation is closed at `6a063a27`; J6.4 exact graphical-host acknowledgement and owner cleanup is closed at `18d17d8b`. J7's one graphical `GameRuntime` root is closed at `ce41efb9`, including the user's 2026-07-27 exact post-cutover visual acceptance. J8 closed Slice J at `a9a822f2` with one shared graphical/no-window root and generation-reset transaction. Slice K Linux headless/multi-session work is closed. K0's tested no-presentation Windows/Linux boundary closed at `aada8a37`, K1's portable single-session host at `f8cb840f`, K2's deterministic scheduler and shared bot API at `7e8acb74` plus `38e83640`, and K3's shared-content/isolation plus connected observer gate at `3f340125`. K4 closed through `776482da`: 1/5/10/30-root isolation and two-hour simulated endurance, death/randomized cancellation, committed resource ceilings, ten minutes of exact native Linux two-account connected sampling, ACE-confirmed graceful logout, and zero-debt Runtime/content convergence all pass. Slice L Linux graphical/platform work is parked at its L1 implementation checkpoint by user direction on 2026-07-27. Issue #225's lifestone/particle alpha comparison remains a separate rendering visual gate.
|
||||||
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
|
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
|
||||||
|
|
||||||
**Slice L checkpoint:** L0 closed at `66f114b2` with one typed graphical
|
**Slice L checkpoint:** L0 closed at `66f114b2` with one typed graphical
|
||||||
|
|
@ -22,9 +22,8 @@ falsification ledger — is [`2026-07-27-vulkan-campaign.md`](2026-07-27-vulkan-
|
||||||
The user signed the V10 cutover; V11 deleted GL (−27,670 lines) and all
|
The user signed the V10 cutover; V11 deleted GL (−27,670 lines) and all
|
||||||
deferred reruns pass on the GL-free tree.
|
deferred reruns pass on the GL-free tree.
|
||||||
|
|
||||||
**Campaign N — retail reliable network transport (CLOSED 2026-07-29,
|
**Campaign N — retail reliable network transport (ACTIVE, started
|
||||||
user-accepted; #260 closed — a real wire loss recovered live during the
|
2026-07-29):** the #260 live-server wedge root-caused to missing packet-loss
|
||||||
acceptance session):** the #260 live-server wedge root-caused to missing packet-loss
|
|
||||||
recovery in both directions (no outbound retransmission; inbound ISAAC burned
|
recovery in both directions (no outbound retransmission; inbound ISAAC burned
|
||||||
in arrival order) — acdream could not survive a single lost UDP packet, and
|
in arrival order) — acdream could not survive a single lost UDP packet, and
|
||||||
loopback gates were structurally blind to it. The campaign ports retail's
|
loopback gates were structurally blind to it. The campaign ports retail's
|
||||||
|
|
@ -35,138 +34,9 @@ N0–N6 with a permanent loss-injection gate at N5 and a user Coldeve
|
||||||
endurance session as final acceptance. The plan is
|
endurance session as final acceptance. The plan is
|
||||||
[`2026-07-29-network-transport-campaign.md`](2026-07-29-network-transport-campaign.md).
|
[`2026-07-29-network-transport-campaign.md`](2026-07-29-network-transport-campaign.md).
|
||||||
|
|
||||||
**Campaign P — physics retail-feel parity (CLOSED 2026-07-31):**
|
|
||||||
user-directed pre-vendor detour closing every physics-scope gap the
|
|
||||||
2026-07-29 audit found: stat-coupled movement (burden/stamina/vitae →
|
|
||||||
run/jump), the collision response-layer edge family (friction gate,
|
|
||||||
PrecipiceSlide, sled, steep-poly chain, #116), remote-object residuals
|
|
||||||
(Setup sphere lists, PK bits, #165), world specials (entry restrictions,
|
|
||||||
water sink-in), deferred fidelity (#167, #153), the #262 login defect, and
|
|
||||||
a ledger pass. Goal: zero physics TS rows, no unargued feel-affecting AP
|
|
||||||
rows, one final batched connected visual matrix. Sonnet implements, Opus
|
|
||||||
reviews. The plan is
|
|
||||||
[`2026-07-29-physics-parity-campaign.md`](2026-07-29-physics-parity-campaign.md).
|
|
||||||
The 2026-07-31 #268 stat-chain package is implemented and user-accepted:
|
|
||||||
panel and Runtime movement share retail's complete augmentation ordering,
|
|
||||||
the authored per-fragment vitae/buff/debuff colors are live, and AP-127 plus
|
|
||||||
TS-8 are retired by focused and end-to-end packet tests. #269's
|
|
||||||
capture-driven slope-slide residual is also closed and user-accepted:
|
|
||||||
`CTransition::validate_transition` now performs retail's non-OK-only
|
|
||||||
remembered-plane restore with the preceding `OBJECTINFO::kill_velocity`.
|
|
||||||
The matrix then exposed #272: burden was invalidated by base Strength but not
|
|
||||||
by Strength enchantment add/purge. Runtime movement plus both retained burden
|
|
||||||
surfaces now use effective Strength and the canonical enchantment-change edge;
|
|
||||||
automated gates pass and the connected buff/death gate was user-accepted on
|
|
||||||
2026-07-31. The final session also accepted burden/exhaustion, wall/corner,
|
|
||||||
crowd, two-client remote/door/portal, and shallow-water behavior. The user
|
|
||||||
waived the general sweep and explicitly deferred the barred-house gate as
|
|
||||||
#274. The later exact-location #273 tight-gap gate is now fixed and accepted.
|
|
||||||
|
|
||||||
**Campaign A — audio retail parity (CODE-COMPLETE 2026-08-08):** ported
|
|
||||||
retail's CPU-side 2-D pan+gain audio model (animation-hook sounds, the
|
|
||||||
`0xF750` server sound channel, PhysicsScripts, the interface sound bank,
|
|
||||||
and the region ambient system); found and corrected three false claims in
|
|
||||||
the old E.2 row (no 3-D pool, gain-based eviction, weighted variant
|
|
||||||
picking) and deleted the music API (retail has none). Slices A1–A6 landed;
|
|
||||||
open tail is #358 (Ctrl+M mute chord never fires) and the formal
|
|
||||||
plan-status flip. Plan:
|
|
||||||
[`2026-08-08-audio-parity-campaign.md`](2026-08-08-audio-parity-campaign.md).
|
|
||||||
|
|
||||||
**Campaign CH — chat & interface-text retail parity (CODE-COMPLETE
|
|
||||||
2026-08-09, pending the connected user gate):** four implementation
|
|
||||||
slices closing the chat surface's biggest retail-parity gaps ahead of the
|
|
||||||
friend-alpha: CH1 the complete 34-value `LogTextType` color table, CH2
|
|
||||||
the on-screen SpewBox interface text (jump-refusal class, WeenieError
|
|
||||||
routing table), CH3 side-channel membership/wire/echo parity (Turbine
|
|
||||||
rooms, the legacy family, self-echo), and CH4 command registry completion
|
|
||||||
(138 of 152 retail verbs now execute locally). Every slice landed a
|
|
||||||
dual-Opus review (retail faithfulness + architecture) before its gate;
|
|
||||||
full Release suite 12,221 passed / 4 skipped / 0 failed. Plan and ledger:
|
|
||||||
[`2026-08-09-chat-parity-campaign.md`](2026-08-09-chat-parity-campaign.md);
|
|
||||||
in-client acceptance script:
|
|
||||||
[`2026-08-09-campaign-ch-test-script.md`](../research/2026-08-09-campaign-ch-test-script.md).
|
|
||||||
|
|
||||||
**Campaign LA — launcher/installer/updater + retail character-select
|
|
||||||
(ACTIVE 2026-08-14):** the alpha-program launcher: an Avalonia app
|
|
||||||
(Windows + Linux) doing triple duty — install (DAT locate → `acdream-bake`
|
|
||||||
with progress → SHA record), update (GitHub Releases manifest, verified
|
|
||||||
download, atomic version swap, launcher self-update), and launch
|
|
||||||
(ThwargLauncher-model server × account × character profiles with full
|
|
||||||
in-UI CRUD; plaintext credential file by explicit user decision).
|
|
||||||
File-contract orchestration of both hosts: session config in (K1 shape +
|
|
||||||
plugins + login commands), password via child stdin, versioned JSONL
|
|
||||||
status events out. Adds the headless character-list probe, plugin hosting
|
|
||||||
+ login commands on both hosts, and the retail character-management
|
|
||||||
screen (recon-corrected: `gmCharacterManagementUI` is a flat listbox with
|
|
||||||
Enter/Delete/Restore — NO 3D preview on retail's select screen; Create is
|
|
||||||
a future campaign). Spec:
|
|
||||||
[`2026-08-14-launcher-campaign-design.md`](../superpowers/specs/2026-08-14-launcher-campaign-design.md);
|
|
||||||
plan + ledger:
|
|
||||||
[`2026-08-14-launcher-campaign.md`](2026-08-14-launcher-campaign.md).
|
|
||||||
LA0 through LA11's automated scope are review-closed: the portable path boundary,
|
|
||||||
failure-isolated launch/status contract, BCL-only launcher core, shared
|
|
||||||
composer-to-both-host-loader anti-drift gate, and character wire messages are
|
|
||||||
landed. The self-contained Avalonia launcher, transactional two-host plugin
|
|
||||||
lifetime, shared login-command route, Runtime-owned retail selection state,
|
|
||||||
authored DAT character screen, crash-safe verified installer, and atomic
|
|
||||||
cross-platform updater/self-updater are integrated. Windows group-isolated
|
|
||||||
Headless stop, isolated A/B update fixtures, strict status/redaction evidence,
|
|
||||||
and one exact Windows/Ubuntu operator script are also landed. The integrated
|
|
||||||
clean preflight passes 32/32 commands and 14,012 tests / 5 skips. Campaign code
|
|
||||||
is complete but not shipped: the connected/visual/real-DAT user gate is the
|
|
||||||
only remaining boundary.
|
|
||||||
|
|
||||||
**Remaining physics-divergence closeout (ACTIVE, checkpoint 2026-08-03):** the user then
|
|
||||||
authorized retirement of the remaining proven collision/placement gaps before
|
|
||||||
vendor work resumes. Nested retry, edge/StepDown/Path-6 ordering, exact cell
|
|
||||||
availability, atomic collision generations, canonical Core SetPosition,
|
|
||||||
Runtime lost-cell residence, authored mover/body preparation, placement
|
|
||||||
receipts and observers, collision-prefix replacement, authoritative route
|
|
||||||
classification, and initial Create residence are landed as bisectable
|
|
||||||
checkpoints. Commit `38fd4b8d` retains the accepted Create placement plus
|
|
||||||
fresher Position FIFO until exact placement and ordered adoption complete.
|
|
||||||
Commit `30012361` completes the bounded inbound-admission checkpoint: all
|
|
||||||
accepted mixed updates remain deep-frozen in exact arrival order while the
|
|
||||||
initial placement waits, with no early canonical/public snapshot, event, or
|
|
||||||
presentation mutation. Missing-parent raw Create, delete, reconnect/reset,
|
|
||||||
GUID reuse, malformed projections, saturation, and reentrant teardown are
|
|
||||||
covered. Commit `5db3de3c` (2026-08-02) completes the continuation executor:
|
|
||||||
retry-idempotent single adoption of the acknowledged initial placement,
|
|
||||||
retail's exact Create tail, GUID-keyed deferred-child and parent-relation
|
|
||||||
replay with cancellation-aware windows, strict-sequence mixed-FIFO drain with
|
|
||||||
execution-time retail Position routing, shared apply bodies keeping one
|
|
||||||
snapshot store in lockstep, and converged ownership ledgers on every
|
|
||||||
abandonment path — dual independent reviews PASS; register rows
|
|
||||||
AD-59/AD-60/AP-130/AP-131/AP-132/TS-62/TS-63 filed in the same commit.
|
|
||||||
Production initial Create registration is now cut over by C3c (`529e0e9d`).
|
|
||||||
The O(changed) collision-publication checkpoint and five stabilization fixes
|
|
||||||
through `175ad6b0` restore recenter convergence, remote world-frame placement
|
|
||||||
and targeting, distant Use, one-shot spell/projectile/static effects, and
|
|
||||||
login materialization; the corresponding connected user gates passed. The
|
|
||||||
fixture reconciliation closed 2026-08-03 as #281 (the recorded "six" measured
|
|
||||||
as 43). **C4 is implementation-complete 2026-08-05**: routes 2 (`9966b531`,
|
|
||||||
user-accepted), 4a (`44830a0e`), 4b-1/4b-2/4b-3
|
|
||||||
(`2e8e09ac`/`7f1c1f5a`/`6dc7ba51` — 4b-2's far-snap and 4b-3's teleport-ts
|
|
||||||
connected gates user-passed), 5 (`36255af0`, test-gated by design — ACE never
|
|
||||||
sends a missile UpdatePosition), 6 (`1b484937`, a zero-production-line
|
|
||||||
closure whose coverage tests found and fixed #314), 7 (`cd3129e9`, child-cell
|
|
||||||
propagation moved from a render tick into Runtime), and 3 (`e0f96a55`, the
|
|
||||||
canonical portal placement authority) all place through the canonical Runtime
|
|
||||||
owner; the complete Release suite measures 11,090 passed / 4 skipped /
|
|
||||||
0 failed. The campaign remains open for C4's four owed connected gates
|
|
||||||
(route 6 drops; route 7 equip/carry counted only with `cause=propagate`
|
|
||||||
probe lines; route 3 portal/recall counted only with `[local-tp]` probe
|
|
||||||
lines and not scored against #318; 4b-3's `cause=cellless` case, whose
|
|
||||||
recorded trigger route 7 invalidated), portal destination prefetch #280, the
|
|
||||||
final-binary C5 legacy-deletion/suite/soak/visual matrix, AP-22 authored
|
|
||||||
object shapes, and AD-10 remote contact-plane projection.
|
|
||||||
Current plan and successor handoff:
|
|
||||||
[`2026-08-02-placement-cutover.md`](2026-08-02-placement-cutover.md) and
|
|
||||||
[`2026-08-05-c4-closeout-handoff.md`](../research/2026-08-05-c4-closeout-handoff.md).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Paused program: world interaction completion (M4 prelude)
|
## Current program: world interaction completion (M4 prelude)
|
||||||
|
|
||||||
The active work order is
|
The active work order is
|
||||||
[`2026-07-23-world-interaction-completion.md`](2026-07-23-world-interaction-completion.md).
|
[`2026-07-23-world-interaction-completion.md`](2026-07-23-world-interaction-completion.md).
|
||||||
|
|
@ -463,7 +333,7 @@ W1 plan: [`docs/superpowers/plans/2026-06-02-unified-cell-graph-stage1.md`](../s
|
||||||
| B.2 | Player movement mode — Tab-toggled WASD ground walking, walk/run/idle animations, third-person chase camera, MoveToState + AutonomousPosition outbound, portal entry. Outdoor-only MVP. | Live ✓ |
|
| B.2 | Player movement mode — Tab-toggled WASD ground walking, walk/run/idle animations, third-person chase camera, MoveToState + AutonomousPosition outbound, portal entry. Outdoor-only MVP. | Live ✓ |
|
||||||
| D.1 | 2D ortho overlay + font rendering (StbTrueTypeSharp atlas + TextRenderer + DebugOverlay) | Visual ✓ |
|
| D.1 | 2D ortho overlay + font rendering (StbTrueTypeSharp atlas + TextRenderer + DebugOverlay) | Visual ✓ |
|
||||||
| E.1 | Motion-hook expansion — AnimationSequencer fires all 27 hook types per crossed frame; PosFrames root motion + vel/omega exposure; IAnimationHookSink + AnimationHookRouter fan-out | Tests ✓ |
|
| E.1 | Motion-hook expansion — AnimationSequencer fires all 27 hook types per crossed frame; PosFrames root motion + vel/omega exposure; IAnimationHookSink + AnimationHookRouter fan-out | Tests ✓ |
|
||||||
| E.2 | Audio engine — OpenAL voice bank, SoundTable/Wave decoding, AudioHookSink wiring. **Superseded 2026-08-08 by Campaign A** (`docs/plans/2026-08-08-audio-parity-campaign.md`), which found three of this row's claims false: the pool was not 3-D in retail (every gameplay buffer is 2-D and spatialization is CPU-side), eviction compared gain rather than retail's DAT priority, and the "probability-weighted variant picking" was the defect itself — probability is a Bernoulli silence gate, not a weight. Campaign A also added the 0xF750 server sound channel, the interface sound bank, and the region ambient system, and deleted the music API (retail has none). | Tests ✓ / listening gate owed |
|
| E.2 | Audio engine — OpenAL 16-voice 3D pool with retail-faithful quieter-slot eviction, SoundTable cookbook (probability-weighted variant picking), Wave PCM decoder, AudioHookSink wiring | Tests ✓ |
|
||||||
| E.3 | Particle system (data layer) — ParticleSystem with 13 motion integrators, EmitterDescRegistry, ParticleHookSink wiring all CreateParticle / DestroyParticle / StopParticle hooks | Tests ✓ |
|
| E.3 | Particle system (data layer) — ParticleSystem with 13 motion integrators, EmitterDescRegistry, ParticleHookSink wiring all CreateParticle / DestroyParticle / StopParticle hooks | Tests ✓ |
|
||||||
| E.4 | Combat notifications + outbound — AttackTargetRequest (0x0008), 7 combat notification parsers (Victim/Defender/Attacker/Evasion/AttackDone/UpdateHealth), CombatState per-entity health tracker | Tests ✓ |
|
| E.4 | Combat notifications + outbound — AttackTargetRequest (0x0008), 7 combat notification parsers (Victim/Defender/Attacker/Evasion/AttackDone/UpdateHealth), CombatState per-entity health tracker | Tests ✓ |
|
||||||
| E.5 | Spell cast wire — CastSpellRequest targeted (0x004A) + untargeted (0x0048), Spellbook (learned spells + active-enchantment layers), 5 enchantment GameEvent parsers | Tests ✓ |
|
| E.5 | Spell cast wire — CastSpellRequest targeted (0x004A) + untargeted (0x0048), Spellbook (learned spells + active-enchantment layers), 5 enchantment GameEvent parsers | Tests ✓ |
|
||||||
|
|
@ -480,7 +350,7 @@ W1 plan: [`docs/superpowers/plans/2026-06-02-unified-cell-graph-stage1.md`](../s
|
||||||
| I.3 | `LiveCommandBus` + `WorldSession.SendTalk` / `SendTell` / `SendChannel` — replaces `NullCommandBus.Instance` with a real handler-registry `ICommandBus`. New `SendChatCmd` record + `ChannelResolver` legacy-id mapping (per holtburger). 3-line wrappers around existing `ChatRequests.BuildTalk/Tell/ChatChannel`. | Tests ✓ |
|
| I.3 | `LiveCommandBus` + `WorldSession.SendTalk` / `SendTell` / `SendChannel` — replaces `NullCommandBus.Instance` with a real handler-registry `ICommandBus`. New `SendChatCmd` record + `ChannelResolver` legacy-id mapping (per holtburger). 3-line wrappers around existing `ChatRequests.BuildTalk/Tell/ChatChannel`. | Tests ✓ |
|
||||||
| I.4 | `ChatPanel` input field + slash commands — Enter-to-submit input field; `ChatInputParser` recognises `/say` `/t` `/tell` `/r` `/g` `/f` `/a` `/m` `/p` `/v` `/cv` `/lfg` `/trade` `/role` `/society` `/olthoi`; `ChatVM.LastIncomingTellSender` tracks for `/r` reply. `ImGui.WantCaptureKeyboard` already suppresses WASD on focus. | Live ✓ |
|
| I.4 | `ChatPanel` input field + slash commands — Enter-to-submit input field; `ChatInputParser` recognises `/say` `/t` `/tell` `/r` `/g` `/f` `/a` `/m` `/p` `/v` `/cv` `/lfg` `/trade` `/role` `/society` `/olthoi`; `ChatVM.LastIncomingTellSender` tracks for `/r` reply. `ImGui.WantCaptureKeyboard` already suppresses WASD on focus. | Live ✓ |
|
||||||
| I.5 | Holtburger inbound chat parity + Windows-1252 codec — `EmoteText (0x01E0)`, `SoulEmote (0x01E2)`, `ServerMessage (0xF7E0)`, `PlayerKilled (0x019E)` parsers + `WeenieError` routing through `GameEventWiring`. Global string codec switch from `Encoding.ASCII` to `Encoding.GetEncoding(1252)` so accented names round-trip per retail + holtburger. | Tests ✓ |
|
| I.5 | Holtburger inbound chat parity + Windows-1252 codec — `EmoteText (0x01E0)`, `SoulEmote (0x01E2)`, `ServerMessage (0xF7E0)`, `PlayerKilled (0x019E)` parsers + `WeenieError` routing through `GameEventWiring`. Global string codec switch from `Encoding.ASCII` to `Encoding.GetEncoding(1252)` so accented names round-trip per retail + holtburger. | Tests ✓ |
|
||||||
| I.6 | TurbineChat codec + `ChatChannelInfo` — full `0xF7DE` codec with three payload variants (`EventSendToRoom`, `RequestSendToRoomById`, `Response`), UTF-16LE strings with variable-length prefix, `SetTurbineChatChannels (0x0295)` parser, unified `ChatChannelInfo` (Legacy + Turbine variants), `TurbineChatState`. **Correction (Campaign CH slice CH3, 2026-08-09): the "ACE doesn't host a TurbineChat server" note above was FALSE — ACE has a complete, on-by-default TurbineChat implementation; see `docs/research/2026-08-09-chat-side-channels-vs-ace.md` §1.** | Tests ✓ |
|
| I.6 | TurbineChat codec + `ChatChannelInfo` — full `0xF7DE` codec with three payload variants (`EventSendToRoom`, `RequestSendToRoomById`, `Response`), UTF-16LE strings with variable-length prefix, `SetTurbineChatChannels (0x0295)` parser, unified `ChatChannelInfo` (Legacy + Turbine variants), `TurbineChatState`. **ACE doesn't host a TurbineChat server — codec is ready when retail-emulating servers exist.** | Tests ✓ |
|
||||||
| I.7 | `CombatChatTranslator` — retail-faithful combat-text formatters into `ChatLog` ("You hit drudge for 50 slashing damage (87%)"). Subscribes to `CombatState`'s `DamageTaken` / `DamageDealtAccepted` / `EvadedIncoming` / `MissedOutgoing` / `KillLanded`; `AttackDone` is control-only and deliberately silent. | Tests ✓ |
|
| I.7 | `CombatChatTranslator` — retail-faithful combat-text formatters into `ChatLog` ("You hit drudge for 50 slashing damage (87%)"). Subscribes to `CombatState`'s `DamageTaken` / `DamageDealtAccepted` / `EvadedIncoming` / `MissedOutgoing` / `KillLanded`; `AttackDone` is control-only and deliberately silent. | Tests ✓ |
|
||||||
| K | Input architecture — `Action` enum, `KeyChord`, `KeyBindings`, multicast `InputDispatcher` with scope-stack + modal capture, retail-default keymap (152 bindings), `keybinds.json` persistence, F11 Settings panel with click-to-rebind + conflict detection, main menu bar + View menu | Live ✓ |
|
| K | Input architecture — `Action` enum, `KeyChord`, `KeyBindings`, multicast `InputDispatcher` with scope-stack + modal capture, retail-default keymap (152 bindings), `keybinds.json` persistence, F11 Settings panel with click-to-rebind + conflict detection, main menu bar + View menu | Live ✓ |
|
||||||
| L.0 | Full retail-style Settings interface — F11 tabbed panel with 6 tabs (Keybinds + Display + Audio + Gameplay + Chat + Character). `settings.json` at `%LOCALAPPDATA%\acdream\`, per-toon `Character` keying (swapped on EnterWorld). Display GL knobs (Resolution / Fullscreen / VSync / FOV / ShowFps) + Audio (Master / SFX) live-wired; Gameplay / Chat / Character settings persist for server-sync wiring later. Tab API extension to `IPanelRenderer`; chat Copy mode (read-only multi-line); per-panel layout reset; FramebufferResize handler keeps GL viewport + camera aspect + panel positions in sync. | Live ✓ |
|
| L.0 | Full retail-style Settings interface — F11 tabbed panel with 6 tabs (Keybinds + Display + Audio + Gameplay + Chat + Character). `settings.json` at `%LOCALAPPDATA%\acdream\`, per-toon `Character` keying (swapped on EnterWorld). Display GL knobs (Resolution / Fullscreen / VSync / FOV / ShowFps) + Audio (Master / SFX) live-wired; Gameplay / Chat / Character settings persist for server-sync wiring later. Tab API extension to `IPanelRenderer`; chat Copy mode (read-only multi-line); per-panel layout reset; FramebufferResize handler keeps GL viewport + camera aspect + panel positions in sync. | Live ✓ |
|
||||||
|
|
@ -999,7 +869,7 @@ the way retail + holtburger expect.
|
||||||
- **✓ SHIPPED — I.3 — `LiveCommandBus` + `WorldSession.Send{Talk,Tell,Channel}`.** Replaces `NullCommandBus.Instance` with a real handler-registry `ICommandBus`. New `SendChatCmd` record + `ChatChannelKind` enum + `ChannelResolver` legacy-id mapping (per holtburger). `WorldSession.SendTalk` / `SendTell` / `SendChannel` are 3-line wrappers around existing `ChatRequests.BuildTalk/Tell/ChatChannel`. Commit `8e6e5a0`.
|
- **✓ SHIPPED — I.3 — `LiveCommandBus` + `WorldSession.Send{Talk,Tell,Channel}`.** Replaces `NullCommandBus.Instance` with a real handler-registry `ICommandBus`. New `SendChatCmd` record + `ChatChannelKind` enum + `ChannelResolver` legacy-id mapping (per holtburger). `WorldSession.SendTalk` / `SendTell` / `SendChannel` are 3-line wrappers around existing `ChatRequests.BuildTalk/Tell/ChatChannel`. Commit `8e6e5a0`.
|
||||||
- **✓ SHIPPED — I.4 — `ChatPanel` input field + slash commands.** Enter-to-submit input field on `ChatPanel`; `ChatInputParser` recognises `/say` `/t` `/tell` `/r` `/g` `/f` `/a` `/m` `/p` `/v` `/cv` `/lfg` `/trade` `/role` `/society` `/olthoi`; `ChatVM.LastIncomingTellSender` tracks for `/r` reply. `ImGui.WantCaptureKeyboard` already suppresses WASD on input focus. Commit `f14296c`.
|
- **✓ SHIPPED — I.4 — `ChatPanel` input field + slash commands.** Enter-to-submit input field on `ChatPanel`; `ChatInputParser` recognises `/say` `/t` `/tell` `/r` `/g` `/f` `/a` `/m` `/p` `/v` `/cv` `/lfg` `/trade` `/role` `/society` `/olthoi`; `ChatVM.LastIncomingTellSender` tracks for `/r` reply. `ImGui.WantCaptureKeyboard` already suppresses WASD on input focus. Commit `f14296c`.
|
||||||
- **✓ SHIPPED — I.5 — Holtburger inbound chat parity + Windows-1252.** `EmoteText (0x01E0)`, `SoulEmote (0x01E2)`, `ServerMessage (0xF7E0)`, `PlayerKilled (0x019E)` parsers + `WeenieError` routing through `GameEventWiring`. Global string codec switch from `Encoding.ASCII` to `Encoding.GetEncoding(1252)` so accented names round-trip per retail + holtburger. Commit `ff5ed9e`.
|
- **✓ SHIPPED — I.5 — Holtburger inbound chat parity + Windows-1252.** `EmoteText (0x01E0)`, `SoulEmote (0x01E2)`, `ServerMessage (0xF7E0)`, `PlayerKilled (0x019E)` parsers + `WeenieError` routing through `GameEventWiring`. Global string codec switch from `Encoding.ASCII` to `Encoding.GetEncoding(1252)` so accented names round-trip per retail + holtburger. Commit `ff5ed9e`.
|
||||||
- **✓ SHIPPED — I.6 — TurbineChat codec + `ChatChannelInfo`.** Full `0xF7DE` codec with three payload variants (`EventSendToRoom`, `RequestSendToRoomById`, `Response`), UTF-16LE strings with variable-length prefix, `SetTurbineChatChannels (0x0295)` parser, unified `ChatChannelInfo` (Legacy + Turbine variants), `TurbineChatState`. **Correction (Campaign CH slice CH3, 2026-08-09): the "ACE doesn't host a TurbineChat server" note above was FALSE — ACE has a complete, on-by-default TurbineChat implementation; see `docs/research/2026-08-09-chat-side-channels-vs-ace.md` §1.** Commit `ca968fc`.
|
- **✓ SHIPPED — I.6 — TurbineChat codec + `ChatChannelInfo`.** Full `0xF7DE` codec with three payload variants (`EventSendToRoom`, `RequestSendToRoomById`, `Response`), UTF-16LE strings with variable-length prefix, `SetTurbineChatChannels (0x0295)` parser, unified `ChatChannelInfo` (Legacy + Turbine variants), `TurbineChatState`. **ACE doesn't host a TurbineChat server — codec is ready when retail-emulating servers exist.** Commit `ca968fc`.
|
||||||
- **✓ SHIPPED — I.7 — `CombatChatTranslator`.** Retail-faithful combat-text formatters into `ChatLog` ("You hit drudge for 50 slashing damage (87%)"). Subscribes to visible damage/evasion/miss/kill events; `AttackDone` was removed from chat after named retail + ACE proved its nonzero final status is control-only. Commit `3d26c8e`, corrected 2026-07-11.
|
- **✓ SHIPPED — I.7 — `CombatChatTranslator`.** Retail-faithful combat-text formatters into `ChatLog` ("You hit drudge for 50 slashing damage (87%)"). Subscribes to visible damage/evasion/miss/kill events; `AttackDone` was removed from chat after named retail + ACE proved its nonzero final status is control-only. Commit `3d26c8e`, corrected 2026-07-11.
|
||||||
- **✓ SHIPPED — I.8 — Docs alignment.** Roadmap (this file) + `docs/ISSUES.md` issues #14-#20 closed + `memory/project_chat_pipeline.md` crib + `MEMORY.md` index entry + `CLAUDE.md` UI strategy paragraph all updated to reflect Phase I shipped state. Commit `(this commit)`.
|
- **✓ SHIPPED — I.8 — Docs alignment.** Roadmap (this file) + `docs/ISSUES.md` issues #14-#20 closed + `memory/project_chat_pipeline.md` crib + `MEMORY.md` index entry + `CLAUDE.md` UI strategy paragraph all updated to reflect Phase I shipped state. Commit `(this commit)`.
|
||||||
|
|
||||||
|
|
@ -2131,7 +2001,6 @@ OpenGL ceiling; revisit macOS only if a supported graphics backend is chosen.
|
||||||
| Can't fight monsters | **M2 LANDED 2026-07-15** ✓ — melee/missile, death, loot, inventory loop user-gated |
|
| Can't fight monsters | **M2 LANDED 2026-07-15** ✓ — melee/missile, death, loot, inventory loop user-gated |
|
||||||
| Can't cast spells | **M3 connected single-client casting/effects gate passed** ✓; final two-client portal observer gate remains |
|
| Can't cast spells | **M3 connected single-client casting/effects gate passed** ✓; final two-client portal observer gate remains |
|
||||||
| No inventory panel | **D.5 / M2 SHIPPED + user-gated** ✓ — bags, stacks, paperdoll, equipment, drag/drop, loot |
|
| No inventory panel | **D.5 / M2 SHIPPED + user-gated** ✓ — bags, stacks, paperdoll, equipment, drag/drop, loot |
|
||||||
| No player-to-player trading | **Secure trade SHIPPED + two-client user gate PASSED 2026-08-14** ✓ — gmSecureTradeUI window (LayoutDesc 0x2100000D), full 0x1F6–0x208 wire, both retail open paths (Use-on-player, drag-item-onto-player option), staged-item trading marker, cancel text; research `docs/research/2026-08-14-trade-lane{A,B,C}-*.md`, memory `project_secure_trade.md` |
|
|
||||||
| No character creation — must use ACE admin | **Phase H.4** |
|
| 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) |
|
| Sky is a flat color | **Phase G.1** (shipped; F7 cycles time, F10 cycles weather) |
|
||||||
| Can't join allegiance | **Phase H.2** |
|
| Can't join allegiance | **Phase H.2** |
|
||||||
|
|
|
||||||
|
|
@ -87,40 +87,8 @@ program: spell-bar overflow, status Use/Assess, assessment information,
|
||||||
equipped-child picking, vendor browsing, and authoritative vendor
|
equipped-child picking, vendor browsing, and authoritative vendor
|
||||||
transactions. This is deliberately using the extracted interaction owners and
|
transactions. This is deliberately using the extracted interaction owners and
|
||||||
canonical shared main-panel host before quest/emote/character-creation bodies
|
canonical shared main-panel host before quest/emote/character-creation bodies
|
||||||
broaden the feature surface. Slices 1–4 are user-accepted. Campaign P's
|
broaden the feature surface. Slices 1–3 are user-accepted; resume at Slice 4
|
||||||
connected feel matrix closed on 2026-07-31 with tight-gap collision clearance
|
equipped-child picking.
|
||||||
(#273) and the deferred restricted-house gate (#274) explicitly carried. The
|
|
||||||
user subsequently authorized the remaining physics-divergence closeout before
|
|
||||||
vendor work. Placement Slice 4B2 is now complete through dormant SetPosition
|
|
||||||
activation, graphical/no-window placement receipts, collision-prefix
|
|
||||||
replacement, authoritative route classification, pre-placement App staging,
|
|
||||||
and Runtime's initial Create residence/FIFO transaction at `38fd4b8d`.
|
|
||||||
The bounded admission checkpoint is complete at `30012361`: every accepted
|
|
||||||
same-incarnation Create, ObjDesc, Parent, Pickup, Position, Movement, State,
|
|
||||||
and Vector update is retained as a deep-frozen, arrival-ordered Runtime action
|
|
||||||
without changing the canonical/public snapshot or presentation while initial
|
|
||||||
placement waits. The continuation executor is complete at `5db3de3c`
|
|
||||||
(2026-08-02): one retry-idempotent Runtime `Execute` transaction adopts the
|
|
||||||
acknowledged initial placement exactly once, applies retail's Create tail,
|
|
||||||
replays deferred missing-parent raw Creates and queued parent relations by
|
|
||||||
parent GUID with cancellation-aware detach/restore windows, and drains the
|
|
||||||
mixed FIFO strictly by sequence with execution-time retail Position routing
|
|
||||||
through the canonical SetPosition lifecycle. Independent retail-conformance
|
|
||||||
and architecture/adversarial reviews both PASS after five implementation
|
|
||||||
rounds; register rows AD-59/AD-60/AP-130/AP-131/AP-132/TS-62/TS-63 document
|
|
||||||
the slice's deviations; Runtime tests 903/903, complete Release solution
|
|
||||||
10,696/4 skips. Production initial Create registration is now cut over by C3c
|
|
||||||
(`529e0e9d`). The O(changed) collision-publication checkpoint and
|
|
||||||
stabilization fixes `01f4791e`, `670f307c`, `1fc529cd`, `f24532ad`, and
|
|
||||||
`175ad6b0` are connected user-accepted for recenter convergence, remote
|
|
||||||
monster/static placement and targeting, distant Use, spell/projectile/static
|
|
||||||
VFX, and login materialization. The remaining order is six selected-fixture
|
|
||||||
reconciliations, C4 routes 2–7, portal destination prefetch #280, C5's
|
|
||||||
final-binary complete suite/soak/two-client matrix, AP-22 shape fidelity,
|
|
||||||
AD-10 remote contact-plane projection, and final ledger closeout. Resume Slice
|
|
||||||
5 vendor browsing only after that closeout or a new explicit user direction.
|
|
||||||
Canonical checkpoint:
|
|
||||||
[`2026-08-02-placement-cutover.md`](2026-08-02-placement-cutover.md).
|
|
||||||
|
|
||||||
The separately authorized modern-runtime performance program has completed
|
The separately authorized modern-runtime performance program has completed
|
||||||
Slices A–D: corrected measurement, prepared-package bake/dedup, package-only
|
Slices A–D: corrected measurement, prepared-package bake/dedup, package-only
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ inline when ported.
|
||||||
### BR-1 — The surface gate — ✅ RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1)
|
### BR-1 — The surface gate — ✅ RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1)
|
||||||
|
|
||||||
**Premise falsified before implementation (the BR-1 pre-check,
|
**Premise falsified before implementation (the BR-1 pre-check,
|
||||||
`Diagnostic_ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
|
`ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
|
||||||
every portal fill** — all four extraction paths skip `Stippling.NoPos`
|
every portal fill** — all four extraction paths skip `Stippling.NoPos`
|
||||||
positive sides (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
|
positive sides (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
|
||||||
`PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, `GfxObjMesh.Build:71`),
|
`PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, `GfxObjMesh.Build:71`),
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,8 @@ component-disabled ACE characters to the modern scarab/prismatic formula,
|
||||||
resolves formula icons by their DAT icon DIDs, installs those icons as each
|
resolves formula icons by their DAT icon DIDs, installs those icons as each
|
||||||
authored template root's own foreground image, and migrates stale examination
|
authored template root's own foreground image, and migrates stale examination
|
||||||
dimensions once to the authored 310 x 400 extent. The final connected
|
dimensions once to the authored 310 x 400 extent. The final connected
|
||||||
assessment gate passed on 2026-07-24. Slice 4 equipped-child world picking
|
assessment gate passed on 2026-07-24. Slices 1–3 are complete; resume at Slice
|
||||||
(with the Opus F1 wielded-pickup-legality correction) passed its connected
|
4, equipped-child world picking.
|
||||||
visual gate on Coldeve and was user-accepted 2026-07-29. Slices 1–4 are
|
|
||||||
complete; resume at Slice 5, vendor browsing.
|
|
||||||
**Milestone:** M4 prerequisite/preamble.
|
**Milestone:** M4 prerequisite/preamble.
|
||||||
**Architecture:** retained gameplay UI over shared selection, object, and
|
**Architecture:** retained gameplay UI over shared selection, object, and
|
||||||
interaction state. `GameWindow` remains a composition/callback shell.
|
interaction state. `GameWindow` remains a composition/callback shell.
|
||||||
|
|
@ -409,8 +407,7 @@ Named retail references and executable pseudocode are recorded in
|
||||||
|
|
||||||
## Slice 4 — equipped-child world picking
|
## Slice 4 — equipped-child world picking
|
||||||
|
|
||||||
**Status:** USER-ACCEPTED 2026-07-29 — the two-client visual gate passed on
|
**Status:** implemented 2026-07-29, pending the two-client visual gate. Owner
|
||||||
Coldeve ("child world picking works"). Owner
|
|
||||||
shape per the program table held: pure world-query/picking policy plus
|
shape per the program table held: pure world-query/picking policy plus
|
||||||
presentation anchor. No wire, physics, renderer, or
|
presentation anchor. No wire, physics, renderer, or
|
||||||
`EquippedChildRenderController` changes. `LiveEntityRuntime` gained scoped
|
`EquippedChildRenderController` changes. `LiveEntityRuntime` gained scoped
|
||||||
|
|
@ -554,211 +551,3 @@ already published per frame to `EntityEffectPoseRegistry` (`PublishChildPose`,
|
||||||
- Existing architectural divergence, unchanged by this slice: retail re-arms
|
- Existing architectural divergence, unchanged by this slice: retail re-arms
|
||||||
the pick every frame for hover/tooltips (`sr_MouseOver`); acdream picks on
|
the pick every frame for hover/tooltips (`sr_MouseOver`); acdream picks on
|
||||||
demand per click against the last published frame, with an identity recheck.
|
demand per click against the last published frame, with an identity recheck.
|
||||||
|
|
||||||
## Slice 5 — vendor browse lifecycle (contract authored 2026-08-08)
|
|
||||||
|
|
||||||
**Research foundation:**
|
|
||||||
`docs/research/2026-08-08-slice5-vendor-browse-research.md` (all wire,
|
|
||||||
retail-symbol, and seam citations live there — this contract only records
|
|
||||||
DECISIONS and ordered work). Browse only; every buy/sell/accept concern is
|
|
||||||
Slice 6 (see the research doc's §D fence).
|
|
||||||
|
|
||||||
### Decisions on the research doc's eight open questions
|
|
||||||
|
|
||||||
1. `VendorState` lives in `AcDream.Core.Items`, a sibling of
|
|
||||||
`ExternalContainerState`.
|
|
||||||
2. The shared `PublicWeenieDesc`-body parser IS extracted from
|
|
||||||
`CreateObject.TryParse` FIRST, as its own behavior-preserving commit
|
|
||||||
(5.0). Existing CreateObject wire tests must pass unchanged; the
|
|
||||||
extraction adds no parsing behavior.
|
|
||||||
3. `ShopSystem::BuyPrice`/`SellPrice` (0x006B6120/0x006B6180,
|
|
||||||
byte-identical to ACE's `GetBuyCost`/`GetSellCost`) are ported NOW as
|
|
||||||
pure Core functions with golden-value conformance tests — the browse
|
|
||||||
list shows retail-correct prices from day one.
|
|
||||||
4. No request-correlation token in Slice 5: the panel always opens on the
|
|
||||||
browse/Buy tab. Slice 6 adds the sell-initiated correlation.
|
|
||||||
5. `VendorProfile::InqAcceptability` (which player items the vendor would
|
|
||||||
accept) is deferred to Slice 6 with the sell UI it gates.
|
|
||||||
6. Category/type filter tabs are IN SCOPE for retail parity. The
|
|
||||||
implementer's D0 reads `VendorItemsUI::AddTypeFilter` /
|
|
||||||
`ListContainsType` (around 0x004C05C0/0x004C0D90) into a pseudocode
|
|
||||||
note before any UI work; if that read reveals a mechanism too large
|
|
||||||
for this slice, STOP and report (fallback — flat list + register row —
|
|
||||||
requires explicit approval, not implementer discretion).
|
|
||||||
7. The vendor panel's top-level LayoutDesc id is NOT yet known: the UI
|
|
||||||
piece budgets a LayoutImporter discovery pass (the exact process that
|
|
||||||
found the examination window's 0x2100006B), cross-checked by the two
|
|
||||||
known tab-control ids (0x100000B9 Buy / 0x100000BB Sell) resolving
|
|
||||||
under the candidate root.
|
|
||||||
8. AP-110 is narrowed in the SAME COMMIT that lands the panel: "vendor"
|
|
||||||
leaves the absent-panels list; whatever sub-scope remains absent after
|
|
||||||
this slice gets its own precise row.
|
|
||||||
|
|
||||||
### Ordered work (each lands separately, bisectable)
|
|
||||||
|
|
||||||
- **5.0** — extract the shared `PublicWeenieDesc`-body parser
|
|
||||||
(behavior-preserving; wire tests unchanged; no vendor code).
|
|
||||||
- **5.1** — `ApproachVendor` (GameEvent 0x0062) inbound parser:
|
|
||||||
`VendorProfile` + the full-desc item list, against the research doc's
|
|
||||||
byte-verified field table; Core.Net tests with golden byte fixtures.
|
|
||||||
- **5.2** — `VendorState` in Core.Items + the BuyPrice/SellPrice pure
|
|
||||||
port + conformance tests.
|
|
||||||
- **5.3** — Runtime ownership: `RuntimeInventoryState` owns the vendor
|
|
||||||
session per the J4.2 pattern (generation-gated, torn down on
|
|
||||||
reset/portal/logout); the 0x0062 route opens it; close is CLIENT-LOCAL
|
|
||||||
(nothing sent on the wire) via the retail distance-watcher semantics;
|
|
||||||
`ItemInteractionController._activeVendorId` /
|
|
||||||
`ItemInteractionPolicy.ActiveVendorId` finally receive the real id.
|
|
||||||
- **5.4** — the authored vendor panel: layout-id discovery, LayoutDesc
|
|
||||||
import via the Slice-3 examination-window pattern (foreground stacking,
|
|
||||||
authored extent), browse list reusing Slice-1's retained list/scrollbar
|
|
||||||
+ DAT icon resolution, category tabs per the D0 read, prices via 5.2.
|
|
||||||
- **5.5** — register narrowing (decision 8) rides the 5.4 landing commit.
|
|
||||||
|
|
||||||
### Trap list (binding)
|
|
||||||
|
|
||||||
Do not touch: the J5.2 strict use gate's semantics (the vendor open rides
|
|
||||||
the EXISTING use transaction — no second gate, per the J4.5 invariant);
|
|
||||||
`CreateObject.TryParse` behavior (5.0 is extraction only); anything in the
|
|
||||||
Slice 6 fence (no buy/sell wire, no currency mutation, no
|
|
||||||
InqAcceptability). New event handling follows the newest existing
|
|
||||||
GameEvent handler's registration pattern, not a bespoke route.
|
|
||||||
|
|
||||||
### Gates
|
|
||||||
|
|
||||||
Per landing: build + full suite green (clean-room before each landing
|
|
||||||
commit). Slice gate (user, connected, ~3 min): approach a Holtburg
|
|
||||||
vendor, use them, the authored panel opens on the browse tab with
|
|
||||||
retail-correct items/icons/prices; category tabs filter; walking out of
|
|
||||||
range closes the panel by itself; nothing is purchasable anywhere.
|
|
||||||
|
|
||||||
## Slice 6 — vendor transactions, buy arc (contract authored 2026-08-08; user-pulled forward)
|
|
||||||
|
|
||||||
**Research:** `docs/research/2026-08-08-slice6-vendor-transactions-research.md`.
|
|
||||||
User direction: "I cant buy anything... Fix that first." Root cause of all
|
|
||||||
four reported symptoms: `VendorUiController` never touches the shared
|
|
||||||
`SelectionState`/`StackSplitQuantityState` owners every other panel uses.
|
|
||||||
|
|
||||||
### Decisions
|
|
||||||
|
|
||||||
1. **Shop items materialize into `ClientObjectTable`** while the session is
|
|
||||||
open (retail creates real CWeenieObjects from the vendor list — Slice 5
|
|
||||||
research §A.2) and are REMOVED on session close/replace/reset. The
|
|
||||||
implementer verifies retail's removal site (gmVendorUI::CloseVendor
|
|
||||||
family) and mirrors its lifecycle. This dissolves F7c's blocker:
|
|
||||||
`ExamineItemRequested` gets wired in this slice.
|
|
||||||
2. **Vendor selection is the GLOBAL selection**: a new vendor change source
|
|
||||||
on the canonical `SelectionState`; row-click selects through it; the
|
|
||||||
status bar and the existing byte-faithful `StackSplitQuantityState`
|
|
||||||
slider follow automatically (the split-size mask helper from 5.4's F2
|
|
||||||
feeds the vendor-owned seeding exactly as gmToolbarUI does at
|
|
||||||
pc:198635-198790).
|
|
||||||
3. **Buy = retail's Buy button**: immediate single-item purchase
|
|
||||||
(gmVendorUI::BuySingleItem, pc:201661) of the selected item with the
|
|
||||||
slider-chosen quantity for stacks. Outbound `0x005F`: vendorGuid,
|
|
||||||
count, per-item (i32 amount, u32 guid), TRAILING u32
|
|
||||||
alternateCurrencyId — the real client sends it (CM_Vendor::Event_Buy,
|
|
||||||
pc:689288) even though ACE's reader ignores it; we port the real
|
|
||||||
client. The request rides the EXISTING J5.2 one-request-at-a-time
|
|
||||||
reservation and completes on `UseDone` (0x01C7) — already the wired
|
|
||||||
completion signal; no second gate.
|
|
||||||
4. **Reconciliation is the existing inbound machinery**: money property
|
|
||||||
updates, inventory CreateObject, and the ApproachVendor refresh
|
|
||||||
(VendorState.Refreshed) all flow through landed handlers — the slice
|
|
||||||
VERIFIES the loop end-to-end rather than adding an owner.
|
|
||||||
5. **No double-click-to-buy**: retail has no such mechanism (confirmed
|
|
||||||
against the full named table). We match retail. If the user wants it
|
|
||||||
as a deliberate modernization it needs their explicit call + an AP row.
|
|
||||||
6. **Deferred, still AP-161**: the Add button / Buying-tab staging list
|
|
||||||
and everything Sell (0x0060 — researched, next arc).
|
|
||||||
|
|
||||||
### Ordered work (one implementer, bisectable commits)
|
|
||||||
|
|
||||||
- **6.1** shop-item materialization + removal lifecycle + examine wiring.
|
|
||||||
- **6.2** the selection coupling (source, row-click, split seeding) —
|
|
||||||
status bar + slider light up.
|
|
||||||
- **6.3** the 0x005F builder (golden-byte tests incl. the trailing dword),
|
|
||||||
Buy-button wiring, gate/UseDone completion, and the verified
|
|
||||||
reconciliation round-trip. Register: AP-161 narrowed in the landing.
|
|
||||||
|
|
||||||
### Gate (user, connected)
|
|
||||||
|
|
||||||
Select a stacked item → it shows in the status bar with the slider; pick
|
|
||||||
a quantity; Buy → coins drop by the displayed price, the stack lands in
|
|
||||||
the pack, the shop refreshes; a single-item buy works; insufficient funds
|
|
||||||
fails cleanly; the session still closes on walk-away/portal with the
|
|
||||||
materialized items removed.
|
|
||||||
|
|
||||||
## Slice 6b/6c — vendor completion (contract authored 2026-08-08)
|
|
||||||
|
|
||||||
**Research:** `docs/research/2026-08-08-slice6b-vendor-completion-research.md`.
|
|
||||||
Closes the user's seven-finding gate batch (dropdown polish landed at
|
|
||||||
`33b45ee5`). Ordered chunks, one implementer:
|
|
||||||
|
|
||||||
1. **Move-to-use (Q2):** wire the existing client-predicted approach
|
|
||||||
primitive (`PlayerInteractionMovementSink.BeginApproach`,
|
|
||||||
`MovementType.MoveToObject` — today Pickup-only) onto `RequestUse`, so
|
|
||||||
using a vendor (or anything) beyond range walks the player in first,
|
|
||||||
retail's shape. No new movement machinery.
|
|
||||||
2. **Buy staging (Q3):** `AddToBuyList` semantics per the research
|
|
||||||
trace — Add stages the selection + slider quantity into the Buying
|
|
||||||
tab's list (rendered count/price), Buy on that tab sends ONE batched
|
|
||||||
0x005F with every staged entry, the two removal shapes + Clear, and
|
|
||||||
retail's X-close confirm dialog when staging is non-empty (the current
|
|
||||||
unconditional hide stays correct only for empty staging).
|
|
||||||
3. **Selling (Q4):** the Selling tab's list is THE drop target — accept
|
|
||||||
pack-item drops via the `ExternalContainerController` drag-handler
|
|
||||||
pattern, filter through `InqAcceptability` (all four rejection reasons
|
|
||||||
with retail's exact strings), staged sell list, batched 0x0060, and
|
|
||||||
the existing reconciliation machinery.
|
|
||||||
4. **Status bar (Q5) — user evidence is the axiom:** the code-reading
|
|
||||||
says our chain already matches retail, but the user's live session
|
|
||||||
says the stack count/value/split-bar presentation is absent for a
|
|
||||||
vendor selection. Reproduce in a UI-level test FIRST (drive the real
|
|
||||||
SelectedObjectController mount with a vendor selection); fix what the
|
|
||||||
reproduction reveals; if it genuinely cannot reproduce, STOP and
|
|
||||||
report with the test as evidence for a live-probe session.
|
|
||||||
5. **Pack order (Q1):** code-verified correct (wire placement position →
|
|
||||||
front insert). NO change; the gate re-checks it live and #352 gets
|
|
||||||
filed only if it reproduces.
|
|
||||||
|
|
||||||
Register: same-commit rows for any deviation; AP-161 narrows again as
|
|
||||||
staging/sell land (its remaining scope should shrink to nothing or to
|
|
||||||
precisely what stays absent).
|
|
||||||
|
|
||||||
**Gate (user):** click a vendor from afar → walk-in + open; stage two
|
|
||||||
different items with quantities → Buy All → one transaction, coins/items
|
|
||||||
correct; drag a sellable item onto the Selling tab → stages → Sell →
|
|
||||||
coins up, item gone; an InqAcceptability-rejected item shows retail's
|
|
||||||
refusal; X with a staged list → confirm dialog; stacked selection shows
|
|
||||||
count/value/split-bar in the toolbar; bought items land at the front of
|
|
||||||
the pack.
|
|
||||||
|
|
||||||
## PROGRAM CLOSEOUT — 2026-08-08: all six slices COMPLETE, user-accepted
|
|
||||||
|
|
||||||
Slices 5 and 6 closed together after the vendor arc's final gates. The
|
|
||||||
complete retail vendor experience is live and user-verified end to end:
|
|
||||||
walk-to-use from afar (the never-animated-target physics-host resolver +
|
|
||||||
the cylinder-gap range watcher), the authored panel with the scrollable
|
|
||||||
category dropdown (arrow-cap, downward, left-aligned), retail cost
|
|
||||||
sentences with live purse repaint on every money change, per-unit and
|
|
||||||
whole-stack pricing per the split-size mask, the MaxStackSize quantity
|
|
||||||
slider with the right-justified count entry and two-line name wrap in
|
|
||||||
the toolbar, staged buying (accumulate + 5000 cap + shop-row decrement +
|
|
||||||
the four pre-send guards + the batched 0x005F + the X-close confirm),
|
|
||||||
selling (Selling-tab drop target with drag-over auto-switch,
|
|
||||||
InqAcceptability with verbatim rejection strings, BF_RETAINED, batched
|
|
||||||
0x0060), double-click-to-buy (AP-171, user-approved modernization),
|
|
||||||
prepend pack ordering (the cross-queue placement replay), and
|
|
||||||
materialized shop objects with ownership-checked lifecycle + examine.
|
|
||||||
|
|
||||||
Four adversarial Opus reviews found 34 defects before the user saw them;
|
|
||||||
the user's connected gates found eleven more that only live sessions
|
|
||||||
expose; two latent client-wide crashers (#348 cursor-handle exhaustion,
|
|
||||||
#350 render-ledger overflow) were exposed, root-caused, and fixed along
|
|
||||||
the way. Landed across `e45c95b0..af1a1ef9`. Deferred with issues/rows:
|
|
||||||
#352 (range-watcher cylinder unit test), AP-166's pending-sell
|
|
||||||
highlight, AP-167 (SellSingleItem's non-empty-container branch), AP-168's
|
|
||||||
shop-stock half, Buying/Selling staging polish beyond the landed scope.
|
|
||||||
|
|
||||||
This closes the pre-M4 world-interaction completion program.
|
|
||||||
|
|
|
||||||
|
|
@ -1,420 +0,0 @@
|
||||||
# Campaign P — Physics Retail-Feel Parity
|
|
||||||
|
|
||||||
**Status:** CLOSED 2026-07-31 — final user matrix accepted; tight-gap
|
|
||||||
clearance issue #273 and deferred restricted-house gate #274 are explicitly
|
|
||||||
carried follow-ups.
|
|
||||||
|
|
||||||
**Filed:** 2026-07-29. **Directed by the user** as a pre-vendor-management
|
|
||||||
detour after the same-day physics/collision retail-fidelity audit. The
|
|
||||||
world-interaction program (Slice 5, vendor browsing) resumes when this
|
|
||||||
campaign closes.
|
|
||||||
|
|
||||||
**Execution model:** Claude drives autonomously slice-to-slice. Sonnet
|
|
||||||
subagents implement bounded chunks against this plan's specs; an Opus
|
|
||||||
review subagent gates every slice boundary. The only user stops are
|
|
||||||
(a) the final batched connected visual matrix, (b) a DO-NOT-RETRY
|
|
||||||
conflict, (c) anything destructive. All the CLAUDE.md workflow rules
|
|
||||||
apply: grep-named-first → pseudocode → port → conformance test; register
|
|
||||||
moves in the same commit; no workarounds.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The parity goal (the autonomy contract)
|
|
||||||
|
|
||||||
**"Retail Movement Parity v1"** — the campaign is DONE when all of the
|
|
||||||
following are auditable-true:
|
|
||||||
|
|
||||||
1. **Zero physics-scope temporary stopgaps.** TS-1, TS-4, TS-5, TS-23,
|
|
||||||
TS-46 retired by porting the retail mechanism. TS-24, TS-35, TS-40
|
|
||||||
either retired or re-classified (IA/AD) with a recorded justification.
|
|
||||||
2. **No feel-affecting approximations left unargued.** AP-7 resolved by
|
|
||||||
decoding retail's friction state gate; AP-10 restored to retail's
|
|
||||||
0.1 m water sink-in; AP-25 replaced by the retail effective-skill
|
|
||||||
chain (vitae/enchantment-aware); AP-71 (`check_entry_restrictions`)
|
|
||||||
ported.
|
|
||||||
3. **Issue ledger:** #262, #165, #166, #116, #167, #72, #153 closed;
|
|
||||||
the stale "pending visual gate" statuses on #172/#173/#174/#175/#41
|
|
||||||
reconciled (folded into the final matrix below). Explicitly excluded:
|
|
||||||
#235 (user-deferred 2026-07-27) and #256/#257 (lifecycle/memory, not
|
|
||||||
physics feel — separate track).
|
|
||||||
4. **Verification:** each port carries decomp citations + conformance
|
|
||||||
tests; `dotnet build` + full Release suite green at every slice
|
|
||||||
commit; one batched connected visual matrix (below) passes at the
|
|
||||||
end, run by the user.
|
|
||||||
|
|
||||||
Anything not in this list is out of scope for the campaign — file it,
|
|
||||||
don't chase it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Slices
|
|
||||||
|
|
||||||
### P1 — Stat-coupled movement (burden / stamina / vitae) — retires TS-5, AP-25, the burden gap
|
|
||||||
|
|
||||||
**Status (2026-07-30): COMPLETE.** Landed at `9355ddce`; Opus review
|
|
||||||
APPROVE at `001e466d` (which also retired UN-8 — the CanJump polarity is
|
|
||||||
byte-PROVEN `load < 2.0` from the PDB-paired binary — and recorded the
|
|
||||||
PK-timer jump-cost decode for P3). Full Release suite 9,880/0/5 at the
|
|
||||||
slice gate. Retail's vitae/enchant chain reuses the M3 bucket-4
|
|
||||||
representation; `JumpStaminaCost` never refuses (weak-jump only) — the
|
|
||||||
plan's formula shorthand had the `+0.5` operand wrong and the
|
|
||||||
implementation follows the decomp's `(load+0.5)*power*8+2`. AP-127 was
|
|
||||||
filed for the then-bounded bonus properties and retired by #268 on
|
|
||||||
2026-07-31.
|
|
||||||
|
|
||||||
Today `PlayerWeenie.SetBurden` has zero callers, `CanJump` is always
|
|
||||||
true, `JumpStaminaCost` is 0, and pushed run/jump skill is
|
|
||||||
attributeBonus + init + ranks only. Retail modulates movement by
|
|
||||||
character state continuously.
|
|
||||||
|
|
||||||
**Retail anchors (named decomp, verified 2026-07-29):**
|
|
||||||
- `ACCWeenieObject::{CanJump 0x0058c400, JumpStaminaCost 0x0058c440, InqJumpVelocity 0x0058c520, InqRunRate 0x0058c560, InqMaxRunRate 0x0058c5a0}` — thin delegations to the qualities DB (pseudo-C ~406512).
|
|
||||||
- `CACQualities::{InqMaxRunRate 0x00591b20, CanJump 0x00591b50, JumpStaminaCost 0x00591b90, InqRunRate 0x00592800, InqJumpVelocity 0x00592980, InqLoad 0x0058f130}` (pseudo-C ~412901–413975, ~409756) — the real load/skill/vitae composition. `InqJumpVelocity` ends in `sqrt(GetJumpHeight(...) * 19.6)` (pc 413975).
|
|
||||||
- `MovementSystem::{GetRunRate 0x006b0950, GetJumpHeight 0x006b09b0, JumpStaminaCost 0x006b0a40}` (pseudo-C ~695958) — GetJumpHeight readable: `LoadMod(load) * (skill/(skill+1300) * 22.2 + 0.05) * power / scaling`, floor 0.35; JumpStaminaCost readable: `ceil(((power + 0.5) * load) * 8 + 2)` on the arg3==0 branch.
|
|
||||||
- `EncumbranceSystem::{EncumbranceCapacity 0x004fcc00, Load 0x004fcc40, LoadMod 0x004fcc70}` (pseudo-C ~256393).
|
|
||||||
- Cross-refs: ACE `MovementSystem`/`EncumbranceSystem` C# ports; holtburger if it models load.
|
|
||||||
|
|
||||||
**Work:**
|
|
||||||
1. Port the full chain into Core (`EncumbranceSystem` + `MovementSystem`
|
|
||||||
statics; `PlayerWeenie` becomes the CACQualities-shaped composition).
|
|
||||||
Where BN x87 mush blocks a branch (GetRunRate body), use Ghidra MCP
|
|
||||||
or the ACE port as the tiebreaker and cite which.
|
|
||||||
2. Determine, from `CACQualities::InqRunRate`'s own body, exactly which
|
|
||||||
skill level retail feeds (base vs enchantment/vitae-adjusted) and
|
|
||||||
port THAT — scoped to the run/jump query path only, reading vitae +
|
|
||||||
relevant skill enchantments from the M3 active-effect state. Do not
|
|
||||||
build a general effective-skill engine.
|
|
||||||
3. Plumb the inputs from Runtime: burden (EncumbranceVal/capacity from
|
|
||||||
PlayerDescription + property updates), current stamina (vitals),
|
|
||||||
vitae. Extend `RuntimeMovementSkillState` (J4.4 seam) so updates flow
|
|
||||||
mid-session, same as run/jump skill today.
|
|
||||||
4. Wire the existing `jump_is_allowed` stamina-refusal branch and the
|
|
||||||
`ReportExhaustion` dual-dispatch gate (R3-W4 seam) to a real
|
|
||||||
consumer, matching retail's refusal/weak-jump behavior.
|
|
||||||
5. Conformance tests: formula tables (golden values incl. 800-skill cap,
|
|
||||||
load-mod knees at 100%/200%, stamina cost ceil), gating tests
|
|
||||||
(no-stamina jump refusal), plumbing tests (burden/stamina/vitae
|
|
||||||
changes move the produced rate). Register: delete TS-5 + AP-25 rows,
|
|
||||||
note the retirement in the same commit.
|
|
||||||
|
|
||||||
### P2 — Response-layer edge family — retires TS-1, TS-4, AP-7; closes #166, #116
|
|
||||||
|
|
||||||
**Status (2026-07-31, FINAL):** TS-1 and AP-7 retired as originally
|
|
||||||
recorded. TS-4's first 2026-07-30 removal was accepted by an incomplete
|
|
||||||
resolver-only fixture, failed the live matrix with a roof wedge/uphill-bounce
|
|
||||||
regression, and was reverted. The 2026-07-31 closure began from a fresh
|
|
||||||
`BSPTREE::find_collisions` read and ports the exact asymmetric Path-6 split:
|
|
||||||
primary/foot hits use `SetCollide` + `LandingZ` + `Adjusted`, while
|
|
||||||
secondary/head hits use `CollisionNormal` + `Collided`; neither writes a
|
|
||||||
sliding normal. Both graph and prepared-flat implementations match that
|
|
||||||
oracle.
|
|
||||||
|
|
||||||
The corrective acceptance no longer calls the old horizontal-input fixture
|
|
||||||
"production-shaped." `Ts4ProductionQuantumConformanceTests` executes the
|
|
||||||
already-airborne, zero-root-motion 30 Hz Core collision tail — acceleration,
|
|
||||||
body integration, transition resolve, exact body/cell commit, then
|
|
||||||
`handle_all_collisions` — and retains its behavior-bearing cell, contact,
|
|
||||||
sliding, stationary-fall, and velocity state for 90 ticks. Graph and flat
|
|
||||||
match by raw bits for vertical/inward/tangential/downhill cases and a genuine
|
|
||||||
positive-Z uphill jump; exact terminal state, non-penetration, no fixed point,
|
|
||||||
and no second launch are pinned. No further product-code correction was
|
|
||||||
needed after that test became faithful, and there is no active AD-56 row. The
|
|
||||||
older resolver-only wedge test remains only as a historical three-second
|
|
||||||
signature control. #116 shape-2 remains closed; shape-1 remains narrowed as
|
|
||||||
recorded in its issue history. AD-55 remains retired by the raw-byte
|
|
||||||
`cos(10°)` proof.
|
|
||||||
|
|
||||||
The collision *response* layer (what happens after a hit): ground
|
|
||||||
friction, cliff edges, downhill landings, near-perpendicular wall
|
|
||||||
slides. One oracle-driven pass; the physics digest's DO-NOT-RETRY table
|
|
||||||
binds every subagent here.
|
|
||||||
|
|
||||||
**Work (research doc FIRST, then port):**
|
|
||||||
1. **AP-7:** decode the state gate on retail's friction block
|
|
||||||
(`calc_friction` region, pseudo-C ~276702-276705) that lets retail
|
|
||||||
use threshold 0.25 without hammering normal locomotion (the reverted
|
|
||||||
L.3c attempt). Ghidra MCP for the x87 branch if BN is garbled.
|
|
||||||
2. **TS-1:** port the `EdgeSlide → PrecipiceSlide / CliffSlide` chain
|
|
||||||
(precipice context, steep-plane bookkeeping) replacing our
|
|
||||||
stop-at-edge.
|
|
||||||
3. **#166:** port the landing "sled" (Sledding state set/clear sites;
|
|
||||||
the sled friction constants already sit in `calc_friction`).
|
|
||||||
4. **TS-4:** remove the Path-6 steep-poly shortcuts and port retail's exact
|
|
||||||
sphere split: primary/foot uses `SetCollide` + `LandingZ` + `Adjusted`;
|
|
||||||
secondary/head uses `CollisionNormal` + `Collided`. Remove every BSP-layer
|
|
||||||
`SetSlidingNormal` write (retail's only in-transition writer is
|
|
||||||
`validate_transition`).
|
|
||||||
5. **#116:** the near-perpendicular lateral-slide loss + first-airborne-
|
|
||||||
frame divergence, driven by the existing tick-22760 replay and D4
|
|
||||||
pins.
|
|
||||||
6. Apparatus: extend the trajectory-replay tests; capture fixtures
|
|
||||||
before changing behavior. Register: delete TS-1/TS-4/AP-7 rows.
|
|
||||||
|
|
||||||
### P3 — Remote-object residuals — retires TS-46, TS-23; closes #165; narrows/retires AD-25
|
|
||||||
|
|
||||||
1. **TS-46:** pass the Setup's verbatim sphere LIST into the transition
|
|
||||||
(`CPhysicsObj::transition 0x00512dc0 → init_sphere`) instead of the
|
|
||||||
two-scalar reconstruction, for local player and remotes; derive
|
|
||||||
remote step-up/step-down from the Setup instead of the pinned 0.4 m.
|
|
||||||
Captured-fixture replays must stay green or be re-baselined with
|
|
||||||
evidence.
|
|
||||||
2. **AD-25:** align the remote post-resolve with the ported
|
|
||||||
`handle_all_collisions` (grounded-bounce rule) as the player half
|
|
||||||
already did in #182.
|
|
||||||
3. **#165:** remotes visibly penetrate walls before stopping — diagnose
|
|
||||||
against the (now Setup-true) sweep; suspect list starts at the
|
|
||||||
catch-up step length vs sweep sub-steps.
|
|
||||||
4. **TS-23:** parse PlayerKillerStatus from PlayerDescription/property
|
|
||||||
updates and plumb PK/PKLite/Impenetrable onto local + remote player
|
|
||||||
movers (`OBJECTINFO::init 0x0050cf30` state bits). Non-PK ACE
|
|
||||||
behavior must be provably unchanged.
|
|
||||||
|
|
||||||
### P4 — World specials — retires AP-71, AP-10
|
|
||||||
|
|
||||||
**Status (2026-07-30): COMPLETE**, including a same-day Opus review
|
|
||||||
fix. AP-71 landed at `d6c3f865` (20 new conformance tests); AP-10 landed at
|
|
||||||
`cc8d57a2` (12 new conformance tests). Complete solution suite at that gate:
|
|
||||||
9,946 total across 9 test projects, 9,941 passed, 5 skipped, 0 failed on a
|
|
||||||
clean run. One run in the same session saw a single unrelated flake
|
|
||||||
(`AcDream.Content.Tests.Vfx.RetailDatLoaderTests
|
|
||||||
.AnimationCache_CoalescesSameDidAndAllowsUnrelatedReadsInParallel`, a
|
|
||||||
parallel-cache-coalescing timing test untouched by either commit) that
|
|
||||||
passed 3/3 in isolation and on the immediate re-run — full-suite parallel
|
|
||||||
contention, not a regression (independently fixed afterward at `dc0468cc`).
|
|
||||||
|
|
||||||
**P4 review verdict: FIX-FIRST (2026-07-30).** `RestrictionObjPrevalenceInspectionTests`
|
|
||||||
(`3b5e0992`) measured the installed cell DAT: 103,766 of 729,888 EnvCells
|
|
||||||
across 1,293 landblocks — the entire housing estate, `restrictionObj` GUIDs
|
|
||||||
`0x70xxxxxx` — carry a baked `RestrictionObj`. AP-71's fail-closed default
|
|
||||||
(landed with `CanMoveInto` deliberately unmodeled, per the original AP-129
|
|
||||||
row) would have locked every apartment/cottage/villa interior for every
|
|
||||||
player, including its own owner — a live regression, not the "inert in dev
|
|
||||||
content" the row assumed. Fixed at `7a0f836a`: `ACCWeenieObject::CanMoveInto`
|
|
||||||
(0x0058da40) and `RestrictionDB::IsAllowedIn` (0x005ae8f0) are now ported
|
|
||||||
verbatim, fed end-to-end from CreateObject's HouseOwner/HouseRestrictions/
|
|
||||||
Monarch PWD-tail fields (previously parsed-and-discarded) plus a new live
|
|
||||||
`House_UpdateRestrictions (0x0248)` parser, and resolved through a new
|
|
||||||
`PhysicsEngine.Objects` property wired to the canonical `ClientObjectTable`
|
|
||||||
in `RuntimeEntityObjectLifetime` (production fix, not just gate logic — an
|
|
||||||
unwired table still fails closed). AP-129 is narrowed (not retired) to the
|
|
||||||
genuine residual: no sequence-based staleness rejection for
|
|
||||||
`House_UpdateRestrictions` (low-probability, self-correcting), and the
|
|
||||||
outdoor `CLandCell` restriction path (a separate DAT structure) remains
|
|
||||||
unported, unaffected by this fix. Gate: `AcDream.Core.Tests` 4,049 passed / 2
|
|
||||||
skipped / 0 failed; `AcDream.Core.Net.Tests` 761 passed / 0 skipped / 0
|
|
||||||
failed; complete solution suite 9,961 total, 9,956 passed, 5 skipped, 0
|
|
||||||
failed.
|
|
||||||
|
|
||||||
1. **AP-71:** port the `CObjCell::check_entry_restrictions` gate at the
|
|
||||||
head of `find_env_collisions` (pc:309576) — barred house cells block
|
|
||||||
at the threshold client-side. Landed: the gate is wired at the top of
|
|
||||||
the indoor branch of `Transition.FindEnvCollisions`; `CellPhysics
|
|
||||||
.RestrictionObj` is fed from the DAT-baked `EnvCell.RestrictionObj`
|
|
||||||
field (§4.3's open question resolved via ACE's DatLoader + an
|
|
||||||
independent `Chorizite.DatReaderWriter` reflection probe — it's a
|
|
||||||
plain per-cell DAT field, not a live wire override) in both the dev
|
|
||||||
and production caching paths, at zero bake-format cost. The mover's
|
|
||||||
`CanBypassMoveRestrictions` (BF_ADMIN & BF_IMMUNE_CELL_RESTRICTIONS)
|
|
||||||
is decoded via the TS-23 PWD-bitfield pipeline. The original landing
|
|
||||||
deliberately left `CanMoveInto` unmodeled (fail-closed default, filed
|
|
||||||
as AP-129) — the P4 review found this fails closed for the ENTIRE
|
|
||||||
housing estate and required the fix-first pass described above.
|
|
||||||
2. **AP-10:** restore retail's 0.1 m water sink-in; while there, verify
|
|
||||||
the water-contact step behavior (`WATER_CONTACT_TS` consumers)
|
|
||||||
against retail and file anything found. Landed: the dry-corner
|
|
||||||
constant is restored (full suite green — the sticky Contact/OnWalkable
|
|
||||||
bit argument held); `WaterContact` is now produced at every
|
|
||||||
`Contact`/`OnWalkable` commit site. No confirmed retail consumer of
|
|
||||||
`WATER_CONTACT_TS` was found this pass; filed as #264 along with two
|
|
||||||
other explicitly-unverified water items (the `ENTIRELY_WATER`
|
|
||||||
ethereal/swim terrain-collision exemption, and jump/swim
|
|
||||||
movement-effects) — none block this port.
|
|
||||||
|
|
||||||
**P4 review addendum (2026-07-30): APPROVED after one FIX-FIRST round.**
|
|
||||||
The initial AP-71 landing failed closed with `CanMoveInto` unmodeled; the
|
|
||||||
prevalence inspection (`3b5e0992`) proved that locks all 103,766 housing
|
|
||||||
EnvCells. `7a0f836a` ports `CanMoveInto`/`IsAllowedIn` verbatim
|
|
||||||
(owner/self/null-db admit; unresolved object blocks), captures the
|
|
||||||
previously-discarded HouseOwner/Monarch PWD fields, parses
|
|
||||||
`House_UpdateRestrictions 0x0248` live, and wires the canonical object
|
|
||||||
table into the gate. AP-129 narrowed to the sequence-byte and outdoor
|
|
||||||
RestrictionTables residuals. Suite 9,956/0/5.
|
|
||||||
|
|
||||||
### P5 — Deferred fidelity — closes #167, #153, #72
|
|
||||||
|
|
||||||
**Status (2026-07-30): item 1 (#167) COMPLETE.** Both blockers resolved
|
|
||||||
without Ghidra/cdb — the two x87-elided constants were byte-decoded
|
|
||||||
straight from the matching binary's raw machine code
|
|
||||||
(`docs/research/2026-07-30-constraint-leash-constants.md`). The leash is
|
|
||||||
now armed at every current acdream inbound-position acceptance seam
|
|
||||||
(`ConstraintDistance`, `LiveEntityNetworkUpdateController`,
|
|
||||||
`PlayerMovementController.SetPosition`/`BlipPosition`), the per-tick
|
|
||||||
`PhysicsBody.IsFullyConstrained` push replaces the always-false stub, and
|
|
||||||
register row TS-35 is deleted. Full Core/Runtime/App suites pass with new
|
|
||||||
conformance tests (leash-armed jump refusal, teleport-vs-blip
|
|
||||||
anchor/teardown, taper reduction, remote-tick push). Items 2 (#153) and 3
|
|
||||||
(#72) remain open.
|
|
||||||
|
|
||||||
1. **#167:** decode the two unknown x87 ConstraintManager constants
|
|
||||||
(Ghidra) and port leash arming.
|
|
||||||
2. **#153:** the far-teleport arrival onto an unstreamed landblock near
|
|
||||||
a 192 m edge — apparatus first (the issue's own trigger table), then
|
|
||||||
the streaming-gap hold shape ALREADY sketched there (freeze the
|
|
||||||
per-tick resolve until the landblock loads — the async equivalent of
|
|
||||||
retail's synchronous load; this is an AD row, not a workaround, and
|
|
||||||
gets filed as one).
|
|
||||||
3. **#72:** close on the R6 evidence (DAT-authored omega ±1.5 rad/s is
|
|
||||||
live; the cdb confirmation ask is obsolete).
|
|
||||||
|
|
||||||
### P6 — #262 login run-on-the-spot (live defect)
|
|
||||||
|
|
||||||
Probe-instrumented fresh-process login repros (`ACDREAM_PROBE_RESOLVE=1`
|
|
||||||
+ net probes) against local ACE; the issue's hypothesis list is the
|
|
||||||
script. Root cause, fix, regression test. No workarounds (no auto-recall,
|
|
||||||
no synthetic position kick). Runs serialized (owns the build tree +
|
|
||||||
client).
|
|
||||||
|
|
||||||
### P7 — Ledger + camera feel
|
|
||||||
|
|
||||||
1. Retire the stale TS-25 row (outbound stance ships via
|
|
||||||
`RawState.CurrentStyle` since #219) and refresh TS-24/TS-35/TS-40
|
|
||||||
classifications.
|
|
||||||
2. #115 camera-drag: investigation-only against `CameraManager`
|
|
||||||
constants (AD-37's vector-nlerp vs retail quaternion-slerp is the
|
|
||||||
prime suspect); fix if a concrete divergence falls out, otherwise
|
|
||||||
re-classify with evidence.
|
|
||||||
3. Reconcile #172/#173/#174/#175/#41 statuses via the final matrix.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation-phase closeout (2026-07-30) — awaiting the matrix
|
|
||||||
|
|
||||||
Every implementation slice is COMPLETE and Opus-reviewed; the campaign now
|
|
||||||
waits on the single user gate below.
|
|
||||||
|
|
||||||
**Register scorecard:** goal-enumerated physics stopgaps at ZERO — TS-1,
|
|
||||||
TS-4 (+ its FlatBspQuery twin), TS-5, TS-23, TS-35, TS-46 retired by
|
|
||||||
ports; TS-25 retired on #219 evidence; TS-24→AD-57, TS-40→AD-58
|
|
||||||
re-argued. AP-7, AP-10, AP-25, AP-71 retired; UN-8 and AD-55 retired by
|
|
||||||
raw-byte proof; AD-25 retired. AP-127 was subsequently retired by #268;
|
|
||||||
the remaining new argued rows are AP-128/129,
|
|
||||||
AD-53/54/55(retired)/56/57/58.
|
|
||||||
|
|
||||||
**Issues:** #72, #153, #167, #255 closed; #116 shape-2 closed /shape-1
|
|
||||||
narrowed to a probable harness artifact (response layer byte-verified);
|
|
||||||
#165 diagnosed to the render-lag candidate (matrix scenario 8a decides);
|
|
||||||
#166 all four composite deviations landed (scenario 5 decides); #262
|
|
||||||
apparatus permanently live + 3/3 clean probe logins (scenario 11
|
|
||||||
decides). Notable finds along the way: ACE's inverted leash-start
|
|
||||||
mapping, ACE's radians/degrees sled-constant bug (cos 10°), the
|
|
||||||
housing-lockout prevalence catch (103,766 restricted cells), and the
|
|
||||||
HouseOwner/Monarch PWD fields that were parsed-and-discarded.
|
|
||||||
|
|
||||||
**Verification:** every slice gated on the complete Release suite; final
|
|
||||||
state 9,977 passed / 0 failed / 4 skipped (the D4 un-skip retired one
|
|
||||||
permanent skip). Suite grew from 8,826 to 9,977 tests over the campaign
|
|
||||||
(+1,151, all conformance/golden/pin coverage).
|
|
||||||
|
|
||||||
## Final batched connected visual matrix (the ONE user gate)
|
|
||||||
|
|
||||||
1. Burden >100%: run slows, jump shrinks; ~200%: barely moves/jumps.
|
|
||||||
2. Repeated jumps drain stamina; low stamina → weak/refused jump;
|
|
||||||
exhaustion behavior matches retail.
|
|
||||||
3. Fresh vitae: movement penalty present.
|
|
||||||
4. Walk off a cliff/roof edge: slides over like retail, no dead stop.
|
|
||||||
5. Downhill jump landing: sled glide + bounce.
|
|
||||||
6. Shallow-angle wall graze: lateral slide preserved.
|
|
||||||
7. Packed crowd: spacing + shuffle-out unchanged (regression).
|
|
||||||
8. Two-client: remote stops at walls without visible penetration;
|
|
||||||
remote ceiling-jump bounces down immediately (#173); Holtburg portal
|
|
||||||
platform step-up (#172); door Use after jumping (#174); closed-door
|
|
||||||
collision matches the visual door (#175); observed-player blips
|
|
||||||
gone (#41).
|
|
||||||
9. Locked/barred house: blocked at the threshold.
|
|
||||||
10. Wading: slight retail sink-in.
|
|
||||||
11. ~20 fresh logins: no run-on-the-spot.
|
|
||||||
12. Regression sweep: walk/run/strafe/turn/jump/stairs/doors/water
|
|
||||||
edges feel unchanged from the accepted R6 baseline.
|
|
||||||
|
|
||||||
## Risk notes
|
|
||||||
|
|
||||||
- P2 and P3 touch the frozen-adjacent transition internals — every
|
|
||||||
subagent prompt must carry the digest's DO-NOT-RETRY table and the
|
|
||||||
no-workarounds rule; 3 failed attempts on any item = stop and build
|
|
||||||
apparatus, per [[feedback_apparatus_for_physics_bugs]].
|
|
||||||
- TS-46 changes the collision capsule of every mover; the captured
|
|
||||||
replay fixtures pin behavior — re-baseline only with a recorded
|
|
||||||
retail argument.
|
|
||||||
- P1's enchantment-aware skill read is the scope-creep risk; it is
|
|
||||||
bounded to the run/jump query path by this plan.
|
|
||||||
|
|
||||||
## Live-gate session 2 (2026-07-30) — speed + bounce family landed
|
|
||||||
|
|
||||||
The matrix's first live rows surfaced three defects; all three are
|
|
||||||
root-caused, retail-ported, and user-accepted in the same session:
|
|
||||||
|
|
||||||
- **#266 CLOSED** — run speed: retail `MovementSystem::GetRunRate`
|
|
||||||
(0x006b0950) treats 800 as an EXACT-EQUALITY sentinel; ACE's `>= 800`
|
|
||||||
reading is a misread of the same x87 mush that our P1 port inherited,
|
|
||||||
flat-lining every maxed character at 4.5 (retail-true ~3.70, +21%,
|
|
||||||
vitae-independent). Byte-decoded, fixed at `61e95916`; the [stat-chain]
|
|
||||||
live capture proved the vitae→skill chain correct end-to-end. Side-by-
|
|
||||||
side pace vs a retail client accepted by the user.
|
|
||||||
- **#265/#166 landing-momentum + bounce family** — two stacked fixes:
|
|
||||||
(1) `c60f6e5d` stopped hand-zeroing grounded residual velocity and
|
|
||||||
wired the never-written `GroundNormal` (roof slides restored);
|
|
||||||
(2) `2d611b2b` replaced the AD-25 landing adaptation with the retail
|
|
||||||
mechanism: `check_contact` (0x0050f5b0) transition seeding, the
|
|
||||||
velocity-free `SetPositionInternal` commit (0x00515330), and the live
|
|
||||||
5%-elasticity landing reflect (DEFAULT_ELASTICITY 0.05 @0x007c6a7c).
|
|
||||||
Downhill bounce chain, flat-ground pop, and clean uphill landings all
|
|
||||||
user-accepted ("almost pass with merits"). Investigation + byte-decode
|
|
||||||
record: `docs/research/2026-07-30-landing-bounce-family.md`.
|
|
||||||
- **#267 shipped** (vitae/buff panel values; attributes vitae-immune).
|
|
||||||
**#268 closed 2026-07-31**: the complete
|
|
||||||
augmentation chain is shared by panel and Runtime movement; AP-127 is
|
|
||||||
retired. Attributes, secondary attributes, and skills use retail's
|
|
||||||
vitae-excluded green/red comparison. The selected-skill footer now renders
|
|
||||||
per-fragment colors through the shared retained text primitive, using the
|
|
||||||
authored 0x1B palette exactly: #7FFFFF vitae, #00FF00 buff, #FF0000
|
|
||||||
debuff. TS-8 is also retired: a real live 0x02C2 payload carries its full
|
|
||||||
StatMod through dispatch and changes the effective skill immediately. The
|
|
||||||
user accepted the live colors, values, footer, and immediate row refresh.
|
|
||||||
- **#269 closed 2026-07-31** — the live 2,184-quantum trace proved the
|
|
||||||
landing reflect and friction math were correct. ACDream omitted retail's
|
|
||||||
`OBJECTINFO::kill_velocity` before restoring a remembered contact plane
|
|
||||||
in `CTransition::validate_transition @ 0x0050AA70`, retaining full
|
|
||||||
downhill velocity while repeatedly re-grounding the mover. The exact
|
|
||||||
non-OK-only restore/kill order and final last-known validity overwrite
|
|
||||||
are now ported, focused/full gates pass, and the user accepted repeated
|
|
||||||
slope jumps. Evidence:
|
|
||||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
|
||||||
- **#271 closed 2026-07-31** — a bounded stair-side
|
|
||||||
trace proved ACDream could bypass retail's current-position edge back-probe
|
|
||||||
by promoting a stale `LastWalkable` tread. That made PrecipiceSlide reverse
|
|
||||||
an uphill tangent and rapidly carry the player down the stairs. The two
|
|
||||||
stale-history substitutions are removed; current-walkable, back-probe, and
|
|
||||||
no-walkable outcomes now follow `CTransition::edge_slide @ 0x0050B3D0`.
|
|
||||||
The exact captured frame is pinned in the existing installed-stair fixture
|
|
||||||
and the complete Release suite passes 10,062 tests / 5 skips. The user
|
|
||||||
accepted repeated uphill runs while pressing into the stair sides. Evidence:
|
|
||||||
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
|
||||||
- **#272 complete and user-accepted 2026-07-31** —
|
|
||||||
`CACQualities::InqLoad` consumes enchantment-adjusted Strength through
|
|
||||||
`InqAttribute`, but Runtime movement and both retained burden displays read
|
|
||||||
raw Strength and did not share the enchantment invalidation edge. They now
|
|
||||||
consume `GetEffectiveAttribute(Strength)` and
|
|
||||||
`Spellbook.EnchantmentsChanged`, so buff, dispel, expiration, and death
|
|
||||||
purge recompute the same burden state immediately. Focused and full
|
|
||||||
Runtime/App tests pass.
|
|
||||||
|
|
||||||
Matrix rows accepted so far: speed parity, roof slide, downhill bounce,
|
|
||||||
flat pop, uphill landing, and #269's slope-stop feel
|
|
||||||
(rows 3/4/5-partial/12-partial). The 2026-07-31 final session accepted
|
|
||||||
burdened movement, exhausted jumping, wall/corner response, crowded-monster
|
|
||||||
movement, two-client remote/door/portal behavior, and shallow water. The user
|
|
||||||
waived the general sweep, deferred restricted-house validation as #274, and
|
|
||||||
retained the separate tight-gap clearance mismatch as #273. Automated
|
|
||||||
scenario 11 remains 20/20 passing. The #269 checkpoint passes 4,107 Core tests / 2 skips and 439
|
|
||||||
Runtime tests / 0 skips; the complete Release suite passes 10,061 tests /
|
|
||||||
5 skips / 0 failures.
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
# Campaign P — final connected visual matrix (runbook)
|
|
||||||
|
|
||||||
**The ONE user stop of the physics parity campaign**
|
|
||||||
(`docs/plans/2026-07-29-physics-parity-campaign.md`). Run after every slice
|
|
||||||
P1–P7 is committed and the full Release suite is green. Each scenario names
|
|
||||||
its setup, the retail-correct outcome, and the ledger items it closes.
|
|
||||||
Scenarios 1–3 need a burden/stamina-capable character on local ACE (use
|
|
||||||
`@god`-style commands sparingly — see `reference_ace_commands` cautions);
|
|
||||||
scenario 8 needs the second client.
|
|
||||||
|
|
||||||
| # | Scenario | Setup | Retail-correct outcome | Closes / confirms |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 1 | Burdened movement | Load the character past 100% burden (pack full of heavy loot), then ~190% | Run speed visibly drops past 100%; near 200% the character can barely move and jumps only inches | TS-5/AP-25 retirement (P1) |
|
|
||||||
| 2 | Exhausted jump | Drain stamina (repeated full-power jumps) to near 0 | Jump cost rises with burden; at insufficient stamina the jump refuses/weakens exactly like retail (no infinite full-height jumps) | TS-5 retirement, ReportExhaustion consumer (P1) |
|
|
||||||
| 3 | Vitae run | Die once, recover the corpse with vitae active | Run/jump measurably below the no-vitae baseline; recovers as vitae expires | AP-25 replacement (P1) |
|
|
||||||
| 4 | Cliff edge | Walk (not jump) off a steep cliff/roof edge (Holtburg bluffs) | The body slides along/over the edge (PrecipiceSlide), never a dead stop pinned at the lip | TS-1 retirement (P2) |
|
|
||||||
| 5 | Downhill sled | Run-jump down a long slope and land | Landing glides ("sleds") with a small bounce, then friction settles it; no instant stick | #166 close, AP-7 gate (P2) |
|
|
||||||
| 6 | Wall graze | Run into a wall at a very shallow angle; also press into a corner and wiggle | Tiny lateral slide is preserved (no dead-stop absorb); corner shuffle-out works | #116 close (P2) |
|
|
||||||
| 7 | Crowd regression | Stand in a packed monster camp; wiggle, jump out | Spacing and shuffle-out unchanged from the accepted #182/#184 baseline | P2/P3 regression guard |
|
|
||||||
| 8 | Two-client remote checks | Second client (retail or acdream) observed from the first | (a) remote stops at walls without sinking in (#165); (b) remote jumping into a dungeon ceiling bounces down immediately (#173); (c) Holtburg town-network portal platform steps up (#172); (d) door Use works after jumping (#174); (e) closed-door collision matches the visual door (#175); (f) no sub-decimeter blips on observed players (#41) | #165 close + the stale #172–#175/#41 gate reconciliation |
|
|
||||||
| 9 | Barred house | Approach a house/cell the character is not a guest of | Blocked at the threshold client-side (no enter-then-server-boot) | AP-71 port (P4) |
|
|
||||||
| 10 | Wading | Walk into shallow water at a shoreline | Feet sink ~0.1 m into the water surface like retail; movement feel unchanged | AP-10 restore (P4) |
|
|
||||||
| 11 | Fresh logins ×20 | 20 fresh-process logins (mix of outdoor/indoor saves) | Zero run-on-the-spot; movement immediate every time | #262 close (P6) |
|
|
||||||
| 12 | General sweep | 10 min free play: walk/run/strafe/turn/jump/stairs/doors/portals incl. one far-town hop | Indistinguishable from the accepted R6 baseline; no new regressions | campaign regression gate; #153 connected confirmation |
|
|
||||||
|
|
||||||
Rubber-band check (rides scenario 12): induce a server correction (e.g.
|
|
||||||
brief packet-loss on Coldeve or a forced position reset) — the leash taper
|
|
||||||
engages and jumping inside a tight leash is refused (0x47), per the #167
|
|
||||||
port (P5).
|
|
||||||
|
|
||||||
**Recording the result:** per scenario PASS/FAIL + a one-line note. Any FAIL
|
|
||||||
reopens its slice; the campaign closes only on a clean sheet. On full pass:
|
|
||||||
close #165/#166/#116/#262 (if not already), mark #172–#175/#41 reconciled
|
|
||||||
with this matrix as the cited gate, update the campaign plan + roadmap +
|
|
||||||
CLAUDE.md current-state, and flip the goal.
|
|
||||||
|
|
||||||
## Automated pillars — BANKED 2026-07-30 (pre-user-session)
|
|
||||||
|
|
||||||
Run on the final tree (post all Campaign P slices), local ACE:
|
|
||||||
|
|
||||||
- **Lifecycle/reconnect gate: PASS** —
|
|
||||||
`logs/connected-world-gate-20260730-130611/report.json` (seven
|
|
||||||
checkpoints, graceful exits). Covers scenario 12's login/portal/
|
|
||||||
teardown backbone.
|
|
||||||
- **Canonical nine-stop soak: PASS** —
|
|
||||||
`logs/connected-r6-soak-20260730-131141.report.json` (production-
|
|
||||||
dispatcher movement input across nine stops). Covers scenario 12's
|
|
||||||
movement-regression backbone.
|
|
||||||
- **Scenario 11 (20 fresh logins): PASS, automated basis** — 20/20
|
|
||||||
fresh-process logins (`artifacts/262-probe/login-{1..20}.log`): every
|
|
||||||
attempt committed the `[snap]` OUTDOOR server-Z branch, held a live
|
|
||||||
resolve stream (~5.3-6.0k lines/40 s), recentered correctly
|
|
||||||
(incidentally onto far-town 0xC95B — the #153 arrival class — all 20
|
|
||||||
times), and closed gracefully. No run-on-the-spot signature. The
|
|
||||||
user's eyes-on confirmation of "movement immediate" on a couple of
|
|
||||||
manual logins completes the scenario.
|
|
||||||
|
|
||||||
Remaining for the user session: scenarios 1-10 (feel/visual) + the
|
|
||||||
manual halves of 11-12.
|
|
||||||
|
|
||||||
## User matrix session 1 results (2026-07-30, partial)
|
|
||||||
|
|
||||||
- Scenario 4/5 (cliff/sled): **FAIL** on the TS-4-removed build — uphill
|
|
||||||
jump-in bounces (non-retail), roof slides lost, occasional edge wedge.
|
|
||||||
→ TS-4 removal REVERTED (`2e27d066`+`a8a7d64b`), row re-opened with
|
|
||||||
live evidence; downhill sled remains #166. Re-test pending.
|
|
||||||
- Speed parity: **FAIL/SUSPECT** — local char faster than retail
|
|
||||||
comparison → #266 (controlled capture needed).
|
|
||||||
- Vitae panel display: **FAIL** (UI, not physics) → #267.
|
|
||||||
- Squeeze-through at the townhall building: **FAIL** → recorded under
|
|
||||||
scenario 12; needs a dedicated capture (suspect list: TS-46 sphere-list
|
|
||||||
threading at a specific site, or the #116 head-sphere change — both
|
|
||||||
P3/final-slice deltas).
|
|
||||||
- Other scenarios: not yet reported.
|
|
||||||
|
|
||||||
## User matrix session 2 results (2026-07-31)
|
|
||||||
|
|
||||||
- Scenario 1 (burdened movement): **PASS**.
|
|
||||||
- Scenario 2 (exhausted jumping): **PASS**.
|
|
||||||
- Scenario 6 (wall graze/corner movement): **PASS**.
|
|
||||||
- Scenario 7 (crowded-monster movement): **PASS**.
|
|
||||||
- Scenario 8 (two-client remote movement, doors, portals, and its collision
|
|
||||||
checks): **PASS**.
|
|
||||||
- Scenario 10 (shallow-water sink-in): **PASS**.
|
|
||||||
- Scenario 9 (restricted/barred house): **DEFERRED BY USER** and retained as
|
|
||||||
issue #274.
|
|
||||||
- Scenario 12 (general movement sweep): **WAIVED BY USER**; the accepted
|
|
||||||
focused rows and existing automated soak are sufficient for this campaign.
|
|
||||||
- A separate live mismatch remains: acdream can squeeze through some tight
|
|
||||||
gaps that block retail. This is outside the accepted wall-graze response
|
|
||||||
check and is retained as issue #273 pending an exact-location capture.
|
|
||||||
|
|
||||||
Together with the previously accepted scenarios 3–5 and the automated
|
|
||||||
20-login scenario 11, the Campaign P matrix is closed with #273 and #274 as
|
|
||||||
explicit carried follow-ups.
|
|
||||||
|
|
@ -1,668 +0,0 @@
|
||||||
# Placement production cutover — campaign plan (2026-08-02)
|
|
||||||
|
|
||||||
> ## ✅ CAMPAIGN LEDGER CLOSED — 2026-08-06, by user direction
|
|
||||||
>
|
|
||||||
> Every slice is landed and dual-reviewed: **C0–C4**, **C5a**, **C5b**
|
|
||||||
> (#275; retired AP-131 + AD-60's legacy half), **#280** (portal destination
|
|
||||||
> prefetch, user-accepted at its connected gate), **#276's remainder**,
|
|
||||||
> **AP-22** and **AD-10** (both retired). **#309** was accepted as a standing
|
|
||||||
> divergence rather than fixed. Final gate: complete Release suite from a
|
|
||||||
> clean build, **11,196 passed / 4 skipped / 0 failed** — campaign net +90
|
|
||||||
> from 11,106.
|
|
||||||
>
|
|
||||||
> **The ledger closes with connected gates outstanding, by user direction —
|
|
||||||
> not because they were discharged.** Only #280's reveal gate was run and
|
|
||||||
> passed. D-1's two reachability scenarios, AP-136's six-step park protocol,
|
|
||||||
> route-7 thickening, the two-client observation, the nine-stop soak and the
|
|
||||||
> lifecycle/reconnect route were **NOT RUN**; the probe family is
|
|
||||||
> **deliberately NOT stripped** for that reason. Anyone citing "the campaign
|
|
||||||
> passed" must cite §2.6 of the closeout alongside it:
|
|
||||||
> [`2026-08-06-c5c-closeout-handoff.md`](../research/2026-08-06-c5c-closeout-handoff.md).
|
|
||||||
>
|
|
||||||
> Follow-ups generated and filed rather than folded in: **#325**, **#330**,
|
|
||||||
> **#331**, **#332**, **AP-149**, **AP-152**, **AD-65**, **AD-66**. Start with
|
|
||||||
> **#331**.
|
|
||||||
|
|
||||||
The final leg of the remaining physics-divergence campaign before AP-22 and
|
|
||||||
AD-10: route graphical AND headless production placement through the
|
|
||||||
residence + continuation-executor owner (`38fd4b8d` / `30012361` /
|
|
||||||
`5db3de3c`), delete the legacy duplicate authorities, and retire AP-1/AD-1
|
|
||||||
behind connected + user-visual gates.
|
|
||||||
|
|
||||||
## Handoff checkpoint — 2026-08-03
|
|
||||||
|
|
||||||
**Status: stabilization checkpoint accepted; campaign closeout is not yet
|
|
||||||
complete.** The C3c production cutover and the O(changed) collision
|
|
||||||
publication checkpoint are now playable after five separately committed
|
|
||||||
root-cause fixes:
|
|
||||||
|
|
||||||
- `01f4791e` stops origin recenter from manufacturing and replaying a second
|
|
||||||
retirement receipt for a pending-only live-projection bucket. Its exact
|
|
||||||
binary passed the complete Release suite, lifecycle route, and canonical
|
|
||||||
nine-stop soak (`connected-r6-soak-20260802-204309`, nine stops, zero
|
|
||||||
failures/wait cues/pending retirements).
|
|
||||||
- `670f307c` keeps remote Create placement, the local-player physics host,
|
|
||||||
targeting, chasing, and attacks in the same world-coordinate frame. The
|
|
||||||
user accepted monster placement/chase/hit behavior and static placement
|
|
||||||
after portals.
|
|
||||||
- `1fc529cd` materializes the canonical minimal static physics host before a
|
|
||||||
distant Use/MoveTo route and reconciles the pre-PartArray startup motion
|
|
||||||
suffix. The user accepted near and distant object use.
|
|
||||||
- `f24532ad` defers one-shot F754/F755 effects until canonical placement has
|
|
||||||
bound presentation, retries projectile/static-animation sidecars on the
|
|
||||||
committed visibility edge, and keeps effect cells synchronized. The user
|
|
||||||
accepted buffs, recalls, arrows, combat spell projectiles, portals, and
|
|
||||||
static animation.
|
|
||||||
- `175ad6b0` sends LoginComplete from the local first-placement terminal edge
|
|
||||||
instead of raw PlayerCreate receipt, so ACE's intentional login Hidden/
|
|
||||||
materialization state cannot race placement. The user accepted the login
|
|
||||||
haze behavior.
|
|
||||||
|
|
||||||
Focused verification after the final fix passed 90 App effect/projectile/
|
|
||||||
static-scheduler tests, two Runtime login tests, the exact live-entity cell
|
|
||||||
tracking regression, all 79 Headless tests, and the Release solution build
|
|
||||||
with zero errors. The long connected soak and complete solution suite have
|
|
||||||
**not** been rerun on the final `175ad6b0` binary. A broader selected fixture
|
|
||||||
run also exposed five `LiveEntityRuntimeTests` failures tied to the still-open
|
|
||||||
placement cutover plus one old remote first-entry fixture that supplies an
|
|
||||||
empty collision source; classify and fix those before claiming C5 closure.
|
|
||||||
**Resolved 2026-08-03 as #281 (DONE):** the "six selected fixture failures"
|
|
||||||
figure was itself a mis-measurement — the measured baseline found **43**
|
|
||||||
(28 App broken by `670f307c`, 2 more by `f24532ad`, 13 Runtime) — repaired
|
|
||||||
without weakening assertions (`6dcb94ac`, `98e9f9e8` and the recent-regression
|
|
||||||
cleanup closed at `2ef02f8c`); every later checkpoint's complete suite ran
|
|
||||||
0-failed.
|
|
||||||
|
|
||||||
Remaining campaign work, in order:
|
|
||||||
|
|
||||||
1. Reproduce and repair the six fixture failures without weakening their
|
|
||||||
assertions or adding compatibility bypasses. **DONE 2026-08-03 (#281 —
|
|
||||||
the real count was 43; see the correction above).**
|
|
||||||
2. Finish C4's routes 2–7 and remove their legacy placement writers; fold in
|
|
||||||
#276 and #277 where their route becomes authoritative. **DONE 2026-08-05
|
|
||||||
except the four owed connected gates (see the C4 slice below). #276 was
|
|
||||||
folded only PARTIALLY — route 5 closed its projectile half; the
|
|
||||||
`SpawnPlacementSettler` settle-cell discard remains OPEN. #277 was NOT
|
|
||||||
folded: no streaming/broadcast radius changed, so its service-window
|
|
||||||
conversion remains a trigger-conditioned carry, not a completed item.**
|
|
||||||
3. Resolve #280 with retail's configured destination-prefetch window so the
|
|
||||||
portal viewport never reveals visibly constructing far terrain.
|
|
||||||
**DONE 2026-08-05 (implementation + suite); the connected/visual gate is
|
|
||||||
batched into C5's matrix. Shape correction: retail has NO separate prefetch
|
|
||||||
window** — it has one landscape square (`LScape::mid_radius`) that is
|
|
||||||
simultaneously the loaded, drawn and blocked-on set, and whose configured
|
|
||||||
value is `Render.LandscapeDrawDistance`. acdream now derives its reveal
|
|
||||||
window from the live streaming radii (`QualitySettings.FarRadius`) and made
|
|
||||||
the render-completeness predicate tier-aware so the outer rings can satisfy
|
|
||||||
it. Contract: [`2026-08-05-280-contract.md`](../research/2026-08-05-280-contract.md).
|
|
||||||
Residual filed as AP-149; the missing user-facing Viewing Distance option is
|
|
||||||
filed separately as #326 and is explicitly NOT part of #280.
|
|
||||||
4. Run C5's complete Release suite, lifecycle/reconnect route, latest-binary
|
|
||||||
nine-stop soak, two-client observation, and the remaining #269 slope-glide
|
|
||||||
visual check. A pass from `01f4791e` is evidence for that fix, not a
|
|
||||||
substitute for the final-binary soak. **Correction 2026-08-05: #269 was
|
|
||||||
already closed and user-accepted 2026-07-31 (before this plan was
|
|
||||||
written); the surviving visual item is #278(b)'s lateral-glide
|
|
||||||
comparison, not #269.**
|
|
||||||
5. Delete the superseded paths, retire AP-1/AD-1/AP-131 and AD-60's legacy
|
|
||||||
half only when the code proves they are gone, then complete AP-22 and
|
|
||||||
AD-10 and close the campaign ledger.
|
|
||||||
**DONE except the ledger close, 2026-08-05/06.** AP-1/AD-1 retired at C5a
|
|
||||||
(`6921a027`); AP-131 and AD-60's legacy half at C5b (`735f0a72`); **AP-22**
|
|
||||||
retired at `bc4679cd` (all three invented-cylinder copies deleted — the row
|
|
||||||
listed one; reachability proved zero over all 5,935 installed Setups by four
|
|
||||||
independent decoders); **AD-10** retired by deletion at `886333a2` (its
|
|
||||||
stated justification was false at HEAD — remotes DO run the sweep, so the
|
|
||||||
projection was an extra non-retail layer, measured bit-identical when
|
|
||||||
removed). Both dual-reviewed, both lenses PASS. Remaining: C5c's gates and
|
|
||||||
the ledger close. Two new divergences were filed out of AD-10's work
|
|
||||||
(AD-65, AD-66) and two issues (#331 uphill-resolve blockage, #332 headless
|
|
||||||
remote dead-reckoning).
|
|
||||||
|
|
||||||
**Inputs (read in order):**
|
|
||||||
1. [`2026-08-02-runtime-continuation-executor-handoff.md`](../research/2026-08-02-runtime-continuation-executor-handoff.md)
|
|
||||||
— the completed dormant mechanism and its cutover notes.
|
|
||||||
2. [`2026-08-02-cutover-route-inventory.md`](../research/2026-08-02-cutover-route-inventory.md)
|
|
||||||
— the full 8-route, both-host call-chain inventory with exact file:line
|
|
||||||
for every duplicate authority to remove. THE map for all slices below.
|
|
||||||
3. [`2026-07-31-remaining-physics-campaign-handoff.md`](../research/2026-07-31-remaining-physics-campaign-handoff.md)
|
|
||||||
— the original per-route requirements and prerequisite definitions.
|
|
||||||
|
|
||||||
**Standing discipline per slice:** pinned contract → single implementer →
|
|
||||||
independent retail-conformance + architecture/adversarial reviews (both must
|
|
||||||
PASS on the final diff) → focused + complete Runtime + Release build +
|
|
||||||
complete solution gates → bisectable behavior commit (register rows in the
|
|
||||||
same commit) → docs/handoff commit. No workarounds; no fused slices.
|
|
||||||
|
|
||||||
## Confirmed pre-cutover gaps (from the inventory)
|
|
||||||
|
|
||||||
- The executor publishes only generic entity deltas; nothing bridges its
|
|
||||||
completion to `RuntimePlacementProjectionChannel`, so no host can learn
|
|
||||||
"my initial placement committed" through the built observer seam.
|
|
||||||
- No atomic controller/body publication owner exists (prerequisite C);
|
|
||||||
App and headless hand-write divergent `PlayerMovementController`
|
|
||||||
construction, and `SubmitPreparedPlacement` requires a canonical
|
|
||||||
`PhysicsBody` that nothing currently publishes atomically.
|
|
||||||
- The dormant placement path's 1,880 B/operation (2,048 cap) allocation
|
|
||||||
remains the activation blocker for frame-frequency routes.
|
|
||||||
- `Execute`'s live inputs (`UsePositionFromServer`, `PlayerDistance`) are
|
|
||||||
computed by no host; they must derive from Runtime's own character-option
|
|
||||||
and local-player owners.
|
|
||||||
- `RuntimePortalPlacementAuthority` has zero producing call sites; the
|
|
||||||
adapter from `RuntimeWorldTransitState` does not exist.
|
|
||||||
**Corrected 2026-08-04 (C4 route 3 closure,
|
|
||||||
`docs/research/2026-08-04-c4-route-3-contract.md`), itself corrected
|
|
||||||
2026-08-05 (A10 architecture review — the first correction asserted a
|
|
||||||
false fact of its own), and rewritten 2026-08-05 (N5 retail-review
|
|
||||||
round-3 fix — the prior wording of this correction contradicted
|
|
||||||
itself).** The original bullet conflated two separate claims into one
|
|
||||||
sentence, and only one of them was true. What pre-dated route 3 and WAS
|
|
||||||
accurate: the `RuntimePortalPlacementAuthority` type existed (referenced
|
|
||||||
by route 2's `Pending.Portal` field, always `Present: false`), its
|
|
||||||
`IsValid` check existed, and the sinks' portal-authority gates plus
|
|
||||||
`BeginAcceptedPlacementCore`'s gate already read it. What was NOT
|
|
||||||
accurate, and is what "zero producing call sites; the adapter does not
|
|
||||||
exist" actually described: the PRODUCER half — nothing built a
|
|
||||||
`Present: true` authority and called the consumer arm
|
|
||||||
(`RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival`/
|
|
||||||
`SubmitAndResolvePortal`/`ClassifyPortalArrival`) — that consumer arm
|
|
||||||
ALSO did not exist before route 3. Route 3 added the producer and the
|
|
||||||
consumer together, in the same slice: the producer is
|
|
||||||
`LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement` (now
|
|
||||||
`TryAdvancePortalCommit`/`TryExecuteCanonicalPortalPlacementCore`, per the
|
|
||||||
2026-08-05 A1 review fix), which builds the authority from
|
|
||||||
`WorldRevealCoordinator`/`RuntimeWorldTransitState` facts and calls
|
|
||||||
`TryExecuteAcceptedPortalArrival`; the identical Runtime entry point is
|
|
||||||
shared by the headless host. So: the type/`IsValid`/consumer-gate facts
|
|
||||||
pre-dated route 3 and were true before it; the arm (both the producer
|
|
||||||
that builds a live authority and the consumer that reads one) did not
|
|
||||||
exist before route 3 and is what the original bullet's "zero producing
|
|
||||||
call sites" language was pointing at.
|
|
||||||
- The exact-Setup mover chain (`PrepareMover` /
|
|
||||||
`RuntimeSetPositionMoverPreparer.TryBuild` /
|
|
||||||
`IPreparedCollisionSource.ReadSetupCollision`) exists piecewise, unwired.
|
|
||||||
- **Corrected 2026-08-04 (C4 route 6 closure,
|
|
||||||
`docs/research/2026-08-04-c4-route-6-contract.md`): all three clauses
|
|
||||||
above were stale.** Route-6 split-recovery does NOT need an effect-replay
|
|
||||||
suppression signal — that premise was unsubstantiated; acdream's only
|
|
||||||
create-time effect replay is the F754/F755 queue drain keyed by server
|
|
||||||
GUID, and the one plausible mechanism (a cloned `DefaultScriptType`
|
|
||||||
surviving `BuildSpawn`) never fires at create in either client
|
|
||||||
(`CPhysicsObj::play_default_script @0x005132B0`/`@0x00513300` has exactly
|
|
||||||
two callers, both animation hooks, verified against
|
|
||||||
`acclient_2013_pseudo_c.txt`). Route-7's `TryCommitParent`/
|
|
||||||
`CommitWithdrawal` cancellation-symmetry fixes and host-visible
|
|
||||||
cancellation receipts were BOTH closed at C0 (see the C0 slice below).
|
|
||||||
What actually remained for route 7: the child's canonical cell had two
|
|
||||||
writers (Runtime committed it cell-less unconditionally in
|
|
||||||
`CommitAcceptedParentCellless`, while `EquippedChildRenderController
|
|
||||||
.TickChild` re-celled it from a per-frame render tick), and headless had no
|
|
||||||
`EquippedChildRenderController` at all, so every headless parented child
|
|
||||||
stayed cell-less forever — the same defect seen from two sides, not two
|
|
||||||
separate gaps. **Closed 2026-08-04
|
|
||||||
(`docs/research/2026-08-04-c4-route-7-contract.md`).** Runtime is now the
|
|
||||||
sole canonical writer: `CommitAcceptedParentCellless` completes retail
|
|
||||||
`set_parent`'s attach-time re-cell (D1), and every canonical cell write
|
|
||||||
funnels through one directory chokepoint that recursively propagates to
|
|
||||||
committed children on every parent cell crossing (D2 —
|
|
||||||
`docs/research/2026-08-04-retail-parent-cell-propagation.md`), not only at
|
|
||||||
attach. `TickChild` is demoted to a presentation-only draw-bucket move
|
|
||||||
(D4); the headless host gained its own parent-realize drive running the
|
|
||||||
same commit pair the graphical host does (D5,
|
|
||||||
`RuntimeLiveEntitySessionController.OnParentUpdated`). The direct headless
|
|
||||||
regression test (a bot with an equipped item shows the child's canonical
|
|
||||||
`FullCellId` equal to the parent's) now passes.
|
|
||||||
|
|
||||||
## Slices
|
|
||||||
|
|
||||||
- **C0 — Runtime bridge + live inputs — COMPLETE at `67f63e85`
|
|
||||||
(2026-08-02, dual reviews PASS).** The executor publishes an
|
|
||||||
acknowledge-only `ExecutorCompleted` receipt through the one placement
|
|
||||||
stream (registered before dispatch; correlation reaped on
|
|
||||||
acknowledgement/discard/clear; `PendingCompletionReceiptCount` in
|
|
||||||
`IsConverged`); all three production sinks acknowledge-and-ignore the
|
|
||||||
kind via early returns proven behavior-preserving for every other kind
|
|
||||||
(sanctioned seam completion — provably inert, no production publisher);
|
|
||||||
`UsePositionFromServer` derives retail-exactly from
|
|
||||||
`RuntimeCharacterState.AutonomyLevel != 2` and `PlayerDistance` from the
|
|
||||||
live movement controller with null-safe fallback to the caller struct;
|
|
||||||
`TryPrepareAndSubmitAuthoredPlacement` chains the prepared-collision
|
|
||||||
Setup read through `PrepareMover` to submission with zero validation
|
|
||||||
changes; `TryCommitParent`/`CommitWithdrawal` gained the sibling
|
|
||||||
cancellation flow (the `LeaveWorld` omission in `TryCommitParent` is
|
|
||||||
retail-REQUIRED per `set_parent` 0x00515A90:283832-283833's single gated
|
|
||||||
`leave_world`). Not fully dormant by design: the two cancellation fixes
|
|
||||||
change live Runtime paths production already calls; everything else has
|
|
||||||
no production caller.
|
|
||||||
**C3 prerequisites recorded from C0's reviews:** (a) the completion
|
|
||||||
receipt/trace surface is internal-only — C3 must define the public host
|
|
||||||
consumption shape when it wires the hosts; (b) `PlayerDistance` is
|
|
||||||
resolved once per `Execute` entry, not per continuation — a multi-Position
|
|
||||||
FIFO classifies later entries against entry-time distance (documented
|
|
||||||
deferral; refine at C3/C4 if the connected gates show it matters);
|
|
||||||
(c) any future host exposure of `TrySetAutonomyLevel` must carry retail's
|
|
||||||
`SendAutonomyLevelEvent` (699550).
|
|
||||||
- **C1 — atomic controller/body publication — SATISFIED BY EXISTING
|
|
||||||
MECHANISM (research finding 2026-08-02, plan amended same session).**
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState` (1,033 lines) plus the
|
|
||||||
~15-method dormant local-activation family on `RuntimeSetPositionState`
|
|
||||||
already implement the full sanctioned option-2 transaction:
|
|
||||||
off-canonical preparation against a scratch quantum clock and a sealed
|
|
||||||
candidate controller, one validated atomic Commit, and a staged
|
|
||||||
Evaluate/Commit/FinalizeActivation chain re-validated against
|
|
||||||
PhysicsOwnershipEpoch/ObjectClockEpoch/ControllerOwnershipEpoch/session
|
|
||||||
identity at every entry — with zero production callers. See
|
|
||||||
[`2026-08-02-canonical-body-writer-map.md`](../research/2026-08-02-canonical-body-writer-map.md)
|
|
||||||
(6 canonical body writers; the two host escape hatches; both hosts'
|
|
||||||
divergences). The remaining work — routing both hosts' local-player
|
|
||||||
construction through the publication lifecycle, sealing the public
|
|
||||||
`RuntimeLocalPlayerMovementState.Controller` setter, retiring App's
|
|
||||||
direct object-clock bypasses, and containing headless's uncaught
|
|
||||||
prepared-collision `InvalidDataException` — IS the C3 route-1 flip and
|
|
||||||
moves there. No separate C1 commit.
|
|
||||||
- **C2 — placement allocation budget — COMPLETE at `63c601ff`
|
|
||||||
(2026-08-02, dual reviews PASS after two fix rounds).** 2,032 → 944
|
|
||||||
B/op via pooled operation envelopes (bounded, reset-at-rent, double-
|
|
||||||
retire guarded, reset/dispose-cleared, ledger-visible), a cached
|
|
||||||
collision-callback delegate over an explicit context stack, and a
|
|
||||||
non-boxing pending-head read; gate tightened to 1,536. The pooling
|
|
||||||
forced a class-wide staleness rework: captured-token-vs-fresh-lookup at
|
|
||||||
every reentrancy-spanning frame (26-site audit), hoisted stack locals
|
|
||||||
for retail's handle_all_collisions bits, token-gated bookkeeping
|
|
||||||
writes, and a deliberately identity-agnostic settle path (retail's
|
|
||||||
SetPositionInternal completes unconditionally even for displaced
|
|
||||||
operations).
|
|
||||||
**Residual floor (documented at the gate, decision deferred to the C3
|
|
||||||
activation gate where the user is in the loop):** ~520 B/op inside
|
|
||||||
Core's `PhysicsEngine.SetPosition` (transition init / query-footprint
|
|
||||||
materialization — a potential C2b if C3's connected profile shows it
|
|
||||||
matters) and ~208 B/op of sorted-tree node per pending receipt.
|
|
||||||
**Maintenance notes from review (no action):** the no-reentrancy
|
|
||||||
proofs on the 15 surviving reference-based currency checks are
|
|
||||||
comment-enforced; `IsCurrent(Operation)` remains available and a new
|
|
||||||
reentrancy-spanning call site would silently inherit the tautology —
|
|
||||||
its doc comment warns.
|
|
||||||
- **C3 — spawn-frequency cutover: routes 1 + 8 — DECOMPOSED 2026-08-02
|
|
||||||
after the first implementation pass stopped with findings.** C3-1 (the
|
|
||||||
public executor-completion surface via
|
|
||||||
`RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion`)
|
|
||||||
landed separately. Two structural gaps halted the flip, both real and
|
|
||||||
neither in the planning docs:
|
|
||||||
**(B)** the local player's residence lease opens its SetPosition
|
|
||||||
operation at Create time, but `SubmitPreparedPlacementCore` requires a
|
|
||||||
pre-existing canonical body that only the zero-caller publication chain
|
|
||||||
can attach — first-entry needs an explicit resumable sequence
|
|
||||||
(begin-placement → publication Prepare/Commit attaches the body →
|
|
||||||
authored-mover submit → Place receipt → Execute), which matches the
|
|
||||||
campaign handoff's route-1 required order but exists nowhere as a
|
|
||||||
driveable state machine;
|
|
||||||
**(C)** ordinary remote-creature Creates classify to `SetPosition` but
|
|
||||||
have NO production body-construction path at Create time (bodies arrive
|
|
||||||
with first motion today; retail constructs physics at CreateObject via
|
|
||||||
`ACCObjectMaint::CreateObject`/`set_description`, which our retail
|
|
||||||
notes fully document — the defaults come from the wire PhysicsDesc,
|
|
||||||
not invention).
|
|
||||||
Sub-slices, each with the standing contract/dual-review/gate
|
|
||||||
discipline:
|
|
||||||
- **C3a — Runtime first-entry sequencing — COMPLETE at `960373df`
|
|
||||||
(2026-08-02, dual reviews PASS).** `RuntimeLocalPlayerFirstEntryState`:
|
|
||||||
five stages (mover-prep → publication Prepare/Commit → activation →
|
|
||||||
acknowledgement → Execute) in retail's own order — mover shapes
|
|
||||||
BEFORE placement, matching makeObject/set_description preceding
|
|
||||||
enter_world; the original contract prose had it backwards and the
|
|
||||||
tested preconditions forced the faithful order. Acknowledge-stage
|
|
||||||
authority discrimination, automatic convergence through the (now
|
|
||||||
multicast, snapshot-iterated) retirement fan-out, ownership-ledger
|
|
||||||
fold, transactional late-bind Publication seam. Dormant: C3c's first
|
|
||||||
act is the GameRuntime binding + production Advance drive.
|
|
||||||
**Carried findings for C3c:** the controller is live from the
|
|
||||||
activation commit onward (abandonment leaves it to ordinary entity
|
|
||||||
teardown — retail has no entry-flow rollback); EvaluateActivation's
|
|
||||||
post-commit DeferredCell overload is encapsulated behind Advance.
|
|
||||||
- **C3b — remote body construction at Create — COMPLETE at `0934a121`
|
|
||||||
(2026-08-02, dual reviews PASS).** `RuntimeRemoteBodyDescription` +
|
|
||||||
`RuntimeRemoteFirstEntryState`: the full `set_description` order with
|
|
||||||
the byte-certain gates (friction [0,1] inclusive, NaN sanctioned-skip;
|
|
||||||
elasticity clamp with retail's unordered-to-zero; translucency
|
|
||||||
!= 0.0f), the movement-branch discriminator on retail's
|
|
||||||
`movement_buffer != 0` (empty-buffer → placement branch, no autonomy),
|
|
||||||
motion-table zero-id pass, ctor-defaults for absent wire fields, and
|
|
||||||
never-clobber coexistence with the build-at-first-motion production
|
|
||||||
path. The acknowledge discriminator is one shared body
|
|
||||||
(`RuntimeFirstEntryAcknowledgement`) for both conductors. Dormant.
|
|
||||||
- **C3c — the host flips (production) — COMPLETE at `529e0e9d`
|
|
||||||
(2026-08-02, dual Opus reviews: initial FAIL 2+2 MAJOR → R1 fix
|
|
||||||
round → delta PASS both).** Both hosts register initial Creates
|
|
||||||
through residence + conductors via the shared
|
|
||||||
`RuntimeFirstEntryDriveController`; Controller setter sealed;
|
|
||||||
rebucketing presentation-only strictly while the residence is
|
|
||||||
ACTIVE (post-residence entities take the full legacy path including
|
|
||||||
the `prepare_to_enter_world` clock edges); content-less headless
|
|
||||||
keeps pre-flip direct registration. Five fix slices landed inside
|
|
||||||
the cutover, each connected-gated: F1 (Runtime ownership seam for
|
|
||||||
movement stats/server physics — the post-logout retired-controller
|
|
||||||
crash), F2 (the login activation wedge: admission-prefix gate
|
|
||||||
factored from the seal, rearm generation identity, auto-entry
|
|
||||||
requires the published controller), F3 (landblock-prefix 0-sentinel
|
|
||||||
→ explicit absent-id; corner landblocks legal), F4 (diagnosis only:
|
|
||||||
the nine-stop soak's convergence failure is pre-existing `6b28ff99`
|
|
||||||
whole-world collision-clone throughput — its fix is the next slice
|
|
||||||
before C5), F5 (local-player first-entry ground contact via the
|
|
||||||
shared `SpawnPlacementSettler` at `FinalizeActivation`; the
|
|
||||||
standing-cast airborne rejections are gone; register AD-61). R1
|
|
||||||
additionally armed the login constraint leash at the committed
|
|
||||||
placement (`HandleReceivedPosition` 0x00453FD0 analog) and
|
|
||||||
refreshed AD-42. Final gates: complete solution 10,816/0/4 skips;
|
|
||||||
lifecycle/reconnect gate PASS (`connected-world-gate-20260802-
|
|
||||||
175401`). Closeout:
|
|
||||||
[`2026-08-02-c3c-cutover-closeout.md`](../research/2026-08-02-c3c-cutover-closeout.md).
|
|
||||||
**Carried to C4/C5:** route-1 far-Create service-window conversion
|
|
||||||
if either streaming/broadcast radius changes (#277); the
|
|
||||||
window-departure park narrowing; `NotifyRetirement`-on-active-entry
|
|
||||||
subscriber invariant; the reachable equip-mid-conductor fail-fast;
|
|
||||||
settle-CellId discard (#276-adjacent, see ISSUES).
|
|
||||||
- **C4 — remaining routes: 2 (ForcePosition), 3 (portal, with the
|
|
||||||
`RuntimeWorldTransitState` → `RuntimePortalPlacementAuthority` adapter),
|
|
||||||
4 (remote Create/Position; delete `RemoteTeleportController`/`Placement`
|
|
||||||
and the inline MoveOrTeleport duplicate), 5 (projectile authoritative),
|
|
||||||
6 (drops + split-recovery marking), 7 (residual pickup/parent/delete
|
|
||||||
polish). — route 2 COMPLETE AND USER-ACCEPTED 2026-08-03 (`9966b531`);
|
|
||||||
routes 3/4/5/6/7 remain OPEN.**
|
|
||||||
|
|
||||||
**C4 IMPLEMENTATION COMPLETE 2026-08-05.** Every route now places through
|
|
||||||
the canonical Runtime owner; the campaign's remaining C4 debt is exactly
|
|
||||||
the four owed connected gates listed at the end of this bullet. Per-route
|
|
||||||
record (each with contract + independent dual reviews per the standing
|
|
||||||
discipline; suite counts measured, never inherited — final complete
|
|
||||||
Release suite **11,090 passed / 4 skipped / 0 failed** at `e0f96a55`):
|
|
||||||
- **4a LANDED `44830a0e`; 4b-1 LANDED `2e8e09ac` (dormant
|
|
||||||
infrastructure); 4b-2 LANDED `7f1c1f5a`** (recorded in the sub-bullets
|
|
||||||
below with its four fix rounds and user-passed far-snap gate).
|
|
||||||
- **4b-3 LANDED `6dc7ba51` (2026-08-04)** — remote teleport + cell-less
|
|
||||||
through the canonical placement; `RemoteTeleportController` (605 lines),
|
|
||||||
`RemoteTeleportPlacement` (85), and ~1,709 test lines deleted. Dual
|
|
||||||
round 1 FAIL/FAIL → round 2 delta PASS/PASS; three NPC-arm MAJORs
|
|
||||||
closed. **Connected gate PASSED-partial (`21cd6e9b`)**: 16
|
|
||||||
`[remote-teleport]` probe lines over 7 creatures, all
|
|
||||||
`cause=teleport-ts` — `cause=cellless` was never observed and remains
|
|
||||||
test-covered only (owed gate 4 below). Docs at `8c269ad1`; findings
|
|
||||||
chain in `2026-08-04-c4-route-4b-3-*.md`.
|
|
||||||
- **Route 5 LANDED `36255af0` (2026-08-04)** — projectile authoritative
|
|
||||||
placement (#276 partial), preceded by a mandatory byte-decode gate
|
|
||||||
(`MoveOrTeleport` @0x00516330 never reads its velocity argument, which
|
|
||||||
also spawned #317). Three dual review rounds closing 8 MAJORs; round 3
|
|
||||||
retail PASS with the AP-141 risk-column retraction (C1), architecture
|
|
||||||
FAIL on a coverage-only C1 closed in-commit with two sabotage-verified
|
|
||||||
retry-arm tests. **NO connected gate exists for this route, by
|
|
||||||
design** — ACE never sends a missile UpdatePosition
|
|
||||||
(`WorldObject_Tick.cs:333-334`); every proof is deterministic-test-gated
|
|
||||||
and recorded as such. Interim landings alongside: the OnPosition
|
|
||||||
dual-tail collapse (`edc911b0`, whose scoping found and filed #316),
|
|
||||||
#315 closed (`aaf0811f`), #314 closed (`daef7c98`).
|
|
||||||
- **Route 6 CLOSED `1b484937` (2026-08-04) with ZERO production lines** —
|
|
||||||
C3c had already flipped both drop flavours onto the canonical create
|
|
||||||
transaction; the landing is 7 sabotage-verified coverage tests, the
|
|
||||||
retail split-marking record (#313 filed for the `DeclareValid`
|
|
||||||
selection transfer), and the correction of this plan's own false
|
|
||||||
effect-replay premise (see the corrected gap list above). Its coverage
|
|
||||||
tests immediately found #314 (split recovery threw on retained
|
|
||||||
timestamps), fixed in its own commit `daef7c98`. **Connected gate owed**
|
|
||||||
(drops recipe — owed gate 1 below).
|
|
||||||
- **Route 7 LANDED `cd3129e9` (2026-08-04)** — child cell propagation
|
|
||||||
moved from a render tick into Runtime: retail `set_parent`'s attach-time
|
|
||||||
re-cell completed in `CommitAcceptedParentCellless`, the recursive
|
|
||||||
parent-cell-crossing propagation at the one directory funnel (iterative
|
|
||||||
worklist — the initial depth-64 cap was deleted after both round-2
|
|
||||||
reviews independently found its truncation residue was the #184 shape),
|
|
||||||
`TickChild` demoted to presentation-only, the headless parent-realize
|
|
||||||
drive added (its direct regression test failed before this work), and
|
|
||||||
the dead `ClassifyLeaveWorld` family deleted. Dual round 1 FAIL/FAIL →
|
|
||||||
round 2 delta PASS/PASS plus a coordinator-required third pass; 5
|
|
||||||
MAJORs. AP-142/AP-143 filed. **Connected gate owed** (equip/carry with
|
|
||||||
`cause=propagate` probe evidence — owed gate 2 below). Route 7 also
|
|
||||||
INVALIDATED 4b-3's recorded cell-less live recipe (contract §11; the
|
|
||||||
supersession note is appended to the 4b-3 contract).
|
|
||||||
- **Route 3 LANDED `e0f96a55` (2026-08-05)** — the LAST route: the first
|
|
||||||
production `RuntimePortalPlacementAuthority` producer, the portal arm on
|
|
||||||
route 2's drive controller, `CommitCanonicalTeleportFrame` with the
|
|
||||||
`PlayerTeleported` port (autorun cancel + one movement event), and both
|
|
||||||
duplicate authorities deleted (`LocalPlayerTeleportPlacement.Place`,
|
|
||||||
`ResynchronizeLocalPlayerForPortalArrival` — AD-42's row deleted with
|
|
||||||
them). Contract at `19ebf043`; scoping/propagation research at
|
|
||||||
`ca96ea5e`. Dual round 1 FAIL/FAIL → dual round 2 FAIL/FAIL (near miss)
|
|
||||||
→ round-3 fix pass accepted per both round-2 reviews' explicit pass
|
|
||||||
conditions; the round-3 record is the commit message plus #318 and
|
|
||||||
AP-144/AP-145 (no standalone round-3 review doc). The fix pass's
|
|
||||||
refusal to accept 7 skipped tests uncovered a real production bug (the
|
|
||||||
canonical portal arm was 100% dead code — the accepted-destination slot
|
|
||||||
it re-read at Place time was already consumed at Aim time). **Connected
|
|
||||||
gate owed** (portal/recall with `[local-tp]` probe evidence — owed gate
|
|
||||||
3 below — and explicitly NOT scored as covering #318).
|
|
||||||
- **The four owed connected gates**, with recipes and pass criteria in
|
|
||||||
[`2026-08-05-c4-closeout-handoff.md`](../research/2026-08-05-c4-closeout-handoff.md):
|
|
||||||
(1) route 6 drops; (2) route 7 equip/carry across landblock boundaries,
|
|
||||||
counted only with `[child-cell]` `cause=propagate` lines; (3) route 3
|
|
||||||
portal/recall, counted only with `[local-tp]` lines, not scored against
|
|
||||||
#318; (4) 4b-3's `cause=cellless` case, whose recorded trigger route 7
|
|
||||||
invalidated — the replacement provocation is UNESTABLISHED and needs its
|
|
||||||
own investigation. None has been run.
|
|
||||||
**Route 4 SPLIT into 4a and 4b (user-directed 2026-08-03).** Scoping
|
|
||||||
([`2026-08-03-c4-route-4-scoping.md`](../research/2026-08-03-c4-route-4-scoping.md))
|
|
||||||
put whole-route 4 at 1,500-2,500 production lines against a stated ~400
|
|
||||||
budget, so it is split to keep each landing reviewable:
|
|
||||||
- **4a — the steady state.** The classifier's `Interpolate` (contact,
|
|
||||||
`PlayerDistance < 96 m`) and `NoPositionOperation` (no contact) branches.
|
|
||||||
NEITHER runs a `SetPosition`, so 4a has no deferred-cell park, no
|
|
||||||
service-window work, and no placement-allocation exposure. Fixes two of the
|
|
||||||
three unfiled divergences (the NPC airborne hard-snap that ignores the wire
|
|
||||||
`IsGrounded` bit; `ConstrainTo` armed before the operation instead of
|
|
||||||
after). Highest visible value — this is what makes creatures move smoothly.
|
|
||||||
- **4b — the edges.** `SetPosition` / `SetPositionSimple`: teleport, far-snap
|
|
||||||
(>= 96 m), and cell-less first placement. This is where the parks, the
|
|
||||||
Position-time service-window guard, #277's broken bound, N3 (headless never
|
|
||||||
calls `RetryPending`), and the third divergence (`ConstrainTo` never armed
|
|
||||||
on the remote teleport branch) all live.
|
|
||||||
|
|
||||||
**4b also inherits 4a's ownership remainder — scheduled here, not implied
|
|
||||||
by code comments.** Two independent reviews flagged that 4a satisfies
|
|
||||||
contract items 1 and 2 only partially, and the plan must carry that rather
|
|
||||||
than leaving it in `// 4b deletes this fallback` comments:
|
|
||||||
- Runtime owns the classification, the request construction (one shared
|
|
||||||
builder, `RuntimeAcceptedPositionRouteRequests`), the near-InterpolateTo
|
|
||||||
decision with AP-87, and the post-operation `ConstrainTo`. **App still
|
|
||||||
owns** branch selection, the airborne early return, the
|
|
||||||
`RemoteMotion.CellId` write, the `WorldEntity` pose write, and the
|
|
||||||
collision-shadow publish — all in `LiveEntityNetworkUpdateController`.
|
|
||||||
- Item 2 ("both hosts drive the identical Runtime entry point") is
|
|
||||||
satisfied only VACUOUSLY: `RuntimeLiveEntitySessionController` returns
|
|
||||||
early for remotes, so no no-window host exercises this path at all and
|
|
||||||
nothing can diverge yet. That stops being true the moment a headless
|
|
||||||
host needs remote motion.
|
|
||||||
- Every legacy fallback 4a deliberately left in place is 4b's to delete:
|
|
||||||
the pre-operation unconditional `ConstrainTo`, the player arm's
|
|
||||||
`!update.IsGrounded` no-op, the player and NPC legacy near/far routing
|
|
||||||
(each still carrying its own duplicate `96f` / `4f` constants), and the
|
|
||||||
airborne-precedence carve-out
|
|
||||||
(`LiveEntityNetworkUpdateController.ApplyRemoteContactRouting`) that
|
|
||||||
keeps a landing body snapping. Retiring the last one is a real behaviour
|
|
||||||
decision — retail makes no player/NPC distinction there — and needs its
|
|
||||||
own live evidence, not a silent convergence.
|
|
||||||
- Register row **AP-135** (the airborne no-op's retained acdream
|
|
||||||
bookkeeping: the server cell id for the free-fall sweep gate, and the
|
|
||||||
last-server-position sample) — **CORRECTED 2026-08-04: this row does NOT
|
|
||||||
retire with 4b.** Its own stated condition is retirement together with the
|
|
||||||
free-fall sweep gate (`RuntimeRemotePhysicsUpdater.cs:342`), which 4b does
|
|
||||||
not touch, and its sites are the airborne no-op branches — 4a-owned
|
|
||||||
dispositions, not 4b's far-snap/teleport/cell-less. The trap is that those
|
|
||||||
two writes sit physically inside `OnPosition`, which 4b rewrites heavily,
|
|
||||||
so an implementer will assume they go. They stay. See
|
|
||||||
[`2026-08-04-c4-route-4b-scoping-and-split.md`](../research/2026-08-04-c4-route-4b-scoping-and-split.md).
|
|
||||||
- **4b is itself split into 4b-1 / 4b-2 / 4b-3** (2026-08-04). Scoping put
|
|
||||||
4b at 1,300-2,200 production lines — 4-6x route 4a — plus ~2,500-3,500
|
|
||||||
lines of test work. 4b-1 is infrastructure with no remote behaviour change
|
|
||||||
(the per-entity placement owner, the service-window guard, the
|
|
||||||
refuse-rather-than-park policy, N3's headless `RetryPending` pump); 4b-2 is
|
|
||||||
the far branch alone; 4b-3 is teleport/cell-less and the ~739-line class
|
|
||||||
deletions. 4b-1 stays a separate landing regardless: it is where the
|
|
||||||
park-withdraws-the-entity failure mode is decided, and it must not be
|
|
||||||
reviewed alongside a large deletion.
|
|
||||||
- **4b-2 LANDED at `7f1c1f5a` (2026-08-04); far-snap connected gate
|
|
||||||
USER-PASSED same day.** Four fix rounds, eight Opus reviews; the slice was
|
|
||||||
fully green at 10,990 / 10,997 / 11,004 while containing real defects
|
|
||||||
(a frozen remote pinned as correct by its own test; a fallback that
|
|
||||||
over-wrote on the exact retail paths that decline to store; a park guard
|
|
||||||
incomplete on two independent axes). Final suite 11,009 / 4 / 0 against a
|
|
||||||
**measured** 10,968 baseline — the 10,973 figure used earlier was wrong.
|
|
||||||
Its real yield was a defect under routes 1 and 2, not the far snap:
|
|
||||||
`ParkDeferred`'s quiescence parks withdrew the entity and were never
|
|
||||||
restorable while `Forget(restoreCancelledPark: true)` runs for every
|
|
||||||
accepted Position on every entity. The restorable decision now lives
|
|
||||||
inside `ParkDeferred` after `SnapToCell`, read against every live
|
|
||||||
quiescence.
|
|
||||||
**Still outstanding: #309.** The `ACDREAM_PROBE_PARK=1` capture from the
|
|
||||||
accepting session shows 11 parks, all `cause=unplaceable` — zero
|
|
||||||
quiescence-cause parks, so the shared-core park change is NOT yet
|
|
||||||
connected-verified. Without the probe that session would have been
|
|
||||||
recorded as a pass. **Re-scoped 2026-08-04: #309 is largely superseded
|
|
||||||
by #312 (closed `b1f914d5`, user-passed); what survives is the narrow
|
|
||||||
`GotoLostCell` half — retail keeps a lost-cell object hidden until
|
|
||||||
`reenter_visibility`; acdream re-shows it on cancel. Re-scope before
|
|
||||||
running it.**
|
|
||||||
Process lesson recorded: the round-1 defect was caused by the contract
|
|
||||||
omitting "and still advance the pose", and the park defect should have
|
|
||||||
been split into its own slice the moment it surfaced in round 2 instead
|
|
||||||
of riding inside 4b-2 for three more rounds.
|
|
||||||
Findings chain:
|
|
||||||
[contract](../research/2026-08-04-c4-route-4b-2-contract.md) →
|
|
||||||
[round 1](../research/2026-08-04-c4-route-4b-2-review-findings.md) →
|
|
||||||
[round 2](../research/2026-08-04-c4-route-4b-2-delta-review-findings.md) →
|
|
||||||
[round 3](../research/2026-08-04-c4-route-4b-2-round3-correction.md) →
|
|
||||||
[round 4](../research/2026-08-04-c4-route-4b-2-round4-correction.md).
|
|
||||||
Note the route-4 Create half is ALREADY DONE (C3b/C3c); the remaining work is
|
|
||||||
steady-state remote Position plus the deletions. AP-131 is NOT retired by
|
|
||||||
either sub-slice — see the scoping doc for why route 4 alone cannot.
|
|
||||||
4a contract: [`2026-08-03-c4-route-4a-contract.md`](../research/2026-08-03-c4-route-4a-contract.md).
|
|
||||||
**Route 2 connected gate PASSED (user, 2026-08-03).** Provoked with the
|
|
||||||
retail `@pklite` entry-collision bump (`69ba9486` — the only reachable ACE
|
|
||||||
trigger for `ObjectForcePosition`; admin teleports advance `ObjectTeleport`
|
|
||||||
and exercise route 3 instead, see
|
|
||||||
[`2026-08-03-c4-route-2-visual-gate.md`](../research/2026-08-03-c4-route-2-visual-gate.md)).
|
|
||||||
The user observed the visible slide off the overlapped character (the
|
|
||||||
ForcePosition applied), correct animation, no heading change, and no leash
|
|
||||||
tethering or rubber-band after the correction — so the two named behaviour
|
|
||||||
changes (ack after commit; no `ConstrainTo` re-arm on this route) are
|
|
||||||
accepted live. Both Opus reviews PASS on the final diff after three FAIL
|
|
||||||
rounds.
|
|
||||||
**Adjacent, NOT a route 2 regression:** shipping `@pklite` made PK Lite
|
|
||||||
reachable for the first time and immediately exposed pre-existing PvP gaps —
|
|
||||||
melee/ranged attacks refuse a PKLite target (auto-target retargets to the
|
|
||||||
nearest other; auto-target off does nothing) while spells on the same target
|
|
||||||
work. Under investigation; filed separately.
|
|
||||||
**Route 2 (ForcePosition) — implemented 2026-08-03, contract:**
|
|
||||||
[`2026-08-03-c4-route-2-contract.md`](../research/2026-08-03-c4-route-2-contract.md),
|
|
||||||
**plan:** [`2026-08-03-c4-route-2-implementation-plan.md`](../research/2026-08-03-c4-route-2-implementation-plan.md).
|
|
||||||
`RuntimeAcceptedPositionDriveController`
|
|
||||||
(`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs`)
|
|
||||||
is the single accepted-Position execution seam for a ForcePosition on the
|
|
||||||
already-live local player; `LocalForcePositionTransaction` and
|
|
||||||
`HeadlessSessionWorldProjection.BlipLocalPlayer` are deleted, and the
|
|
||||||
generic App render-tail is skipped for the local player's ForcePosition.
|
|
||||||
Named behaviour changes (both retail-exact, ISSUES #285): the outbound
|
|
||||||
ack now fires strictly after the canonical commit, and the constraint
|
|
||||||
leash is no longer re-armed on this route (retail's FORCE_POSITION branch
|
|
||||||
never reaches `ConstrainTo`).
|
|
||||||
**Fix round (2026-08-03):** both independent dual reviews (retail-
|
|
||||||
conformance + architecture/adversarial) FAILed the first pass — see
|
|
||||||
[`2026-08-03-c4-route-2-review-findings.md`](../research/2026-08-03-c4-route-2-review-findings.md)
|
|
||||||
for the full R1-R9 list. The critical finding (R1) was that the
|
|
||||||
DeferredCell park could not survive a single ACE broadcast interval in
|
|
||||||
production (`RuntimeEntityObjectLifetime.TryApplyPosition`'s unconditional
|
|
||||||
`Forget` on every accepted Position cancelled it before its collision
|
|
||||||
generation could commit), silently dropping the correction forever;
|
|
||||||
`RuntimeAcceptedPositionDriveController.Advance` now detects the dead
|
|
||||||
watch and re-issues from the entity's current canonical snapshot. R2/R3
|
|
||||||
restored headless's collision re-centering and login-window fallback; R4
|
|
||||||
stopped the force-ack from stealing a receipt the presentation sink had
|
|
||||||
legitimately declined; R5/R6/R9 corrected false doc claims, closed a
|
|
||||||
`_pending`-leak/overwrite gap, and fixed streaming-observer/pose-dirty
|
|
||||||
side effects firing on a declined placement. R7 corrected a fixture bug
|
|
||||||
(a dummy Setup sphere with its centre at the origin) that had been
|
|
||||||
written up as a retail fidelity gain; R8 added App-layer double-write
|
|
||||||
source pins and corrected an overclaimed single-ack test. Full detail:
|
|
||||||
[`2026-08-03-c4-route-2-review-findings.md`](../research/2026-08-03-c4-route-2-review-findings.md).
|
|
||||||
Complete Release solution after the fix round: **10,853 passed / 4
|
|
||||||
skipped / 0 failed** (baseline 10,844/4/0; first pass 10,848/4/0).
|
|
||||||
|
|
||||||
**Acceptance item 2 is NOT met — recorded gap, B2 (2026-08-03 round 2).**
|
|
||||||
An earlier revision of this paragraph claimed R8 "added the App-layer
|
|
||||||
double-write source pins the plan's own acceptance item required". That was
|
|
||||||
a claim of coverage this changeset does not have, and it is corrected here
|
|
||||||
rather than left as the citation a future session trusts (same rule that
|
|
||||||
produced R7). The truth, per the adversarial review:
|
|
||||||
- *First half — "the generic tail no longer double-writes the local
|
|
||||||
player":* **source-pinned, not proven.** The pin is a regex/`Assert.Single`
|
|
||||||
over `LiveEntityNetworkUpdateController`'s source text, so it would still
|
|
||||||
pass if a second write were spelled differently, and **no test exercises
|
|
||||||
the branch** at runtime.
|
|
||||||
- *Second half — "the committed projection is what moves the render
|
|
||||||
entity":* **uncovered at any layer.** No test drives a route-2
|
|
||||||
ForcePosition through `RuntimePlacementPresentationSink` /
|
|
||||||
`TryApplyRuntimePlacementPlace` and asserts the `WorldEntity` actually
|
|
||||||
moved. Given R4 (the force-ack no longer consumes a declined `Place`),
|
|
||||||
this is precisely the seam whose failure mode is silent: the canonical
|
|
||||||
body moves and the render entity stays put.
|
|
||||||
Closing this gap needs an App-layer test that runs the accepted
|
|
||||||
ForcePosition end to end and asserts the render entity's position/cell came
|
|
||||||
from the committed placement receipt — carry it into C5's parity tests or
|
|
||||||
file it before this sub-landing closes.
|
|
||||||
**Not yet done:** both reviews must be RE-RUN on this fixed diff, and the
|
|
||||||
connected (user-gated) acceptance gate this campaign's standing
|
|
||||||
discipline requires, before this sub-landing is considered closed — those,
|
|
||||||
and the commit itself, are next. May land as more than one commit if a
|
|
||||||
route proves large; each sub-landing keeps the full review discipline.
|
|
||||||
- **C5 — legacy deletion + closeout gates — OPEN.** Delete every superseded legacy
|
|
||||||
path; parity tests; exact lifecycle/reconnect + canonical nine-stop
|
|
||||||
connected routes; two-client observation; **user visual matrix** (the
|
|
||||||
campaign's stopping point for user acceptance). Retire AP-1, AD-1,
|
|
||||||
AP-131, AD-60's legacy half, and close #275. Update register/roadmap/
|
|
||||||
milestones/architecture/memory + successor handoff.
|
|
||||||
**Inheritance recorded at C4 closeout (2026-08-05, full detail in
|
|
||||||
[`2026-08-05-c4-closeout-handoff.md`](../research/2026-08-05-c4-closeout-handoff.md)):**
|
|
||||||
the #318 end-to-end portal composition test, whose discriminating
|
|
||||||
assertion is that **`PhysicsEngine.ShadowObjects` holds a row at the
|
|
||||||
destination — not just `LocalPlayerShadowState`'s dedup cache** (AP-145's
|
|
||||||
cache-without-publish asymmetry is why a cache-only assertion is satisfied
|
|
||||||
by the bug); the route-3 C5 sweep candidates (`ILocalPlayerTeleportPlacement`
|
|
||||||
as a thin acknowledge seam; the test-only `BeginAcceptedPlacement`/
|
|
||||||
`BeginAuthoredPlacement` wrappers); #276's settle-cell remainder and
|
|
||||||
#277's trigger-conditioned conversion; #316's measure-before-fix, #317's
|
|
||||||
velocity-chain audit, #313, and #309's re-scoped narrow half; the
|
|
||||||
cell-less live-trigger investigation (owed gate 4); and the TEMPORARY
|
|
||||||
physics probe family strip (`REMOTE_LANDING`/`REMOTE_SLIDE`/`PARK`/
|
|
||||||
`REMOTE_TELEPORT`/`CHILD_CELL`/`LOCAL_TELEPORT`) — after, never before,
|
|
||||||
the four owed gates consume them.
|
|
||||||
**#280's connected gate rides this matrix (added 2026-08-05).** Release,
|
|
||||||
`ACDREAM_RETAIL_UI=1`, `ACDREAM_STREAM_RADIUS` **UNSET** (it forces
|
|
||||||
`NearRadius` and only raises `FarRadius`, so a run with it set measures a
|
|
||||||
different window than production). Run the route TWICE on the same binary —
|
|
||||||
once with `ACDREAM_PROBE_REVEAL_RADIUS=1` (reproduces the pre-#280 gate) and
|
|
||||||
once without — and report BOTH. The user-facing observable is an ABSENCE, so
|
|
||||||
the pass criteria are three positive artifacts per stop, all from existing
|
|
||||||
machinery: (1) a `world-visible` checkpoint JSON whose
|
|
||||||
`StreamingWork.NearBacklog` / `.FarBacklog` / `.DestinationBacklog` /
|
|
||||||
`.PendingPublications` are zero for the destination window at the moment the
|
|
||||||
viewport opened; (2) a hold-duration pair — **the post-fix hold is EXPECTED
|
|
||||||
to be LONGER**, and a hold that is not longer means the gate did not widen
|
|
||||||
and the run proves nothing; (3) a paired screenshot per stop, where the
|
|
||||||
pre-fix run is the one that shows the defect. `wait world-visible 30000` in
|
|
||||||
`tools/connected-world-lifecycle.route.txt` is the convergence ceiling — a
|
|
||||||
trip is a failure, a longer pass is not. **The reported repro was a RECALL,
|
|
||||||
not `/teleloc`: the matrix needs a lifestone/recall leg**, and it must
|
|
||||||
include a first-login stop, because login shares the same barrier and its
|
|
||||||
gate widened too.
|
|
||||||
|
|
||||||
After C5: ~~AP-22~~ (RETIRED 2026-08-06, bc4679cd) and ~~AD-10~~ (RETIRED
|
|
||||||
2026-08-06 by deletion, 886333a2) are both DONE. Historical text follows.
|
|
||||||
After C5: AP-22 (authored collision shapes), then AD-10 (remote
|
|
||||||
contact-plane projection), then the campaign's final matrix and ledger
|
|
||||||
closeout; vendor Slice 5 resumes.
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
# Recent-regression cleanup — plan (2026-08-03)
|
|
||||||
|
|
||||||
Three defects introduced by the 2026-08-02/03 stabilization batch, found while
|
|
||||||
reconciling the #281 test failures. All three are **ours, days old, and inside
|
|
||||||
the least-verified code in the tree**. They are cleared before C4 resumes so
|
|
||||||
six more placement routes are not stacked on top of them.
|
|
||||||
|
|
||||||
Issues: #282 (two cell fields), #283 (two world origins), #284 (silent park).
|
|
||||||
|
|
||||||
## Status — CLOSED 2026-08-03
|
|
||||||
|
|
||||||
All three landed and are user-accepted.
|
|
||||||
|
|
||||||
| Slice | Issue | Commit | Outcome |
|
|
||||||
|---|---|---|---|
|
|
||||||
| S1 | #284 | `97d11e6c` | Park reasons named; terminal on the contradictory state |
|
|
||||||
| S2 | #282 | `3c36b4cc` | One cell owner (`VisibilityCellId`); register row AP-133 |
|
|
||||||
| S3 | #283 | `898ff18b` → `89cf1e66` | Measured UNREACHABLE; permanent invariant instead of a restructure |
|
|
||||||
|
|
||||||
Connected Release gate (retail UI) on S1+S2: user verdict "works fine"; log
|
|
||||||
showed 9 completed reveals, 58 reveal events all `failures=0`, zero unhandled
|
|
||||||
exceptions, zero parked placements, graceful exit (`0c14c402`).
|
|
||||||
|
|
||||||
S3's probe run recorded zero disagreements across 11 reveals and six landblocks
|
|
||||||
spanning ~45 km, so ownership was deliberately left alone — the evidence
|
|
||||||
disproved the hypothesis, and the guard exists to keep it disproven.
|
|
||||||
|
|
||||||
Final complete Release solution: **10,844 passed / 4 skipped / 0 failed.**
|
|
||||||
|
|
||||||
Next: the original campaign order below, starting at C4 route 2.
|
|
||||||
|
|
||||||
## Why these first
|
|
||||||
|
|
||||||
Every one is an instance of the exact weakness the placement campaign exists to
|
|
||||||
remove: **two owners of one fact, with no single writer keeping them agreed.**
|
|
||||||
#282 duplicates "which cell is this in". #283 duplicates "where is zero". #284
|
|
||||||
is why both stayed invisible. Fixing them inside C4 would mean diagnosing them
|
|
||||||
through C4's much larger diff.
|
|
||||||
|
|
||||||
## Standing discipline for this plan
|
|
||||||
|
|
||||||
- Retail is the oracle. Grep `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
|
||||||
by `class::method` before writing.
|
|
||||||
- Root causes only. No timeouts, grace periods, suppression flags, or
|
|
||||||
catch-and-ignore. #284 in particular is observability + fail-fast, never a
|
|
||||||
retry cap.
|
|
||||||
- **The complete Release solution suite must be green before every commit.**
|
|
||||||
Focused-run-only gating is exactly what let #281–#284 ship. The full suite
|
|
||||||
takes about 30 seconds; there is no excuse.
|
|
||||||
- Each fix is its own bisectable commit with root-cause evidence, and updates
|
|
||||||
the issue + divergence ledgers in that same commit.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## S1 — #284: make a parked placement visible (do this first)
|
|
||||||
|
|
||||||
Smallest, and it turns the other two from archaeology into observation.
|
|
||||||
|
|
||||||
1. Classify the park reason at the single site that produces it
|
|
||||||
(`RuntimeSetPositionState.PrepareMover`): awaiting collision generation,
|
|
||||||
awaiting Setup, awaiting world frame.
|
|
||||||
2. Fold per-reason parked counts into the existing physics ownership snapshot
|
|
||||||
(`RuntimePhysicsState.CaptureOwnership`) so they appear wherever ledgers are
|
|
||||||
already asserted, and in the connected gates' `report.json`.
|
|
||||||
3. Fail fast on unresolvable parks. A park awaiting the world frame *while a
|
|
||||||
local player is already registered* is not a wait — it is a contradiction.
|
|
||||||
Surface it as a committed invariant exception, the pattern `01f4791e`
|
|
||||||
established for receipt-ledger violations.
|
|
||||||
4. Convergence contract: parked entries must be zero at every stable
|
|
||||||
checkpoint. Wire that into the lifecycle/nine-stop gate assertions.
|
|
||||||
|
|
||||||
**Tests:** each park reason is reported exactly once and clears on resolution;
|
|
||||||
the contradictory park throws rather than retrying; ledgers converge to zero.
|
|
||||||
**Gate:** focused Runtime + complete solution suite.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## S2 — #282: one owner for an entity's visibility cell
|
|
||||||
|
|
||||||
1. **Establish the retail model.** `CPhysicsObj::set_cell_id` @0x0050f4f0,
|
|
||||||
`change_cell` @0x00513390, `set_cell_id_recursive` @0x00510da0,
|
|
||||||
`ShouldDrawParticles` @0x0050fe60. Retail carries ONE cell per physics
|
|
||||||
object, and particle gating reads that same cell. Write the pseudocode note
|
|
||||||
before touching C#.
|
|
||||||
2. **Audit the writers.** 12+ sites write `ParentCellId`
|
|
||||||
(`LiveEntityNetworkUpdateController` ×4, `RemotePhysicsUpdater` ×2,
|
|
||||||
`ProjectileController` ×3, `LiveEntityOrdinaryPhysicsUpdater`,
|
|
||||||
`LocalPlayerProjectionController`, `RemoteTeleportController`, …); 3 write
|
|
||||||
`EffectCellId`, all in `LiveEntityRuntime`. For each `ParentCellId` writer
|
|
||||||
record whether it also rebuckets — a rebucket with an exact cell currently
|
|
||||||
repairs the pair by accident. Produce the table before choosing the fix.
|
|
||||||
3. **Decide the shape.** `EffectCellId`'s documented purpose is narrow: outdoor
|
|
||||||
dat stabs that keep a null render parent while retail still gives them an
|
|
||||||
outdoor landcell. Live/interior entities were explicitly meant to use
|
|
||||||
`ParentCellId`. Preferred fix, in retail's direction: live entities stop
|
|
||||||
populating `EffectCellId`, the stab case keeps it as the documented
|
|
||||||
exception, and one owner writes the visibility cell that the effects path
|
|
||||||
reads. If the audit shows live entities genuinely need it, the alternative
|
|
||||||
is a single writer that maintains both — but never 12 independent writers
|
|
||||||
against a field that wins.
|
|
||||||
4. **Divergence register.** The two-field split is an adaptation from retail's
|
|
||||||
single cell. Add the row if none exists; delete it if step 3 collapses the
|
|
||||||
split.
|
|
||||||
|
|
||||||
**Tests:** an entity crossing a cell boundary keeps its particles and lights
|
|
||||||
attached; an equipped/attached child keeps its parent-relative behaviour; the
|
|
||||||
outdoor dat stab case is unchanged.
|
|
||||||
**Gate:** focused App + complete suite, then a **user visual check** — a
|
|
||||||
monster with an active spell effect crossing a cell boundary, and a lit static
|
|
||||||
object, indoors and outdoors.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## S3 — #283: one owner for the world origin
|
|
||||||
|
|
||||||
Sequenced last of the three and immediately before C4 route 3, which touches
|
|
||||||
the same portal code.
|
|
||||||
|
|
||||||
1. **Prove or disprove reachability first.** With S1 landed, assert at the
|
|
||||||
placement site that Runtime's frame center and App's `LiveWorldOriginState`
|
|
||||||
center agree; run the portal/recall routes. If they never diverge in
|
|
||||||
practice, the fix is a permanent invariant rather than a behaviour change —
|
|
||||||
record that and stop. Do not restructure on a hypothesis.
|
|
||||||
2. **Retail evidence.** How retail rebases its landblock offsets across a
|
|
||||||
teleport, and the ordering around `TAS_TUNNEL_CONTINUE` — the same
|
|
||||||
sequence #280 already needs read. Read once, use twice.
|
|
||||||
3. **Fix shape.** Runtime owns the world frame; App projects it. Today
|
|
||||||
`LiveWorldOriginState` is an independent owner with its own rebase edge.
|
|
||||||
Make it a projection of Runtime's frame, so there is exactly one origin and
|
|
||||||
the retirement-driven edge becomes a *publication* of that origin rather
|
|
||||||
than a second decision. This is the same ownership move the campaign has
|
|
||||||
already applied to entities, physics, and placement.
|
|
||||||
4. **Ordering invariant.** No placement may commit against an origin the
|
|
||||||
render side has not adopted. Whether that is expressed as a gate or made
|
|
||||||
structurally impossible falls out of step 3.
|
|
||||||
|
|
||||||
**Tests:** a teleport whose old-window retirement lags by many frames cannot
|
|
||||||
commit a placement against a mismatched origin; frame and origin rebase
|
|
||||||
together; ordinary movement rebases neither (already pinned by
|
|
||||||
`RuntimeWorldFrameTests`).
|
|
||||||
**Gate:** focused + complete suite, lifecycle/reconnect, and a **user visual
|
|
||||||
check** on repeated portal/recall arrivals with objects present.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## After S1–S3
|
|
||||||
|
|
||||||
Resume the original campaign order, unchanged:
|
|
||||||
|
|
||||||
1. **C4 routes 2–7** — ForcePosition, portal (with S3 landed), remote
|
|
||||||
Create/Position, projectile correction, drops, pickup/parent/delete.
|
|
||||||
Fold in #276 and #277 where their route becomes authoritative.
|
|
||||||
2. **#280** — retail destination prefetch, landed adjacent to route 3.
|
|
||||||
3. **C5** — delete superseded writers, complete suite, lifecycle/reconnect,
|
|
||||||
nine-stop soak **on the final binary**, two-client observation, the #269/#278
|
|
||||||
slope-glide check. Only then retire AP-1, AD-1, AP-131, and AD-60's legacy
|
|
||||||
half.
|
|
||||||
4. **AP-22** — `ShadowShapeBuilder` as sole authority for authored Setup
|
|
||||||
collision shapes.
|
|
||||||
5. **AD-10** — remote contact-plane projection through the real transition
|
|
||||||
sweep.
|
|
||||||
6. Final movement/collision matrix; ledger, architecture, roadmap, milestones,
|
|
||||||
memory, `CLAUDE.md`, `AGENTS.md`. Vendor Slice 5 resumes only after that.
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
# Campaign S — collision shape & response fidelity
|
|
||||||
|
|
||||||
**Opened:** 2026-08-06, immediately after #333/#337 closed (`ea83b043`).
|
|
||||||
**Status:** IN FLIGHT — overnight session 2026-08-07 ledger:
|
|
||||||
S1A (AP-157) closed by measurement, no code · S1B contract ready, not
|
|
||||||
implemented · S2 contract ready, not implemented · S3 CANCELLED (planned on a
|
|
||||||
misreading — see its section) · S1B LANDED (b3e43d22, #335 closed) and S2 LANDED (9671af02, AP-155
|
|
||||||
narrowed), Session-B dungeon gate USER-PASSED 2026-08-07 evening ("Feels
|
|
||||||
good!") · S4 half-landed (AD-65 shipped and USER-PASSED at the 2026-08-07 morning gate; AD-66 withheld
|
|
||||||
behind #341's measurement anomaly; AD-69 filed) · S5 closed (fix predated the
|
|
||||||
campaign; zombie register row) · S6 LANDED (containment: the camera provably reaches BOTH ACE-derived TOI
|
|
||||||
tails live — the rows' dormancy premise was false; guarded with counters +
|
|
||||||
one-shot unverified-mover log, four tests, sabotage on the real exemption
|
|
||||||
axis; AP-83/AP-91 rewritten CONTAINED-not-dormant, severity camera-feel
|
|
||||||
only) · **CAMPAIGN CLOSED 2026-08-07 night** with ONE honestly-open item:
|
|
||||||
AD-66's reland is blocked by the #341 codegen-shape measurement instability
|
|
||||||
(twice self-refused by its own stability gate; the ABA evidence and the
|
|
||||||
first discriminating experiment are in #341). The user's final slope look
|
|
||||||
travels with that reland. Next: #344, #343, #341's boundary hunt, then
|
|
||||||
vendors (M4) · #330 hoist landed, wiring
|
|
||||||
withheld with a seven-point scope map · #32/#338 pre-work both closed.
|
|
||||||
**Scope:** the twelve remaining collision-domain items — five shape/membership
|
|
||||||
divergences, three resolution-math divergences, two undecodable-math rows, and
|
|
||||||
three open bugs.
|
|
||||||
**SSOT while active:** this file. Digest:
|
|
||||||
`claude-memory/project_physics_collision_digest.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why a campaign and not twelve tickets
|
|
||||||
|
|
||||||
Three of these rows edit the same two functions. `AP-157` and the `AP-156`
|
|
||||||
residual both live in `ShadowObjectRegistry.BuildFloodSpheres`; `AP-159`/`#335`
|
|
||||||
lives one call away in `CellTransit.BuildShadowCellSetFromParts` and in
|
|
||||||
`ShadowObjectRegistry.BuildBspPartSpheres`. Shipping them as separate tickets
|
|
||||||
means three review cycles over the same code and three chances to reintroduce
|
|
||||||
each other's bugs. This project already has a written rule about exactly that
|
|
||||||
shape of work: **shared-file slices are ONE agent against a pinned contract**
|
|
||||||
(`feedback_dont_parallelize_coupled_plan_slices`).
|
|
||||||
|
|
||||||
The second reason is ordering. **Membership gaps mask query gaps.** We just
|
|
||||||
watched it happen: AP-156 put geometry into the right cell and AP-158 threw it
|
|
||||||
away one layer down, so AP-156's entire visible benefit was invisible until
|
|
||||||
#333 landed. Anything upstream of the query has to be correct before a gate on
|
|
||||||
the downstream math means anything.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The governing lesson from the last campaign
|
|
||||||
|
|
||||||
**The register rows are leads, not specifications. Measure before you fix.**
|
|
||||||
The evidence, all from the last two weeks:
|
|
||||||
|
|
||||||
- **AP-155(b) recorded the flood approximation as OVER-inclusive**, and used
|
|
||||||
that direction as the reason it was safe to defer. Measured, it was
|
|
||||||
UNDER-inclusive for 428 of 530 Setups — the opposite, and the dangerous
|
|
||||||
direction.
|
|
||||||
- **AP-156's risk column was wrong**, and its wrongness is precisely why #334
|
|
||||||
— a user-visible loss of collision — sat inside it unnoticed.
|
|
||||||
- **AP-22 described an unreachable branch.** 0 of 5,935 installed Setups could
|
|
||||||
satisfy its guard. The correct fix was deletion, not a port.
|
|
||||||
- **#331's headline claim was refuted outright.** The behaviour was already
|
|
||||||
retail-faithful.
|
|
||||||
|
|
||||||
So one row in four, in this exact domain, was materially wrong about its own
|
|
||||||
population, direction, or existence. **Every slice below opens with a
|
|
||||||
measurement that can cancel it.** A slice that measures its population at zero
|
|
||||||
closes as "row deleted", and that is a success, not a wasted slice.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Slice order
|
|
||||||
|
|
||||||
### Pre-work — two cheap unblocks, before the campaign proper
|
|
||||||
|
|
||||||
Both are the user's own outstanding reports and both are blocked on a
|
|
||||||
measurement that costs far less than the fix. Neither is a campaign slice.
|
|
||||||
|
|
||||||
**PW-1 — #338, the step heights.** Setup `0x02000001` authors
|
|
||||||
`StepUpHeight = 0.600` / `StepDownHeight = 1.500`; the client resolves with
|
|
||||||
`0.400` / `0.400`. **First question is whether retail reads the authored field
|
|
||||||
at all** — grep `named-retail` for the step-height getters and their callers.
|
|
||||||
If retail substitutes its own constants, 0.4 is correct and #338 closes as a
|
|
||||||
non-defect. ~30 minutes. Do not touch code before that answer.
|
|
||||||
|
|
||||||
**PW-2 — #32, local-player cliff edge-slide.** This is the *other half of the
|
|
||||||
original two-bug report* and the thing the user will feel most, so it does not
|
|
||||||
sit behind six slices. Research is already done (`38db9fff`):
|
|
||||||
`CollisionInfo.SetContactPlane` latches last-known at all 13 call sites where
|
|
||||||
retail's `COLLISIONINFO::set_contact_plane` @0x00509d80 — 22 bytes — never
|
|
||||||
does. Fix is ~20–25 lines, mostly deletion, in 2 files. **Blocked on a live
|
|
||||||
`ACDREAM_DUMP_EDGE_SLIDE=1` capture**: the report's six-row decision table has
|
|
||||||
three rows that redirect the fix entirely. Needs the user at the client.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### S1 — The flood / membership pipeline
|
|
||||||
|
|
||||||
**Rows:** AP-157, AP-156 residual, AP-159 / #335.
|
|
||||||
**Files:** `ShadowObjectRegistry.cs` (`BuildFloodSpheres`, `BuildBspPartSpheres`),
|
|
||||||
`CellTransit.cs` (`BuildShadowCellSetFromParts` indoor arm),
|
|
||||||
`ShadowShapeBuilder.cs`.
|
|
||||||
**One agent. Pinned contract. Not parallelised.**
|
|
||||||
|
|
||||||
This is the walk-through direction and the largest single win in the list.
|
|
||||||
|
|
||||||
- **AP-159 / #335** — indoors we admit an EnvCell neighbour on a *sphere* test
|
|
||||||
where retail hands the part array to each cell's own `find_transit_cells` and
|
|
||||||
tests every part's sphere against that cell's portal planes in cell-local
|
|
||||||
space. Port the part-array overload. This is the last of AP-156's traversal
|
|
||||||
residual; the outdoor half already closed with #334.
|
|
||||||
- **AP-157** — retail's third `calc_cross_cells` branch floods from ONE
|
|
||||||
`CPartArray::GetSortingSphere`; we flood from every Sphere shape. Our
|
|
||||||
cylinder flood also ignores `CylHeight`.
|
|
||||||
- **AP-156 residual** — we scale the flood sphere by entity/part scale; retail's
|
|
||||||
`CEnvCell::find_transit_cells` reads only `CPhysicsPart::pos` and never
|
|
||||||
`gfxobj_scale`. Note the asymmetry before changing anything: retail's cross-cell
|
|
||||||
walk is itself under-inclusive for scaled parts and ours is not, so "match
|
|
||||||
retail" here means **deliberately adopting a retail bug**. That is a decision
|
|
||||||
to make explicitly with the user, not silently — over-inclusive is safe,
|
|
||||||
under-inclusive is the walk-through direction.
|
|
||||||
|
|
||||||
**Opens with:** an installed-DAT sweep giving each row its true population and
|
|
||||||
direction, measured against a DAT field that is not the one being fixed (the
|
|
||||||
non-circular-oracle rule that caught AP-156's identically-zero assertion).
|
|
||||||
|
|
||||||
**Gate:** offline differential over installed DATs, plus one live indoor run —
|
|
||||||
a dungeon with tight rooms and a door.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### S2 — Static publication shape fidelity
|
|
||||||
|
|
||||||
**Row:** AP-155. **Files:** `LandblockPhysicsPublisher.cs`,
|
|
||||||
`LandblockPhysicsContentBuilder.cs`.
|
|
||||||
|
|
||||||
The static-load paths emit an authored Setup Sphere as a height-capped
|
|
||||||
Cylinder. Different files from S1, so it is separable — but it must land
|
|
||||||
**after** S1, because S1's flood consumes what these paths produce, and
|
|
||||||
measuring S2's effect while S1 is in flight would confound both.
|
|
||||||
|
|
||||||
**Gate:** shares S1's live indoor run if S1 and S2 land together; otherwise
|
|
||||||
offline only, since a shape substitution's population is fully measurable from
|
|
||||||
the DATs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### S3 — Animated collision pose — CANCELLED 2026-08-07 (the slice was planned on a misreading)
|
|
||||||
|
|
||||||
**Row:** AP-84 — which stays exactly as it is.
|
|
||||||
|
|
||||||
This plan's original S3 text claimed "a door's collision stays where the shut
|
|
||||||
door was." **That scenario cannot occur, and AP-84's own row says why:** an
|
|
||||||
open door is ETHEREAL (#150) and bypasses collision entirely, so the only
|
|
||||||
pose a door ever collides in IS the registered default pose. The row's risk
|
|
||||||
column already carries the honest residual — "an entity whose server-driven
|
|
||||||
motion state materially moves a BSP-bearing part while NON-ethereal would
|
|
||||||
collide at the stale default pose (no known case)" — with the revisit
|
|
||||||
trigger written. The register was right; this plan's summary of it was
|
|
||||||
wrong, which is the same reading failure the campaign's own governing
|
|
||||||
lesson warns about, committed by the campaign plan itself.
|
|
||||||
|
|
||||||
No fix, no gate, no door row in the morning sitting. AP-84 remains an
|
|
||||||
active, deliberate approximation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### S4 — Push-out math
|
|
||||||
|
|
||||||
**Rows:** AD-65 + AD-66. **File:** `TransitionTypes.cs` (`AdjustOffset`) — both
|
|
||||||
in the same function, so one slice.
|
|
||||||
|
|
||||||
- **AD-65** — the `collisionAngle > 0` arm substitutes `result -= N * angle`
|
|
||||||
for retail's `Plane::snap_to_plane`. Recorded effect: downhill XY travel short
|
|
||||||
by cos²θ (25% at 30°, 50% at 45°).
|
|
||||||
- **AD-66** — the safety push-out substitutes `radius * ContactPlane.Normal.Z`
|
|
||||||
for retail's bare `radius`, in both the trigger comparison and the `zDist`
|
|
||||||
numerator.
|
|
||||||
|
|
||||||
**Correction carried in from the closeout:** AD-65 was previously described as
|
|
||||||
"a live lead for #269". That framing is **retracted** — #269 closed 2026-07-31
|
|
||||||
on the user's own live gate, and AD-65's sign is *opposite* to that symptom.
|
|
||||||
AD-65 stands on its own merits. (#269's do-not-retry covers friction and jump
|
|
||||||
chains, which are byte-exonerated; `AdjustOffset` is a different function and
|
|
||||||
is not covered by it.)
|
|
||||||
|
|
||||||
This is feel, not pass-through. It cannot be gated by a test asserting "did I
|
|
||||||
fall through" — it needs a movement-feel gate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### S5 — The Sledding flatness constant
|
|
||||||
|
|
||||||
**Row:** AD-55. **File:** `PhysicsBody.cs` (`calc_friction`).
|
|
||||||
|
|
||||||
We compare `GroundNormal.Z > 0.99999536f` (≈0.175° from flat); the raw decomp
|
|
||||||
literally computes `__fcos(0.17453292519943295)` = cos(10°) ≈ 0.984808. One of
|
|
||||||
the two is a decode artefact. **Resolve by byte-decoding the constant from the
|
|
||||||
PDB-paired binary** — there is a documented method for exactly this
|
|
||||||
(`reference_pe_byte_decode`), and it has already caught one inverted mapping
|
|
||||||
this project inherited from ACE.
|
|
||||||
|
|
||||||
Cheap. **Batch its live gate with S4's** — both are movement feel on slopes,
|
|
||||||
and asking for two separate slope-feel sessions wastes the only genuinely
|
|
||||||
scarce resource in this campaign.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### S6 — Containment, NOT a fix
|
|
||||||
|
|
||||||
**Rows:** AP-83, AP-91.
|
|
||||||
|
|
||||||
**These are not portable and should not be listed as fixes.** The PerfectClip
|
|
||||||
time-of-impact tails in `CCylSphere::collide_with_point` and
|
|
||||||
`CSphere::collide_with_point` are x87 sequences that do not decompile legibly;
|
|
||||||
we took them from ACE. There is no retail text to port. Pretending otherwise
|
|
||||||
would put a "port it" ticket in the backlog forever.
|
|
||||||
|
|
||||||
The honest deliverable is **containment**: prove no production mover sets
|
|
||||||
PerfectClip, and add a guard or test that fails loudly if one ever does. Then
|
|
||||||
the rows describe a branch we can show is unreachable, which is the same
|
|
||||||
resolution AP-22 got.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Parallel track — #330, headless live-entity collision
|
|
||||||
|
|
||||||
The headless host registers no live-entity collision at all: a bot walks
|
|
||||||
through every NPC and every server-spawned object. The graphical client is
|
|
||||||
unaffected.
|
|
||||||
|
|
||||||
**Genuinely parallel.** Different host, and — unlike everything above — it
|
|
||||||
needs **no live gate from the user at all**, because the headless suite can
|
|
||||||
assert it directly. It is the one item that can proceed while the user is
|
|
||||||
unavailable, which makes it the right thing to pick up whenever a live gate is
|
|
||||||
blocking.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gate economics
|
|
||||||
|
|
||||||
The user's time at the client is the only scarce resource here. Live gates are
|
|
||||||
therefore **batched, not per-slice**:
|
|
||||||
|
|
||||||
| Session | Covers | What to do in-world |
|
|
||||||
|---|---|---|
|
|
||||||
| A | PW-2 capture | drive the cliff edges that misbehave; capture only, no fix yet |
|
|
||||||
| B | S1 + S2 | a tight dungeon with a door; walk boundaries, jump, drop a corpse |
|
|
||||||
| C | S3 | doors: open, walk through, close, walk into |
|
|
||||||
| D | S4 + S5 | slopes: run down, run across, sled, land on inclines |
|
|
||||||
|
|
||||||
Four sessions for the whole campaign. Everything else is offline.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Definition of done
|
|
||||||
|
|
||||||
- Each row either **retired** with its evidence, or **rewritten** with a
|
|
||||||
corrected population/direction, or **deleted** as describing something that
|
|
||||||
does not exist. All three are acceptable outcomes.
|
|
||||||
- No row is closed on a test that re-computes the production expression as its
|
|
||||||
own oracle. Use an independent DAT field or an independent implementation.
|
|
||||||
- Every discriminating test is **sabotage-verified**: break the production line
|
|
||||||
and watch the test redden, in the same session it is written.
|
|
||||||
- `docs/ISSUES.md` and the divergence register updated in the **same commit**
|
|
||||||
as the code, per the register's own two binding rules.
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
# Morning gate — 2026-08-07 (one sitting)
|
|
||||||
|
|
||||||
**Status: COMPLETE 2026-08-07 — G1 user-passed ("Slopes feels good").**
|
|
||||||
|
|
||||||
Everything below folds the deferred visual gates into one sitting, ordered so
|
|
||||||
travel between sites doubles as #339 reproduction attempts. Launch is
|
|
||||||
prepared by the overnight session; **the client is NOT launched until you're
|
|
||||||
back** (your instruction).
|
|
||||||
|
|
||||||
## 0. Launch (the lead runs this when you say go)
|
|
||||||
|
|
||||||
Main checkout, absolute path, probes armed. The first `[step-h]` line in the
|
|
||||||
log must print the assembly path under
|
|
||||||
`C:\Users\erikn\source\repos\acdream\src\` — that is the wrong-binary guard,
|
|
||||||
now part of every capture.
|
|
||||||
|
|
||||||
## 1. Closed overnight with nothing owed to your eyes
|
|
||||||
|
|
||||||
- **#32 local edge-slide** — you passed it at the Rithwic cliff ("Yes works
|
|
||||||
now"). Ledger closed, AD-67 filed for the one kept residual.
|
|
||||||
- **#338 step heights** — headline refuted by full-capture statistics
|
|
||||||
(111,248 authored-pair resolves vs 358 placeholder ones); no production
|
|
||||||
change, nothing to look at. AD-68 records the benign async-residency
|
|
||||||
placeholder.
|
|
||||||
- **AP-157 (S1A)** — measured, both halves resolved without a code change:
|
|
||||||
the CylHeight half is retail's own behaviour (byte-verified), and the
|
|
||||||
sorting-sphere half is real vs retail's registration set but PROVEN unable
|
|
||||||
to change any collision outcome (flood and test geometry are the same
|
|
||||||
spheres). Fix deferred to the next bake-schema revision.
|
|
||||||
- **AD-55 (S5) — closed, and the fix turned out to be a WEEK OLD.** The
|
|
||||||
cos(10°) constant has been live in production since 2026-07-30
|
|
||||||
(`252e8068`); you've been playing on it. Tonight's byte-decode is an
|
|
||||||
independent confirmation. The register row claiming otherwise was a
|
|
||||||
ZOMBIE: an unrelated revert (`a8a7d64b`) resurrected the retired row —
|
|
||||||
and, found by auditing that same revert, had also silently DELETED the
|
|
||||||
then-active AD-56 row. Both corruptions fixed; the class is now a memory
|
|
||||||
rule. No feel gate owed for S5.
|
|
||||||
|
|
||||||
## 2. The sitting — ONE row
|
|
||||||
|
|
||||||
| # | What | Where / how | Pass looks like |
|
|
||||||
|---|---|---|---|
|
|
||||||
| G1 | **S4 / AD-65 — downhill slope feel.** The away-from-plane response now
|
|
||||||
snaps to the surface (XY preserved) instead of projecting (XY shrunk by
|
|
||||||
cos²θ — 25% at 30°, 50% at 45°). | Run DOWN a long slope (the Rithwic
|
|
||||||
descent works), then across it diagonally; jump down-slope and land
|
|
||||||
running. | Downhill ground speed feels like retail — no "wading" slowdown
|
|
||||||
on descents; no new stutter or floatiness; landings keep momentum
|
|
||||||
downhill. |
|
|
||||||
|
|
||||||
That is the whole sitting: **one slope run, ~3 minutes.** Everything else
|
|
||||||
either closed with no gate owed or was deliberately withheld (below).
|
|
||||||
|
|
||||||
## 2.5 Withheld / deferred overnight — nothing to test, decisions recorded
|
|
||||||
|
|
||||||
- **AD-66 (the push-out's bare radius) — WITHHELD, issue #341.** The port is
|
|
||||||
byte-proven twice over, but the landing produced a measurement that
|
|
||||||
contradicted itself (same binaries, opposite outcomes flipping with test
|
|
||||||
assert shape). Parked behind an instrumentation plan rather than guessed
|
|
||||||
at. AD-69 filed alongside: the same block misses retail's seam-frame
|
|
||||||
correction.
|
|
||||||
- **#330 headless collision — wiring withheld, hoist landed.** Dual review
|
|
||||||
converged on a real dependency the contract missed: headless has no
|
|
||||||
remote-motion tick, so spawn-registered shadows would freeze into phantom
|
|
||||||
obstacles. The issue now carries the full seven-point scope map.
|
|
||||||
- **S1B (indoor box-admit) and S2 (static sphere emission)** — contracts
|
|
||||||
written and committed, not implemented; next session picks them up
|
|
||||||
directly.
|
|
||||||
- **S3 — cancelled**: planned on a misreading; open doors are ethereal, so
|
|
||||||
AP-84's approximation is behaviourally equivalent. The register was right.
|
|
||||||
|
|
||||||
## 3. Free riders during travel
|
|
||||||
|
|
||||||
- **#339 portal-space hang** — every portal you take is a reproduction
|
|
||||||
attempt; if you get stuck again, the log captures the readiness flags and
|
|
||||||
the session becomes the diagnosis session. Nothing to do actively.
|
|
||||||
|
|
||||||
## 4. Deliberately NOT in this sitting
|
|
||||||
|
|
||||||
- **C5c's connected-gate batch** (~1 hr: D-1 scenarios, AP-136 six-step park,
|
|
||||||
route-7, two-client, nine-stop soak) — standing debt by your direction;
|
|
||||||
say the word any morning and it becomes its own sitting. The probe family
|
|
||||||
stays in the tree until it runs.
|
|
||||||
- **AP-156 scale question** — retail does NOT scale cross-cell flood spheres;
|
|
||||||
we do. Matching retail here means adopting a retail bug whose direction is
|
|
||||||
walk-through for shrunk objects. Your call, explicitly, before any code
|
|
||||||
changes: keep our over-inclusive scaling (safe, divergent) or match retail
|
|
||||||
(faithful, under-inclusive for scale<1). One sentence from you settles it.
|
|
||||||
|
|
@ -1,379 +0,0 @@
|
||||||
# Campaign A — Audio retail-feel parity
|
|
||||||
|
|
||||||
**Status: CODE-COMPLETE 2026-08-08 — awaiting the user listening gate.** All six slices landed (see the ledger). Research phase complete;
|
|
||||||
six-lane named-retail decode done, all load-bearing claims byte-verified
|
|
||||||
against the PDB-paired 2013 binary (BN pseudo-C alone was NOT sufficient —
|
|
||||||
see "BN traps" below).
|
|
||||||
|
|
||||||
**Goal:** the client sounds like retail. Every divergence between our audio
|
|
||||||
runtime and the 2013 EoR client is either fixed to the retail mechanism or
|
|
||||||
recorded in the divergence register with a reason.
|
|
||||||
|
|
||||||
**Research base (read the lane note before implementing its slice):**
|
|
||||||
|
|
||||||
| Lane | Note | Owns |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | `docs/research/2026-08-08-audio-retail-soundmanager-core.md` | SoundManager, falloff, pan, voice pool, prefs |
|
|
||||||
| 2 | `docs/research/2026-08-08-audio-retail-ambient-runtime.md` | AmbientSound/ConstantSound/IntermitSound runtime |
|
|
||||||
| 3 | `docs/research/2026-08-08-audio-retail-ambient-authoring.md` | Region-file authoring chain, dat coverage |
|
|
||||||
| 4 | `docs/research/2026-08-08-audio-retail-dat-layer.md` | SoundTable/Wave formats, selection model, dat census |
|
|
||||||
| 5 | `docs/research/2026-08-08-audio-retail-server-sounds.md` | 0xF750 wire path, play_sound, trigger catalog |
|
|
||||||
| 6 | `docs/research/2026-08-08-audio-retail-music-absence.md` | Music (there is none), MediaMachine, AdminEnvirons |
|
|
||||||
|
|
||||||
The older `docs/research/deepdives/r05-audio-sound.md` is SUPERSEDED where it
|
|
||||||
conflicts with the lane notes (its §5.1 falloff, §6 music, and §7 ambient
|
|
||||||
sections are wrong — Ghidra-era `FUN_xxx` reads that the named decomp + byte
|
|
||||||
decode overturned). Slice A6 adds the banner.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What retail's audio engine actually is (one page)
|
|
||||||
|
|
||||||
Retail is a **2D pan+gain engine**, not 3D audio. Every gameplay buffer is
|
|
||||||
created with `m_3D = 0`; the DirectSound 3D listener the client sets up is
|
|
||||||
dead code. Spatialization is CPU-side per voice at play time:
|
|
||||||
|
|
||||||
- **Gain** (`SoundManager::GetAttenuation @ 0x00550020`; byte-decoded):
|
|
||||||
`g = dist < 5m ? vol : 25·vol/dist²`, clamped to 1.0, then multiplied by ONE
|
|
||||||
master knob (`effect_sound_volume` or `ambient_sound_volume`) — clamp
|
|
||||||
first, multiply second — then `db = ceil(20·log10 g)` with a hard floor at
|
|
||||||
−50 dB, below which the voice is **not allocated at all**. Audible radius
|
|
||||||
≈ 94.2 m at vol 1.0.
|
|
||||||
- **Pan**: `pan_dB = (int)(−15·sin(Δbearing))`, truncating toward zero,
|
|
||||||
saturated at ±15 dB, forced to 0 when `(int)distance < 5`. No front/back,
|
|
||||||
no elevation.
|
|
||||||
- **Listener** = `SmartBox::viewer` — the **collided third-person camera**
|
|
||||||
Position, refreshed once per rendered frame (`SmartBox::set_viewer` @
|
|
||||||
`0x00452D36`, `SmartBox::update_viewer` @ `0x00453CE0`), falling back to the
|
|
||||||
player's own position when the camera sweep fails. Only its origin and
|
|
||||||
`Frame::get_heading` are read. (An earlier draft of this plan said the
|
|
||||||
listener is the player and called acdream's camera listener a defect —
|
|
||||||
wrong, and corrected at A2. Do not re-"fix" it.)
|
|
||||||
- **Voice pool**: allocator is `SoundManager::PlaySoundInternal @ 0x0054FEC0`.
|
|
||||||
Eviction compares the DAT-authored **float priority** (0..1); equal
|
|
||||||
priority never evicts. (`FUN_00550AD0` cited in our code is a hash-table
|
|
||||||
constructor — wrong symbol.)
|
|
||||||
- **No loops, no pitch**: retail never sets the DSound loop flag and never
|
|
||||||
calls `SetFrequency`. "Looping" ambients are re-fired one-shots.
|
|
||||||
- **Variant selection** (`SoundTableData::Lookup` + play sites): pick
|
|
||||||
`idx = (int)(roll01 · (n−1))` — uniform over all but the LAST entry
|
|
||||||
(a genuine Turbine off-by-one; the last variant is unreachable and a
|
|
||||||
faithful port keeps that) — then a SEPARATE Bernoulli gate
|
|
||||||
`rand()/32767 < probability`, else **silence**. Probability is a gate,
|
|
||||||
not a weight.
|
|
||||||
- **Volume field** is unbounded gain (dats go up to 10.0); retail clamps
|
|
||||||
only AFTER the distance divide, so >1 volumes extend audible range.
|
|
||||||
- **Prefs** (`InitPrefs @ 0x005503F0`, 8 keys): three float volumes
|
|
||||||
(effect / ambient / interface — interface is registered but **never
|
|
||||||
read**), three enable bools, `Sound Features` (==1 disables pan),
|
|
||||||
`Play Sound Only When Active`. There is NO music knob.
|
|
||||||
- **Quirk (faithful-port decision)**: effect and ambient volumes are each
|
|
||||||
applied twice (once at the play site, again inside GetAttenuation) — the
|
|
||||||
sliders are effectively **squared**.
|
|
||||||
|
|
||||||
**Sound triggers, exhaustively** (lane 5): (1) animation hooks
|
|
||||||
(SoundHook/SoundTableHook/SoundTweakedHook) — footsteps, combat swooshes,
|
|
||||||
all authored in MotionTables; (2) the server `Sound` event **0xF750**
|
|
||||||
(`guid, SoundType, vol`) — hits, wounds, wield, pickup, locks, lifestone,
|
|
||||||
spell resists; retail queues an event for a not-yet-known guid and replays
|
|
||||||
it on CreateObject, and plays at the WIRE volume, ignoring the table
|
|
||||||
entry's volume (the hook path does the opposite); (3) PhysicsScripts
|
|
||||||
(0xF754/5 — already live in acdream); (4) UI sounds via the ClientUISystem
|
|
||||||
sound table loaded by `DBObj::GetByEnum(0x22, slot 7)`; (5) region-authored
|
|
||||||
ambients (below). `CPhysicsObj::play_sound` has exactly ONE caller — the
|
|
||||||
0xF750 handler. There are NO client-local collision/jump/water sound call
|
|
||||||
sites; inventing one is a divergence.
|
|
||||||
|
|
||||||
**Ambients** (lanes 2+3): authored entirely in `region.dat`
|
|
||||||
(`Region.SoundInfo` → `AmbientSTBDesc[]` referenced by scene types ← terrain
|
|
||||||
types). On every **objcell change** (24 m), `CellManager::ChangePosition`
|
|
||||||
rebuilds weights by walking the **3×3 landblock ring × 64 land cells each**,
|
|
||||||
decoding each cell's terrain word to (terrainType, sceneIdx) and
|
|
||||||
accumulating per-sound inverse-square weight (1.0 inside 20 m, `(20/d)²` to
|
|
||||||
120 m, 0 beyond) plus an 8-way bearing histogram. Playback is a min-heap of
|
|
||||||
absolute deadlines ticked from the frame loop; each pop plays a one-shot
|
|
||||||
and re-arms. `base_chance == 0` ⇒ **ConstantSound**: non-positional,
|
|
||||||
volume = its weight share of total (a real terrain crossfade), re-fires
|
|
||||||
every `min_rate` s. Non-zero ⇒ **IntermitSound**: authored fixed volume,
|
|
||||||
positioned at a random accumulated bearing ±11.25° at distance
|
|
||||||
`min + (max−min)·t²`, gated by `roll ≤ base_chance`, interval
|
|
||||||
`RollDice(min_rate, max_rate)`. **Indoors is silent by design** —
|
|
||||||
`CEnvCell::add_ambient_sounds` is an empty folded `ret`; EnvCell has no
|
|
||||||
sound data. No day/night/weather selection exists.
|
|
||||||
|
|
||||||
**Music does not exist** (lane 6): the linked winmm MIDI player has zero
|
|
||||||
callers (`midiPlay` = 4 textual occurrences: definition + its own queue
|
|
||||||
drainer; verified independently), "music" appears 0 times in the 65 MB
|
|
||||||
decomp, no SoundType music member, no music pref, no music files shipped.
|
|
||||||
`MediaMachine` is a UI-state media bytecode VM whose `Update_Sound` routes
|
|
||||||
LayoutDesc-authored waves/table rows to the interface bus.
|
|
||||||
|
|
||||||
### BN traps (binding on every slice — reread before porting)
|
|
||||||
|
|
||||||
Binary Ninja renders x87 memory-operand compares as unimplemented `bool p`
|
|
||||||
and elides the constants; **five** sites would have ported with inverted
|
|
||||||
polarity or zeroed math: `is_continuous`, both `CanHear`s, `PlayNow`,
|
|
||||||
`PlayProbability`, plus `GetAttenuation` printing `* 0f`. Byte-decode the
|
|
||||||
PDB-paired binary (`reference_pe_byte_decode.md` workflow) for ANY float
|
|
||||||
compare or constant in this subsystem. The lane notes contain the verified
|
|
||||||
values; if a needed constant is not in a note, decode it — do not trust
|
|
||||||
the pseudo-C rendering and do not guess.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Where acdream is today
|
|
||||||
|
|
||||||
Working and retail-correct-in-shape: the animation-hook trigger path
|
|
||||||
(`AudioHookSink`, correctly the only client-local trigger), SoundTable/Wave
|
|
||||||
dat parsing (byte-exact vs retail), `SoundId` enum (golden-conformance
|
|
||||||
tested), entity→SoundTable resolution (Setup-then-wire precedence),
|
|
||||||
world-audio quiescence across portal transitions, AL buffer budget/lifetime.
|
|
||||||
|
|
||||||
Divergent or missing, ranked by audible impact:
|
|
||||||
|
|
||||||
| # | Defect | Where | Symptom |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | Probability gate absent: `SoundCookbook.Roll` short-circuits single-entry lists (4,183/4,184 entries!) before any roll; CDF walk instead of `(n−1)` pick + gate | `SoundCookbook.cs` | Idle chatter ~20× too often; nothing ever randomly silent — the "incorrect ambient-ish noise" complaint |
|
|
||||||
| 2 | 0xF750 unhandled — zero hits in `src/` | `Core.Net` routing | Every server cue silent (hits, wounds, pickup, locks, lifestone…) |
|
|
||||||
| 3 | Falloff: AL `InverseDistanceClamped` ref 2 m ⇒ `2/d` first-power, no −50 dB cutoff; AL's 3D panner instead of retail's ±15 dB angular pan | engine + `WorldRenderFrameBuilder` | Wrong loudness curve in both directions — quieter than retail up close, audible where retail is silent; stereo image wider and 3-D where retail's is a narrow angular pan (AP-28) |
|
|
||||||
| 4 | Priority float [0,1] cast to int 0..7 → 4,100 entries collapse to 0; eviction compares gain not priority | `AudioModel`/engine | Eviction ordering gutted under voice pressure |
|
|
||||||
| 5 | Volume clamped at field instead of after distance divide | `AudioHookSink` | >1-gain sounds lose up to 3× audible range |
|
|
||||||
| 6 | Region ambient system absent (`StartAmbient` stub) | engine | Silent outdoors atmosphere (TS-29 half) |
|
|
||||||
| 7 | UI sound bank absent; AdminEnvirons stingers logged not played; portal enter/exit cues missing | — | TS-54, AP-115 |
|
|
||||||
| 8 | `PlayMusic`/`StopMusic`/`MusicVolume` model retail code that never runs | `IAudioEngine`, settings | Dead API + misleading settings knob |
|
|
||||||
| 9 | Dead code: `AudioFalloff` (wrong constants, unused), wrong `FUN_00550ad0` citation, invented `PitchMin/PitchMax`+`Loop`+`Is3D` fields on `SoundEntry` | `AudioModel.cs`, engine header | Traps for future readers |
|
|
||||||
|
|
||||||
Register rows in scope: **AP-28** (retire at A2), **AP-115** sound half
|
|
||||||
(retire at A4), **TS-29** (retire at A5/A6), **TS-54** (retire at A4),
|
|
||||||
**TS-9** (re-scope at A6 — dat census says exactly 1 of 786 waves is MP3).
|
|
||||||
Issue **#321** (sound-cache decode-dedup race) folds into A6.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Slices
|
|
||||||
|
|
||||||
Ordering is audible-value per effort; A1–A2 are the "it sounds wrong"
|
|
||||||
fixes, A3–A4 the "it's silent where retail speaks" fixes, A5 the big new
|
|
||||||
system, A6 the cleanup. Each slice: grep-named → (byte-decode if any new
|
|
||||||
constant) → pseudocode check against lane note → port → conformance tests →
|
|
||||||
build/test green → commit; user listening gates where marked.
|
|
||||||
|
|
||||||
### A1 — Selection-model correctness (small; biggest audible fix)
|
|
||||||
|
|
||||||
Replace `SoundCookbook.Roll` with retail's exact model: uniform
|
|
||||||
`idx = (int)(roll01 · (n−1))` (preserving the last-entry-unreachable
|
|
||||||
off-by-one), then the separate Bernoulli probability gate returning
|
|
||||||
"silence" — including for single-entry lists. Priority stays float [0,1]
|
|
||||||
end-to-end (`SoundEntry.Priority`, engine slots). Volume passes through
|
|
||||||
unclamped; clamp moves to post-attenuation (staged here, consumed by A2).
|
|
||||||
Delete the invented `PitchMin/PitchMax/Loop/Is3D` fields. Rewrite
|
|
||||||
`SoundCookbookTests` against golden values from the lane-4 note's decoded
|
|
||||||
tables; add a distribution test for the gate.
|
|
||||||
|
|
||||||
Acceptance: conformance tests green; connected sanity — creature idle
|
|
||||||
chatter audibly rare (Speak1 ≈ 5% per trigger, was 100%).
|
|
||||||
|
|
||||||
### A2 — Falloff/pan/listener/voice parity (retires AP-28)
|
|
||||||
|
|
||||||
Port `GetAttenuation` + pan CPU-side exactly (5 m knee, `25·vol/d²`,
|
|
||||||
clamp-after, ceil-dB, −50 dB no-allocate floor, `−15·sin(Δheading)` pan
|
|
||||||
±15 dB with 5 m dead zone, `Sound Features==1` pan disable). OpenAL
|
|
||||||
becomes a dumb 2D voice bank: source-relative sources, per-voice gain +
|
|
||||||
pan (AL_POSITION azimuth from pan only); remove AL's distance model and the
|
|
||||||
listener orientation math.
|
|
||||||
|
|
||||||
**Listener correction (2026-08-08, from the lane-1 decode):** an earlier draft
|
|
||||||
of this plan said the listener must move "from camera pose to player
|
|
||||||
position/heading" and listed "listener = CAMERA" as a defect. That was wrong,
|
|
||||||
and it was written before lane 1 landed. Retail's listener IS the camera:
|
|
||||||
`SmartBox::set_viewer` @ `0x00452D36` hands the same COLLIDED third-person
|
|
||||||
camera Position to `SoundManager::SetPlayerPosition`, the sky, and the camera
|
|
||||||
setup, refreshed once per rendered frame from `SmartBox::update_viewer` @
|
|
||||||
`0x00453CE0` (falling back to the player's own position when the sphere sweep
|
|
||||||
fails). acdream's chase camera collides too, so the position source was already
|
|
||||||
faithful; only the HEADING extraction changes, since retail reads
|
|
||||||
`Frame::get_heading` — one compass bearing — and never a forward/up basis.
|
|
||||||
Do not "fix" this back.
|
|
||||||
|
|
||||||
Eviction compares float priority (equal never
|
|
||||||
evicts); fix the pool citation to `PlaySoundInternal @ 0x0054FEC0`.
|
|
||||||
Keep the squared-volume quirk faithful (register row if we later soften
|
|
||||||
it). Map settings: Master (ours, AL listener gain) + Effect + Ambient +
|
|
||||||
Interface mirroring retail's knobs; note interface is read by no retail
|
|
||||||
path (we wire it to the UI bus anyway — divergence row, deliberate).
|
|
||||||
|
|
||||||
Acceptance: unit tests on gain/pan tables (golden distances from lane 1
|
|
||||||
note); **user listening gate** — side-by-side with retail: walk away from
|
|
||||||
a blacksmith's hammering, confirm matching fade-out distance (~94 m) and
|
|
||||||
pan behavior.
|
|
||||||
|
|
||||||
### A3 — Server sound path (0xF750)
|
|
||||||
|
|
||||||
Parse `Sound` (guid, SoundType u32, volume f32) in the message router;
|
|
||||||
route to a new `ServerSoundController`: resolve guid → entity; unknown
|
|
||||||
guid ⇒ queue the event and replay on CreateObject (retail
|
|
||||||
`CObjectMaint` behavior); known guid without SoundTable ⇒ silent drop;
|
|
||||||
play via the SoundTable at the **wire volume** (ignore table volume —
|
|
||||||
asymmetric with the hook path, byte-verified). Position at the entity's
|
|
||||||
current origin.
|
|
||||||
|
|
||||||
Acceptance: wire-format conformance test (three-oracle layout);
|
|
||||||
connected gate — melee hits, item pickup/drop, lifestone bind audibly
|
|
||||||
fire against ACE.
|
|
||||||
|
|
||||||
### A4 — UI + interface sounds (retires TS-54, AP-115's sound half)
|
|
||||||
|
|
||||||
Load the ClientUISystem sound table (`GetByEnum` cache 0x22, enum slot 7 —
|
|
||||||
resolve the actual DID at port time from `ClientUISystem::GetUISoundTable`).
|
|
||||||
Route `PlayUi(SoundId)` through it (delete the no-op). Wire:
|
|
||||||
AdminEnvirons 0x65..0x7C → `PlaySoundFromCenter` stingers
|
|
||||||
(`WorldEnvironmentController.ApplyAdminEnvirons` already parses them);
|
|
||||||
portal enter/exit `UI_EnterPortal`/`UI_ExitPortal`; button/panel cues where
|
|
||||||
the retained UI already has command seams; `MediaDescSound` support in
|
|
||||||
`LayoutImporter` (DatReaderWriter parses it; interface bus, per lane 6).
|
|
||||||
|
|
||||||
Acceptance: connected gate — `@environs` thunder/drums audible; portal
|
|
||||||
enter/exit cues audible on recall; **user listening gate** vs retail.
|
|
||||||
|
|
||||||
### A5 — Region ambient system (retires TS-29's ambient half)
|
|
||||||
|
|
||||||
New `AmbientSoundSystem` (App layer, owned like other world controllers):
|
|
||||||
rebuild on objcell change (reuse streaming's cell-transit signal), walk
|
|
||||||
the 3×3 ring × 64 cells via the SAME terrain-word decode the scenery
|
|
||||||
pipeline uses (`SceneryGenerator`-shared helper), accumulate weight
|
|
||||||
(1.0 ≤ 20 m, `(20/d)²` ≤ 120 m) + 8-way bearing, build
|
|
||||||
Constant/Intermit instances from `AmbientSTBDesc` (`base_chance == 0` ⇒
|
|
||||||
constant — the byte-verified polarity), min-heap of absolute deadlines
|
|
||||||
ticked per frame, one-shots through the ambient volume path (squared,
|
|
||||||
faithful). ConstantSound non-positional; IntermitSound positioned at
|
|
||||||
random accumulated bearing, `min + (max−min)·t²` distance. Teardown on
|
|
||||||
world transition via the existing quiescence edge. Indoors: NO ambients
|
|
||||||
(retail-faithful); `seen_outside` cells get the outdoor set. Delete
|
|
||||||
`StartAmbient`/`StopAmbient` from `IAudioEngine` (wrong shape — looping
|
|
||||||
handle API models a mechanism retail doesn't have).
|
|
||||||
|
|
||||||
Acceptance: unit tests on weight accumulation + scheduler with a synthetic
|
|
||||||
region; **user listening gate** — Holtburg outdoors vs retail side-by-side
|
|
||||||
(birdsong/wind character and rough cadence), dungeon silence, ambient
|
|
||||||
crossfade walking shore → grass.
|
|
||||||
|
|
||||||
### A6 — Deletions, bookkeeping, and the long tail
|
|
||||||
|
|
||||||
- Delete `PlayMusic`/`StopMusic`/`MusicVolume` and the `AudioSettings.Music`
|
|
||||||
knob (settings migration: drop the field, tolerate old json). Retail has
|
|
||||||
no music system; register row NOT needed once the API is gone (nothing
|
|
||||||
diverges — absence matches retail).
|
|
||||||
- Delete dead `AudioFalloff` (superseded by A2's ported math).
|
|
||||||
- r05 doc: SUPERSEDED banner pointing at the six lane notes; corrections
|
|
||||||
list from lane notes §12/§13.
|
|
||||||
- TS-9 re-scope: 1 MP3 wave in the shipped dats (`0x0A000393`, ~2 s) —
|
|
||||||
either a ~50-line managed MP3 decode for one asset or an accepted-loss
|
|
||||||
row with the census cited. ADPCM count to be measured the same way
|
|
||||||
before deciding.
|
|
||||||
- #321: make `DatSoundCache` decode-dedup safe under concurrent access
|
|
||||||
(single-flight per wave id) — the full-suite flake.
|
|
||||||
- Register sweep: retire AP-28/TS-29/TS-54 rows in their landing slices'
|
|
||||||
commits (rule 1); add rows for: interface-volume wired (A2), any
|
|
||||||
softened quirk, and anything discovered mid-campaign.
|
|
||||||
|
|
||||||
Acceptance: build/test green, register diff reviewed, no orphaned
|
|
||||||
API/settings references.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Out of scope (explicitly)
|
|
||||||
|
|
||||||
- Client-local physics sounds (collision/jump/water) — retail has none;
|
|
||||||
the server sends them. Do not invent.
|
|
||||||
- Indoor ambient beds — retail is silent indoors.
|
|
||||||
- A music system — retail has none. (If we ever WANT music, that's a
|
|
||||||
new-feature decision for the user, not parity work.)
|
|
||||||
- HRTF/doppler/reverb — no retail counterpart.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
Each slice is one commit (A6 possibly two); rollback is `git revert
|
|
||||||
<slice-sha>`, recorded in this doc's ledger as slices land. A2 and A5 are
|
|
||||||
the only slices touching frame-loop code paths; both are behind the
|
|
||||||
existing audio-availability guard, so `ACDREAM_NO_AUDIO=1` remains the
|
|
||||||
global kill switch.
|
|
||||||
|
|
||||||
## Ledger
|
|
||||||
|
|
||||||
| Slice | Status | Commit | Gates |
|
|
||||||
|---|---|---|---|
|
|
||||||
| A1 | **COMPLETE** 2026-08-08 | `c69b3bde` | 42 Core audio tests; full Release suite 11,563 passed / 4 skipped / 0 failed. Closes #355. |
|
|
||||||
| A2 | **COMPLETE** 2026-08-08 | `e42b9948` | 118 Core audio tests (mixer + voice pool + cookbook); full Release suite 11,639 passed / 4 skipped / 0 failed. Opus review run and applied — 2 HIGH (pan-law saturation, stale `FUN_00550ad0` header), 5 MEDIUM (untested clamp order / pan truncation / voice pool, dead `PlayingGain`, duplicated heading helper), 5 LOW. Retires AP-28; files AP-173, AP-174, TS-64, TS-65. **Owed: user listening gate.** |
|
|
||||||
| A3 | **COMPLETE** 2026-08-08 | `8bc458fb` | 14 wire-conformance tests + 5 controller tests; full Release suite 11,658 passed / 4 skipped / 0 failed. **Owed: connected gate** (melee hit / pickup / lifestone audible against ACE). |
|
|
||||||
| A4 | **COMPLETE** 2026-08-08 | `6eaa490b` | UI bank DID resolved from the dats (`0x2000004B`, content-verified: exactly the 32 `UI_*` slots) + 21-case environ table, 30 new Core tests; full Release suite 11,691 passed / 4 skipped. Retires TS-54; narrows AP-115 to notice-only. **Owed: connected gate** (`@environs` thunder + recall cues audible). **Suite note:** two load-dependent measurement flakes were observed on separate full-suite runs (`RuntimeCollisionReportingStateTests.WarmedSteadyContactRefreshDoesNotAllocate`, and one unnamed Core.Net test); both pass in isolation and neither touches audio. |
|
|
||||||
| A5 | **COMPLETE** 2026-08-08 | `7c4dd1ad` | 46 ambient conformance tests; full Release suite 11,739 passed / 4 skipped. Opus review run and applied — it caught a FATAL frame bug (cell offsets built in absolute world coordinates while the listener is in the streamed frame: every contribution culled at ~32 km, feature silent with no error), a per-entry vs per-cell denominator error that would have pushed multi-entry beds under the audibility floor, an infinite loop on a zero play-rate, and newly-audible ambients not firing until a full period later. Also moved beds onto retail's single 16-voice priority pool and made the in-block direction test XY-only. Retires TS-29; files TS-66 (`seen_outside` interiors), TS-67 (in-plane weight). **Owed: user listening gate.** |
|
|
||||||
| A6 | **COMPLETE** 2026-08-08 | `dd2cb92b` | Full Release suite 11,739 passed / 4 skipped. Deleted the music API (`PlayMusic`/`StopMusic`/`MusicVolume` + the `AudioSettings.Music` knob); exposed the Ambient slider now that A5 drives it; reset the invented 0.8 ambient default to retail's 1.0; SUPERSEDED banner on `r05-audio-sound.md` listing its five wrong sections; TS-9 re-scoped to the measured one-wave blast radius. **Deferred:** #321's decode-dedup race (a pre-existing concurrency flake, not audio-parity behaviour — not fixed speculatively without reproducing it). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CAMPAIGN CLOSEOUT (code-complete 2026-08-08)
|
|
||||||
|
|
||||||
Six slices, six commits, `c69b3bde` → A6. The full Release suite ends at
|
|
||||||
**11,739 passed / 4 skipped / 0 failed**, up from 11,563 at campaign start;
|
|
||||||
the audio subsystem went from 42 tests (one of which pinned the wrong model)
|
|
||||||
to ~215 conformance tests written against byte-decoded values.
|
|
||||||
|
|
||||||
**What was wrong, and is now right:**
|
|
||||||
|
|
||||||
| Was | Now |
|
|
||||||
|---|---|
|
|
||||||
| Probability treated as a selection weight, and skipped entirely for the 4,183/4,184 single-entry sounds | Retail's uniform `(n−1)` pick plus an independent Bernoulli silence gate |
|
|
||||||
| OpenAL 3-D spatialization, `2/d` falloff, no cutoff | Retail's CPU 2-D model: `25·vol/d²` past a 5 m knee, −50 dB no-allocate floor (~94 m), ±15 dB sine pan with a 5 m dead zone |
|
|
||||||
| Voices evicted by gain | Evicted by DAT-authored float priority, strictly-less, ring order |
|
|
||||||
| `0xF750` unparsed — every server cue silent | Parsed, guid-queued-and-replayed, played at the wire volume |
|
|
||||||
| No interface sound bank | Bank DID resolved from the dats' EnumIDMap chain; portal cues + 21 AdminEnvirons stingers live |
|
|
||||||
| No ambient system at all | Region-authored, per-land-cell, 3×3 ring, deadline-queue one-shots with terrain crossfade |
|
|
||||||
| A music API | Deleted — retail has no music system |
|
|
||||||
|
|
||||||
**Process notes worth carrying forward:**
|
|
||||||
|
|
||||||
1. **Binary Ninja could not be trusted anywhere in this subsystem.** Five
|
|
||||||
float compares render inverted or with elided constants; `GetAttenuation`
|
|
||||||
prints `* 0f`, which ports as silence at every distance. Every load-bearing
|
|
||||||
value here came from byte-decoding the PDB-paired binary. The lane notes
|
|
||||||
record the verified values; a future reader should decode rather than
|
|
||||||
re-read the pseudo-C.
|
|
||||||
2. **Two research notes were wrong and were corrected in place** — lane 1's
|
|
||||||
30 m decibel row (contradicted its own gain column) and lane 5's
|
|
||||||
transposed `GetByEnum` arguments (which would have made the UI bank
|
|
||||||
unresolvable). Both were caught by recomputing rather than copying.
|
|
||||||
3. **The reviews earned their cost.** A2's review caught a pan mapping that
|
|
||||||
saturated to full separation where retail gives 15 dB. A5's caught a
|
|
||||||
coordinate-frame error that would have made the entire ambient system
|
|
||||||
silent with nothing logged — the tests passed, the build was green, and it
|
|
||||||
would have failed only at the listening gate.
|
|
||||||
4. **An architecture guard caught a design error the tests could not**:
|
|
||||||
`ExtractedUpdateOwners_DoNotRetainAnonymousCallbacks` rejected an
|
|
||||||
`Action<float>` frame hook and forced the typed `IAmbientFramePhase`.
|
|
||||||
|
|
||||||
**A4 correction (2026-08-08, from a user question):** the enter cue was hung on the
|
|
||||||
`EnterTunnel` event — the first tunnel-family frame — instead of the sequencer's
|
|
||||||
`PlayEnterSound`, which is `Begin()` and is what retail's
|
|
||||||
`BeginTeleportAnimation` @ `0x004D638E` plays. That delayed it by a whole
|
|
||||||
TunnelFadeIn. Both cues now fire on the sequencer's own dedicated sound events
|
|
||||||
(`PlayEnterSound` had been emitted and dropped by every consumer since R6), and
|
|
||||||
`PortalCues_FireOnTheSequencersOwnSoundEvents_NotOnTheTunnelVisuals` pins the
|
|
||||||
moments. The exit cue was already correct.
|
|
||||||
|
|
||||||
**Listening-gate round 2 (2026-08-08, inn-chatter finding):** interior
|
|
||||||
soundscapes (inn talk-and-laughter) are NOT dat-authored — proven by three
|
|
||||||
installed-dat scans pinned in `EnvCellSoundEmitterInventoryTests`: no interior
|
|
||||||
static carries an ambient-slot table, no Setup in the whole portal dat
|
|
||||||
references one, yet 23 ambient-only soundscape banks exist. They are
|
|
||||||
WIRE-BOUND: the server attaches them to emitter objects and fires the slots
|
|
||||||
over 0xF750 (ACE: `EmoteType.Sound` heartbeat emotes). Our A3 path is the
|
|
||||||
receiver and is live; a probed session (`ACDREAM_PROBE_SOUND_WIRE=1`)
|
|
||||||
received ZERO 0xF750 events across a town walkabout, so the silence is ACE
|
|
||||||
world-content, not a client drop. Also added this round: `Ctrl+M` instant
|
|
||||||
mute (`AcdreamToggleAudioMute` → AL listener gain, unused since A2 — silences
|
|
||||||
playing voices immediately without touching the retail mixing math or any
|
|
||||||
persisted setting).
|
|
||||||
|
|
||||||
**Still owed:** the user listening gate (A2 falloff, A4 cues, A5 ambients) and
|
|
||||||
the connected gates for A3/A4. Open rows: AP-173, AP-174, TS-64, TS-65, TS-66,
|
|
||||||
TS-67, TS-9 (re-scoped), #321.
|
|
||||||
|
|
@ -1,445 +0,0 @@
|
||||||
# Campaign OP — retail four-tab Options panel
|
|
||||||
|
|
||||||
> **For agentic workers:** slices are executed by ONE Sonnet implementer at a
|
|
||||||
> time against this contract, then dual-lens Opus-reviewed, per the binding
|
|
||||||
> process rules in §8. The four research docs in §1 are the spec's data
|
|
||||||
> appendix — implementers MUST read the cited sections before coding; every
|
|
||||||
> table this plan references by section number is committed there in full.
|
|
||||||
|
|
||||||
**Status: CODE-COMPLETE 2026-08-11, PARKED 2026-08-11 (user direction) —
|
|
||||||
all nine slices landed and reviewed; OP1/OP2/OP7/OP9 CLOSED; OP3–OP6 + OP8
|
|
||||||
owe their connected user gates.** Four connected gate rounds ran before
|
|
||||||
parking; every finding was root-caused and fixed same-day: #372
|
|
||||||
(blank tabs + click diagnostics), #374 (dropdown pointer routing; #376
|
|
||||||
split out), #375 (keyboard string resolver + parked prototypes + tab
|
|
||||||
activation), #371 (viewport clip), the AD-78 store-only dimming
|
|
||||||
(user-directed), and the gate-4 batch #378–#382 (dropdown chrome, chat-only
|
|
||||||
opacity scope, slider captions from DAT catalog 0x78000000, the AP-205
|
|
||||||
footer field, the AP-206 state-cascade fix). **Resume point: the gate-4
|
|
||||||
fixes (`c1218426`..`d1c60df9`) are committed and probe-verified but have
|
|
||||||
NOT been seen by human eyes — the user's re-check of those five plus the
|
|
||||||
full §OP3–§OP6/§OP8 script is the remaining work. Launch with
|
|
||||||
`ACDREAM_RETAIL_UI=1`.** Gate script:
|
|
||||||
`docs/research/2026-08-11-campaign-op-test-script.md`. Open tail: #373,
|
|
||||||
#376, #377 (fullscreen startup crash — settings.json workaround noted in
|
|
||||||
the issue), AP-198/199/202/203/205/206, the Shift-chord display cosmetics
|
|
||||||
and the radar-text-over-panel z-order noted in session.**
|
|
||||||
|
|
||||||
**Goal:** retail's four-tab in-game Options panel (Gameplay Options /
|
|
||||||
Character / Chat / Config), retail-faithful mechanics end-to-end: authored
|
|
||||||
LayoutDescs, the real option storage/wire split (`0x0005` single-option vs
|
|
||||||
the batched `0x01A1` PlayerModule blob), retail Apply/Reset/Defaults
|
|
||||||
semantics, live consumers where acdream has the subsystem and honest
|
|
||||||
store-only + register rows where it doesn't, plus the headless-bot
|
|
||||||
`characterOptions` seam. Campaign handoff:
|
|
||||||
`docs/research/2026-08-10-settings-track-handoff.md`.
|
|
||||||
|
|
||||||
**Architecture:** the panel is retained retail UI exactly like Campaign CH's
|
|
||||||
chat windows — LayoutDesc `0x2100002B` imported through `LayoutImporter`,
|
|
||||||
mounted in the `gmFloatyPanelUI` host `0x2100006E` under
|
|
||||||
`RetailWindowManager`, driven by a focused controller. All option
|
|
||||||
storage/policy is Runtime-owned (`RuntimeCharacterOptionsState` widened to
|
|
||||||
the full retail table); the graphical panel and the headless host are both
|
|
||||||
consumers of the one generation-gated `IRuntimeCharacterCommands` seam
|
|
||||||
(CH3's precedent). Wire builders live in `AcDream.Core.Net` beside the
|
|
||||||
existing `0x0005` codec.
|
|
||||||
|
|
||||||
**Tech stack:** existing retained UI stack (`DatWidgetFactory`,
|
|
||||||
`LayoutImporter`, `UiRoot`, `RetailWindowManager`), `AcDream.Runtime`
|
|
||||||
gameplay owners, `AcDream.Core.Net` message builders, `DatCollection` for
|
|
||||||
DAT reads. No new dependencies.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Research base (committed; the spec's data appendix)
|
|
||||||
|
|
||||||
| Doc | What it pins |
|
|
||||||
|---|---|
|
|
||||||
| `docs/research/2026-08-10-options-panel-structure.md` (lane A) | Layout `0x2100002B` structural inventory (§10.1), tab table property `0x2E`, row-template mechanism (ListBox `P0x64` + `AddItemFromTemplateList`), Apply/Reset/Defaults + visibility semantics (§6, §10.4), Chat tab's 13 checkbox masks + defaults (§8), Config tab's 27 rows + `UserPreferences.ini` keys (§9), open path (F11 action `0x1000001A`, toolbar button `0x1000019B`) |
|
|
||||||
| `docs/research/2026-08-10-character-options-map.md` (lane B) | The 50-row / 6-group Character-tab inventory with per-row storage bit, wire route, ACE handling, acdream consumer state (§2–§5); implement-vs-store split (§7.1); bot tiers (§5.2) |
|
|
||||||
| `docs/research/2026-08-10-set-character-options-wire.md` (lane C) | The `0x01A1` body = `PlayerModule::Pack` field order (§2.3–§2.7), header invariant `0x460`, the 21-id auto-save table (§3.2), 480 s timer + logout + Apply flush triggers (§3.3–§3.5), ACE acceptance/landmines (§5), CH3 builder post-mortem (§6) |
|
|
||||||
| `docs/research/2026-08-10-keyboard-config-and-gameplay-tab.md` (lane D) | The seven Gameplay-tab button behaviours with byte-verified strings (§1–§4), `gmKeyboardUI` structure + DAT ActionMap storage (§5–§6), per-button implementability (§7.1), Config-tab ordered dump (§7.3) |
|
|
||||||
|
|
||||||
Coordinator-verified during planning (this session): toolbar button
|
|
||||||
`0x1000019B` authors `P0x12 = 0x1000001A` (committed fixture);
|
|
||||||
`retail-default.keymap.txt:148` binds `ToggleOptionsPanel` to `DIK_F11`;
|
|
||||||
`PlayerModulePackHeader` verbatim at `acclient.h:7835`; the headless
|
|
||||||
local-write gap at `src/AcDream.Runtime/Session/DirectGameRuntimeCommandAdapter.cs`
|
|
||||||
(`SetSingleOption`) vs `src/AcDream.App/Net/LiveSessionRuntimeFactory.cs:347`;
|
|
||||||
**U1 closed**: `UIOption::InqDefaultGameplayOptionProperty @0x004ef8d0`
|
|
||||||
resolves per-option defaults from the DAT `DBPropertyCollection` at
|
|
||||||
`DBCache::GetDIDFromEnumStatic(0x16, 2)` — the Defaults button restores
|
|
||||||
DAT-authored values.
|
|
||||||
|
|
||||||
## 2. Design decisions (stated, per the campaign directive; reactable at gates)
|
|
||||||
|
|
||||||
- **D1 — the retail Options panel is acdream's one in-client settings
|
|
||||||
surface.** Lane D established the F11 `SettingsPanel` was never rendered
|
|
||||||
post-V11 (`ToggleSettingsPanel()` no-op; only `IPanelRenderer` is a test
|
|
||||||
fake). Retail's own F11 IS `ToggleOptionsPanel`. So: F11 + the toolbar
|
|
||||||
button open THIS panel; acdream's client-only settings live on the
|
|
||||||
**Config tab** (retail's own client-settings tab — its 27 rows are
|
|
||||||
`UserPreferences.ini` preferences, nothing on the wire), backed by
|
|
||||||
acdream's existing settings store. The old `SettingsPanel`/`SettingsVM`
|
|
||||||
IPanel surface is retired in OP9; its tested keybind model feeds OP8.
|
|
||||||
- **D2 — retail's wire split ships exactly.** The 21 auto-save ids send
|
|
||||||
`0x0005` immediately; the rest dirty the module and ride the real
|
|
||||||
`0x01A1` blob with retail's three flush triggers (Apply, logout, 480 s
|
|
||||||
timer). No "send everything as 0x0005" shortcut (lane C: the split is
|
|
||||||
load-bearing in both directions).
|
|
||||||
- **D3 — the 50th Character row ships.** "Listen to PK death messages" is
|
|
||||||
2015-client; the DAT string exists (`0x0D16E9A3`), ACE maps id `0x34` ↔
|
|
||||||
`CharacterOptions2 0x02000000` but never reads it. Ship the row
|
|
||||||
(user-gate axiom: the user's retail memory includes it), wire+store only,
|
|
||||||
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.
|
|
||||||
- **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
|
|
||||||
emits the button's own byte-verified failure notice through the
|
|
||||||
interface-text seam (retail text, not invented), register rows filed.
|
|
||||||
In-Game Help mirrors retail-with-missing-`ACHelpPlugin.dll` behaviour.
|
|
||||||
- **D6 — Exit to Character Selection behaves as Exit Game** (+ register
|
|
||||||
row) until a pre-world character-select flow exists, but retail's
|
|
||||||
confirmation dialog (`ID_Client_EndCharacterSessionConfirm`) and mid-air
|
|
||||||
refusal ("Cannot log off while in mid-air.", byte-verified) ship now.
|
|
||||||
- **D7 — Group C re-points to server truth.** The seven currently-live
|
|
||||||
`settings.json`-backed options (lane B §7.1 group C) become
|
|
||||||
server-bit-authoritative per CH3 precedent (reseed from
|
|
||||||
PlayerDescription; local-write-then-send). `GameplaySettings`' 16
|
|
||||||
server-shadowing booleans die in OP9.
|
|
||||||
- **D8 — bots declare options by NAME.** The K1 strict config gains an
|
|
||||||
optional `characterOptions` block accepting exactly the lane-B tier-1+2
|
|
||||||
names; unknown names fail load (ACE throws on unknown ids — never let one
|
|
||||||
reach the wire). Diff-then-send after PlayerDescription seeds state;
|
|
||||||
idempotent on reconnect.
|
|
||||||
|
|
||||||
## 3. Slice map
|
|
||||||
|
|
||||||
Dependencies: OP1 → (OP4, OP7); OP2 → (OP3, OP4, OP5, OP6, OP8); OP3 → OP4/5/6
|
|
||||||
(the shell hosts the tabs). OP7 needs only OP1. Execution order below is the
|
|
||||||
default; OP7 may run any time after OP1 when the tree is free.
|
|
||||||
|
|
||||||
| Slice | Contract (summary) | Gate |
|
|
||||||
|---|---|---|
|
|
||||||
| OP1 | Runtime option map + dirty model + the real `0x01A1` builder + headless local-write fix | automated only |
|
|
||||||
| OP2 | Type 8 tab control + Type 5 template-list ListBox + remaining `UIOption_*` widget mappings + fixtures | automated only |
|
|
||||||
| OP3 | Panel shell + open paths + Gameplay tab end-to-end | user (connected) |
|
|
||||||
| OP4 | Character tab: 50 rows, consumers, Apply/Reset/Defaults | user (connected) |
|
|
||||||
| OP5 | Chat tab: opacity sliders + 5 per-window filter blocks | user (connected) |
|
|
||||||
| OP6 | Config tab: 27 rows over the client settings store | user (connected) |
|
|
||||||
| OP7 | Headless `characterOptions` block | automated + bot-vs-ACE run |
|
|
||||||
| OP8 | Configure Keyboard screen | user (connected) |
|
|
||||||
| OP9 | Closeout: retire dead surfaces, bookkeeping, test script | user (final matrix) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Slice contracts
|
|
||||||
|
|
||||||
### OP1 — the Runtime option map, dirty model, and the real blob
|
|
||||||
|
|
||||||
**Files.**
|
|
||||||
- Modify `src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs`
|
|
||||||
(`RuntimeCharacterOptionsState`, `:628`): widen to the full table.
|
|
||||||
- Create `src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs`: the ONE
|
|
||||||
typed table `PlayerOption id → (word: Options1|Options2, mask, isAutoSave,
|
|
||||||
clientDefault)` for ids `0x00..0x34`, transcribed from lane B §2 (verbatim
|
|
||||||
`acclient.h:4162` enum names) + lane C §3.2 (auto-save column) + D3's
|
|
||||||
`0x34` row. Reject `0x35`/`0x36` and unknown ids at the seam (lane C
|
|
||||||
§5.4.3 — ACE throws).
|
|
||||||
- Modify `src/AcDream.Runtime/GameRuntimeCommands.cs`: add
|
|
||||||
`IRuntimeCharacterCommands.SaveOptions(RuntimeGenerationToken)` (the
|
|
||||||
blob-flush verb) beside `SetSingleOption`.
|
|
||||||
- Modify `src/AcDream.Runtime/Session/DirectGameRuntimeCommandAdapter.cs`
|
|
||||||
and `src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs` +
|
|
||||||
`src/AcDream.App/Net/LiveSessionRuntimeFactory.cs:335-350`: move
|
|
||||||
local-write-then-send INTO the shared Runtime seam so BOTH hosts get
|
|
||||||
retail's ordering (fixes the headless gap lanes B+C independently found).
|
|
||||||
The `LiveSessionRuntimeFactory` local closure is deleted, not duplicated.
|
|
||||||
- Create `src/AcDream.Core.Net/Messages/SocialActions.cs` addition:
|
|
||||||
`BuildSetCharacterOptions(...)` per lane C §2.7 EXACTLY — header always
|
|
||||||
`0x460` OR-ed with present optional sections; four unconditional u32s
|
|
||||||
(header, options1, spellbookFilters, options2 in pack order §2.3); echo
|
|
||||||
last-parsed shortcuts / 8 spell lists / desired comps rather than zeroing
|
|
||||||
(§5.3); NEVER set `0x100` (lane C U2); omit `0x200` while acdream packs
|
|
||||||
nothing (safe per §2.5); 4-byte tail pad.
|
|
||||||
- Modify `src/AcDream.Core.Net/WorldSession.cs`: `SendSetCharacterOptions`.
|
|
||||||
- Dirty model in `RuntimeCharacterOptionsState`: `MarkDirty` on any
|
|
||||||
non-auto-save change, `FirstDirtiedAt`, flush triggers = explicit
|
|
||||||
`SaveOptions` command, session logout, and the 480 s timer (retail
|
|
||||||
constant, lane C §3.3) driven from the existing Runtime tick.
|
|
||||||
- Tests: `tests/AcDream.Runtime.Tests/` (table completeness ×53, auto-save
|
|
||||||
split ×53 vs lane B §2's byte-verified column, local-write-then-send on
|
|
||||||
BOTH adapter paths, dirty/flush state machine, unknown-id rejection);
|
|
||||||
`tests/AcDream.Core.Net.Tests/` blob conformance: golden byte vector +
|
|
||||||
round-trip through `PlayerDescriptionParser` (lane C §9 S-c — the CH3
|
|
||||||
builder died of green tests pinning a wrong shape; the golden vector is
|
|
||||||
non-negotiable).
|
|
||||||
|
|
||||||
**Register rows (same commit):** `0x34` mapping ACE-sourced (D3); the
|
|
||||||
480 s autosave if any part is deferred (target: not deferred);
|
|
||||||
`GetDefaultOptionValue @0x005D2A30` vs ctor-default disagreement recorded
|
|
||||||
when the client-default column lands (lane C §8.2 — reproduce, don't fix).
|
|
||||||
|
|
||||||
**Acceptance:** build + FULL Release suite green; blob golden vector
|
|
||||||
byte-exact; both adapters share one code path for the local write.
|
|
||||||
|
|
||||||
### OP2 — the two widget primitives + remaining UIOption mappings
|
|
||||||
|
|
||||||
**Files.**
|
|
||||||
- Modify `src/AcDream.App/UI/Layout/LayoutImporter.cs` +
|
|
||||||
`src/AcDream.App/UI/Layout/ElementReader.cs`: read tab-table property
|
|
||||||
`0x2E` (struct array `{0x30 button, 0x31 page, 0x32 isDefault}`) and
|
|
||||||
ListBox template-list property `0x64` (entries `{0x63 layout DID,
|
|
||||||
0x62 element id}`) into `ElementInfo`.
|
|
||||||
- Create `src/AcDream.App/UI/UiTabPanel.cs` (element Type 8 — retail
|
|
||||||
`UIElement_Panel`; renamed from this plan's original `UiTabControl` at the
|
|
||||||
OP2 rework, and a dormant `UiDatElement` subclass per AD-73): tab-button
|
|
||||||
↔ page-slot switching per lane A §5; default tab honoured.
|
|
||||||
- Create `src/AcDream.App/UI/UiTemplateListBox.cs` (element Type 5 with
|
|
||||||
authored template list): `AddItemFromTemplateList(index)` instantiates a
|
|
||||||
row from the authored template layout/element via `DatWidgetFactory`,
|
|
||||||
scrollbar named by `P0x72`.
|
|
||||||
- Modify `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`: map Types 5/8 and
|
|
||||||
`UIOption_Slider 0x10000037`, `UIOption_Menu 0x10000038`,
|
|
||||||
`UIOption_CheckboxSlider 0x10000036`, `UIOption_CheckboxBitfield64
|
|
||||||
0x10000044` (LED checkbox `0x10000035` already maps to `BuildCheckbox`).
|
|
||||||
- Modify `tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs`
|
|
||||||
(`Layouts`, `:17`): add `0x2100002B`, `0x2100002A`, `0x21000028`,
|
|
||||||
`0x2100005C`, `0x21000029`. **Coordinator runs the generator**
|
|
||||||
(`ACDREAM_REGENERATE_UI_FIXTURES=1`, serial tree) and commits fixtures.
|
|
||||||
- Conformance tests pin: the tab table (4 entries, Gameplay default), all
|
|
||||||
three template arrays (lane A §10.1), the Character ListBox's 6-header /
|
|
||||||
49-toggle authored row build, scrollbar linkage.
|
|
||||||
|
|
||||||
**Acceptance:** build + FULL suite; fixtures committed and pinned; no
|
|
||||||
change to any existing widget's behaviour (the S2/AP-192 outline seams from
|
|
||||||
`aa6635ae` must be preserved in new widget builds).
|
|
||||||
|
|
||||||
### OP3 — panel shell + open paths + Gameplay tab (first vertical)
|
|
||||||
|
|
||||||
**Files.**
|
|
||||||
- Create `src/AcDream.App/UI/Layout/OptionsPanelController.cs`: mounts
|
|
||||||
`0x2100002B` in host `0x2100006E` slot `0x1000018D` (stack key 10) via
|
|
||||||
`RetailWindowManager`; tab control wiring; close button fires
|
|
||||||
`0x1000001A`.
|
|
||||||
- Create `src/AcDream.App/UI/Layout/OptionPageModel.cs`: the
|
|
||||||
`OptionPage`/`PlayerOptionPage` model — per-page option array of
|
|
||||||
`(current, saved, default)` triples + verbs Apply/Reset/Defaults with
|
|
||||||
retail's exact semantics (lane A §10.4): LED click applies immediately
|
|
||||||
(`SetCurrentValue → Apply(1)`); Apply commits baseline + `SaveOptions`
|
|
||||||
flush; Reset reverts to baseline; Defaults applies live without
|
|
||||||
committing and is never disabled; Apply/Reset disable when clean; hide →
|
|
||||||
revert uncommitted; show → apply + commit. Pure logic, unit-tested
|
|
||||||
without DAT.
|
|
||||||
- Input action `ToggleOptionsPanel` (`0x1000001A`, F11) in
|
|
||||||
`src/AcDream.UI.Abstractions/Input/` (`KeyBindings.RetailDefaults()` —
|
|
||||||
the ONLY production table; #358's lesson) +
|
|
||||||
`src/AcDream.App/Input/GameplayInputCommandController.cs` routing +
|
|
||||||
toolbar button `0x1000019B` (already authors `P0x12`).
|
|
||||||
- Gameplay tab (`0x2100002A`, class `gmGameplayOptionsUI`), seven buttons
|
|
||||||
per D5/D6 and lane D §1: Exit Game → the existing graceful-close path;
|
|
||||||
Exit to Char Selection → retail confirm dialog (`RetailDialogFactory`) +
|
|
||||||
mid-air refusal via the interface-text seam, then D6's Exit-Game
|
|
||||||
behaviour; Configure Keyboard → opens OP8's screen; until OP8 lands the
|
|
||||||
button is INERT (authored, clickable, no handler — no invented text, no
|
|
||||||
stub screen), the OP3 gate script says so explicitly, and OP8's gate
|
|
||||||
re-tests it (the campaign cannot close with the button inert); Use Mouse Turning Settings → the six-option macro
|
|
||||||
(lane D §4.4) with its six retail chat lines, camera-mode consumer
|
|
||||||
verified against the camera digest in-slice (register row if the mode is
|
|
||||||
absent); In-Game Help / Urgent Assistance / Report Abuse per D5.
|
|
||||||
|
|
||||||
**Register rows:** Exit-to-char-select adaptation (D6); UA/RA dead-URL
|
|
||||||
short-circuit (D5); help-plugin behaviour (D5); mouse-turning consumer row
|
|
||||||
if needed.
|
|
||||||
|
|
||||||
**Gate:** connected — panel opens via F11 AND toolbar; tabs switch with
|
|
||||||
Gameplay default; the seven buttons behave per contract; window drags /
|
|
||||||
resizes / stacks like the CH6 floaties.
|
|
||||||
|
|
||||||
### OP4 — the Character tab
|
|
||||||
|
|
||||||
**Files.**
|
|
||||||
- Create `src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs`:
|
|
||||||
binds layout `0x21000028` root `0x100001F9` ListBox `0x100001FA` through
|
|
||||||
OP2's template mechanism; 6 headers + 50 toggles (D3) in lane B §2's
|
|
||||||
authored order; every row bound by `PlayerOption` id through OP1's table
|
|
||||||
and the shared seam (auto-save ids → `0x0005` on click-apply; batched ids
|
|
||||||
→ dirty + blob per OP1).
|
|
||||||
- Defaults: extract the DAT `DBPropertyCollection`
|
|
||||||
(`GetDIDFromEnumStatic(0x16, 2)`, §1 U1-closure) via `DatCollection` at
|
|
||||||
import; conformance-pin extracted values; cross-check overlapping ids
|
|
||||||
against lane B/C's byte-verified default words and RECORD any
|
|
||||||
disagreement as a finding (never silently pick).
|
|
||||||
- Consumers (lane B §7.1): group B one-line binds — timestamps + filter
|
|
||||||
language at `RuntimeCommunicationState.AddText`; daylight
|
|
||||||
(`ForcedDayGroupIndex`), weather, fog; run-as-default in
|
|
||||||
`RuntimeLocalPlayerMovementState`; main-pack default at the pickup path;
|
|
||||||
the UI-display bits at their existing retained controllers. Group C
|
|
||||||
re-pointing per D7. Group A wire+store only. Group D register rows.
|
|
||||||
- Conformance test: all 50 rows ↔ table ↔ storage bit ↔ wire route pinned
|
|
||||||
in both directions (the CH4 registry-conformance pattern — an invented
|
|
||||||
row or a dropped row fails the build).
|
|
||||||
|
|
||||||
**Register rows:** group D deferrals (salvage, housing, fellowship-share
|
|
||||||
field, PK-deaths already rowed in OP1, mouse-turning row lives in OP3);
|
|
||||||
each group-C re-point that changes an observable default.
|
|
||||||
|
|
||||||
**Gate:** connected — LED rows toggle + persist across relogin (server
|
|
||||||
echo), timestamps/daylight/fog/weather/run-default observably switch, Apply
|
|
||||||
/ Reset / Defaults exercise retail semantics, tab-switch reverts
|
|
||||||
uncommitted edits.
|
|
||||||
|
|
||||||
### OP5 — the Chat tab
|
|
||||||
|
|
||||||
**Files.** Create
|
|
||||||
`src/AcDream.App/UI/Layout/ChatOptionsPageController.cs` binding
|
|
||||||
`0x2100005C` root `0x1000050A`: the two LINKED opacity sliders (bound to
|
|
||||||
the existing `RetailWindowOpacityController` values through
|
|
||||||
`ChatOpacityLink` — AP-190's model, now user-reachable) and the five
|
|
||||||
per-window filter blocks (SetUserData ids 8/2/3/4/5; main window 12 rows,
|
|
||||||
floaties 13 — lane A §8's byte-decoded masks) writing the per-window filter
|
|
||||||
state CH6 already consumes (`ChatWindowState.ShouldDisplay`). The
|
|
||||||
`0x1000008C` per-window blob stays local-only (already-anticipated register
|
|
||||||
row from `2026-08-09-chat-retail-window-shell.md` §6.3 — cite, don't
|
|
||||||
duplicate).
|
|
||||||
|
|
||||||
**Gate:** connected — filter checkboxes change window routing live;
|
|
||||||
opacity sliders drive the focus fade; settings survive relogin locally.
|
|
||||||
|
|
||||||
### OP6 — the Config tab
|
|
||||||
|
|
||||||
**Files.** Create
|
|
||||||
`src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` binding
|
|
||||||
`0x21000029` root `0x100001FF`: all 27 rows in lane A §9 / lane D §7.3's
|
|
||||||
authored order, backed by acdream's client settings store
|
|
||||||
(`%LOCALAPPDATA%\acdream\` — the D1 home). Rows with live subsystems bind
|
|
||||||
now: the three volume trios → the audio pipeline, "play sound only when
|
|
||||||
active", mouse-look sensitivity + invert Y, FOV, chat font size/face if the
|
|
||||||
chat pipeline exposes them. Rows without a subsystem (resolution +
|
|
||||||
fullscreen + sync, degrades, texture detail family, multi-pass alpha)
|
|
||||||
persist store-only under ONE register row enumerating them (the goal's
|
|
||||||
"honest store-only handling"); resolution's `SetConfirmChange` flow ships
|
|
||||||
whenever the consumer lands, not now.
|
|
||||||
|
|
||||||
**Gate:** connected — audio sliders audibly change mix; mouse sensitivity
|
|
||||||
observably changes; store-only rows persist across relaunch.
|
|
||||||
|
|
||||||
### OP7 — headless `characterOptions`
|
|
||||||
|
|
||||||
**Files.** Modify
|
|
||||||
`src/AcDream.Headless/Configuration/HeadlessConfiguration.cs` +
|
|
||||||
`HeadlessConfigurationLoader.cs`: optional `characterOptions` block, strict
|
|
||||||
— keys are exactly the lane B §5.2 tier-1+2 option NAMES, values bool;
|
|
||||||
unknown key = load failure (D8). Modify the session host
|
|
||||||
(`src/AcDream.Headless/Hosting/`) to diff declared vs
|
|
||||||
PlayerDescription-seeded state after LoginComplete and send changes through
|
|
||||||
the shared seam (auto-save ids as `0x0005`, remainder via one
|
|
||||||
`SaveOptions` blob), honouring #368's one-dedicated-update-thread contract.
|
|
||||||
Idempotent on reconnect (retail itself no-ops unchanged options — lane C
|
|
||||||
§3.5). Headless tests: schema rejection, diff-only sends, reconnect
|
|
||||||
idempotence, thread affinity preserved.
|
|
||||||
|
|
||||||
**Gate:** automated + one live bot-vs-local-ACE run (no graphical client)
|
|
||||||
showing declared options land and survive reconnect.
|
|
||||||
|
|
||||||
### OP8 — Configure Keyboard
|
|
||||||
|
|
||||||
**Files.** Create
|
|
||||||
`src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (+ a
|
|
||||||
`src/AcDream.Core/...` DAT `ActionMap` (DBO type `0x27`) reader if
|
|
||||||
`DatCollection` lacks one): `gmKeyboardUI`'s six ActionClass list boxes
|
|
||||||
via OP2's Type 5 widget, rows = label + tooltip + N key buttons + Clear
|
|
||||||
from the DAT master maps (DIDs `0x14000000`/`0x14000002` — the exact
|
|
||||||
enum→DID pairing is lane D unknown #4, resolved in-slice by dumping both),
|
|
||||||
merged with live `KeyBindings`; left-click key button → `InputDispatcher`
|
|
||||||
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.
|
|
||||||
|
|
||||||
**Gate:** connected — rebind a movement key, conflict prompt on a taken
|
|
||||||
chord, persistence across relaunch, reset restores retail defaults.
|
|
||||||
|
|
||||||
### OP9 — closeout
|
|
||||||
|
|
||||||
- Retire `SettingsPanel`/`SettingsVM`'s IPanel surface +
|
|
||||||
`SettingsDevToolsComposition` wiring + `DevToolsGameplayCommands`
|
|
||||||
no-ops; delete `GameplaySettings`' 16 server-shadowed booleans (lane B
|
|
||||||
§7.2.5), re-scoping true client-only settings into the D1 store. Every
|
|
||||||
deletion checked against consumers; no behaviour regression.
|
|
||||||
- Bookkeeping: plan status flips, ISSUES sweep (#358 retest against OP8's
|
|
||||||
screen — its Ctrl+M lesson lives in `RetailDefaults()`), register
|
|
||||||
reconciliation, CLAUDE.md Current-state paragraph (per
|
|
||||||
`feedback_claude_md_staleness`), memory digest update
|
|
||||||
(`project_chat_digest` addendum or a new settings digest).
|
|
||||||
- Write `docs/research/2026-08-10-campaign-op-test-script.md`: the
|
|
||||||
connected-gate script covering every OP3–OP8 gate item, per-tab, with
|
|
||||||
expected retail behaviours — the campaign's stop condition is this
|
|
||||||
script ready plus all slices code-complete.
|
|
||||||
|
|
||||||
**Gate:** the user's final connected matrix (their eyes, their pace).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. What is explicitly OUT of scope
|
|
||||||
|
|
||||||
- Packing the `0x200` GameplayOptions blob section (per-window chat state
|
|
||||||
on the wire) — a follow-on (CH6f shape), pre-anchored by lane C U1 and
|
|
||||||
`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).
|
|
||||||
- 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).
|
|
||||||
|
|
||||||
## 6. Verification discipline
|
|
||||||
|
|
||||||
Per commit: `dotnet build` + FULL Release suite green (baseline at plan
|
|
||||||
time: 12,611 / 4 skips / 0 failures at `29138430`+). Golden byte vectors
|
|
||||||
for every wire builder. Conformance pins for every authored inventory
|
|
||||||
(row lists, template arrays, tab table, checkbox masks). Register rows in
|
|
||||||
the SAME commit as the deviation. No user-visible placeholder text ever —
|
|
||||||
all user-facing strings resolve from the DAT string tables
|
|
||||||
(`0x23000001`/`0x23000003`) by `compute_str_hash` name, never hard-coded
|
|
||||||
English.
|
|
||||||
|
|
||||||
## 7. Review protocol
|
|
||||||
|
|
||||||
Dual-lens Opus review per slice (mechanism-faithfulness lens ×
|
|
||||||
regression/blast-radius lens), fixes applied by the implementer; REJECT →
|
|
||||||
focused re-review; TWO failures → Fable fixes directly (user-directed
|
|
||||||
2026-08-10). Review findings that feed both a fixer and a re-reviewer are
|
|
||||||
persisted to a committed findings doc BEFORE dispatch.
|
|
||||||
|
|
||||||
## 8. Process rules (binding, inherited from Campaign CH via the handoff)
|
|
||||||
|
|
||||||
Max 3–4 agents in parallel INCLUDING children; every agent prompt carries
|
|
||||||
an explicit no-subagent clause; ONE builder/tester on the tree at a time
|
|
||||||
(read-only research may overlap); agents never launch the graphical
|
|
||||||
client; the user runs all connected gates; screenshots transcribed into
|
|
||||||
docs immediately; ledger placeholders anchored per-row, post-amend SHAs
|
|
||||||
recorded by the coordinator; decomp claims byte-verified against the
|
|
||||||
PDB-paired binary (`check_exe_pdb.py` MATCH first); stalled agents resumed
|
|
||||||
via SendMessage before any redo; agent claims spot-verified at the seams
|
|
||||||
before anything builds on them.
|
|
||||||
|
|
||||||
## 9. Ledger
|
|
||||||
|
|
||||||
| Slice | Status | Commit(s) | Review | Gate |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| OP1 | CLOSED | `86c0a7e0` + fixes `09029f9f` + residuals `6f48e341` | dual APPROVE-WITH-FIXES (`2026-08-10-op1-review-{mechanism,blast}.md`) → re-review CLOSED (`2026-08-11-op1-rereview.md`); residuals R1/R2/R3 landed | automated only — n/a |
|
|
||||||
| OP2 | CLOSED | `df9c7a35` (REJECTED) → rework `b236a442` → closure (this commit) | dual REJECT (`2026-08-11-op2-review-{mechanism,blast}.md`) → re-review blast CLOSED / mechanism REOPEN-on-one (`2026-08-11-op2-rereview-{mechanism,blast}.md`) → coordinator closure: AP-195 filed, AD-73 addendum, tooltip port, zero-children pin | automated only — n/a |
|
|
||||||
| OP3 | CODE-COMPLETE, gate READY | `9d26ecc6` → fixes `386076af` → residuals `cb334690` | dual APPROVE-WITH-FIXES (`2026-08-11-op3-review-{mechanism,blast}.md`) → re-review REOPEN-narrow (`2026-08-11-op3-rereview.md`) → coordinator residuals landed | connected gate OWED (script §OP3) |
|
|
||||||
| OP4 | CODE-COMPLETE, gate READY | `22b86b9f` → fixes `bc43fb1d` → residuals `ac0304dc` | dual APPROVE-WITH-FIXES (`2026-08-11-op4-review-{mechanism,blast}.md`) → re-review REOPEN-narrow (`2026-08-11-op4-rereview.md`) → coordinator residuals landed | connected gate OWED (script §OP4) |
|
|
||||||
| OP5 | CODE-COMPLETE, gate READY | `e71e5a96` (AP-195 retired) → fixes `6d0b0f92` → residuals `67b0815c` | combined APPROVE-WITH-FIXES (`2026-08-11-op5-review.md`) → re-check CLOSED (`2026-08-11-op5-recheck.md`) → coordinator drag residuals landed | connected gate OWED (script §OP5) |
|
|
||||||
| OP6 | CODE-COMPLETE, gate READY | `f5ac1742` (REJECTED) → rework `472525b9` → doc residuals (coordinator) | REJECT (`2026-08-11-op6-review.md`) → re-review CLOSED, all six caption sites byte-decoded (`2026-08-11-op6-rereview.md`) | connected gate OWED (script §OP6) |
|
|
||||||
| OP7 | CLOSED | `09cb548a` → fixes in `7b60e71b` (shared commit, see its message) | combined-lens APPROVE-WITH-FIXES (`2026-08-11-op7-review.md`); all nine findings closed | live bot-vs-ACE gate PASSED 2026-08-11 (coordinator; evidence in script §OP7) |
|
|
||||||
| OP8 | CODE-COMPLETE, gate READY | `b4edee97` (REJECTED) → rework `b1968ce9` → residuals `f1d50207` → merge `1c5cd969` | dual REJECT (`2026-08-11-op8-review-{mechanism,blast}.md`) → rework → re-review REOPEN-narrow (`2026-08-11-op8-rereview.md`) → coordinator round-2 residuals (inert-row conflict exclusion, DAT-default display, injectivity pin; #373 filed) | connected gate OWED (script §OP8) — merged onto the campaign tip AFTER `057d8cd7` per the re-review's merge note, so the #372 viewport fix covers OP8's six ListBoxes |
|
|
||||||
| OP9 | CLOSED | `371197a3` → review residuals `07f2b3f7` | combined-lens APPROVE-WITH-FIXES (`2026-08-11-op9-review.md`, `289bf5bc`): MUST-FIX 1 (SaveAudio→ApplyAudio live-apply lost its only assertion — restored + failure-ordering pin), SF-2 (code-structure.md seam list), SF-3 (dead residues: private SaveCharacter, ISettingsStorage.SaveCharacter, IngressShutdownRoots.Settings — deleted; SettingsStore's public SaveCharacter kept as the tested storage API), SF-4 (test-delta was −84 = 94 removed/10 added, not the commit message's "−80 exactly"; no live-behavior test in the gap besides MUST-FIX 1's), SF-5 (dangling comment), NIT 6 (AP-196 channel attribution corrected in-register) — all closed by coordinator residuals | closeout gate = the user's final connected matrix over §OP3–§OP8 |
|
|
||||||
|
|
@ -1,860 +0,0 @@
|
||||||
# Campaign LA — launcher / installer / updater + retail character-select
|
|
||||||
|
|
||||||
**Status:** ACTIVE (started 2026-08-14)
|
|
||||||
**Spec (approved):** `docs/superpowers/specs/2026-08-14-launcher-campaign-design.md`
|
|
||||||
**Memory crib:** `claude-memory/project_launcher_direction.md`
|
|
||||||
**Branch:** `claude/acdream-launcher-credentials-4d2f7c` (merge to main at coherent checkpoints)
|
|
||||||
|
|
||||||
Campaign LA ships the alpha launcher (Avalonia, Windows + Linux): triple-duty
|
|
||||||
launcher + installer + updater, ThwargLauncher-model profiles with full in-UI
|
|
||||||
CRUD, plaintext credential file (user-decided), file-contract orchestration of
|
|
||||||
`AcDream.App` and `AcDream.Headless`, plugins + login commands on both hosts,
|
|
||||||
the headless character probe, and the retail character-select screen (no
|
|
||||||
Create). All architectural decisions live in the spec — this plan sequences
|
|
||||||
the work.
|
|
||||||
|
|
||||||
## Process (binding)
|
|
||||||
|
|
||||||
- **Fable plans/sequences/integrates. Sonnet implements bounded slices. Opus
|
|
||||||
reviews at every slice boundary, dual-lens:** (a) architectural — ownership,
|
|
||||||
layering, dependency-guard integrity, seams; (b) retail fidelity vs
|
|
||||||
`docs/research/named-retail/` wherever the slice touches retail behavior.
|
|
||||||
Findings → fixes → narrow re-review.
|
|
||||||
- Max 3–4 agents in parallel including children; subagents never spawn
|
|
||||||
subagents; implementer prompts carry spec+plan paths, files-to-read,
|
|
||||||
acceptance criteria, commit style.
|
|
||||||
- `dotnet build` + `dotnet test` green before a slice is DONE; ≥1 commit per
|
|
||||||
slice tagged `Campaign LA`; retail deviations add their
|
|
||||||
`docs/architecture/retail-divergence-register.md` row in the same commit;
|
|
||||||
no workarounds without explicit user approval.
|
|
||||||
- Connected/visual gates are the ONLY stop-and-wait points; each gets an
|
|
||||||
exact script under `docs/research/` and non-blocked slices keep moving.
|
|
||||||
|
|
||||||
## Slice map
|
|
||||||
|
|
||||||
| Slice | Deliverable | Depends on |
|
|
||||||
|---|---|---|
|
|
||||||
| LA0 | `AcDream.Platform` extraction (`ApplicationPathSet`) + guard amendments | — |
|
|
||||||
| LA1 | Launch contract: App `--session-config` + stdin credential; status.jsonl writer both hosts; roster plumbing | LA0 |
|
|
||||||
| LA2 | Headless probe mode + `idle` policy | LA1 |
|
|
||||||
| LA3 | `AcDream.Launcher.Core`: profile store CRUD, config composition, spawn/supervise, status reader | LA0 (LA1 contract shapes) |
|
|
||||||
| LA4 | `AcDream.Launcher` Avalonia UI: CRUD views, per-char settings, sessions, probe action | LA3 |
|
|
||||||
| LA5 | Plugin hosting: headless `IPluginHost` + capability flag; session-driven plugin set both hosts | LA1 |
|
|
||||||
| LA6 | Login commands: parser-core extraction + execution on both hosts | LA1, LA5 |
|
|
||||||
| LA7 | Character-select: Runtime selection state + wire (delete/restore/error) + no-selector flow | LA1 |
|
|
||||||
| LA8 | Character-select authored retail screen (flat listbox — NO 3D preview, recon-corrected) | LA7 |
|
|
||||||
| LA9 | Installer: first-run wizard (DAT locate/validate, bake w/ progress, SHA record) | LA3, LA4 |
|
|
||||||
| LA10 | Updater: GitHub Releases manifest, download/verify/install/swap, self-update | LA3, LA4 |
|
|
||||||
| LA11 | Closeout: connected-gate script, roadmap/CLAUDE.md/memory, program ledger | all |
|
|
||||||
|
|
||||||
Parallelism guide: LA3/LA4 (launcher side) proceed alongside LA5–LA8 (client
|
|
||||||
side) — different assemblies, no shared files. LA9/LA10 close the launcher
|
|
||||||
side; LA11 closes the campaign.
|
|
||||||
|
|
||||||
## Linux posture (binding — user decision 2026-08-14)
|
|
||||||
|
|
||||||
Everything the launcher does must WORK ON LINUX in this campaign, except
|
|
||||||
GUI client launches: the Linux graphical client is Slice L, parked at L1,
|
|
||||||
resuming later ("ok we will do it later"). Concretely:
|
|
||||||
|
|
||||||
- **Linux-shipping in LA:** the Avalonia launcher UI, profile CRUD +
|
|
||||||
0600-permission file, installer (manual DAT picker — the auto-detect
|
|
||||||
paths are Windows-only; `acdream-bake` is GL-free and runs on Linux),
|
|
||||||
updater (staged swap; Linux can replace a running binary but keep the
|
|
||||||
same staged-atomic flow), headless launches with plugins + login
|
|
||||||
commands, and the character probe.
|
|
||||||
- **Launcher UX on Linux:** the `gui` / `guiSelect` launch modes render
|
|
||||||
disabled with an explicit "requires the Linux graphical client (Slice
|
|
||||||
L)" note — never a silent failure.
|
|
||||||
- **Per-slice enforcement:** every slice touching Launcher.Core, Headless,
|
|
||||||
Runtime, Bake, or Platform runs its test projects on Linux (native
|
|
||||||
Ubuntu or WSL, matching the K-slice practice) before the slice is DONE;
|
|
||||||
LA4/LA9/LA10 additionally prove a real `linux-x64` self-contained
|
|
||||||
publish. LA11's connected-gate script gets a Linux section: launcher on
|
|
||||||
Ubuntu doing CRUD, probe, headless launch with plugin + login commands,
|
|
||||||
first-run install with a manual DAT path, and an update swap.
|
|
||||||
- When Slice L later ships, the launcher's Linux GUI modes light up with
|
|
||||||
NO launcher changes (the session-config contract is host-agnostic) —
|
|
||||||
that expectation is part of LA's design acceptance.
|
|
||||||
|
|
||||||
## LA0 — `AcDream.Platform` extraction
|
|
||||||
|
|
||||||
New BCL-only project `src/AcDream.Platform/` holding `ApplicationPathSet` +
|
|
||||||
`IApplicationPathEnvironment` (today
|
|
||||||
`src/AcDream.Runtime/Platform/ApplicationPathSet.cs` — self-contained, no
|
|
||||||
intra-Runtime dependencies; clean cut). Runtime/App/Headless reference it.
|
|
||||||
|
|
||||||
Recon facts (2026-08-14): blast radius is the definition, six source files
|
|
||||||
(`GraphicalHostPlatformServices.cs`, `GraphicalLegacyConfigurationMigrator.cs`,
|
|
||||||
`App/Program.cs`, `GameWindow.cs:533`, `HeadlessPathSet.cs`,
|
|
||||||
`HeadlessPlatformEnvironment.cs`; two more files are doc-comment-only), two
|
|
||||||
test files (`ApplicationPathSetTests.cs` moves to a new
|
|
||||||
`tests/AcDream.Platform.Tests/`;
|
|
||||||
`GraphicalLegacyConfigurationMigratorTests.cs` fixtures), and the dependency
|
|
||||||
guards — CORRECTED post-review (the original recon here asserted the wrong
|
|
||||||
guard, the C4-closeout failure mode): the K0 Headless guard
|
|
||||||
(`HeadlessAssemblyReferencesOnlyTheRuntimeProject`) asserts HEADLESS's own
|
|
||||||
csproj reference list, which this move does not touch — it stays UNCHANGED;
|
|
||||||
the guard that actually needs amending is Runtime's own
|
|
||||||
`RuntimeDependencyBoundaryTests.RuntimeProjectDeclaresOnlyApprovedProjectDependencies`
|
|
||||||
(Runtime gains the `AcDream.Platform` reference), amended with a cited
|
|
||||||
comment in the same commit. Namespace stays `AcDream.Runtime.Platform`?
|
|
||||||
NO — rename to `AcDream.Platform` and fix the eight usings (clean naming beats
|
|
||||||
avoiding a mechanical edit). Register new projects in `AcDream.slnx`.
|
|
||||||
|
|
||||||
**Acceptance:** build + full test suite green; guard test asserts the new
|
|
||||||
exact reference set; launcher-side consumability proven by the LA3 project
|
|
||||||
referencing only `AcDream.Platform`.
|
|
||||||
|
|
||||||
## LA1 — launch contract (client side)
|
|
||||||
|
|
||||||
### Pinned launch-contract schema (v1, BINDING — committed per LA3 review)
|
|
||||||
|
|
||||||
This text is the single source of truth for the launcher↔host file
|
|
||||||
contract. Both host readers (LA1), the composer (LA3), and the probe
|
|
||||||
loader (LA2) implement EXACTLY this; any change is an amendment to THIS
|
|
||||||
section first, implementations second. The LA1+LA3 merge adds a
|
|
||||||
cross-assembly test feeding a composer-produced document to both host
|
|
||||||
loaders — that test is the seam's permanent enforcement.
|
|
||||||
|
|
||||||
Session-config document (System.Text.Json, camelCase,
|
|
||||||
`UnmappedMemberHandling.Disallow`, camelCase string enums):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"process": {
|
|
||||||
"content": { "datDirectory": "...", "preparedAssetPath": "..." }
|
|
||||||
},
|
|
||||||
"sessions": [{
|
|
||||||
"id": "sess-1",
|
|
||||||
"endpoint": { "host": "127.0.0.1", "port": 9000 },
|
|
||||||
"account": "testaccount",
|
|
||||||
"mode": "probe",
|
|
||||||
"character": { "id": 1342177290 },
|
|
||||||
"policy": { "id": "idle" },
|
|
||||||
"credential": { "provider": "standardInput", "reference": "session" },
|
|
||||||
"plugins": ["ExamplePlugin"],
|
|
||||||
"loginCommands": ["/vt start"],
|
|
||||||
"loginCommandDelayMs": 500,
|
|
||||||
"statusFile": ".../launcher/sessions/sess-1/status.jsonl"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Field rules:
|
|
||||||
- `process.paths` is OMITTED unless a caller genuinely supplies overrides
|
|
||||||
(never an empty object — the App reader has no `paths` member and
|
|
||||||
strict parsing rejects unknown keys; LA3 review finding 1).
|
|
||||||
- `mode`: ABSENT for normal play sessions; `"probe"` for the LA2 probe
|
|
||||||
(connect → characterList → graceful disconnect, no EnterWorld). The
|
|
||||||
headless loader accepts the field starting at LA2.
|
|
||||||
- `character`: exactly ONE of index|id|name; OMITTED entirely (not null)
|
|
||||||
for guiSelect and for probe sessions.
|
|
||||||
- `policy`: `{ "id": "idle" }` for headless play sessions ONLY; omitted
|
|
||||||
for gui/guiSelect/probe.
|
|
||||||
- `credential`: always `{ "provider": "standardInput", "reference":
|
|
||||||
"session" }` for launcher-composed configs.
|
|
||||||
- `plugins`: absent/null means load all discovered plugins (preserving the
|
|
||||||
developer flow); explicit `[]` means load none. Launcher-composed
|
|
||||||
normal-empty and probe sessions emit `[]` so they cannot load arbitrary
|
|
||||||
machine-local plugins.
|
|
||||||
- `loginCommands`/`loginCommandDelayMs`/`statusFile`: optional,
|
|
||||||
omitted-when-unset (never null, never `[]` for empty). Absent
|
|
||||||
`loginCommandDelayMs` means 500.
|
|
||||||
|
|
||||||
Status stream (`statusFile`, one JSON object per line, writer flushes per
|
|
||||||
line, writer opens `FileShare.Read`, tailer opens
|
|
||||||
`Read/FileShare.ReadWrite|Delete`): events `started`, `connected`,
|
|
||||||
`characterList{accountName,slotCount,characters[{id,name,secondsGreyedOut}]}`,
|
|
||||||
`enteredWorld{characterId,characterName}`, `pluginLoaded{plugin}`,
|
|
||||||
`pluginFailed{plugin,error}`,
|
|
||||||
`loginCommandFailed{commandIndex,command,error}`,
|
|
||||||
`characterCreated{guid,name}`, `creationFailed{code,reason,name}`,
|
|
||||||
`disconnected{reason}`,
|
|
||||||
`exited{code,reason}` — every line carries `"v":1`, `"e"`, `"t"`
|
|
||||||
(ISO-8601 UTC), `"sessionId"`. `secondsGreyedOut` is a uint on BOTH
|
|
||||||
sides. Unknown `e` values must parse to a typed Unknown event, never
|
|
||||||
throw; a known `e` with a wrong payload shape should be distinguishable
|
|
||||||
from an unknown `e` (LA3 review finding 12).
|
|
||||||
|
|
||||||
**Campaign CC CC2 amendment (this section is the contract; the writer and
|
|
||||||
tailer below implement it, in that order):** `characterCreated{guid,name}`
|
|
||||||
fires on the Ok reply to a `CharacterCreate` (opcode `0xF656`) request —
|
|
||||||
`guid`/`name` come straight off the shared `0xF643`
|
|
||||||
`CharGenVerificationResponse` Ok identity payload
|
|
||||||
(`AcDream.Core.Net.Messages.CharGenVerificationResponse`), deliberately
|
|
||||||
named `guid`/`name` rather than `characterId`/`characterName` to mirror
|
|
||||||
that payload's own field names and to read distinctly from
|
|
||||||
`enteredWorld` — a freshly created character is logged straight in by
|
|
||||||
retail without a fresh `characterList` (see that type's doc comment), so
|
|
||||||
`characterCreated` can precede an `enteredWorld` for the same character
|
|
||||||
rather than replacing it. `creationFailed{code,reason,name}` fires on any
|
|
||||||
non-Ok reply: `code` is the raw wire `CharGenVerificationResponse.Code`
|
|
||||||
value, `reason` is that code's enum member name (e.g. `"NameInUse"`) so a
|
|
||||||
reader gets a stable readable reason without hard-coding the numeric
|
|
||||||
mapping itself, and `name` is the ATTEMPTED character name so a launcher
|
|
||||||
can render "the name Bob is taken". (CC2 review F4: the enum member
|
|
||||||
originally rode the `name` key, colliding in meaning with
|
|
||||||
`characterCreated.name`; renamed before any consumer shipped.)
|
|
||||||
|
|
||||||
`loginCommandFailed.commandIndex` is the zero-based index in the configured
|
|
||||||
`loginCommands` array. `command` is the exact configured line and `error` is
|
|
||||||
the isolated parser/router/handler failure. The event is observational: the
|
|
||||||
host continues with the next configured line and never converts the command
|
|
||||||
failure into a login, plugin, session, or process failure.
|
|
||||||
|
|
||||||
**Known LA1 status limitation:** the stream has no independent mid-play
|
|
||||||
wire-drop detector. If a transport becomes silent without raising through the
|
|
||||||
host's tick/teardown path, no immediate `disconnected` line can be promised;
|
|
||||||
the launcher must not treat the absence of that line as proof that the socket
|
|
||||||
is healthy. Explicit reconnect is ordered and observable — it emits
|
|
||||||
`disconnected{reason:"reconnect"}` before the replacement connection's second
|
|
||||||
`connected` — and normal stop/process teardown closes any still-open
|
|
||||||
connection before `exited`. A future transport-health signal may improve the
|
|
||||||
timing without changing this pinned event vocabulary.
|
|
||||||
|
|
||||||
Three pieces, one slice, because they share the session-config/status seam:
|
|
||||||
|
|
||||||
1. **App `--session-config <path>`:** parsed once in `Program.cs` into
|
|
||||||
`RuntimeOptions` (code-structure rule 4); carries endpoint, account,
|
|
||||||
optional character selector, `Plugins`, `LoginCommands`, `Content`
|
|
||||||
(DatDirectory/PreparedAssetPath), status-file path, credential reference.
|
|
||||||
Recon: `Program.cs` has NO subcommand dispatch today — args handling is
|
|
||||||
one positional DAT-dir (`Program.cs:35`), so the flag is purely additive
|
|
||||||
(preserve the positional arg). The live-credential seam is a single call
|
|
||||||
site (`SessionPlayerComposition.cs:1128-1135` →
|
|
||||||
`LiveSessionConnectOptions`); the config path populates the same
|
|
||||||
`RuntimeOptions` fields from a different source. Env-var dev flow
|
|
||||||
untouched. App gains the `StandardInput` credential read (mirroring
|
|
||||||
`HeadlessCredentialResolver.ResolveStandardInput` — one line, immediately
|
|
||||||
wrapped in an erasable secret, redacted `ToString`; today
|
|
||||||
`RuntimeOptions.LivePass` is a bare string — the config path must not
|
|
||||||
widen that exposure).
|
|
||||||
2. **Status stream both hosts:** per-session `status.jsonl` (path given in
|
|
||||||
config; absent → permanent no-op sink). Versioned event
|
|
||||||
vocabulary (`"v":1`): `started`, `connected`, `characterList`,
|
|
||||||
`enteredWorld`, `pluginLoaded`/`pluginFailed`,
|
|
||||||
`loginCommandFailed`, `characterCreated`/`creationFailed` (Campaign CC
|
|
||||||
CC2), `disconnected`, `exited`.
|
|
||||||
Recon: today's `HeadlessDiagnosticWriter` is a single shared-stdout JSONL
|
|
||||||
sink with four kinds (lifecycle/failure/event/resources) and NO per-session
|
|
||||||
file — the status writer is a second, separate sink, not a rework of the
|
|
||||||
diagnostics writer. App has no structured writer today; it gets the same
|
|
||||||
shared implementation (lands in Runtime so both hosts borrow it).
|
|
||||||
3. **Roster plumbing:** `CharacterList.Parsed` is consumed inside
|
|
||||||
`LiveSessionController.StartCore` (`LiveSessionController.cs:612`) and
|
|
||||||
never escapes — add a typed roster report on the lifecycle-host seam
|
|
||||||
(`ILiveSessionLifecycleHost`) so hosts can emit the `characterList` status
|
|
||||||
event and (later) the char-select screen can populate. No behavior change
|
|
||||||
to selection itself in this slice.
|
|
||||||
|
|
||||||
**Acceptance:** round-trip tests (config → `RuntimeOptions`; stdin credential;
|
|
||||||
status events in order with exact shapes; roster surfaced); App/Headless/
|
|
||||||
Runtime suites green; redaction test proves the password never appears in
|
|
||||||
status/diagnostics output.
|
|
||||||
|
|
||||||
## LA2 — headless probe mode + `idle` policy
|
|
||||||
|
|
||||||
Recon facts: the probe's shape already exists as the `NoCharacters` early-exit
|
|
||||||
(`LiveSessionController.cs:613-622` → `StopCore()` → 4-stage
|
|
||||||
`SessionScope.DrainTeardown`, graceful, `_inWorld == false` so no pre-logoff
|
|
||||||
flush) — but it fires only on selection FAILURE and maps to exit code 5
|
|
||||||
(`HeadlessProcessHost.RunOnUpdateThread:203-212` treats any non-`Connected`
|
|
||||||
start as `ConnectionError`).
|
|
||||||
|
|
||||||
1. **Probe:** a `Probe` flag on the connect options short-circuits `StartCore`
|
|
||||||
right after `GetCharacters` (before `TrySelectCharacter`): report roster,
|
|
||||||
`StopCore()`, return a NEW `LiveSessionStartStatus.ProbeComplete`.
|
|
||||||
`HeadlessProcessHost` maps it to exit code 0 with a final `characterList` +
|
|
||||||
`exited(reason: "probe")` status pair. Config: `mode: "probe"` on the
|
|
||||||
session descriptor relaxes the `JsonRequired` character selector + policy
|
|
||||||
for probe sessions ONLY (loader keeps strict validation otherwise —
|
|
||||||
recon: violations currently surface as raw `JsonException` → exit 3; probe
|
|
||||||
relaxation must be shape-level in the loader, not attribute removal).
|
|
||||||
2. **`idle` policy:** new consumer `HeadlessBotPolicy` id — enter world, run
|
|
||||||
plugins/login-commands (arrive in LA5/LA6), stay until stopped, clean
|
|
||||||
SIGINT teardown (K4's graceful-logout path already proves the mechanism).
|
|
||||||
|
|
||||||
**Acceptance:** probe test (fixture session → roster event → graceful teardown
|
|
||||||
receipt → exit 0, no `EnterWorld` on the wire); loader tests for probe-shape
|
|
||||||
relaxation + strict normal validation; idle-policy lifecycle test; suites
|
|
||||||
green. Connected verification (user gate, LA11 batch): live probe against ACE
|
|
||||||
twice in a row with no lingering session (spec §11.9).
|
|
||||||
|
|
||||||
## LA3 — `AcDream.Launcher.Core`
|
|
||||||
|
|
||||||
New BCL-only project + `tests/AcDream.Launcher.Core.Tests/`. References
|
|
||||||
`AcDream.Platform` ONLY.
|
|
||||||
|
|
||||||
- Profile store: `launcher-profiles.json` (spec §5 schema) — load/save/
|
|
||||||
validate, full CRUD operations, roster merge (fold `characterList` events
|
|
||||||
in, preserving per-character user settings), 0600 on Linux.
|
|
||||||
- Session-config composition: profile + install records → the LA1 config
|
|
||||||
shape (typed writer; probe shape included). Passwords excluded — stdin only.
|
|
||||||
- Process orchestration: spawn App/Headless per launch mode, feed password to
|
|
||||||
child stdin then close, supervise lifetime, tail `status.jsonl`
|
|
||||||
(share-tolerant reads), surface typed session state.
|
|
||||||
- SHA-256 utility (pak record + download verify — consumed by LA9/LA10).
|
|
||||||
|
|
||||||
**Acceptance:** CRUD/round-trip/merge tests; composition tests (all three
|
|
||||||
modes + probe); supervision tests against a fake child process (echo script);
|
|
||||||
status-tail tests including partial-line handling; suites green.
|
|
||||||
|
|
||||||
## LA4 — `AcDream.Launcher` (Avalonia)
|
|
||||||
|
|
||||||
New Avalonia project (Windows + Linux). MVVM over Launcher.Core; no game
|
|
||||||
solution references beyond `AcDream.Platform` transitively.
|
|
||||||
|
|
||||||
- Views: server list → accounts → characters tree; add/edit/remove dialogs
|
|
||||||
for servers (name/host/port) and accounts (account + password entry);
|
|
||||||
per-character settings editor (launch mode, plugin set, login commands);
|
|
||||||
per-account "refresh characters" (probe); running-sessions status column.
|
|
||||||
- Launch actions per mode (`gui` / `guiSelect` / `headless`); probe disabled
|
|
||||||
while the launcher runs a session for that account.
|
|
||||||
- First-run wizard shell + update prompt shell (bodies land in LA9/LA10).
|
|
||||||
|
|
||||||
**Acceptance:** ViewModel tests in Launcher.Core.Tests patterns (VMs live in
|
|
||||||
the Avalonia project but stay logic-thin; anything testable pushes down);
|
|
||||||
build green on Windows; `linux-x64` publish compiles. Visual polish is gated
|
|
||||||
at LA11 (user).
|
|
||||||
|
|
||||||
## LA5 — plugin hosting on both hosts
|
|
||||||
|
|
||||||
Recon facts (2026-08-14): `PluginLoader`/`PluginDiscovery`/`PluginManifest`
|
|
||||||
already live in `AcDream.Core` (Headless-reachable). App's single load loop
|
|
||||||
(`App/Program.cs:110-121`) loads ALL discovered plugins from two roots
|
|
||||||
(`AppContext.BaseDirectory/plugins` + `ApplicationPathSet.PluginsDirectory`,
|
|
||||||
dup-id skip) — no allow-list exists on either host. `AppPluginHost` is a
|
|
||||||
26-line pass-through; three of four `IPluginHost` surfaces (`State` →
|
|
||||||
`WorldGameState`, `Events` → `WorldEvents`, `Selection` → `SelectionState`)
|
|
||||||
are backed by Core-owned types already; only `Ui` (`BufferedUiRegistry`) is
|
|
||||||
genuinely App-only. Headless has zero plugin hosting today (confirmed).
|
|
||||||
|
|
||||||
1. Session-config `Plugins` allow-list filters the discovery result on BOTH
|
|
||||||
hosts (absent/null list = load all, preserving today's dev behavior;
|
|
||||||
explicit `[]` = load none). Launcher-composed normal-empty and probe
|
|
||||||
sessions emit `[]`.
|
|
||||||
2. `HeadlessPluginHost : IPluginHost` in Headless over the same Core-owned
|
|
||||||
`State`/`Events`/`Selection`; `Ui` is an explicit no-op behind a new
|
|
||||||
capability flag on `IPluginHost` (e.g. `HasUi`) so plugins can detect
|
|
||||||
headless. Contract documented in `Plugin.Abstractions`.
|
|
||||||
3. `pluginLoaded`/`pluginFailed` status events from both hosts' load loops.
|
|
||||||
|
|
||||||
**Acceptance:** fixture plugin in Headless suite (load, capability flag,
|
|
||||||
markup no-op, teardown via collectible ALC); allow-list filter tests both
|
|
||||||
hosts; status events asserted; suites green.
|
|
||||||
|
|
||||||
## LA6 — login commands on both hosts
|
|
||||||
|
|
||||||
Recon facts (2026-08-14): the command core is dependency-CLEAN —
|
|
||||||
`ChatInputParser` (zero usings), `ChatCommandRouter` (BCL +
|
|
||||||
`AcDream.Core.Chat`), `RetailClientCommandCatalog` (FrozenDictionary),
|
|
||||||
`ChatVM` (Core.Chat/Combat + `System.Numerics` only), `ICommandBus` + the
|
|
||||||
four command records (BCL-only). The block is assembly identity, not
|
|
||||||
coupling. `ChatCommandRouter.Submit`'s two entanglements: a hard `ChatVM`
|
|
||||||
parameter (uses only `ShowInterfaceText`/`ShowSystemMessage`/
|
|
||||||
`LastIncomingTellSender`/`LastOutgoingTellTarget`) and the `ICommandBus`,
|
|
||||||
whose production implementation (`LiveSessionCommandRouter`,
|
|
||||||
`App/Net/LiveSessionCommandRouter.cs`) is App-only and wraps wire-send
|
|
||||||
delegates from the live session. GUI already has a login-command analog:
|
|
||||||
`RetailUiAutomationScriptRunner` feeds `ChatCommandRouter.Submit` at
|
|
||||||
`RetailUiRuntime.cs:523-527`.
|
|
||||||
|
|
||||||
1. **Extraction:** move parser/router/catalog + `ICommandBus` + the four
|
|
||||||
command records (+ sibling tables they require) into Runtime
|
|
||||||
(`AcDream.Runtime/Chat/...`); the router's `ChatVM` parameter becomes a
|
|
||||||
narrow feedback interface defined beside it (exactly the four members
|
|
||||||
used); `ChatVM` (stays in UI.Abstractions) implements it. GUI path stays
|
|
||||||
bit-identical — same call sites (`ChatWindowController.cs:326`,
|
|
||||||
`FloatingChatWindowController.cs:157`), same routing, CH-accepted
|
|
||||||
behavior regression-checked by the existing chat suites.
|
|
||||||
2. **Headless dispatch:** a Runtime/Headless `ICommandBus` binding the same
|
|
||||||
session send delegates (`SendTalk`/`SendTell`/`SendChannel`/
|
|
||||||
`SendTurbineChat`) + Runtime state that App's router binds — paralleling
|
|
||||||
`LiveSessionCommandRouter`'s registrations, feedback lands in
|
|
||||||
`RuntimeCommunicationState.AddText`.
|
|
||||||
3. **Execution:** both hosts run `LoginCommands` sequentially as-if-typed
|
|
||||||
(default 500 ms inter-command delay, config-overridable) once
|
|
||||||
entered-world; per-command failures → status stream, never abort.
|
|
||||||
4. K0 guard: if the code folds into Runtime, the single-reference assertion
|
|
||||||
stands untouched; the forbidden-prefix closure tests keep passing. Any
|
|
||||||
guard text change is deliberate and documented.
|
|
||||||
|
|
||||||
**Acceptance:** extraction lands with zero GUI chat test regressions
|
|
||||||
(UI.Abstractions + App chat suites bit-green); headless executes a
|
|
||||||
login-command script against a fixture session with ordered wire sends;
|
|
||||||
delay + failure-tolerance tests; suites green.
|
|
||||||
|
|
||||||
## LA7 — character-select: state + wire
|
|
||||||
|
|
||||||
Recon facts (2026-08-14): retail's screen is `gmCharacterManagementUI`
|
|
||||||
(`acclient.h:56545`) — flat listbox + Create/Enter/Delete/Restore buttons +
|
|
||||||
dialog contexts. **No 3D preview exists on retail's select screen** (the
|
|
||||||
`gmCG3DView`/`CreatureMode` viewport is chargen-only; the old
|
|
||||||
"rotating pedestal" line in `retail-ui/05-panels.md` §13 is uncited and
|
|
||||||
wrong). Our `CharacterList` parse already matches ACE's serializer exactly
|
|
||||||
(two-array shape, status/deleted always zero from ACE) and the two-phase
|
|
||||||
enter-world (0xF7C8 → 0xF7DF → 0xF657) is implemented. Missing wire:
|
|
||||||
delete/restore/error.
|
|
||||||
|
|
||||||
1. **Wire messages** (`AcDream.Core.Net/Messages/`, retail citations in
|
|
||||||
file docs per house style): `CharacterDelete` 0xF655 — outbound
|
|
||||||
account String16L + **slot index** (`Proto_UI::SendDeleteCharacter
|
|
||||||
@0x00546b30`; NOT guid), inbound opcode-only ack followed by a fresh
|
|
||||||
CharacterList; `CharacterRestore` 0xF7D9 guid-only (ACE + holtburger
|
|
||||||
consensus; the decomp's apparent extra strings are a decompiler
|
|
||||||
artifact — spec §11.4), response 0xF643 (flag + guid + name +
|
|
||||||
secondsDisabled); `CharacterError` 0xF659 parser (new — today NO
|
|
||||||
character-stage server error can be surfaced).
|
|
||||||
2. **Runtime selection state** (J-owner pattern): roster with per-entry
|
|
||||||
greyed/pending-delete state (`SecondsGreyedOut != 0` ⇒ pending; ACE
|
|
||||||
sends a constant 1 during the grace window — treat as boolean, never a
|
|
||||||
countdown), highlight, pending-delete dialog state, typed commands
|
|
||||||
(highlight / enter / delete-request / delete-confirm / restore).
|
|
||||||
Retail behavior oracles: `RebuildCharacterList@0x004ec3a0`,
|
|
||||||
`SelectCharacter@0x004ec160`, `UpdateButtons@0x004ec240`
|
|
||||||
(Delete↔Restore swap on greyed state), `EnterGame@0x004ed440`.
|
|
||||||
3. **No-selector flow:** a graphical session config without a character
|
|
||||||
selector stops at selection state instead of auto-enter; the
|
|
||||||
first-available fallback (`CharacterList.TrySelectFirstAvailable`,
|
|
||||||
used at `LiveSessionController.cs:848-851`) remains ONLY for
|
|
||||||
selector-carrying/headless sessions. Selection feeds the existing
|
|
||||||
`EnterWorld` path unchanged.
|
|
||||||
|
|
||||||
LA7b hazards carried from the LA7a review (2026-08-14): ACE's restore
|
|
||||||
handler has a SILENT no-reply path (unknown guid → `return;`, no 0xF643,
|
|
||||||
no 0xF659) — selection state must never block awaiting a restore reply;
|
|
||||||
outbound routing is delete via retail's SendToLogon, restore via
|
|
||||||
SendToControl, ACE replies on UIQueue; `charError.NumErrors` (0x19) is an
|
|
||||||
enum-range sentinel and must never render as a user-facing message.
|
|
||||||
Register row AD-97 (guid-only restore request, an adaptation) rides the
|
|
||||||
LA7a branch.
|
|
||||||
|
|
||||||
**Acceptance:** message round-trip tests against ACE's serializer shapes;
|
|
||||||
selection-state tests (greyed transitions, delete→list-refresh, restore,
|
|
||||||
error surfacing); no-selector stop + enter flow tests; suites green.
|
|
||||||
|
|
||||||
## LA8 — character-select: authored retail screen
|
|
||||||
|
|
||||||
Scope: project LA7's state through the REAL retail screen. No 3D preview
|
|
||||||
(recon-corrected; a preview would be an unapproved divergence).
|
|
||||||
|
|
||||||
1. **Layout resolution:** retail resolves the root via
|
|
||||||
`UIMainFramework::CreateAndAddRootElement(0x10000005, 0x1000039a)` +
|
|
||||||
`DBObj::GetDIDByEnum(..., 5)` — reuse OP8's ported GetDIDByEnum
|
|
||||||
machinery (category 4 precedent) for enum-table 5; slice starts by
|
|
||||||
dumping that table from installed DATs to pin the concrete DataID.
|
|
||||||
Child ids: listbox `0x1000039d`, create `0x100003a0` (present,
|
|
||||||
disabled — Create is a future campaign), enter `0x100003a2`, delete
|
|
||||||
`0x1000039f`, restore `0x1000039e`.
|
|
||||||
2. **Dialogs:** delete-confirm, please-wait, entering-world, error — the
|
|
||||||
retail dialog machinery from the OP8 WaitDialog work (`2a81e813`
|
|
||||||
mapped WaitDialog class type 0x19) is the base.
|
|
||||||
3. **Open item resolved here:** whether retail draws a render-loop
|
|
||||||
background scene behind the UI (pseudo-C proves only that the UI class
|
|
||||||
owns no viewport) — settle via user recollection + the visual gate
|
|
||||||
before polishing.
|
|
||||||
|
|
||||||
**Acceptance:** authored screen builds from DAT assets; button-state
|
|
||||||
matrix matches `UpdateButtons` oracle (incl. Delete↔Restore swap);
|
|
||||||
enter/delete/restore/error flows drive LA7 state end-to-end; suites
|
|
||||||
green. User visual gate at LA11 (screen look, dialog flows, delete +
|
|
||||||
restore against local ACE).
|
|
||||||
|
|
||||||
## LA9 — installer (first-run)
|
|
||||||
|
|
||||||
- DAT locate: auto-detect `%USERPROFILE%\Documents\Asheron's Call` and
|
|
||||||
`C:\Turbine\Asheron's Call` + manual picker; validate the four DATs.
|
|
||||||
- Bake: spawn `acdream-bake --dat-dir <dats> --out <DataDirectory>/pak/acdream.pak
|
|
||||||
--threads N`. Recon: default `--out` is INSIDE the DAT dir — the launcher
|
|
||||||
always passes `--out` explicitly. Progress: add `--progress-json` to
|
|
||||||
`AcDream.Bake` (JSONL progress lines alongside the existing 5-second human
|
|
||||||
text, which stays default) — scraping human text is fragile and we own the
|
|
||||||
tool. Recon: the bake has NO whole-file SHA — after a successful bake the
|
|
||||||
LAUNCHER computes and records SHA-256 + size + `BakeToolVersion` in its
|
|
||||||
install record, and re-verifies on subsequent startups (fast corruption
|
|
||||||
check trades a few seconds of hashing for never launching against a
|
|
||||||
half-written pak).
|
|
||||||
- Install record feeds LA3's session-config composition
|
|
||||||
(DatDirectory/PreparedAssetPath).
|
|
||||||
|
|
||||||
**Acceptance:** wizard flow tests over Launcher.Core (fake bake child emitting
|
|
||||||
`--progress-json` lines); bake-tool progress flag tests in
|
|
||||||
`tests/AcDream.Bake.Tests`; SHA record/verify tests; suites green. Connected
|
|
||||||
gate (user): clean-profile first-run against real DATs.
|
|
||||||
|
|
||||||
## LA10 — updater
|
|
||||||
|
|
||||||
- Manifest: GitHub Releases; `manifest.json` release asset — version, per-RID
|
|
||||||
client zip URL + SHA-256 + size, minimum-launcher version. Launcher pins
|
|
||||||
owner/repo in its config.
|
|
||||||
- Client update: poll on launch (+ manual check), download to staging, SHA
|
|
||||||
verify, unpack to `DataDirectory/app/<version>/`, atomic `current.json`
|
|
||||||
pointer swap, refuse while any session runs, keep previous version for
|
|
||||||
one-step rollback.
|
|
||||||
- Launcher self-update: staged download + target-local atomic replacement on
|
|
||||||
next start.
|
|
||||||
- Session-config composition targets `app/current`'s binaries.
|
|
||||||
|
|
||||||
**Acceptance:** manifest/download/verify/swap tests against a local HTTP
|
|
||||||
fixture; rollback test; refusal-while-running test; self-update staging test;
|
|
||||||
suites green. Connected gate (user): staged-manifest update swap end-to-end.
|
|
||||||
|
|
||||||
### Pinned updater contracts (v1, BINDING)
|
|
||||||
|
|
||||||
This section is the single source of truth for every LA10 feed and on-disk
|
|
||||||
shape. Readers use strict, case-sensitive `System.Text.Json` parsing, reject
|
|
||||||
unknown or duplicate properties, and reject unsupported schema versions
|
|
||||||
before doing network, extraction, or activation work.
|
|
||||||
|
|
||||||
The production feed is pinned to GitHub owner/repository
|
|
||||||
`eriknihlen/acdream`; the launcher reads
|
|
||||||
`https://github.com/eriknihlen/acdream/releases/latest/download/manifest.json`.
|
|
||||||
Tests use a separate internal fixture constructor that may admit loopback HTTP;
|
|
||||||
that allowance never propagates to the production feed. Production manifest
|
|
||||||
and artifact URIs use HTTPS. Automatic redirects are disabled and every
|
|
||||||
redirect hop is validated before it is requested; redirect loops, a chain over
|
|
||||||
five hops, and any HTTPS-to-HTTP downgrade are rejected. `manifest.json` is:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"version": "1.2.3",
|
|
||||||
"minimumLauncherVersion": "1.1.0",
|
|
||||||
"clients": {
|
|
||||||
"win-x64": {
|
|
||||||
"url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-client-win-x64.zip",
|
|
||||||
"sha256": "<64 hex characters>",
|
|
||||||
"size": 123
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"launchers": {
|
|
||||||
"win-x64": {
|
|
||||||
"url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-launcher-win-x64.zip",
|
|
||||||
"sha256": "<64 hex characters>",
|
|
||||||
"size": 123
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`version` and `minimumLauncherVersion` are strict SemVer 2.0 strings. Build
|
|
||||||
metadata is ignored for precedence; numeric identifiers are compared without
|
|
||||||
fixed-width integer overflow. RID keys are exact lowercase portable RIDs.
|
|
||||||
Both dictionaries are required and the running RID must have a client and a
|
|
||||||
launcher row. Artifact sizes are positive and capped by the launcher's
|
|
||||||
download limit; SHA-256 is exactly 64 hex characters. ZIP URLs are absolute.
|
|
||||||
Client ZIPs have the two host executables at their root
|
|
||||||
(`AcDream.App[.exe]`, `acdream-headless[.exe]`); launcher ZIPs have
|
|
||||||
`acdream-launcher[.exe]` at their root. No implicit wrapper directory exists.
|
|
||||||
|
|
||||||
Every extracted client version has
|
|
||||||
`DataDirectory/app/<version>/install.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"version": "1.2.3",
|
|
||||||
"rid": "win-x64",
|
|
||||||
"archiveSha256": "<64 hex characters>",
|
|
||||||
"archiveSize": 123,
|
|
||||||
"files": [
|
|
||||||
{ "path": "AcDream.App.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Paths use `/`, are relative, normalized, unique under ordinal-ignore-case,
|
|
||||||
and sorted ordinally. `unixMode` contains only the portable permission bits
|
|
||||||
captured from the ZIP entry. Startup verifies every recorded regular file by
|
|
||||||
size/SHA, rejects unrecorded files/reparse points, and requires the two host
|
|
||||||
executables before admitting a version. Extraction uses a random sibling
|
|
||||||
directory under `DataDirectory/app/`; promotion to `<version>/` is one
|
|
||||||
same-volume directory rename.
|
|
||||||
|
|
||||||
`DataDirectory/app/current.json` is the only activation authority:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "schemaVersion": 1, "currentVersion": "1.2.3", "previousVersion": "1.1.0" }
|
|
||||||
```
|
|
||||||
|
|
||||||
`previousVersion` is omitted for the first activation. Pointer writes are
|
|
||||||
write-through temporary-file + same-directory atomic rename. The last valid
|
|
||||||
pointer is also atomically preserved as `current.previous.json`; startup may
|
|
||||||
restore that exact backup only when `current.json` is missing/malformed and
|
|
||||||
the referenced version verifies. Orphan LA10 staging directories, download
|
|
||||||
archives, corrupt-version quarantine directories, and pointer temporaries are
|
|
||||||
transaction-owned by exact lowercase GUID names and are removed only under the
|
|
||||||
exclusive update lease; near-matching user names are preserved. A corrupt
|
|
||||||
installed version is never silently selected; the explicit one-step rollback
|
|
||||||
swaps the two verified pointer versions.
|
|
||||||
|
|
||||||
`DataDirectory/app/.update-session.lock` is the cross-process barrier. Each
|
|
||||||
supervised launcher activity holds a shared OS handle from before executable
|
|
||||||
resolution until terminal process observation; launcher disposal requests
|
|
||||||
child termination and does not release that handle until the child is actually
|
|
||||||
observed terminal. An update/rollback holds the
|
|
||||||
exclusive handle for its entire recovery/download/extract/promote/pointer
|
|
||||||
transaction. Failure to acquire the exclusive handle is an immediate refusal,
|
|
||||||
not a wait behind a running session. The open handle, not lock-file contents,
|
|
||||||
owns the lease and therefore releases after process death.
|
|
||||||
|
|
||||||
Launcher self-update staging lives at
|
|
||||||
`DataDirectory/launcher-update/transactions/<transactionId>/` and the sole
|
|
||||||
durable authority is `DataDirectory/launcher-update/pending.json` (schema 3):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schemaVersion": 3,
|
|
||||||
"transactionId": "0123456789abcdef0123456789abcdef",
|
|
||||||
"state": "staged",
|
|
||||||
"version": "1.2.3",
|
|
||||||
"rid": "win-x64",
|
|
||||||
"targetDirectory": "<absolute current launcher directory>",
|
|
||||||
"archiveSha256": "<64 hex characters>",
|
|
||||||
"archiveSize": 123,
|
|
||||||
"files": [
|
|
||||||
{ "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
|
||||||
],
|
|
||||||
"apply": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Before mutation the verified staged launcher becomes the next-start helper and
|
|
||||||
waits for the initiating launcher PID without invoking a shell. It first copies
|
|
||||||
the complete verified payload into the target-local
|
|
||||||
`.acdream-self-update-<transactionId>/incoming/` tree. The plan then advances
|
|
||||||
to `applying`; `apply` is an ordinally sorted union of new payload paths, the
|
|
||||||
owned metadata path, and obsolete paths from the previous ownership record:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"path": "acdream-launcher.exe",
|
|
||||||
"operation": "install",
|
|
||||||
"hadOriginal": true,
|
|
||||||
"priorSha256": "<64 hex characters>",
|
|
||||||
"priorSize": 123,
|
|
||||||
"priorUnixMode": 0,
|
|
||||||
"replacementSha256": "<64 hex characters>",
|
|
||||||
"replacementSize": 456,
|
|
||||||
"replacementUnixMode": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "new-support.dat",
|
|
||||||
"operation": "install",
|
|
||||||
"hadOriginal": false,
|
|
||||||
"priorSha256": null,
|
|
||||||
"priorSize": null,
|
|
||||||
"priorUnixMode": null,
|
|
||||||
"replacementSha256": "<64 hex characters>",
|
|
||||||
"replacementSize": 456,
|
|
||||||
"replacementUnixMode": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Every `hadOriginal` entry persists the exact pre-mutation SHA-256, length, and
|
|
||||||
Linux mode bits; a no-original entry has all three prior fields null. Every
|
|
||||||
install entry likewise persists the verified replacement metadata, while a
|
|
||||||
remove entry has all three replacement fields null. The journal is invalid
|
|
||||||
unless those fields agree with `hadOriginal` and `operation`.
|
|
||||||
|
|
||||||
Existing targets are replaced with one same-filesystem atomic replace whose
|
|
||||||
backup is also target-local. Previously absent noncanonical files use one
|
|
||||||
same-filesystem rename; obsolete owned files use one rename into backup. The
|
|
||||||
canonical launcher path therefore contains either the complete old file or the
|
|
||||||
complete new file at every durable crash boundary. Rollback first performs a
|
|
||||||
zero-mutation preflight of the complete target-local transaction and every
|
|
||||||
journal entry. It rejects reparse points, unsafe parents, unrecorded paths,
|
|
||||||
ambiguous file layouts, and any SHA-256/length/mode mismatch in a prior,
|
|
||||||
incoming, or discard file. Only a fully preflighted rollback may atomically
|
|
||||||
restore backups; newly created files move to target-local discard rather than
|
|
||||||
being deleted. The complete prior target set is then reverified before the
|
|
||||||
plan enters durable `rolledBack` state while retaining the journal. Retry is
|
|
||||||
allowed only after that prior set is reverified again and the plan returns to
|
|
||||||
`staged`. Thus rollback is atomic per file and idempotent after a process/power
|
|
||||||
loss. Any ambiguity preserves the applying plan and transaction evidence and
|
|
||||||
forbids launching the canonical path for manual recovery. Linux mode bits come
|
|
||||||
from the verified incoming file. A helper that cannot immediately
|
|
||||||
acquire the exclusive update lease defers the staged plan and exits without
|
|
||||||
restarting the old launcher, preventing restart loops.
|
|
||||||
|
|
||||||
Successful application writes strict target ownership metadata at
|
|
||||||
`<launcher directory>/launcher.install.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"version": "1.2.3",
|
|
||||||
"rid": "win-x64",
|
|
||||||
"files": [
|
|
||||||
{ "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The archive may not supply that reserved metadata path. A prior valid record is
|
|
||||||
the only authority for obsolete-file removal; the first managed update does
|
|
||||||
not infer ownership of unrelated legacy files. On success the plan becomes
|
|
||||||
`awaitingConfirmation`; the new launcher confirms at its first managed
|
|
||||||
instruction, after which the helper releases its lease and the confirmed
|
|
||||||
launcher reclaims plan, data-transaction, and target-local residue. An
|
|
||||||
`applying` plan is rolled back before retry, and failure to start/confirm the
|
|
||||||
new launcher restores every original (and removes every no-original target).
|
|
||||||
The helper restarts the restored canonical launcher only after a fresh complete
|
|
||||||
verification of the retained `rolledBack` journal; rollback corruption or an
|
|
||||||
unsafe backup/discard tree exits without starting either launcher.
|
|
||||||
Reading `pending.json` never performs cleanup. Ordinary startup attempts the
|
|
||||||
exclusive lease without waiting and skips update cleanup entirely when another
|
|
||||||
session/staging transaction owns it. All plan paths are re-derived/contained
|
|
||||||
under pinned roots; the target directory must equal the actual launcher base
|
|
||||||
directory.
|
|
||||||
|
|
||||||
Every portable archive and persisted relative path rejects Windows device
|
|
||||||
segments on every host: `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`,
|
|
||||||
`CONOUT$`, `COM1`-`COM9`, `LPT1`-`LPT9`, and the Windows-equivalent superscript
|
|
||||||
forms `COM¹`/`COM²`/`COM³` and `LPT¹`/`LPT²`/`LPT³`, including extensions.
|
|
||||||
|
|
||||||
## LA11 — closeout
|
|
||||||
|
|
||||||
- One exact operator script
|
|
||||||
`docs/research/2026-08-14-campaign-la-test-script.md`, fronted by the
|
|
||||||
connection-free `tools/run-campaign-la-preflight.ps1` and followed by
|
|
||||||
serial user rows,
|
|
||||||
covering: all three launch modes vs local ACE, probe round-trip ×2 (no
|
|
||||||
lingering session), char-select visual matrix + delete flow, login-commands
|
|
||||||
+ plugin behavior on both hosts, add-server/add-account purely in UI,
|
|
||||||
clean-profile first-run wizard, staged update swap.
|
|
||||||
- Roadmap shipped-table entry, CLAUDE.md Current-state flip, memory distill,
|
|
||||||
ledger below completed, program closeout section.
|
|
||||||
|
|
||||||
## Review protocol
|
|
||||||
|
|
||||||
Per slice: implementer commit(s) → Opus dual-lens review (architectural +
|
|
||||||
retail-where-applicable) → fix round → narrow re-review of fixes → slice DONE
|
|
||||||
in ledger. Reviews name blast radius explicitly
|
|
||||||
(`claude-memory/feedback_blast_radius_single_lens.md`). Slices LA7/LA8 add the
|
|
||||||
retail-fidelity lens against named-retail symbols cited in the slice body;
|
|
||||||
LA6 adds CH-regression scrutiny; LA0 adds guard-integrity scrutiny.
|
|
||||||
|
|
||||||
## Gate round 1 — 2026-08-15 (first live launch by the user)
|
|
||||||
|
|
||||||
The user's first hands-on launch found the launcher exiting on every click.
|
|
||||||
Root cause (`d54b8a78`): `MainWindow`'s constructor called
|
|
||||||
`AvaloniaXamlLoader.Load(this)` instead of the generated
|
|
||||||
`InitializeComponent()`, so every `x:Name` backing field was null and any
|
|
||||||
modal open/close threw out of the dispatcher into `Program`'s exit-74
|
|
||||||
guard. It reached the gate because NO test constructed `MainWindow` —
|
|
||||||
filed and closed as **#399** (`2b439cc1`, merged): `Avalonia.Headless.XUnit`
|
|
||||||
view tests with falsification evidence (12/12 fail against the old code,
|
|
||||||
12/12 pass against the fix; launcher suite 66/66 Windows + native Ubuntu;
|
|
||||||
xunit→xunit.v3 in that test project). Same round (`e1e94697`): **#398**
|
|
||||||
closed — fatal exceptions now write a full-stack crash report under the
|
|
||||||
data root (isolated-roots-safe; the first cut leaked to the real data root
|
|
||||||
when parsing failed, caught live and fixed) — and `acdream-bake.exe` is now
|
|
||||||
co-deployed on plain Build, not just Publish, so a developer-built launcher
|
|
||||||
can actually run its first-run wizard (79.6 MB single file beside the
|
|
||||||
launcher, incremental, `--help` verified). One transient 65/66 on the first
|
|
||||||
post-merge test run did not reproduce across a clean rebuild + six repeats —
|
|
||||||
consistent with stale-artifact mixing, but if it EVER recurs, capture the
|
|
||||||
failing test name before anything else. Merged slice worktrees/branches
|
|
||||||
(la2/la3/la7a/la-uitest) removed. Opus batch review: PASS (HIGH
|
|
||||||
confidence) with 6 findings, all landed same-day: F1 the crash reporter's
|
|
||||||
by-construction claim was FALSE (the launcher holds passwords in three
|
|
||||||
fields; the true invariant — no throw site interpolates a credential
|
|
||||||
value — is now pinned by a forced-failure test), F2 the co-deploy's
|
|
||||||
Inputs covered only Bake's own sources, not its Content/Platform/Core/
|
|
||||||
Plugin.Abstractions closure (the stale-artifact class again; fixing it
|
|
||||||
exposed and fixed two more incrementality traps: SkipUnchangedFiles
|
|
||||||
leaving outputs older than inputs, and %(Item.Metadata) in a plain
|
|
||||||
Include not batching — a literal '%(...)' input is permanently
|
|
||||||
out-of-date), F3 dual bake publish on RID publishes (guarded by
|
|
||||||
_IsPublishing; verified 0 build-target co-deploys during a real publish),
|
|
||||||
F4 misattributed comment, F5 template-scoped x:Name false-fail (sweep now
|
|
||||||
walks the XML with template-ancestor tolerance), F6 dead using, plus the
|
|
||||||
optional Path.IsPathFullyQualified hardening on the crash reporter's
|
|
||||||
--data-dir fallback. Launcher 67/67, Launcher.Core 317/317. The §A–I
|
|
||||||
connected script remains the open user gate.
|
|
||||||
|
|
||||||
## Gate round 2 — 2026-08-15 (first live launcher→client flow) — char-select matrix USER-PASSED
|
|
||||||
|
|
||||||
**USER-PASSED 2026-08-15 (end of round):** the character-select visual/
|
|
||||||
interaction matrix — stretched-canvas look with bilinear filtering,
|
|
||||||
aligned widgets, left-justified roster, World box reading the live server
|
|
||||||
name ("sawato"), and the centered exit confirmation — all accepted on the
|
|
||||||
live launcher→client flow. Round-2 commits after the round-1 batch:
|
|
||||||
`6e1c0967` (session-config launches force the retail UI), `9ce72925`
|
|
||||||
(PFID_CUSTOM_RAW_JPEG decode + resolution guards), `73041d70`
|
|
||||||
(whole-canvas AD-98 scale + inverse input), `308f40a3` (linear-twin
|
|
||||||
bilinear stretch), `ef96c554` (exit confirmation + authored justify +
|
|
||||||
world name, AD-99), `2e6d69dd` (#400), `0a7dc7d6` (durable world-name
|
|
||||||
read + canvas-centered dialogs). Remaining before shipment: the formal
|
|
||||||
§A–I script rows (probe ×2, headless+plugins+login commands, delete/
|
|
||||||
restore, A→B update swap, row I Linux), and the final-HEAD preflight
|
|
||||||
re-run.
|
|
||||||
|
|
||||||
**Round REVIEW-CLOSED 2026-08-15:** the owed Opus dual-lens batch review
|
|
||||||
of the six round-2 commits returned PASS with 8 findings; the fix round
|
|
||||||
(`0baebce2` — headline: `RetailWaitDialogView` was the ONE dialog view the
|
|
||||||
EffectiveCanvasSize sweep missed, firing on ENTER; plus the two stale
|
|
||||||
deleted-mechanism doc assertions, the Confirmation `0xAC` property,
|
|
||||||
truncating input mapping, the IsCurrent world-name gate, the AD-98
|
|
||||||
evidence note) closed all seven in the narrow re-review; F2 filed as
|
|
||||||
#401 (invert RetailUi to opt-out). The review also proved the
|
|
||||||
`DatWidgetFactory` justify widening has ZERO regressions across all 35
|
|
||||||
layout fixtures (303 buttons swept; the 16 authored-Left all already
|
|
||||||
left-aligned via their face-child branch) and is a move TOWARD retail
|
|
||||||
(`CalcJustification @0x00467260` has no lifted-from-child condition).
|
|
||||||
|
|
||||||
Two real defects, both root-caused and fixed:
|
|
||||||
|
|
||||||
1. **`6e1c0967` — launcher-spawned clients had NO interface at all.**
|
|
||||||
`RetailUi` rode the dev env var `ACDREAM_RETAIL_UI`; `FromSessionConfig`
|
|
||||||
inherited the env parse; the launcher strips `ACDREAM_*` from children
|
|
||||||
(LA11 isolation). Product launches therefore got the dev default: world
|
|
||||||
rendering, zero UI — character screen included. Session-config launches
|
|
||||||
now force `RetailUi = true` (a session-config launch IS a product
|
|
||||||
launch); the env flag remains the dev-launch opt-in. Test-pinned with a
|
|
||||||
null env.
|
|
||||||
2. **`9ce72925` — character-select screen rendered magenta background/
|
|
||||||
fills.** The screen's 800×600 root background (`0x06007576`) is
|
|
||||||
`PFID_CUSTOM_RAW_JPEG` — a complete JFIF stream retail hands to the
|
|
||||||
Intel JPEG Library (`RenderSurface::CreateFromSourceData @0x004440a0`),
|
|
||||||
with Width/Height legitimately 0 on disk. `SurfaceDecoder` had no JPEG
|
|
||||||
case AND a non-positive-dimension guard, so it fell silently to the
|
|
||||||
magenta placeholder; the listbox/ENTER fills are transparent, so one
|
|
||||||
broken background bled through as three symptoms. Fixed via
|
|
||||||
StbImageSharp (managed, Linux-safe; codec-library substitution per the
|
|
||||||
BCnEncoder precedent — no register row). BOTH silent traps now log once
|
|
||||||
per id (id-resolves-but-undecodable in `SurfaceDecoder`;
|
|
||||||
id-missing-from-DATs in `TextureCache`) — the existing magenta guard
|
|
||||||
only covered id-0. New installed-DAT sweep asserts every char-select
|
|
||||||
media id decodes non-magenta. Full suite 14,034 green.
|
|
||||||
|
|
||||||
Session-orchestration facts this round: the machine gained PowerShell 7
|
|
||||||
(winget, user-approved — the LA fixture tooling hard-requires it); an
|
|
||||||
orphan feed server from the earlier session held port 43119 with stale
|
|
||||||
fixture data (stopped); the launcher self-update bootstrap restart on a
|
|
||||||
dev binary is EXPECTED (staged launcher update → exit → respawn).
|
|
||||||
Observations still open for this round: the duplicated "versioned client
|
|
||||||
is unavailable" status line (cosmetic), and verifying Create Character is
|
|
||||||
disabled on the live screen.
|
|
||||||
|
|
||||||
## Ledger
|
|
||||||
|
|
||||||
| Slice | Status | Commits | Review | Notes |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| LA0 | **DONE 2026-08-14** | `cb6502c8`, `a49e92df` | Opus dual-lens PASS; all 6 findings CLOSED in narrow re-review | Byte-identity proven; Linux CI lanes restored; Platform BCL-only self-guard added; K0 guard untouched |
|
|
||||||
| LA1 | **DONE 2026-08-14** | `db9ad53c` (mixed — see `e1322a06`), `75a6724d` (recovery WIP), `d511e4c3`, ledger `890cf267` | Initial review FIX FIRST; F1–F8 CLOSED; narrow dual-lens re-review PASS | Release build green (0 errors / 18 warnings). Windows: Runtime 1634 / Headless 127 / App 5038+3skip. WSL: Runtime 1634 / Headless 127. Known mid-play silent-wire-drop limitation recorded above. The LA1+LA3 composer-to-both-hosts contract gate and portable CI lane landed at `8a03a25f`. |
|
|
||||||
| LA2 | **DONE + MERGED 2026-08-14** | `c6019424` (recovery WIP), `000ea979`, `1c5e66c0`, merge `e01b2cd1` | Dual-lens review FIX FIRST; all 3 findings CLOSED; final narrow re-review PASS | Probe success requires a reported roster and remains before selection/EnterWorld; terminal status derives from the actual start outcome; conditional fields distinguish omission from explicit null without weakening strict JSON. Branch gates: Runtime 1,632/1,632 and Headless 149/149 on both Windows and Ubuntu/WSL. Integrated gates: Release solution build green; Windows Runtime 1,636/1,636, Headless 151/151, App 5,039+3 skip, Launcher.Core 114/114; WSL Runtime 1,636/1,636, Headless 151/151, Launcher.Core 114/114. Repeated live ACE probe remains the LA11 user gate. |
|
|
||||||
| LA3 | **DONE + MERGED 2026-08-14** | `37d74e44`, `26feba81`, `347a1a5d`, merge `7749545d`, seam `8a03a25f` | Initial 12 findings CLOSED; four-gap narrow review FIX FIRST; final narrow re-review PASS | `AcDream.Launcher.Core` remains BCL + Platform only. Windows/WSL Core 114/114; full Release build green. Composer output is parsed by BOTH real host loaders from one linked fixture; Launcher.Core build/tests run in the portable Windows+Ubuntu lane. Windows graceful-stop gap remains tracked as #397. |
|
|
||||||
| LA4 | **DONE + MERGED 2026-08-14** | `d0a9c65d`, `10a712d6`, `ae2cbbee`, merge `60f62799` | Initial dual-lens review found 10 issues; fix re-review left one Linux execute-bit gap; final narrow re-review PASS | Avalonia 12.1.1 launcher remains thin over one BCL-only Core orchestrator. Windows/WSL Launcher.Core 162/162 and Launcher 17/17. Native `linux-x64` publish evaluates self-contained + single-file, runs without a discoverable runtime, and CI verifies executable launcher/App/Headless artifacts. LA9/LA10 bodies and LA11 visual/accessibility confirmation remain intentionally later. |
|
|
||||||
| LA5 | **DONE + MERGED 2026-08-14** | `95f4be94`, `fbe9c8a2`, `f820eb25`, merge `5535d0ad` | Initial review found 5 issues; first narrow re-review found 4 ownership/race gaps; final narrow re-review PASS | Both hosts share exact absent/null=`all`, `[]`=`none` allow-listing; transactional scoped UI/entity/selection rollback precedes unload; graphical/headless status and teardown ordering match; headless replay is exact-once under Runtime's borrowed membership lease. Branch complete suite 13,679+4 skip; portable WSL closure green. |
|
|
||||||
| LA6 | **DONE + MERGED 2026-08-14** | `41b15efd`, `259f0e5a`, merge `2bb8ccb6` | Dual-lens/CH regression review found one Headless wire-parity gap; narrow re-review PASS | Runtime owns the sole parser/router/catalog and shared four-route live binding. Both hosts run generation-scoped login commands after world entry with strict monotonic delay and nonterminal v1 failure status. Headless permit/chat/notell semantics match App. Branch complete suite 13,787+4 skip; WSL Runtime 1,662, Headless 165, Launcher.Core 167, UI/chat 922. |
|
|
||||||
| LA7 | **DONE + MERGED 2026-08-14** | LA7a `6a32f375`, `4338b1c1`, `0c8643a7`, merge `fa2de1c4`; LA7b `0e82cbf7`, `1b9e7e41`, `ff406562`, merge `7691cf75` | LA7a retail-lens PASS; LA7b review found 4 issues, first narrow pass left one restore/delete interleave, final narrow re-review PASS; AD-97 filed | Runtime owns the sole generation-scoped pre-world selection graph. Exact retail roster/grey/button/delete/restore behavior and queue routing are preserved; `NumErrors` is a sentinel, paused selection retains reliable transport sweeping, silent restore cannot block, and App has no mirror. Windows Runtime 1,653, Core.Net 958, App 5,042+3 skip; WSL Runtime/Core.Net green. |
|
|
||||||
| LA8 | **DONE + MERGED 2026-08-14** | `6cfab727`, `aeac874d`, `1dd5706e`, merge `fe63ce18` | Initial retail/architecture review found 4 issues; first narrow re-review left 2 retry-transaction/order gaps; final narrow re-review PASS | Installed DAT enum table 5 proves `0x10000005 -> 0x21000004`, root `0x1000039A`, exact flat list/buttons/templates/dialog assets, and no viewport. Runtime remains the only selection owner; row sizing, modal priority/retry, restore ordering, reset/disposal, and explicit live-DAT skip/probe are covered. Branch full suite 13,796+5 skip; LA11 owns physical visual/live-ACE acceptance. |
|
|
||||||
| LA9 | **DONE + MERGED 2026-08-14** | `ff6ebb6a`, `3f688951`, `208a70ac`, merge `2198a0cc` | Initial integrity review found 5 issues; narrow re-review left one orphan-child publication race; final narrow re-review PASS | First-run installer validates four DATs, consumes strict v1 Bake JSONL, preserves/reverifies SHA+size+tool-version records, and co-publishes self-contained launcher+Bake. Cross-process install/publish locks plus durable nonce prevent post-recovery mutation across real parent-only hard kills on Windows/Linux. Branch full suite 13,799+4 skip; real retail-DAT bake remains LA11. |
|
|
||||||
| LA10 | **DONE + MERGED 2026-08-14** | `2d2a5b50`, `1955ca8a`, `09d84387`, merge `da4fb3de` | Initial architecture/security review found 10 crash, trust, integrity, cleanup, and lifecycle issues; first narrow re-review left one rollback-source P1; final narrow re-review PASS | Production feeds and redirects are HTTPS-only, fixture loopback trust is explicit, downloads and archives are bounded and verified, version activation and rollback are atomic, active sessions hold the cross-process update lease, and schema-v3 self-update recovery verifies every prior/replacement file before apply, rollback, or restart. Real Windows/Linux process tests cover kill boundaries, staging races, lease deferral, corrupt backups, junctions/symlinks, and fail-closed recovery. Branch gates: Core 302/302 and Launcher 29/29 on Windows/WSL, full Release 13,945+4 skip, win/linux self-contained publishes. Integrated LA0–LA10 gate: 13,972+5 skip. |
|
|
||||||
| LA11 | **AUTOMATED CLOSEOUT REVIEW-CLOSED + MERGED 2026-08-15 — USER GATE PENDING** | `f881e5b4`, `134edabe`, `accd01a0`, `9f9c1167`, merge `d39f3098` | Initial dual-lens review found 7 startup/evidence/safety issues; first narrow re-review left 2 PID-reuse/ZIP-mode gaps; final narrow re-review PASS | Strict isolated roots and process-local feed override compose one exact launcher path graph. Windows targeted CTRL_BREAK is group-isolated and preserves stdin; exact-PID/start-identity status validation, credential-value scanning, deterministic Unix-mode A/B fixtures, Windows/native-Linux helper safety, and the exact A–I operator script are implemented. Clean branch preflight passed 32/32 with 13,985 tests + 4 skips. Integrated clean-head preflight at `a22f5411` passed 32/32 with 14,012 tests + 5 skips and report SHA-256 `49f225bc6043b9256f17b7bf0f29df919c894b8355633077751fd279756470df`. No connected/UI/real-DAT row has run; campaign shipment and #397 closure remain pending the user gate. |
|
|
||||||
|
|
@ -1,691 +0,0 @@
|
||||||
# Release stabilization and human-maintainability campaign
|
|
||||||
|
|
||||||
**Status:** PROPOSED — plan recorded; implementation not started
|
|
||||||
**Created:** 2026-08-18
|
|
||||||
**Audit baseline:** `15539a22a67f8d915d88f8b1d8126cd55eedda6e`
|
|
||||||
**Evidence:** [`../reviews/2026-08-17-release-maintainability-audit.md`](../reviews/2026-08-17-release-maintainability-audit.md)
|
|
||||||
**Findings:** [`../reviews/findings-ledger.md`](../reviews/findings-ledger.md)
|
|
||||||
**Coverage proof:** [`../reviews/coverage-ledger.md`](../reviews/coverage-ledger.md)
|
|
||||||
|
|
||||||
## 1. Goal
|
|
||||||
|
|
||||||
Prepare acdream for a responsible public release and for maintenance by human
|
|
||||||
developers who do not have access to prior AI conversations, private worktrees,
|
|
||||||
or campaign memory.
|
|
||||||
|
|
||||||
The campaign succeeds when a new maintainer can clone the repository, identify
|
|
||||||
the current architecture and supported release, reproduce the build and test
|
|
||||||
gate, understand why non-obvious retail behavior exists, and publish or roll
|
|
||||||
back an authenticated release using repository-owned instructions.
|
|
||||||
|
|
||||||
This is a stabilization program, not a rewrite. The existing Runtime/App/
|
|
||||||
Headless architecture remains the foundation unless a slice proves a specific
|
|
||||||
boundary is wrong.
|
|
||||||
|
|
||||||
## 2. Binding principles
|
|
||||||
|
|
||||||
1. **Protect behavior before cleanup.** Establish a deterministic complete gate
|
|
||||||
before broad refactors, comment cleanup, or file decomposition.
|
|
||||||
2. **Distill knowledge; do not erase it.** No note, comment, issue history,
|
|
||||||
diagnostic, capture, or raw artifact is removed until its durable value has
|
|
||||||
a verified destination.
|
|
||||||
3. **One current truth.** Stable architecture and release state must not depend
|
|
||||||
on choosing between README, roadmap, milestone, campaign, memory, or
|
|
||||||
tool-specific instruction copies.
|
|
||||||
4. **Separate evidence from contracts.** Source comments explain the current
|
|
||||||
invariant. Dated research records preserve investigation history. Raw
|
|
||||||
captures live in an explicit artifact tier.
|
|
||||||
5. **No count-only gates.** A test total is meaningful only when the report says
|
|
||||||
which hermetic, installed-DAT, live, visual, manual, and diagnostic lanes
|
|
||||||
actually ran.
|
|
||||||
6. **Bound every external interaction.** Process waits, network operations,
|
|
||||||
test runs, and release steps require timeouts, cancellation, and diagnostic
|
|
||||||
artifacts on failure.
|
|
||||||
7. **Small reversible slices.** Each slice gets focused tests, a complete gate,
|
|
||||||
a reviewable commit, a rollback description, and a plan-ledger update.
|
|
||||||
8. **No opportunistic feature work.** New gameplay features wait unless needed
|
|
||||||
to prove or repair a release blocker.
|
|
||||||
|
|
||||||
## 3. Knowledge-preservation protocol
|
|
||||||
|
|
||||||
Every cleanup candidate is classified before it moves:
|
|
||||||
|
|
||||||
| Class | Durable value | Destination |
|
|
||||||
|---|---|---|
|
|
||||||
| Current invariant | Required behavior, ordering, ownership, threading, or retail rule | Short source comment and/or maintained architecture contract |
|
|
||||||
| Decision rationale | Alternatives considered, failed attempts, tradeoffs, gate outcome | Dated decision/research record linked from the current contract |
|
|
||||||
| Reproducible evidence | Minimal fixture, retail symbol/address, script, checksum, expected result | Versioned fixture/research record in Git |
|
|
||||||
| Raw evidence | Large logs, captures, Ghidra state, screenshots, dumps | Approved versioned artifact store with manifest, hash, provenance, and retention policy |
|
|
||||||
| Superseded or incorrect claim | Historically useful but no longer operative | Marked `SUPERSEDED` with successor link; archive after references are migrated |
|
|
||||||
| Duplication/noise | Repeats a preserved fact and adds no independent evidence | Delete only after destination/link validation |
|
|
||||||
|
|
||||||
Before deleting or rewriting historical material, all of these must be true:
|
|
||||||
|
|
||||||
- its current invariant is recorded at the owning code or architecture seam;
|
|
||||||
- useful retail provenance, failed approaches, and acceptance evidence remain
|
|
||||||
searchable under stable identifiers;
|
|
||||||
- inbound links and source comments point at the surviving destination;
|
|
||||||
- any raw artifact has an approved distribution, privacy, and licensing status;
|
|
||||||
- the replacement was reviewed by someone other than its author;
|
|
||||||
- the complete gate passes after the move.
|
|
||||||
|
|
||||||
Git history alone is not the preservation mechanism. History may later be
|
|
||||||
rewritten to remove large or legally restricted artifacts.
|
|
||||||
|
|
||||||
## 4. Campaign dependency map
|
|
||||||
|
|
||||||
```text
|
|
||||||
R0 baseline/authority
|
|
||||||
-> R1 launcher deadlock
|
|
||||||
-> R2 reproducible complete gate
|
|
||||||
-> R3 truthful test lanes
|
|
||||||
-> R5 documentation authority
|
|
||||||
-> R6 dead surfaces and tools
|
|
||||||
-> R7 plugin/config hardening
|
|
||||||
-> R8 bounded decomposition
|
|
||||||
-> R10 release candidate
|
|
||||||
|
|
||||||
R4 licence/provenance/release governance ---------------------> R10
|
|
||||||
R9 artifact migration (depends on R4 decisions) --------------> R10
|
|
||||||
```
|
|
||||||
|
|
||||||
R4 starts in parallel because it requires owner/legal decisions. It blocks a
|
|
||||||
public release but does not block the technical safety work in R1–R3.
|
|
||||||
|
|
||||||
## 5. Slice map
|
|
||||||
|
|
||||||
| Slice | Outcome | Depends on | Release blocking |
|
|
||||||
|---|---|---|---|
|
|
||||||
| R0 | Baseline and durable campaign authority accepted | — | yes |
|
|
||||||
| R1 | Launcher shutdown lock inversion fixed and deterministic | R0 | yes |
|
|
||||||
| R2 | Pinned clean build and complete bounded CI gate | R1 | yes |
|
|
||||||
| R3 | Test results truthfully distinguish executed, skipped, and diagnostic work | R2 | yes |
|
|
||||||
| R4 | Licence, provenance, credential disclosure, and release ownership decided | R0; parallel | yes |
|
|
||||||
| R5 | One current documentation authority; public docs match the product | R2–R3 | yes |
|
|
||||||
| R6 | Dead presentation/backend/probe surfaces removed; supported tools reproducible | R3–R5 | normally yes |
|
|
||||||
| R7 | Plugin compatibility/lifetime and diagnostic configuration hardened | R3 | yes for advertised plugin release |
|
|
||||||
| R8 | Highest-risk giant owners decomposed only at proven seams | R3, R6–R7 | selective |
|
|
||||||
| R9 | Large/generated research artifacts moved under an approved policy | R4 | yes if repository is distributed |
|
|
||||||
| R10 | Clean-clone release candidate and rollback rehearsal | all blocking slices | yes |
|
|
||||||
|
|
||||||
## 6. R0 — Baseline and authority
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Review and accept this plan and the six documents under `docs/reviews/`.
|
|
||||||
- Record the exact starting commit, SDK, package sources, operating systems,
|
|
||||||
supported release platforms, and current external prerequisites.
|
|
||||||
- Decide who owns legal/provenance decisions, CI/release credentials, and final
|
|
||||||
release approval.
|
|
||||||
- Pause unrelated feature campaigns until R1–R3 establish the safety net.
|
|
||||||
- Preserve the audit baseline before any cleanup; do not rewrite artifact
|
|
||||||
history in this slice.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- Plan and audit artifacts are tracked in the repository.
|
|
||||||
- One named owner exists for technical release approval and one for
|
|
||||||
licensing/provenance approval; the same person may hold both roles.
|
|
||||||
- The ledger in §17 identifies R1 as the only active implementation slice.
|
|
||||||
|
|
||||||
## 7. R1 — Fix the launcher shutdown deadlock first
|
|
||||||
|
|
||||||
### Confirmed failure
|
|
||||||
|
|
||||||
`LauncherProcessSupervisor.Dispose` currently holds the supervisor `_gate`
|
|
||||||
while disposing a child. `WindowsSystemChildProcess.Dispose` enters
|
|
||||||
`System.Diagnostics.Process.Dispose`; concurrently, the process-exit callback
|
|
||||||
can enter `OnProcessExited` and try to publish state through the same supervisor
|
|
||||||
gate. The captured wait cycle is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
shutdown: supervisor _gate -> Process internals
|
|
||||||
exit callback: Process internals -> supervisor _gate
|
|
||||||
```
|
|
||||||
|
|
||||||
The complete solution test process hangs on
|
|
||||||
`ANullStderrLogPathBehavesExactlyAsBeforeForBothChildProcessKinds`; the
|
|
||||||
Launcher.Core project can pass alone because the race timing changes.
|
|
||||||
|
|
||||||
### Design constraints
|
|
||||||
|
|
||||||
- Never call child/process operations that may wait, dispose, raise callbacks,
|
|
||||||
or execute external code while holding `_gate`.
|
|
||||||
- Under `_gate`, make only the minimal state transition and snapshot the exact
|
|
||||||
children/work to retire.
|
|
||||||
- Perform unsubscribe, stop, kill, wait, and dispose work outside `_gate`.
|
|
||||||
- Exit callback and explicit disposal must converge idempotently regardless of
|
|
||||||
which arrives first.
|
|
||||||
- Preserve exact terminal-state ordering, status publication, graceful-stop
|
|
||||||
behavior, and child ownership; do not solve the hang by dropping callbacks.
|
|
||||||
- A failed cleanup must remain observable without starving later cleanup.
|
|
||||||
|
|
||||||
### Required tests
|
|
||||||
|
|
||||||
- Add a barrier-controlled race test that deterministically pauses the child
|
|
||||||
exit callback while disposal begins. Do not use `Thread.Sleep` as the oracle.
|
|
||||||
- Cover exit-before-dispose, dispose-before-exit, simultaneous exit/dispose,
|
|
||||||
repeated dispose, stop timeout/kill fallback, and callback failure.
|
|
||||||
- Assert one terminal publication, no resurrection, no orphan child, no held
|
|
||||||
supervisor lock during process disposal, and bounded completion.
|
|
||||||
- Run the focused race test repeatedly after its deterministic single pass.
|
|
||||||
- Run all Launcher.Core tests.
|
|
||||||
- Run the complete Release solution twice in fresh processes under a documented
|
|
||||||
timeout and retain hang dumps if either run fails to terminate.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- No process/child disposal occurs under the supervisor gate.
|
|
||||||
- The deterministic race test fails against the baseline mechanism and passes
|
|
||||||
against the fix.
|
|
||||||
- Two complete bounded solution runs finish with zero failures.
|
|
||||||
- F-009 and T-001 receive exact fix commit and gate evidence.
|
|
||||||
|
|
||||||
## 8. R2 — Reproducible build and complete CI gate
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Pin the accepted .NET 10 SDK feature band in `global.json`.
|
|
||||||
- Centralize common compiler/analyzer settings and package versions; enable
|
|
||||||
locked restore for release/CI.
|
|
||||||
- Eliminate all 26 clean-rebuild test warnings or make a narrowly justified,
|
|
||||||
centrally documented exception fail-safe.
|
|
||||||
- Build every supported product and every supported tool from a clean checkout.
|
|
||||||
- Make CI run the complete solution, not only portability subsets. Give each
|
|
||||||
project/process a timeout and collect test logs plus managed dumps on hangs.
|
|
||||||
- Preserve focused Windows/Linux portability lanes, but do not represent them
|
|
||||||
as the complete gate.
|
|
||||||
- Record restore sources, SDK/runtime, RID, commit, executed/skipped counts, and
|
|
||||||
artifact hashes in every release report.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- A clean clone restores and builds with the pinned toolchain and no warnings.
|
|
||||||
- Complete CI runs every default release test project and fails on timeout.
|
|
||||||
- The launcher hang cannot silently consume the CI job indefinitely.
|
|
||||||
- Package resolution and the gate command are reproducible from repository
|
|
||||||
instructions alone.
|
|
||||||
|
|
||||||
## 9. R3 — Make test reporting truthful
|
|
||||||
|
|
||||||
### Required lanes
|
|
||||||
|
|
||||||
1. **Hermetic release lane:** default CI; unavailable local data is never a
|
|
||||||
silent passing return.
|
|
||||||
2. **Installed-DAT/prepared-package lane:** explicit prerequisites and per-suite
|
|
||||||
skip identity; result published separately.
|
|
||||||
3. **Live/connected/visual/listening lane:** operator-owned, dated evidence;
|
|
||||||
never counted as ordinary unit coverage.
|
|
||||||
4. **Diagnostic/manual lane:** probes, dumps, fixture generators, and
|
|
||||||
characterization programs invoked explicitly outside default test totals.
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Replace the 271 asset/environment empty-return tests with explicit lane
|
|
||||||
requirements, truthful skips, or hermetic fixtures.
|
|
||||||
- Move or give stable assertions to the 51 output-only diagnostic methods.
|
|
||||||
- Delete/replace the three confirmed useless entire cases and the tautological
|
|
||||||
assertions catalogued in the audit.
|
|
||||||
- Remove the duplicate theory row and make the clean rebuild warning-free.
|
|
||||||
- Remove or re-home at least 52 tests with the unreachable panel stack.
|
|
||||||
- Retire temporary source-shape freezes once a semantic architecture/behavior
|
|
||||||
guard exists; retain whole-tree dependency guards that express real rules.
|
|
||||||
- Assign and stabilize the seven documented load-sensitive tests. Do not hide
|
|
||||||
them with generic retries.
|
|
||||||
- Inject controllable time into double-click and real-time transport tests.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- Default test success means every discovered default contract executed.
|
|
||||||
- Reports give exact reasons and prerequisite identity for every skip.
|
|
||||||
- Diagnostics and manual generators do not inflate release regression totals.
|
|
||||||
- No known duplicate row, literal tautology, or permanent empty scaffold remains
|
|
||||||
in the default suite.
|
|
||||||
|
|
||||||
## 10. R4 — Licence, provenance, security, and release ownership
|
|
||||||
|
|
||||||
This slice requires explicit project-owner decisions and, where appropriate,
|
|
||||||
qualified legal review. The implementation agent records evidence but does not
|
|
||||||
invent a redistribution basis.
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Select the project licence and establish contributor/code ownership.
|
|
||||||
- Audit WorldBuilder-derived code, dependency notices, named-retail/decompiler
|
|
||||||
exports, PDB-derived data, Ghidra databases, captures, images, and DAT-derived
|
|
||||||
fixtures for provenance and redistribution status.
|
|
||||||
- Decide which research artifacts may be public, private, regenerated, or
|
|
||||||
deleted from distributable history.
|
|
||||||
- Add SECURITY, CONTRIBUTING, changelog/version authority, disclosure and
|
|
||||||
deletion behavior for plaintext launcher credentials, and a vulnerability
|
|
||||||
response path.
|
|
||||||
- Define release artifact contents, supported platforms, SBOM/provenance,
|
|
||||||
signing/attestation policy, checksums, update manifest generation, rollback,
|
|
||||||
and release approval.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- Publicly distributed source and artifacts have an approved licence and
|
|
||||||
complete notices/provenance inventory.
|
|
||||||
- Users are told exactly how credentials are stored and removed.
|
|
||||||
- The launcher updater's production manifest and archives are generated and
|
|
||||||
verified by a repository-owned release process.
|
|
||||||
|
|
||||||
## 11. R5 — One current documentation authority
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Correct public README claims to the actual Vulkan-only client and retained UI.
|
|
||||||
- Make `docs/README.md` stable navigation plus a generated current-status block
|
|
||||||
sourced from one structured milestone/release ledger.
|
|
||||||
- Keep architecture documents limited to durable boundaries, ownership,
|
|
||||||
threading/lifetime, and data flow. Move commit/test-count/rollback chronology
|
|
||||||
to dated closeout records.
|
|
||||||
- Replace duplicated `AGENTS.md`/`CLAUDE.md` product truth with one maintained
|
|
||||||
tool-neutral source and generated thin adapters; fail CI on drift.
|
|
||||||
- Mark every memory/plan/spec as current, active, superseded, or historical.
|
|
||||||
- Normalize active issue/divergence indexes and validate their IDs, statuses,
|
|
||||||
paths, and links mechanically.
|
|
||||||
- Apply the knowledge-preservation protocol before shortening any campaign or
|
|
||||||
issue record.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- A new maintainer receives the same current answer from README, documentation
|
|
||||||
map, architecture, milestone/status ledger, and agent instructions.
|
|
||||||
- No current authority links missing private `claude-memory`, absent skills, or
|
|
||||||
developer-local paths.
|
|
||||||
- Historical records remain searchable but cannot override current truth.
|
|
||||||
|
|
||||||
## 12. R6 — Dead surfaces, diagnostics, and tools
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Decide and then remove or explicitly support the unreachable
|
|
||||||
`IPanelRenderer`/old panel stack and its tests.
|
|
||||||
- Remove stale OpenGL/framebuffer/ImGui apparatus and failed temporary-cleanup
|
|
||||||
markers from shipping assemblies after preserving useful evidence.
|
|
||||||
- Stop including the smoke plugin in release output by default.
|
|
||||||
- Classify every tool/script as supported, diagnostic, research-only, or
|
|
||||||
archived. Repair the five broken C# tools chosen as supported; remove
|
|
||||||
developer-home/old-worktree paths and document exact prerequisites.
|
|
||||||
- Centralize environment/diagnostic configuration at composition roots.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- Shipping assemblies and package contents contain no abandoned presentation
|
|
||||||
backend or sample plugin by accident.
|
|
||||||
- Every supported tool builds from the pinned clean checkout.
|
|
||||||
- Research-only tools are clearly invoked outside the release build.
|
|
||||||
|
|
||||||
## 13. R7 — Plugin and configuration contracts
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Enforce supported plugin API versions before loading code.
|
|
||||||
- Implement manifest dependencies or remove the unsupported promise.
|
|
||||||
- Publish/version `AcDream.Plugin.Abstractions` if plugins are advertised.
|
|
||||||
- Report registration cleanup and callback failures without preventing
|
|
||||||
best-effort teardown; prove collectible load-context release under failures.
|
|
||||||
- Replace absent/null allow-list ambiguity with one explicit production default.
|
|
||||||
- Replace direct hot-path environment reads/process-static mutable diagnostics
|
|
||||||
with immutable session-scoped configuration and typed sinks.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- An incompatible plugin fails before activation with an actionable message.
|
|
||||||
- Disable/dispose reports all cleanup failures and cannot silently retain host
|
|
||||||
registrations.
|
|
||||||
- Graphical and headless hosts have the same documented plugin/config default.
|
|
||||||
|
|
||||||
## 14. R8 — Bounded structural decomposition
|
|
||||||
|
|
||||||
This slice begins only after R1–R3. File size alone does not authorize a split.
|
|
||||||
|
|
||||||
### Priority candidates
|
|
||||||
|
|
||||||
- `RuntimeSetPositionState`
|
|
||||||
- `TransitionTypes`
|
|
||||||
- `RetailUiRuntime`
|
|
||||||
- `LiveEntityRuntime`
|
|
||||||
- `WorldSession`
|
|
||||||
- `WbDrawDispatcher`
|
|
||||||
|
|
||||||
### Rules
|
|
||||||
|
|
||||||
- Identify one ownership/lifetime or pure-algorithm seam at a time.
|
|
||||||
- Preserve a single state owner; do not replace a large class with mirrored
|
|
||||||
mutable state or a service graph of aliases.
|
|
||||||
- Prefer partial-file navigation when a state machine must remain one owner.
|
|
||||||
- Establish behavior/sabotage tests before extraction and remove corresponding
|
|
||||||
temporary source-text freezes afterward.
|
|
||||||
- Replace Chorizite/GL vocabulary in prepared-content DTOs with versioned
|
|
||||||
acdream-owned semantics at a separately reviewed boundary.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- Each extraction reduces change coupling or improves ownership clarity; line
|
|
||||||
count reduction alone is not success.
|
|
||||||
- Runtime behavior, retail evidence, allocations, and teardown ledgers remain
|
|
||||||
equivalent under the relevant focused and complete gates.
|
|
||||||
|
|
||||||
## 15. R9 — Repository artifact migration
|
|
||||||
|
|
||||||
### Work
|
|
||||||
|
|
||||||
- Inventory the approximately 575 MiB of Ghidra state, 299 MiB research tree,
|
|
||||||
and 151 MiB tracked logs by provenance, sensitivity, reproducibility, and
|
|
||||||
ongoing value.
|
|
||||||
- Keep compact fixtures, scripts, tool versions, summaries, and checksums in
|
|
||||||
Git. Move approved raw bundles to a versioned artifact store.
|
|
||||||
- Add a manifest/bootstrap command that verifies artifact identity.
|
|
||||||
- Define retention, redaction, access, and backup policy.
|
|
||||||
- Treat Git-history rewriting as a separately approved migration with backup,
|
|
||||||
contributor coordination, remote replacement, and verification. Never do it
|
|
||||||
as an incidental cleanup command.
|
|
||||||
|
|
||||||
### Exit criteria
|
|
||||||
|
|
||||||
- A normal clone contains what build/test/maintenance requires without opaque
|
|
||||||
generated databases or raw logs.
|
|
||||||
- Authorized researchers can retrieve exact approved evidence by manifest and
|
|
||||||
hash.
|
|
||||||
- Restricted or non-redistributable material is absent from public history.
|
|
||||||
|
|
||||||
## 16. R10 — Release-candidate gate
|
|
||||||
|
|
||||||
From a fresh clone on every supported release platform:
|
|
||||||
|
|
||||||
- restore with the pinned, locked toolchain;
|
|
||||||
- build every shipped product and supported tool with zero warnings;
|
|
||||||
- run the complete hermetic test lane with no failures, silent no-ops, hangs,
|
|
||||||
or generic retries;
|
|
||||||
- run and report the applicable DAT, connected, visual/listening, updater,
|
|
||||||
installer, graceful-shutdown, and rollback gates;
|
|
||||||
- generate versioned per-RID packages, plugin abstraction package if supported,
|
|
||||||
SBOM/provenance/checksums, and updater manifest;
|
|
||||||
- install/update/rollback using only public release instructions;
|
|
||||||
- verify package contents contain no credentials, developer paths, smoke
|
|
||||||
plugin, raw probes, or unapproved research artifacts;
|
|
||||||
- obtain technical and licensing/provenance approval.
|
|
||||||
|
|
||||||
Public release remains **NO-GO** until every blocking slice is closed.
|
|
||||||
|
|
||||||
## 17. Cross-session execution ledger
|
|
||||||
|
|
||||||
This table is the resume authority. Update it in the same commit as every slice
|
|
||||||
checkpoint. Do not infer status from chat history.
|
|
||||||
|
|
||||||
| Slice | Status | Commit(s) | Evidence/gates | Exact next action |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| R0 | PLAN ACCEPTED; R4 OWNER DECISION DEFERRED | — | Audit complete at `15539a22`; user completed R1–R3 | Retain the plan and audit artifacts as campaign authority |
|
|
||||||
| R1 | COMPLETE; MERGE AUTHORIZED 2026-08-18 | `0a934cf5` | 2026-08-18 checkpoint below; F-009/T-001 resolved and committed | Merge with the complete R1–R3 stabilization branch |
|
|
||||||
| R2 | COMPLETE; MERGE AUTHORIZED 2026-08-18 | `2ac05486`, `c38f6b88` | Checkpoints below; F-014/F-019 resolved, F-010 machine-readable, supported .NET tool portion of F-004 resolved | Merge with the complete R1–R3 stabilization branch |
|
|
||||||
| R3 | COMPLETE; MERGE AUTHORIZED 2026-08-18 | `14d371a0`, `b64c8041` | Final inventory: 1,254 files, 11,414 attributed methods, 22 approved source readers; Release gate: 14,346/14,346 | Merge the stabilization branch; retain the closeout ledger as authority |
|
|
||||||
| R4 | DEFERRED BY USER FOR FRIEND-ONLY RELEASE | — | F-001/F-026/F-031/F-034 remain public-release blockers | Resume before any public release; keep credentials and research artifacts out of friend packages |
|
|
||||||
| R5 | DEFERRED BY USER | — | F-002/F-003/F-006/F-007/F-011/F-020/F-027/F-029/F-032 | Resume later with the bounded documentation-authority goal |
|
|
||||||
| R6 | NOT STARTED | — | F-004/F-005/F-012/F-016/F-023/F-028 | Wait for R3/R5 |
|
|
||||||
| R7 | NOT STARTED | — | F-017/F-018/F-024/F-025 | Wait for R3 |
|
|
||||||
| R8 | NOT STARTED | — | F-008/F-013/F-033 | Wait for R1–R3 and cleanup decisions |
|
|
||||||
| R9 | NOT STARTED | — | F-034 plus R4 provenance decisions | Wait for R4 |
|
|
||||||
| R10 | NOT STARTED | — | All blocking findings | Wait for blocking slices |
|
|
||||||
|
|
||||||
### R1 implementation checkpoint — 2026-08-18
|
|
||||||
|
|
||||||
**Working-tree base:** `15539a22a67f8d915d88f8b1d8126cd55eedda6e`
|
|
||||||
**Commit:** `0a934cf5` on `codex/release-stabilization`; the checkpoint is
|
|
||||||
durable on that campaign branch but is not yet merged to `main`.
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
|
|
||||||
- `LauncherProcessSupervisor.Dispose` now transfers `_process` ownership to a
|
|
||||||
local and clears the field under `_gate`, then performs stop, event removal,
|
|
||||||
and child disposal outside the gate.
|
|
||||||
- Public `Stop` and disposal share one `StopProcess` implementation, preserving
|
|
||||||
graceful-stop, close-window, timeout, kill, and post-kill observation order.
|
|
||||||
- `OnProcessExited` reads the event sender's optional exit code without holding
|
|
||||||
`_gate`, then commits the terminal transition through the existing ordered
|
|
||||||
state publisher. A callback already captured during teardown may therefore
|
|
||||||
finish instead of forming the supervisor/Process lock cycle.
|
|
||||||
- `DisposeAllowsAnAlreadyCapturedExitCallbackToComplete` uses explicit barriers:
|
|
||||||
the fake captures the exit delegate before unsubscription; its disposal
|
|
||||||
releases the callback and waits for it to return. There are no timing sleeps
|
|
||||||
in the oracle, and an emergency release keeps failure against the old code
|
|
||||||
bounded rather than wedging the test host.
|
|
||||||
|
|
||||||
Sabotage and focused evidence:
|
|
||||||
|
|
||||||
- With the old lock shape temporarily restored and the new test retained, the
|
|
||||||
test failed with its expected five-second timeout. The fake's cleanup barrier
|
|
||||||
then released the old cycle, so the test process exited normally.
|
|
||||||
- With the fix restored, the same test passed in 21 ms.
|
|
||||||
- The race test passed 25/25 times in fresh `dotnet test` processes.
|
|
||||||
- All `LauncherProcessSupervisorTests` passed: 22/22.
|
|
||||||
- Complete Launcher.Core passed under a 180-second hard process bound:
|
|
||||||
339 passed / 0 skipped / 0 failed in 52 seconds.
|
|
||||||
|
|
||||||
Complete-solution evidence:
|
|
||||||
|
|
||||||
- Release build completed inside a 300-second bound with 0 warnings / 0 errors
|
|
||||||
in the evaluated incremental build.
|
|
||||||
- The exact serialized command was
|
|
||||||
`dotnet test AcDream.slnx -c Release --no-build --no-restore --nologo -m:1`,
|
|
||||||
launched in a fresh child process for each run. An outer process watchdog
|
|
||||||
allowed 900 seconds, killed the complete process tree on expiry, and treated
|
|
||||||
timeout as failure.
|
|
||||||
- Run 1: 12 assemblies, 14,748 passed / 77 skipped / 0 failed, 1:28.822,
|
|
||||||
bounded exit code 0.
|
|
||||||
- Run 2: 12 assemblies, 14,748 passed / 77 skipped / 0 failed, 1:30.241,
|
|
||||||
bounded exit code 0.
|
|
||||||
|
|
||||||
Adjacent evidence, deliberately not folded into R1:
|
|
||||||
|
|
||||||
- One default-parallel whole-solution run terminated normally in 58.148
|
|
||||||
seconds—important negative evidence for the former hang—but failed one
|
|
||||||
`AcDream.Launcher.Tests` Avalonia headless cleanup because a compositor was
|
|
||||||
accessed from a non-owning thread. The Launcher test project then passed
|
|
||||||
67/67 alone. R1 makes no Avalonia changes; R2/R3 must decide the supported CI
|
|
||||||
scheduling and ownership of that pre-existing parallel-run failure.
|
|
||||||
- The known duplicate Core theory row and 77 skip classifications remain
|
|
||||||
unchanged and belong to R3. R1 does not use their headline count as proof of
|
|
||||||
test quality.
|
|
||||||
|
|
||||||
Changed implementation/test files:
|
|
||||||
|
|
||||||
- `src/AcDream.Launcher.Core/Launching/LauncherProcessSupervisor.cs`
|
|
||||||
- `tests/AcDream.Launcher.Core.Tests/Launching/LauncherProcessSupervisorTests.cs`
|
|
||||||
|
|
||||||
Rollback is `git revert 0a934cf5`; do not rewrite branch history.
|
|
||||||
|
|
||||||
### R2 complete-gate checkpoint — 2026-08-18
|
|
||||||
|
|
||||||
**Working-tree base:** `0a934cf5781c003375c14af9a1f565254df0f9f9`
|
|
||||||
**Commit:** `2ac05486` on `codex/release-stabilization`; the checkpoint is
|
|
||||||
durable on that campaign branch but is not yet merged to `main`.
|
|
||||||
|
|
||||||
Implemented gate:
|
|
||||||
|
|
||||||
- `global.json` pins the accepted .NET 10 SDK feature band at `10.0.300` with
|
|
||||||
`latestPatch` roll-forward and prerelease SDKs disabled. Every existing
|
|
||||||
`actions/setup-dotnet` step now reads that file instead of floating on
|
|
||||||
`10.0.x`.
|
|
||||||
- `tools/run-release-gate.ps1` discovers every project under `tests/` that
|
|
||||||
declares `Microsoft.NET.Test.Sdk` or `IsTestProject`, verifies the project is
|
|
||||||
present in `AcDream.slnx`, restores/builds the solution, and runs each test
|
|
||||||
assembly exactly once in its own Release process. It does not retry.
|
|
||||||
- Restore, build, and each test process have 600/900/600-second outer bounds.
|
|
||||||
Tests additionally use VSTest's 180-second per-test blame-hang collector with
|
|
||||||
mini dumps. An outer timeout kills the complete process tree and reports exit
|
|
||||||
code 124; GitHub Actions adds a 45-minute job bound.
|
|
||||||
- Every run writes exact commands and output, one TRX per assembly, any blame
|
|
||||||
sequence/dumps, `dotnet --info`, configured NuGet sources, commit/branch/RID,
|
|
||||||
aggregate executed/passed/skipped/failed counts, and `SHA256SUMS.txt`.
|
|
||||||
- `.github/workflows/release-gate.yml` runs the gate on pull requests, pushes
|
|
||||||
to `main`, and manual dispatch on `windows-latest`, then uploads the evidence
|
|
||||||
even when the gate fails. The focused Windows/Linux portability and Vulkan
|
|
||||||
lanes remain separate and are no longer the only deterministic CI coverage.
|
|
||||||
- `docs/release-gate.md` is the repository-owned local/CI runbook.
|
|
||||||
|
|
||||||
Test-isolation corrections, with no product behavior change:
|
|
||||||
|
|
||||||
- Four `MainWindowViewTests` that call `Show()` now close their window and pump
|
|
||||||
dispatcher cleanup in `finally` on the owning Avalonia test session. The old
|
|
||||||
tests leaked shown, thread-affine compositor state to runner teardown; no
|
|
||||||
suite serialization or retry was added.
|
|
||||||
- The complete gate's first evidence run correctly failed
|
|
||||||
`RealChildStderrIsCapturedForTheProcessStartInfoPath`: its live polling helper
|
|
||||||
briefly denied write sharing, so the final async stderr callback observed an
|
|
||||||
`IOException` and the deliberately no-throw capture sink latched off. The
|
|
||||||
helper now reads with `FileShare.ReadWrite | FileShare.Delete`, matching the
|
|
||||||
production status tailer; its assertions and five-second bound are unchanged.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- Focused `MainWindowViewTests`: 13 passed / 0 skipped / 0 failed.
|
|
||||||
- Three fresh default-parallel whole-solution runs completed inside independent
|
|
||||||
180-second process bounds with 12/12 TRX files and no Avalonia cleanup error:
|
|
||||||
56.719, 56.321, and 58.506 seconds. Each reported 14,748 passed / 77 skipped /
|
|
||||||
0 failed.
|
|
||||||
- The stderr ProcessStartInfo test passed 25/25 fresh-process repetitions after
|
|
||||||
the live-reader correction.
|
|
||||||
- The actual outer-watchdog function killed a controlled fixture process tree
|
|
||||||
at 2.107 seconds, returned 124, and left no child process.
|
|
||||||
- The repository command `pwsh ./tools/run-release-gate.ps1` completed in
|
|
||||||
107.337 seconds on SDK `10.0.300`, RID `win-x64`: 12 assemblies, 14,748
|
|
||||||
executed and passed / 77 skipped / 0 failed. The evidence manifest contains
|
|
||||||
28 verified SHA-256 entries.
|
|
||||||
|
|
||||||
Deliberately still open in the wider R2 slice:
|
|
||||||
|
|
||||||
- the 26 warnings observed by a clean recompilation, centralized compiler and
|
|
||||||
package settings, package lock files/locked restore, and broken or unsupported
|
|
||||||
tool-project decisions;
|
|
||||||
- the known duplicate Core theory row and classification of the 77 skips, which
|
|
||||||
remain R3 work; and
|
|
||||||
- stale public headline counts, which must be corrected with the documentation
|
|
||||||
authority work rather than hand-edited as part of this gate checkpoint.
|
|
||||||
|
|
||||||
Therefore this checkpoint closes F-014 on the campaign branch and the SDK part
|
|
||||||
of F-019, and gives F-010 a truthful machine-readable count. It does not claim
|
|
||||||
the broader R2 reproducibility slice or R3 test-quality cleanup is complete.
|
|
||||||
|
|
||||||
Rollback is `git revert 2ac05486`; do not rewrite branch history.
|
|
||||||
|
|
||||||
### R2 reproducibility closeout — 2026-08-18
|
|
||||||
|
|
||||||
**Working-tree base:** `b459e0cf0cab9241b0771d2ce6838083f2c84162`
|
|
||||||
|
|
||||||
**Implementation commit:** `c38f6b88522750abc2e4acf1898ed566c5576e4a`
|
|
||||||
on `codex/release-stabilization`; the closeout is durable on that campaign
|
|
||||||
branch but is not yet merged to `main`.
|
|
||||||
|
|
||||||
Repository policy and dependency graph:
|
|
||||||
|
|
||||||
- `Directory.Build.props` is the common .NET 10, language, nullable, latest
|
|
||||||
analysis, warnings-as-errors, deterministic-build, and lock-file authority.
|
|
||||||
- `Directory.Packages.props` centrally pins all 30 direct package versions.
|
|
||||||
All 89 `PackageReference` sites are versionless; no project-local version can
|
|
||||||
silently drift.
|
|
||||||
- `NuGet.Config` clears machine fallback folders and package sources, then
|
|
||||||
declares only `nuget.org`.
|
|
||||||
- Every supported project owns `packages.neutral.lock.json` (44 files). Every
|
|
||||||
shippable source project additionally owns `packages.win-x64.lock.json` and
|
|
||||||
`packages.linux-x64.lock.json` (14 of each; 72 graphs total). Conventional
|
|
||||||
`packages.lock.json` files are intentionally absent because NuGet gives that
|
|
||||||
filename precedence over `NuGetLockFilePath`, preventing adjacent neutral
|
|
||||||
and RID graphs.
|
|
||||||
- `tools/update-package-locks.ps1` is the one intentional update path. The
|
|
||||||
release gate and the launcher's nested Bake publish use forced locked restore,
|
|
||||||
so stale `obj/` assets cannot hide a disagreement and a normal gate cannot
|
|
||||||
rewrite dependency resolution.
|
|
||||||
|
|
||||||
Complete maintained build surface:
|
|
||||||
|
|
||||||
- All 13 tracked .NET tools were repaired against package/owned interfaces,
|
|
||||||
documented in `tools/README.md`, and added to `AcDream.slnx`.
|
|
||||||
- The gate now verifies that every `.csproj` under `src/`, `tests/`, and
|
|
||||||
`tools/` is a solution member, requires the expected lock graphs, and records
|
|
||||||
their hashes in the evidence bundle. The supported graph is 44 projects.
|
|
||||||
- The older script/probe archive under F-004 is unchanged. Classifying that
|
|
||||||
historical material remains R6 work; the R2 change only makes the maintained
|
|
||||||
.NET tools truthful and reproducible.
|
|
||||||
|
|
||||||
Warning cleanup and test-gate stability:
|
|
||||||
|
|
||||||
- A clean complete recompilation originally exposed 26 warnings, all in test
|
|
||||||
and diagnostic code. Assertion-specific analyzers, nullable test doubles,
|
|
||||||
and nullable DAT probe boundaries were corrected without changing product
|
|
||||||
behavior.
|
|
||||||
- The one known redundant historical Core theory input remains for R3 behind a
|
|
||||||
site-scoped `xUnit1025` suppression. The central policy still makes any new
|
|
||||||
duplicate row a build failure.
|
|
||||||
- The launcher's seven editor-focus variants now execute in one Avalonia test
|
|
||||||
application session. This prevents the headless framework from attempting
|
|
||||||
compositor reinitialization on a non-owning thread. The suite passed 11
|
|
||||||
consecutive focused runs before the full gate. Aggregating seven theory rows
|
|
||||||
into one fact reduces the headline passed count by six; all seven variants
|
|
||||||
are still executed and asserted.
|
|
||||||
- The launcher package-boundary test now verifies versionless project
|
|
||||||
references against the central version table rather than incorrectly
|
|
||||||
requiring inline versions.
|
|
||||||
|
|
||||||
Reproducibility evidence:
|
|
||||||
|
|
||||||
- Forced locked re-evaluation of all 44 neutral and all 28 RID graphs changed
|
|
||||||
zero lock hashes.
|
|
||||||
- A locked restore into an empty global package cache, with `--no-cache`,
|
|
||||||
succeeded from the sole configured source. NuGet assets recorded no fallback
|
|
||||||
package folder.
|
|
||||||
- A forced nested launcher-to-Bake publish selected the appropriate RID graph,
|
|
||||||
emitted the Bake executable, and changed zero lock hashes.
|
|
||||||
- `pwsh ./tools/run-release-gate.ps1` ran on the clean exact commit
|
|
||||||
`c38f6b88522750abc2e4acf1898ed566c5576e4a`, SDK `10.0.300`, RID `win-x64`,
|
|
||||||
in 121.398 seconds. Restore was forced and locked; all 44 projects built with
|
|
||||||
0 warnings / 0 errors; all 12 test assemblies completed with 14,742 passed /
|
|
||||||
77 skipped / 0 failed. The gate recorded `WorktreeDirty: false`.
|
|
||||||
|
|
||||||
No product source behavior changed in this closeout. The known duplicate theory
|
|
||||||
row, classification of the 77 environment-dependent skips, test naming/value
|
|
||||||
review, and stale public headline counts remain explicitly assigned to R3 and
|
|
||||||
the later documentation-authority slice. R2 is complete on the campaign branch.
|
|
||||||
|
|
||||||
Rollback is `git revert c38f6b88`; do not rewrite branch history.
|
|
||||||
|
|
||||||
## 18. Session start protocol
|
|
||||||
|
|
||||||
Every implementation session begins by:
|
|
||||||
|
|
||||||
1. reading this plan, the executive audit, and the findings for the active
|
|
||||||
slice;
|
|
||||||
2. running `git status --short`, `git rev-parse HEAD`, and checking the ledger's
|
|
||||||
recorded commit against the working tree;
|
|
||||||
3. reading all files/tests named by the active finding before editing;
|
|
||||||
4. confirming there is no overlapping uncommitted user work;
|
|
||||||
5. restating the bounded slice outcome and gates in the session update;
|
|
||||||
6. working only the first non-blocked active slice unless the plan explicitly
|
|
||||||
allows parallel work.
|
|
||||||
|
|
||||||
## 19. Session handoff protocol
|
|
||||||
|
|
||||||
Before ending any session, record in §17 or a linked dated closeout:
|
|
||||||
|
|
||||||
- exact commit/worktree state and every file changed;
|
|
||||||
- decisions made and alternatives rejected;
|
|
||||||
- invariant/evidence destinations for anything removed;
|
|
||||||
- exact commands, pass/fail/skip counts, timeouts, and artifact paths;
|
|
||||||
- review findings and whether they were closed;
|
|
||||||
- remaining risks, blockers, and user/legal decisions;
|
|
||||||
- rollback command or precise reversal procedure;
|
|
||||||
- one exact next action that can be started without chat context.
|
|
||||||
|
|
||||||
A slice is not `DONE` because its code compiles or a focused test passes. It is
|
|
||||||
done only when its exit criteria, complete required gate, evidence update,
|
|
||||||
review, and cross-session ledger entry are all complete.
|
|
||||||
|
|
||||||
## 20. Immediate next action
|
|
||||||
|
|
||||||
Fast-forward `main` through the completed R1–R3 stabilization branch and push
|
|
||||||
the merged result. R4 and R5 are explicitly deferred for the friend-only
|
|
||||||
release. Before any public release, resume R4; when maintainability work
|
|
||||||
resumes, begin with the bounded R5 documentation-authority goal. Do not begin
|
|
||||||
bulk comment, artifact, giant-file, or unrelated cleanup first.
|
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
# 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 <path>`; 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.
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
# Complete Release gate
|
|
||||||
|
|
||||||
The default release gate is repository-owned and uses the SDK feature band in
|
|
||||||
`global.json`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pwsh ./tools/run-release-gate.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
The command verifies that `AcDream.slnx` contains every `.csproj` under `src/`,
|
|
||||||
`tests/`, and `tools/`, performs a locked restore, builds that complete graph,
|
|
||||||
then discovers and runs every hermetic test in every default test assembly once
|
|
||||||
in a fresh Release process. It does not retry failures. Tests carrying an
|
|
||||||
explicit non-hermetic `Lane` trait (`InstalledDat`, `PreparedPackage`, `Live`,
|
|
||||||
`Manual`, `Timing`, `Windows`, `Linux`, or `SystemFont`), `Purpose=Diagnostic`, or
|
|
||||||
`Status=KnownFailure` are excluded from the hermetic total and run through
|
|
||||||
their owned lane instead. The graph currently contains 44 projects,
|
|
||||||
including all 13 maintained .NET tools; data-dependent tools are built but are
|
|
||||||
not executed as tests.
|
|
||||||
|
|
||||||
Build and dependency policy is repository-owned:
|
|
||||||
|
|
||||||
- `global.json` pins the accepted .NET SDK feature band;
|
|
||||||
- `Directory.Build.props` supplies the common target framework, language,
|
|
||||||
nullable, analyzer, warnings-as-errors, deterministic-build, and lock-file
|
|
||||||
settings;
|
|
||||||
- `Directory.Packages.props` is the only direct package-version table;
|
|
||||||
- `NuGet.Config` clears machine sources and permits only `nuget.org`; and
|
|
||||||
- each supported project commits its own `packages.neutral.lock.json`; shipped
|
|
||||||
source projects also commit `packages.win-x64.lock.json` and
|
|
||||||
`packages.linux-x64.lock.json` for RID-specific publishes.
|
|
||||||
|
|
||||||
The nonstandard neutral name is intentional. NuGet always prefers a
|
|
||||||
conventional `packages.lock.json` when one exists, even when
|
|
||||||
`NuGetLockFilePath` selects a RID-specific file. Do not introduce conventional
|
|
||||||
lock files beside these three repository-owned graphs.
|
|
||||||
|
|
||||||
The gate uses `dotnet restore --locked-mode --force-evaluate`. The forced
|
|
||||||
evaluation makes the result independent of stale `obj/` assets; locked mode
|
|
||||||
still prevents rewriting. If a project or central package version disagrees
|
|
||||||
with a committed lock file, restore fails instead of silently changing the
|
|
||||||
dependency graph. The launcher's nested Bake publish uses the matching
|
|
||||||
RID-specific lock and the same forced locked evaluation.
|
|
||||||
|
|
||||||
Each restore, build, and test process has an outer hard timeout. Every test
|
|
||||||
also runs with VSTest blame-hang enabled: after three minutes in one test, the
|
|
||||||
test host is terminated and a mini dump is collected; after ten minutes, the
|
|
||||||
outer watchdog kills the complete `dotnet test` process tree. CI additionally
|
|
||||||
has a 45-minute job bound.
|
|
||||||
|
|
||||||
Evidence is written to `artifacts/release-gate/`:
|
|
||||||
|
|
||||||
- `release-gate-summary.json` records the commit, branch, worktree state, SDK,
|
|
||||||
RID, bounds, process outcomes, assembly list, and
|
|
||||||
executed/passed/skipped/failed totals;
|
|
||||||
- `environment.txt` records `dotnet --info`, configured NuGet sources, and the
|
|
||||||
supported project set, package-lock hashes, and discovered test-project set;
|
|
||||||
- `test-results/` contains one TRX per assembly plus any VSTest hang sequence
|
|
||||||
and dump files;
|
|
||||||
- `logs/` contains the exact command and complete output for every child
|
|
||||||
process; and
|
|
||||||
- `SHA256SUMS.txt` hashes the evidence bundle.
|
|
||||||
|
|
||||||
The complete gate runs on Windows because it exercises the full product and
|
|
||||||
launcher surface. Hosted GitHub Actions execution is deliberately parked as of
|
|
||||||
2026-08-18 while runner policy is decided; the checked-in workflow definitions
|
|
||||||
are preserved for later use. Until then, the repository command above is the
|
|
||||||
authoritative gate. Focused portability or Vulkan jobs are not substitutes for
|
|
||||||
the complete gate.
|
|
||||||
|
|
||||||
The JSON summary records the exact test filter. Environment-dependent,
|
|
||||||
diagnostic, manual, and known-failure results must be published as their own
|
|
||||||
lane and must never be added to the hermetic pass headline.
|
|
||||||
|
|
||||||
## The Timing lane
|
|
||||||
|
|
||||||
`Lane=Timing` marks tests whose outcome depends on **real elapsed time or OS
|
|
||||||
scheduling** rather than on logic: simulated packet-loss soaks, a virtual-clock
|
|
||||||
transport session that still waits on wall-clock windows, signalling a real
|
|
||||||
child process, orphaned-process restart recovery. They pass on an idle machine
|
|
||||||
and fail intermittently under full-assembly load, so they cannot gate a push
|
|
||||||
without making the gate untrustworthy.
|
|
||||||
|
|
||||||
They are not weakened or deleted — run them deliberately, on a machine that is
|
|
||||||
not saturated:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pwsh ./tools/run-release-gate.ps1 -SkipRestore -SkipBuild `
|
|
||||||
-TestFilter 'Lane=Timing&Status!=KnownFailure&Purpose!=Diagnostic'
|
|
||||||
```
|
|
||||||
|
|
||||||
Measured before laning: on the 6-core Linux runner, three stress rounds of the
|
|
||||||
full suite failed `GracefulStopSignalSendsSigintToARealChildOnLinux` 3/3 (it
|
|
||||||
passes in ~47 ms alone) and two loss-simulation tests 1/3 each. Chasing them one
|
|
||||||
at a time did not converge — four separate fixes, each surfacing a different
|
|
||||||
member of the same family, and one of those fixes regressed the other platform.
|
|
||||||
|
|
||||||
Add to this lane only with evidence that a test fails under load and passes in
|
|
||||||
isolation. A test that fails consistently is a bug, not a timing lane member.
|
|
||||||
|
|
||||||
## Continuous integration
|
|
||||||
|
|
||||||
This document owns the LOCAL gate. Pushes to `main` are gated on self-hosted
|
|
||||||
runners and publish alpha releases — see
|
|
||||||
[`ci-and-releases.md`](ci-and-releases.md). Note that CI deliberately does NOT
|
|
||||||
invoke `run-release-gate.ps1`: that script redirects child output to log files,
|
|
||||||
and Forgejo fails a task that stops reporting as a zombie.
|
|
||||||
|
|
||||||
## Non-hermetic test lanes
|
|
||||||
|
|
||||||
Installed-DAT tests require an explicit opt-in and a retail DAT directory:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:ACDREAM_RUN_INSTALLED_DAT_TESTS = '1'
|
|
||||||
$env:ACDREAM_DAT_DIR = 'C:\path\to\Asherons Call'
|
|
||||||
pwsh ./tools/run-release-gate.ps1 -SkipRestore -SkipBuild `
|
|
||||||
-TestFilter 'Lane=InstalledDat&Status!=KnownFailure&Purpose!=Diagnostic'
|
|
||||||
```
|
|
||||||
|
|
||||||
The prepared-package lane additionally requires a validated `acdream.pak`
|
|
||||||
beside the DATs or at `ACDREAM_PAK_PATH`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:ACDREAM_DAT_DIR = 'C:\path\to\Asherons Call'
|
|
||||||
$env:ACDREAM_PAK_PATH = 'C:\path\to\acdream.pak'
|
|
||||||
pwsh ./tools/run-release-gate.ps1 -SkipRestore -SkipBuild `
|
|
||||||
-TestFilter 'Lane=PreparedPackage&Status!=KnownFailure&Purpose!=Diagnostic'
|
|
||||||
```
|
|
||||||
|
|
||||||
Regenerate all committed UI fixtures through the one comprehensive manual
|
|
||||||
generator (the former chat/radar-only generators were redundant):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:ACDREAM_REGENERATE_UI_FIXTURES = '1'
|
|
||||||
$env:ACDREAM_DAT_DIR = 'C:\path\to\Asherons Call'
|
|
||||||
dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release `
|
|
||||||
--filter 'Lane=Manual&ManualTask=FixtureGeneration'
|
|
||||||
```
|
|
||||||
|
|
||||||
The retained live-DAT probes are manual evidence, not InstalledDat regression
|
|
||||||
contracts. Run each opt-in family independently so a probe command can never
|
|
||||||
regenerate fixtures as a side effect:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:ACDREAM_DAT_DIR = 'C:\path\to\Asherons Call'
|
|
||||||
$env:ACDREAM_PROBE_LIVE_MOUNT = '1'
|
|
||||||
dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release `
|
|
||||||
--filter 'Lane=Manual&ManualTask=LiveMountProbe'
|
|
||||||
|
|
||||||
$env:ACDREAM_PROBE_POWERBAR = '1'
|
|
||||||
dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release `
|
|
||||||
--filter 'Lane=Manual&ManualTask=PowerbarProbe'
|
|
||||||
```
|
|
||||||
|
|
||||||
Known failures (`Status=KnownFailure`) are never part of a green release total.
|
|
||||||
Run them explicitly with their prerequisite lane configured; a failure is
|
|
||||||
expected until the linked defect is fixed. Diagnostic apparatus
|
|
||||||
(`Purpose=Diagnostic`) likewise reports separately and does not inflate the
|
|
||||||
contract-test pass count.
|
|
||||||
|
|
||||||
The current diagnostic apparatus lives in App and Core. It is retained for
|
|
||||||
investigation output, and several methods require installed DATs:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release `
|
|
||||||
--filter 'Purpose=Diagnostic&Lane!=Manual'
|
|
||||||
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Release `
|
|
||||||
--filter 'Purpose=Diagnostic&Lane!=Manual'
|
|
||||||
```
|
|
||||||
|
|
||||||
Operating-system contracts are likewise explicit. Run `Lane=Windows` on a
|
|
||||||
Windows host and `Lane=Linux` on a native Linux host; a lane is not portable
|
|
||||||
evidence when executed on the other operating system.
|
|
||||||
|
|
||||||
`Lane=SystemFont` exercises the BitmapFont path against a host-provided TTF.
|
|
||||||
It is separate because the supported runtime can legitimately have none of the
|
|
||||||
well-known development fonts installed.
|
|
||||||
|
|
||||||
## Updating dependencies
|
|
||||||
|
|
||||||
Do not edit lock files by hand. To make an intentional dependency change:
|
|
||||||
|
|
||||||
1. Change the version once in `Directory.Packages.props` (or add/remove a
|
|
||||||
versionless `PackageReference` in a project).
|
|
||||||
2. Regenerate the neutral graph and both supported release-RID graphs from the
|
|
||||||
repository root:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pwsh ./tools/update-package-locks.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Review the central-version and `packages.*.lock.json` diffs.
|
|
||||||
4. Prove locked resolution and run the gate:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet restore AcDream.slnx --locked-mode --force-evaluate
|
|
||||||
pwsh ./tools/run-release-gate.ps1
|
|
||||||
```
|
|
||||||
|
|
@ -371,7 +371,7 @@ area files.
|
||||||
> (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
|
> (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
|
||||||
> `PrepareCellStructMeshData:1394`, `CellMesh.Build:44`,
|
> `PrepareCellStructMeshData:1394`, `CellMesh.Build:44`,
|
||||||
> `GfxObjMesh.Build:71`), and the fills have no negative surface
|
> `GfxObjMesh.Build:71`), and the fills have no negative surface
|
||||||
> (`Diagnostic_ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every
|
> (`ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every
|
||||||
> fill). The equivalence pin (`StipplingSurfaceEquivalenceTests`, 2,607
|
> fill). The equivalence pin (`StipplingSurfaceEquivalenceTests`, 2,607
|
||||||
> polys, 0 violations) proves our build-time skip ⇔ retail's draw-time
|
> polys, 0 violations) proves our build-time skip ⇔ retail's draw-time
|
||||||
> `skipNoTexture` on this content. Consequences: the ledger rows
|
> `skipNoTexture` on this content. Consequences: the ledger rows
|
||||||
|
|
|
||||||
|
|
@ -141,19 +141,6 @@ internal element states `SetDragAcceptState` writes — both are real; the Layou
|
||||||
states and the `0x1000003x/4x` UIStateIds are the same overlay seen from the dat side vs.
|
states and the `0x1000003x/4x` UIStateIds are the same overlay seen from the dat side vs.
|
||||||
the C++ side. CONFIRMED.
|
the C++ side. CONFIRMED.
|
||||||
|
|
||||||
> **Correction 2026-08-08 (spell-bar drop-ring research):** the parenthetical
|
|
||||||
> above has the accept/reject ids SWAPPED. The true mapping is
|
|
||||||
> `ItemSlot_DragOver_Accept = 0x10000040 → 0x060011F9` and
|
|
||||||
> `ItemSlot_DragOver_Reject = 0x10000041 → 0x060011F8`, confirmed by
|
|
||||||
> DatReaderWriter's retail-derived `UIStateId` enum, by the legal/illegal
|
|
||||||
> branches of `gmPaperDollUI::HandlePaperDollDragOver` @ 0x004A3AC9/0x004A3AEB
|
|
||||||
> and `VendorSellUI::OnItemListDragOver` @ 0x004C2327/0x004C2336, and by the
|
|
||||||
> machine layout dump (`2026-06-25-retail-ui-layout-dump.json`, states
|
|
||||||
> 268435520/268435521 on elements 0x1000046D/0x1000046C). The table row's
|
|
||||||
> name→art column above was always right; only this paragraph's numeric
|
|
||||||
> pairing was inverted (and propagated into
|
|
||||||
> `2026-07-13-retail-item-drag-visuals-pseudocode.md`, corrected the same day).
|
|
||||||
|
|
||||||
### 2.3 Key methods + the update pass (`UIItem_Update`, decomp 230226)
|
### 2.3 Key methods + the update pass (`UIItem_Update`, decomp 230226)
|
||||||
|
|
||||||
`UIItem_Update` is the per-change refresh; the controller calls it whenever the bound
|
`UIItem_Update` is the per-change refresh; the controller calls it whenever the bound
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ draw the purple lightning over the floor"):
|
||||||
3. **Striped floor z-fight-like artifact.** User's 2nd screenshot: regular
|
3. **Striped floor z-fight-like artifact.** User's 2nd screenshot: regular
|
||||||
magenta bands across one floor region, "like something is fighting to draw
|
magenta bands across one floor region, "like something is fighting to draw
|
||||||
the purple over the floor." **NOT attributed.** Ruled out: not coincident dat
|
the purple over the floor." **NOT attributed.** Ruled out: not coincident dat
|
||||||
geometry (the `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep
|
geometry (the `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep
|
||||||
found only the legit z=−12 under-hall floor quad-fan, nothing near the −6
|
found only the legit z=−12 under-hall floor quad-fan, nothing near the −6
|
||||||
corridor floor); not a striped texture (all corridor surfaces are plain
|
corridor floor); not a striped texture (all corridor surfaces are plain
|
||||||
`Base1Image` stone 0x08000375/6/7/8). Leading guess: two draws of the same
|
`Base1Image` stone 0x08000375/6/7/8). Leading guess: two draws of the same
|
||||||
|
|
@ -116,7 +116,7 @@ draw the purple lightning over the floor"):
|
||||||
stair geometry owner (`0x8A020182`'s ramp shell, vertical portals, ZERO
|
stair geometry owner (`0x8A020182`'s ramp shell, vertical portals, ZERO
|
||||||
statics), CellBSP containment (partitions exactly at portal planes),
|
statics), CellBSP containment (partitions exactly at portal planes),
|
||||||
under-hall + corridor drawn-poly surface colors, DXT1 alpha histograms (0
|
under-hall + corridor drawn-poly surface colors, DXT1 alpha histograms (0
|
||||||
transparent texels), and `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
|
transparent texels), and `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
|
||||||
(the stripe-geometry sweep — came back empty for the −6 floor).
|
(the stripe-geometry sweep — came back empty for the −6 floor).
|
||||||
- **`tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs`**
|
- **`tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs`**
|
||||||
— production-matched portal-flood replays (approach/descent/gaze-sweep/walk +
|
— production-matched portal-flood replays (approach/descent/gaze-sweep/walk +
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ NOT throw more lighting fixes at it (two already did nothing).
|
||||||
| Lighting **selection** | `SelectForCell` (all dynamic lights per cell, retail-exact) → **no visual change** |
|
| Lighting **selection** | `SelectForCell` (all dynamic lights per cell, retail-exact) → **no visual change** |
|
||||||
| Light-set **camera-cap churn** (the OLD "confirmed" #176 theory) | visible-cell scoping shipped (`c500912b`); probe-proven ~285 through-floor lights dropped/frame — symptom unchanged. **REFUTED.** |
|
| Light-set **camera-cap churn** (the OLD "confirmed" #176 theory) | visible-cell scoping shipped (`c500912b`); probe-proven ~285 through-floor lights dropped/frame — symptom unchanged. **REFUTED.** |
|
||||||
| **Membership / the "flap"** | `ACDREAM_PROBE_FLAP`: render cell = `0x8A020164` on **100%** of 188,732 frames across **526 distinct camera angles**, `res=None` always; `ACDREAM_PROBE_PVINPUT` flood is stable-per-angle (17/10/8…), never oscillates at a fixed view; 1 `[cell-transit]` (the spawn teleport) total |
|
| **Membership / the "flap"** | `ACDREAM_PROBE_FLAP`: render cell = `0x8A020164` on **100%** of 188,732 frames across **526 distinct camera angles**, `res=None` always; `ACDREAM_PROBE_PVINPUT` flood is stable-per-angle (17/10/8…), never oscillates at a fixed view; 1 `[cell-transit]` (the spawn teleport) total |
|
||||||
| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=−6 corridor floor (only the benign same-cell z=−12 under-hall floor tiling in `0x011E`) |
|
| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=−6 corridor floor (only the benign same-cell z=−12 under-hall floor tiling in `0x011E`) |
|
||||||
| A2C / alpha-hole see-through | corridor floor surface `0x08000377` is **fully opaque** (`alpha0Texels=0`, `transl=0.00`) |
|
| A2C / alpha-hole see-through | corridor floor surface `0x08000377` is **fully opaque** (`alpha0Texels=0`, `transl=0.00`) |
|
||||||
| Translucent under-surface blend | the one translucent colored surface `0x08000034` is `NoPos` (not drawn) |
|
| Translucent under-surface blend | the one translucent colored surface `0x08000034` is `NoPos` (not drawn) |
|
||||||
| Flat (per-face) normals | corridor floor uses **smooth per-vertex dat normals** (center `(0,0,1)`, corners tilted ~27° — retail-style edge smoothing), NOT flat (`CellVertexNormals_SmoothOrFaceted_Dump`) |
|
| Flat (per-face) normals | corridor floor uses **smooth per-vertex dat normals** (center `(0,0,1)`, corners tilted ~27° — retail-style edge smoothing), NOT flat (`CellVertexNormals_SmoothOrFaceted_Dump`) |
|
||||||
|
|
@ -129,7 +129,7 @@ RenderDoc (do NOT assume — capture and read):
|
||||||
- `ACDREAM_PROBE_INDOOR_LIGHT=1` — `[indoor-light]` scoped-pool SET composition.
|
- `ACDREAM_PROBE_INDOOR_LIGHT=1` — `[indoor-light]` scoped-pool SET composition.
|
||||||
- `tools/cdb/issue176-floor-light.cdb` — retail light-setup trace.
|
- `tools/cdb/issue176-floor-light.cdb` — retail light-setup trace.
|
||||||
- `Issue176177DungeonSeamInspectionTests` — dat truth (coplanar sweep, floor
|
- `Issue176177DungeonSeamInspectionTests` — dat truth (coplanar sweep, floor
|
||||||
surfaces, vertex normals); `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
|
surfaces, vertex normals); `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs`
|
||||||
seed = `{0164,0165,016E,017A}`+neighbors.
|
seed = `{0164,0165,016E,017A}`+neighbors.
|
||||||
|
|
||||||
## Repro + launch protocol
|
## Repro + launch protocol
|
||||||
|
|
|
||||||
|
|
@ -79,28 +79,12 @@ if target list is a container selector
|
||||||
target.SetDragAcceptState(0x10000046) # ItemSlot_DragOver_DropIn
|
target.SetDragAcceptState(0x10000046) # ItemSlot_DragOver_DropIn
|
||||||
# 0x060011F7 green arrow
|
# 0x060011F7 green arrow
|
||||||
else if target accepts an ordinary item-list placement:
|
else if target accepts an ordinary item-list placement:
|
||||||
target.SetDragAcceptState(0x10000040) # ItemSlot_DragOver_Accept
|
target.SetDragAcceptState(0x10000041) # ItemSlot_DragOver_Accept
|
||||||
# 0x060011F9 green circle
|
# 0x060011F9 green circle
|
||||||
else:
|
else:
|
||||||
target.SetDragAcceptState(0x10000041) # ItemSlot_DragOver_Reject
|
target.SetDragAcceptState(0x10000040) # 0x060011F8 reject
|
||||||
# 0x060011F8 reject
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Correction 2026-08-08 (spell-bar drop-ring research):** the block above
|
|
||||||
> originally had the Accept/Reject numeric ids swapped (`0x10000041` labeled
|
|
||||||
> Accept, `0x10000040` labeled reject). Three primary sources agree the true
|
|
||||||
> mapping is `ItemSlot_DragOver_Accept = 0x10000040 → 0x060011F9` and
|
|
||||||
> `ItemSlot_DragOver_Reject = 0x10000041 → 0x060011F8`: DatReaderWriter's
|
|
||||||
> retail-derived `UIStateId` enum; the legal/illegal branches in
|
|
||||||
> `gmPaperDollUI::HandlePaperDollDragOver` (`AutoWearIsLegal` → 0x10000040
|
|
||||||
> @ 0x004A3AC9, else 0x10000041 @ 0x004A3AEB) and
|
|
||||||
> `VendorSellUI::OnItemListDragOver` (`DragItemAcceptable` → 0x10000040
|
|
||||||
> @ 0x004C2327, else 0x10000041 @ 0x004C2336); and the machine layout dump
|
|
||||||
> (`2026-06-25-retail-ui-layout-dump.json`: state 268435520 = 0x10000040 →
|
|
||||||
> image 0x060011F9, state 268435521 = 0x10000041 → 0x060011F8). The
|
|
||||||
> art-per-semantic mapping in the shipped code was always correct; only the
|
|
||||||
> numeric labels here were swapped.
|
|
||||||
|
|
||||||
Therefore the backpack contents grid uses the green circle; the side-bag column
|
Therefore the backpack contents grid uses the green circle; the side-bag column
|
||||||
and main-pack container cell use the green drop-in arrow. The selected/open
|
and main-pack container cell use the green drop-in arrow. The selected/open
|
||||||
indicators remain visible while `m_elem_Icon_Ghosted` is active, so the
|
indicators remain visible while `m_elem_Icon_Ghosted` is active, so the
|
||||||
|
|
|
||||||
|
|
@ -206,56 +206,25 @@ which confirms that worker completion alone is not draw readiness.
|
||||||
|
|
||||||
### 2.1. Destination placement enters the spatial cell before simulation resumes
|
### 2.1. Destination placement enters the spatial cell before simulation resumes
|
||||||
|
|
||||||
> **2026-08-04 correction (C4 route 3, D-T9), itself corrected 2026-08-05
|
|
||||||
> (R6 retail review):** the listing below attributes portal arrival to
|
|
||||||
> `player.enter_world(destination)`. That is wrong — a caller sweep of the
|
|
||||||
> named retail decomp
|
|
||||||
> (`docs/research/named-retail/acclient_2013_pseudo_c.txt:93770-93828`) shows
|
|
||||||
> both `CPhysicsObj::enter_world` call sites (pseudo-C `:93797` @0x004550EC
|
|
||||||
> and `:93824` @0x00455095) living inside **`SmartBox::HandleCreateObject`
|
|
||||||
> @0x00454C80** — `CObjectMaint::CreateObject` @0x00454FD8 is merely a
|
|
||||||
> *callee* it invokes partway through, not the enclosing function the first
|
|
||||||
> correction pass named. The two call sites are also **not both in the
|
|
||||||
> player branch**: @0x004550EC sits in the `if (arg3 != this->player_id)`
|
|
||||||
> NON-player branch (`PhysicsDesc::get_position` → `enter_world` for a
|
|
||||||
> newly-created REMOTE object); only @0x00455095 sits in the player branch,
|
|
||||||
> after `SmartBox::init_player` + `CellManager::ChangePosition`. Both sites
|
|
||||||
> are the LOGIN/CreateObject path that creates a physics object for the
|
|
||||||
> first time — neither is portal arrival. Portal arrival is
|
|
||||||
> `SmartBox::TeleportPlayer` (`0x00453910`) → `CPhysicsObj::SetPositionSimple`
|
|
||||||
> (`0x00453924`/`0x005162B0`) — confirmed by C4 route 3's own §1 citations
|
|
||||||
> and grep at `acclient_2013_pseudo_c.txt:92514-92521`. The conclusion below
|
|
||||||
> (commit the cell before releasing simulation) is unaffected —
|
|
||||||
> `SetPositionSimple` reaches the identical `change_cell`/`update_object`
|
|
||||||
> machinery this section describes — only the entry-point name and
|
|
||||||
> pseudocode's `enter_world` call are wrong; read `SetPositionSimple(destination)`
|
|
||||||
> wherever this section says `enter_world(destination)`.
|
|
||||||
>
|
|
||||||
> This routing is `SmartBox::TeleportPlayer` → `SetPositionSimple`
|
|
||||||
> everywhere; nothing in the passages below distinguishes retail's specific
|
|
||||||
> Recall/Lifestone/GM-teleport CAUSES, since they all funnel through the same
|
|
||||||
> accepted-destination Position at this layer.
|
|
||||||
|
|
||||||
Named retail references:
|
Named retail references:
|
||||||
|
|
||||||
- `CPhysicsObj::change_cell` at `0x00513390`
|
- `CPhysicsObj::change_cell` at `0x00513390`
|
||||||
- `CPhysicsObj::update_object` at `0x00515D10`
|
- `CPhysicsObj::update_object` at `0x00515D10`
|
||||||
- `SmartBox::TeleportPlayer` at `0x00453910`
|
- `CPhysicsObj::enter_world` at `0x00516170`
|
||||||
- `CPhysicsObj::SetPositionSimple` at `0x005162B0`
|
|
||||||
- `CPhysicsObj::prepare_to_enter_world` at `0x00511FA0`
|
- `CPhysicsObj::prepare_to_enter_world` at `0x00511FA0`
|
||||||
- `CPhysicsObj::set_hidden` at `0x00514C60`
|
- `CPhysicsObj::set_hidden` at `0x00514C60`
|
||||||
|
|
||||||
Retail does not separate an accepted destination Position from the object's
|
Retail does not separate an accepted destination Position from the object's
|
||||||
live cell pointer. `SetPositionSimple` installs the object in its destination
|
live cell pointer. `enter_world` runs `SetPosition`, which installs the object
|
||||||
`CObjCell`, before the PartArray and MovementManager enter-world boundaries
|
in its destination `CObjCell`, before the PartArray and MovementManager
|
||||||
complete. `update_object` then rejects only a parented object, a null `cell`,
|
enter-world boundaries complete. `update_object` then rejects only a parented
|
||||||
or a Frozen object; Hidden is not a reason to skip the
|
object, a null `cell`, or a Frozen object; Hidden is not a reason to skip the
|
||||||
ScriptManager/ParticleManager tail.
|
ScriptManager/ParticleManager tail.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
accepted portal destination becomes ready:
|
accepted portal destination becomes ready:
|
||||||
SmartBox.TeleportPlayer(destination)
|
player.enter_world(destination)
|
||||||
SetPositionSimple(destination)
|
SetPosition(destination)
|
||||||
change_cell(destination CObjCell)
|
change_cell(destination CObjCell)
|
||||||
PartArray.HandleEnterWorld()
|
PartArray.HandleEnterWorld()
|
||||||
MovementManager.HandleEnterWorld()
|
MovementManager.HandleEnterWorld()
|
||||||
|
|
|
||||||
|
|
@ -1,682 +0,0 @@
|
||||||
# #265 capture-driven bisection — steep-slope response family
|
|
||||||
|
|
||||||
**Status: FIX IMPLEMENTED 2026-07-30 (same day, §9 as-fixed addendum) —
|
|
||||||
closure pends the user's visual-gate acceptance.** S1 and S2 both CLEARED
|
|
||||||
for the two concrete mined events; the real mechanism was a pre-existing
|
|
||||||
(frozen-phase) architecture, not a Campaign P regression — §1-§8 below are
|
|
||||||
the original bisection pass (research-only, no production code changed
|
|
||||||
at that point). §9 records what was actually implemented against that
|
|
||||||
verdict: `PlayerMovementController.cs`'s grounded residual-velocity zero is
|
|
||||||
removed for the animation-root-motion path, and `PhysicsEngine.cs` now
|
|
||||||
wires `PhysicsBody.GroundNormal` from the committed contact plane. The
|
|
||||||
harness (committed,
|
|
||||||
`tests/AcDream.Core.Tests/Physics/Issue265SteepSlopeCaptureBisectTests.cs`)
|
|
||||||
and mining tool (`tools/analyze_265_steep_slope_capture.py`) are permanent;
|
|
||||||
the A/B code toggles described in §3 were applied and reverted locally
|
|
||||||
and never committed (they remain historical — the actual fix is
|
|
||||||
unrelated to S1/S2, see §9).
|
|
||||||
|
|
||||||
## 0. Scope recap
|
|
||||||
|
|
||||||
Issue #265 (`docs/ISSUES.md`): after the TS-4-removal-then-revert
|
|
||||||
(`2e27d066`+`a8a7d64b`), the live matrix gate (2026-07-30, scenarios 4/5)
|
|
||||||
found three symptoms: (a) jumping INTO an uphill slope bounces (retail does
|
|
||||||
not), (b) house-roof slides no longer happen, (c) occasional
|
|
||||||
stuck-sliding-on-an-edge. Two remaining Campaign-P suspects were named:
|
|
||||||
|
|
||||||
- **S1** — `db2889af` ("#116 shape-1"): `BSPQuery.cs` Path-6's `hasSphere1`
|
|
||||||
(head-sphere-only hit while airborne, foot sphere clear) branch changed
|
|
||||||
from a steepness-gated dual path (steep → slide-tangent-then-`Slid`;
|
|
||||||
shallow → `SetCollide`+`Adjusted`) to an unconditional
|
|
||||||
`SetCollisionNormal` + `return Collided`.
|
|
||||||
- **S2** — the AP-7 `calc_friction` threshold rewrite (merge `26e0334a`):
|
|
||||||
`0.0` → `0.25`, unconditional into-plane velocity subtraction past the
|
|
||||||
threshold.
|
|
||||||
|
|
||||||
## 1. Segment mining
|
|
||||||
|
|
||||||
Captures used: `artifacts/matrix-session2-resolve.jsonl` (15,726 records,
|
|
||||||
copied from the coordinator worktree's `artifacts/matrix-session2-resolve.jsonl`)
|
|
||||||
and `artifacts/matrix-session3-resolve.jsonl` (12,145 records at copy time).
|
|
||||||
|
|
||||||
**Session3 is not usable.** Every one of its 12,145 records shows the
|
|
||||||
identical position `(60.372223, 9.071998, 79.344925)`, zero velocity, and
|
|
||||||
`transientState=3` (Contact|OnWalkable) from tick 0 to tick 12144 — the
|
|
||||||
player was standing perfectly still (likely AFK / alt-tabbed) for the
|
|
||||||
entire ~7.2-minute capture window. It contains no motion at all and was
|
|
||||||
excluded from further analysis.
|
|
||||||
|
|
||||||
### 1.1 First pass — strict signature scan (`tools/analyze_265_steep_slope_capture.py`)
|
|
||||||
|
|
||||||
Two signatures were scanned for directly on the JSONL fields:
|
|
||||||
|
|
||||||
- **Signature A (uphill-jump bounce)**: an airborne record (`bodyBefore`
|
|
||||||
Contact bit clear) with `result.collisionNormalValid=true` and a "steep,
|
|
||||||
non-floor, non-wall" normal (`0.02 < normal.Z < FloorZ=0.6642`), followed
|
|
||||||
by a next-tick upward jump in `bodyBefore.velocity.z` while still
|
|
||||||
airborne.
|
|
||||||
- **Signature B (lost-slide / edge-wedge)**: ≥6 consecutive ticks with
|
|
||||||
Contact set but OnWalkable clear (resting against a non-walkable steep
|
|
||||||
surface), a non-trivial requested move each tick, and near-zero net
|
|
||||||
advance.
|
|
||||||
|
|
||||||
**Result: 0 hits for both signatures, in both files.** Session2 has only
|
|
||||||
38 `collisionNormalValid=true` records total (out of 15,726), and every one
|
|
||||||
of them has `normal.Z` in the `[0.85, 1.0]` bucket — i.e. every reported
|
|
||||||
collision normal in this capture is CLOSE TO FLAT/floor-like, never in the
|
|
||||||
"genuinely steep" `< FloorZ` band my first-pass signature targeted. This is
|
|
||||||
an honest negative result for the specific "steep" heuristic; the actual
|
|
||||||
symptom-bearing frames, mined below, are moderate-angle (Z≈0.86–0.95,
|
|
||||||
above `FloorZ` — walkable BY THE THRESHOLD) and are found by a different
|
|
||||||
signature.
|
|
||||||
|
|
||||||
### 1.2 Second pass — velocity-annihilation scan
|
|
||||||
|
|
||||||
Widened the signature to "a tick where `|horizontal velocity|` before is
|
|
||||||
`>2 m/s` and after (next tick) is `<0.05 m/s`, then how long the position
|
|
||||||
stays frozen afterward." This found exactly **two events**, both in
|
|
||||||
session2:
|
|
||||||
|
|
||||||
| idx (0-based) | tick | `|v_horiz|` before | frozen for | cell |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 3153 | 3153 | 8.90 m/s | 46+ ticks (session2 continues past it; not EOF) | `0xAAB30007` |
|
|
||||||
| **3434** | **3434** | **18.00 m/s** | **12,292 ticks — to EOF** | `0xAAB40011` |
|
|
||||||
|
|
||||||
**Event at idx 3433/3434 (the primary oracle for this pass), full trace**
|
|
||||||
(`records[3415..3434+41]`, printed via ad-hoc Python — see
|
|
||||||
`tools/analyze_265_steep_slope_capture.py` for the reusable scanner):
|
|
||||||
|
|
||||||
- Ticks 3415–3432: clean ballistic fall. `vBefore = (11.15, 14.13, vz)`
|
|
||||||
with `vz` accumulating from ‑16.72 to ‑23.14 (pure gravity, no further
|
|
||||||
horizontal drive — a jump/leap with residual momentum, exactly the kind
|
|
||||||
of trajectory the oracle plan's §1.3 predicted would NOT hit the
|
|
||||||
TS-4 degenerate case). `Z` falls from 92.79 to 79.90.
|
|
||||||
- **Tick 3433 (the landing):** `result.collisionNormalValid=true`,
|
|
||||||
`result.collisionNormal=(0.2857143, 0.42857143, 0.85714287)` — exactly
|
|
||||||
`(2,3,6)/7`, a REAL polygon normal (not the `UnitZ` degenerate default).
|
|
||||||
`result.isOnGround=true`. `bodyAfter.contactPlaneValid=true`,
|
|
||||||
`bodyAfter.walkablePolygonValid=true`, `bodyAfter.walkableVertices` = the
|
|
||||||
triangle `(240,0,88), (264,0,80), (264,24,68)` — `normal.Z=0.857`, well
|
|
||||||
ABOVE `PhysicsGlobals.FloorZ` (0.6642): **a legitimately walkable roof
|
|
||||||
slope, not the "steep, non-walkable" case either S1 or the original TS-4
|
|
||||||
shortcut ever targeted.** `bodyAfter.velocity` is UNCHANGED
|
|
||||||
`(11.15,14.13,‑23.14)` — confirms (see `PhysicsEngine.cs:1489`, capture
|
|
||||||
fires inside `ResolveWithTransition`, before any caller-side velocity
|
|
||||||
response) that neither `calc_friction` nor `HandleAllCollisions` ran yet.
|
|
||||||
- **Tick 3434 (the very next resolve call):** `input.currentPos ==
|
|
||||||
input.targetPos` (ZERO requested motion this tick — the previous frame's
|
|
||||||
integration already produced zero displacement). `bodyBefore.velocity =
|
|
||||||
(0, 0, 0)` — **already fully zeroed by the time THIS resolve call even
|
|
||||||
starts.** `transientState=7` (Contact|OnWalkable|Sliding). Every
|
|
||||||
subsequent record (12,292 of them, to the literal end of the file) is
|
|
||||||
byte-identical: same position, same zero velocity, same
|
|
||||||
`transientState=7`.
|
|
||||||
|
|
||||||
**Event at idx 3152/3153** is the same shape at a shallower ~18° roof edge
|
|
||||||
(`normal≈(0,0.32,0.95)`): the player glides/climbs cleanly along the edge
|
|
||||||
for ~140 ticks (idx 3016–3152, gaining ~10 m of Z — this portion is
|
|
||||||
healthy behavior), then at idx 3153 horizontal velocity is forced to
|
|
||||||
exactly zero in one tick and the position freezes for the rest of the
|
|
||||||
examined window.
|
|
||||||
|
|
||||||
**Both events are the SAME shape**: a real, correct, non-default collision
|
|
||||||
normal is recorded on the landing tick; on the very next tick the mover's
|
|
||||||
full horizontal velocity has already vanished and the position never
|
|
||||||
changes again. This is what the user experiences as "roof slides no
|
|
||||||
longer happen" (symptom b) and "occasional stuck-sliding-on-an-edge"
|
|
||||||
(symptom c). Neither event's landing surface is steep by `FloorZ` — both
|
|
||||||
are moderate, walkable-by-threshold roof pitches.
|
|
||||||
|
|
||||||
## 2. Replay harness
|
|
||||||
|
|
||||||
`Issue265SteepSlopeCaptureBisectTests.cs` builds a synthetic
|
|
||||||
`PhysicsEngine` containing ONE polygon — the exact real triangle recovered
|
|
||||||
from record 3433's `bodyAfter.walkableVertices` — registered via
|
|
||||||
`ShadowObjectRegistry`, then replays the EXACT real captured ballistic
|
|
||||||
state (position + velocity, record index 3415) forward with real gravity
|
|
||||||
at 30 Hz, calling `PhysicsEngine.ResolveWithTransition` every tick exactly
|
|
||||||
like `PlayerMovementController` does at the Core boundary. Once the mover
|
|
||||||
reports `IsOnGround`, the harness keeps REQUESTING the same forward
|
|
||||||
velocity every tick (simulating held input) — this is deliberate: it turns
|
|
||||||
the harness from "replay what the live game did" (which trivially
|
|
||||||
reproduces the freeze, since the live game's own subsequent inputs were
|
|
||||||
already zero — see §4) into "does the physics engine itself allow
|
|
||||||
continued advance across this surface," which is the actual question S1
|
|
||||||
and S2 bear on.
|
|
||||||
|
|
||||||
### 2.1 Harness commissioning (three real bugs found and fixed while building it — kept as code comments)
|
|
||||||
|
|
||||||
1. `ShadowObjectRegistry.Register`'s broad-phase culls by distance from
|
|
||||||
`worldPos`. Registering at the literal real-world coordinates (X≈256)
|
|
||||||
while querying at world origin put the polygon ~264 units away — the
|
|
||||||
very first run found **zero collisions at all**. Fixed by re-anchoring
|
|
||||||
the whole synthetic scene (triangle + approach trajectory) at the
|
|
||||||
triangle's centroid.
|
|
||||||
2. `CellTransit.BuildShadowCellSet`'s outdoor flood
|
|
||||||
(`CellTransit.AddAllOutsideCells`) treats world position as
|
|
||||||
landblock-local (an anchor-frame convention shared with
|
|
||||||
`Ts4SteepRoofWedgeCaptureTests`/`DoorBugTrajectoryReplayTests`, active
|
|
||||||
whenever `CellGraph.TryGetTerrainOrigin` has no real terrain to
|
|
||||||
consult). The real-world coordinates (X≈256) are outside the valid
|
|
||||||
`[0,192)` per-landblock range even after centroid re-anchoring picked a
|
|
||||||
bad cell id — still zero collisions.
|
|
||||||
3. `LandDefs.AdjustToOutside` (inside the flood) **silently re-derives**
|
|
||||||
the actual `(lx,ly)` grid cell from the sphere's real position and
|
|
||||||
corrects a mismatched seed rather than honoring the literal
|
|
||||||
`seedCellId` passed to `Register` — an arbitrary chosen cell id
|
|
||||||
(`0x00000011`) registered successfully (`TotalRegistered=1`) but
|
|
||||||
`GetObjectsInCell(0x00000011)` came back empty; the entity had actually
|
|
||||||
landed in cell `0x00000001` (the canonical grid-(0,0) cell, matching
|
|
||||||
the re-anchored centroid). Switching the harness's cell id to
|
|
||||||
`0x00000001` fixed it.
|
|
||||||
|
|
||||||
These are documented in the test file's code comments in case another
|
|
||||||
harness hits the same three traps.
|
|
||||||
|
|
||||||
## 3. A/B outcomes
|
|
||||||
|
|
||||||
### (i) HEAD vs (ii) S1-reverted
|
|
||||||
|
|
||||||
The S1 revert (`BSPQuery.cs`'s `hasSphere1` branch restored to the
|
|
||||||
pre-`db2889af` steepness-gated dual path, mirroring the still-current
|
|
||||||
`sphere0` branch — applied locally, verified `git diff --stat` clean
|
|
||||||
before and after, never committed) produced **byte-identical output** to
|
|
||||||
HEAD for the full 80-tick replay: same landing tick (19), same clean
|
|
||||||
44-tick glide (ticks 20–62, `adv=0.5149` every tick, `cnv=false`), same
|
|
||||||
freeze at tick 63 (`adv=0.0000` for 16+ consecutive ticks, capped by the
|
|
||||||
harness's tick budget — it would continue indefinitely), same recorded
|
|
||||||
`collisionNormal=(-0.958,0.128,0.256)` from that point on.
|
|
||||||
|
|
||||||
**Why they're identical — confirmed by diagnostic instrumentation**
|
|
||||||
(`PhysicsDiagnostics.ProbeIndoorBspEnabled`/`ProbeBuildingEnabled`, the
|
|
||||||
`[path-dispatch]`/`[path5-diag]` probes `db2889af` itself added): across
|
|
||||||
the entire 80-tick replay, **`hit1=True` never appears once.** The two
|
|
||||||
`[path-dispatch] ... collide=True ... contact=False ...` lines (Path 6
|
|
||||||
firing during the airborne approach) are followed by
|
|
||||||
`insertType=Placement` (Phase 3's walkable-landing retry succeeding) —
|
|
||||||
this is the STILL-UNCHANGED `sphere0` (foot) branch's graceful
|
|
||||||
`SetCollide`→`Adjusted`→Phase-3-Placement chain, not the `hasSphere1`
|
|
||||||
branch S1 touched. Once grounded, every subsequent Path-5 dispatch reports
|
|
||||||
`hit0=False hitPoly0=False` then `hit1=False hitPoly1=False` — a genuinely
|
|
||||||
clean glide with no collision at all, which is why the S1 edit (which only
|
|
||||||
fires inside `if (hit1 || hitPoly1 is not null)`) never executes for this
|
|
||||||
trajectory. **S1's site is provably unreached by the real mined
|
|
||||||
trajectory that produced the freeze.** Reverting code that never runs
|
|
||||||
cannot change the outcome — this is not a coincidence, it's the direct
|
|
||||||
mechanical explanation.
|
|
||||||
|
|
||||||
### (iii) S2 toggle
|
|
||||||
|
|
||||||
**Not run as a harness A/B — proven inert by static analysis instead.**
|
|
||||||
`grep -rn "\.calc_friction(" src/` returns **zero production call sites** —
|
|
||||||
the only callers of `PhysicsBody.calc_friction` in the entire repository
|
|
||||||
are its own unit tests (`tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs`).
|
|
||||||
`PlayerMovementController.cs` mentions it only in a code comment
|
|
||||||
(line ~2021, "friction next frame") — it is never invoked. Neither
|
|
||||||
`ResolveWithTransition` nor `PlayerMovementController`'s tick loop calls
|
|
||||||
`calc_friction` anywhere. **S2's threshold value (0.0 vs 0.25) cannot
|
|
||||||
affect any live or replayed behavior, full stop** — there is no toggle to
|
|
||||||
run because there is no live code path to toggle.
|
|
||||||
|
|
||||||
### (iv) Both reverted
|
|
||||||
|
|
||||||
Follows immediately from (ii) and (iii): with S1 reverted producing
|
|
||||||
byte-identical output to HEAD, and S2 provably inert, the "both" variant
|
|
||||||
is mathematically identical to (ii), which is identical to (i). No
|
|
||||||
separate run was needed.
|
|
||||||
|
|
||||||
### A/B summary table
|
|
||||||
|
|
||||||
| Variant | Landing tick | Clean glide (ticks 20-62) | Freeze at tick 63+ | Notes |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| (i) HEAD | 19 | yes, `adv=0.5149`/tick | yes, frozen forever | `hit1` never true |
|
|
||||||
| (ii) S1 reverted | 19 (identical) | yes (identical) | yes (identical) | S1's branch unreached |
|
|
||||||
| (iii) S2 toggle | n/a | n/a | n/a | dead code, no call sites |
|
|
||||||
| (iv) both | 19 (identical) | yes (identical) | yes (identical) | follows from (ii)+(iii) |
|
|
||||||
|
|
||||||
## 4. The actual mechanism (found by hand-tracing the live capture against `PlayerMovementController.cs`, independently confirming it explains BOTH mined freeze events exactly)
|
|
||||||
|
|
||||||
Neither S1 nor S2 touch velocity. The full-zero-in-one-tick signature
|
|
||||||
(§1.2) is produced by two pre-existing, Campaign-P-independent pieces
|
|
||||||
working in sequence:
|
|
||||||
|
|
||||||
1. **The landing tick** (`PlayerMovementController.cs`, the
|
|
||||||
`if (resolveResult.IsOnGround && _body.Velocity.Z <= 0f)` block):
|
|
||||||
Contact+OnWalkable are set, and — because `Velocity.Z < 0` — ONLY the
|
|
||||||
Z component is hand-zeroed: velocity becomes `(11.15, 14.13, 0)`.
|
|
||||||
`PhysicsObjUpdate.HandleAllCollisions` then runs with `shouldReflect =
|
|
||||||
true` (the mover was airborne the frame before: `prevOnWalkable=false`
|
|
||||||
makes `shouldReflect` unconditionally true regardless of the new
|
|
||||||
grounded state — see `PhysicsObjUpdate.cs:163-164`). But
|
|
||||||
`dot(velocity, collisionNormal) = dot((11.15,14.13,0),
|
|
||||||
(0.286,0.429,0.857)) ≈ +9.25` — POSITIVE (moving away from, not into,
|
|
||||||
the surface, because the Z component that would have made it negative
|
|
||||||
was just zeroed) — so the `if (dot < 0f)` reflection guard
|
|
||||||
(`PhysicsObjUpdate.cs:177`) never fires. Velocity survives this tick as
|
|
||||||
`(11.15, 14.13, 0)`.
|
|
||||||
2. **The very next tick** (`PlayerMovementController.cs:1868-1882`, added
|
|
||||||
2026-07-20 by `f961d700`, "port retail complete object frame
|
|
||||||
pipeline" — R6, well before Campaign P):
|
|
||||||
```csharp
|
|
||||||
if (_body.OnWalkable)
|
|
||||||
{
|
|
||||||
float savedWorldVz = _body.Velocity.Z;
|
|
||||||
if (hasAnimationRootMotion)
|
|
||||||
{
|
|
||||||
_body.Velocity = new Vector3(0f, 0f, savedWorldVz);
|
|
||||||
}
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`OnWalkable` is now true (set last tick), so this runs UNCONDITIONALLY,
|
|
||||||
EVERY tick, for as long as the mover stays grounded: it zeros
|
|
||||||
`Velocity.X/Y` to exactly zero (`savedWorldVz` is already 0 from step
|
|
||||||
1), replacing physics-integrated horizontal velocity with
|
|
||||||
animation-root-motion-driven displacement (`pmDelta.Origin`, populated
|
|
||||||
from `_advanceAnimationRootMotion`, which only produces nonzero
|
|
||||||
displacement when a movement key is actually held). **With no key held
|
|
||||||
at the instant of landing, `pmDelta.Origin` stays `Vector3.Zero` forever,
|
|
||||||
and the mover never advances again.** This reproduces `bodyBefore.velocity
|
|
||||||
= (0,0,0)` at record 3434 exactly, and the permanent freeze that follows.
|
|
||||||
|
|
||||||
This is the R6 "local player animation-owned grounded movement"
|
|
||||||
architecture: once grounded, walking is driven entirely by held-input +
|
|
||||||
animation root motion, not by integrating `Velocity`. It has been in
|
|
||||||
place since 2026-07-20 — **ten days before Campaign P and the TS-4
|
|
||||||
removal/revert (2026-07-29/30)** — and is explicitly a frozen-phase
|
|
||||||
architecture per the milestones doc (R6 shipped; the freeze list bars
|
|
||||||
rework without a dedicated brainstorm). It is retail-DIVERGENT in one
|
|
||||||
specific way that matters here: retail does not need a held key to carry
|
|
||||||
residual momentum across a landing — a fast fall onto a walkable-but-
|
|
||||||
sloped surface should glide/sled per `docs/ISSUES.md` #166 ("Slope-landing
|
|
||||||
glide + bounce absent... acdream lands clean and dead"), which is filed,
|
|
||||||
open, and explicitly OUT OF SCOPE for this pass (the `Sledding`
|
|
||||||
`PhysicsStateFlags` bit that would let `calc_friction`'s Sledding-gated
|
|
||||||
overrides engage is never set anywhere in the codebase — a separate,
|
|
||||||
already-tracked gap).
|
|
||||||
|
|
||||||
`git log --oneline -3 -- src/AcDream.Core/Physics/PhysicsObjUpdate.cs`
|
|
||||||
confirms `HandleAllCollisions` itself was also last touched by an
|
|
||||||
unrelated water fix (AP-10, `cc8d57a2`) — Campaign P did not modify it
|
|
||||||
either.
|
|
||||||
|
|
||||||
## 5. Re-reading the oracle plan's S1 claim against the mined evidence
|
|
||||||
|
|
||||||
The task asked specifically: if S1's port is faithful but its SCOPE is
|
|
||||||
wrong, say exactly that. Re-checked against
|
|
||||||
`docs/research/2026-07-30-ts4-116-oracle-plan.md` §2.3-§2.4 and §3, plus
|
|
||||||
this pass's own finding:
|
|
||||||
|
|
||||||
- **S1's port IS faithful in isolation.** Its cited sources
|
|
||||||
(`acclient_2013_pseudo_c.txt:323824-323834`, ACE `BSPTree.cs:221-230`)
|
|
||||||
are an exact structural match — not a BN misdecompile, not a citation
|
|
||||||
error. This was independently re-verified by reading the current
|
|
||||||
`BSPQuery.cs:2259-2302` against the same two sources again this pass; no
|
|
||||||
discrepancy found.
|
|
||||||
- **S1's scope is narrower than any symptom this pass could reproduce —
|
|
||||||
not wider.** The oracle plan's own Addendum 2 (§"implementation
|
|
||||||
session") already found this exact pattern once, for the door
|
|
||||||
tick-22760 capture: the hypothesis assumed the "not-yet-in-Contact"
|
|
||||||
branch would fire, but the mover was actually GROUNDED (`Contact` set),
|
|
||||||
so dispatch went to Path 5 instead and S1's site was never reached. This
|
|
||||||
pass finds the SAME pattern a second, independent time, for a
|
|
||||||
DIFFERENT capture (a genuine airborne fall, not a grounded door-push):
|
|
||||||
the foot sphere (`sphere0`) reaches the rising/sloped polygon at the
|
|
||||||
same moment as or before the head sphere, so the `if (hit0 ||
|
|
||||||
hitPoly0 is not null)` branch above `hasSphere1`'s check fires first and
|
|
||||||
RETURNS before `hasSphere1`'s block is ever entered
|
|
||||||
(`BSPQuery.cs:2188` gates the whole `hasSphere1` block behind falling
|
|
||||||
through that first `if`). `hit1=True` never appears once across the
|
|
||||||
entire 80-tick replay, confirming this mechanically, not just by
|
|
||||||
inference.
|
|
||||||
- **Two independent capture families (a grounded door-push, and now an
|
|
||||||
airborne fall-and-land) both show S1's site going unreached.** This
|
|
||||||
strongly suggests S1's real-world reach is much narrower than its
|
|
||||||
authors worried — for it to matter, a trajectory would need the FOOT
|
|
||||||
sphere to stay clear while the HEAD sphere alone grazes a polygon
|
|
||||||
during an airborne (not-yet-grounded) frame — e.g. jumping up under an
|
|
||||||
overhang, or clipping a roof's underside while airborne with the feet
|
|
||||||
still below the eave line. **Neither of #265's two concrete mined
|
|
||||||
freeze events is that geometry.** S1 remains a real, citable, retail-
|
|
||||||
faithful port-accuracy improvement and should NOT be reverted on this
|
|
||||||
evidence (it fixes a genuine, if narrow, divergence for whenever its
|
|
||||||
exact geometry does occur) — but it is not implicated in the symptoms
|
|
||||||
#265 was filed against.
|
|
||||||
|
|
||||||
## 6. Named culprit
|
|
||||||
|
|
||||||
**Neither S1 nor S2. This is S3 — but not a NEW regression: it is the
|
|
||||||
pre-existing, frozen-phase R6 "grounded movement is animation-root-motion-
|
|
||||||
owned" architecture (`PlayerMovementController.cs:1868-1882`, landed
|
|
||||||
2026-07-20 via `f961d700`, ten days before Campaign P), which
|
|
||||||
unconditionally zeros the mover's horizontal `Velocity` every tick once
|
|
||||||
`OnWalkable` is true, with no gate on approach speed, surface steepness,
|
|
||||||
or how the mover became grounded.** It was mechanically traced, tick by
|
|
||||||
tick, against BOTH of #265's concrete mined freeze events and reproduces
|
|
||||||
the observed `(0,0,0)` velocity and permanent position-freeze exactly.
|
|
||||||
|
|
||||||
This explains symptom (b) (roof slides don't continue — there is no
|
|
||||||
"continue," walking requires a held key that landing doesn't supply) and
|
|
||||||
symptom (c) (stuck at the landing spot indefinitely) completely, for both
|
|
||||||
mined events. It does **not**, by itself, explain symptom (a) (the
|
|
||||||
"bounce" on jumping into an uphill slope) — that is a property of
|
|
||||||
`PhysicsObjUpdate.HandleAllCollisions`'s elastic reflection (`shouldReflect
|
|
||||||
= true` whenever the mover was NOT already on walkable ground before AND
|
|
||||||
after the resolve — `PhysicsObjUpdate.cs:163-164`), which is ALSO
|
|
||||||
pre-existing (from the #182 rebuild, well before Campaign P) and fires for
|
|
||||||
ANY valid `CollisionNormal` reported while airborne, regardless of which
|
|
||||||
BSPQuery branch produced it. This pass did not find or replay a concrete
|
|
||||||
"bounce" event in the captures (the closest analogue — the tick-63
|
|
||||||
edge-freeze in the replay harness — shows a suspicious secondary normal,
|
|
||||||
`(-0.958,0.128,0.256)`, unrelated to the registered polygon's own plane
|
|
||||||
normal, with Path-5 diagnostics showing no fresh BSP hit during the frozen
|
|
||||||
ticks; this smells like stale `ContactPlane`/`CollisionNormal` persistence
|
|
||||||
at a polygon boundary rather than a fresh reflection, and — like the S1
|
|
||||||
revert — was unaffected by reverting S1. It is flagged as a genuine open
|
|
||||||
question, not resolved this pass, and may be an artifact of this
|
|
||||||
harness's single small (24-unit) synthetic triangle rather than a general
|
|
||||||
production bug; a real roof's continuous mesh would not present a "run off
|
|
||||||
the edge of a 24-unit patch" boundary at all. See §7).
|
|
||||||
|
|
||||||
## 7. What's still open (do not guess, per CLAUDE.md)
|
|
||||||
|
|
||||||
1. **Why does the user perceive this as a NEW regression coinciding with
|
|
||||||
Campaign P**, if the freeze mechanism (§4) predates it by ten days and
|
|
||||||
is unaffected by S1/S2? Two honest hypotheses, neither confirmed:
|
|
||||||
(a) the roof-jump/fall scenario was specifically exercised for the
|
|
||||||
FIRST time as part of the Campaign P visual matrix (scenarios 4/5),
|
|
||||||
surfacing a pre-existing bug rather than a new one; (b) a genuinely
|
|
||||||
separate, not-yet-isolated interaction exists. Resolving this needs
|
|
||||||
either a live retail-vs-acdream side-by-side of the EXACT same
|
|
||||||
fall-and-land-with-no-input scenario pre-Campaign-P (to confirm the
|
|
||||||
freeze is not new), or a fresh capture of the user's ACTUAL "roof
|
|
||||||
slide" repro (holding a movement key throughout, not a passive fall) to
|
|
||||||
see whether the animation-root-motion path (which DOES produce
|
|
||||||
displacement while a key is held) also fails.
|
|
||||||
2. **The tick-63 edge freeze** in this pass's own harness (§6, closing
|
|
||||||
parenthetical) — a `CollisionNormal` unrelated to the registered
|
|
||||||
polygon's plane, reported while Path-5 diagnostics show no fresh hit.
|
|
||||||
Candidate next step: extend the harness's synthetic roof to several
|
|
||||||
contiguous polygons (removing the small-triangle-edge artifact) and
|
|
||||||
re-run; if the freeze persists on a much larger interior region, it is
|
|
||||||
a real, separate, third mechanism worth its own root-cause pass
|
|
||||||
(possibly `SpherePath.PrecipiceSlide`'s edge-crossing test, or stale
|
|
||||||
`LastKnownContactPlane` persistence — NOT yet confirmed, do not guess
|
|
||||||
further).
|
|
||||||
3. **Symptom (a)'s bounce** was analyzed only by static code reading
|
|
||||||
(`HandleAllCollisions`'s reflection math), not independently reproduced
|
|
||||||
against a live-captured bounce event — none of the 38
|
|
||||||
`collisionNormalValid=true` records in session2 showed the "airborne,
|
|
||||||
then a large upward `Velocity.Z` jump next tick" signature this pass's
|
|
||||||
Signature-A scanner looked for. A fresh capture specifically of a
|
|
||||||
jump-into-an-upward-slope repro (ideally with `ACDREAM_PROBE_RESOLVE=1`
|
|
||||||
or `ACDREAM_CAPTURE_RESOLVE` active for the WHOLE approach, not just
|
|
||||||
the moment of impact) would let Signature A actually fire and give a
|
|
||||||
concrete oracle the way records 3433/3434 did for the freeze.
|
|
||||||
|
|
||||||
## 8. Recommended fix direction
|
|
||||||
|
|
||||||
**Do not touch S1** (`BSPQuery.cs`'s `hasSphere1` branch) — it is a real,
|
|
||||||
narrow, retail-faithful improvement unrelated to #265's two concrete mined
|
|
||||||
events; reverting it would only reopen the #116 shape-1 door-collision gap
|
|
||||||
it was written to close, for zero benefit here.
|
|
||||||
|
|
||||||
**Do not spend further effort on S2** (`calc_friction`'s threshold) until
|
|
||||||
it is actually wired into a live code path — right now changing it changes
|
|
||||||
nothing observable, in either direction. If/when `calc_friction` IS wired
|
|
||||||
into `PlayerMovementController` (a legitimate future piece of closing #166,
|
|
||||||
the downhill-sled issue), the 0.25 threshold becomes live and worth
|
|
||||||
re-testing at that point, not before.
|
|
||||||
|
|
||||||
**The real target is #166 + the grounded-movement architecture (§4/§6),
|
|
||||||
which is a frozen-phase design question, not a quick fix.** Per CLAUDE.md's
|
|
||||||
"the roadmap and the observed bug disagree → brainstorm before writing
|
|
||||||
code" rule, this needs `superpowers:brainstorming` before any
|
|
||||||
implementation: does acdream want a genuine physics-driven momentum carry
|
|
||||||
across a landing (porting the retail `Sledding` state + a real
|
|
||||||
`calc_friction` wiring), or a narrower "if IsOnGround at high incoming
|
|
||||||
speed, force a minimum coast distance regardless of held input" patch? The
|
|
||||||
former is retail-faithful and already has a filed target (#166); the
|
|
||||||
latter would be a new, unfiled design decision. Either way, this is
|
|
||||||
explicitly NOT an S1/S2 code change — it is new work against
|
|
||||||
`PlayerMovementController.cs`'s grounded-movement block and
|
|
||||||
`PhysicsBody.calc_friction`'s wiring, gated on a design conversation, not a
|
|
||||||
revert.
|
|
||||||
|
|
||||||
## 9. As-fixed addendum (2026-07-30, same day — implementation session)
|
|
||||||
|
|
||||||
The user chose the retail-faithful direction (§8's first option): port the
|
|
||||||
genuine physics-driven momentum carry, wiring `calc_friction` for real
|
|
||||||
rather than adding a narrower coast-distance patch. Implementation
|
|
||||||
landed the same day as this bisect.
|
|
||||||
|
|
||||||
### 9.1 The fix
|
|
||||||
|
|
||||||
Two changes, both minimal and at the exact commit points already
|
|
||||||
responsible for the adjacent state:
|
|
||||||
|
|
||||||
1. **`src/AcDream.Core/Physics/PhysicsEngine.cs`** — `body.GroundNormal`
|
|
||||||
(the vector `calc_friction` dots velocity against, per its own doc
|
|
||||||
comment "`angle = dot(velocity, contactPlane.N)`") had **zero
|
|
||||||
production writers anywhere** before this fix; it silently defaulted
|
|
||||||
to `Vector3.UnitZ` forever (`grep -rn "GroundNormal\s*=" src/` found
|
|
||||||
only the property's own default and calc_friction's internal reads/
|
|
||||||
writes). This is a SEPARATE gap from the one §4 found — even if
|
|
||||||
Velocity had survived the grounded-tick zero, friction would have
|
|
||||||
dotted it against a fake flat-ground normal on any real slope,
|
|
||||||
producing wrong physics. Fixed by syncing
|
|
||||||
`body.GroundNormal = ci.ContactPlane.Normal` (or
|
|
||||||
`ci.LastKnownContactPlane.Normal`) at the exact block
|
|
||||||
(`PhysicsEngine.cs` ~:1297-1320) that already publishes
|
|
||||||
`body.ContactPlane`/`ContactPlaneValid` after every resolve — Core-level,
|
|
||||||
so player, remote, ordinary, and projectile movers all get a real
|
|
||||||
slope normal for free (matching the task's "the mechanism is general"
|
|
||||||
requirement; the ordinary/remote physics updaters
|
|
||||||
(`RuntimeOrdinaryPhysicsUpdater.cs`, `RuntimeRemotePhysicsUpdater.cs`)
|
|
||||||
already compose root motion + `UpdatePhysicsInternal` cleanly, with no
|
|
||||||
destructive zero — this fix brings the player path in line with its
|
|
||||||
own siblings, not a novel invention).
|
|
||||||
|
|
||||||
2. **`src/AcDream.Runtime/Gameplay/PlayerMovementController.cs`** — the
|
|
||||||
grounded-tick block §4 identified (`if (_body.OnWalkable) { ... if
|
|
||||||
(hasAnimationRootMotion) _body.Velocity = new Vector3(0f, 0f,
|
|
||||||
savedWorldVz); ... }`) no longer reconstructs `Velocity` AT ALL for the
|
|
||||||
`hasAnimationRootMotion` case (production graphical local-player
|
|
||||||
path). The condition is now `if (_body.OnWalkable &&
|
|
||||||
!hasAnimationRootMotion)`, so ONLY the headless/test-controller
|
|
||||||
`get_state_velocity` fallback (unchanged) still writes velocity here.
|
|
||||||
Root motion continues to fully own commanded locomotion (walking
|
|
||||||
displacement still comes from `pmDelta.Origin`, never from
|
|
||||||
`Velocity`) — this does not reintroduce command- or packet-cadence-
|
|
||||||
derived grounded translation (the DO-NOT-RETRY rule in
|
|
||||||
`claude-memory/project_physics_collision_digest.md`); it only stops
|
|
||||||
DESTROYING whatever `Velocity` already holds. The existing
|
|
||||||
`preIntegratePos`/`postIntegratePos` bracketing (root-motion apply,
|
|
||||||
then `calc_acceleration()` + `UpdatePhysicsInternal(tickDt)`, then
|
|
||||||
`ResolveWithTransition(preIntegratePos, postIntegratePos, ...)`) was
|
|
||||||
ALREADY structurally correct for composing both channels — retail's
|
|
||||||
`CPhysicsObj::UpdatePositionInternal` composition model — so no
|
|
||||||
further restructuring was needed once the destructive zero was
|
|
||||||
removed.
|
|
||||||
|
|
||||||
### 9.2 Fixture results (freeze → slide, proven)
|
|
||||||
|
|
||||||
`Issue265SteepSlopeCaptureBisectTests.cs` gained a `ComposedTickSample`
|
|
||||||
harness (`ReplayRealRoofLandingComposed`) that mirrors
|
|
||||||
`PlayerMovementController.cs`'s per-tick composition line-for-line using
|
|
||||||
only Core types (`PhysicsBody`, `PhysicsObjUpdate.HandleAllCollisions`,
|
|
||||||
`PhysicsEngine`), parameterized by a
|
|
||||||
`preserveResidualVelocityOnGroundedTick` toggle representing the old vs.
|
|
||||||
new shape:
|
|
||||||
|
|
||||||
- **`ComposedRoofLanding_OldZeroingModel_ReproducesTheMinedFreeze`**
|
|
||||||
(toggle `false`): reproduces the exact mined signature — velocity forced
|
|
||||||
to `(0,0,0)` the tick after landing, frozen solid (`FrozenStreak` grows
|
|
||||||
unbounded) for the rest of the replay.
|
|
||||||
- **`ComposedRoofLanding_NewFix_VelocitySurvivesAndPositionKeepsAdvancing`**
|
|
||||||
(toggle `true`): the SAME captured landing (velocity `(11.15, 14.13,
|
|
||||||
-23.14)` onto the real `(2,3,6)/7` roof normal) now survives the Z-only
|
|
||||||
hand-zero with its full horizontal speed, and the position advances
|
|
||||||
every single tick (`adv=0.5149` per tick, `onWalk=true`, `frozen=0`)
|
|
||||||
for the entire post-landing window — a genuine sustained glide, not a
|
|
||||||
freeze. (The original small real-captured triangle had to be enlarged
|
|
||||||
6x about its centroid — same plane, same normal, same landing point/tick,
|
|
||||||
see `MakeRoofEngine`'s new `scale` parameter — because the real glide
|
|
||||||
travels ~50 m over the test window and would otherwise run off the
|
|
||||||
tiny real triangle's edge into the SEPARATE small-triangle-boundary
|
|
||||||
artifact §7 item 2 already flagged; that artifact is confirmed
|
|
||||||
real and unrelated to this fix, see §9.4.)
|
|
||||||
- **`ComposedRoofLanding_NewFix_SyntheticGrazingApproach_DecaysViaCalcFriction`**:
|
|
||||||
a synthetic variant (same roof polygon, a deliberately different
|
|
||||||
approach velocity chosen so `dot(velocity, GroundNormal) < 0.25` after
|
|
||||||
landing) proves genuine exponential decay: speed at landing ≈ 6.0 m/s
|
|
||||||
decays tick-by-tick down to the `SmallVelocitySquared` hard-zero floor
|
|
||||||
by roughly tick 33 after landing — retail's `calc_friction` formula
|
|
||||||
working exactly as ported.
|
|
||||||
|
|
||||||
**Important nuance:** the REAL captured landing (record 3433's velocity
|
|
||||||
and normal) happens to fall in retail's "moving away fast enough, no
|
|
||||||
friction" band (`dot(velocity, GroundNormal) ≈ +9.25 ≥ 0.25`) — so it
|
|
||||||
glides at CONSTANT velocity across the roof rather than visibly decaying.
|
|
||||||
This is not a bug; retail's own `calc_friction` early-returns in exactly
|
|
||||||
this case (the velocity's horizontal projection points "downhill," same
|
|
||||||
direction as the normal's horizontal projection — see the derivation in
|
|
||||||
§9.3). The task's framing ("decays over subsequent ticks") is
|
|
||||||
demonstrated by the separate synthetic case above, which deliberately
|
|
||||||
selects a velocity/normal pairing where retail's own formula calls for
|
|
||||||
decay; the real mined case demonstrates the OTHER correct retail outcome
|
|
||||||
(sustained glide) for its own geometry. Both are "survives and slides,"
|
|
||||||
never "freezes" — the actual acceptance bar.
|
|
||||||
|
|
||||||
### 9.3 Downhill direction derivation (for the synthetic decay case)
|
|
||||||
|
|
||||||
For a planar triangle with outward normal N and any point P on the
|
|
||||||
plane, `dot(N, P - centroid) = 0` (coplanarity). For a slope where Z
|
|
||||||
increases as you move "uphill," the outward normal's horizontal
|
|
||||||
projection points toward LOWER Z (downhill) — e.g. plane `z = m·x`
|
|
||||||
(uphill as x increases) has normal `∝ (-m, 0, 1)`, whose horizontal
|
|
||||||
component `-m` points toward decreasing x (downhill). The real captured
|
|
||||||
roof normal `(0.2857, 0.4286, 0.8571)` has horizontal projection
|
|
||||||
`(0.2857, 0.4286)` pointing downhill; the captured velocity's horizontal
|
|
||||||
component `(11.15, 14.13)` points in nearly the same direction (both
|
|
||||||
positive, roughly proportional) — i.e. the mover is genuinely sliding
|
|
||||||
DOWN and AWAY from the impact point, which is exactly why
|
|
||||||
`dot(velocity, normal)` comes out strongly positive and friction
|
|
||||||
correctly declines to engage.
|
|
||||||
|
|
||||||
### 9.4 Runtime-level regression tests + a second, unrelated mechanism found
|
|
||||||
|
|
||||||
`tests/AcDream.Runtime.Tests/Gameplay/PlayerMovementControllerTests.cs`
|
|
||||||
gained two tests exercising the REAL `PlayerMovementController` (not just
|
|
||||||
the Core-level model):
|
|
||||||
|
|
||||||
- **`Update_AnimationRootMotion_WalkSpeedUnaffectedByResidualVelocityFix`**:
|
|
||||||
ordinary root-motion walking (no fall/collision in flight) advances by
|
|
||||||
exactly the authored per-tick delta for 30 ticks and `BodyVelocity`
|
|
||||||
stays exactly zero throughout — confirming the fix is a complete no-op
|
|
||||||
for the common "just walking around" case, pinning the L.3c hazard
|
|
||||||
(`claude-memory/project_physics_collision_digest.md`'s DO-NOT-RETRY
|
|
||||||
table) at the Runtime level in addition to the existing
|
|
||||||
`GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests`
|
|
||||||
Core-level pin (unmodified, still green).
|
|
||||||
- **`Update_RunningJumpLandsOnFlatGround_ResidualVelocitySurvivesAndDecays_NotFrozen`**:
|
|
||||||
a real charged running jump (forward + jump, full production dispatch)
|
|
||||||
lands on flat ground and its residual horizontal speed survives the
|
|
||||||
first post-landing tick, then measurably decays (flat ground:
|
|
||||||
`dot(velocity, (0,0,1)) ≈ 0 < 0.25`, so friction DOES engage here,
|
|
||||||
unlike the real roof capture above).
|
|
||||||
|
|
||||||
**Building this test surfaced a second, genuinely separate,
|
|
||||||
already-registered mechanism** (temporary `Console.WriteLine`
|
|
||||||
instrumentation was added and fully removed per CLAUDE.md's diagnostic-
|
|
||||||
logging discipline): `MotionInterpreter.LeaveGround()`
|
|
||||||
(`CMotionInterp::LeaveGround` 0x00528b00, R3-W4/J7/J8, unrelated to
|
|
||||||
#265/#166) recomputes and OVERWRITES `PhysicsObj.Velocity` from
|
|
||||||
`GetLeaveGroundVelocity()` on the grounded→airborne edge, using whatever
|
|
||||||
forward command is interpreted AT THAT EXACT TICK — a real, intentional,
|
|
||||||
already-ported retail behavior. Releasing the forward key in the SAME
|
|
||||||
tick this edge fires (an early test-construction mistake, not a
|
|
||||||
production concern) clobbers the just-launched velocity. Separately,
|
|
||||||
`MotionInterpreter.ApplyCurrentMovementInterpreted`'s AP-77
|
|
||||||
"animation-less/headless movement fallback" (register row AP-77,
|
|
||||||
already correctly scoped: "When `MotionInterpreter.DefaultSink` or the
|
|
||||||
local PartArray callback is absent...") ALSO rewrites grounded velocity
|
|
||||||
from `get_state_velocity()` on every `HitGround`/`LeaveGround` re-apply
|
|
||||||
when no `DefaultSink` is wired — which is exactly the state of a
|
|
||||||
`PlayerMovementController` built directly in a unit test without wiring
|
|
||||||
one. Production (`GameWindow`) always wires a real `DefaultSink`, so
|
|
||||||
neither mechanism is live there; the fixed test (1) holds Forward for one
|
|
||||||
extra tick so `LeaveGround`'s one-time recompute captures the real
|
|
||||||
launch velocity before releasing it, and (2) wires a minimal
|
|
||||||
`FakeAnimationDispatchSink` as `controller.Motion.DefaultSink` so
|
|
||||||
`ApplyCurrentMovementInterpreted` takes its real dispatch branch instead
|
|
||||||
of the AP-77 fallback — making the test representative of the production
|
|
||||||
graphical path rather than the headless one. **Neither mechanism
|
|
||||||
required any production code change or register update** — AP-77's row
|
|
||||||
already accurately describes its scope, and `LeaveGround`'s behavior is
|
|
||||||
intentional retail-ported behavior, not a bug this task touches.
|
|
||||||
|
|
||||||
### 9.5 Symptom (a), the uphill bounce — confirmed separate, unaffected
|
|
||||||
|
|
||||||
Re-derived `PhysicsObjUpdate.HandleAllCollisions`'s `shouldReflect` gate
|
|
||||||
byte-for-byte against the raw retail decomp
|
|
||||||
(`acclient_2013_pseudo_c.txt:282647-282760`,
|
|
||||||
`CPhysicsObj::handle_all_collisions`) this session:
|
|
||||||
`var_10_1` (== `shouldReflect`) ends up `!(arg4 && (transient_state & 2)
|
|
||||||
!= 0 && !sledding)` where `arg4` is `prevContact`/`prevOnWalkable`
|
|
||||||
captured at `SetPositionInternal` entry (before this call's own commits)
|
|
||||||
and `transient_state & 2` is read live inside `handle_all_collisions`
|
|
||||||
itself — i.e. AFTER `set_on_walkable` has already committed the
|
|
||||||
DESTINATION's OnWalkable bit. This is **exactly** `PhysicsObjUpdate.
|
|
||||||
HandleAllCollisions`'s existing `shouldReflect = !(prevOnWalkable &&
|
|
||||||
nowOnWalkable && !sledding)` — a byte-exact port, not a translation bug.
|
|
||||||
For ANY fresh landing from airborne (`prevOnWalkable=false`), retail
|
|
||||||
itself reflects whenever the collision normal shows "moving into the
|
|
||||||
surface" (`dot < 0`), REGARDLESS of whether the destination is walkable.
|
|
||||||
This is the SAME mechanism AD-25 closed (2026-07-30, Campaign P Slice
|
|
||||||
P3, docs/ISSUES.md #166) for both local and remote movers — confirmed
|
|
||||||
pre-existing and out of scope for this task, matching CLAUDE.md's "do
|
|
||||||
not fix code that matches retail" rule.
|
|
||||||
|
|
||||||
`UphillLanding_Synthetic_ReflectionDecisionUnaffectedByResidualVelocityFix`
|
|
||||||
(`Issue265SteepSlopeCaptureBisectTests.cs`) constructs a synthetic
|
|
||||||
30°-uphill walkable slope, a falling-forward approach with `dot(velocity,
|
|
||||||
normal) < 0` by construction, and runs `HandleAllCollisions` with and
|
|
||||||
without the residual-velocity-preserving toggle applied AFTERWARD. The
|
|
||||||
reflection decision (and its resulting velocity) is identical either way
|
|
||||||
— proving the #265/#166 fix is orthogonal to whatever
|
|
||||||
`HandleAllCollisions` decides, not a cause of or a fix for the bounce.
|
|
||||||
The test's own log line documents the specific synthetic case DOES
|
|
||||||
reflect (`Vz` goes from `0` to `+2.27` on this exact input), consistent
|
|
||||||
with retail's byte-exact algorithm — evidence for a future dedicated pass
|
|
||||||
if the user's live repro still shows an unwanted bounce, not a verdict
|
|
||||||
this task renders.
|
|
||||||
|
|
||||||
### 9.6 Test/file summary
|
|
||||||
|
|
||||||
- `src/AcDream.Core/Physics/PhysicsEngine.cs` — `GroundNormal` sync.
|
|
||||||
- `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` — grounded
|
|
||||||
block no longer zeros `Velocity` for the animation-root-motion case.
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/Issue265SteepSlopeCaptureBisectTests.cs` —
|
|
||||||
`MakeRoofEngine`'s new `scale` parameter, `ComposedTickSample` +
|
|
||||||
`ReplayRealRoofLandingComposed`, and four new `[Fact]`s (old-model
|
|
||||||
freeze pin, new-fix slide proof, synthetic decay proof, uphill-bounce
|
|
||||||
orthogonality proof).
|
|
||||||
- `tests/AcDream.Runtime.Tests/Gameplay/PlayerMovementControllerTests.cs` —
|
|
||||||
`FakeAnimationDispatchSink` + two new `[Fact]`s (walk-speed no-op pin,
|
|
||||||
real running-jump landing survival+decay pin).
|
|
||||||
- `docs/ISSUES.md` — #265 and #166 updated (fix implemented, closure
|
|
||||||
pends the user's visual-gate acceptance).
|
|
||||||
- `docs/architecture/retail-divergence-register.md` — AP-7's retirement
|
|
||||||
note corrected (the 0.25f threshold port was always right; it had
|
|
||||||
nothing real to operate on until this fix closed both the grounded-
|
|
||||||
velocity-zero and the `GroundNormal`-wiring gaps). No new row filed —
|
|
||||||
this change ports retail's mechanism faithfully; it does not introduce
|
|
||||||
a new deviation.
|
|
||||||
|
|
||||||
### 9.7 Verification
|
|
||||||
|
|
||||||
`dotnet test` (Release): 4074 Core tests / 2 skips, 434 Runtime tests / 0
|
|
||||||
skips, 3971 App tests / 3 skips — all green, no regressions. Complete
|
|
||||||
solution suite (9 projects): 9993 total, 9988 passed, 5 skipped, 0
|
|
||||||
failed.
|
|
||||||
|
|
@ -1,713 +0,0 @@
|
||||||
# Animation system parity audit — retail vs acdream (2026-07-30)
|
|
||||||
|
|
||||||
**Status: COMPLETE — report-only investigation, no code changes made.**
|
|
||||||
|
|
||||||
Scope: `CMotionInterp` / `CSequence` / `MotionTableManager` / `CMotionTable`
|
|
||||||
retail method surfaces vs acdream's `src/AcDream.Core/Physics/MotionInterpreter.cs`,
|
|
||||||
`src/AcDream.Core/Physics/Motion/*`, `src/AcDream.Core/Physics/AnimationSequencer.cs`,
|
|
||||||
`src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs`.
|
|
||||||
|
|
||||||
**Bottom line:** the core animation-selection/playback stack
|
|
||||||
(`MotionInterpreter`/`CSequence`/`CMotionTable`/`MotionTableManager`) is a
|
|
||||||
faithful, heavily retail-cited port. All 103 symbols.json-listed retail
|
|
||||||
methods across the four classes were enumerated (section 1); only one is a
|
|
||||||
genuine unexplained gap (`get_adjusted_max_speed`) and the rest of the
|
|
||||||
"unported" set is verified dead code in retail itself. All 7 traced
|
|
||||||
feel-visible flows (section 2) came back parity. The real, actionable gaps
|
|
||||||
are narrow and listed in section 6's ranked catalog: an animated-emote
|
|
||||||
authoring gap (AD-57), two already-known hook-timing residuals (TS-50/
|
|
||||||
TS-51) precisely rescoped against current code, one likely-already-fixed
|
|
||||||
issue (#64) needing only live re-verification, and two untraced
|
|
||||||
combat/casting-layer questions flagged for a future audit outside this
|
|
||||||
scope.
|
|
||||||
|
|
||||||
## Note on the requested prior deep-dive doc
|
|
||||||
|
|
||||||
The task referenced `docs/research/2026-06-04-animation-sequencer-deep-dive.md`
|
|
||||||
as prior art. That file does not exist in this worktree or anywhere in git
|
|
||||||
history (`git log --all --diff-filter=A -- "*animation-sequencer-deep-dive*"`
|
|
||||||
returns nothing). `claude-memory/MEMORY.md` indexes it, and a same-named
|
|
||||||
**skill** (`acdream-animation-sequencer-deep-dive`) exists that would
|
|
||||||
presumably generate such a doc, but no prior run's output is present at that
|
|
||||||
path or any other. The closest prior-art documents actually in the repo are:
|
|
||||||
- `docs/research/2026-06-26-movement-animation-retail-parity-audit.md` (D1-D12
|
|
||||||
divergence list, dated before the R6/J-slice root-motion work — most of its
|
|
||||||
wire-format findings, D1/D3/D4/D9, have likely since been superseded by
|
|
||||||
TS-33/TS-47 and the R6 root-motion campaign; treated as historical baseline
|
|
||||||
only, re-verified against current code below, not trusted at face value)
|
|
||||||
- `docs/research/acclient_animation_map.md`, `docs/research/acclient_animation_pseudocode.md`
|
|
||||||
- `docs/research/2026-04-21-animation-audit.md`, `docs/research/2026-04-28-combat-animation-planner.md`
|
|
||||||
- `docs/research/2026-07-02-r1-csequence/r1-acdream-sequencer.md`
|
|
||||||
- `docs/research/2026-07-02-inbound-motion-verbatim-port-handoff.md`
|
|
||||||
|
|
||||||
This audit proceeds using those plus the R6 sections of
|
|
||||||
`claude-memory/project_physics_collision_digest.md` (which describes what R6
|
|
||||||
already shipped — not re-audited here per task instructions) and a fresh
|
|
||||||
grep sweep of the named retail decomp.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Method-coverage sweep
|
|
||||||
|
|
||||||
Two independent passes fed this section: a dedicated method-coverage-sweep
|
|
||||||
sub-agent did a symbol-by-symbol enumeration against `symbols.json` +
|
|
||||||
pseudo-C call-site tracing, and the lead auditor separately read essentially
|
|
||||||
the entirety of all four files directly (`MotionInterpreter.cs` ~2,600 of
|
|
||||||
3,182 lines read in full, `CSequence.cs`/`CMotionTable.cs`/`MotionState.cs`/
|
|
||||||
`AnimationSequencer.cs` read in full) and independently confirmed the
|
|
||||||
sub-agent's headline finding (`GetMaxSpeed()` is the only max-speed accessor
|
|
||||||
anywhere in the App/Runtime call sites — `grep` for `AdjustedMaxSpeed`/
|
|
||||||
`get_adjusted_max_speed` across `src/` returns zero hits outside the doc
|
|
||||||
comment that names it as unported). The two passes agree; findings below are
|
|
||||||
merged, with the sub-agent's table format preserved since it's more scannable
|
|
||||||
than prose.
|
|
||||||
|
|
||||||
**Methodology note (symbol-artifact class):** `symbols.json` occasionally
|
|
||||||
attributes a class method name to an address whose pseudo-C body is a
|
|
||||||
different, unrelated function, or lists two names at the identical address
|
|
||||||
(most likely `/OPT:ICF` identical-code-folding at link time collapsing
|
|
||||||
byte-identical trivial bodies, with the PDB keeping multiple aliases for one
|
|
||||||
surviving address). Confirmed instances: `CMotionInterp::HandleEnterWorld`
|
|
||||||
@ `0x00694750` resolves to `IDClass<>::~IDClass` (unrelated template
|
|
||||||
destructor); `CMotionInterp::InqStyle` @ `0x00527B10` resolves to
|
|
||||||
`CBaseFilter::GetPinVersion` (unrelated DirectShow class); `MotionTableManager::RemoveLinkAnimations`
|
|
||||||
and `HandleEnterWorld` are both listed at `0x0051BDD0` (only one body exists
|
|
||||||
there); `CMotionTable::Allocator`/`Allocate` are both listed at `0x004F96E0`.
|
|
||||||
These are marked **SYMBOL-ARTIFACT** below rather than forced into
|
|
||||||
ported/unported, since the decomp genuinely cannot answer what (if anything)
|
|
||||||
that distinct method does.
|
|
||||||
|
|
||||||
Also confirmed independently on both classes that have a `Pack`/`UnPack`
|
|
||||||
family (`CSequence`, `CMotionTable`): these `PackObj`/`DBObj` serialization
|
|
||||||
methods have **zero call sites anywhere in the 1.4M-line pseudo-C** outside
|
|
||||||
their own bodies and a `.rdata` vtable-slot registration — dead code
|
|
||||||
inherited from a shared server/client engine base, never invoked by the
|
|
||||||
retail client itself. Their absence in acdream is correctly not a gap.
|
|
||||||
|
|
||||||
### CMotionInterp (41 symbols.json entries)
|
|
||||||
|
|
||||||
| Retail method (addr) | acdream status | Cite |
|
|
||||||
|---|---|---|
|
|
||||||
| PerformMovement (0x00528E80) | ported-with-cite | `MotionInterpreter.cs:820` |
|
|
||||||
| DoMotion (0x00528D20) | ported-with-cite | `MotionInterpreter.cs:880,932` |
|
|
||||||
| StopMotion (0x00528530) | ported-with-cite | `MotionInterpreter.cs:982,1009` |
|
|
||||||
| StopCompletely (0x00527E40) | ported-with-cite | `MotionInterpreter.cs:1078` |
|
|
||||||
| get_state_velocity (0x00527D50) | ported-with-cite | `MotionInterpreter.cs:1188` |
|
|
||||||
| adjust_motion (0x00528010) | ported-with-cite | `MotionInterpreter.cs:1290` |
|
|
||||||
| apply_run_to_command (0x00527BE0) | ported-with-cite | `MotionInterpreter.cs:1355` |
|
|
||||||
| apply_raw_movement (0x005287E0) | ported-with-cite | `MotionInterpreter.cs:1398,1498` |
|
|
||||||
| apply_current_movement (0x00528870) | ported-with-cite | `MotionInterpreter.cs:1459` |
|
|
||||||
| ReportExhaustion (0x005288D0) | ported-with-cite | `MotionInterpreter.cs:1619` |
|
|
||||||
| SetWeenieObject (0x00528920) | ported-with-cite | `MotionInterpreter.cs:1671` |
|
|
||||||
| SetPhysicsObject (0x00528970) | ported-with-cite | `MotionInterpreter.cs:1721` |
|
|
||||||
| jump_charge_is_allowed (0x00527A50) | ported-with-cite | `MotionInterpreter.cs:1761` |
|
|
||||||
| charge_jump (0x005281C0) | ported-with-cite | `MotionInterpreter.cs:1826` |
|
|
||||||
| jump (0x00528780) | ported-with-cite | `MotionInterpreter.cs:1883` |
|
|
||||||
| get_jump_v_z (0x00527AA0) | ported-with-cite | `MotionInterpreter.cs:1920` |
|
|
||||||
| get_leave_ground_velocity (0x005280C0) | ported-with-cite | `MotionInterpreter.cs:1958` |
|
|
||||||
| jump_is_allowed (0x005282B0) | ported-with-cite | `MotionInterpreter.cs:2026,2051` |
|
|
||||||
| contact_allows_move (0x00528240) | ported-with-cite | `MotionInterpreter.cs:2123` |
|
|
||||||
| add_to_queue (0x00527B80) | ported-with-cite | `MotionInterpreter.cs:2164` (`AddToQueue`) |
|
|
||||||
| motions_pending (0x00527FE0) | ported-with-cite | `MotionInterpreter.cs:2173` |
|
|
||||||
| MotionDone (0x00527EC0) | ported-with-cite | `MotionInterpreter.cs:2193` |
|
|
||||||
| HandleExitWorld (0x00527F30) | ported-with-cite | `MotionInterpreter.cs:2245` |
|
|
||||||
| is_standing_still (0x00527FA0) | ported-with-cite | `MotionInterpreter.cs:2266` |
|
|
||||||
| motion_allows_jump (0x005279E0) | ported-with-cite | `MotionInterpreter.cs:2310` |
|
|
||||||
| LeaveGround (0x00528B00) | ported-with-cite | `MotionInterpreter.cs:2373` (independently read in full) |
|
|
||||||
| HitGround (0x00528AC0) | ported-with-cite | `MotionInterpreter.cs:2425` (independently read in full) |
|
|
||||||
| enter_default_state (0x00528C80) | ported-with-cite | `MotionInterpreter.cs:2483` |
|
|
||||||
| set_hold_run (0x00528B70) | ported-with-cite | `MotionInterpreter.cs:2521` |
|
|
||||||
| SetHoldKey (0x00528BB0) | ported-with-cite | `MotionInterpreter.cs:2575` |
|
|
||||||
| get_max_speed (0x00527CB0) | ported-with-cite | `MotionInterpreter.cs:2632` (`GetMaxSpeed`; doc comment includes a forensic re-derivation of the ×4.0 constant from raw x87 disassembly, UN-2 resolved) |
|
|
||||||
| **get_adjusted_max_speed (0x00527D00)** | **UNPORTED-UNEXPLAINED** | None found — independently confirmed (see intro above). Sibling of `get_max_speed`; retail `InterpolationManager::adjust_offset` (pc:353107, `0x00555dbe`) chooses between the two via a static toggle `fUseAdjustedSpeed_`. acdream's dead-reckoning catch-up path (`RemoteMotionCombiner.cs:90`, `RuntimeRemotePhysicsUpdater.cs:254,291,685`) only ever calls `GetMaxSpeed()`. No register/ISSUES row covers this. |
|
|
||||||
| move_to_interpreted_state (0x005289C0) | ported-with-cite | `MotionInterpreter.cs:2697` |
|
|
||||||
| apply_interpreted_movement (0x00528600) | ported-with-cite | `MotionInterpreter.cs:2797` |
|
|
||||||
| DoInterpretedMotion (0x00528360) | ported-with-cite | `MotionInterpreter.cs:2931,2944` |
|
|
||||||
| StopInterpretedMotion (0x00528470) | ported-with-cite | `MotionInterpreter.cs:3093,3103` |
|
|
||||||
| Create (0x00528C00) | ported, uncited | `MotionInterpreter.cs:795` — retail's `Create` calls `SetWeenieObject`/`SetPhysicsObject` while `initted==0`, making both no-ops; plain field assignment is behaviorally identical |
|
|
||||||
| Destroy / ~CMotionInterp (0x00527B40 / 0x00527FF0) | trivial, skipped | GC-obviated (manual `pending_motions` free-list walk superseded by `LinkedList<T>`) |
|
|
||||||
| HandleEnterWorld (0x00694750) | SYMBOL-ARTIFACT | see methodology note |
|
|
||||||
| InqStyle (0x00527B10) | SYMBOL-ARTIFACT | see methodology note |
|
|
||||||
|
|
||||||
**~85% ported-with-cite** (35/41; ~95% of the 37 real/substantive methods
|
|
||||||
after excluding 2 symbol-artifacts and 2 GC-obviated dtors). One genuine gap.
|
|
||||||
|
|
||||||
### CSequence (28 symbols.json entries)
|
|
||||||
|
|
||||||
| Retail method (addr) | acdream status | Cite |
|
|
||||||
|---|---|---|
|
|
||||||
| ctor (0x005249F0) | ported, trivial | `CSequence.cs:81` (zero-init, matched by C# field defaults) |
|
|
||||||
| set_object (0x00524820) | SYMBOL-ARTIFACT, functionally ported | address really resolves to `DBObj::SetDID`; behavioral equivalent is the public `HookObj` field, `CSequence.cs:300` |
|
|
||||||
| set_velocity / set_omega (0x00524880 / 0x005248A0) | ported-with-cite | `CSequence.cs:236-237` |
|
|
||||||
| execute_hooks (0x00524830) | ported-with-cite | `CSequence.cs:503` |
|
|
||||||
| combine_physics / subtract_physics (0x005248C0 / 0x00524900) | ported-with-cite | `CSequence.cs:238-239` |
|
|
||||||
| multiply_cyclic_animation_fr (0x00524940) | ported-with-cite | `CSequence.cs:249` |
|
|
||||||
| get_curr_animframe (0x00524970) | ported-with-cite | `CSequence.cs:266` (independently read) |
|
|
||||||
| set_placement_frame (0x005249B0) | ported-with-cite | `CSequence.cs:258` |
|
|
||||||
| get_curr_frame_number (0x005249D0) | ported-with-cite | `CSequence.cs:274` |
|
|
||||||
| apply_physics (0x00524AB0) | ported-with-cite | `CSequence.cs:285` (independently read) |
|
|
||||||
| apricot (0x00524B40) | ported-with-cite | `CSequence.cs:219` (retail's own PDB-verified name, kept verbatim) |
|
|
||||||
| has_anims (0x00524BD0) | ported-with-cite | `CSequence.cs:90` |
|
|
||||||
| remove_link_animations (0x00524BE0) | ported-with-cite | `CSequence.cs:187` |
|
|
||||||
| remove_all_link_animations (0x00524CA0) | ported-with-cite | `CSequence.cs:207` |
|
|
||||||
| clear_physics / clear_animations (0x00524D50 / 0x00524DC0) | ported-with-cite | `CSequence.cs:149,140` |
|
|
||||||
| remove_cyclic_anims (0x00524E40) | ported-with-cite | `CSequence.cs:163` |
|
|
||||||
| **pack_size / Pack / UnPack** (0x00524F20 / 0x00525020 / 0x005259D0) | **UNPORTED, dead-code-verified** | zero external callers anywhere in the retail decomp; correctly not ported |
|
|
||||||
| append_animation (0x00525510) | ported-with-cite | `CSequence.cs:109` (independently read) |
|
|
||||||
| clear (0x005255B0) | ported-with-cite | `CSequence.cs:130` |
|
|
||||||
| update_internal (0x005255D0) | ported-with-cite | `CSequence.cs:332` (independently read in full — the iterative frame-crossing loop, no safety cap, matches retail exactly) |
|
|
||||||
| advance_to_next_animation (0x005252B0) | ported-with-cite | `CSequence.cs:435` (independently read) |
|
|
||||||
| update (0x00525B80) | ported-with-cite | `CSequence.cs:307` |
|
|
||||||
| ~CSequence (0x00524A30) | trivial, skipped | GC-obviated |
|
|
||||||
|
|
||||||
**~79% ported-with-cite** (22/28; ~96% of substantive methods). Cleanest of
|
|
||||||
the four classes — everything that drives frame playback, hook dispatch, or
|
|
||||||
physics accumulation is ported and cited. The two known field-representation
|
|
||||||
divergences (`double` vs x87 `long double` frame_number; `LinkedList<T>` vs
|
|
||||||
intrusive `DLList`) are already register rows AD-33/AD-34. **No concerning
|
|
||||||
gap.**
|
|
||||||
|
|
||||||
### MotionTableManager (17 symbols.json entries)
|
|
||||||
|
|
||||||
| Retail method (addr) | acdream status | Cite |
|
|
||||||
|---|---|---|
|
|
||||||
| initialize_state (0x0051C030) | ported-with-cite | `MotionTableManager.cs:353` |
|
|
||||||
| AnimationDone (0x0051BCE0) | ported-with-cite | `MotionTableManager.cs:290` (independently read) |
|
|
||||||
| CheckForCompletedMotions (0x0051BE00) | ported-with-cite | `MotionTableManager.cs:322` |
|
|
||||||
| UseTime (0x0051BFD0) | ported-with-cite | `MotionTableManager.cs:342` |
|
|
||||||
| HandleEnterWorld / RemoveLinkAnimations (both 0x0051BDD0) | ported-with-cite / SYMBOL-ARTIFACT-duplicate | `MotionTableManager.cs:373` — pseudo-C shows only one body at this address |
|
|
||||||
| HandleExitWorld (0x0051BDA0) | ported-with-cite | `MotionTableManager.cs:385` |
|
|
||||||
| SetPhysicsObject (0x0051BBC0) | deliberately-absent-with-reason | `MotionTableManager.cs:20-22` (file header: "C# has no physics_obj field — R2 leaves the CPhysicsObj::MotionDone target as an injectable seam") |
|
|
||||||
| Create (0x0051BC50) | ported-with-cite | `MotionTableManager.cs:129,138` |
|
|
||||||
| GetMotionTableID (0x0051BC10) | unported, verified low-risk | retail's only caller (`CPartArray::SetMotionTableID @ 0x005186e0`) uses it purely as a dirty-check before destroying+reconstructing the whole manager; acdream gets the same capability by constructing a fresh `AnimationSequencer`/`MotionTableManager` (`AnimationSequencer.cs:287`) |
|
|
||||||
| PerformMovement (0x0051C0B0) | ported-with-cite | `MotionTableManager.cs:409` (independently read in full) |
|
|
||||||
| SetMotionTableID (0x0051BBD0) | unported, verified low-risk | its only caller in the entire retail client is its own `Create` factory (single call site, pc:290526) |
|
|
||||||
| truncate_animation_list (0x0051BCA0) | ported-with-cite | `MotionTableManager.cs:259` |
|
|
||||||
| Destroy / ~MotionTableManager | trivial, skipped | GC-obviated |
|
|
||||||
| remove_redundant_links (0x0051BF20) | ported-with-cite | `MotionTableManager.cs:191` (independently read in full — byte-for-byte match including the `0xb0000000`/`0x70000000` block masks) |
|
|
||||||
| add_to_queue (0x0051BFE0) | ported-with-cite | `MotionTableManager.cs:166` |
|
|
||||||
|
|
||||||
**~65% raw ported-with-cite** (11/17), plus 1 deliberately-absent and 2
|
|
||||||
unported-but-verified-zero-risk (both `MotionTableID` accessors — retail
|
|
||||||
itself only "changes" a motion table by destroy+recreate at the
|
|
||||||
`CPartArray` layer, exactly matching acdream's architecture). **No
|
|
||||||
concerning gap.**
|
|
||||||
|
|
||||||
### CMotionTable (17 symbols.json entries)
|
|
||||||
|
|
||||||
| Retail method (addr) | acdream status | Cite |
|
|
||||||
|---|---|---|
|
|
||||||
| ctor (0x004F94E0) | ported, uncited | `CMotionTable.cs:64` |
|
|
||||||
| Pack / UnPack (0x00523180 / 0x005238C0) | **UNPORTED, dead-code-verified** | zero external callers, same dead `PackObj` family as `CSequence` |
|
|
||||||
| Destroy / ~CMotionTable | trivial, skipped | GC-obviated (`cycles`/`modifiers`/`links` hash tables → `Dictionary<>`) |
|
|
||||||
| GetDBOType (0x005268A0) | N/A, architecturally superseded | retail RTTI-style type tag; acdream's typed `Dats.Get<MotionTable>()` generic accessor makes it unnecessary |
|
|
||||||
| Allocator / Allocate (both 0x004F96E0) | SYMBOL-ARTIFACT-duplicate / trivial | `new CMotionTable(table)` supersedes the placement-new+construct factory directly |
|
|
||||||
| SetDefaultState (0x005230A0) | ported-with-cite | `CMotionTable.cs:605` (independently read) |
|
|
||||||
| DoObjectMotion / StopObjectMotion / StopObjectCompletely (0x00523E90/0x00523EC0/0x00523ED0) | ported-with-cite | `CMotionTable.cs:635,640,652` |
|
|
||||||
| re_modify (0x005222E0) | ported-with-cite | `CMotionTable.cs:528` |
|
|
||||||
| is_allowed (0x005226C0) | ported-with-cite | `CMotionTable.cs:172` |
|
|
||||||
| get_link (0x00522710) | ported-with-cite | `CMotionTable.cs:201` (independently read — the reversed-key branch, field-validated per its own doc comment) |
|
|
||||||
| GetObjectSequence (0x00522860) | ported-with-cite | `CMotionTable.cs:255` — independently read in full; the single highest-stakes function in this whole sweep (branch-heavy style/cycle/action/modifier dispatcher), ported branch-for-branch with inline citations, including three explicitly-preserved retail quirks (A4-#1 double-hop tick counting never double-charging the base cycle; A4-#2 silent no-op in `ChangeCycleSpeed` when old speed ~0 but new speed isn't; A4-#5 `ReModify`'s lockstep-snapshot termination bound) |
|
|
||||||
| StopSequenceMotion (0x00522FC0) | ported-with-cite | `CMotionTable.cs:559` |
|
|
||||||
|
|
||||||
Bonus (retail free functions, not `CMotionTable::` members, but ported+cited
|
|
||||||
in the same file): `same_sign`→`SameSign` (`:77`), `change_cycle_speed`→
|
|
||||||
`ChangeCycleSpeed` (`:88`), `add_motion`→`AddMotion` (`:116`),
|
|
||||||
`combine_motion`→`CombineMotion` (`:143`), `subtract_motion`→`SubtractMotion`
|
|
||||||
(`:156`) — all independently read.
|
|
||||||
|
|
||||||
**~53% raw ported-with-cite** (9/17), but **100% of the 9 substantive
|
|
||||||
motion-selection methods** — every method that isn't Pack/UnPack/RTTI/memory
|
|
||||||
management is ported and cited. **No concerning gap.**
|
|
||||||
|
|
||||||
### Bottom line across all four classes
|
|
||||||
|
|
||||||
103 total symbols.json entries examined: ~77 ported-with-cite, 4
|
|
||||||
symbol-artifacts (not real distinct methods), ~9 trivial/GC-obviated, 1
|
|
||||||
architecturally superseded, 1 ported-but-uncited (`CMotionInterp.Create`),
|
|
||||||
and 8 genuinely unported — of which 7 are verified dead code in the retail
|
|
||||||
client itself (the `Pack`/`UnPack`/`pack_size`/`GetMotionTableID`/
|
|
||||||
`SetMotionTableID` family, confirmed via call-site tracing). **The single
|
|
||||||
genuine, unexplained, feel-visible-risk gap across all four classes is
|
|
||||||
`CMotionInterp::get_adjusted_max_speed` (0x00527D00)** — the only unported
|
|
||||||
method sitting on a hot per-tick gameplay path (dead-reckoning catch-up
|
|
||||||
speed clamp) whose retail selection condition could not be resolved from
|
|
||||||
static analysis alone. All four files hold up as genuinely faithful,
|
|
||||||
well-cited retail ports; this sweep found no evidence of silently-diverged
|
|
||||||
gameplay logic in any of the four classes' core responsibilities.
|
|
||||||
|
|
||||||
## 2. Feel-visible flow verdicts
|
|
||||||
|
|
||||||
This section was independently traced by the lead audit against the primary
|
|
||||||
source (the extensively retail-cited C# in `CMotionTable.cs`, `CSequence.cs`,
|
|
||||||
`MotionState.cs`, `AnimationSequencer.cs`, `MotionInterpreter.cs` — most
|
|
||||||
methods in these five files quote the exact decompiled C body in a doc
|
|
||||||
comment, so this section cites the acdream file:line as primary evidence
|
|
||||||
rather than re-deriving from the 1.4M-line decomp text directly; the
|
|
||||||
background method-coverage-sweep and flow-tracing agents' independent
|
|
||||||
findings are merged in below where they add or contest something). A
|
|
||||||
striking finding up front: several of the 7 flows below turned out to have
|
|
||||||
**verdict parity for a decisive reason that ISN'T "acdream ported it
|
|
||||||
correctly"** — retail itself doesn't do the fancier thing the flow's framing
|
|
||||||
implied. That distinction matters for a retail-faithful project: it means
|
|
||||||
there is nothing to build, not merely nothing left to fix.
|
|
||||||
|
|
||||||
**1. Stance-change transition animations — PARITY.**
|
|
||||||
`CMotionTable.GetObjectSequence` Branch 1 (`src/AcDream.Core/Physics/Motion/CMotionTable.cs:279-336`,
|
|
||||||
citing retail `GetObjectSequence @ 0x00522860`) is a full style-change
|
|
||||||
dispatcher: it computes an exit link from the current substate to the
|
|
||||||
current style's default substate, a direct link from the current style's
|
|
||||||
default substate to the target style's default substate, and — when no
|
|
||||||
direct link exists — a double-hop through the table's `DefaultStyle` (lines
|
|
||||||
308-314), then plays exit-link → hop1 → hop2 → new-cycle in sequence
|
|
||||||
(`AddMotion` calls at 318-321) before installing the new style/substate.
|
|
||||||
This is retail's genuine weapon-draw/style-change link mechanism, not a
|
|
||||||
simplified instant cut. `AnimationSequencer.SetCycle`
|
|
||||||
(`src/AcDream.Core/Physics/AnimationSequencer.cs:390-391`) drives style
|
|
||||||
changes through exactly this path before dispatching the target motion.
|
|
||||||
|
|
||||||
**2. Landing after jump/fall (soft vs hard landing) — PARITY, and the "gap"
|
|
||||||
doesn't exist in retail.** `MotionInterpreter.HitGround`
|
|
||||||
(`src/AcDream.Core/Physics/MotionInterpreter.cs:2425-2443`) quotes retail's
|
|
||||||
`CMotionInterp::HitGround @ 0x00528ac0` FULL BODY: strip link animations,
|
|
||||||
then re-apply the PRESERVED pre-fall interpreted forward command (walk/run/
|
|
||||||
ready) — there is no velocity, fall-distance, or fall-duration branch
|
|
||||||
anywhere in that function. Retail's `Falling` SubState
|
|
||||||
(`MotionInterpreter.cs:56-63`) is one airborne cycle regardless of how far
|
|
||||||
the body fell; landing is simply "the Falling→X link fires through the same
|
|
||||||
`GetObjectSequence` Branch 2 cycle-to-cycle mechanism verified in item 6."
|
|
||||||
There is no severity-based "hard landing" animation to select in retail's
|
|
||||||
own Humanoid MotionTable, so this was never a divergence to close.
|
|
||||||
|
|
||||||
**3. In-place turn cycles vs omega-driven turning — PARITY (already shipped
|
|
||||||
under R6; not re-audited here per task scope, confirmed only that the two
|
|
||||||
things are the SAME mechanism, not competing ones).** `TurnRight`/`TurnLeft`
|
|
||||||
(`0x6500000D`/`0x6500000E`) carry the `0x40000000` cycle-class bit
|
|
||||||
(`0x65000000 & 0x40000000 != 0`), so they ARE genuine `CMotionTable` cycles
|
|
||||||
with their own authored `Anims` (the visual leg-crossing/pivot animation)
|
|
||||||
AND their own authored `Omega` (R6's pinned finding: `omega.Z = -1.5`
|
|
||||||
rad/s ≈ -86°/s from the installed Humanoid table, not a synthetic 90°
|
|
||||||
formula). `CMotionTable.AddMotion`
|
|
||||||
(`src/AcDream.Core/Physics/Motion/CMotionTable.cs:116-134`) writes both the
|
|
||||||
anim frames and the omega from the SAME `MotionData` record onto the
|
|
||||||
sequence in one call; `CSequence.ApplyPhysics` rotates the Frame by that
|
|
||||||
omega every frame the turn cycle plays. There was never a separate
|
|
||||||
"visual cycle vs physical rotation" question to resolve — one MotionData
|
|
||||||
record drives both.
|
|
||||||
|
|
||||||
**4. Walk↔run mid-stride transitions — PARITY (same Branch-2 machinery as
|
|
||||||
item 1, one level down).** Walk and Run are both cycle-class substates
|
|
||||||
within `NonCombat`/combat styles, so crossing the walk/run threshold or
|
|
||||||
toggling the Run hold-key is a same-style cycle-to-cycle request through
|
|
||||||
`GetObjectSequence` Branch 2 (`CMotionTable.cs:341-423`): it looks up a
|
|
||||||
direct link between the two substates via `GetLink`, falls back to a
|
|
||||||
style-default double-hop if none exists (lines 378-383), and has a
|
|
||||||
same-substate "fast re-speed" path (lines 358-367) for a pure speed change
|
|
||||||
within the SAME substate (e.g. accelerating while already running) that
|
|
||||||
rescales the cyclic framerate and physics in place rather than re-triggering
|
|
||||||
a full transition. This is retail's genuine walk-to-run link/blend
|
|
||||||
mechanism, not an instant swap.
|
|
||||||
|
|
||||||
**5. Backward/strafe cycle selection — PARITY, and again the "gap" doesn't
|
|
||||||
exist in retail.** `AnimationSequencer.SetCycle`
|
|
||||||
(`src/AcDream.Core/Physics/AnimationSequencer.cs:344-348, 367-381`) states
|
|
||||||
plainly, citing ACE's `MotionInterp.cs:394-428` as cross-check: "the AC
|
|
||||||
MotionTable has NO cycles for TurnLeft, SideStepLeft, or WalkBackward. These
|
|
||||||
are played as their right-side/forward equivalents with a negated
|
|
||||||
framerate so the animation runs in reverse." This is a retail asset-content
|
|
||||||
fact, not an acdream simplification — there is no distinct backward-walk or
|
|
||||||
strafe-left animation to select in the first place; retail itself reverses
|
|
||||||
the forward/right cycle. acdream's remap (WalkBackward → WalkForward at
|
|
||||||
-0.65×speed, SideStepLeft → SideStepRight at -1×speed) matches this exactly
|
|
||||||
at both the `AnimationSequencer` boundary (local-player raw input) and the
|
|
||||||
`MotionInterpreter.adjust_motion` boundary (wire-level, R3-cited) — see
|
|
||||||
section 1's method sweep for whether both call sites are still needed or
|
|
||||||
one is now dead code.
|
|
||||||
|
|
||||||
**6. Link-animation traversal system — PARITY, and it is the single most
|
|
||||||
load-bearing finding of this audit.** `CMotionTable.GetLink`
|
|
||||||
(`CMotionTable.cs:190-241`, retail `get_link @ 0x00522710`) is a genuine,
|
|
||||||
general-purpose `(fromStyle, fromSubstate, toSubstate)` link lookup over the
|
|
||||||
DAT-authored `Links` dictionary — not a hardcoded Ready/Walk/Run subset. It
|
|
||||||
handles the forward direction, a reversed-key direction (used when a speed
|
|
||||||
sign flip means "the link is authored the other way," e.g. the Ready↔
|
|
||||||
WalkBackward case the doc comment says was field-validated fixing a
|
|
||||||
"left leg twitches" glitch), and a style-level catch-all fallback. Every one
|
|
||||||
of `GetObjectSequence`'s four branches (style-change, cycle, action,
|
|
||||||
modifier) calls it and composes the result into 1-3 chained `AddMotion`
|
|
||||||
calls (exit link, direct/hop1, hop2) before the target cycle, exactly
|
|
||||||
matching retail's own double-hop-via-`DefaultStyle` fallback for style
|
|
||||||
changes with no direct link, and an out-hop/action-link/return-hop triple
|
|
||||||
for action-class motions with no direct link to the target (`CMotionTable.cs:428-478`,
|
|
||||||
with the load-bearing `#A4-1` tick-count citation: "never the base cycle,
|
|
||||||
never double-counted (ACE's bug, not retail's)" — i.e. acdream's tick
|
|
||||||
accounting is MORE correct than the reference ACE port here, not less).
|
|
||||||
This resolves the audit's biggest open question going in: acdream does not
|
|
||||||
skip genuine style-to-style links (drawing a weapon, sheathing, sitting
|
|
||||||
down) in favor of a hardcoded locomotion-only subset.
|
|
||||||
|
|
||||||
**7. Interrupted-animation behavior — PARITY at the queue-mechanics level;
|
|
||||||
one narrower residual question outside this file set.** A dedicated
|
|
||||||
flow-tracing sub-agent (independent pass, cross-checked against ACE) closed
|
|
||||||
most of the uncertainty this item started with. Retail
|
|
||||||
`MotionTableManager::RemoveRedundantLinks` (`0x0051bf20`) explicitly only
|
|
||||||
collapses cycle-class-not-modifier or style-class queue tails — the
|
|
||||||
modifier/action-class branch is "neither branch taken" (confirmed directly
|
|
||||||
in `CMotionTable.cs`'s ported `RemoveRedundantLinks`, see section 1): action-class
|
|
||||||
one-shots (attacks, casts) are **never truncated** by this mechanism and
|
|
||||||
always run their tick-countdown to natural completion. Separately, retail
|
|
||||||
`CPhysicsObj::interrupt_current_movement` (`0x005101f0`) is called
|
|
||||||
unconditionally from `jump()` and cancels an in-flight `MoveToManager`
|
|
||||||
transition — a wholly different mechanism from the action queue, not a
|
|
||||||
"cancel this attack" primitive. acdream's `MotionTableManager.RemoveRedundantLinks`
|
|
||||||
(`src/AcDream.Core/Physics/Motion/MotionTableManager.cs:191-248`) is a
|
|
||||||
byte-for-byte match including the identical `0xb0000000`/`0x70000000` block
|
|
||||||
masks and the same fallthrough, and the `InterruptCurrentMovement` seam
|
|
||||||
(`MotionInterpreter.cs:658`) is fully wired in PRODUCTION — not a stub — to
|
|
||||||
real `MoveToManager.CancelMoveTo(WeenieError.ActionCancelled)` in both
|
|
||||||
`src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs:152-154`
|
|
||||||
(remote) and `src/AcDream.App/Input/PlayerModeController.cs:354-362` (local
|
|
||||||
player), plus 4 call sites in `StickyManager.cs`. **Net: attack/cast
|
|
||||||
animations are uninterruptible by movement/jump input in BOTH clients —
|
|
||||||
movement just queues behind them; jump only ever cancels an in-flight
|
|
||||||
move-to, never the action queue.** The one thing this audit still did not
|
|
||||||
verify: whether higher-level combat/magic-casting code (entirely outside
|
|
||||||
`MotionInterpreter`/`MotionTableManager`, not read for this audit) layers
|
|
||||||
its own ADDITIONAL "can't move while casting" rule on top of this queue
|
|
||||||
mechanism — that would live in the combat/magic subsystem and needs a
|
|
||||||
separate targeted read, not a live capture.
|
|
||||||
|
|
||||||
**Additional residual surfaced by the flow-tracing pass, item 1 (stance
|
|
||||||
change):** the mechanism (`GetObjectSequence` Branch 1 + `GetLink`) is
|
|
||||||
confirmed parity, but whether acdream's higher-level default-combat-mode
|
|
||||||
selection (`CombatInputPlanner.GetDefaultCombatModeDecision`, not read in
|
|
||||||
this audit) picks the exact same weapon-style-to-CombatMode mapping as
|
|
||||||
retail's `ClientCombatSystem::GetDefaultCombatMode` (`0x0056B310`) in every
|
|
||||||
edge case was NOT traced — flagged as a small untraced item, not a
|
|
||||||
confirmed divergence.
|
|
||||||
|
|
||||||
**Ranked feel-impact of these 7, most to least:** all 7 came back parity —
|
|
||||||
an unusual, striking result for a from-scratch port of this scope. Ranking
|
|
||||||
by residual RISK rather than impact (i.e., where a future capture is most
|
|
||||||
likely to still surface a surprise, since several parity claims rest partly
|
|
||||||
on DAT-content assumptions rather than pure code): (1) item 7's untraced
|
|
||||||
combat/casting-layer interrupt rule and item 1's untraced default-combat-mode
|
|
||||||
mapping are the two loose threads worth a follow-up read (not a live
|
|
||||||
capture); (2) items 3/4/6 (turn cycles, walk-run link, general link
|
|
||||||
traversal) are the most solid — confirmed via both the acdream code AND an
|
|
||||||
independent cross-check against ACE's own C# `MotionTable.cs` port, which
|
|
||||||
shows the same double-hop structure; (3) items 2/5 (landing, backward/strafe)
|
|
||||||
are effectively closed — in both cases the "gap" the flow's framing
|
|
||||||
hypothesized doesn't exist in retail itself (one universal landing
|
|
||||||
transition; forward/right cycles reverse-played rather than distinct
|
|
||||||
backward/left clips), corroborated for item 5 by holtburger's wire-level
|
|
||||||
`MovementCommand` enum showing `WalkBackwards`/`TurnLeft`/`SidestepLeft` as
|
|
||||||
distinct wire ids (confirming the reversal is a client animation-layer
|
|
||||||
transform, not a wire-format absence). Net: of the 7 flows the task asked to
|
|
||||||
trace, all are parity; the two follow-up threads (combat-layer cast
|
|
||||||
interrupt, default-combat-mode mapping) are outside the `MotionInterpreter`/
|
|
||||||
`CMotionTable`/`CSequence` file set this audit focused on and are noted for
|
|
||||||
a future combat/magic-scoped audit, not scheduled as animation fixes.
|
|
||||||
|
|
||||||
## 3. TS-50 / TS-51 current scoping (verified against current code, 2026-07-30)
|
|
||||||
|
|
||||||
Both rows are precisely as described in the register — re-reading the actual
|
|
||||||
code confirms rather than narrows either row. No promotion to a fix is
|
|
||||||
recommended; both remain the correct classification (deliberate ordering
|
|
||||||
adaptation with a bounded, named residual), not silent regressions.
|
|
||||||
|
|
||||||
**TS-50 — which hook types still deliver late.** Read
|
|
||||||
`src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs:37-86` (`Capture`):
|
|
||||||
for every hook produced by a sequence advance, the queue tests
|
|
||||||
`hooks[i] is AnimationDoneHook` (line 75) and, ONLY for that one hook type,
|
|
||||||
synchronously calls `sequencer.Manager.AnimationDone(success: true)` at
|
|
||||||
capture time — i.e. inside the same call that advanced the sequence, matching
|
|
||||||
retail `CPhysicsObj::process_hooks @ 0x00511550` timing exactly (semantic
|
|
||||||
motion completion, Target/Movement/PartArray/Position manager tail all see it
|
|
||||||
in the same quantum). EVERY OTHER hook type reaching this queue (from the
|
|
||||||
`DatReaderWriter.Types.AnimationHook` hierarchy routed through
|
|
||||||
`AnimationHookRouter` to `AudioHookSink`, `ParticleHookSink`,
|
|
||||||
`TranslucencyHookSink`, `LightingHookSink` — i.e. sound playback, particle
|
|
||||||
creation including `RetailCreateBlockingParticleHook`, translucency-fade
|
|
||||||
starts, light attach, and `PhysicsScriptHook`/CallPES-adjacent triggers) is
|
|
||||||
unconditionally appended to `_entries` (line 82-85) and only fires later, in
|
|
||||||
`Drain()` (lines 88-125), which is called exactly once per render/update
|
|
||||||
frame from `LiveEffectFrameController.Tick` at
|
|
||||||
`src/AcDream.App/Update/LiveObjectFrameController.cs:107-126` — AFTER every
|
|
||||||
live entity's root/part/equipped-child pose has been published for that
|
|
||||||
frame (the comment at `LiveObjectFrameController.cs:109-113` names this
|
|
||||||
explicitly: "acdream currently keeps non-AnimationDone hooks at this
|
|
||||||
deferred shared boundary under TS-50"). So the answer to "which hook types
|
|
||||||
still deliver late": **all of them except semantic AnimationDone** — sound,
|
|
||||||
particle, light, translucency, and CallPES/script-chain hooks can be up to
|
|
||||||
one render frame later than retail's per-object `process_hooks` moment.
|
|
||||||
Feel-visible risk is concentrated in **CallPES** (a hook that triggers a
|
|
||||||
PhysicsScript chain, e.g. spawning a follow-up effect keyed to a specific
|
|
||||||
animation frame) and blocking-particle creation tied to an attack's exact
|
|
||||||
swing frame — a one-frame-late particle spawn on a fast weapon swing is the
|
|
||||||
kind of thing a careful side-by-side viewer could notice, though nobody has
|
|
||||||
filed a symptom against it yet. Audio/light/translucency lateness is far
|
|
||||||
less likely to be perceptible at typical frame rates.
|
|
||||||
|
|
||||||
**TS-51 — per-render-frame vs per-quantum tails.** Confirmed at
|
|
||||||
`LiveObjectFrameController.cs:107-126`: `LiveEffectFrameController.Tick(float
|
|
||||||
deltaSeconds)` advances `_particles.Tick(deltaSeconds)` and
|
|
||||||
`_scripts.Tick(_scriptTime.CurrentScriptTime)` exactly once per call, and
|
|
||||||
this controller is driven once per render/update frame (not once per
|
|
||||||
admitted 30 Hz physics quantum per live object). Retail's
|
|
||||||
`CPhysicsObj::UpdateObjectInternal @ 0x005156B0` advances each ordinary
|
|
||||||
object's own ParticleManager then ScriptManager inside EVERY admitted
|
|
||||||
quantum for THAT object, and `animate_static_object @ 0x00513DF0` uses a
|
|
||||||
different order (Script → Particle → hooks) for the static-object workset.
|
|
||||||
acdream's shared tail is Particle → Script after static hook capture,
|
|
||||||
uniformly, once per render frame regardless of how many physics quanta a
|
|
||||||
given object admitted that frame. Practical effect: on a catch-up frame
|
|
||||||
(object advances several quanta at once, e.g. after a stall), the object's
|
|
||||||
root/pose advances through all of them but its particle/script tail only
|
|
||||||
advances once — an emitter that should have spawned N times in that
|
|
||||||
interval spawns once with N ticks' worth of `deltaSeconds`, and static
|
|
||||||
default-script/particle ordering runs in the opposite sequence from
|
|
||||||
`animate_static_object`. This is a real feel-visible risk specifically for
|
|
||||||
dense fast-tick emitters (rapid-fire spell effects, chain particle bursts)
|
|
||||||
but is architecturally deep to fix (needs incarnation-bound per-object
|
|
||||||
particle/script manager instances, which the register row itself names as
|
|
||||||
the retirement condition) — not a quick promotion candidate.
|
|
||||||
|
|
||||||
**Verdict:** neither row's scope has changed since the register was last
|
|
||||||
written; both remain accurately described. Of the two, TS-50's CallPES
|
|
||||||
lateness is the more plausible candidate for a future promotion (narrower
|
|
||||||
blast radius — "make CallPES and blocking-particle hooks fire at capture
|
|
||||||
time like AnimationDone, keep the rest deferred" is a bounded change),
|
|
||||||
whereas TS-51 needs the larger incarnation-bound-manager refactor the row
|
|
||||||
already flags.
|
|
||||||
|
|
||||||
## 4. Issue #64 (local pickup animation) reassessment
|
|
||||||
|
|
||||||
**Original hypothesis (filed 2026-05-14, pre-R3/R4/R6):** `OnLiveMotionUpdated`
|
|
||||||
filters local-player self-echoes wholesale, so ACE's server-authored
|
|
||||||
`Motion(MotionCommand.Pickup)` broadcast (via
|
|
||||||
`Player_Inventory.AddPickupChainToMoveToChain` →
|
|
||||||
`EnqueueBroadcastMotion(motion)`) never reaches the local player's animation
|
|
||||||
path. That exact function (`OnLiveMotionUpdated`) no longer exists in the
|
|
||||||
current tree (`git grep` for it returns nothing) — the inbound motion path
|
|
||||||
has been rewritten at least twice since (R4-V5's local/remote unification,
|
|
||||||
then the J-slice Runtime extraction), so the original hypothesis needs to be
|
|
||||||
re-evaluated against the CURRENT architecture, not assumed stale or assumed
|
|
||||||
still-broken.
|
|
||||||
|
|
||||||
**Current architecture, traced end to end:**
|
|
||||||
|
|
||||||
1. `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:216-241`
|
|
||||||
(`OnMotion`) — the first gate is a TIMESTAMP staleness check
|
|
||||||
(`_authorityGate.TryAcceptMotion`), unrelated to self-echo.
|
|
||||||
2. Lines 224 and 243-259 — the REAL self-echo gate, `R4-V5 (pin P1)`: `bool
|
|
||||||
retainPayload = update.Guid != _playerServerGuid || !update.IsAutonomous;`
|
|
||||||
This drops an entire UpdateMotion packet ONLY when it targets the local
|
|
||||||
player's guid AND the packet's wire-level `IsAutonomous` byte is set — the
|
|
||||||
comment cites retail `CPhysics::SetObjectMovement`'s autonomous gate
|
|
||||||
(`0x00509690 @0050972e`, raw 271370-271431) and explains WHY: ACE reflects
|
|
||||||
the client's own outbound `MoveToState` back to the sender with
|
|
||||||
`IsAutonomous=1` hardcoded (`MovementData.cs:162`,
|
|
||||||
`Player_Networking.cs:365` in the ACE reference), so this gate exists
|
|
||||||
specifically to drop THAT reflection, not every inbound packet addressed
|
|
||||||
to the player.
|
|
||||||
3. If the packet survives, the local-player branch at lines 450-509 routes
|
|
||||||
through the SAME `RemoteInboundMotionDispatcher.Apply` used for remotes
|
|
||||||
(`src/AcDream.App/Physics/RemoteInboundMotionDispatcher.cs`), which for a
|
|
||||||
`MovementType == 0` packet calls `motion.MoveToInterpretedState(interpreted,
|
|
||||||
animationSink)` (`RemoteInboundMotionDispatcher.cs:108-112`).
|
|
||||||
4. `MotionInterpreter.MoveToInterpretedState`
|
|
||||||
(`src/AcDream.Core/Physics/MotionInterpreter.cs:2697-2743`) replays each
|
|
||||||
entry in `ims.Actions` (the Commands[] one-shot list, populated by
|
|
||||||
`InboundInterpretedMotionFactory.Create` from the wire's `Commands`
|
|
||||||
field — `src/AcDream.App/Physics/InboundInterpretedMotionFactory.cs:46-63`)
|
|
||||||
through `DispatchInterpretedMotion`, with exactly ONE local-player-specific
|
|
||||||
filter at line 2735: `if (IsLocalPlayer && a.Autonomous) continue;` — this
|
|
||||||
is scoped to the PER-ACTION autonomous bit inside the Commands[] entry
|
|
||||||
(`MotionItem.PackedSequence & 0x8000`), not a blanket "is this the local
|
|
||||||
player" drop.
|
|
||||||
|
|
||||||
**Current best hypothesis:** ACE's Pickup broadcast is server-initiated (built
|
|
||||||
via `EnqueueBroadcastMotion`, not a reflected client `MoveToState`), so both
|
|
||||||
gates that could drop it — the packet-level `IsAutonomous` check (step 2) and
|
|
||||||
the per-action `Autonomous` bit check (step 4) — should read `false` for it,
|
|
||||||
same as for any other server-authored one-shot action a remote observer would
|
|
||||||
see. If that reading of ACE's flag values is correct, **the R4-V5 local/remote
|
|
||||||
unification (which post-dates #64's filing by roughly two months) likely
|
|
||||||
fixed this issue as an architectural side effect**, without anyone
|
|
||||||
specifically targeting #64. This is a hypothesis, not a confirmed fix — this
|
|
||||||
audit is report-only and did not launch the client or trigger a live pickup.
|
|
||||||
|
|
||||||
**Recommended next step (not performed here):** re-test #64 live with
|
|
||||||
`ACDREAM_DUMP_MOTION=1` set, trigger a close-range pickup as `+Acdream`, and
|
|
||||||
check the log for a `UM guid=<player> ... cmd=...` line whose resolved
|
|
||||||
command carries the Pickup action bit, followed by the sequencer actually
|
|
||||||
playing the one-shot cycle. If it still fails, the next diagnostic is to
|
|
||||||
confirm whether ACE's Pickup broadcast really sets `Autonomous=false` at
|
|
||||||
both the packet and per-action level (a WireMCP capture on the loopback
|
|
||||||
`UpdateMotion (0xF74D)` packet during a pickup would settle this instead of
|
|
||||||
reading ACE source), since a wrong assumption there is the one way this
|
|
||||||
hypothesis could be wrong.
|
|
||||||
|
|
||||||
## 5. Emote/action surface (AD-57) gap sizing
|
|
||||||
|
|
||||||
**What retail would play:** an animated social emote (retail's action-class
|
|
||||||
MotionCommand — the `0x10000000` bit family, e.g. a wave/point/bow/salute
|
|
||||||
cycle, as opposed to `/e <text>` roleplay chat text) is queued through the
|
|
||||||
SAME `CMotionInterp` action-list machinery already ported: `AddAction` onto
|
|
||||||
`RawMotionState`/`InterpretedMotionState`, packed onto the wire by
|
|
||||||
`RawMotionState::Pack` (retail `0x0051ed10`) as `num_actions` +
|
|
||||||
per-action pairs, broadcast to observers as a Commands[] entry on
|
|
||||||
`UpdateMotion`, and resolved into a `CMotionTable` action-class cycle via the
|
|
||||||
same `CMotionTable::GetObjectSequence` path used for locomotion cycles.
|
|
||||||
|
|
||||||
**What acdream has, precisely:** per section 4's trace, the RECEIVING half of
|
|
||||||
this pipeline is fully wired and (per the current-best-hypothesis above)
|
|
||||||
likely already plays a server-broadcast one-shot action correctly for both
|
|
||||||
local and remote observers — `InboundInterpretedMotionFactory` parses
|
|
||||||
Commands[] into `InboundMotionAction`s, `MotionInterpreter.MoveToInterpretedState`
|
|
||||||
replays them through `DispatchInterpretedMotion` into the same
|
|
||||||
`CMotionTable`/`CSequence` cycle-selection path as any other motion. The
|
|
||||||
SENDING half — the local player's own input constructing and enqueueing an
|
|
||||||
autonomous action, e.g. from a `/wave`-style command — has no production call
|
|
||||||
site: `grep -r "\.AddAction(" src/AcDream.App` returns nothing outside test
|
|
||||||
code (`RawMotionState.AddAction`/`InterpretedMotionState.AddAction` are
|
|
||||||
exercised only by unit tests, confirmed during this audit). The chat command
|
|
||||||
catalog (`src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs:144-150,232-234`)
|
|
||||||
has `/emote` and `/emotes`, but these are documented as the roleplay TEXT
|
|
||||||
emote (`/e <text>` — chat-log output), not an animated action — there is no
|
|
||||||
slash command or UI affordance in the catalog for a genuine visual emote.
|
|
||||||
|
|
||||||
**Gap size:** this is a clean, well-isolated feature gap, not a divergence
|
|
||||||
of any shipped behavior — the register row (AD-57, re-argued 2026-07-30) is
|
|
||||||
correct that "every currently-shipped movement packet matches retail byte-shape;
|
|
||||||
the gap only manifests when emote-class autonomous actions are implemented."
|
|
||||||
The work to close it is bounded and almost entirely additive: (a) a
|
|
||||||
retail-sourced list of which MotionCommand action IDs are genuine social
|
|
||||||
emotes and what UI/command surface retail exposes them through (character
|
|
||||||
menu right-click? a `/motion` or numbered emote command? — this needs a
|
|
||||||
named-retail grep, not guessed), (b) a client input path that calls
|
|
||||||
`MotionInterpreter.DoMotion`/`AddAction` with the right action ID and
|
|
||||||
`Autonomous=true`, and (c) confirming the existing outbound packer already
|
|
||||||
emits it correctly (it should, since `RawMotionStatePacker` already handles
|
|
||||||
the `Actions` list per AD-57's own text). No architecture changes are
|
|
||||||
required — this is squarely a "wire up an existing, tested machine" gap, sized
|
|
||||||
small-to-medium (one research pass to find the retail command surface, one
|
|
||||||
implementation pass to wire input → `AddAction`).
|
|
||||||
|
|
||||||
## 6. Ranked gap catalog + recommended fix order
|
|
||||||
|
|
||||||
**Headline result:** this audit set out to find where acdream's animation
|
|
||||||
system diverges from retail and found the system in unusually good shape.
|
|
||||||
Two independent research passes (a symbol-by-symbol method-coverage sweep
|
|
||||||
and a 7-flow feel-visible trace) plus the lead auditor's own full read of
|
|
||||||
the four core files converged on the same conclusion: `MotionInterpreter`
|
|
||||||
(`CMotionInterp`), `CSequence`, `CMotionTable`, and `MotionTableManager` are
|
|
||||||
faithful, extensively retail-cited ports, and all 7 traced feel-visible
|
|
||||||
flows came back parity — three of them (landing severity, backward/strafe
|
|
||||||
cycles, in-place-turn-cycle-vs-omega) because the richer retail behavior the
|
|
||||||
flow's framing assumed doesn't actually exist in retail either. The gap
|
|
||||||
catalog below is therefore short and each entry is genuinely small.
|
|
||||||
|
|
||||||
**Ranked by feel-impact, most to least:**
|
|
||||||
|
|
||||||
1. **AD-57 — animated emote authoring gap (feel-visible, bounded scope).**
|
|
||||||
The RECEIVING half of retail's action-class one-shot animation system
|
|
||||||
(server-broadcast one-shots like Pickup, and presumably other players'
|
|
||||||
emotes) is fully wired and plays correctly through the same
|
|
||||||
`GetObjectSequence` Branch 3 machinery as any other motion (see section
|
|
||||||
4/5). The SENDING half — the local player triggering their OWN animated
|
|
||||||
emote (retail's `/wave`-equivalent) — has no production call site;
|
|
||||||
`RawMotionState.AddAction`/`InterpretedMotionState.AddAction` are
|
|
||||||
exercised only by unit tests. This is the most user-visible gap in the
|
|
||||||
catalog (a whole category of retail behavior — animated social
|
|
||||||
gestures — is simply absent from the client), but it is squarely a
|
|
||||||
"wire up an existing, tested machine" gap: no architecture change, no
|
|
||||||
new port, just (a) a named-retail grep for which MotionCommand action
|
|
||||||
IDs are genuine emotes and what UI surface retail exposes them through,
|
|
||||||
(b) an input path that calls `DoMotion`/`AddAction` with the right
|
|
||||||
action ID and `Autonomous=true`, (c) confirming the existing
|
|
||||||
`RawMotionStatePacker` emits it correctly (should already, per AD-57).
|
|
||||||
|
|
||||||
2. **TS-50 residual — CallPES and blocking-particle hooks up to one render
|
|
||||||
frame late (already known, narrow promotion candidate).** Verified
|
|
||||||
against current code (section 3): only the semantic `AnimationDoneHook`
|
|
||||||
fires at capture time; every other hook type (CallPES/script-chain
|
|
||||||
triggers, particle creation including blocking particles, sound, light,
|
|
||||||
translucency-fade starts) is deferred to `AnimationHookFrameQueue.Drain()`,
|
|
||||||
called once per render frame after all entities' poses publish. Most
|
|
||||||
feel-visible on a fast weapon swing where a blocking-particle effect is
|
|
||||||
keyed to an exact frame. Bounded fix: make CallPES and blocking-particle
|
|
||||||
hooks fire at capture time like `AnimationDoneHook`, keep the rest
|
|
||||||
deferred — narrower than the full TS-51 refactor below.
|
|
||||||
|
|
||||||
3. **Issue #64 — local pickup animation not rendering (likely already
|
|
||||||
fixed, zero-cost to verify).** Section 4's trace shows the R4-V5 local/
|
|
||||||
remote unification (which post-dates #64's filing) architecturally
|
|
||||||
closed the exact mechanism the original hypothesis blamed: the
|
|
||||||
packet-level and per-action autonomous-echo gates are now scoped
|
|
||||||
precisely enough that a server-authored one-shot (non-autonomous, by
|
|
||||||
construction) should reach the local player's `DispatchInterpretedMotion`
|
|
||||||
exactly like it does for remotes. This needs a 2-minute live re-test
|
|
||||||
(`ACDREAM_DUMP_MOTION=1`, trigger a close-range pickup), not an
|
|
||||||
engineering investment — likely already closed as a side effect of
|
|
||||||
unrelated work and just needs its ISSUES.md status updated.
|
|
||||||
|
|
||||||
4. **`CMotionInterp::get_adjusted_max_speed` unported (narrow, needs a
|
|
||||||
cdb read before it's even confirmed live).** The one genuine
|
|
||||||
unexplained gap from the method-coverage sweep (section 1): retail's
|
|
||||||
dead-reckoning catch-up path chooses between `get_max_speed` and this
|
|
||||||
sibling via a static toggle (`InterpolationManager::fUseAdjustedSpeed_`)
|
|
||||||
whose default this audit could not resolve from static analysis alone.
|
|
||||||
acdream always uses `GetMaxSpeed()` for remote catch-up. If the toggle
|
|
||||||
defaults to the adjusted variant in retail, acdream's remote
|
|
||||||
dead-reckoning catch-up speed could be systematically using the wrong of
|
|
||||||
two very similar formulas — narrow blast radius (one clamp value in one
|
|
||||||
catch-up path), plausible feel effect (slightly different snap-back
|
|
||||||
speed on remotes catching up after a network stall). Per the project's
|
|
||||||
own retail-debugger toolchain, this is a cdb-read-the-static-value
|
|
||||||
question, not a guess-and-ship one.
|
|
||||||
|
|
||||||
5. **TS-51 residual — particle/script tails once per render frame instead
|
|
||||||
of once per admitted physics quantum (already known, larger refactor).**
|
|
||||||
Verified against current code (section 3): on a catch-up frame where an
|
|
||||||
object advances several 30 Hz quanta at once, its particle/script tail
|
|
||||||
only advances once with the accumulated `deltaSeconds`, and static
|
|
||||||
default-script/particle ordering runs Particle→Script instead of
|
|
||||||
retail's Script→Particle→hooks. Most feel-visible for dense fast-tick
|
|
||||||
emitters (rapid spell-effect chains). The register row itself names the
|
|
||||||
retirement condition (incarnation-bound per-object particle/script
|
|
||||||
manager instances) — this is real architectural work, not a quick
|
|
||||||
promotion, and should stay queued behind the current M4 feature-work
|
|
||||||
order rather than jumping the line for this audit.
|
|
||||||
|
|
||||||
6. **Two untraced items outside this audit's file scope (flagged, not
|
|
||||||
confirmed divergences).** The flow-tracing pass surfaced two loose
|
|
||||||
threads it didn't have scope to chase: (a) whether combat/magic-casting
|
|
||||||
code (entirely outside `MotionInterpreter`/`MotionTableManager`) layers
|
|
||||||
its own additional "can't move while casting" rule on top of the
|
|
||||||
confirmed-parity action-queue mechanism; (b) whether
|
|
||||||
`CombatInputPlanner.GetDefaultCombatModeDecision` picks the same
|
|
||||||
weapon-style-to-CombatMode mapping as retail's
|
|
||||||
`ClientCombatSystem::GetDefaultCombatMode` (`0x0056B310`) in every edge
|
|
||||||
case. Both are plausible-but-unconfirmed and belong to a combat/magic-
|
|
||||||
scoped audit, not this animation-scoped one — recommended as a future
|
|
||||||
audit topic, not a fix.
|
|
||||||
|
|
||||||
**Recommended fix order** (cheapest/highest-confidence first): (1) re-test
|
|
||||||
#64 live and close the issue if confirmed — essentially free; (2) research
|
|
||||||
+ wire the emote-sending path (AD-57) — bounded, additive, no architecture
|
|
||||||
change, the single most user-visible improvement available; (3) narrow
|
|
||||||
TS-50's promotion to cover CallPES + blocking-particle hooks specifically;
|
|
||||||
(4) cdb-verify `fUseAdjustedSpeed_`'s retail default before deciding whether
|
|
||||||
`get_adjusted_max_speed` needs porting at all; (5) queue the full TS-51
|
|
||||||
incarnation-bound-manager refactor behind current M4 feature work, since it
|
|
||||||
is real architectural investment rather than a bounded fix; (6) file a
|
|
||||||
follow-up combat/magic-scoped audit for the two untraced items rather than
|
|
||||||
guessing at their status here.
|
|
||||||
|
|
||||||
This audit made no code changes and files no fixes directly — items 1-4
|
|
||||||
above are small enough that the user may want to fold them into the next
|
|
||||||
convenient M4 work session; item 5 should go through the normal roadmap
|
|
||||||
process (a new phase/slice, not a drive-by fix) given its architectural
|
|
||||||
size; item 6 needs its own investigation before any fix is proposed.
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
# #167 ConstraintManager leash — constants recovered + arming flow (Campaign P P5)
|
|
||||||
|
|
||||||
**2026-07-30.** Both #167 blockers are now research-solved; only the port
|
|
||||||
remains. No cdb session was needed: the two "unknown x87 constants" were
|
|
||||||
recovered by decoding the raw machine code of the matching binary
|
|
||||||
(`C:\Users\erikn\Downloads\acclient.exe`, v11.4186, PDB-paired — verified
|
|
||||||
GUID match per the retail debugger toolchain doc).
|
|
||||||
|
|
||||||
## 1. The getters, byte-decoded (FACT)
|
|
||||||
|
|
||||||
`CPhysicsObj::GetStartConstraintDistance @ 0x0050ebc0` and
|
|
||||||
`GetMaxConstraintDistance @ 0x0050ec10` are FPU-return getters whose
|
|
||||||
`fld` operands Binary Ninja elided (the pseudo-C shows a bare
|
|
||||||
`this->m_position;`). Raw bytes (file offset 0x10ebc0/0x10ec10):
|
|
||||||
|
|
||||||
```
|
|
||||||
3b 0d 58 3d 84 00 cmp ecx, [0x00843d58] ; this == player_object?
|
|
||||||
75 1d jnz non_player
|
|
||||||
8b 41 4c mov eax, [ecx+0x4c] ; m_position.objcell_id
|
|
||||||
25 ff ff 00 00 and eax, 0xFFFF
|
|
||||||
3d 00 01 00 00 cmp eax, 0x100
|
|
||||||
73 07 jae indoor ; low16 >= 0x100 = EnvCell
|
|
||||||
d9 05 <rdata> fld dword [outdoor_const]
|
|
||||||
c3 ret
|
|
||||||
indoor: d9 05 <..> fld dword [indoor_const]
|
|
||||||
c3 ret
|
|
||||||
non_player: ; identical cell test, second constant pair
|
|
||||||
```
|
|
||||||
|
|
||||||
Constant values read from `.rdata`:
|
|
||||||
|
|
||||||
| | player outdoor | player indoor | remote outdoor | remote indoor |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Start (0x007c6abc..c8) | **10.0** | **5.0** | 10.0 | 5.0 |
|
|
||||||
| Max (0x007c6acc..d8) | **50.0** | **20.0** | 50.0 | 20.0 |
|
|
||||||
|
|
||||||
Two consequences (FACT):
|
|
||||||
|
|
||||||
1. **The player-vs-remote branch is vestigial** — both sides load
|
|
||||||
identical values. Effective semantics: `start = outdoor 10 m /
|
|
||||||
indoor 5 m`, `max = outdoor 50 m / indoor 20 m` (indoor = cell low16
|
|
||||||
≥ 0x100).
|
|
||||||
2. **ACE's `GetStartConstraintDistance` is INVERTED**
|
|
||||||
(`ACE PhysicsObj.cs:620`: outdoor 5 / indoor 10). ACE's max mapping
|
|
||||||
(outdoor 50 / indoor 20) matches the binary. Do NOT copy ACE's start
|
|
||||||
mapping. (feedback_acme_oracle / binary-wins rule.)
|
|
||||||
|
|
||||||
## 2. The arming flow — `SmartBox::HandleReceivedPosition @ 0x00453fd0` (FACT)
|
|
||||||
|
|
||||||
Pseudo-C lines ~92940-93060. After the update-time staleness gates and
|
|
||||||
`unset_parent`/`SetPlacementFrame` handling:
|
|
||||||
|
|
||||||
- **Remote object** (`arg2 != this->player`): call
|
|
||||||
`MoveOrTeleport(obj, &recvPos, ts, arg5, arg6)`; **only if it returns
|
|
||||||
nonzero** (the position was NOT hard-teleport-applied), arm the leash
|
|
||||||
**anchored to the object's own current position**:
|
|
||||||
`ConstrainTo(obj, &obj->m_position, start, max)` (0x00454254-72).
|
|
||||||
- **Player, teleport-newer** (`newer_event(TELEPORT_TS, ts)`):
|
|
||||||
`SmartBox::TeleportPlayer(&recvPos)`, then
|
|
||||||
`ConstrainTo(player, &recvPos, start, max)` — anchored to the
|
|
||||||
**received** position — then `set_velocity(player, {0,0,0}, 1)`
|
|
||||||
(0x0045415f-c0).
|
|
||||||
- **Player, normal**: `ConstrainTo(player, &recvPos, start, max)`
|
|
||||||
anchored to the received position; then, if
|
|
||||||
`cmdinterp->UsePositionFromServer() && arg5`,
|
|
||||||
`InterpolateTo(&recvPos, -GetAutonomyLevel())` (0x004541c9-422c).
|
|
||||||
|
|
||||||
The taper/enforcement side (`ConstraintManager::UseTime` feeding
|
|
||||||
`adjust_offset`, `IsFullyConstrained = ConstraintDistanceMax * 0.9 <
|
|
||||||
offset`) is already ported in
|
|
||||||
`src/AcDream.Core/Physics/Motion/ConstraintManager.cs` (R5-V1,
|
|
||||||
`docs/research/2026-07-03-r5-managers/`); it has simply never been armed.
|
|
||||||
|
|
||||||
## 3. Port shape for P5 (INFERENCE — implementation guidance)
|
|
||||||
|
|
||||||
1. Add the four-constant getters (outdoor/indoor by full cell id low16)
|
|
||||||
at the body/host layer; keep the vestigial player/remote split OUT
|
|
||||||
(note it in a code comment with this doc as the cite).
|
|
||||||
2. Arm at acdream's inbound-position equivalents of the three branches:
|
|
||||||
the remote UpdatePosition acceptance tail (post-`MoveOrTeleport`
|
|
||||||
routing in the live-entity network update path) and the local
|
|
||||||
player's accepted-Position path (normal + teleport). Anchor per §2.
|
|
||||||
3. `PhysicsBody.IsFullyConstrained` (register TS-35 stub) becomes a read
|
|
||||||
through `PositionManager`/`ConstraintManager`, so
|
|
||||||
`jump_is_allowed`'s ported gate fires (WeenieError 0x47) while
|
|
||||||
rubber-banding. TS-35 and #167 retire together, same commit.
|
|
||||||
4. Conformance tests: constant table incl. the ACE-inversion pin
|
|
||||||
(outdoor start MUST be 10, not 5); leash-armed jump refusal;
|
|
||||||
remote-vs-player anchor difference; teleport-branch velocity zero.
|
|
||||||
|
|
||||||
## Open questions
|
|
||||||
|
|
||||||
None for the constants/flow. Remaining implementation risk is only
|
|
||||||
where acdream's position-acceptance seams sit today (J6.3 moved
|
|
||||||
teleport correlation into Runtime — the implementer must find the
|
|
||||||
current owner rather than trusting older file cites).
|
|
||||||
|
|
||||||
## As-ported (Campaign P Slice P5, 2026-07-30)
|
|
||||||
|
|
||||||
The implementation risk flagged above resolved to these CURRENT seam owners
|
|
||||||
(post-J-slices) — recorded here so the next reader doesn't have to re-derive
|
|
||||||
them:
|
|
||||||
|
|
||||||
- **Constants** — `src/AcDream.Core/Physics/Motion/ConstraintDistance.cs`.
|
|
||||||
Keyed purely on the object's own full cell id's low 16 bits (`>= 0x0100` =
|
|
||||||
indoor); the vestigial player/remote branch from §1 is deliberately not
|
|
||||||
represented as an API parameter.
|
|
||||||
- **Remote arm** — `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs`,
|
|
||||||
the inbound `UpdatePosition` handler's remote branch (`update.Guid !=
|
|
||||||
_playerServerGuid`), immediately after the `remotePlacementRequired`
|
|
||||||
hard-teleport block returns (that block already covers retail's
|
|
||||||
`MoveOrTeleport` Branch A / hard-place case; everything reached past it is
|
|
||||||
"did not hard-place"). One call site covers BOTH player-remote and NPC
|
|
||||||
remotes — retail's `SmartBox::HandleReceivedPosition` doesn't distinguish
|
|
||||||
them either, only `GetStart/MaxConstraintDistance`'s now-omitted vestigial
|
|
||||||
branch did. Anchored to the live `IPhysicsObjHost.Position` (which reads
|
|
||||||
`RemoteMotion.Body.Position` + the tracked cell id), matching retail's
|
|
||||||
"anchored to the object's own current position" — since the anchor and
|
|
||||||
`_host.Position` read are the same value at call time,
|
|
||||||
`ConstraintManager.ConstrainTo`'s initial offset is always 0 regardless of
|
|
||||||
whether the routing above just far-snapped or left a near-correction
|
|
||||||
queued.
|
|
||||||
- **Remote per-tick taper + `IsFullyConstrained` push** — already wired
|
|
||||||
pre-P5 for the taper (`RuntimeRemotePhysicsUpdater.Tick`/`TickHidden` call
|
|
||||||
`PositionManager.AdjustOffset` every tick via the pre-existing R5-V3
|
|
||||||
sticky/constraint chain); P5 added the `PhysicsBody.IsFullyConstrained =
|
|
||||||
host.PositionManager.IsFullyConstrained()` push at the same two call
|
|
||||||
sites, since `MotionInterpreter` only holds a `PhysicsBody` (no host
|
|
||||||
reference) and needs a live value to read.
|
|
||||||
- **Local player arm** — `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs`:
|
|
||||||
`SetPositionCore` (teleport: `UnConstrain` then re-`ConstrainTo` after the
|
|
||||||
existing `StopCompletelyAtPhysicsObjectBoundary` velocity zero — composed,
|
|
||||||
not duplicated) and `CommitPreparedPosition` (mirrors the same pair for
|
|
||||||
the deferred player-mode-entry commit path); `BlipPosition` (ForcePosition:
|
|
||||||
`ConstrainTo` only, no teardown — matches `SmartBox::BlipPlayer` surviving
|
|
||||||
motion/velocity/stick). Anchored to `_body.CellPosition` (the just-applied
|
|
||||||
received position).
|
|
||||||
- **Local player per-tick taper + push** — `PlayerMovementController.Update`
|
|
||||||
already called `PositionManager.AdjustOffset` every physics tick pre-P5;
|
|
||||||
P5 added the `_body.IsFullyConstrained = PositionManager?.IsFullyConstrained()
|
|
||||||
?? false` push immediately after, at the same chokepoint.
|
|
||||||
- **TS-35 retirement** — `PhysicsBody.IsFullyConstrained` stayed a plain
|
|
||||||
settable bool (not a computed property) so the ~40 pre-existing direct-set
|
|
||||||
unit tests keep working; the per-tick pumps above are its single writers
|
|
||||||
now, matching the project's per-entity single-owner-write pattern.
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
# The landing bounce family — retail bounce vs ground vs slide (investigation, report-only)
|
|
||||||
|
|
||||||
**Date:** 2026-07-30 · **Status:** IMPLEMENTED (same day — see §Implementation)
|
|
||||||
**Symptoms (user, live gate):** (1) downhill jumps glide instead of bouncing;
|
|
||||||
(2) flat-ground jumps at speed/height don't bounce; (3) uphill jumps get stuck
|
|
||||||
in weird animations, flapping and gliding. Speed (#266) and roof slide
|
|
||||||
(#265's freeze) are fixed and unaffected.
|
|
||||||
|
|
||||||
## The retail mechanism (decomp, read end-to-end this session)
|
|
||||||
|
|
||||||
Three functions compose the whole behavior:
|
|
||||||
|
|
||||||
### 1. The floor-touch dual record (plane handler, 0x0050d100-0x0050d30c)
|
|
||||||
|
|
||||||
Touching a floor plane records **two independent facts**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (step_down || !(state & CONTACT) || is_valid_walkable(plane))
|
|
||||||
set_contact_plane(plane) // grounding fact
|
|
||||||
if (!(state & CONTACT) && !step_down) {
|
|
||||||
set_collision_normal(plane.N); // collision fact
|
|
||||||
collided_with_environment = 1;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A landing (not already in contact, not a step-down probe) is BOTH a contact
|
|
||||||
AND an environment collision carrying the floor normal. Ordinary walking
|
|
||||||
(already in contact / step-down glue probes) records only the contact —
|
|
||||||
that is why walking never bounces.
|
|
||||||
**acdream's transition already ports this faithfully**
|
|
||||||
(`TransitionTypes.cs:3410-3415` — `!oi.Contact && !sp.StepDown` →
|
|
||||||
`SetCollisionNormal` + `CollidedWithEnvironment = true`).
|
|
||||||
|
|
||||||
### 2. SetPositionInternal (0x00515330, read fully — VELOCITY-SIGN-FREE)
|
|
||||||
|
|
||||||
```
|
|
||||||
contact = collision_info.contact_plane_valid (no velocity test)
|
|
||||||
on_walkable = contact && contact_plane.N.z >= floor_z (set_on_walkable → HitGround/LeaveGround)
|
|
||||||
handle_all_collisions(collision_info, prevContact, prevOnWalkable) ← velocity UNMODIFIED
|
|
||||||
```
|
|
||||||
|
|
||||||
There is **no `Velocity.Z <= 0` landing gate and no velocity zeroing**
|
|
||||||
anywhere in retail's commit. Contact is a per-frame fact from the
|
|
||||||
transition's contact plane; the bounce is the velocity reflect; they are
|
|
||||||
independent and coexist — you can be "landed" this frame AND carry
|
|
||||||
reflected +Z that lifts you off next frame. That IS the bounce chain.
|
|
||||||
(Our Core `PhysicsObjUpdate.CommitSetPositionTransition` is already a
|
|
||||||
faithful port of this function — used by teleport/remote placement, NOT by
|
|
||||||
the local player's per-tick path.)
|
|
||||||
|
|
||||||
### 3. handle_all_collisions (0x00514780) + elasticity
|
|
||||||
|
|
||||||
For fsf≤1, should-reflect (NOT(was-walkable AND still-walkable) or the
|
|
||||||
garbled state-flag override), valid collision normal, and `v·n < 0`:
|
|
||||||
|
|
||||||
```
|
|
||||||
v += -(v·n) · (elasticity + 1) · n // pc:282712
|
|
||||||
```
|
|
||||||
|
|
||||||
`DEFAULT_ELASTICITY = 0.05` (byte constant @0x007c6a7c; ctor writes at
|
|
||||||
0x005124d3/0x0051d537; `set_elasticity` clamps to [0, 0.1]). So every
|
|
||||||
landing reverses 5% of the impact's normal component and keeps the full
|
|
||||||
tangential component:
|
|
||||||
|
|
||||||
- **Flat ground at speed/height:** v=(6,0,−7) → v'=(6,0,+0.35) — forward
|
|
||||||
carry plus a visible pop at speed. Symptom (2).
|
|
||||||
- **Downhill:** reflect is off the SLOPE normal — each contact pops the body
|
|
||||||
off-slope while tangential speed persists → contact/airborne chain =
|
|
||||||
the characteristic downhill bounce. Symptom (1).
|
|
||||||
- **Uphill:** the reflect kills the into-slope component at impact, contact
|
|
||||||
stands, HitGround fires once, land animation plays. Symptom (3)'s clean
|
|
||||||
retail counterpart.
|
|
||||||
|
|
||||||
## Why acdream glides/flaps instead (the adaptation stack)
|
|
||||||
|
|
||||||
`PlayerMovementController.cs:2032-2079` (the per-tick commit) replaces
|
|
||||||
retail's SetPositionInternal with a hand-rolled block:
|
|
||||||
|
|
||||||
1. **AD-25 landing gate:** `if (resolveResult.IsOnGround && Velocity.Z <= 0)`
|
|
||||||
— needed because *our resolver reports IsOnGround even during an UPWARD
|
|
||||||
jump (it always step-downs)*. Retail has no such gate: an ascending mover
|
|
||||||
simply finds no contact plane (it moves away from it; the touch test
|
|
||||||
fails), so contact clears naturally.
|
|
||||||
2. **The bounce killer:** `if (Velocity.Z < 0) Velocity.Z = 0` on landing,
|
|
||||||
whose comment says its purpose plainly: *"makes handle_all_collisions'
|
|
||||||
landing reflect a no-op — dot(v,n)=0."* This retired the old
|
|
||||||
"micro-bounce death spiral" — but that spiral was caused by our OWN
|
|
||||||
gate (reflected +Z defeating the `Velocity.Z<=0` landing test), not by
|
|
||||||
the reflect being wrong. The workaround deleted retail's legitimate
|
|
||||||
bounce.
|
|
||||||
3. With the reflect suppressed, the new #265 residual-velocity fix correctly
|
|
||||||
preserves landing momentum — which now SLIDES via calc_friction instead
|
|
||||||
of bouncing. Hence "I glide but that's incorrect."
|
|
||||||
4. **Uphill flap:** during the up-leg our resolver glues to the slope
|
|
||||||
(IsOnGround true) while the gate refuses to ground (v.z > 0) →
|
|
||||||
Contact/OnWalkable and HitGround/LeaveGround edges cycle against the
|
|
||||||
animation state machine → "weird animations, flapping and gliding."
|
|
||||||
|
|
||||||
## Hypotheses (ranked)
|
|
||||||
|
|
||||||
1. **H1 (root, high confidence — every link read this session):** the
|
|
||||||
AD-25 landing gate + Velocity.Z hand-zero must be REPLACED by retail's
|
|
||||||
SetPositionInternal semantics, which requires first fixing the underlying
|
|
||||||
resolver divergence: **the transition must not produce a contact plane
|
|
||||||
for a mover ascending away from the ground** (retail's step-down/touch
|
|
||||||
conditions do this naturally; ours "always step-downs"). With that fixed,
|
|
||||||
route the per-tick commit through the already-ported
|
|
||||||
`CommitSetPositionTransition` and delete the hand-rolled block — reflect,
|
|
||||||
contact, HitGround/LeaveGround, and land animation then compose exactly
|
|
||||||
as retail.
|
|
||||||
- Falsify by: cdb trace on retail (bp SetPositionInternal +
|
|
||||||
handle_all_collisions, dump v before/after while jumping downhill) —
|
|
||||||
expect unmodified impact v entering, 5% normal reversal exiting.
|
|
||||||
2. **H2 (contributing detail):** the garbled `state & <mush>` override in
|
|
||||||
handle_all_collisions' gate (our port maps it to Sledding) and the
|
|
||||||
`0x20000` Inelastic mapping need byte-decode confirmation before the
|
|
||||||
rework — a wrong flag here changes when reflects fire while grounded.
|
|
||||||
3. **H3 (animation-side residual):** if flap persists after H1, the
|
|
||||||
MotionInterp land/fall transition (LandJump vs falling-hold) has its own
|
|
||||||
gate to audit — deferred until H1 is in.
|
|
||||||
|
|
||||||
## What we've ruled out
|
|
||||||
|
|
||||||
- The transition's landing dual-record being missing — ours is faithful
|
|
||||||
(TransitionTypes.cs:3410).
|
|
||||||
- HandleAllCollisions' reflect math/elasticity — ported correctly
|
|
||||||
(PhysicsObjUpdate.cs:198, elasticity 0.05 default present).
|
|
||||||
- The #265 residual-velocity fix being wrong — it exposed the missing
|
|
||||||
bounce; it didn't cause it.
|
|
||||||
|
|
||||||
## Recommended next step
|
|
||||||
|
|
||||||
Approve H1 for implementation: (a) byte-decode the two garbled flags (H2)
|
|
||||||
first; (b) find + port retail's exact ascent/step-down gating in the
|
|
||||||
transition (the one remaining unread mechanism); (c) cut the per-tick commit
|
|
||||||
over to `CommitSetPositionTransition`; (d) re-run the roof/downhill/flat/
|
|
||||||
uphill matrix live. Optional pre-implementation confirmation: the H1 cdb
|
|
||||||
trace against live retail.
|
|
||||||
|
|
||||||
## What this is NOT
|
|
||||||
|
|
||||||
Not a missing-elasticity port and not a missing collision-record — both
|
|
||||||
exist and are faithful; the bounce is suppressed by our own landing-commit
|
|
||||||
adaptation (AD-25 family), whose reason-for-being is the resolver's
|
|
||||||
ascent-glue divergence.
|
|
||||||
|
|
||||||
|
|
||||||
## Implementation (2026-07-30, user-approved)
|
|
||||||
|
|
||||||
All three retail mechanisms are now live; the AD-25 adaptation stack is
|
|
||||||
deleted:
|
|
||||||
|
|
||||||
1. **check_contact seeding** (`PhysicsEngine.ResolveWithTransition`): a body
|
|
||||||
in transient CONTACT seeds the transition's contact state ONLY while
|
|
||||||
`v · contactPlane.N <= ε` (0.0002 = PhysicsGlobals.EPSILON, retail
|
|
||||||
0x0050f5b0); a failing body seeds the last-known plane alone (retail
|
|
||||||
get_object_info's init_last_known_contact_plane branch). The plane
|
|
||||||
requirement is strict — Contact-without-plane is unrepresentable in
|
|
||||||
retail. Body-less callers keep the legacy isOnGround seed (test rigs).
|
|
||||||
2. **SetPositionInternal-shaped commit** (`PlayerMovementController`): the
|
|
||||||
`Velocity.Z <= 0` landing gate and the landing `Velocity.Z = 0` hand-zero
|
|
||||||
are DELETED. Contact commits purely from `resolveResult.InContact` /
|
|
||||||
`OnWalkable`, HitGround fires on the airborne→walkable edge, and
|
|
||||||
`HandleAllCollisions` runs with the UNMODIFIED impact velocity — the 5%
|
|
||||||
elasticity reflect is live. The whole commit is gated on
|
|
||||||
`resolveResult.Ok && candidateMoved` (retail runs SetPositionInternal
|
|
||||||
only when the transition succeeded AND the candidate moved — pc:283657;
|
|
||||||
AD-41's row updated accordingly). Zero-move frames leave contact state
|
|
||||||
untouched (this is what keeps a standing body stable: a zero-move
|
|
||||||
resolve cannot re-derive a plane because no sweep runs).
|
|
||||||
3. **Byte decodes** (this doc's H2): the handle_all_collisions gate override
|
|
||||||
is `state & 0x800000` = Sledding; the zero branch is `state & 0x20000` =
|
|
||||||
Inelastic; the reflect fires strictly on `dot < 0` (`test ah, 5; jp`).
|
|
||||||
Our port had all three correct already — no change.
|
|
||||||
|
|
||||||
Settle behavior: a real landing (|v| ≥ 0.25 m/s) bounces at 5% and the hop
|
|
||||||
chain decays geometrically; sub-0.25 m/s impacts are consumed by retail's
|
|
||||||
unconditional small-velocity zero (PhysicsBody.UpdatePhysicsInternal), so a
|
|
||||||
standing body never micro-bounces. calc_acceleration turns gravity off for
|
|
||||||
Contact+OnWalkable bodies, which is what makes rest bit-stable.
|
|
||||||
|
|
||||||
Test re-baselines (each documented in place): the landing-survival pin now
|
|
||||||
measures decay after the hop chain settles; `LiveCompare_Tick0/376` pin the
|
|
||||||
new IsOnGround=false on their zero-move ticks (the captured `true` was the
|
|
||||||
retired seed echo — tick 376's captured body even carries an 11.8 m/s
|
|
||||||
grounded velocity from the deleted get_state_velocity-overwrite era);
|
|
||||||
`RemoteDeOverlapMechanismTests.GroundedBody` now carries the plane a real
|
|
||||||
grounded body always has (the big-creature 1.80 m expectation was calibrated
|
|
||||||
against the unrepresentable flags-without-plane fixture; production settles
|
|
||||||
at 1.58 m, unchanged before/after). New pins:
|
|
||||||
`LandingBounceSeedingTests` (ascent no-seed, rest keeps-contact, strict
|
|
||||||
plane, slope 5% reversal + tangential preservation, Sledding override).
|
|
||||||
|
|
||||||
Verification: complete Release suite 10,031 passed / 5 skips / 0 failures.
|
|
||||||
Live gate (downhill bounce chain, flat-ground pop, uphill clean landing,
|
|
||||||
roof slide intact, walking intact) pends the user's next session.
|
|
||||||
|
|
@ -1,618 +0,0 @@
|
||||||
# Movement Parity Audit — Retail vs acdream (2026-07-30)
|
|
||||||
|
|
||||||
**Status: COMPLETE — report-only investigation, no code changes made.**
|
|
||||||
|
|
||||||
Scope: input → intent → wire → presentation for player + remote movement.
|
|
||||||
Explicitly OUT of scope (closed by Campaign P, physics/collision proper):
|
|
||||||
stat chain, friction, sphere lists, leash, PK flags. See
|
|
||||||
`docs/plans/2026-07-29-physics-parity-campaign.md`'s closeout. This audit
|
|
||||||
picks up the *movement wire/presentation* seam Campaign P did not touch.
|
|
||||||
|
|
||||||
Legend: **FACT** = confirmed against named-retail decomp byte/pseudo-C
|
|
||||||
(cited address + `acclient_2013_pseudo_c.txt` line) or cross-referenced
|
|
||||||
against a second independent client (holtburger / Chorizite). **INFERENCE**
|
|
||||||
= plausible reading where the decompiler dropped x87 detail (a known
|
|
||||||
Binary Ninja artifact class, see `claude-memory/feedback_bn_decomp_field_names.md`)
|
|
||||||
and could not be fully disambiguated in this pass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Outbound semantics table
|
|
||||||
|
|
||||||
Retail's outbound tree (from `claude-memory/project_retail_motion_outbound.md`,
|
|
||||||
re-verified this session):
|
|
||||||
|
|
||||||
```
|
|
||||||
WASD keypress → CommandInterpreter::SendMovementEvent (0x006B4680, per-frame)
|
|
||||||
→ MoveToStatePack → SendMoveToStateEvent → 0xF61C
|
|
||||||
at-rest heartbeat → CommandInterpreter::ShouldSendPositionEvent (0x006B45E0)
|
|
||||||
→ SendPositionEvent (0x006B4770) → AutonomousPositionPack → 0xF753
|
|
||||||
```
|
|
||||||
|
|
||||||
| Intent | Retail send decision | acdream send decision | Verdict |
|
|
||||||
|---|---|---|---|
|
|
||||||
| W (run) | `SendMovementEvent` fires on any command-list head edge; wire carries `WalkForward`, `HoldKey.Run`, raw `forward_speed` (pre-scale). ACE auto-upgrades to `RunForward` for observers. | `PlayerMovementController` line 2106-2225: `outForwardCmd=WalkForward`, `outForwardSpeed=1.0f` (raw), `IsRunning=input.Run`; `changed` fires on cmd/hold/speed edges. `RawMotionStatePacker` D1 default-diff omits unchanged fields. | **Parity** (D6.2b/D1 shipped, verified 2026-07-01) |
|
|
||||||
| W+Shift (walk) | Same tree, `HoldKey.None`, `forward_speed=1.0` (not run-scaled — ACE/observer scaling is a display-time concern, not sender concern). | Same — `axisHoldKey = movement.IsRunning ? Run : None` in `LocalPlayerOutboundController.BuildRawMotionState` (:234-244). | **Parity** |
|
|
||||||
| Backward (X) | `WalkBackward` tag, own independent forward-channel entry; `adjust_motion` applies a flat **-0.65×** speed multiplier for the walk-forward↔backward pair (`apply_run_to_command`/`adjust_motion` 0x00528010, `acclient_2013_pseudo_c.txt:305343-305400` — **FACT**, spot-read confirms the `0x45000006→WalkForward, speed*=-0.65` canonicalization). | `outForwardCmd=WalkBackward`, `outForwardSpeed=1.0f` (PlayerMovementController :2111-2115). The **-0.65× backward scale lives in `MotionInterpreter.cs:543-546`** ("Retail-exact value; do not round to 0.65f") and is applied on the *interpreted* (local-animation) side, not re-derived on the wire (wire stays raw 1.0, matching D6.2b's "ACE recomputes" model). | **Parity** — same separation-of-concerns retail uses (raw wire, scaled interpretation) |
|
|
||||||
| Strafe (Z/C) | `SideStepRight`/`SideStepLeft`; `adjust_motion` applies a flat **×1.248** (`(3.12/1.25)*0.5`) animation-rate scale, THEN `apply_run_to_command`'s SideStepRight branch (if Run) multiplies by `runRate` and clamps magnitude to **3.0** (`0x00527be0:305102-305122` — **FACT** for the 3.0 constant and the runRate scale; **INFERENCE** on the exact snap-vs-clamp branch polarity, x87 flag test unresolved by BN). | `MotionInterpreter.cs:558-564` cites the retail `±3.0` clamp and the 1.248 sidestep scale explicitly; `_activeInputSidestepCommand`/`SidestepUsesRunHold` in `PlayerMovementController.cs:2129-2133` carry the channel through to the wire. | **Parity** (ported; the one open item is the same x87-ambiguous branch retail's own disassembly leaves fuzzy — not an acdream gap) |
|
|
||||||
| Turn (A/D) keyboard | `adjust_motion` canonicalizes Left→Right (`speed *= -1`), then `apply_run_to_command`'s TurnRight branch multiplies by a flat **1.5×** when hold key is Run (`0x00527be0:305096-305100` — **FACT**, byte-confirmed this session). Turn is a channel fully independent of forward/sidestep. | `MotionInterpreter.cs:554` `RunTurnFactor = 1.5f`, applied inside the ported `apply_run_to_command` (:1355+). Turn channel (`_activeInputTurnCommand`/`_activeInputTurnSpeed`) is independent of forward/sidestep in `PlayerMovementController.cs:2139-2143`. | **Parity** |
|
|
||||||
| Autorun (Q) | See §4 below — separate section, real divergence found. | | **Divergent** |
|
|
||||||
| Mouse-look turn (MMB) | `CameraSet::ToggleMouseLook`/`Rotate` (0x00457490/0x00458310) drive ordinary `TurnLeft`/`TurnRight` `MovePlayer` calls, always `HoldKey.Run`; speed = 2×filtered horizontal delta, dead-zone 0.02, cap 1.5. `MoveToState` sent on start/stop and every 0.5 s while active. | `MouseTurnDeadZone=0.02f`, `MouseTurnSpeedScale=2.0f`, `MouseTurnMaximumSpeed=1.5f`, `MouseMovementEventInterval=0.5f` (`PlayerMovementController.cs:376-380`) — exact match. | **Parity** (previously verified 2026-07-15, re-confirmed this session) |
|
|
||||||
| Mouse-move-to (click-to-move) | Not part of the CommandInterpreter WASD tree; routes through `MoveToManager`/`MoveToPosition` (§3). | Same split in acdream (`MoveToManager.cs`, separate from `PlayerMovementController`'s per-frame channel). | **Parity** (architectural match) |
|
|
||||||
| Stop (S key / all keys released) | `CommandInterpreter::UseTime` gates `ShouldSendPositionEvent` first, then falls through; a full command-list-empty state issues `MovePlayer(Ready, ...)` idle re-sync via `ApplyCurrentMovement`. | `PlayerMovementController` idle path falls to `_motion.RawState.ForwardCommand` staying at `Ready` default (0x41000003), consistent with retail's ctor default. | **Parity** |
|
|
||||||
|
|
||||||
### TS-33 residual (exact current-code read)
|
|
||||||
|
|
||||||
Register row (`docs/architecture/retail-divergence-register.md:264`, re-read
|
|
||||||
this session): **"NARROWED 2026-07-15 — full AP tracker semantics are
|
|
||||||
ported... Residual: acdream's single update path snapshots the AP predicate,
|
|
||||||
emits a same-update MTS first when input changed, then AP. Retail proves
|
|
||||||
`UseTime` performs Should→AP, but MTS originates in separate input
|
|
||||||
callbacks; their relative same-tick callback/wire order is not yet
|
|
||||||
traced."** This is confirmed still accurate: `PlayerMovementController.cs`'s
|
|
||||||
per-frame method computes `MovementResult` (lines 2080-2226, the MTS side)
|
|
||||||
and `LocalPlayerOutboundController.SendPreNetworkActions`/
|
|
||||||
`SendPostNetworkPosition` (its own file, :50-144) split MTS-before-inbound
|
|
||||||
vs AP-after-inbound exactly as retail's `UseTime` (0x006B3BF0, decomp
|
|
||||||
699564-699583) does: `ShouldSendPositionEvent()→SendPositionEvent()` FIRST,
|
|
||||||
then (separately, from input callbacks, not shown in `UseTime` itself)
|
|
||||||
`SendMovementEvent`. TS-33's residual is real but narrow: it's an *ordering*
|
|
||||||
question (does retail's per-frame input callback that calls
|
|
||||||
`SendMovementEvent` run before or after that frame's `UseTime` AP check?),
|
|
||||||
not a values/cadence question. Unchanged this session — still needs a cdb
|
|
||||||
trace to close, not a code fix.
|
|
||||||
|
|
||||||
### AP-30 — STALE register row (found this session)
|
|
||||||
|
|
||||||
**FACT.** The register (`retail-divergence-register.md:153`) currently
|
|
||||||
reads: *"AutonomousPosition diff cadence compares with epsilons (1 mm pos,
|
|
||||||
1e-4 normal, 1 mm dist); retail's `Frame::is_equal` is an exact float
|
|
||||||
compare... `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:1110`."*
|
|
||||||
Both halves of this row are now wrong relative to current code:
|
|
||||||
|
|
||||||
1. **Line citation is stale.** Line 1110 of `PlayerMovementController.cs`
|
|
||||||
today is inside `AttachAnimationRootMotionSource`'s parameter list — unrelated
|
|
||||||
code. The actual epsilon logic lives at `PlayerMovementController.cs:2234-2263`
|
|
||||||
(`ApproxFrameEqual`/`ApproxPlaneEqual`).
|
|
||||||
2. **The epsilon claim is factually wrong about retail.** Read directly
|
|
||||||
from the named decomp: `Frame::is_equal` (`0x00424c30`, line 38461-38468)
|
|
||||||
calls `Vector3Math::AreEqual(origin, origin, 0.000199999995f)` and
|
|
||||||
`Frame::is_quaternion_equal` (0x00424c70, line 38472-38505), which
|
|
||||||
compares all four quaternion components against the **same
|
|
||||||
`0.000199999995f` (0.0002) epsilon** — not an exact bit compare.
|
|
||||||
Likewise `Plane::operator==` (`0x006b3dd0`, line 699713-699743) compares
|
|
||||||
`N.x`/`N.y`/`N.z`/`d` against the identical `0.000199999995f` epsilon.
|
|
||||||
`ApproxFrameEqual`/`ApproxPlaneEqual` in current acdream code (lines
|
|
||||||
2234-2263) use exactly `0.000199999995f` uniformly for both — i.e.
|
|
||||||
**acdream's current code already matches retail's real (epsilon, not
|
|
||||||
exact) comparison byte-for-byte**, and the code's own doc-comment says so
|
|
||||||
correctly ("Retail `Frame::is_equal` ... compares ... with a 0.0002-unit
|
|
||||||
epsilon"). The register row documents a bug that no longer exists.
|
|
||||||
|
|
||||||
**Recommendation:** retire/correct AP-30 in the register (delete the row,
|
|
||||||
or rewrite it to note the epsilon match is intentional retail parity, not a
|
|
||||||
divergence) as a small housekeeping fix — no runtime behavior change
|
|
||||||
needed, since the code is already correct.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Inbound presentation
|
|
||||||
|
|
||||||
### 2a. Interpolation catch-up rate — **DIVERGENT, high-severity** (found this session, per coordinator's byte-decode addendum)
|
|
||||||
|
|
||||||
**FACT (P-review byte decode, 2026-07-30).** Retail's
|
|
||||||
`InterpolationManager::adjust_offset`/`UseTime` (0x00555d30/0x00555f20)
|
|
||||||
gates its catch-up-speed source on a **static flag**,
|
|
||||||
`InterpolationManager::fUseAdjustedSpeed_` (`.data` at `0x0081f418`,
|
|
||||||
initialized to `0x1` — confirmed directly, line 1102675):
|
|
||||||
|
|
||||||
```
|
|
||||||
if (fUseAdjustedSpeed_ == 0) catchUpBase = get_max_speed(); // DEAD by default
|
|
||||||
else catchUpBase = get_adjusted_max_speed(); // the LIVE path
|
|
||||||
catchUp = catchUpBase * 2.0f; // MaxInterpolatedVelocityMod
|
|
||||||
```
|
|
||||||
(confirmed directly, `acclient_2013_pseudo_c.txt:353104-353123`).
|
|
||||||
|
|
||||||
`CMotionInterp::get_adjusted_max_speed` (`0x00527d00`, line 305145-305156,
|
|
||||||
read directly — BN drops the x87 return values into dead-looking
|
|
||||||
statements, the same artifact class as `get_max_speed`'s ×4 dropout that
|
|
||||||
UN-2 already resolved by disassembly) is **conditional on the entity's
|
|
||||||
current interpreted forward command**:
|
|
||||||
|
|
||||||
- `forward_command != RunForward (0x44000007)` (i.e. standing, walking,
|
|
||||||
turning, sidestepping, backing up — anything but an actual run cycle):
|
|
||||||
returns the **bare run rate** (`InqRunRate`/`my_run_rate`), **no ×4**.
|
|
||||||
- `forward_command == RunForward`: returns
|
|
||||||
`interpreted_state.forward_speed ÷ current_speed_factor`, **× 4.0**
|
|
||||||
(`RunAnimSpeed`, `0x007c8918`) — per the coordinator's disassembly-level
|
|
||||||
decode (the BN pseudo-C alone drops this trailing multiply, matching the
|
|
||||||
established `get_max_speed`/UN-2 artifact pattern).
|
|
||||||
|
|
||||||
**acdream's current code does not port `get_adjusted_max_speed` at all** —
|
|
||||||
there is no `CurrentSpeedFactor`/`current_speed_factor` field anywhere in
|
|
||||||
`MotionInterpreter.cs` (confirmed by grep, zero hits). Every call site that
|
|
||||||
feeds the interpolation catch-up cap instead calls the **unconditional**
|
|
||||||
`MotionInterpreter.GetMaxSpeed()` (`MotionInterpreter.cs:2632-2642`, itself
|
|
||||||
a faithful, byte-verified port of retail's `get_max_speed` alone — always
|
|
||||||
`runRate × RunAnimSpeed(4.0)`, regardless of forward_command):
|
|
||||||
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:254`,
|
|
||||||
`:291`, `:685` (remote NPC/player catch-up)
|
|
||||||
- `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs:174`
|
|
||||||
- `src/AcDream.App/Input/PlayerModeController.cs:328`
|
|
||||||
|
|
||||||
**Consequence:** for any remote entity that is standing, walking, turning,
|
|
||||||
sidestepping, or backing up (i.e. every state except actively running
|
|
||||||
forward), acdream's catch-up cap is **exactly 4× retail's** (both use the
|
|
||||||
×2.0 `MaxInterpolatedVelocityMod`, but acdream also always applies the ×4.0
|
|
||||||
`RunAnimSpeed` that retail reserves for the RunForward-only branch). For a
|
|
||||||
run-rate-2.94 character standing still: retail caps catch-up at
|
|
||||||
2×2.94 ≈ **5.9 m/s**; acdream currently caps it at 2×2.94×4 ≈ **23.5 m/s**
|
|
||||||
— a 4× overshoot. Only while the remote is genuinely in a `RunForward`
|
|
||||||
cycle does acdream's flat ×4 approach retail's own (still not identical,
|
|
||||||
since retail additionally normalizes by `current_speed_factor`, an
|
|
||||||
unported field).
|
|
||||||
|
|
||||||
This existed underneath a prior investigation (UN-2, resolved 2026-06-12,
|
|
||||||
cited directly in `MotionInterpreter.cs:2601-2630`) that correctly
|
|
||||||
byte-verified the ×4.0 constant *inside* `get_max_speed`, but did not catch
|
|
||||||
that `get_max_speed` itself is the **dead default branch** — retail's real
|
|
||||||
call site always takes `get_adjusted_max_speed`, which only applies that
|
|
||||||
×4 conditionally. This is a strong root-cause candidate for the "remote
|
|
||||||
catch-up feels too fast/twitchy for non-running remotes" symptom family
|
|
||||||
(#41 blips, #165 wall-penetration-before-stop) that the same doc-comment
|
|
||||||
explicitly says to look elsewhere for — this audit's finding redirects that
|
|
||||||
search back to this exact seam.
|
|
||||||
|
|
||||||
**Recommendation (report-only — no fix applied):** port
|
|
||||||
`CMotionInterp::get_adjusted_max_speed` as a new `MotionInterpreter` method
|
|
||||||
(needs `current_speed_factor`, currently absent — a new tracked field,
|
|
||||||
citing `0x00527d00`/line 305145), and switch every catch-up-cap call site
|
|
||||||
above from `GetMaxSpeed()` to the new adjusted accessor, gated by the
|
|
||||||
(retail-fixed-true) `fUseAdjustedSpeed_` semantics — i.e. always call the
|
|
||||||
adjusted variant, since retail's own flag is always on. This is the
|
|
||||||
single highest-value fix candidate in this audit.
|
|
||||||
|
|
||||||
### 2b. Snap / teleport thresholds — two distinct constants, both present
|
|
||||||
|
|
||||||
**FACT.** Retail has two separate thresholds, and acdream has ported both
|
|
||||||
correctly:
|
|
||||||
|
|
||||||
| Constant | Retail value | Retail site | acdream value | acdream site |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Hard routing snap (give up on interpolation entirely, `MoveOrTeleport`) | **96.0 m** | `CPhysicsObj::MoveOrTeleport` 0x00516330, line 284342-284361 | Not separately named in `InterpolationManager.cs` — this gate lives upstream, at the physics dispatch layer (`RuntimeRemotePhysicsUpdater`/teleport handling), not audited line-by-line this session; flagged as **needs a follow-up grep to confirm the 96 m constant is present at the equivalent acdream call site** (not found during this pass — see gap catalog). | — |
|
|
||||||
| Enqueue-time "far jump, pre-arm blip" (`AutonomyBlipDistance`) | **100 m outdoor / 20 m indoor** per prior cdb live-attach (project's own 2026-05-0x capture) — the *decomp* constant itself (`GetAutonomyBlipDistance`, 0x0050eb70) is BN-garbled and not independently re-derivable from static text alone this session (**INFERENCE**, cdb-sourced not decomp-sourced) | `CPhysicsObj::GetAutonomyBlipDistance` 0x0050eb70 | `AutonomyBlipDistance = 100.0f` (`InterpolationManager.cs:99`), comment explicitly notes "indoor is 20 m" as a known-but-unported distinction | `InterpolationManager.cs:99` |
|
|
||||||
|
|
||||||
**Verdict: Parity** for the enqueue-time 100 m outdoor constant (matches
|
|
||||||
the project's own prior cdb finding); the indoor-20m variant is
|
|
||||||
**Divergent/incomplete** — acdream uses a flat 100 m regardless of
|
|
||||||
indoor/outdoor, an existing known gap already flagged in the code's own
|
|
||||||
comment (not a new finding, confirmed still present).
|
|
||||||
|
|
||||||
### 2c. Position-history queue depth — Parity
|
|
||||||
|
|
||||||
**FACT.** Retail: 20 entries (`0x14`), head-evicted on overflow, confirmed
|
|
||||||
directly at `InterpolateTo` line 353004-353021. acdream:
|
|
||||||
`QueueCap = 20` (`InterpolationManager.cs:49`), enforced identically
|
|
||||||
(`Enqueue`, :254-256, `RemoveFirst()` on cap). **Parity.**
|
|
||||||
|
|
||||||
### 2d. Stall/give-up mechanics — Parity
|
|
||||||
|
|
||||||
**FACT**, all four constants cross-checked directly against the decomp
|
|
||||||
this session and via the subagent's independent read of
|
|
||||||
`InterpolationManager.cs`:
|
|
||||||
|
|
||||||
| Constant | Retail (line) | acdream (`InterpolationManager.cs`) |
|
|
||||||
|---|---|---|
|
|
||||||
| Stall check window | 5 frames (353146) | `StallCheckFrameInterval = 5` (:79) |
|
|
||||||
| Min progress distance | 0.20 m (353185-353190) | `MinDistanceToReachPosition = 0.20f` (:67) |
|
|
||||||
| Min progress fraction | 0.30 (353172-353177) | `StallProgressMinFraction = 0.30f` (:86) |
|
|
||||||
| Fail-count blip threshold | `> 3` (353270) | `StallFailCountThreshold = 3` (:92), fires at 4+ |
|
|
||||||
|
|
||||||
**Verdict: Parity.**
|
|
||||||
|
|
||||||
### 2e. TS-44 sticky-gated enqueue suppression
|
|
||||||
|
|
||||||
Not inside `InterpolationManager.cs` itself — lives in the consumer,
|
|
||||||
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:1637-1643`.
|
|
||||||
Suppresses a raw `UpdatePosition`-driven snap for an NPC currently
|
|
||||||
sticky-attached to a target (`PositionManager.GetStickyObjectId() != 0`),
|
|
||||||
bounded by the ~1 s sticky lease; register TS-44 is **narrowed, not
|
|
||||||
retired** (per `docs/ISSUES.md` 2026-07-07 pass). User-visible effect:
|
|
||||||
while a monster is sticky-melee-locked onto a target, an incoming server
|
|
||||||
position correction that would otherwise snap the NPC is suppressed and
|
|
||||||
the sticky steering keeps driving it instead — server truth reasserts on
|
|
||||||
the first UpdatePosition after the lease expires. This is a deliberate,
|
|
||||||
already-registered adaptation, not a newly found gap.
|
|
||||||
|
|
||||||
### 2f. MoveToRunRate consumption
|
|
||||||
|
|
||||||
**FACT.** Wire-parsed in `CreateObject.cs:269-296` (`ServerMotionState`
|
|
||||||
field) and consumed at `LiveEntityNetworkUpdateController.cs:323,433` and
|
|
||||||
`LiveEntityMotionRuntimeController.cs:394`. A pre-existing, separately
|
|
||||||
tracked gap (M13 plan doc `docs/research/2026-07-03-r4-moveto/r4-port-plan.md:87`)
|
|
||||||
notes `MoveToRunRate` feeds the PlanMoveToStart seed but not
|
|
||||||
`MotionInterpreter.MyRunRate` directly during a live moveto, so
|
|
||||||
`apply_run_to_command`'s speed scale can use a stale rate mid-MoveTo. Not
|
|
||||||
re-litigated further this session — flagged as a known, already-filed item
|
|
||||||
in the gap catalog below (not new).
|
|
||||||
|
|
||||||
### 2g. Walk↔Run mid-hold promote/demote render fidelity
|
|
||||||
|
|
||||||
**Issue #39** — "Run↔Walk cycle transition not visible on observed player
|
|
||||||
remotes." **Status confirmed directly this session: CLOSED 2026-07-02**
|
|
||||||
(`docs/ISSUES.md:7107`). The original 2026-05-06 root-cause ("ACE goes
|
|
||||||
silent on HoldKey-only toggle") was refuted by a 2026-07-02
|
|
||||||
three-oracle-plus-live-capture re-investigation
|
|
||||||
(`docs/research/2026-07-02-inbound-motion-deviation-map.md`, §S0): retail
|
|
||||||
DOES send a fresh MoveToState on HoldRun toggle while moving, and ACE DOES
|
|
||||||
rebroadcast it; the refinement machinery #39 built to compensate for the
|
|
||||||
(non-existent) gap was deleted (commit S5) after it caused spurious
|
|
||||||
Ready↔Run animation thrash. **CLAUDE.md's phrasing that this is an open
|
|
||||||
uncertainty ("ACE's behavior on relay is uncertain") is stale relative to
|
|
||||||
the ISSUES.md record** — worth a small doc correction, not a code gap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. MoveTo/TurnTo parameters
|
|
||||||
|
|
||||||
Retail's `MovementParameters` ctor (`0x00524380`, decomp line 300510-300534,
|
|
||||||
struct verbatim at `acclient.h:31453-31465`) vs acdream's
|
|
||||||
`MovementParameters.cs` (already cites the same address):
|
|
||||||
|
|
||||||
| Field | Retail default | acdream default | Cite | Verdict |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `MinDistance` | 0.0 | 0f (`:164`) | 300510-300534 | Parity |
|
|
||||||
| `DistanceToObject` | 0.6 m | 0.6f (`:161`) | same | Parity |
|
|
||||||
| `FailDistance` | FLT_MAX (3.40282347e+38) | `float.MaxValue` (`:173`) | same | Parity |
|
|
||||||
| `Speed` | 1.0 | 1f (`:170`) | same | Parity |
|
|
||||||
| `WalkRunThreshhold` | **15.0 m** | 15f (`:179`) | same | Parity — and acdream's own comment explicitly flags the ACE-divergence trap (ACE uses 1.0) and refuses to copy it |
|
|
||||||
| `CanCharge` (bitfield 0x10) | **clear (false)** | `false` (`:102`) | same | Parity — same explicit ACE-divergence-trap comment (ACE sets it true by default) |
|
|
||||||
| `HoldKeyToApply` | `HoldKey_Invalid` | `HoldKey.Invalid` (`:185`) | same | Parity |
|
|
||||||
|
|
||||||
**Verdict: full parity.** acdream's `MovementParameters.cs` is a verbatim,
|
|
||||||
already-well-cited port with correct, explicit call-outs of two known
|
|
||||||
ACE-vs-retail divergence traps (`CanCharge`, `WalkRunThreshhold`) that it
|
|
||||||
deliberately does NOT copy from ACE. No gap found here.
|
|
||||||
|
|
||||||
### Turn/arrival thresholds beyond the ctor
|
|
||||||
|
|
||||||
- `HandleMoveToPosition`'s aux-turn deadband: **20°/340°**
|
|
||||||
(`MoveToManager.cs:1057`, retail `0x00529d80` line 307187-307438) —
|
|
||||||
matches the retail function it cites; not independently re-derived from
|
|
||||||
raw bytes this session (**INFERENCE** on the exact retail constant, but
|
|
||||||
the citation chain is pre-existing and was not contradicted by anything
|
|
||||||
found this session).
|
|
||||||
- `BeginTurnToHeading`/`HandleTurnToHeading` epsilon-snap logic
|
|
||||||
(`MoveToManager.cs:819-873`, `:1155-1203`) cites retail addresses
|
|
||||||
`0x00529b90`/`0x0052a0c0` directly; the code's own comments flag two
|
|
||||||
retail quirks as deliberately preserved: `FailProgressCount` is
|
|
||||||
write-only in retail (no give-up threshold exists — do not invent one),
|
|
||||||
and `HandleMoveToPosition` has **no** `set_heading` call (ACE's
|
|
||||||
"sync for server tickrate" addition is explicitly NOT ported). Both
|
|
||||||
read as correct, deliberate non-divergences.
|
|
||||||
|
|
||||||
### MinDistance vs FailDistance semantics
|
|
||||||
|
|
||||||
**FACT** (both retail and acdream, per direct decomp read this session and
|
|
||||||
independent MoveToManager.cs read): both `MoveToObject` and
|
|
||||||
`MoveToPosition` share one `movement_params`/`Params` struct and one
|
|
||||||
handler. `MinDistance`/`DistanceToObject` gates *arrival* (current distance
|
|
||||||
to target this tick); `FailDistance` gates *give-up* against **total
|
|
||||||
distance traveled since the move began**, defaulting to FLT_MAX so it is
|
|
||||||
effectively inert unless a caller tightens it. No object-vs-position
|
|
||||||
asymmetry exists in either client. **Parity.**
|
|
||||||
|
|
||||||
### AP-23 — pickup/use-radius heuristic (current scope)
|
|
||||||
|
|
||||||
Register row (`retail-divergence-register.md:148`): an invented per-type
|
|
||||||
radius bucket (3 m creatures / 2 m doors-lifestones-portals-corpses / 0.6 m
|
|
||||||
rest) for close-range gating. **Narrowed 2026-07-25 (R5-V3):** the
|
|
||||||
speculative install now threads the target's real Setup
|
|
||||||
radius/height (`GetSetupCylinder`) and the player's real radius; only the
|
|
||||||
bucket bounds remain invented, and Use itself was retired from the
|
|
||||||
speculative-moveto seam entirely (sends immediately now). Located at
|
|
||||||
`src/AcDream.App/Interaction/WorldSelectionQuery.cs:78-83` (constants),
|
|
||||||
`:490-498` (`GetUseRadius`). **One live consumer still cites this seam as
|
|
||||||
its root mechanism**: `docs/ISSUES.md` issue at line 3825-3826 (2026-07-05,
|
|
||||||
door-Use-swallowed, HIGH severity), whose resolution is explicitly folded
|
|
||||||
into Campaign P's physics-parity visual matrix scenario 8
|
|
||||||
(`docs/plans/2026-07-30-physics-parity-visual-matrix.md`) — i.e. this is
|
|
||||||
already tracked and pending the user's visual gate, not a newly found gap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Autorun + mouse semantics
|
|
||||||
|
|
||||||
### Retail mechanics (all FACT, byte-read this session)
|
|
||||||
|
|
||||||
- **Toggle entry point:** `CommandInterpreter::ToggleAutoRun` (`0x006b3cc0`,
|
|
||||||
line 699625-699631): `SetAutoRun(auto_run==0, 1)`. Bound via
|
|
||||||
`CommandInterpreter::HandleKeyboardCommand` (`0x006b3690`, line
|
|
||||||
699262-699289) on keyboard command `0x90000c7`: reads an **optional
|
|
||||||
trailing float from the same keybind's argument stream** as
|
|
||||||
`autorun_speed` (defaults to **1.0** if the keybind carries no extra
|
|
||||||
argument — confirmed this is the stock case: the retail keymap's
|
|
||||||
`MovementRunLock [ "" [ 0 DIK_Q ] ]` entry carries no such argument).
|
|
||||||
- **Default key:** `Q` — confirmed identical in
|
|
||||||
`docs/research/named-retail/retail-default.keymap.txt:105` and
|
|
||||||
acdream's `KeyBindings.RetailDefaults():174`.
|
|
||||||
- **What "on" actually sends:** `CommandInterpreter::ApplyCurrentMovement`
|
|
||||||
(`0x006b3430`, line 699146-699183): when `auto_run != 0`, retail
|
|
||||||
unconditionally calls
|
|
||||||
`MovePlayer(WalkForward(0x45000005), 1, autorun_speed, SetHoldKey=1, HoldKeyToApply=1(Run))`
|
|
||||||
— **autorun ALWAYS forces `HoldKey.Run`**, hard-coded, independent of any
|
|
||||||
live walk/run toggle state. Since stock `autorun_speed` defaults to 1.0
|
|
||||||
and the hold key is forced Run, **retail's default autorun always runs**
|
|
||||||
(WalkForward+HoldKey.Run, which ACE/observers see as RunForward), never
|
|
||||||
walks, regardless of whether the player has Shift/walk-mode held at
|
|
||||||
toggle time or afterward.
|
|
||||||
- **Cancel conditions, two independent mechanisms:**
|
|
||||||
1. `CommandInterpreter::HandleNewForwardMovement` (`0x006b3d60`, line
|
|
||||||
699672-699676): **any fresh Forward key press cancels autorun**
|
|
||||||
(`SetAutoRun(0, 1)`).
|
|
||||||
2. `CInputManager::ActivateActionKey` (`0x00432650`, line 699243-699258
|
|
||||||
region, specifically 699496-699502): on a genuine key-down edge
|
|
||||||
(not a repeat) for action IDs `0x29`/`0x2a`/`0x2b`, calls
|
|
||||||
`CInputManager::TurnOffRunLock` (`0x004325e0`, line 699424-699442),
|
|
||||||
which removes the `MovementRunLock` action state and fires its
|
|
||||||
release-equivalent listener callback. (The exact identity of actions
|
|
||||||
`0x29`-`0x2b` as raw `CInputManager` action-ID ordinals was not
|
|
||||||
resolved from static text this session — **INFERENCE** that they are
|
|
||||||
Backward/StrafeLeft/StrafeRight, based on process-of-elimination
|
|
||||||
against `HandleNewForwardMovement`'s separate, explicit Forward-only
|
|
||||||
handling.)
|
|
||||||
3. Also unconditionally cleared on `LoseControlToServer`,
|
|
||||||
`PlayerTeleported`, `PlayerIsDead`-detected `MovePlayer` calls, and
|
|
||||||
`HandleKeyboardCommand`'s own `LoseKeyboardFocus`/death paths.
|
|
||||||
|
|
||||||
### acdream mechanics
|
|
||||||
|
|
||||||
`RuntimeLocalPlayerMovementState.cs` (`Execute(ToggleRunLock)`, :116-119;
|
|
||||||
`CancelAutoRun()`, :148-156) + `DispatcherMovementInputSource.cs` (:48-96):
|
|
||||||
|
|
||||||
- Default key: **Q** — matches (`KeyBindings.cs:174`).
|
|
||||||
- **On:** `Forward: forward || AutoRunActive` (:65) — autorun simply forces
|
|
||||||
the `Forward` boolean true; `Run: !walking` (:71) is evaluated **live,
|
|
||||||
every poll**, from whatever `InputAction.MovementWalkMode` (Shift) is
|
|
||||||
currently held — **independent of autorun state**.
|
|
||||||
- **Cancel set:** `HandlePressedAction` (:80-96) cancels autorun on Press
|
|
||||||
of `{MovementBackup, MovementStop, MovementStrafeLeft, MovementStrafeRight}`
|
|
||||||
only.
|
|
||||||
|
|
||||||
### Verdicts
|
|
||||||
|
|
||||||
| Behavior | Retail | acdream | Verdict |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Default key | Q | Q | Parity |
|
|
||||||
| Pace while autorunning | **Always Run** (hard-forced `HoldKey.Run`, independent of walk-mode toggle) | **Follows the live `MovementWalkMode` toggle** — if the user has Shift/walk-mode held (or toggled) while or after engaging autorun, autorun walks instead of runs | **Divergent.** Confirmed by direct read of `DispatcherMovementInputSource.cs:71` (`Run: !walking`, unconditioned on `AutoRunActive`) against retail's `ApplyCurrentMovement` autorun branch (`SetHoldKey=1, HoldKeyToApply=1` hard-coded, `0x006b3486`). |
|
|
||||||
| Cancel on Backward/Strafe | Yes (input-layer `TurnOffRunLock`, **INFERENCE** on exact action IDs) | Yes, explicit (`MovementBackup`, `MovementStrafeLeft`, `MovementStrafeRight`) | Parity (functional match) |
|
|
||||||
| Cancel on Stop key | Not separately identified in retail's cancel set this session (no explicit S/Stop-key cancel site found; likely folds through `auto_run`/`transient_state` reset elsewhere) | Yes, explicit (`MovementStop`) | Likely parity, low-confidence on the retail side |
|
|
||||||
| **Cancel on fresh Forward press** | **Yes** — `HandleNewForwardMovement` explicitly cancels autorun on every new W press (`0x006b3d60`) | **No** — `MovementForward` is absent from `HandlePressedAction`'s cancel list (:86-91); architecturally reachable (the same Press edge already drives `CombatAttackInputFrameAdapter.HandleMovementInput`'s abort-for-movement check, `GameplayInputFrameController.cs:24-39`) but not wired to `CancelAutoRun()` | **Divergent, confirmed gap.** In acdream, pressing W while autorunning is currently a no-op (autorun stays latched, `Forward` was already true); in retail, the same press explicitly drops autorun and hands control back to the held key. |
|
|
||||||
| Mouse-look interaction with autorun | No evidence found of a direct interaction; mouse-look drives its own `TurnLeft`/`TurnRight` channel independent of `auto_run` | Same — mouse-look turn channel (`_activeInputTurnCommand`) is independent of `AutoRunActive` | Parity (no interaction expected on either side) |
|
|
||||||
| Both-mouse-buttons-run | No evidence found of a distinct "both mouse buttons = run forward" binding in the decompiled `CommandInterpreter`/`CInputManager` text searched this session | Not implemented (no such binding in `KeyBindings.RetailDefaults()`) | **Ruled out as a feature** — this session found no retail mechanism for it, so acdream's absence is not a gap. (If the user recalls this from live retail play, it would warrant a targeted cdb trace on `IInputActionCallback`/mouse-button handlers; not found in the static decomp searched here.) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Turn-rate composition
|
|
||||||
|
|
||||||
**FACT**, direct decomp read this session, `CMotionInterp::apply_raw_movement`
|
|
||||||
(`0x005287e0`, line 305817-305834) → three independent
|
|
||||||
`adjust_motion(forward)`, `adjust_motion(sidestep)`, `adjust_motion(turn)`
|
|
||||||
calls → `apply_interpreted_movement` (`0x00528600`, line 305713-305788)
|
|
||||||
dispatches `DoInterpretedMotion` **separately** per axis.
|
|
||||||
|
|
||||||
- **No cross-axis normalization exists in retail.** Forward, sidestep, and
|
|
||||||
turn are fully independent scalar channels; there is no diagonal-movement
|
|
||||||
magnitude clamp (no "moving diagonally isn't faster than moving straight"
|
|
||||||
logic anywhere in this pipeline) — retail "naively adds commands," to use
|
|
||||||
the literal reading of `apply_interpreted_movement`'s three sequential,
|
|
||||||
unconditional `DoInterpretedMotion` calls.
|
|
||||||
- **Turning while moving backward:** confirmed **no interaction** — the
|
|
||||||
backward `-0.65×` scale (`adjust_motion`'s `0x45000006→WalkForward`
|
|
||||||
canonicalization) only touches the forward channel; the turn channel's
|
|
||||||
own `adjust_motion(turn)` call and its 1.5× run-turn factor are
|
|
||||||
processed independently with zero shared state.
|
|
||||||
- **Order of operations relative to dt:** the 1.5× turn multiplier (and
|
|
||||||
the 1.248× sidestep scale, and the ×4.0/`current_speed_factor` catch-up
|
|
||||||
math) all operate on the pre-integration **speed scalar**; dt-integration
|
|
||||||
happens downstream in physics, not inside `adjust_motion`/
|
|
||||||
`apply_run_to_command`. So "run-turn factor before or after dt scaling"
|
|
||||||
is moot — it's applied to the same speed value physics later multiplies
|
|
||||||
by dt, in both clients.
|
|
||||||
|
|
||||||
**acdream's port** (`MotionInterpreter.cs`, `adjust_motion` :1290-1321,
|
|
||||||
`apply_raw_movement`-equivalent :1386-1424) mirrors this structure exactly:
|
|
||||||
three independent `adjust_motion` calls per axis (:1416, :1420, :1424), no
|
|
||||||
cross-axis clamp anywhere in the surrounding code, and the code's own
|
|
||||||
comment (:1268-1271) explicitly documents the same ordering subtlety
|
|
||||||
retail has (sign-flip on canonicalization happens BEFORE the 1.248
|
|
||||||
sidestep scale, so the net multiplier for SideStepLeft is `-1.248×speed`,
|
|
||||||
not `-1×(1.248×speed)` — same value algebraically, but the comment shows
|
|
||||||
the port tracked retail's actual operation order, not just its result).
|
|
||||||
|
|
||||||
**Verdict: full parity.** No combined-input normalization gap found on
|
|
||||||
either side (neither client has one) — this is a "ruled out" item, not an
|
|
||||||
open question. Backward+turn and strafe+turn combinations have no special
|
|
||||||
case in retail and none in acdream, matching.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Wire-format cross-check: holtburger + Chorizite
|
|
||||||
|
|
||||||
### RawMotionState / MoveToState / AutonomousPosition bit layout — three-way parity
|
|
||||||
|
|
||||||
**FACT.** `references/holtburger/crates/holtburger-protocol/src/messages/movement/types.rs:45-61`
|
|
||||||
(`RawMotionFlags` bitflags) is bit-for-bit identical to acdream's
|
|
||||||
`RawMotionStatePacker.cs:44-55` flag constants (0x001 CurrentHoldKey through
|
|
||||||
0x400 TurnSpeed, `num_actions` in bits 11+ via
|
|
||||||
`packed_flags >> 11` matching acdream's `NumActionsShift = 11`), and both
|
|
||||||
match the named-retail `RawMotionState::Pack` (0x0051ed10) bitfield this
|
|
||||||
project already ported. `MoveToStateActionData`
|
|
||||||
(`.../movement/actions.rs:9-18`) field order (raw_motion_state, position,
|
|
||||||
4× u16 sequence, one trailing byte) matches acdream's `MoveToState.Build`
|
|
||||||
call shape exactly, including the trailing
|
|
||||||
`(standingLongjump?2:0)|(contact?1:0)` byte (holtburger's
|
|
||||||
`contact_long_jump: u8`, same slot). `AutonomousPositionActionData`
|
|
||||||
(:141-149) matches `AutonomousPosition.Build` field-for-field.
|
|
||||||
|
|
||||||
Independently, holtburger's own `AUTONOMOUS_POSITION_HEARTBEAT_INTERVAL`
|
|
||||||
(`crates/holtburger-core/src/client/movement/common.rs:22`) is
|
|
||||||
**`Duration::from_secs(1)`** — a third independent confirmation (after
|
|
||||||
retail's decomp ctor default `0x3ff00000`=1.0 at line 699783, and acdream's
|
|
||||||
`HeartbeatInterval = 1.0f`) that the AP heartbeat is exactly 1 second across
|
|
||||||
all three. **Parity, three-way confirmed.**
|
|
||||||
|
|
||||||
### Jump packet — acdream matches retail; BOTH holtburger and Chorizite are wrong here
|
|
||||||
|
|
||||||
**FACT, byte-verified this session.** Retail's `JumpPack::Pack`
|
|
||||||
(`0x00516d10`, decomp line 284915-284967, read directly) writes, in exact
|
|
||||||
order: `extent` (f32) → `velocity.x/y/z` (f32×3) →
|
|
||||||
**`this->position.vtable->Pack(...)`** (a full `Position` pack: objcell_id
|
|
||||||
+ frame origin + quaternion) → `instance_timestamp`/`server_control_timestamp`/
|
|
||||||
`teleport_timestamp`/`force_position_ts` (u16×4) → 4-byte align. This
|
|
||||||
matches the `JumpPack` **constructor** signature
|
|
||||||
(`0x00516c70`, line 284887: `float, Vector3 const*, Position const*, u16×4`)
|
|
||||||
exactly — Position genuinely is part of the wire bytes, not just a
|
|
||||||
constructor-time convenience.
|
|
||||||
|
|
||||||
acdream's `JumpAction.Build(gameActionSequence, extent, velocity, cellId,
|
|
||||||
position, rotation, instanceSequence, serverControlSequence,
|
|
||||||
teleportSequence, forcePositionSequence)` (called from
|
|
||||||
`LocalPlayerOutboundController.cs:73-84`) matches this exactly — this was
|
|
||||||
already the subject of a correction (memory: "D4 `JumpAction` = retail
|
|
||||||
`JumpPack` (extent·velocity·Position·4 ts); spurious objectGuid/spellId
|
|
||||||
removed, Position now packed").
|
|
||||||
|
|
||||||
By contrast:
|
|
||||||
- **holtburger's `JumpActionData`** (`.../movement/actions.rs:73-82`) has
|
|
||||||
**no `Position` field at all** — instead `extent`, `velocity`, 4×
|
|
||||||
sequence, then `object_guid: Guid` and `spell_id: u32`. This is the
|
|
||||||
*pre-correction* shape acdream itself used to have before the D4 fix
|
|
||||||
(per the same memory note) — i.e. holtburger's Jump model reproduces the
|
|
||||||
same historical mistake acdream already found and fixed via the named
|
|
||||||
decomp.
|
|
||||||
- **Chorizite's `JumpPack.generated.cs`** (`Types/JumpPack.generated.cs:22-83`)
|
|
||||||
has **neither Position nor object_guid/spell_id** — just `Extent`,
|
|
||||||
`Velocity`, and the 4 sequence ushorts, then straight to 4-byte
|
|
||||||
alignment. Also missing the Position bytes.
|
|
||||||
|
|
||||||
**Conclusion: acdream's Jump packet is the retail-correct one; do not use
|
|
||||||
holtburger's or Chorizite's Jump models as a tiebreaker for this specific
|
|
||||||
packet** — both diverge from the byte-verified retail shape in the same
|
|
||||||
direction (omitting Position), and holtburger additionally invents
|
|
||||||
object_guid/spell_id fields that do not exist on the wire. This is a
|
|
||||||
genuine finding worth remembering for future cross-reference work on this
|
|
||||||
one packet (not something to act on in acdream — acdream is already
|
|
||||||
correct), and is exactly the kind of case the project's reference-hierarchy
|
|
||||||
rule anticipates ("the intersection of the relevant references is almost
|
|
||||||
always the truth... a single reference can be misleading") — here the
|
|
||||||
*retail decomp itself*, not the intersection, was the tiebreaker, since two
|
|
||||||
of three references independently share the same divergence.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Ranked gap catalog
|
|
||||||
|
|
||||||
1. **[HIGH] Interpolation catch-up cap is 4× too fast for any non-running remote (§2a).**
|
|
||||||
`MotionInterpreter` never ported `get_adjusted_max_speed`
|
|
||||||
(`0x00527d00`) or `current_speed_factor`; every catch-up-cap call site
|
|
||||||
(`RuntimeRemotePhysicsUpdater.cs:254,291,685`,
|
|
||||||
`LiveEntityMotionRuntimeController.cs:174`, `PlayerModeController.cs:328`)
|
|
||||||
uses the always-×4 `GetMaxSpeed()` instead of the conditional accessor
|
|
||||||
retail's own `fUseAdjustedSpeed_=1` static makes the *only* live path.
|
|
||||||
Root-cause candidate for observed remote catch-up feeling too
|
|
||||||
fast/twitchy outside full sprint. **Recommended fix order: first**,
|
|
||||||
since it's concrete, well-cited, and plausibly explains existing
|
|
||||||
symptom reports (#41/#165 family) the project has been chasing under
|
|
||||||
other theories.
|
|
||||||
2. **[MEDIUM] Autorun always inherits the live walk/run toggle instead of always forcing Run (§4).**
|
|
||||||
`DispatcherMovementInputSource.cs:71` computes `Run: !walking` every
|
|
||||||
poll, unconditioned on `AutoRunActive`; retail's `ApplyCurrentMovement`
|
|
||||||
hard-forces `HoldKey.Run` for the entire duration of an autorun latch
|
|
||||||
regardless of walk-mode state. User-visible: toggling walk-mode while
|
|
||||||
autorunning in acdream can make it walk; retail autorun never walks
|
|
||||||
(absent a custom keybind speed argument, which the stock keymap doesn't
|
|
||||||
carry).
|
|
||||||
3. **[MEDIUM] Autorun does not cancel on a fresh Forward (W) press (§4).**
|
|
||||||
`HandlePressedAction`'s cancel set omits `InputAction.MovementForward`;
|
|
||||||
retail's `HandleNewForwardMovement` explicitly cancels on every new W
|
|
||||||
edge. Currently a silent no-op difference (autorun stays latched) that
|
|
||||||
is architecturally trivial to close — the same Press edge is already
|
|
||||||
routed through the pipeline for the unrelated combat-abort check.
|
|
||||||
4. **[LOW, doc-only] AP-30 register row is stale (§1).** Both its file:line
|
|
||||||
citation and its epsilon claim about retail no longer match reality —
|
|
||||||
the code already matches retail's real (epsilon-based, not exact)
|
|
||||||
`Frame::is_equal`/`Plane::operator==` comparison. Recommend
|
|
||||||
retiring/correcting the row; zero runtime risk either way.
|
|
||||||
5. **[LOW] Indoor `AutonomyBlipDistance` uses a flat 100 m regardless of indoor/outdoor (§2b).**
|
|
||||||
Already flagged in the code's own comment as a known simplification (cdb
|
|
||||||
sourced 20 m indoor vs 100 m outdoor); not a new finding, but grouped
|
|
||||||
here since it's the one open item in an otherwise clean interpolation
|
|
||||||
audit.
|
|
||||||
6. **[LOW, needs follow-up not fix] Confirm the 96 m hard-teleport-snap threshold's acdream equivalent (§2b).**
|
|
||||||
This session did not locate the acdream call site that mirrors retail's
|
|
||||||
`MoveOrTeleport` 96 m routing gate (`0x00516330`) — flagged as an
|
|
||||||
unresolved research gap, not a confirmed divergence. Worth a follow-up
|
|
||||||
grep for wherever acdream decides "too far to interpolate, snap
|
|
||||||
instead" at the physics-dispatch layer (outside `InterpolationManager.cs`
|
|
||||||
itself).
|
|
||||||
7. **[INFO, no action] CLAUDE.md's "ACE's Run↔Walk relay behavior is uncertain" phrasing is stale (§2g).**
|
|
||||||
Issue #39 closed 2026-07-02 with the opposite finding (retail does send
|
|
||||||
a fresh MoveToState on HoldRun toggle; ACE does relay it). Small doc
|
|
||||||
correction candidate, zero code impact.
|
|
||||||
8. **[INFO, no action] Two of three wire-format oracles have a wrong Jump packet model (§6).**
|
|
||||||
holtburger and Chorizite both omit `Position` from their Jump packet
|
|
||||||
type; acdream's is byte-verified correct. No action needed on acdream's
|
|
||||||
side — recorded so a future cross-reference pass doesn't get misled by
|
|
||||||
holtburger/Chorizite's shared mistake on this one packet.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sources consulted
|
|
||||||
|
|
||||||
- `docs/plans/2026-07-29-physics-parity-campaign.md` (scope boundary —
|
|
||||||
what Campaign P already closed)
|
|
||||||
- `docs/architecture/retail-divergence-register.md` (rows TS-33, TS-28,
|
|
||||||
AP-30, AD-57, and the full IA/AD/TS header banners for context)
|
|
||||||
- `docs/ISSUES.md` (#235, #262, #39, the AP-23 door-Use item at :3825-3826)
|
|
||||||
- `claude-memory/project_retail_motion_outbound.md`,
|
|
||||||
`claude-memory/project_input_pipeline.md`,
|
|
||||||
`claude-memory/project_physics_collision_digest.md` (Campaign P summary
|
|
||||||
section only)
|
|
||||||
- `claude-memory/feedback_autowalk_cancharge_bit.md`
|
|
||||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` — direct reads at
|
|
||||||
lines 38445-38505 (`Frame::is_equal`/`is_quaternion_equal`), 305062-305156
|
|
||||||
(`apply_run_to_command`, `get_adjusted_max_speed`), 305160-305199
|
|
||||||
(`get_state_velocity`), 353095-353135 (`InterpolationManager` catch-up
|
|
||||||
dispatch), 353261-353344 region, 284887-284967 (`JumpPack::Pack`/ctor),
|
|
||||||
698940-699850 (autorun/`CommandInterpreter` family), 699560-699830
|
|
||||||
(`UseTime`, `ToggleAutoRun`, `HandleNewForwardMovement`, `Plane::operator==`,
|
|
||||||
`CommandInterpreter` ctor), 699120-699220 (`ApplyCurrentMovement`,
|
|
||||||
`ApplyListHeadMovement`), 55424-55520 (`CInputManager::TurnOffRunLock`/
|
|
||||||
`ActivateActionKey`), 700233-700420 (`ShouldSendPositionEvent`,
|
|
||||||
`SendMovementEvent`, `SendPositionEvent`, `SetAutoRun`); plus targeted
|
|
||||||
greps for `JumpPack`, `apply_run_to_command`, `auto_run`, `Plane::operator==`,
|
|
||||||
`Frame::is_equal`, `0x45000005`.
|
|
||||||
- `docs/research/named-retail/retail-default.keymap.txt` (Q=MovementRunLock,
|
|
||||||
S=Stop confirmed)
|
|
||||||
- acdream source: `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs`,
|
|
||||||
`LocalPlayerOutboundController.cs`, `RuntimeLocalPlayerMovementState.cs`;
|
|
||||||
`src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs`;
|
|
||||||
`src/AcDream.Core/Physics/RawMotionState.cs`, `MotionInterpreter.cs`,
|
|
||||||
`InterpolationManager.cs`, `Motion/MoveToManager.cs`,
|
|
||||||
`Motion/MovementParameters.cs`; `src/AcDream.App/Input/DispatcherMovementInputSource.cs`,
|
|
||||||
`GameplayInputFrameController.cs`; `src/AcDream.UI.Abstractions/Input/KeyBindings.cs`,
|
|
||||||
`InputAction.cs`; `src/AcDream.App/Interaction/WorldSelectionQuery.cs`;
|
|
||||||
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs`,
|
|
||||||
`LiveEntityMotionRuntimeController.cs`; `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs`
|
|
||||||
- `references/holtburger/crates/holtburger-protocol/src/messages/movement/types.rs`,
|
|
||||||
`actions.rs`; `references/holtburger/crates/holtburger-core/src/client/movement/{system.rs,common.rs}`
|
|
||||||
- `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Types/JumpPack.generated.cs`,
|
|
||||||
`Messages/C2S/Actions/Movement_Jump.generated.cs`
|
|
||||||
- Two Sonnet research subagents (retail-decomp MoveTo/interpolation/turn
|
|
||||||
research; acdream MoveToManager/InterpolationManager code research) —
|
|
||||||
their findings were spot-checked directly against the named decomp and
|
|
||||||
source files in this pass (per `feedback_verify_subagent_claims_against_source.md`);
|
|
||||||
all spot-checks (MovementParameters ctor defaults, `apply_run_to_command`,
|
|
||||||
issue #39 status) matched their reports.
|
|
||||||
|
|
@ -1,453 +0,0 @@
|
||||||
# Campaign P — P1 stat-coupled movement: pseudocode + retail chain
|
|
||||||
|
|
||||||
Filed 2026-07-30 ahead of the P1 implementation (burden/stamina/vitae feeding
|
|
||||||
run rate, jump height, jump permission, jump stamina cost). All addresses are
|
|
||||||
from `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR
|
|
||||||
build) unless marked ACE-cross-reference. Ghidra MCP was unavailable for this
|
|
||||||
slice (operator note); ACE (`references/ACE/Source/ACE.Server/Physics/`) is
|
|
||||||
the tiebreaker wherever BN's x87 mush drops a branch, called out explicitly
|
|
||||||
below.
|
|
||||||
|
|
||||||
## 1. The call chain (top to bottom)
|
|
||||||
|
|
||||||
```
|
|
||||||
CMotionInterp (our MotionInterpreter.cs, unchanged this slice)
|
|
||||||
jump_is_allowed / ChargeJump / JumpChargeIsAllowed
|
|
||||||
-> WeenieObj.CanJump(extent) [IWeenieObject +0x3C]
|
|
||||||
-> WeenieObj.JumpStaminaCost(extent, out cost) [IWeenieObject +0x44]
|
|
||||||
GetJumpVZ -> WeenieObj.InqJumpVelocity(extent, out vz) [+0x30]
|
|
||||||
apply_run_to_command -> WeenieObj.InqRunRate(out rate) [+0x34]
|
|
||||||
|
|
||||||
ACCWeenieObject (thin delegation, pc 406512+)
|
|
||||||
CanJump/JumpStaminaCost/InqRunRate/InqJumpVelocity/InqMaxRunRate all
|
|
||||||
gate on IsThePlayer() first (0058c400/40/520/560/5a0) — NPCs/monsters/
|
|
||||||
remote players never reach m_pQualities for these queries. Confirms P1
|
|
||||||
is scoped correctly to PlayerWeenie only; RemoteWeenie is untouched.
|
|
||||||
|
|
||||||
CACQualities (the "qualities DB" == our PlayerWeenie, pc 412901-414050)
|
|
||||||
InqLoad 0x0058f130 (pc 409756) — burden/load ratio
|
|
||||||
CanJump 0x00591b50 — burden hard-gate
|
|
||||||
JumpStaminaCost 0x00591b90 — stamina cost + PK flag
|
|
||||||
InqRunRate 0x00592800 — full skill+vitae chain
|
|
||||||
InqJumpVelocity 0x00592980 — mirrors InqRunRate for Jump
|
|
||||||
|
|
||||||
MovementSystem (pure formulas, pc 695958+)
|
|
||||||
GetRunRate 0x006b0950
|
|
||||||
GetJumpHeight 0x006b09b0
|
|
||||||
JumpStaminaCost 0x006b0a40
|
|
||||||
|
|
||||||
EncumbranceSystem (pure formulas, pc 256393+)
|
|
||||||
EncumbranceCapacity 0x004fcc00
|
|
||||||
Load 0x004fcc40
|
|
||||||
LoadMod 0x004fcc70
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. InqLoad (0x0058f130, pc 409756) — FULLY READABLE
|
|
||||||
|
|
||||||
```c
|
|
||||||
InqLoad(this, &loadOut):
|
|
||||||
strength = InqAttribute(this, ATTRIBUTE_STRENGTH=1) // default 0xa if absent
|
|
||||||
aug = InqInt(this, PROPERTY_INT_AUGMENTATION_INCREASED_CARRYING_CAPACITY=0xE6 /*230*/)
|
|
||||||
capacity = EncumbranceSystem::EncumbranceCapacity(strength, aug)
|
|
||||||
burden = InqInt(this, PROPERTY_INT_ENCUMBRANCE_VAL=5) // default 0 if absent
|
|
||||||
*loadOut = EncumbranceSystem::Load(capacity, burden)
|
|
||||||
return 1 // always succeeds for CACQualities (has vtable)
|
|
||||||
```
|
|
||||||
|
|
||||||
The property/capacity shape matches acdream's
|
|
||||||
`IndicatorBarController.UpdateBurden()` /
|
|
||||||
`InventoryController.RefreshBurden()` pattern (Strength attribute + prop 0xE6
|
|
||||||
aug + prop 5 EncumbranceVal, falling back to `SumCarriedBurden` when the wire
|
|
||||||
value is absent). A 2026-07-31 connected gate exposed one omitted retail
|
|
||||||
detail: `InqAttribute` returns the enchantment-adjusted attribute, while all
|
|
||||||
three acdream burden consumers still read raw `AttributeValue.Current`.
|
|
||||||
Issue #272 corrects them to `LocalPlayerState.GetEffectiveAttribute(Strength)`
|
|
||||||
and invalidates burden on the canonical `Spellbook.EnchantmentsChanged` edge.
|
|
||||||
**`AcDream.Core.Items.BurdenMath`
|
|
||||||
(`EncumbranceCapacity`/`LoadRatio`/`LoadModifier`) is the SAME formulas at
|
|
||||||
the SAME addresses.** P1's `EncumbranceSystem` (Physics-namespaced, for
|
|
||||||
citation clarity next to `MovementSystem`) delegates to `BurdenMath` rather
|
|
||||||
than re-deriving — one source of truth, no drift between the burden HUD and
|
|
||||||
movement physics.
|
|
||||||
|
|
||||||
## 3. CanJump (0x00591b50, pc 412907) — X87 MUSH, POLARITY RESOLVED BY PLAUSIBILITY
|
|
||||||
|
|
||||||
```c
|
|
||||||
CanJump(this, extent):
|
|
||||||
load = 0
|
|
||||||
if (InqLoad(this, &load) != 0):
|
|
||||||
p = <fcompp load, 2.0f; fnstsw; test ah,0x05> // "load < 2.0" per BN's own
|
|
||||||
// asserted C0 subexpression
|
|
||||||
if (!p) return 1
|
|
||||||
return 0
|
|
||||||
```
|
|
||||||
|
|
||||||
Literal BN reading: `if (!p) return 1` = "if load is NOT < 2.0 (i.e. >= 2.0),
|
|
||||||
return CAN-jump; otherwise CANNOT". That is backwards from every other
|
|
||||||
retail-movement fact we have (LoadMod's own floor sits at 2.0; the campaign's
|
|
||||||
connected-matrix acceptance is "≥200% barely moves/jumps", not "can only jump
|
|
||||||
when overloaded"). This is the documented BN "bitfield mush" artifact class
|
|
||||||
(`feedback_bn_decomp_field_names.md`) — the flag-synthesis is unreliable for
|
|
||||||
the FOLLOWING `test ah,mask` interpretation even when the preceding
|
|
||||||
subexpression is trustworthy.
|
|
||||||
|
|
||||||
**Resolution (register row UN-8, see §6):** `CanJump` returns `load < 2.0`
|
|
||||||
(can jump under 200% burden; refused at/above it) — the polarity a normal
|
|
||||||
AC player's lived experience requires, and the one that makes CanJump's own
|
|
||||||
threshold coincide with `LoadMod`'s floor. ACE gives no tiebreaker
|
|
||||||
(`WeenieObject.CanJump` is an unconditional `return true` stub — never
|
|
||||||
ported burden gating at all). Ghidra MCP was down for this slice; flagged
|
|
||||||
for a future confirmation pass, not blocking this port.
|
|
||||||
|
|
||||||
## 4. JumpStaminaCost (0x00591b90, pc 412949) — FULLY READABLE
|
|
||||||
|
|
||||||
```c
|
|
||||||
CACQualities::JumpStaminaCost(this, extent, &costOut):
|
|
||||||
load = 0
|
|
||||||
if (InqLoad(this, &load) == 0) return 0
|
|
||||||
pk = 0
|
|
||||||
pkStatus = InqInt(this, PROPERTY_INT_PLAYER_KILLER_STATUS=0x86, default=8)
|
|
||||||
if (pkStatus == 4 || pkStatus == 0x40): // PK / PKLite
|
|
||||||
pkTimestamp = InqFloat(this, PROPERTY_FLOAT_LAST_PK_ATTACK_TIMESTAMP=0x91)
|
|
||||||
if (pkTimestamp is present && !(pkTimestamp + 20.0 < Timer::cur_time)):
|
|
||||||
pk = 1 // PK timer active (<20s since last PK act)
|
|
||||||
*costOut = MovementSystem::JumpStaminaCost(extent, load, pk)
|
|
||||||
return 1 // ALWAYS true once InqLoad succeeds — no affordability
|
|
||||||
// check lives in this function.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key finding:** retail's `CanQualities::JumpStaminaCost` NEVER returns false
|
|
||||||
(except when `InqLoad` itself fails, which doesn't happen for a real player).
|
|
||||||
`jump_is_allowed`'s `if (!WeenieObj.JumpStaminaCost(...)) return 0x47` branch
|
|
||||||
(the "refusal" path our own `MotionInterpreter.cs` already ports verbatim,
|
|
||||||
W0-pins.md A2) is real retail *machinery*, but `CACQualities` never actually
|
|
||||||
exercises the refusing side of it. **"Refused jump" does not happen via this
|
|
||||||
mechanism in retail — only "weak jump" (see §5).** P1 ports
|
|
||||||
`JumpStaminaCost` to always return `true` with the REAL computed cost
|
|
||||||
(retiring the TS-5 zero-cost stub), matching this decomp exactly.
|
|
||||||
|
|
||||||
The `pk` flag is `PlayerKillerStatus`/`LastPkAttackTimestamp` — TS-23's
|
|
||||||
exact scope (P3, not P1). P1 hardcodes `pk: false` at the one new call site
|
|
||||||
(`PlayerWeenie.JumpStaminaCost`) and documents the dependency against TS-23
|
|
||||||
rather than re-implementing PK parsing here.
|
|
||||||
|
|
||||||
## 5. InqRunRate (0x00592800, pc 413824) / InqJumpVelocity (0x00592980, pc 413902) — FULLY READABLE
|
|
||||||
|
|
||||||
Both functions share one shape (Run uses skill id 0x18=24, Jump uses 0x16=22):
|
|
||||||
|
|
||||||
```c
|
|
||||||
InqRunRate(this, &rateOut):
|
|
||||||
load = 1.0
|
|
||||||
if (InqLoad(this, &load) == 0) return 0
|
|
||||||
|
|
||||||
currentStamina = 0
|
|
||||||
if (AttributeCache::InqAttribute2nd(attribCache, ATTR2ND_STAMINA=4, ¤tStamina) == 0)
|
|
||||||
return 0
|
|
||||||
EnchantAttribute2nd(this, 4, ¤tStamina) // vital-buff adjusts the LOCAL COPY only
|
|
||||||
// (not the wire "current stamina" state)
|
|
||||||
|
|
||||||
skill = InqSkillBaseLevel(this, SKILL_RUN=0x18) // formula-bonus + init + ranks
|
|
||||||
skill += max(PropertyInt 0x16D, 0) // LumAugAllSkills
|
|
||||||
skill += matching category augmentation ? 10 : 0 // 0x12C melee / 0x12D missile /
|
|
||||||
// 0x12E magic; exact skill-id switch
|
|
||||||
EnchantSkill(this, 0x18, &skill) // vitae * skill-enchantments, floor@0.5, truncate
|
|
||||||
if (PropertyInt 0x146 > 0) skill += 5 // Jack of All Trades
|
|
||||||
if (skill is specialized) skill += 2 * max(PropertyInt 0x158, 0)
|
|
||||||
|
|
||||||
if (currentStamina == 0) skill = 0 // THE stamina-gates-movement mechanism
|
|
||||||
|
|
||||||
*rateOut = MovementSystem::GetRunRate(load, skill, 1.0)
|
|
||||||
return 1
|
|
||||||
```
|
|
||||||
|
|
||||||
`InqJumpVelocity` is identical but for skill id 0x16=22, and finishes with
|
|
||||||
`sqrt(MovementSystem::GetJumpHeight(load, skill, extent, 1.0) * 19.6)` (pc
|
|
||||||
413975, matching `GetJumpVZ`'s existing sqrt call already in
|
|
||||||
`MotionInterpreter.cs`/`PlayerWeenie.cs` — unchanged).
|
|
||||||
|
|
||||||
**Answering the plan's question — "which skill level does retail feed?"**
|
|
||||||
Neither raw base nor a separately-cached value: retail re-derives, on every
|
|
||||||
query, `EnchantSkill(baseSkill)` where `EnchantSkill` (`CEnchantmentRegistry::
|
|
||||||
EnchantSkill` 0x005947b0, pc 416240, FULLY READABLE) is:
|
|
||||||
|
|
||||||
```c
|
|
||||||
EnchantSkill(registry, skillId, &valueInOut):
|
|
||||||
value = *valueInOut // base skill (formulaBonus+init+ranks)
|
|
||||||
if (registry._vitae != null):
|
|
||||||
value = Enchant(registry._vitae, value) // vitae multiplier FIRST
|
|
||||||
matching = CullEnchantmentsFromList(mult_list, category=SKILL=0x10, skillId)
|
|
||||||
++ CullEnchantmentsFromList(add_list, category=SKILL=0x10, skillId)
|
|
||||||
for each e in matching: value = Enchant(e, value) // per-record mult OR add
|
|
||||||
if (value < 0.5) value = 0 // floor
|
|
||||||
*valueInOut = (int)value // truncate (ftol2)
|
|
||||||
return ...
|
|
||||||
```
|
|
||||||
|
|
||||||
`CEnchantmentRegistry::EnchantAttribute2nd` (0x00594670, pc 416169, the
|
|
||||||
vitals path our `EnchantmentMath.GetMod` already ports for
|
|
||||||
`LocalPlayerState.GetMaxApprox`) applies `_vitae` in the **identical**
|
|
||||||
position (first, before the mult/add lists) — confirming our existing vitae
|
|
||||||
representation (`ActiveEnchantmentRecord.Bucket == 4`, a StatModType `Vitae`
|
|
||||||
flag `0x00800000` classified in `GameEventWiring.ClassifyLiveEnchantmentBucket`)
|
|
||||||
is the right vehicle: **P1 reuses it unmodified**, adding a sibling
|
|
||||||
`EnchantmentMath.GetSkillMod` (filtered by `StatModType & Skill(0x10) != 0`
|
|
||||||
instead of the vitals' implicit attribute2nd filter) rather than inventing a
|
|
||||||
new vitae channel. This satisfies "vitae/enchant-adjusted effective run/jump
|
|
||||||
skill... reading vitae + relevant skill enchantments from the M3
|
|
||||||
active-effect state" without a general effective-skill engine — the only new
|
|
||||||
code is the type-flag filter and the skill-id key.
|
|
||||||
|
|
||||||
**2026-07-31 #268 closeout:** the previously bounded augmentation terms are
|
|
||||||
now ported in shared `PlayerSkillMath`, after a complete read of
|
|
||||||
`CACQualities::InqSkill @ 0x00592660`. The exact order matters:
|
|
||||||
|
|
||||||
1. intrinsic formula/init/ranks;
|
|
||||||
2. positive property 0x16D plus the exact category +10 switch;
|
|
||||||
3. `EnchantSkill`;
|
|
||||||
4. property 0x146 contributes +5 when positive;
|
|
||||||
5. specialized skills receive `2 × max(property 0x158, 0)`.
|
|
||||||
|
|
||||||
The character panel and Runtime movement both consume this one Core
|
|
||||||
calculation. AP-127 is retired. The apparent current-stamina-copy residual
|
|
||||||
does not create an independently reachable effect for ordinary stat
|
|
||||||
enchantments: current and maximum stamina use distinct secondary-attribute
|
|
||||||
keys, and a max-stamina enchantment cannot turn zero current stamina nonzero.
|
|
||||||
|
|
||||||
## 6. GetRunRate / GetJumpHeight / JumpStaminaCost formula bodies (MovementSystem, pc 695958+)
|
|
||||||
|
|
||||||
`GetRunRate` (0x006b0950) and the `arg3!=0` (PK) branch of `JumpStaminaCost`
|
|
||||||
(0x006b0a40) have their GENERAL-CASE arithmetic entirely dropped by BN (only
|
|
||||||
the `EncumbranceSystem::LoadMod`/`800`-skill-compare calls and the `arg3==0`
|
|
||||||
ceil expression survive uncollapsed — the same information-loss class as the
|
|
||||||
x87 mush, just total rather than partial). **ACE is the cross-reference
|
|
||||||
tiebreaker for those two spots** (`references/ACE/Source/ACE.Server/Physics/
|
|
||||||
Animation/MovementSystem.cs`), matching this exact acdream port's ORIGINAL
|
|
||||||
citation style (`PlayerWeenie.cs`'s pre-P1 doc comments already said
|
|
||||||
"decompiled + ACE MovementSystem" for these two formulas — nothing new here,
|
|
||||||
just now with a named-decomp address alongside):
|
|
||||||
|
|
||||||
- `GetRunRate(load, skill, scaling) = skill==800 ? 18/4 : ((LoadMod(load) * (skill/(skill+200)*11) + 4) / scaling) / 4` —
|
|
||||||
**§12c correction (#266, 2026-07-30): the 800 branch is EXACT EQUALITY,
|
|
||||||
not `>=`.** Raw byte decode of 0x006b0950 (PDB-paired binary):
|
|
||||||
`fild skill; fcom [0x00803b94 = 800f]; fnstsw ax; test ah, 0x44; jp
|
|
||||||
0x6b097f` — the C2/C3 parity idiom in which `jp` (general path) fires
|
|
||||||
for `<`, `>`, AND unordered; the `fld [18f]; fdiv [4f]; ret` fall-through
|
|
||||||
executes only when C3=1/C2=0, i.e. skill == 800 exactly. The general
|
|
||||||
path decodes instruction-by-instruction to
|
|
||||||
`(LoadMod(load) * (skill/(skill+200)*11) + 4) / scaling / 4`
|
|
||||||
(constants 200f @0x00803b8c, 11f @0x00803b88, 4f @0x007c6174, /scaling
|
|
||||||
from `[esp+0xc]`, final /4f @0x00803b80). **ACE's `>= 800` "max run
|
|
||||||
speed?" reading is a misread of the same mush and must not be used as a
|
|
||||||
tiebreaker here** — it flat-lined every maxed character at 4.5 (retail
|
|
||||||
general formula gives ~3.70) and erased the vitae speed differential
|
|
||||||
(#266: 33%-vitae +Acdream visibly outran 5%-vitae +Je in acdream while
|
|
||||||
retail runs them within ~0.4%). `InqMaxRunRate`'s skill=9999 probe gets
|
|
||||||
the general formula (~3.6961), not 4.5. The true retail signature
|
|
||||||
carries a 3rd `scaling` arg (confirmed by the decomp's own function
|
|
||||||
signature), and every known call site (`InqMaxRunRate`, `InqRunRate`)
|
|
||||||
passes `1f`.
|
|
||||||
- `GetJumpHeight(load, skill, extent, scaling)` — BN's extent-clamp
|
|
||||||
micro-branch (pc 006b09b0-006b09ca) is the SAME x87-mush pattern as §3;
|
|
||||||
ACE's `Math.Clamp(extent, 0, 1)` is the tiebreaker (matches the EXISTING
|
|
||||||
acdream code, which already does this — unchanged).
|
|
||||||
`= LoadMod(load) * (skill/(skill+1300)*22.2 + 0.05) * clampedExtent / scaling`,
|
|
||||||
floored at 0.35 — matches acdream's pre-existing formula exactly.
|
|
||||||
- `JumpStaminaCost(power, load, pk)`:
|
|
||||||
- `pk==0`: `ceil((load + 0.5) * power * 8 + 2)` — **the campaign plan's
|
|
||||||
own shorthand ("ceil((power+0.5)*load*8+2)") has the `+0.5` term on the
|
|
||||||
wrong operand; the verbatim decomp (fully readable, no mush) is
|
|
||||||
`(load + 0.5) * power`, confirmed against ACE's identical
|
|
||||||
`(burden + 0.5f) * power`.**
|
|
||||||
- `pk!=0`: BN drops the body entirely (bare `_ftol2()` tailcall, no
|
|
||||||
operands survive); ACE's `(int)((power + 1.0f) * 100.0f)` is the
|
|
||||||
tiebreaker. Unused by P1 (`pk` is hardcoded `false` — see §4), ported
|
|
||||||
anyway for signature completeness/citation.
|
|
||||||
- `EncumbranceSystem::{EncumbranceCapacity, Load, LoadMod}` (0x004fcc00/40/70,
|
|
||||||
pc 256393+) — already verbatim-ported as `AcDream.Core.Items.BurdenMath`;
|
|
||||||
P1's `EncumbranceSystem` delegates (see §2).
|
|
||||||
|
|
||||||
## 7. What "weak jump" actually is (no hard refusal exists)
|
|
||||||
|
|
||||||
Given §4 (JumpStaminaCost never refuses) and §5 (stamina==0 zeroes the
|
|
||||||
EFFECTIVE skill, not the extent), the retail zero-stamina jump is:
|
|
||||||
`GetJumpHeight(load, skill=0, extent, 1) = LoadMod(load) * 0.05 * extent`,
|
|
||||||
floored to the 0.35 m minimum by the function's own clamp — i.e. **every
|
|
||||||
jump attempt, however exhausted, still produces at least the 0.35 m floor
|
|
||||||
hop.** There is no code path in `CACQualities` that makes `jump_is_allowed`
|
|
||||||
return `GeneralMovementFailure` due to low stamina. The campaign plan's
|
|
||||||
"weak/refused jump" acceptance phrasing is satisfied by "weak" (the floor
|
|
||||||
hop); "refused" does not occur via burden/stamina in this chain and P1 does
|
|
||||||
not invent it.
|
|
||||||
|
|
||||||
## 8. ReportExhaustion — wiring the dead R3-W4 seam
|
|
||||||
|
|
||||||
`ReportExhaustion()` (`MotionInterpreter.cs:1619`, already a full verbatim
|
|
||||||
port of `CMotionInterp::ReportExhaustion` 0x005288d0) has ZERO callers
|
|
||||||
anywhere in the codebase today. Retail's caller chain is
|
|
||||||
`CPhysicsObj::report_exhaustion` (0x0050fdd0) →
|
|
||||||
`MovementManager::ReportExhaustion` (0x00524360), both outside
|
|
||||||
`CMotionInterp`'s scope and not yet located precisely in the decomp
|
|
||||||
(out of P1's bounded scope to hunt down the exact upstream trigger site).
|
|
||||||
What we DO know precisely: its effect is "re-apply current movement through
|
|
||||||
the SAME dual-dispatch predicate as `apply_current_movement`" — i.e. force a
|
|
||||||
fresh `WeenieObj.InqRunRate`/`InqJumpVelocity` query against the CURRENT
|
|
||||||
physics/interpreted state, with no new input event.
|
|
||||||
|
|
||||||
That is exactly the primitive needed to make a live burden/stamina/vitae
|
|
||||||
change visible immediately (mid-run, mid-charge) instead of waiting for the
|
|
||||||
next keypress. **P1 wires `ReportExhaustion()` as the "re-evaluate movement
|
|
||||||
now" trampoline any time Runtime pushes a fresh burden, stamina, or
|
|
||||||
vitae-adjusted-skill value into the active `PlayerMovementController`** —
|
|
||||||
plausible given `ReportExhaustion`'s documented purpose, and the least
|
|
||||||
speculative real consumer available for a seam that otherwise never fires.
|
|
||||||
|
|
||||||
## 9. Design: where each input is computed and pushed
|
|
||||||
|
|
||||||
```
|
|
||||||
Runtime (AcDream.Runtime, presentation-free):
|
|
||||||
RuntimeCharacterState
|
|
||||||
- Spellbook (existing) -- vitae + skill enchantments live here
|
|
||||||
- MovementSkills: RuntimeMovementSkillState (existing, EXTENDED)
|
|
||||||
RunSkill / JumpSkill -- now the ADJUSTED (EnchantSkill'd) values
|
|
||||||
Burden (float, new) -- InqLoad's load ratio
|
|
||||||
CurrentStamina (int, new, -1 sentinel = unknown/don't-gate)
|
|
||||||
- _runSkillBase / _jumpSkillBase (new, private) -- pre-EnchantSkill values
|
|
||||||
- UpdateMovementSkillBase(runBase, jumpBase) -- stores base, recomputes+pushes adjusted
|
|
||||||
- RecomputeMovementSkills() -- base * EnchantmentMath.GetSkillMod(skillId), floor/round
|
|
||||||
- wired: Spellbook.EnchantmentsChanged -> RecomputeMovementSkills (vitae/buff changes
|
|
||||||
recompute WITHOUT a fresh PD skill push)
|
|
||||||
|
|
||||||
LiveSessionEventRouter.Attach() (cross-owner wiring hub; already the home
|
|
||||||
of the existing onSkillsUpdated -> MovementSkills.Update plumbing)
|
|
||||||
- onSkillsUpdated callback -> character.Character.UpdateMovementSkillBase(...)
|
|
||||||
- NEW: inventory.Objects.{ObjectAdded,ObjectUpdated,ObjectRemoved,ObjectMoved,
|
|
||||||
ContainerContentsReplaced,Cleared} + LocalPlayer.AttributeChanged(Strength)
|
|
||||||
+ Spellbook.EnchantmentsChanged
|
|
||||||
-> recompute burden (Strength + prop 0xE6 aug + prop 5 EncumbranceVal,
|
|
||||||
SAME shape as IndicatorBarController.UpdateBurden/InventoryController.RefreshBurden)
|
|
||||||
using effective/enchantment-adjusted Strength
|
|
||||||
-> character.Character.MovementSkills.UpdateBurden(ratio)
|
|
||||||
- NEW: character.Character.LocalPlayer.Changed(VitalKind.Stamina)
|
|
||||||
-> character.Character.MovementSkills.UpdateStamina(current)
|
|
||||||
- all three trigger points additionally invoke the new
|
|
||||||
LiveCharacterSessionBindings.OnMovementStatsUpdated callback
|
|
||||||
|
|
||||||
RuntimeMovementSkillProjection.ApplyTo(skills, controller) (existing seam,
|
|
||||||
called at construction AND reactively from OnSkillsUpdated/OnMovementStatsUpdated)
|
|
||||||
- SetCharacterSkills(run, jump) (existing)
|
|
||||||
- NEW: SetCharacterBurden(burden), SetCharacterStamina(stamina)
|
|
||||||
|
|
||||||
App (LiveSessionRuntimeFactory) / Headless (HeadlessSessionHost):
|
|
||||||
- OnMovementStatsUpdated: App wires ApplyTo(...) + controller.Motion.ReportExhaustion()
|
|
||||||
(mirrors the existing OnSkillsUpdated body, which P1 ALSO extends with
|
|
||||||
the ReportExhaustion() call for consistency); Headless passes null,
|
|
||||||
matching its existing OnSkillsUpdated: null (headless bots don't need
|
|
||||||
live mid-session re-apply to a controller that may not exist yet).
|
|
||||||
|
|
||||||
Core (AcDream.Core.Physics, presentation-free, pure):
|
|
||||||
EncumbranceSystem -- EncumbranceCapacity/Load/LoadMod, delegates to BurdenMath
|
|
||||||
MovementSystem -- GetRunRate/GetJumpHeight/JumpStaminaCost/GetJumpPower
|
|
||||||
PlayerWeenie (CACQualities-shaped)
|
|
||||||
_burden (float), _currentStamina (int?, null=unknown) -- pushed via
|
|
||||||
SetBurden/SetStamina (SetBurden already existed, wires the dead setter)
|
|
||||||
_runSkill/_jumpSkill (int) -- pushed via SetSkills, ALREADY vitae/enchant-
|
|
||||||
adjusted by Runtime before it arrives here (PlayerWeenie itself stays
|
|
||||||
a pure formula consumer -- no Spellbook/enchantment dependency, keeping
|
|
||||||
it trivially testable)
|
|
||||||
CanJump(extent) -> _burden < 2.0 (UN-8 polarity, §3)
|
|
||||||
JumpStaminaCost(extent, out cost)
|
|
||||||
-> cost = MovementSystem.JumpStaminaCost(extent, _burden, pk:false);
|
|
||||||
return true; (§4 -- always true, TS-23 owns pk)
|
|
||||||
InqRunRate(out rate) -> effSkill = _currentStamina == 0 ? 0 : _runSkill;
|
|
||||||
rate = MovementSystem.GetRunRate(_burden, effSkill, 1f);
|
|
||||||
InqJumpVelocity(extent, out vz)
|
|
||||||
-> effSkill = _currentStamina == 0 ? 0 : _jumpSkill;
|
|
||||||
vz = sqrt(MovementSystem.GetJumpHeight(_burden, effSkill, extent, 1f) * 19.6f);
|
|
||||||
```
|
|
||||||
|
|
||||||
`_currentStamina == null` (never set — matches every existing test /
|
|
||||||
call site that doesn't call `SetStamina`) never zeroes the skill, preserving
|
|
||||||
every pre-P1 `PlayerWeenieTests.cs` expectation unchanged.
|
|
||||||
|
|
||||||
## 10. Register bookkeeping (same commit as the port)
|
|
||||||
|
|
||||||
- **Delete TS-5** (`CanJump` always true / `JumpStaminaCost` zero-cost stub) —
|
|
||||||
retired: both now real, decomp-cited.
|
|
||||||
- **Delete AP-25** (run/jump skill = attributeBonus+init+ranks only, no
|
|
||||||
vitae) — retired: vitae now flows through `EnchantmentMath.GetSkillMod`.
|
|
||||||
- **TS-21 untouched** — still valid (pre-PD fallback defaults 200/300 are a
|
|
||||||
separate divergence, not addressed by P1).
|
|
||||||
- **TS-23 extended** (not a new row) — its "PlayerKillerStatus not parsed"
|
|
||||||
scope now also covers the new `MovementSystem.JumpStaminaCost` `pk`
|
|
||||||
parameter, hardcoded `false` at the `PlayerWeenie` call site pending P3.
|
|
||||||
- **AP-127 retired 2026-07-31 (#268)** — the complete 0x16D/category/
|
|
||||||
0x146/0x158 chain is shared by panel and movement (§5 closeout).
|
|
||||||
- **New UN-8** — `CACQualities::CanJump`'s x87 comparison polarity resolved
|
|
||||||
by domain plausibility rather than a literal BN read (§3); Ghidra MCP
|
|
||||||
confirmation is the retire path.
|
|
||||||
|
|
||||||
## 11. Test plan
|
|
||||||
|
|
||||||
- `MovementSystemTests` (new, Core): golden tables for `GetRunRate` (0/200/
|
|
||||||
800 skill, load knees), `GetJumpHeight` (extent 0/0.5/1, 0.35 floor,
|
|
||||||
load knees), `JumpStaminaCost` (ceil rounding, load/power sweep),
|
|
||||||
`GetJumpPower` (inverse sanity, not consumed by P1 but ported for
|
|
||||||
signature completeness / future charge-meter work).
|
|
||||||
- `EncumbranceSystemTests` (new, Core): capacity at 100%/200% aug clamp,
|
|
||||||
Load ratio, LoadMod knees — cross-checked 1:1 against the EXISTING
|
|
||||||
`BurdenMath` tests (same formulas, must agree bit-for-bit).
|
|
||||||
- `PlayerWeenieTests` (extend): CanJump refusal at load>=2.0 / allowed
|
|
||||||
below; JumpStaminaCost real nonzero cost; InqRunRate/InqJumpVelocity
|
|
||||||
zero at stamina==0 (skill forced to 0, still floors at 0.35 m for jump);
|
|
||||||
ALL pre-existing tests must stay green unmodified (no SetStamina call ->
|
|
||||||
null sentinel -> no gating, exactly today's behavior).
|
|
||||||
- `EnchantmentMathTests` (extend): `GetSkillMod` type-flag filtering
|
|
||||||
(Skill-flagged records match; vital-only records with a colliding
|
|
||||||
numeric key do NOT), vitae-first-then-mult-then-add ordering.
|
|
||||||
- `RuntimeCharacterStateTests` / `RuntimeMovementSkillStateTests` (Runtime):
|
|
||||||
burden/stamina push + revision bump; `RecomputeMovementSkills` fires on
|
|
||||||
`Spellbook.EnchantmentsChanged` without a fresh base push; ResetSession
|
|
||||||
convergence includes Burden==0/CurrentStamina==-1.
|
|
||||||
- `LiveSessionEventRouterTests` (Runtime, if a harness exists) or a focused
|
|
||||||
new test: ObjectTable burden-trigger events recompute and push burden;
|
|
||||||
Stamina vital change pushes CurrentStamina.
|
|
||||||
|
|
||||||
## 12. P1 Opus-review addenda (2026-07-30, post-implementation)
|
|
||||||
|
|
||||||
### 12a. UN-8 RETIRED — CanJump polarity byte-proven
|
|
||||||
|
|
||||||
Raw bytes of `CACQualities::CanJump @ 0x00591b50` in the PDB-paired
|
|
||||||
v11.4186 binary (technique: `reference_pe_byte_decode`):
|
|
||||||
|
|
||||||
```
|
|
||||||
e8 ca d5 ff ff call InqLoad (0x0058f130)
|
|
||||||
85 c0 / 74 1a test eax,eax; jz return0 ; load unknowable -> 0
|
|
||||||
d9 44 24 00 fld dword [esp] ; st0 = load
|
|
||||||
d8 1d 24 5e 7c 00 fcomp dword [0x007c5e24] ; vs 2.0f (verified read)
|
|
||||||
df e0 fnstsw ax
|
|
||||||
f6 c4 05 test ah, 0x05 ; C0|C2
|
|
||||||
7a 09 jp return0 ; PF=1 on {neither, both}
|
|
||||||
b8 01 00 00 00 mov eax, 1 ; fall-through: C0 only
|
|
||||||
```
|
|
||||||
|
|
||||||
`test ah,5` result parity: `0x00` (load ≥ 2.0, incl. ==) → PF=1 → 0;
|
|
||||||
`0x01` (load < 2.0) → PF=0 → 1; `0x05` (unordered) → PF=1 → 0.
|
|
||||||
**`CanJump = (load < 2.0f)`; NaN/unordered refuses.** The shipped
|
|
||||||
`_burden < CanJumpLoadThreshold` matches exactly, including the NaN edge.
|
|
||||||
|
|
||||||
### 12b. PK-timer jump-cost semantics (for Slice P3 / TS-23)
|
|
||||||
|
|
||||||
`CACQualities::JumpStaminaCost @ 0x00591b90` (pc 412934-412968), fully
|
|
||||||
readable: the `pk` flag passed to `MovementSystem::JumpStaminaCost` is
|
|
||||||
|
|
||||||
```
|
|
||||||
pk = InqInt(0x86 /*134 PlayerKillerStatus*/, default 8) in {4 /*PK*/, 0x40 /*PKLite*/}
|
|
||||||
&& InqFloat(0x91 /*145*/) succeeded
|
|
||||||
&& (that_float + 20.0) >= Timer::cur_time
|
|
||||||
```
|
|
||||||
|
|
||||||
i.e. PK/PKLite status AND a 20-second recency window on PropertyFloat
|
|
||||||
0x91. The P3 implementer should plumb exactly this pair alongside the
|
|
||||||
mover-flag work; `MovementSystem.JumpStaminaCost`'s pk branch
|
|
||||||
(`(int)((power + 1) * 100)`, ACE-derived — the retail branch is an
|
|
||||||
elided `_ftol2` tailcall) is already in place.
|
|
||||||
|
|
@ -1,741 +0,0 @@
|
||||||
# TS-4 / #116 oracle pass — Campaign P final physics slice
|
|
||||||
|
|
||||||
**Status: RESEARCH ONLY. No source changes.** This is a follow-up oracle
|
|
||||||
pass on top of `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md`
|
|
||||||
(hereafter "the P2 doc"), specifically its §4 (TS-4), §5 (#116), and §7
|
|
||||||
item 6 (the P2 implementation attempt's wedge diagnosis). That attempt
|
|
||||||
correctly localized the freeze to `TransitionalInsert`'s Phase 2 retry
|
|
||||||
loop but concluded the mechanism was "Phase 3 structurally unreachable"
|
|
||||||
without tracing far enough to find the actual convergence/divergence
|
|
||||||
point. This pass reads one layer deeper — into `BSPQuery.cs`'s `Path 4`
|
|
||||||
dispatch (the `path.Collide` gate) and `AdjustOffset`'s crease-projection
|
|
||||||
math — and finds a concrete, retail-decomp-cited mechanism for both TS-4
|
|
||||||
and (as a byproduct of reading the same dispatch structure) strong new
|
|
||||||
evidence for #116 shapes 1 and 2.
|
|
||||||
|
|
||||||
Every claim is tagged **FACT** (read directly from the named-retail
|
|
||||||
pseudo-C, ACE source, or current acdream source in this worktree, with
|
|
||||||
file:line / address citations) or **INFERENCE** (derived from those FACTs
|
|
||||||
by direct reasoning, not yet confirmed by a live capture/cdb run).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Binding DO-NOT-RETRY entries (copied verbatim)
|
|
||||||
|
|
||||||
From `memory/project_physics_collision_digest.md` (3-day-old snapshot,
|
|
||||||
re-verified against current source where cited below) and
|
|
||||||
`docs/ISSUES.md` #116:
|
|
||||||
|
|
||||||
1. **Do NOT add `SetSlidingNormal` calls in the BSP/sphere collision
|
|
||||||
layer.** Retail's only in-transition writer of
|
|
||||||
`collision_info.sliding_normal` is `validate_transition`
|
|
||||||
(`0x0050ac21`/`0x0050aa70`). A leaked normal + success writeback = an
|
|
||||||
absorbing wedge at empty space. **This pass's TS-4 finding is a
|
|
||||||
variant of exactly this failure class — see §1 below — but the
|
|
||||||
writer in question (`validate_transition`'s unconditional
|
|
||||||
`SetSlidingNormal(CollisionNormal)`) IS the retail-faithful one; the
|
|
||||||
problem is not an extra writer, it's what `AdjustOffset` does with a
|
|
||||||
*placeholder* `UnitZ` value when it reads `SlidingNormal` back.**
|
|
||||||
2. **Do NOT re-add a forced constant-shell de-penetration.** Retail
|
|
||||||
slides tangentially and never force-separates.
|
|
||||||
3. **`SphereCollision` no longer calls `SetSlidingNormal`** (TS-45
|
|
||||||
retired) — keep it that way.
|
|
||||||
4. **Do NOT patch the degenerate-offset guard in `slide_sphere` ad
|
|
||||||
hoc** for #116 — oracle-driven only.
|
|
||||||
5. **Do NOT re-introduce a topology-based outside-add / radial sweep**
|
|
||||||
to cell membership while touching this family.
|
|
||||||
6. **`calc_friction` threshold is retail 0.25 vs acdream 0.0` (AP-7)** —
|
|
||||||
orthogonal to this slice, do not fold in.
|
|
||||||
7. **Shape-1 of #116 is NOT the degenerate-offset guard threshold** —
|
|
||||||
that guard kills slides under ~1.4 cm; the lost tick-22760 slide was
|
|
||||||
3.57 cm. The divergence is the collision-normal SOURCE.
|
|
||||||
8. **Do NOT guess the BN `test ah,5` x87 branch polarity/squaring** in
|
|
||||||
`slide_sphere` — Ghidra MCP is down for this pass too; this pass does
|
|
||||||
**not** touch that question (see §3, shape-2 — the finding here is
|
|
||||||
about dispatch *routing*, not the x87 comparisons inside
|
|
||||||
`slide_sphere`/`AdjustOffset` themselves, which remain unconfirmed
|
|
||||||
and out of scope).
|
|
||||||
9. **AP-4 (CliffSlide check moved before retail's Branch-1 gate)** — a
|
|
||||||
live, load-bearing reordering. Not touched by this pass.
|
|
||||||
10. **TS-46 (two-scalar sphere reconstruction) is OUT OF SCOPE.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. TS-4 — the actual convergence/divergence mechanism
|
|
||||||
|
|
||||||
### 1.1 Summary answer (read this first)
|
|
||||||
|
|
||||||
**Retail does not "avoid" the Adjusted↔retry oscillation inside
|
|
||||||
`transitional_insert`'s attempt loop any differently than acdream does —
|
|
||||||
both structurally deadlock the same way within a single resolve.** What
|
|
||||||
lets retail's *live* trace escape (and what the P2 fixture's synthetic
|
|
||||||
trajectory does not) is that **retail's `AdjustOffset`
|
|
||||||
(`CTransition::adjust_offset`, `0x0050a370`) re-projects the *next
|
|
||||||
tick's* gravity offset through whatever `ContactPlane` +
|
|
||||||
`SlidingNormal` survived the previous tick's collision — and for a
|
|
||||||
pure, zero-horizontal-velocity vertical fall onto a steep surface, that
|
|
||||||
projection is mathematically degenerate and crushes the offset to
|
|
||||||
(near-)zero every tick, which abort-small-offsets before
|
|
||||||
`TransitionalInsert` even runs again.** This is retail-faithful
|
|
||||||
behavior, present identically in the raw decomp, in ACE's port, and in
|
|
||||||
acdream's current port — it is not a bug introduced by the TS-4
|
|
||||||
shortcut's removal. The `Ts4SteepRoofWedgeCaptureTests` fixture
|
|
||||||
reproduces it because it drops the body **straight down with zero
|
|
||||||
horizontal velocity**, which is very likely a different (and more
|
|
||||||
degenerate) input than the live 2026-04-30 debugger trace that
|
|
||||||
validated the shortcut (a player *jumping or running* onto a roof,
|
|
||||||
which has residual horizontal velocity).
|
|
||||||
|
|
||||||
### 1.2 The chain, FACT by FACT
|
|
||||||
|
|
||||||
**Step A — Path 6 fires, sets `Collide`, does not reposition (FACT).**
|
|
||||||
`BSPQuery.cs:2217-2224` (faithful branch, shortcut removed):
|
|
||||||
```csharp
|
|
||||||
path.SetCollide(worldNormal0);
|
|
||||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
|
||||||
return TransitionState.Adjusted;
|
|
||||||
```
|
|
||||||
`SpherePath.SetCollide` (`TransitionTypes.cs:752-759`) only sets
|
|
||||||
`Collide=true`, backs up `CheckPos`, and stores `StepUpNormal` — it does
|
|
||||||
**not** touch `CollisionInfo.ContactPlane` or `CollisionNormal`. Matches
|
|
||||||
retail exactly: pseudo-C:323818-323821 (`0x0053a7bf`,
|
|
||||||
`SPHEREPATH::set_collide(&sphere_path, &normal); walkable_allowance =
|
|
||||||
0.0871556997f; return 3;`) — no `set_collision_normal`, no
|
|
||||||
`set_contact_plane` call at this site either.
|
|
||||||
|
|
||||||
**Step B — the SAME attempt's retry does NOT re-hit Path 6; it routes to
|
|
||||||
Path 4 (FACT, both acdream and retail).** `BSPQuery.cs:1961` gates on
|
|
||||||
`if (path.Collide)` — checked **before** the Path 5/6 tests, at the top
|
|
||||||
of the same dispatch function. Since `Collide` was just set in Step A
|
|
||||||
and is **never cleared** except inside `TransitionalInsert`'s Phase 3
|
|
||||||
(`sp.Collide = false` at `TransitionTypes.cs:1816`, reachable only on an
|
|
||||||
`OK` result — never reached while Path 6/Path 4 keep returning
|
|
||||||
`Adjusted`), every subsequent attempt (within the same resolve **and**
|
|
||||||
across ticks) dispatches to Path 4, not back to Path 6. Retail: raw
|
|
||||||
pseudo-C:323784 `if (eax->sphere_path.collide == 0) {...} else {...}` —
|
|
||||||
the identical gate, at the identical position in the dispatch (confirmed
|
|
||||||
independently against ACE `BSPTree.cs:163-187`, `if (path.Collide) {
|
|
||||||
RootNode.find_walkable(...); if (changed) {... return Adjusted;} else
|
|
||||||
return OK; }`).
|
|
||||||
|
|
||||||
**Step C — Path 4 (`FindWalkableInternal`) is what actually establishes
|
|
||||||
`ContactPlaneValid` (FACT).** `BSPQuery.cs:1968-2018`: calls
|
|
||||||
`FindWalkableInternal`; if it finds a candidate (`changed &&
|
|
||||||
hitPoly is not null`), it **repositions** the sphere
|
|
||||||
(`path.AddOffsetToCheckPos(worldOffset)`), sets a **real**
|
|
||||||
`ContactPlane` via `collisions.SetContactPlane(worldPlane, ...)`
|
|
||||||
(line 2006), caches the walkable polygon (`SetWalkableTransformed`), and
|
|
||||||
returns `Adjusted`. This is the only site that gives the mover a real
|
|
||||||
(steep) contact plane in this whole trajectory — **not** the Phase-3
|
|
||||||
`DoCheckWalkable` gate the P2 doc's item-6 diagnosis assumed was the
|
|
||||||
relevant site (that gate is downstream and, per Step B, unreachable
|
|
||||||
here). Matches ACE `BSPTree.cs:163-184` exactly (`SetContactPlane`,
|
|
||||||
`SetWalkable`, `return Adjusted`).
|
|
||||||
|
|
||||||
**Step D — the attempt-exhausted `Adjusted` gets collapsed to `OK` with
|
|
||||||
position reverted, but `ContactPlaneValid` survives the revert (FACT,
|
|
||||||
both engines).** `TransitionalInsert`'s outer for-loop exhausts (acdream
|
|
||||||
hardcodes `return TransitionState.Slid;` at `TransitionTypes.cs:2093`;
|
|
||||||
ACE/retail return the true last value, `Adjusted` here — see §1.4 for why
|
|
||||||
this particular divergence doesn't change the outcome). Either way,
|
|
||||||
`ValidateTransition`'s "not OK" branch runs
|
|
||||||
(`TransitionTypes.cs:5493-5501`): `if (!CollisionNormalValid)
|
|
||||||
SetCollisionNormal(UnitZ);` (fires — Path 4/6 never touched
|
|
||||||
`CollisionNormal`, only `ContactPlane`/`StepUpNormal`), then
|
|
||||||
`SetCheckPos(CurPos, CurCellId)` (revert — no net movement),
|
|
||||||
`transitionState = OK`. Retail: pseudo-C:272563-272596 (`0x0050aad9`),
|
|
||||||
identical collapse (`COLLIDED_TS`/`ADJUSTED_TS`/`SLID_TS` all treated the
|
|
||||||
same, default `CollisionNormal=UnitZ` if unset, revert `check_pos` to
|
|
||||||
`curr_pos`). **Crucially, none of this touches `ContactPlaneValid`** — it
|
|
||||||
carries forward from Step C untouched by the revert. Then the shared
|
|
||||||
tail (`TransitionTypes.cs:5504-5533`, retail pc:272621-272656) runs:
|
|
||||||
`if (CollisionNormalValid) SetSlidingNormal(CollisionNormal)` — now
|
|
||||||
**`SlidingNormal = UnitZ`** (the placeholder from the default, not a
|
|
||||||
real second surface) — and `if (ContactPlaneValid) { ...;
|
|
||||||
oi.State|=Contact; if (Normal.Z>=FloorZ) OnWalkable=true else false; }`
|
|
||||||
— since the steep polygon's `Normal.Z` (≈0.447 for the fixture's 63.4°
|
|
||||||
slope) `< FloorZ` (≈0.664), `OnWalkable` stays **false** but `Contact`
|
|
||||||
becomes **true**. **This exactly reproduces the fixture's own captured
|
|
||||||
state at the landing tick: `InContact=true, OnWalkable=false`.**
|
|
||||||
|
|
||||||
**Step E — the NEXT tick's `AdjustOffset` crushes a purely-vertical
|
|
||||||
offset to zero (FACT for the math, INFERENCE that this is the actual
|
|
||||||
observed freeze cause — not independently re-run this pass).**
|
|
||||||
`TransitionTypes.cs:4936-5014` (acdream), `Transition.cs:34-87` (ACE),
|
|
||||||
pseudo-C:272271-272393 (`0x0050a370`, retail) are all structurally
|
|
||||||
identical:
|
|
||||||
```
|
|
||||||
slidingAngle = Dot(offset, SlidingNormal)
|
|
||||||
if (SlidingNormalValid) { if (slidingAngle < 0) checkSlide = true; else SlidingNormalValid = false; }
|
|
||||||
...
|
|
||||||
if (checkSlide) {
|
|
||||||
slideOffset = Cross(ContactPlane.Normal, SlidingNormal)
|
|
||||||
normalize slideOffset (or zero out if degenerate)
|
|
||||||
result = Dot(slideOffset, offset) * slideOffset
|
|
||||||
}
|
|
||||||
```
|
|
||||||
With `offset = (0, 0, -dz)` (pure gravity, zero horizontal component),
|
|
||||||
`SlidingNormal = UnitZ = (0,0,1)`: `slidingAngle = -dz < 0` →
|
|
||||||
`checkSlide = true`. `slideOffset = Cross(ContactPlane.Normal, UnitZ)` —
|
|
||||||
for any non-vertical plane normal `N=(Nx,Ny,Nz)`, this cross product is
|
|
||||||
`(Ny, -Nx, 0)` — a **horizontal** vector (Z=0), lying in the slope's
|
|
||||||
*contour* line (perpendicular to the downhill direction), **not the
|
|
||||||
degenerate/near-zero case** (the 63.4° slope's normal is not parallel to
|
|
||||||
UnitZ, so `NormalizeCheckSmall` does not fire). `Dot(slideOffset,
|
|
||||||
offset) = Dot((Ny,-Nx,0), (0,0,-dz)) = 0` exactly, because
|
|
||||||
`slideOffset.Z = 0` and `offset` is purely `Z`. **`result = 0 *
|
|
||||||
slideOffset = Vector3.Zero`.** The projected `GlobalOffset` is zero (up
|
|
||||||
to float noise), which trips the "abort-small-offset" guard
|
|
||||||
(`TransitionTypes.cs:1466-1478`, retail's non-viewer `|offset|² <
|
|
||||||
F_EPSILON²` gate at pseudo-C:272845/`0x0050bdf0`, cited already in the
|
|
||||||
existing `AdjustOffset` port comment) **before `TransitionalInsert` is
|
|
||||||
even called again** — so `ValidateTransition` never runs on subsequent
|
|
||||||
ticks either, meaning the stale `ContactPlaneValid`/`SlidingNormal=UnitZ`
|
|
||||||
state simply perpetuates unchanged, forever. This is the freeze.
|
|
||||||
|
|
||||||
**Step F — why the existing frames_stationary_fall (fsf) escape valve
|
|
||||||
can't rescue this case (INFERENCE, follows directly from Step E).** The
|
|
||||||
digest's #182 rebuild already ported retail's fsf ladder
|
|
||||||
(`TransitionTypes.cs:5625-5667`, ACE `Transition.cs:1029-1061`,
|
|
||||||
pseudo-C:272625-656) — after 3 consecutive non-advancing ticks it
|
|
||||||
manufactures a flat `UnitZ` contact plane and forces `OnWalkable=true`,
|
|
||||||
which is exactly the kind of "unstick" mechanism one would look for
|
|
||||||
here. **But that ladder lives inside `ValidateTransition`, which Step
|
|
||||||
E's abort-small-offset guard prevents from ever running again** once the
|
|
||||||
crease projection first crushes the offset to zero. The rescue mechanism
|
|
||||||
is downstream of a gate the degenerate input never lets execution
|
|
||||||
reach — in both acdream and (per identical source) retail.
|
|
||||||
|
|
||||||
### 1.3 Why this reconciles the shortcut's own "retail did not wedge" comment (INFERENCE)
|
|
||||||
|
|
||||||
The shortcut's comment (`BSPQuery.cs:2190-2199`) says the interim fix was
|
|
||||||
"Validated against retail debugger trace 2026-04-30: retail body did not
|
|
||||||
wedge." A live player jumping or walking onto a roof virtually always
|
|
||||||
carries **some** horizontal velocity component (WASD input, residual
|
|
||||||
momentum). For a non-purely-vertical `offset`, `Dot(slideOffset, offset)`
|
|
||||||
is generally **non-zero** (only a component exactly along the pure
|
|
||||||
downhill/gravity line is annihilated by this specific cross product —
|
|
||||||
any lateral drift survives), so `AdjustOffset` would produce a small but
|
|
||||||
non-zero *sideways* offset each tick — enough to move the sphere off the
|
|
||||||
exact same collision point, avoid the abort-small-offset short-circuit,
|
|
||||||
let `TransitionalInsert`/`ValidateTransition` run again, and (via
|
|
||||||
repeated Path-4 `find_walkable` re-probes and the fsf ladder) eventually
|
|
||||||
resolve. **The `Ts4SteepRoofWedgeCaptureTests` fixture's `pos =
|
|
||||||
(0.5, 0, 3.0)` straight-down drop with `fallVelocityZ` as the only
|
|
||||||
non-zero component is very likely a stricter, more degenerate input than
|
|
||||||
the live 2026-04-30 repro ever exercised.** This is not yet independently
|
|
||||||
re-confirmed by re-running the fixture with a horizontal component (see
|
|
||||||
§4 Step 1 below for the concrete next action), so it is flagged
|
|
||||||
INFERENCE — but it is the only hypothesis consistent with every FACT
|
|
||||||
gathered in §1.2, and it does not require inventing any new mechanism.
|
|
||||||
|
|
||||||
### 1.4 The acdream-only bug that does NOT explain the freeze, but is real and should still be fixed
|
|
||||||
|
|
||||||
`TransitionTypes.cs:2091-2093`:
|
|
||||||
```csharp
|
|
||||||
// Exhausted retry attempts — return whatever the last iteration said.
|
|
||||||
// (Defaults to Slid in practice since that's the only case that retries.)
|
|
||||||
return TransitionState.Slid;
|
|
||||||
```
|
|
||||||
This is **hardcoded**, not "whatever the last iteration said" as the
|
|
||||||
comment claims. ACE's equivalent (`Transition.cs:933`, `return
|
|
||||||
transitState;`) and retail's (pseudo-C:273363, `0x0050b949`, `return
|
|
||||||
edi;`) both return the **true** last value — `Adjusted` in this
|
|
||||||
scenario, not `Slid`. **FACT: this is a real, citable divergence.**
|
|
||||||
**FACT: it does not explain the freeze** — `ValidateTransition`'s
|
|
||||||
"not OK" branch (§1.2 Step D) treats `Collided`/`Adjusted`/`Slid`
|
|
||||||
**identically** (acdream `TransitionTypes.cs:5493-5501`, ACE
|
|
||||||
`Transition.cs:993-1017`, retail pseudo-C:272563-272596 all gate on
|
|
||||||
`result > OK_TS && result <= SLID_TS` as one combined range, with no
|
|
||||||
per-value branching). Fixing the hardcoded return is a one-line,
|
|
||||||
zero-risk correctness fix (worth doing — it's a real citable
|
|
||||||
port-accuracy bug and prevents future confusion when tracing this loop)
|
|
||||||
but it is **not** the TS-4 fix and should not be presented as one.
|
|
||||||
|
|
||||||
### 1.5 What TS-4's actual fix shape is, given this
|
|
||||||
|
|
||||||
The mechanism in §1.2 is **not something `BSPQuery.cs`'s Path 6 can fix
|
|
||||||
by itself** — the freeze happens one tick *after* Path 6/Path 4 run,
|
|
||||||
inside `AdjustOffset`, and is a property of the (already retail-faithful)
|
|
||||||
`validate_transition` + `adjust_offset` pipeline reacting to a specific
|
|
||||||
degenerate trajectory. Concretely, TS-4's shortcut removal is very
|
|
||||||
likely **safe for the realistic case** (nonzero horizontal velocity) and
|
|
||||||
only exposes this specific zero-horizontal-velocity degenerate, which:
|
|
||||||
- may be a genuine, narrow, retail-faithful edge case (a player falling
|
|
||||||
perfectly plumb onto a slope with zero horizontal drift essentially
|
|
||||||
never happens in live play — WASD input, camera-relative movement, and
|
|
||||||
even tiny numerical noise almost always inject some horizontal
|
|
||||||
component), in which case it is not a blocker for TS-4 at all and
|
|
||||||
should be documented as an accepted (retail-matching) corner case
|
|
||||||
rather than "fixed", **or**
|
|
||||||
- may indicate the fixture itself should be revised to match the
|
|
||||||
original live repro's actual trajectory shape (nonzero horizontal
|
|
||||||
velocity) before it's trusted as TS-4's gating fixture.
|
|
||||||
|
|
||||||
See §4 for the concrete, low-cost verification step (re-run the fixture
|
|
||||||
with a small horizontal velocity component) that would settle which of
|
|
||||||
these is true without guessing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. #116 shape-1 — collision-normal recording divergence (new candidate, INFERENCE, needs one instrumentation run to confirm)
|
|
||||||
|
|
||||||
### 2.1 What the existing research already ruled out (FACT, restated)
|
|
||||||
|
|
||||||
Ghidra-confirmed (2026-06-12, digest lines 1268-1275): acdream's
|
|
||||||
`cn=UnitZ` default on a blocked move **is** retail-faithful
|
|
||||||
(`validate_transition` does the identical default). The real divergence
|
|
||||||
is **upstream** — at tick-22760, acdream's `collision_normal_valid` was
|
|
||||||
`false` where retail's was `true` (retail had recorded the door-face
|
|
||||||
normal `(0,+1,0)`). The candidate site named in the P2 doc §5 was "the
|
|
||||||
`PathClipped`/`collide_with_pt` arm... or a sibling Path-1-class function
|
|
||||||
not yet read."
|
|
||||||
|
|
||||||
### 2.2 PathClipped is NOT the answer (checked this pass, negative result — FACT)
|
|
||||||
|
|
||||||
`ObjectInfoState.PathClipped` (`TransitionTypes.cs:32`, bit `0x8`) is
|
|
||||||
only set on a mover when `MoverPhysicsState & PhysicsStateFlags.Missile
|
|
||||||
!= 0` (`PhysicsEngine.cs:1160-1163`), with an explicit citation to
|
|
||||||
retail's own `CPhysicsObj::get_object_info` (`0x00511CC0`): "Missile
|
|
||||||
contributes PathClipped only." A normal player push against a door is
|
|
||||||
not a missile mover, so **neither acdream nor retail would set
|
|
||||||
PathClipped for this scenario** — this rules out "PathClipped state
|
|
||||||
differs between engines" as shape-1's cause. (The camera/viewer sweep
|
|
||||||
does carry PathClipped via a different, explicit caller-supplied flag,
|
|
||||||
but that's a different mover than the one in the tick-22760 door-push
|
|
||||||
capture.)
|
|
||||||
|
|
||||||
### 2.3 The real candidate: acdream's Path-6 sphere1(head)-hit handling diverges from retail/ACE (FACT for the divergence, INFERENCE that it explains tick-22760)
|
|
||||||
|
|
||||||
Retail's `BSPTREE::find_collisions`, in the **not-yet-in-Contact**
|
|
||||||
branch (`state&1==0`, i.e. airborne / first contact — pseudo-C:323784-
|
|
||||||
323836, `0x0053a4e3`-`0x0053a730`+): when sphere0 (foot) does **not**
|
|
||||||
hit but `num_sphere > 1` and sphere1 (head) **does** hit, retail does
|
|
||||||
**not** defer through `SetCollide`/`Adjusted` — it calls
|
|
||||||
`COLLISIONINFO::set_collision_normal` **directly** with the head poly's
|
|
||||||
transformed normal and returns `COLLIDED_TS` (`2`) immediately
|
|
||||||
(pseudo-C:323824-323834, `0x0053a793`/`0x0053a7a4`). Cross-checked
|
|
||||||
independently against ACE `BSPTree.cs:221-230`:
|
|
||||||
```csharp
|
|
||||||
else if (path.NumSphere > 1)
|
|
||||||
{
|
|
||||||
if (RootNode.sphere_intersects_poly(localSphere_, movement, ref hitPoly, ref contactPoint) || hitPoly != null)
|
|
||||||
{
|
|
||||||
var collisionNormal = path.LocalSpacePos.LocalToGlobalVec(hitPoly.Plane.Normal);
|
|
||||||
collisions.SetCollisionNormal(collisionNormal);
|
|
||||||
return TransitionState.Collided;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
— an exact structural match to the raw decomp, confirming this is not a
|
|
||||||
BN misdecompile.
|
|
||||||
|
|
||||||
**acdream's corresponding code (`BSPQuery.cs:2227-2264`) does NOT do
|
|
||||||
this.** It applies the *same* SetCollide-and-defer (or steep→`Slid`)
|
|
||||||
treatment to a sphere1 hit as it does to sphere0 — there is no branch
|
|
||||||
that returns `Collided` with a direct `SetCollisionNormal` write for "foot
|
|
||||||
clear, head hit" while airborne. This means: **in acdream, an airborne
|
|
||||||
mover whose HEAD sphere alone contacts a polygon (foot sphere clear) gets
|
|
||||||
`SetCollide` + deferred `Adjusted` (no immediate `CollisionNormal`
|
|
||||||
write) — exactly the same "the real normal gets lost until
|
|
||||||
`validate_transition`'s `UnitZ` default kicks in" symptom the digest
|
|
||||||
already diagnosed for shape-1.** A door push where the player's capsule
|
|
||||||
brushes the door frame near chest/head height while the foot sphere
|
|
||||||
tracks slightly differently (a very plausible geometry for "pushing a
|
|
||||||
closed door face at a near-perpendicular angle," matching the tick-22760
|
|
||||||
description) is a strong candidate for exactly this code path.
|
|
||||||
|
|
||||||
**Caveat, stated honestly:** this is contingent on sphere0 (foot) *not*
|
|
||||||
fully hitting while sphere1 (head) *does* — if the door's collision
|
|
||||||
geometry is a full vertical plane, sphere0 would very likely hit too,
|
|
||||||
and the code would never reach the sphere1 branch (`BSPQuery.cs:2156`'s
|
|
||||||
`if (hit0 || hitPoly0 is not null)` returns early). This has **not**
|
|
||||||
been confirmed against the actual tick-22760 replay this pass — it is
|
|
||||||
the single next concrete step (see §4).
|
|
||||||
|
|
||||||
### 2.4 Instrumentation to run (concrete, low-cost, no guessing required)
|
|
||||||
|
|
||||||
Extend `DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals`
|
|
||||||
(`tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs:162`)
|
|
||||||
to log, at the tick-22760 resolve, which of `hit0`/`hitPoly0`/`hit1`/
|
|
||||||
`hitPoly1` were non-null/true inside `BSPQuery.cs`'s Path-6 dispatch
|
|
||||||
(a one-line `Console.WriteLine` gated behind the existing
|
|
||||||
`ProbeIndoorBspEnabled`/`ProbeBuildingEnabled` diagnostics, or a new
|
|
||||||
narrowly-scoped probe flag per the project's diagnostic-owner pattern).
|
|
||||||
|
|
||||||
**Accept criterion:** if `hit0`/`hitPoly0` are both null/false **and**
|
|
||||||
`hit1`/`hitPoly1` fire, §2.3's hypothesis is confirmed — the fix is to
|
|
||||||
port retail's direct sphere1-hit-without-sphere0-hit → `Collided` +
|
|
||||||
`SetCollisionNormal` branch into `BSPQuery.cs`'s Path 6 (mirroring the
|
|
||||||
already-correct Path 5/Contact-branch treatment at
|
|
||||||
`BSPQuery.cs:2103-2140`, which already handles the analogous grounded
|
|
||||||
case correctly — this would be a narrow, well-precedented port, not a
|
|
||||||
new design).
|
|
||||||
|
|
||||||
**Reject criterion:** if sphere0 hits too (`hit0` or `hitPoly0` truthy),
|
|
||||||
this hypothesis is wrong for tick-22760 specifically, and the search
|
|
||||||
should move to the *other* named-retail sibling not yet read this pass —
|
|
||||||
`BSPTREE::collide_with_pt`'s own internal structure for a **non-PathClipped**
|
|
||||||
context is not reachable (its outer gate requires `state&8`), so the
|
|
||||||
next candidate would be whatever governs `CObjCell::find_obj_collisions`'s
|
|
||||||
insertion order relative to `find_env_collisions` for a door's *building*
|
|
||||||
channel (the BR-7/A6.P4 per-cell shadow architecture) — not yet examined
|
|
||||||
this pass; would need a fresh read of that dispatch specifically for
|
|
||||||
polygon ordering/precedence when multiple candidate polys are tested per
|
|
||||||
cell.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. #116 shape-2 — first-airborne-frame hard-stop vs in-frame slide (strong structural finding, INFERENCE, narrows but does not eliminate the need for a confirming run)
|
|
||||||
|
|
||||||
### 3.1 The dispatch structure resolves the ROUTING question without cdb (FACT, cross-referenced against 3 sources: raw BN pseudo-C, ACE, current acdream)
|
|
||||||
|
|
||||||
Both the raw retail decomp and ACE's `BSPTree.cs` (an independent,
|
|
||||||
clean-language port — the "fastest oracle" the mission suggested)
|
|
||||||
show the **same two-tier gate**, keyed on `ObjectInfoState.Contact`:
|
|
||||||
|
|
||||||
- **Already grounded (`Contact` set) + head-sphere hit** → `slide_sphere`
|
|
||||||
called **directly, in-line, same tick** (ACE `BSPTree.cs:192-202`;
|
|
||||||
retail pseudo-C region immediately following `0x0053a730`'s `state&1`
|
|
||||||
branch — the `else` arm at ~323838+, not fully re-quoted here but
|
|
||||||
structurally mirrored by ACE's clean port). acdream's `BSPQuery.cs`
|
|
||||||
Path 5 (`:2103-2120`) already matches this exactly — `SlideSphere`
|
|
||||||
called directly for a grounded head-hit.
|
|
||||||
- **NOT yet grounded (`Contact` unset, i.e. airborne / first contact) +
|
|
||||||
foot-sphere hit** → the **Path-6 default**: `SetCollide` +
|
|
||||||
`WalkableAllowance=LandingZ` + return `Adjusted` — **no
|
|
||||||
repositioning, no `slide_sphere` call at all** (ACE `BSPTree.cs:210-219`;
|
|
||||||
retail pseudo-C:323815-323821). Only a sphere1(head)-hit-without-
|
|
||||||
sphere0-hit gets an immediate response in this branch, and that
|
|
||||||
response is `Collided` (§2.3), **still not `slide_sphere`**.
|
|
||||||
|
|
||||||
**This means: for a genuine first-airborne-frame FOOT-sphere wall hit
|
|
||||||
(the D4 fixture's actual shape — a mover falling into a tall wall),
|
|
||||||
neither retail nor ACE's port calls `slide_sphere` on contact frame 1.**
|
|
||||||
The sphere is left exactly where it was (`SetCollide` does not
|
|
||||||
reposition — confirmed in §1.2 Step A), `Collide` gets set, and the
|
|
||||||
**very next retry attempt** (same tick, same `TransitionalInsert` loop,
|
|
||||||
per §1.2 Step B) routes to **Path 4** (`find_walkable`) instead. For a
|
|
||||||
**tall, vertical wall** (D4's actual geometry — "TallWall" per the test
|
|
||||||
name), `find_walkable`'s nearby-walkable-surface search would very
|
|
||||||
plausibly find **no** candidate (a sheer vertical face has no
|
|
||||||
near-horizontal polygon to "land" on nearby) — `changed=false` — so
|
|
||||||
Path 4 returns `OK` (ACE `BSPTree.cs:185-186`, `else return
|
|
||||||
TransitionState.OK;`). `TransitionalInsert`'s Phase 3 (`if
|
|
||||||
(sp.Collide)`, now finally reachable since `objState==OK`) then runs:
|
|
||||||
`ContactPlaneValid` is **false** (Path 4's `changed=false` arm never
|
|
||||||
sets it), so the `else reset=true;` branch fires
|
|
||||||
(`TransitionTypes.cs:1842-1843`), `RestoreCheckPos()` reverts to the
|
|
||||||
pre-hit position, and the retail-faithful gate at
|
|
||||||
`TransitionTypes.cs:1863-1898` (matching pseudo-C:273231-273239 exactly,
|
|
||||||
already cited in-code) fires: since this is the *first* airborne
|
|
||||||
contact, `LastKnownContactPlaneValid` is false, so
|
|
||||||
`SetCollisionNormal(sp.StepUpNormal)` (the wall's **real** normal,
|
|
||||||
captured back at the original Path-6 hit) runs and the function returns
|
|
||||||
**`Collided`** — a **hard stop, in place, with the correct wall normal
|
|
||||||
recorded** — not a slide.
|
|
||||||
|
|
||||||
### 3.2 What this means for D4
|
|
||||||
|
|
||||||
**INFERENCE, well-supported but not independently re-run this pass:**
|
|
||||||
the D4 pin's original expectation (frame 1 hard-stops at Z=2.0, the
|
|
||||||
slide begins frame 2 off the cached sliding normal) is structurally much
|
|
||||||
closer to what retail's own dispatch produces for a true vertical-wall
|
|
||||||
first-contact than the engine's current in-frame slide-to-Z=1.92
|
|
||||||
behavior. **This narrows — but does not eliminate — the open question.**
|
|
||||||
What remains genuinely unconfirmed by static reading (and is exactly
|
|
||||||
the class of question DO-NOT-RETRY item 8 warns against guessing):
|
|
||||||
|
|
||||||
- Whether `find_walkable`'s internal walkable-search radius/height
|
|
||||||
actually returns "nothing found" for the *specific* D4 fixture
|
|
||||||
geometry (a wall tall enough that no nearby floor exists within its
|
|
||||||
search envelope) — this is a **testable, non-cdb** question: instrument
|
|
||||||
or step through `FindWalkableInternal` for the D4 geometry and confirm
|
|
||||||
`changed=false`.
|
|
||||||
- The exact x87 comparison polarities *inside* `slide_sphere` and
|
|
||||||
`find_walkable` themselves (unrelated to this pass's routing finding)
|
|
||||||
remain unconfirmed per DO-NOT-RETRY item 8 — but those don't matter for
|
|
||||||
D4 if `slide_sphere` is never reached on frame 1 in the first place.
|
|
||||||
|
|
||||||
### 3.3 Recommended next step for shape-2 (no cdb needed for the routing question; cdb only if the confirming run disagrees)
|
|
||||||
|
|
||||||
1. **First (cheap, no cdb):** run/instrument the existing
|
|
||||||
`BSPStepUpTests.D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames`
|
|
||||||
fixture (currently `Skip`-tagged citing #116) with a probe on which
|
|
||||||
`BSPQuery.cs` path fires on frame 1 (Path 6 vs Path 4 vs a full-hit-
|
|
||||||
the-second-attempt path) and whether `FindWalkableInternal` returns
|
|
||||||
`changed=true` or `false` for that specific wall. **Accept:** if
|
|
||||||
Path 6 fires (`SetCollide`+`Adjusted`, no reposition), Path 4 then
|
|
||||||
fires with `changed=false`, and the final result is `Collided` with
|
|
||||||
`StepUpNormal` as the recorded normal — this confirms §3.1/§3.2, and
|
|
||||||
the fix is to **flip the D4 pin back to hard-stop** (retire the
|
|
||||||
`Skip`, assert Z=2.0 frame 1) rather than changing the engine.
|
|
||||||
**Reject:** if the trace shows something else (e.g. Path 4 actually
|
|
||||||
finds a walkable candidate for this wall, or a different dispatch arm
|
|
||||||
fires entirely) — then the routing hypothesis in §3.1 doesn't hold for
|
|
||||||
this specific fixture geometry, and a live cdb trace becomes necessary
|
|
||||||
after all.
|
|
||||||
2. **Only if step 1 disagrees with the FACT-cited dispatch structure:**
|
|
||||||
a live cdb trace of an actual airborne wall hit in retail, per the
|
|
||||||
CLAUDE.md "Retail debugger toolchain" section. Concrete script
|
|
||||||
outline (adapting the documented pattern):
|
|
||||||
```
|
|
||||||
.logopen ts4-116-airborne-wallhit.log
|
|
||||||
.sympath C:\Users\erikn\source\repos\acdream\refs
|
|
||||||
.symopt+ 0x40
|
|
||||||
.reload /f acclient.exe
|
|
||||||
|
|
||||||
r $t0 = 0
|
|
||||||
bp acclient!BSPTREE::find_collisions "r $t0 = @$t0 + 1; .if (@$t0 % 1 == 0) { .printf \"hit %d: state=%%d collide=%%d\\n\", @$t0 } gc"
|
|
||||||
bp acclient!CSphere::slide_sphere "r $t1 = @$t1 + 1; .printf \"SLIDE_SPHERE HIT #%d\\n\", @$t1; .if (@$t1 >= 3) { qd } .else { gc }"
|
|
||||||
bp acclient!BSPTREE::collide_with_pt "r $t2 = @$t2 + 1; .printf \"COLLIDE_WITH_PT HIT #%d\\n\", @$t2; gc"
|
|
||||||
g
|
|
||||||
```
|
|
||||||
User reproduces: jump toward a tall vertical wall so the FIRST wall
|
|
||||||
contact happens while airborne (not already grounded). The key
|
|
||||||
signal is whether `slide_sphere` fires on the **same** engine tick
|
|
||||||
as the first `find_collisions` hit against that wall (in-frame slide,
|
|
||||||
confirming the CURRENT engine behavior) or only on a **later** tick
|
|
||||||
(confirming the hard-stop-then-slide-frame-2 pin). Auto-detaches via
|
|
||||||
`qd` after 3 `slide_sphere` hits to bound game lag.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Recommended execution order + blast radius
|
|
||||||
|
|
||||||
1. **[Lowest risk, do first] Fix the `TransitionalInsert` exhausted-loop
|
|
||||||
hardcoded return** (§1.4): change `return TransitionState.Slid;` to
|
|
||||||
return the real last `transitState` value, matching ACE/retail. Blast
|
|
||||||
radius: essentially zero — `ValidateTransition` treats
|
|
||||||
`Collided`/`Adjusted`/`Slid` identically downstream (confirmed §1.2
|
|
||||||
Step D), so this is a pure code-correctness fix with no observable
|
|
||||||
behavior change in any currently-passing test. Good precursor because
|
|
||||||
it removes a misleading comment/return before anyone traces this loop
|
|
||||||
again.
|
|
||||||
|
|
||||||
2. **[Cheap, decides whether TS-4 needs anything further] Re-run
|
|
||||||
`Ts4SteepRoofWedgeCaptureTests` with a small horizontal velocity
|
|
||||||
component** (e.g. `vx = 0.3` m/s alongside the existing straight-down
|
|
||||||
fall), shortcut removed. Per §1.3's hypothesis, this should **not**
|
|
||||||
wedge (the crease projection produces a non-zero tangential offset).
|
|
||||||
**Accept (doesn't wedge):** TS-4's shortcut removal is safe for the
|
|
||||||
realistic case; land it, retire the TS-4 register row, and either (a)
|
|
||||||
accept the pure-vertical case as a documented, retail-faithful corner
|
|
||||||
case (cite §1.2/§1.3 in the register row) or (b) if the team wants
|
|
||||||
zero residual risk, also file a narrow follow-up for the
|
|
||||||
zero-horizontal-velocity degenerate specifically (not a TS-4 blocker).
|
|
||||||
**Reject (still wedges even with horizontal velocity):** §1.3's
|
|
||||||
hypothesis is wrong or incomplete; do NOT land TS-4 yet — re-open with
|
|
||||||
a fresh capture of the actual velocity vector at the wedge point and
|
|
||||||
compare against what `AdjustOffset` computes step by step (a
|
|
||||||
`ACDREAM_DUMP_EDGE_SLIDE`-style trace of `AdjustOffset`'s intermediate
|
|
||||||
`slidingAngle`/`collisionAngle`/`slideOffset` values, not yet
|
|
||||||
instrumented, would be the concrete next apparatus).
|
|
||||||
|
|
||||||
3. **[Independent of 1-2] #116 shape-1 instrumentation** (§2.4): add the
|
|
||||||
one-line hit0/hitPoly0/hit1/hitPoly1 probe to
|
|
||||||
`Diagnostic_Tick22760_DumpEngineInternals` and re-run. Blast radius:
|
|
||||||
zero (diagnostic-only). If confirmed, the fix (porting retail's direct
|
|
||||||
sphere1-hit → `Collided`+`SetCollisionNormal` branch into Path 6) is a
|
|
||||||
narrow, well-precedented addition mirroring the already-correct Path 5
|
|
||||||
treatment — moderate blast radius (touches the shared Path-6 dispatch
|
|
||||||
used by every airborne two-sphere mover), needs the existing
|
|
||||||
`SphereCollisionFamilyTests`/`Issue137*` suites re-run plus a fresh
|
|
||||||
tick-22760 comparison before landing.
|
|
||||||
|
|
||||||
4. **[Independent of 1-3] #116 shape-2 instrumentation** (§3.3 step 1):
|
|
||||||
add the BSPQuery-path + `FindWalkableInternal` `changed` probe to the
|
|
||||||
D4 fixture. Blast radius: zero (diagnostic-only) for the instrumentation
|
|
||||||
itself. If confirmed, flipping the D4 pin (un-skip, assert hard-stop
|
|
||||||
frame 1) is a **test-only** change with **zero production code
|
|
||||||
change** — the engine's current dispatch already produces this
|
|
||||||
result per §3.1's reading; only the test's own expectation is
|
|
||||||
currently wrong. This is the lowest-risk of all four items once
|
|
||||||
confirmed, because it requires touching zero engine code.
|
|
||||||
|
|
||||||
**Suggested order given the above:** 1 → 4 → 3 → 2, since 4 (#116
|
|
||||||
shape-2) is the cheapest to fully resolve (test-only fix, zero engine
|
|
||||||
change, per this pass's structural finding) and 2 (TS-4's own
|
|
||||||
confirming run) benefits from having item 1's return-value fix landed
|
|
||||||
first (removes a confusing false signal before re-tracing).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. What genuinely still needs cdb or Ghidra (not resolved by this pass)
|
|
||||||
|
|
||||||
1. **#116 shape-2, only if §3.3 step 1's confirming run disagrees with
|
|
||||||
the FACT-cited dispatch structure.** The routing question itself
|
|
||||||
(does frame 1 reach `slide_sphere`) is resolved by static reading
|
|
||||||
against 3 independent sources in this pass; only a surprising,
|
|
||||||
contradicting instrumentation result would re-open the need for a
|
|
||||||
live trace. The cdb script outline is in §3.3 step 2.
|
|
||||||
2. **The x87 comparison polarities inside `slide_sphere`,
|
|
||||||
`find_walkable`, and `AdjustOffset`'s own internal branches**
|
|
||||||
(DO-NOT-RETRY item 8) — untouched by this pass, remain Ghidra/cdb-
|
|
||||||
gated as before. This pass's findings are about which *function*
|
|
||||||
gets called (dispatch routing), not the exact comparison operators
|
|
||||||
inside those functions.
|
|
||||||
3. **AP-7's `cos(10°)` vs `0.99999536f` discrepancy** (P2 doc §1) —
|
|
||||||
unrelated to this pass, still needs a Ghidra decompile of
|
|
||||||
`0050ee70` when Ghidra MCP is back up.
|
|
||||||
4. **TS-1 gaps #2/#3's `last_known_contact_plane` maintenance and
|
|
||||||
Path-4 `LandingZ` acceptance audit** (P2 doc §2, §6 Step 2) — per the
|
|
||||||
current source read in this pass, this already carries an in-code
|
|
||||||
citation ("TS-1 gap #3 (register AD-54, Campaign P Slice P2
|
|
||||||
2026-07-30)") suggesting it was addressed in the same implementation
|
|
||||||
session that produced the P2 doc's item-6 update; not independently
|
|
||||||
re-verified this pass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. One-paragraph summary for the calling agent
|
|
||||||
|
|
||||||
**TS-4:** the Adjusted↔retry loop the P2 doc's implementation attempt
|
|
||||||
found is real, but its root cause is one layer downstream of where that
|
|
||||||
attempt looked. `Path 6` sets `Collide=true` without moving the sphere;
|
|
||||||
every subsequent attempt (same tick and later ticks, since `Collide` is
|
|
||||||
never cleared outside Phase 3) routes to `Path 4`
|
|
||||||
(`FindWalkableInternal`), which is what actually establishes the steep
|
|
||||||
`ContactPlane` (matching the fixture's observed `InContact=true,
|
|
||||||
OnWalkable=false`). The freeze itself happens one tick later, inside
|
|
||||||
`AdjustOffset`: `validate_transition`'s retail-faithful `CollisionNormal
|
|
||||||
→ UnitZ` default feeds `SetSlidingNormal`, and `AdjustOffset`'s
|
|
||||||
crease-projection (`Cross(ContactPlane.Normal, SlidingNormal)`) is
|
|
||||||
mathematically orthogonal to a **purely vertical** input offset — every
|
|
||||||
subsequent tick's gravity-only offset gets crushed to zero and
|
|
||||||
abort-small-offsets before the engine can run again. This exact
|
|
||||||
mechanism is present identically in the raw retail decomp, ACE's port,
|
|
||||||
and acdream's current port — it is very likely not a code bug but a
|
|
||||||
narrow degenerate case that a live player's residual horizontal velocity
|
|
||||||
(present in the original validating debugger trace) would not trigger.
|
|
||||||
The concrete next step is cheap and decisive: re-run
|
|
||||||
`Ts4SteepRoofWedgeCaptureTests` with a small horizontal velocity
|
|
||||||
component before deciding whether TS-4's shortcut removal needs anything
|
|
||||||
beyond the register-row writeup.
|
|
||||||
|
|
||||||
## Addendum (P-review byte decode, 2026-07-30): AD-55 RESOLVED — retail's sled flatness test is cos(10°), ACE's constant is a radians/degrees bug
|
|
||||||
|
|
||||||
Raw bytes of `CPhysicsObj::calc_friction @ 0x0050ee70` (PDB-paired binary,
|
|
||||||
technique `reference_pe_byte_decode`), Sledding fast-sled branch at
|
|
||||||
0x0050ef52-0x0050ef6a:
|
|
||||||
|
|
||||||
```
|
|
||||||
d9 86 38 01 00 00 fld dword [esi+0x138] ; contact_plane.Normal.Z
|
|
||||||
dd 05 28 6b 7c 00 fld qword [0x007c6b28] ; = 0.17453292519943295 (10 deg in RADIANS)
|
|
||||||
d9 ff fcos ; st0 = cos(10 deg) = 0.984807753
|
|
||||||
de d9 fcompp
|
|
||||||
df e0 / f6 c4 41 / 7a fnstsw; test ah,0x41; jp
|
|
||||||
```
|
|
||||||
|
|
||||||
FACT: retail genuinely computes `cos(10°) ≈ 0.9848078` at runtime and
|
|
||||||
compares `Normal.Z` against it. ACE's `0.99999536f` equals
|
|
||||||
`cos(0.1745 DEGREES)` — the radian literal evaluated in degree mode; a
|
|
||||||
proven ACE porting error, not a BN artifact. Sibling constants
|
|
||||||
byte-confirmed: threshold float 0.25 @0x007c6b00, doubles 6.25/1.5625
|
|
||||||
@0x007c6b30/38, friction overrides 1.0f/0.2f as immediates.
|
|
||||||
|
|
||||||
Feel impact: retail's 0.2-friction fast-sled override engages on ground
|
|
||||||
within 10° of flat; the shipped ACE-derived constant engages only within
|
|
||||||
0.17° (never, in practice) — part of the #166 sled family. FIX (queue for
|
|
||||||
the TS-4/#116 implementation slice, which owns `PhysicsBody`): replace
|
|
||||||
`0.99999536f` with `0.98480775f` (cos 10°), cite this addendum, retire
|
|
||||||
register row AD-55 in the same commit.
|
|
||||||
|
|
||||||
## Addendum 2 (implementation session, 2026-07-30): #116 shape-1's tick-22760
|
|
||||||
## confirming run DISAGREES with this plan's hypothesis — the real mechanism
|
|
||||||
## is one layer further upstream, and it's retail-faithful there too
|
|
||||||
|
|
||||||
Per this doc's own §4 execution order, item 1 (the `TransitionalInsert`
|
|
||||||
exhausted-loop hardcoded return) landed first — mechanical, zero
|
|
||||||
observable behavior change, confirmed by the full `AcDream.Core.Tests`
|
|
||||||
suite (4059 passed / 2 skipped, no change in pass count). Then §2.3's
|
|
||||||
shape-1 fix landed verbatim in `BSPQuery.cs`'s Path 6 `hasSphere1`
|
|
||||||
branch: a foot-clear/head-hit airborne contact now returns
|
|
||||||
`TransitionState.Collided` with a direct `collisions.SetCollisionNormal`
|
|
||||||
write, exactly matching pc:323824-323834 (`0x0053a793`/`0x0053a7a4`) and
|
|
||||||
ACE `BSPTree.cs:221-230`. This is a real, independently-decomp-confirmed
|
|
||||||
port-accuracy fix and is kept regardless of the result below.
|
|
||||||
|
|
||||||
**The confirming instrumentation run (§2.4) DISAGREES with the plan's
|
|
||||||
tick-22760 hypothesis.** Re-running
|
|
||||||
`DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals`
|
|
||||||
after the fix landed shows **no change**: harness still reports
|
|
||||||
`cn=(0,0,1)` (the `UnitZ` ground-fallback default) against live's
|
|
||||||
`cn=(0,+1,0)` (the door-face normal). Adding a dispatcher-entry probe
|
|
||||||
(`[path-dispatch]`, `[path5-diag]`, gated on the existing
|
|
||||||
`ProbeIndoorBspEnabled` flag, kept in `BSPQuery.cs` as permanent
|
|
||||||
diagnostics) traced the ACTUAL call sequence for this capture:
|
|
||||||
|
|
||||||
1. The seeded body's `TransientState` (131 = `Contact | OnWalkable |
|
|
||||||
Active`) means `ObjectInfo.State & Contact != 0` for this mover — it
|
|
||||||
is **grounded**, so `BSPQuery.FindCollisionsCore` dispatches to
|
|
||||||
**Path 5** (the Contact/grounded branch), never Path 6 at all. The
|
|
||||||
plan's shape-1 hypothesis was explicitly scoped to "the not-yet-in-
|
|
||||||
Contact branch" (§2.3) — that scoping was itself the unconfirmed
|
|
||||||
part, and it does not hold for tick-22760.
|
|
||||||
2. Path 5's own dispatch for the door's BSP shape at this exact position
|
|
||||||
finds **neither sphere hitting nor near-missing**
|
|
||||||
(`hit0=False hitPoly0=False hit1=False hitPoly1=False`) — the
|
|
||||||
simplified fixture registration this test uses
|
|
||||||
(`BuildEngineWithDoorFixture`, which places the raw GfxObj BSP
|
|
||||||
directly at its captured world-space bounding-sphere center rather
|
|
||||||
than via the faithful `ShadowShapeBuilder.FromSetup` +
|
|
||||||
`PlacementFrame` transform that `BuildFaithfulDoorEngine` uses
|
|
||||||
elsewhere in the same file) returns `OK` for the door here.
|
|
||||||
3. `TransitionalInsert`'s step-down gate then fires
|
|
||||||
(`contactInvalidOrSteep` is true because the per-substep walk loop
|
|
||||||
clears `ContactPlaneValid` before every `TransitionalInsert` call —
|
|
||||||
`TransitionTypes.cs` around the `FindValidPosition` per-step reset —
|
|
||||||
so the door BSP is queried TWICE MORE via `DoStepDown`'s two half-
|
|
||||||
height attempts, dispatching to **Path 3** (`StepSphereDown` →
|
|
||||||
`FindWalkableInternal`), which also finds no walkable candidate here
|
|
||||||
(the door face is not a floor-like polygon) and returns `OK` both
|
|
||||||
times.
|
|
||||||
4. Both `DoStepDown` calls therefore fail (return `false`), which routs
|
|
||||||
into `EdgeSlideAfterStepDownFailed`. With `ContactPlaneValid` false,
|
|
||||||
`OnWalkable` true (seeded), `EdgeSlide` true (mover flags), and the
|
|
||||||
RESTORED walkable polygon from the body's own snapshot (a flat
|
|
||||||
triangle `(144,0,94)-(144,24,94)-(120,24,94)`, `Normal.Z=1 >=
|
|
||||||
FloorZ`), execution reaches `sp.PrecipiceSlide(this)`
|
|
||||||
(`TransitionTypes.cs` "branch3/precipice-slide").
|
|
||||||
5. **`SpherePath.PrecipiceSlide` calls `BSPQuery.FindCrossedEdge` against
|
|
||||||
that seeded triangle. The player's actual sweep (X≈133, Y from 18.02
|
|
||||||
to 17.60) does not cross ANY of that triangle's three edges** (the
|
|
||||||
triangle spans roughly X∈[120,144], and its hypotenuse sits at
|
|
||||||
X+Y=144 — at X=133 that's Y≈11, far south of the player's Y range).
|
|
||||||
`FindCrossedEdge` returns false, and acdream's `PrecipiceSlide`
|
|
||||||
(`TransitionTypes.cs:1039-1054`) does exactly what retail's
|
|
||||||
`SPHEREPATH::precipice_slide` does on the identical branch — **read
|
|
||||||
fresh this session, pc:274316-274326, `0x0050cc80`**:
|
|
||||||
```
|
|
||||||
int32_t eax = CPolygon::find_crossed_edge(...);
|
|
||||||
if (eax == 0) { this->walkable = eax; return 2; /* COLLIDED_TS */ }
|
|
||||||
```
|
|
||||||
**No `set_collision_normal` call on this path in retail either.**
|
|
||||||
This is a byte-exact match, not an inference — acdream's
|
|
||||||
`ClearWalkable(); return TransitionState.Collided;` on a failed
|
|
||||||
`FindCrossedEdge` is retail-faithful. `ValidateTransition`'s
|
|
||||||
`UnitZ`-default-on-invalid-normal fires identically in both engines
|
|
||||||
for this exact mechanism.
|
|
||||||
|
|
||||||
**Conclusion: the tick-22760 divergence is NOT explained by anything this
|
|
||||||
plan identified, and the mechanism this pass traced down to (Path 5 →
|
|
||||||
StepSphereDown → EdgeSlideAfterStepDownFailed → PrecipiceSlide's
|
|
||||||
no-crossed-edge fallback) is independently confirmed retail-faithful at
|
|
||||||
every step, including a fresh byte-level read of `precipice_slide`
|
|
||||||
itself.** The remaining candidates, none guessed at here: (a) this
|
|
||||||
specific harness (`BuildEngineWithDoorFixture`) may simply not place the
|
|
||||||
door's BSP polygons where live retail's did at that exact tick — a
|
|
||||||
harness/fixture-geometry gap, not a response-layer code bug — worth
|
|
||||||
re-running this same capture through `BuildFaithfulDoorEngine`'s
|
|
||||||
Setup-based registration to check whether a REAL BSP hit against the
|
|
||||||
door (rather than the seeded generic floor triangle) changes the
|
|
||||||
outcome; (b) the seeded `WalkableVertices` triangle itself may not match
|
|
||||||
what retail's own walkable-polygon bookkeeping held at that instant
|
|
||||||
(a state-capture gap in the original 2026-05-24 live-capture tooling,
|
|
||||||
not necessarily an engine bug); (c) a genuinely different upstream
|
|
||||||
mechanism not yet traced. Per CLAUDE.md's no-guessing rule, none of
|
|
||||||
these is adopted without further evidence — #116 shape-1 stays
|
|
||||||
**narrowed, not closed**: the Path-6 fix is a real, independent
|
|
||||||
retail-faithfulness improvement, and the original tick-22760 acceptance
|
|
||||||
criterion is NOT met by it. See ISSUES.md #116 for the updated status.
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
# Issue #269 — slope-stop capture and retail correction
|
|
||||||
|
|
||||||
**Date:** 2026-07-31
|
|
||||||
**Status:** implemented; user live gate passed
|
|
||||||
**Scope:** landing-bounce follow-up, `CTransition::validate_transition`
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
|
|
||||||
After the retail 5%-elasticity landing reflection was restored for #265,
|
|
||||||
the character could retain too much downhill speed after landing on a
|
|
||||||
walkable slope. The user described the residual as “slides too far on
|
|
||||||
landing.”
|
|
||||||
|
|
||||||
## ACDream live capture
|
|
||||||
|
|
||||||
`ACDREAM_CAPTURE_PLAYER_QUANTA=<jsonl-path>` records the local player's
|
|
||||||
complete admitted object quantum without changing simulation order:
|
|
||||||
|
|
||||||
1. quantum start;
|
|
||||||
2. root/PositionManager composition;
|
|
||||||
3. pre- and post-`UpdatePhysicsInternal`;
|
|
||||||
4. transition result;
|
|
||||||
5. final collision-response commit.
|
|
||||||
|
|
||||||
The accepted repro contained 2,184 quanta. The clearest landing was:
|
|
||||||
|
|
||||||
| Quantum | Event | Velocity |
|
|
||||||
|---|---|---|
|
|
||||||
| 1740 | final airborne quantum | `(-12.316, 8.187, -26.266)` |
|
|
||||||
| 1741 | slope collision, normal `(-0.236, 0.236, 0.943)` | |
|
|
||||||
| 1741 post-response | correct 5% reflect | `(-17.391, 13.262, -6.576)` |
|
|
||||||
| 1742–1758 | still Contact + OnWalkable, no new collision normal | velocity unchanged |
|
|
||||||
| 1759+ | contact relationship changes | friction finally begins decaying |
|
|
||||||
|
|
||||||
The reflected velocity had `dot(v, normal) = +1.0252`: it pointed away
|
|
||||||
from the slope. Retail `calc_friction` correctly skips while this value is
|
|
||||||
at least `0.25`, so friction was not the defect. ACDream was repeatedly
|
|
||||||
restoring the remembered slope plane and re-grounding the body without
|
|
||||||
performing retail's accompanying velocity stop.
|
|
||||||
|
|
||||||
## Retail oracle
|
|
||||||
|
|
||||||
Named-retail:
|
|
||||||
|
|
||||||
- `CPhysicsObj::check_contact` `0x0050F5B0`
|
|
||||||
- `CPhysicsObj::get_object_info` `0x00511CC0`
|
|
||||||
- `CTransition::validate_transition` `0x0050AA70`
|
|
||||||
- `OBJECTINFO::kill_velocity` `0x0050CFE0`
|
|
||||||
|
|
||||||
The exact `validate_transition` order at
|
|
||||||
`0x0050AAED–0x0050AB42` is:
|
|
||||||
|
|
||||||
1. enter only for a non-OK collision/adjusted/slid result;
|
|
||||||
2. if `last_known_contact_plane_valid`, call
|
|
||||||
`OBJECTINFO::kill_velocity`;
|
|
||||||
3. test the current sphere center against the remembered plane using
|
|
||||||
`radius + 0.0002`;
|
|
||||||
4. restore the contact plane only when still within that distance;
|
|
||||||
5. later, at `0x0050ACFF`, overwrite last-known validity with final
|
|
||||||
contact-plane validity.
|
|
||||||
|
|
||||||
`OBJECTINFO::kill_velocity` calls
|
|
||||||
`CPhysicsObj::set_velocity({0,0,0}, 0)`. ACDream had ported the proximity
|
|
||||||
test and plane restore but omitted this call. It also allowed the
|
|
||||||
last-known plane to re-ground clean accepted moves, although retail only
|
|
||||||
consumes it in the non-OK recovery branch.
|
|
||||||
|
|
||||||
## Correction
|
|
||||||
|
|
||||||
`Transition.ValidateTransition` now:
|
|
||||||
|
|
||||||
- calls `ObjectInfo.StopVelocity()` before the remembered-plane
|
|
||||||
proximity/restore test on a non-OK recovery;
|
|
||||||
- performs that restore only in the retail branch;
|
|
||||||
- overwrites last-known validity from final contact validity, so a clean
|
|
||||||
move away cannot be re-grounded from stale memory.
|
|
||||||
|
|
||||||
The existing `PhysicsEngine.ResolveWithTransition` consumption of
|
|
||||||
`VelocityKilled` applies the zero to the canonical `PhysicsBody` before
|
|
||||||
the collision-response tail. The initial 5% landing reflection remains;
|
|
||||||
only a following collision recovery performs the retail stop.
|
|
||||||
|
|
||||||
## Gates
|
|
||||||
|
|
||||||
- New focused pins:
|
|
||||||
- collision recovery with a remembered plane kills velocity;
|
|
||||||
- clean advance with a remembered plane neither kills nor re-grounds.
|
|
||||||
- Full `AcDream.Core.Tests`: 4,107 passed / 2 skipped.
|
|
||||||
- Full `AcDream.Runtime.Tests`: 439 passed.
|
|
||||||
- `AcDream.App` Release build: 0 warnings / 0 errors.
|
|
||||||
- Complete Release suite: 10,061 passed / 5 skipped / 0 failed.
|
|
||||||
- User live gate: **PASS** — repeated slope jumps now settle correctly
|
|
||||||
(“Perfect! Works great!”).
|
|
||||||
|
|
||||||
## Diagnostic tools retained
|
|
||||||
|
|
||||||
- `tools/analyze_269_slope_stop_capture.py`
|
|
||||||
- `tools/cdb/run-issue269-slope-stop.ps1`
|
|
||||||
- `tools/cdb/issue269-slope-stop.cdb`
|
|
||||||
|
|
||||||
The cdb runner refuses to attach unless the live retail executable matches
|
|
||||||
the Sept 2013 named PDB. The locally installed 2015 retail executable does
|
|
||||||
not match; the static named-retail decode above is therefore the retail
|
|
||||||
oracle used for this correction.
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
# #271 — Stair-side uphill reversal capture
|
|
||||||
|
|
||||||
**Date:** 2026-07-31
|
|
||||||
|
|
||||||
**Status:** closed; retail control flow restored and user live gate passed
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
|
|
||||||
When the local player ran diagonally uphill while pressing into the side of
|
|
||||||
an outdoor staircase, the character could suddenly move backward and rapidly
|
|
||||||
slide to the bottom. The symptom was intermittent because it required the
|
|
||||||
forward candidate to hit the side wall while the step-down recovery crossed a
|
|
||||||
tread edge.
|
|
||||||
|
|
||||||
This is not an RDP, render-rate, animation, or gravity symptom. It reproduced
|
|
||||||
inside the pure Core collision resolver from one captured input frame.
|
|
||||||
|
|
||||||
## Live evidence
|
|
||||||
|
|
||||||
The bounded capture is under the ignored local artifact pointer:
|
|
||||||
|
|
||||||
`artifacts/issue271-stair-side/LATEST.txt`
|
|
||||||
|
|
||||||
It contains 677 local-player physics quanta plus the matching resolver stream.
|
|
||||||
The first decisive frame is quantum 310:
|
|
||||||
|
|
||||||
```text
|
|
||||||
current = (133.03775, 75.53931, 59.608147)
|
|
||||||
target = (133.33783, 76.42308, 59.608147)
|
|
||||||
input = forward + run
|
|
||||||
result = (133.18779, 75.19872, 59.316677)
|
|
||||||
normal = (-1, approximately 0, approximately 0)
|
|
||||||
```
|
|
||||||
|
|
||||||
The X side-wall collision was valid, but the tangential Y component reversed:
|
|
||||||
an uphill request of `+0.88377` produced `-0.34059`. Three frames later,
|
|
||||||
quantum 313 snapped from Z `59.52598` to terrain Z `58.005`. A second attempt
|
|
||||||
reproduced the same family at quanta 479–482, falling from Z `60.96376` to
|
|
||||||
`58.005`.
|
|
||||||
|
|
||||||
## Retail oracle
|
|
||||||
|
|
||||||
Named retail:
|
|
||||||
|
|
||||||
- `CTransition::edge_slide` at `0x0050B3D0`
|
|
||||||
- current-walkable branch at `0x0050B44A`
|
|
||||||
- no-walkable back-probe at `0x0050B458–0x0050B50F`
|
|
||||||
- `SPHEREPATH::precipice_slide` at `0x0050CC80`
|
|
||||||
|
|
||||||
Retail tests only the current `SPHEREPATH::walkable` pointer. If it is null,
|
|
||||||
retail:
|
|
||||||
|
|
||||||
1. offsets the failed candidate back to the current sphere center;
|
|
||||||
2. runs `step_down` there to rediscover the surface actually under the mover;
|
|
||||||
3. restores the failed candidate;
|
|
||||||
4. runs `precipice_slide` against the newly discovered polygon; or
|
|
||||||
5. returns `COLLIDED_TS` when the back-probe found no walkable polygon.
|
|
||||||
|
|
||||||
Retail has no substitution of an older saved walkable polygon in either null
|
|
||||||
case.
|
|
||||||
|
|
||||||
## ACDream divergence and root cause
|
|
||||||
|
|
||||||
`EdgeSlideAfterStepDownFailed` previously called
|
|
||||||
`SpherePath.RestoreLastWalkable()`:
|
|
||||||
|
|
||||||
- before deciding whether to enter the retail back-probe; and
|
|
||||||
- again when the back-probe found no current walkable polygon.
|
|
||||||
|
|
||||||
`LastWalkable` is a separate ACDream history used by the still-open
|
|
||||||
CliffSlide compatibility path. At a staircase side wall it could describe the
|
|
||||||
preceding tread rather than the surface below the current player position.
|
|
||||||
Promoting it into the current slot bypassed retail's back-probe.
|
|
||||||
`PrecipiceSlide` then projected the failed forward candidate along the stale
|
|
||||||
tread edge, producing the backward/downhill displacement seen in the capture.
|
|
||||||
|
|
||||||
The fix removes both stale-history promotions from the edge-slide dispatch.
|
|
||||||
Current walkable state still takes retail's direct precipice path; absent
|
|
||||||
state now always takes retail's current-position back-probe.
|
|
||||||
|
|
||||||
## Deterministic regression
|
|
||||||
|
|
||||||
`Issue185OutdoorStairsSeamReplayTests` reuses the captured
|
|
||||||
`0x01000AC5` staircase collision fixture and the exact quantum-310 position,
|
|
||||||
contact plane, movement delta, player flags, and 1.5 m Setup step-down height.
|
|
||||||
|
|
||||||
Pre-fix:
|
|
||||||
|
|
||||||
```text
|
|
||||||
out = (133.187790, 75.346481, 59.430882)
|
|
||||||
```
|
|
||||||
|
|
||||||
Fixed:
|
|
||||||
|
|
||||||
```text
|
|
||||||
out = (133.187790, 76.078186, 60.016247)
|
|
||||||
```
|
|
||||||
|
|
||||||
The regression requires meaningful positive uphill progress and forbids a
|
|
||||||
downhill Z displacement. The complete Core Release suite passes 4,108 tests /
|
|
||||||
2 skips; the complete Release solution passes 10,062 tests / 5 skips.
|
|
||||||
|
|
||||||
## Live acceptance
|
|
||||||
|
|
||||||
The user repeatedly ran uphill while pressing into both sides of the affected
|
|
||||||
staircase. Movement remained stable and the former rapid downhill reversal did
|
|
||||||
not recur. The client then closed through the normal logout path, with ACE
|
|
||||||
confirming graceful logout.
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
# Atomic collision-generation activation (Slice 3B)
|
|
||||||
|
|
||||||
## Retail anchor
|
|
||||||
|
|
||||||
Retail hydrates a cell synchronously. `CObjCell::init_objects`
|
|
||||||
(`0x0052B420`) visits objects associated with that cell and invokes
|
|
||||||
`CPhysicsObj::recalc_cross_cells` (`0x00515A30`). The final position path also
|
|
||||||
replaces shadows as one `SetPositionInternal` operation (`0x00515330`). Retail
|
|
||||||
therefore never exposes a world where the new cell exists but the objects that
|
|
||||||
overlap it still have their old cross-cell set.
|
|
||||||
|
|
||||||
Acdream streams a landblock over several update frames. Literal per-cell
|
|
||||||
mutation during those frames was not equivalent: the active `PhysicsDataCache`,
|
|
||||||
`CellGraph`, `PhysicsEngine`, buildings, static shadows, and retained-object
|
|
||||||
refloods changed at different cursors. Collision queries could observe a mixed
|
|
||||||
generation, and correctness depended on a later optional landblock callback.
|
|
||||||
|
|
||||||
## Ported adaptation
|
|
||||||
|
|
||||||
The asynchronous unit is now one Runtime-owned collision generation:
|
|
||||||
|
|
||||||
1. `BeginCollisionAdmission` issues the exact Runtime/landblock generation.
|
|
||||||
2. `PrepareCollisionGeneration` creates empty private cache, graph, engine, and
|
|
||||||
shadow facades and retains the active aggregate root reference in O(1).
|
|
||||||
Global immutable GfxObj/Setup catalogs are not copied; the accepted build's
|
|
||||||
exact closure is populated by the existing cursors.
|
|
||||||
3. Stable
|
|
||||||
landblock and logical-owner slot suffixes then materialize each non-target
|
|
||||||
cache, CellGraph, engine, and shadow leaf into an empty private root under
|
|
||||||
the host's existing frame meter. The 32-resident-landblock gate proves
|
|
||||||
admission performs no resident copy and every advance reports at most one
|
|
||||||
work unit.
|
|
||||||
4. App and Headless publish terrain, EnvCells, topology, buildings, prepared
|
|
||||||
collision assets, and target-root static owners only into that private
|
|
||||||
generation.
|
|
||||||
5. Stable per-prefix owner slots capture every non-suspended owner that touches
|
|
||||||
or has a withdrawn repair marker for the target prefix. That includes live
|
|
||||||
dynamic owners and statics rooted in an adjacent landblock. Only a
|
|
||||||
target-root static is omitted, because the authored replacement supersedes
|
|
||||||
it. The scan has a fixed slot suffix and is unaffected by mutations in other
|
|
||||||
prefixes. Vacated slots are tombstoned and reused rather than retained for
|
|
||||||
the whole session. A single Runtime-scoped versioned journal records each
|
|
||||||
mutation once and coalesces repeated changes by logical owner, independently
|
|
||||||
of the number of live drafts. After topology sealing, each draft reconciles
|
|
||||||
the latest exact state of owners changed during that draft's lifetime one
|
|
||||||
owner per seal call. A discovered relevant owner then receives scoped exact
|
|
||||||
updates, preserving continuous-motion progress without restoring global
|
|
||||||
fanout. A membership transition is routed by the owner's changed landblock
|
|
||||||
prefix to the one matching draft, so an owner first entering or leaving the
|
|
||||||
target after its global journal slot was visited is still reconciled once.
|
|
||||||
During topology construction, a visited unrelated owner retains only a cheap
|
|
||||||
coalesced notification; its exact mirror runs later as one metered seal unit
|
|
||||||
rather than once per draft on the mutation path. Once the topology seal
|
|
||||||
exists, observed owners temporarily write through exactly until same-call
|
|
||||||
activation. The finite pre-seal queue therefore drains even when two or more
|
|
||||||
unrelated owners mutate before every host step. Slots predating a newer root snapshot are superseded
|
|
||||||
by a tail slot, not reused behind live cursors. New drafts begin at their
|
|
||||||
captured suffix, obsolete slots compact one visit per seal call, and the
|
|
||||||
journal clears when its last draft closes. Unrelated and continuously moving
|
|
||||||
owners therefore never restart capture or sealing.
|
|
||||||
6. Explicit one-work-unit cursors build the complete replacement before the
|
|
||||||
activation frame: requested global collision records, cells/topology,
|
|
||||||
buildings, cell graph removals, affected static owners, retained-owner
|
|
||||||
states, and removal lists. A late unarmed relevant owner consumes at most
|
|
||||||
one refresh unit on a seal call; when that drains the queue an already-built
|
|
||||||
seal is immediately ready. Immutable global GfxObj/Setup closure entries are
|
|
||||||
preinstalled during these metered steps, not during activation.
|
|
||||||
7. Cache, CellGraph, engine-landblock, and shadow topology share one
|
|
||||||
`CollisionWorldStateSlot`. `CommitCollisionGeneration` transfers the
|
|
||||||
complete off-side aggregate through one volatile reference on the update
|
|
||||||
thread, then revokes the staging slot. The public `PhysicsDataCache`,
|
|
||||||
`CellGraph`, `PhysicsEngine`, and `ShadowObjectRegistry` facade identities
|
|
||||||
stay stable. Warm 256-owner, cold
|
|
||||||
first-load, changed EnvCell/building, and new static-bucket gates all measure
|
|
||||||
exactly zero managed bytes in final activation. Only afterwards does Runtime
|
|
||||||
emit `CollisionGenerationCommitted` and a ready acknowledgement.
|
|
||||||
8. Multiple landblocks may prepare concurrently. Preparation order is the
|
|
||||||
activation order. Only after an older generation commits is its exact delta
|
|
||||||
queued into every later draft. Each additional seal call applies at most one
|
|
||||||
cache, CellGraph, synthesized outdoor-cell, engine-landblock, or logical
|
|
||||||
owner leaf. Later generations retain their own completed target seal but
|
|
||||||
cannot activate before every committed delta drains. Cancelled older drafts
|
|
||||||
therefore contribute nothing, final activation performs no peer work, and a
|
|
||||||
later root cannot overwrite or expose an older snapshot. Seam-crossing
|
|
||||||
statics are forcibly re-evaluated against the later topology. Demotion and
|
|
||||||
withdrawal cancel a matching queued or active rebase, suppress that prefix
|
|
||||||
in unfinished source scans, and retire one shadow owner, cache/graph leaf,
|
|
||||||
authored outdoor cell, or landblock leaf per later seal call before
|
|
||||||
activation. Retirement storage is growable rather than coupled to the
|
|
||||||
concurrent-preparation limit, and final commit rechecks both pending rebase
|
|
||||||
and retirement work after the seal-to-commit gap.
|
|
||||||
The host performs the zero-work root transfer in the same update-thread call
|
|
||||||
that completes final reconciliation, eliminating a seal-to-next-frame quiet
|
|
||||||
window for continuously moving unrelated owners.
|
|
||||||
9. GfxObj/Setup closure entries are immutable content-addressed catalog data,
|
|
||||||
not world topology. Their metered early installation may survive a cancelled
|
|
||||||
generation as ordinary process cache residency; no cell, building,
|
|
||||||
landblock, or shadow becomes visible through that catalog alone.
|
|
||||||
|
|
||||||
Presentation and no-window hosts use the same Runtime transaction. Network
|
|
||||||
workers still enqueue immutable messages and cannot mutate collision or shadow
|
|
||||||
state.
|
|
||||||
|
|
||||||
## Failure and lifetime rules
|
|
||||||
|
|
||||||
- A newer admission invalidates an older prepared generation.
|
|
||||||
- Cancellation names one admission and its private staging generation. It can
|
|
||||||
never withdraw or demote the active landblock, and cancelling a stale receipt
|
|
||||||
cannot invalidate a newer admission.
|
|
||||||
- Demotion, withdrawal, reset, and disposal invalidate the admission before
|
|
||||||
changing the active generation.
|
|
||||||
- Disposing a stale/cancelled prepared generation clears only its private
|
|
||||||
engine/cache/shadows.
|
|
||||||
- The prior complete generation remains queryable throughout preparation.
|
|
||||||
- The commit notification is the future lost-cell-registry seam. Slice 3B does
|
|
||||||
not implement `GotoLostCell` or change `SetPosition` recovery behavior.
|
|
||||||
|
|
||||||
## Deterministic evidence
|
|
||||||
|
|
||||||
The focused Runtime/App tests pin:
|
|
||||||
|
|
||||||
- previous terrain/cells/buildings/statics remain visible until commit;
|
|
||||||
- exactly one notification after a successful complete activation;
|
|
||||||
- stale admission replacement has no active-world side effect;
|
|
||||||
- unrelated movement on every capture/seal step never restarts the target;
|
|
||||||
- two relevant owners moving on every seal step converge without restarting
|
|
||||||
the topology meter and install their latest positions at activation;
|
|
||||||
- an authoritative state change on a retained rowless owner updates an
|
|
||||||
already-sealed generation without a global restart;
|
|
||||||
- a neighboring static whose shadow crossed the seam is restored atomically
|
|
||||||
on reload and its withdrawn-prefix marker clears only at activation;
|
|
||||||
- a late spawn blocks activation until its one metered refresh; deletion of an
|
|
||||||
armed owner writes through directly;
|
|
||||||
- Headless faults immediately after admission and after staging preserve the
|
|
||||||
prior complete world and leave no collision admission behind;
|
|
||||||
- dense sealing consumes at most one work unit per call, while warm 256-owner,
|
|
||||||
cold first-load, changed EnvCell/building, and new static-bucket activation
|
|
||||||
all allocate zero managed bytes;
|
|
||||||
- concurrently prepared landblocks rebase and preserve both terrain roots and
|
|
||||||
static-shadow owners across their activation order, with zero-byte final
|
|
||||||
commits and revoked staging access;
|
|
||||||
- dense 32-landblock admission performs no resident copy, stays within its
|
|
||||||
constant allocation envelope, and materializes at most one leaf per advance;
|
|
||||||
- cancelled older drafts contribute no topology to later roots, while a live
|
|
||||||
owner mutation after the older commit wins over the queued rebase;
|
|
||||||
- a newly committed seam-crossing static refloods against the later draft's
|
|
||||||
topology before that draft may activate;
|
|
||||||
- post-seal arrivals drain one owner per seal call without resetting capture;
|
|
||||||
- an unrelated owner entering the target after its journal slot was visited is
|
|
||||||
routed by prefix and reconciled in one metered seal unit;
|
|
||||||
- target departure and same-ID reuse preserve the exact new-prefix owner rows;
|
|
||||||
- unrelated state mutation publishes only after every row changes;
|
|
||||||
- unrelated demotion/withdrawal and the live `CurrCell` cannot be resurrected
|
|
||||||
or rolled back by a later draft;
|
|
||||||
- queued and partially applied peer rebases cannot resurrect a later demoted or
|
|
||||||
withdrawn landblock;
|
|
||||||
- deleting an outgoing target static before, during, or after staging cannot
|
|
||||||
erase an authored same-ID replacement;
|
|
||||||
- 10,000 repeated mutations with 32 drafts retain one coalesced journal entry
|
|
||||||
and allocate no more than the owner mutation itself;
|
|
||||||
- 512 unique changed owners reconcile in exactly 512 metered seal units and
|
|
||||||
the final activation still allocates zero managed bytes;
|
|
||||||
- compacted journal slots are never reused behind a live cursor, while a
|
|
||||||
4,096-slot obsolete tail retires incrementally and a later draft starts at
|
|
||||||
its captured suffix rather than scanning old tombstones;
|
|
||||||
- post-seal retirement blocks activation until its cursor drains, and more
|
|
||||||
than 256 distinct retirements remain metered and lossless;
|
|
||||||
- prefix-owner slots remain bounded under GUID churn and empty containers are
|
|
||||||
reclaimed across unique prefixes without invalidating a live seal cursor;
|
|
||||||
- graphical and no-window publishers use the same Runtime transaction;
|
|
||||||
- removal and terminal teardown converge the active ownership ledger.
|
|
||||||
|
|
||||||
This retires divergence row AD-6. The remaining lost-cell state-machine work is
|
|
||||||
deliberately outside this slice.
|
|
||||||
|
|
@ -1,333 +0,0 @@
|
||||||
# Canonical retail `SetPosition` — placement/streaming Slice 4A
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This note pins the pure physics half of the placement/streaming closeout.
|
|
||||||
Slice 4A lands the retail placement transaction as a separately testable Core
|
|
||||||
mechanism. It deliberately does **not** replace the production snap-only
|
|
||||||
resolver yet: Runtime lost-cell ownership and the complete inbound-route
|
|
||||||
cutover remain Slice 4B. AP-1 and AD-1 therefore remain active until that
|
|
||||||
cutover is complete.
|
|
||||||
|
|
||||||
Named-retail oracle, Sept 2013 EoR:
|
|
||||||
|
|
||||||
- `CPhysicsObj::SetPosition` `0x005160C0`
|
|
||||||
- `CPhysicsObj::SetPositionInternal` `0x00515BD0`
|
|
||||||
- `CPhysicsObj::AdjustPosition` `0x00511D80`
|
|
||||||
- `CPhysicsObj::CheckPositionInternal` `0x00511E90`
|
|
||||||
- `CTransition::find_valid_position` `0x0050C310`
|
|
||||||
- `CTransition::find_placement_position` `0x0050C170`
|
|
||||||
- `CTransition::find_placement_pos` `0x0050BA50`
|
|
||||||
- `CTransition::validate_placement_transition` `0x0050ADC0`
|
|
||||||
- `CTransition::validate_placement` `0x0050B210`
|
|
||||||
- `CPhysicsObj::ForceIntoCell` `0x00515660`
|
|
||||||
- `CPhysicsObj::handle_all_collisions` `0x00514780`
|
|
||||||
|
|
||||||
## Retail transaction
|
|
||||||
|
|
||||||
```text
|
|
||||||
SetPosition(request):
|
|
||||||
transition = makeTransition() // GENERAL_FAILURE if none
|
|
||||||
init_object(transition, object)
|
|
||||||
|
|
||||||
if the PartArray has no spheres:
|
|
||||||
init_sphere(1, dummy center=(0,0,0.1), radius=0.1, scale=1)
|
|
||||||
else:
|
|
||||||
init_sphere(first min(count,2) authored spheres, exact object scale)
|
|
||||||
|
|
||||||
if flags & RANDOM_SCATTER (0x200):
|
|
||||||
return scatter only
|
|
||||||
|
|
||||||
result = SetPositionInternal(request)
|
|
||||||
if result != OK and flags & SCATTER (0x100):
|
|
||||||
return scatter
|
|
||||||
return result
|
|
||||||
|
|
||||||
SetPositionInternal(request):
|
|
||||||
AdjustPosition(request frame, first sphere, noCreate=(flags & 0x20))
|
|
||||||
if no resident cell:
|
|
||||||
store the adjusted authoritative frame and enter lost-cell lifetime
|
|
||||||
return OK
|
|
||||||
|
|
||||||
if the live weenie is Hook, Storage, or Corpse:
|
|
||||||
return ForceIntoCell(resident cell, frame)
|
|
||||||
|
|
||||||
set do_not_load_cells from flag 0x20
|
|
||||||
if !CheckPositionInternal(...):
|
|
||||||
handled = handle_all_collisions(...)
|
|
||||||
return handled ? COLLIDED : NO_VALID_POSITION
|
|
||||||
if transition.curr_cell == null:
|
|
||||||
return NO_CELL
|
|
||||||
commit the complete transition
|
|
||||||
return OK
|
|
||||||
```
|
|
||||||
|
|
||||||
`AdjustPosition` branches on the claimed cell shape. A direct outdoor claim
|
|
||||||
runs pure `LandDefs::adjust_to_outside` normalization before visible-cell
|
|
||||||
lookup. An indoor claim first resolves the visible cell and child; only a
|
|
||||||
resident indoor cell marked `seen_outside` falls back to outdoor
|
|
||||||
normalization. An absent indoor cell (including the `0xFFFF` sentinel) remains
|
|
||||||
the exact claimed cell/frame. A map-edge outdoor normalization failure stores
|
|
||||||
cell zero with the otherwise unchanged frame. The old `max(terrainZ, z)` lift
|
|
||||||
and nearest-in-Z scan do not occur in this canonical mechanism.
|
|
||||||
|
|
||||||
`CheckPositionInternal` calls the complete placement transition. Without the
|
|
||||||
slide flag, retail accepts the result only when signed
|
|
||||||
`resolvedX-requestedX <= 0.0500000007`, the same signed Y condition holds, and
|
|
||||||
the cell is unchanged. Z is not part of that predicate. The resolved origin is
|
|
||||||
accepted while the requested orientation remains intact.
|
|
||||||
|
|
||||||
## Two validators, not one
|
|
||||||
|
|
||||||
The similarly named retail helpers have different contracts and stay separate
|
|
||||||
in the port:
|
|
||||||
|
|
||||||
- `validate_placement_transition` is the inner `find_placement_pos` validator.
|
|
||||||
Any non-OK state from `COLLIDED` through `SLID`, when sliding is permitted,
|
|
||||||
resets `COLLISIONINFO`; it never retries placement.
|
|
||||||
- `validate_placement` is the outer initial/final validator. Only `ADJUSTED`
|
|
||||||
or `SLID`, and only while its retry argument is true, performs one
|
|
||||||
`placement_insert`; `COLLIDED` neither resets nor retries.
|
|
||||||
|
|
||||||
Step-down is disabled only for missiles. For fewer than two spheres retail
|
|
||||||
first clamps the requested height to half the radius when the sphere diameter
|
|
||||||
is less than or equal to that height. It then performs one full probe when the
|
|
||||||
diameter is greater than the resulting height, otherwise two half probes.
|
|
||||||
Equality belongs to the two-half-probe branch.
|
|
||||||
|
|
||||||
## Modern seam
|
|
||||||
|
|
||||||
`PhysicsEngine.SetPosition` returns one immutable
|
|
||||||
`PhysicsSetPositionResult`. `SetPositionError` retains the header values
|
|
||||||
(`OK=0`, `GENERAL=1`, `NO_VALID=2`, `NO_CELL=3`, `COLLIDED=4`,
|
|
||||||
`INVALID_ARGS=0x100`) while `PhysicsResidenceDisposition` separately reports
|
|
||||||
`Committed`, `DeferredCell`, or `Unchanged`. Missing content is therefore
|
|
||||||
successful-but-deferred, never misreported as a placement failure.
|
|
||||||
|
|
||||||
The result carries the complete commit packet: root/cell-local frame,
|
|
||||||
contact/walkable/water state, sliding and collision normals, stationary-fall
|
|
||||||
counter, a complete immutable `COLLISIONINFO` snapshot for the real
|
|
||||||
`handle_all_collisions` callback (including contact/last-contact, sliding,
|
|
||||||
collision normal, stationary-fall, environment, adjustment, and object
|
|
||||||
fields), the callback result, and an explicit shadow action. A PhysicsBSP or changed-cell force commit
|
|
||||||
requests canonical shadow recalculation; a non-BSP transition replaces its
|
|
||||||
shadows only when the transition produced a nonempty cell array, otherwise it
|
|
||||||
preserves the prior list. Unchanged force placement changes only the frame.
|
|
||||||
|
|
||||||
Core never creates cells synchronously, so retail flag `0x20`
|
|
||||||
(`DoNotCreateCells`) has no differential loader branch inside this pure
|
|
||||||
mechanism. Both flag states can only observe already-published immutable cell
|
|
||||||
content and otherwise return `DeferredCell`. Carrying the flag into exact-cell,
|
|
||||||
generation-scoped async admission is part of the still-open Slice 4B
|
|
||||||
adaptation tracked with AD-2; this slice does not claim a dead SpherePath field
|
|
||||||
as exact behavior.
|
|
||||||
|
|
||||||
The existing public `Resolve` compatibility entry remains entirely unchanged
|
|
||||||
for production movement and zero-delta callers. Its result cannot represent a
|
|
||||||
successful-but-deferred residence, so hiding `DeferredCell` inside
|
|
||||||
`ResolveResult.Ok` would corrupt the contract. Slice 4B will route every
|
|
||||||
authoritative placement family through `SetPosition`, atomically install its
|
|
||||||
packet, and own exact lost-cell wakeup/commit.
|
|
||||||
|
|
||||||
## Automated oracle
|
|
||||||
|
|
||||||
`PhysicsSetPositionTests` pins error values, absent/invalid outdoor and indoor
|
|
||||||
claims, cross-landblock frame normalization, map-edge failure and the `0xFFFF`
|
|
||||||
sentinel, dummy/authored sphere setup, nonpositive scale, Ethereal seeding,
|
|
||||||
explicit-only PathClipped, missile step-down, exact equality schedules, both validators,
|
|
||||||
the late compass sample's float bits, signed no-slide behavior, actual
|
|
||||||
collision-handler mapping, force-class policy, explicit shadow actions, null
|
|
||||||
current-cell wakeup, ten-record scratch exhaustion, exact scatter ordering,
|
|
||||||
failed-probe scratch lifetime, and deferred-scatter stop. The legacy public
|
|
||||||
Resolve fixture remains unchanged until Slice 4B.
|
|
||||||
|
|
||||||
## Slice 4B1 — Runtime residence owner
|
|
||||||
|
|
||||||
Slice 4B1 adds the presentation-independent half of the cutover without
|
|
||||||
changing a production graphical route yet. `RuntimeSetPositionState` accepts
|
|
||||||
an exact entity/position token before graphical DAT preparation, consumes the
|
|
||||||
immutable Core result, and commits body, contact, full cell, shadows, object
|
|
||||||
clock, and Runtime spatial worksets before publishing one ordered placement
|
|
||||||
delta. The delta carries the exact `RuntimeEntityKey`, session lifetime,
|
|
||||||
position/spatial/placement versions, adjusted cell, collision generation, and
|
|
||||||
optional portal-authority shape. A throwing or unavailable host does not roll
|
|
||||||
simulation back: Runtime republishes the same projection token until the
|
|
||||||
exact FIFO head is acknowledged. A newer operation changes an already-
|
|
||||||
published token to `Discard` and increments its projection revision, so an
|
|
||||||
acknowledgement of the previously observed Place/Withdraw cannot consume an
|
|
||||||
unseen Discard. It is never silently forgotten. An unacknowledged lost-cell
|
|
||||||
Withdraw transfers intact to a replacing accepted operation and remains the
|
|
||||||
FIFO head before that replacement may publish Place.
|
|
||||||
|
|
||||||
The successful missing-cell path owns retail's residence shape:
|
|
||||||
|
|
||||||
```text
|
|
||||||
SetPosition -> OK + DeferredCell
|
|
||||||
retain adjusted Position and the same PhysicsBody/components
|
|
||||||
clear only Active and suspend the object clock
|
|
||||||
withdraw Runtime spatial worksets and shadow rows
|
|
||||||
retain shadow registration and exact authored mover request
|
|
||||||
append parentless root to (exact cell, collision generation)
|
|
||||||
arm independent exact-key 25 s deadlines for root + direct children
|
|
||||||
publish Withdraw
|
|
||||||
|
|
||||||
exact cell generation resident + Withdraw acknowledged
|
|
||||||
re-run SetPosition with retained authored spheres and CurrentCellId=null
|
|
||||||
atomically install complete result
|
|
||||||
publish Place
|
|
||||||
```
|
|
||||||
|
|
||||||
Lost-cell membership buckets use retail-shaped append plus swap-remove.
|
|
||||||
Destruction deadlines use an exact-key hash plus a bounded indexed min-heap,
|
|
||||||
the allocation-bounded modern equivalent of retail's hash +
|
|
||||||
`PQueueArray<double>` priority owner. Rearm/removal updates the exact heap node;
|
|
||||||
there are no stale tombstones. Only committed, current direct children from
|
|
||||||
the parent-incarnation ordered CHILDLIST participate; unresolved or future
|
|
||||||
relations cannot inherit a deadline. Parent, pickup, delete, GUID
|
|
||||||
replacement, newer Position, reset, and disposal cancel the exact incarnation
|
|
||||||
and use leave-world semantics rather than a wakeable lost entry. The dormant
|
|
||||||
collision-retirement entry parks non-static parentless indoor roots and, for
|
|
||||||
complete withdrawal, affected outdoor roots. It performs a complete preflight
|
|
||||||
and rejects overlap with any active accepted/host-ack-pending placement before
|
|
||||||
mutating one resident. It then installs every affected canonical lost
|
|
||||||
residence and operation before publishing the first synchronous Withdraw, so
|
|
||||||
an observer re-entering for a later root inherits that root's exact pending
|
|
||||||
Withdraw instead of having its newly accepted placement cancelled by the
|
|
||||||
retirement loop. 4B2 must quiesce that placement prefix before invoking the
|
|
||||||
entry in the same transaction that installs host acknowledgements.
|
|
||||||
|
|
||||||
Runtime retains the last accepted prepared mover request, including exact
|
|
||||||
off-center/two-sphere payloads, scale, flags, and step values. A cold resident
|
|
||||||
with no prepared request still withdraws atomically but remains explicitly in
|
|
||||||
`AwaitingPreparation`; it cannot wake through an invented empty-sphere shape.
|
|
||||||
Preparation is cached only after Core accepts it as Committed or DeferredCell.
|
|
||||||
A rejected/malformed preparation keeps the same accepted token retryable and
|
|
||||||
cannot replace the last validated mover used by later collision retirement.
|
|
||||||
Runtime's host boundary rejects only non-finite consumed frame/shape values;
|
|
||||||
retail-valid oddities such as nonpositive authored scale remain untouched.
|
|
||||||
Likewise, a non-deferred wake failure retains the withdrawn body, independent
|
|
||||||
25-second lifetime, and exact operation: invalid arguments return to
|
|
||||||
AwaitingPreparation and re-index for the next exact generation, while other
|
|
||||||
world-placement failures also re-index. The last successful DeferredCell
|
|
||||||
result and adjusted frame remain canonical across the failed attempt. No
|
|
||||||
failed wake can leave a live entity withdrawn without a Runtime owner. The
|
|
||||||
graphical/no-window cutover in 4B2 supplies that exact preparation token.
|
|
||||||
|
|
||||||
Retail `CPhysicsObj::SetPositionInternal` (`0x00515BD0`) calls
|
|
||||||
`prepare_to_enter_world` (`0x00511FA0`) only when `this->cell == 0`.
|
|
||||||
Consequently the physics `update_time` (`PhysicsBody.LastUpdateTime`) and
|
|
||||||
active bit are reset only on the cellless-to-world edge. Ordinary same-cell or
|
|
||||||
cross-cell in-world SetPosition preserves the already-consumed physics clock;
|
|
||||||
entering the lost-cell residence also preserves it until the eventual
|
|
||||||
cellless wake commit. Runtime pins both sides and does not inherit the older
|
|
||||||
graphical teleport helper's unconditional timer reset.
|
|
||||||
|
|
||||||
The wake timestamp is in the Runtime simulation-time domain, never Unix/UTC:
|
|
||||||
`GameRuntime` binds its instance `GameRuntimeClock` through the entity/physics
|
|
||||||
owner, and RetryDeferred samples `SimulationTimeSeconds`. Standalone Runtime
|
|
||||||
fixtures without a bound game clock retain the accepted command time. This is
|
|
||||||
a Runtime dependency only; no App delegate enters the owner.
|
|
||||||
|
|
||||||
The canonical commit also installs every SetPosition-derived body invariant
|
|
||||||
before host publication: Contact/OnWalkable/WaterContact, the current contact
|
|
||||||
plane and slope `GroundNormal`, Sliding plus its normal, and the complete
|
|
||||||
StationaryFall/Stop/Stuck encoding. Named retail
|
|
||||||
`CPhysicsObj::SetPositionInternal(CTransition const*)` (`0x00515330`) copies
|
|
||||||
only the transition's current contact plane/water flag, walkability, sliding
|
|
||||||
normal/valid flag, and collision state (`0x005153E5–0x005154FE`). It does not
|
|
||||||
publish `last_known_contact_plane` or the SpherePath walkable polygon on this
|
|
||||||
path, so those ordinary-update-only fields are intentionally absent from the
|
|
||||||
immutable SetPosition result and remain unchanged. A zero expected velocity
|
|
||||||
version in host preparation preserves the nonzero version captured when the
|
|
||||||
operation was accepted; an intervening Vector/Movement therefore suppresses
|
|
||||||
only the stale collision-velocity response. A bodyless cancellation terminates
|
|
||||||
without fabricating a PhysicsBody or a host Withdraw projection. The two time
|
|
||||||
domains remain explicit: `PhysicsBody.LastUpdateTime` consumes the instance
|
|
||||||
simulation clock, while `IRuntimeRemotePlacement.LastServerPositionTime`
|
|
||||||
remains Unix-UTC receipt time because the remote stale-velocity owner ages it
|
|
||||||
against `RuntimePhysicsState.UtcNowSeconds`. A deferred wake therefore cannot
|
|
||||||
make fresh authoritative remote velocity appear years old. Runtime preparation
|
|
||||||
also applies the existing retail `PositionFrameValidation` before Core or
|
|
||||||
prepared-mover caching, and caps synchronous Scatter/RandomScatter work at 64
|
|
||||||
attempts; this keeps valid authored retail request shapes while rejecting a
|
|
||||||
hostile `uint.MaxValue` loop at the authority boundary.
|
|
||||||
|
|
||||||
One authority boundary intentionally remains open for 4B2:
|
|
||||||
|
|
||||||
- `RuntimePortalPlacementAuthority` validates immutable token shape only;
|
|
||||||
4B2 must bind it to active `RuntimeWorldTransitState` generation, teleport
|
|
||||||
sequence, destination, and host acknowledgement before reveal.
|
|
||||||
|
|
||||||
The former collision-report boundary is closed by
|
|
||||||
`RuntimeCollisionReportingState`. Runtime now owns retail's exact-key object
|
|
||||||
contact table, environment latch, strict ordinary/ethereal expiry, force-end,
|
|
||||||
static and `ReportAsEnvironment` routing, reciprocal callback eligibility,
|
|
||||||
missile-state clearing, ordered reentrant dispatch, and the report-result
|
|
||||||
boolean which distinguishes placement `Collided` from `NoValidPosition`.
|
|
||||||
Successful SetPosition commits reporting after Contact/OnWalkable and ground
|
|
||||||
callbacks but before its single physical response and shadow reflood. See
|
|
||||||
`docs/research/2026-07-31-runtime-set-position-collision-reporting.md`.
|
|
||||||
|
|
||||||
### Slice 4B2 checkpoint 1 — public dormant host seam
|
|
||||||
|
|
||||||
The first 4B2 checkpoint exposes the dormant receipt owner through
|
|
||||||
`RuntimePlacementProjectionChannel`. Graphical and no-window hosts can observe
|
|
||||||
the one ordered placement stream, retry the exact immutable pending receipts,
|
|
||||||
peek the FIFO head, measure pending debt, and acknowledge only the exact head.
|
|
||||||
Mutation and retry calls require the current `RuntimeGenerationToken`; a stale
|
|
||||||
generation, stale revision, reordered token, duplicate acknowledgement, or
|
|
||||||
reused GUID cannot consume current placement debt. The channel delegates to
|
|
||||||
`RuntimeSetPositionState` and `RuntimeEntityObjectEventStream`; it owns no
|
|
||||||
second queue, mirror, or rollback path.
|
|
||||||
|
|
||||||
Shared local-controller body adoption is deliberately deferred. A reviewed
|
|
||||||
prototype that prepared directly on the canonical body was rejected: a
|
|
||||||
snapshot/rollback lease cannot safely coexist with reentrant SetPosition,
|
|
||||||
remote/projectile binding, deletion/GUID reuse, owner replacement, object-clock
|
|
||||||
epoch changes, or disposal. Correct adoption requires either an exclusive
|
|
||||||
Runtime transaction integrated with every canonical writer, or off-canonical
|
|
||||||
preparation followed by one validated atomic body/controller publication.
|
|
||||||
Either choice belongs to the all-route ownership cutover, not this narrow
|
|
||||||
dormant-seam checkpoint.
|
|
||||||
|
|
||||||
This remains a deliberately non-activating checkpoint. Production spawn,
|
|
||||||
Position, projectile, drop/pickup/parent, and portal routes do not submit to
|
|
||||||
the dormant SetPosition owner yet. The cutover remains blocked on exact
|
|
||||||
ordered Setup spheres/scale/step heights/flags/cell-local preparation,
|
|
||||||
presentation-only rebucketing, and placement-prefix quiescence before
|
|
||||||
collision retirement. AP-1 and AD-1 remain open until those prerequisites and
|
|
||||||
every production route land together.
|
|
||||||
|
|
||||||
AD-2 remains the explicit async adaptation: collision readiness can publish in
|
|
||||||
a different frame from retail's blocking load. A failed wake is safely re-
|
|
||||||
indexed to the next exact generation instead of inheriting retail's
|
|
||||||
synchronous assumption. When older unbound survivors meet newer entities
|
|
||||||
already indexed into that future generation, Runtime merges them into one
|
|
||||||
bucket with the older survivor order first and retains one bucket-order entry.
|
|
||||||
AP-1 and AD-1 remain open until 4B2 removes the legacy graphical/headless
|
|
||||||
placement paths.
|
|
||||||
|
|
||||||
`RuntimeSetPositionStateTests` pins accepted-before-preparation ownership,
|
|
||||||
portal-shape rejection, canonical-before-projection ordering, retry and
|
|
||||||
reentrant discard, cross-landblock adjusted-cell park/wake, same-body
|
|
||||||
identity, retained contact/water/sliding/velocity, exact generation gating,
|
|
||||||
authored two-sphere retention, cold preparation, bounded priority-deadline
|
|
||||||
rearm/cancel, zero-allocation empty ticks, independent ordered direct-child
|
|
||||||
deadlines, actual collision-admission supersession/invalidation, missing exact
|
|
||||||
indoor-cell generation rebind/wake, collision demotion/withdrawal preflight,
|
|
||||||
failed-wake retry, malformed-preparation retry without cache poisoning,
|
|
||||||
simulation-clock-domain wake, velocity-version preservation, derived body-bit
|
|
||||||
writeback, immediate remote-velocity survival across the simulation/UTC clock
|
|
||||||
boundary, invalid cell/frame/quaternion and extreme-scatter rejection before
|
|
||||||
Core/caching, bodyless cancellation, newer Position/pickup/parent/delete, GUID
|
|
||||||
reuse, reset, and complete index/node terminal convergence. Existing zero-
|
|
||||||
allocation collision-generation gates remain unchanged on the no-deferred
|
|
||||||
fast path.
|
|
||||||
|
|
||||||
The warmed immediate commit/ack route currently measures exactly **1,880
|
|
||||||
managed bytes per operation** in the Release Runtime test host (1,000
|
|
||||||
iterations after 64 warmups); the regression gate caps it at 2,048 bytes.
|
|
||||||
This dormant-path result is an explicit 4B2 activation blocker rather than a
|
|
||||||
claim of allocation-free production readiness: 4B2 must either pool/remove
|
|
||||||
the operation and projection envelopes or record an approved measured budget
|
|
||||||
before routing frame-frequency placement through this owner.
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# Retail cell availability and containment-root validation — 2026-07-31
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This note closes divergence rows AD-3 and AD-4. It does not begin AD-6's
|
|
||||||
atomic streaming-generation work.
|
|
||||||
|
|
||||||
The corrected port distinguishes these states:
|
|
||||||
|
|
||||||
1. no visible cell payload is loaded;
|
|
||||||
2. a malformed raw/prepared payload has no containment root;
|
|
||||||
3. a loaded CellStruct has a valid authored containment root (its physics root
|
|
||||||
may independently be absent).
|
|
||||||
|
|
||||||
Only (3) is published. State (1) remains unavailable and retryable. State (2)
|
|
||||||
is quarantined atomically so a later valid hydration can retry; it must not
|
|
||||||
become a world-wide containing cell.
|
|
||||||
|
|
||||||
## Installed-data audit
|
|
||||||
|
|
||||||
The complete installed EoR catalog and matching prepared package were audited
|
|
||||||
before choosing this invariant:
|
|
||||||
|
|
||||||
- enumerated EnvCells: **729,888**;
|
|
||||||
- raw: 0 missing EnvCells, 0 missing Environments, 0 missing CellStructs,
|
|
||||||
0 null `CellBSP` objects, **0 null `CellBSP.Root`**, 729,888 valid roots;
|
|
||||||
- prepared `acdream.pak`: 0 missing aliases, 0 corrupt payloads,
|
|
||||||
**0 `ContainmentBsp.RootIndex < 0`**, 729,888 valid roots;
|
|
||||||
- 6,940 raw EnvCells have zero portals, so the retail portal-pointer guard is
|
|
||||||
a real catalog path rather than dead defensive code.
|
|
||||||
|
|
||||||
There are therefore no root-null record IDs to preserve in either source.
|
|
||||||
|
|
||||||
## Retail oracle
|
|
||||||
|
|
||||||
`CObjCell::find_cell_list @ 0x0052B4E0` in
|
|
||||||
`docs/research/named-retail/acclient_2013_pseudo_c.txt:308742` establishes the
|
|
||||||
availability gates:
|
|
||||||
|
|
||||||
- `CEnvCell::GetVisible` / `CLandCell::GetVisible` resolves the active seed at
|
|
||||||
`0x0052B50C..0x0052B515`;
|
|
||||||
- the outdoor branch still calls `CLandCell::add_all_outside_cells` at
|
|
||||||
`0x0052B53F`, even when that seed lookup returned null;
|
|
||||||
- the complete growing-array transit walk and containing-cell pick are gated
|
|
||||||
by `seed != null && num_spheres != 0` at `0x0052B576`;
|
|
||||||
- each later candidate is independently skipped when its stored cell pointer
|
|
||||||
is null at `0x0052B58E`.
|
|
||||||
|
|
||||||
`CEnvCell::point_in_cell @ 0x0052C300` first returns false when
|
|
||||||
`this->portals == 0`, then transforms the point and calls
|
|
||||||
`CCellStruct::point_in_cell`.
|
|
||||||
|
|
||||||
`CCellStruct::point_in_cell @ 0x005338F0` calls
|
|
||||||
`BSPTREE::point_inside_cell_bsp @ 0x005398C0`, which immediately invokes
|
|
||||||
`BSPNODE::point_inside_cell_bsp(this->root_node, ...)`. The BSP node method at
|
|
||||||
`0x0053C1F0` dereferences `this` before walking positive children. Only a
|
|
||||||
missing **positive child below a valid root** is the inside terminal case. A
|
|
||||||
missing root is not.
|
|
||||||
|
|
||||||
## Ported behavior
|
|
||||||
|
|
||||||
- `PhysicsDataCache` publishes graph, collision, and prepared records only
|
|
||||||
after a valid raw/prepared containment root is present. A missing physics
|
|
||||||
root is retained as a valid non-colliding cell. Invalid containment
|
|
||||||
publication changes no cache, so later hydration can retry.
|
|
||||||
- `CollisionTraversal.HasCellContainment` tests `Root` / `RootIndex`.
|
|
||||||
- Both raw and prepared `EnvCell.PointInCell` paths apply the zero-portals
|
|
||||||
guard before containment. `CellTransit` applies the same guard to its
|
|
||||||
`CellPhysics` representation.
|
|
||||||
- `CellTransit.BuildShadowCellSet` still seeds all overlapped outdoor cells,
|
|
||||||
but skips the transit walk when the active outdoor seed cannot be resolved
|
|
||||||
from `CellGraph`. Every later outdoor candidate independently resolves via
|
|
||||||
`GetVisible` before building transit, so a stale building cannot promote an
|
|
||||||
object through an unavailable adjacent landcell.
|
|
||||||
- The existing reflood lifecycle remains the recovery mechanism. Once terrain
|
|
||||||
or a valid indoor CellStruct publishes, the next reflood walks the authored
|
|
||||||
portal/building relationships without reconstructing a different rule.
|
|
||||||
|
|
||||||
## Gates
|
|
||||||
|
|
||||||
Focused tests cover raw/prepared rootless quarantine and valid retry, valid
|
|
||||||
containment with missing physics, raw/prepared zero-portal parity, indoor and
|
|
||||||
outdoor seeds, preservation of outside-cell seeding, per-candidate adjacent
|
|
||||||
landcell availability, suppression of stale-building promotion, and
|
|
||||||
hydration/reflood recovery. The corrective checkpoint passes:
|
|
||||||
|
|
||||||
- focused cell-availability suite: **54/54**;
|
|
||||||
- Core Release: **4,165 passed / 1 skipped**;
|
|
||||||
- Runtime Release: **440/440**;
|
|
||||||
- App Release: **4,002 passed / 3 skipped**;
|
|
||||||
- complete Release solution: **10,122 passed / 4 skipped**;
|
|
||||||
- `dotnet build AcDream.slnx -c Release`: **0 warnings / 0 errors**.
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
# Issue #273 — Holtburg tight-gap support validation
|
|
||||||
|
|
||||||
**Date:** 2026-07-31
|
|
||||||
**Status:** implementation, automated gates, and exact live gate pass
|
|
||||||
**Scope:** grounded player step-down support at a floor edge beside a static
|
|
||||||
cylinder
|
|
||||||
|
|
||||||
## Captured scene
|
|
||||||
|
|
||||||
The reproducible gap is in outdoor cell `0xA9B40032`, between:
|
|
||||||
|
|
||||||
- building shell GfxObj `0x01000F69`, placed at
|
|
||||||
`(158.178, 37.7055, 94.0)` with quaternion
|
|
||||||
`(w=.939319, x=0, y=0, z=-.343045)`;
|
|
||||||
- static post `0xCA9B4027`, placed at `(160.173, 34.487, 95.975)`,
|
|
||||||
represented by its Setup-authored cylinder (`radius=.282`,
|
|
||||||
`height=5.564`);
|
|
||||||
- the local player Setup's exact two spheres (`radius=.48`, origins
|
|
||||||
`z=.475` and `z=1.35`).
|
|
||||||
|
|
||||||
The building's supporting ledge terminates at local `x=4`. The first
|
|
||||||
post-side response moved the player's foot-sphere center to approximately
|
|
||||||
local `x=4.33`. The full `.48` movement sphere still overlapped the floor, so
|
|
||||||
the existing step-down path accepted the candidate. Repeated frames then
|
|
||||||
carried the player around the post and outside the building shell.
|
|
||||||
|
|
||||||
The fixture
|
|
||||||
`tests/AcDream.Core.Tests/Fixtures/issue273/0x01000F69.gfxobj.json` preserves
|
|
||||||
the installed DAT PhysicsBSP. The replay in
|
|
||||||
`Issue273HoltburgTightGapReplayTests` uses the captured object placement,
|
|
||||||
player spheres, static posts, and movement offsets.
|
|
||||||
|
|
||||||
## Retail mechanism
|
|
||||||
|
|
||||||
The missing rule is not extra collision padding and is not a larger player
|
|
||||||
sphere. It is retail's second-stage support validation:
|
|
||||||
|
|
||||||
1. `CTransition::step_down` (`0x0050B2A0`) performs the ordinary downward
|
|
||||||
collision probe.
|
|
||||||
2. After finding a walkable contact plane, an EdgeSlide mover that is not in
|
|
||||||
StepUp calls `CTransition::check_walkable` (`0x0050AFF0`). The binary
|
|
||||||
sequence is `test ah,2` at `0x0050B36A`, which is state bit `0x200`
|
|
||||||
(`EdgeSlide`), followed by the `step_up == 0` test and call at
|
|
||||||
`0x0050B380`.
|
|
||||||
3. `CTransition::check_walkable` first calls
|
|
||||||
`SPHEREPATH::check_walkables` (`0x0050C3E0`).
|
|
||||||
4. `SPHEREPATH::check_walkables` halves the saved foot-sphere radius and
|
|
||||||
calls `CPolygon::check_walkable` (`0x00538E60`).
|
|
||||||
5. If the remembered polygon does not support that smaller sphere,
|
|
||||||
`CTransition::check_walkable` performs a downward CheckWalkable insertion.
|
|
||||||
BSP leaves require both `walkable_hits_sphere` and
|
|
||||||
`CPolygon::check_small_walkable` (`BSPLEAF::hits_walkable`,
|
|
||||||
`0x0053D670`).
|
|
||||||
6. If neither check finds support, `CTransition::step_down` rejects the
|
|
||||||
candidate and the existing edge-response chain handles it.
|
|
||||||
|
|
||||||
ACDream already had the small-radius BSP-leaf test, but
|
|
||||||
`DoCheckWalkable` treated the mere presence of a remembered polygon as
|
|
||||||
success, and the ordinary `DoStepDown(..., runPlacement:false)` path never
|
|
||||||
called it. This let a full-radius overlap stand in for actual foot support.
|
|
||||||
|
|
||||||
## Port
|
|
||||||
|
|
||||||
- `BSPQuery.CheckWalkableSupport` is the shared resolved-polygon form of
|
|
||||||
retail `CPolygon::check_walkable`.
|
|
||||||
- `SpherePath.CheckWalkables` implements the retail half-radius remembered
|
|
||||||
polygon check without mutating canonical sphere state.
|
|
||||||
- `Transition.DoCheckWalkable` now tests the remembered polygon rather than
|
|
||||||
treating a non-null polygon as sufficient.
|
|
||||||
- `Transition.DoStepDown` restores the EdgeSlide/non-StepUp support gate
|
|
||||||
before the existing placement-policy seam.
|
|
||||||
|
|
||||||
There are no location checks, object IDs, guessed radii, widened collision
|
|
||||||
shapes, or gap-specific tolerances in the production fix.
|
|
||||||
|
|
||||||
## Regression impact
|
|
||||||
|
|
||||||
The existing #271 staircase-side replay begins with its center `.288 m`
|
|
||||||
outside a tread whose retail half-radius support boundary is `.24 m`.
|
|
||||||
Retail may therefore stop that exact candidate. The test now preserves the
|
|
||||||
original user-visible invariant—never reverse or accelerate downhill—without
|
|
||||||
requiring forward progress beyond retail's support boundary. The ordinary
|
|
||||||
continuous staircase replay still requires and achieves forward progress.
|
|
||||||
|
|
||||||
## Gates
|
|
||||||
|
|
||||||
- issue #273 fixture/replay: 3 passed;
|
|
||||||
- focused BSP, step-up, edge-slide, #185/#271 family: 42 passed / 1 skipped;
|
|
||||||
- complete Core tests: 4,111 passed / 2 skipped;
|
|
||||||
- Release solution build: passed;
|
|
||||||
- complete Release solution tests: 10,068 passed / 5 skipped.
|
|
||||||
|
|
||||||
The user accepted the exact in-client Holtburg gap gate on 2026-07-31: the
|
|
||||||
gap blocks from the tested approach, and the adjacent movement checks remain
|
|
||||||
healthy.
|
|
||||||
|
|
@ -1,497 +0,0 @@
|
||||||
# Remaining physics-divergence campaign handoff — 2026-07-31
|
|
||||||
|
|
||||||
> **Checkpoint 2 update:** Slice 4B2 prerequisite A, Runtime SetPosition
|
|
||||||
> collision-report ownership, is implemented in the next checkpoint. Continue
|
|
||||||
> with the dedicated
|
|
||||||
> [`runtime SetPosition collision-reporting handoff`](2026-07-31-runtime-set-position-collision-reporting-handoff.md),
|
|
||||||
> not the prerequisite-A instructions preserved below as historical context.
|
|
||||||
|
|
||||||
## Purpose and stopping point
|
|
||||||
|
|
||||||
This is the deliberate handoff boundary requested after placement Slice 4B2
|
|
||||||
checkpoint 1. The repository is stopped before any production graphical or
|
|
||||||
headless route submits to the canonical Runtime SetPosition owner.
|
|
||||||
|
|
||||||
The completed foundation is useful and tested, but the overall campaign is
|
|
||||||
**not complete**. AP-1 and AD-1 remain narrowed/open. AP-22 and AD-10 remain
|
|
||||||
open. Do not retire those rows until their exact automated and connected gates
|
|
||||||
pass.
|
|
||||||
|
|
||||||
### Exact workspace
|
|
||||||
|
|
||||||
- Worktree: `C:\Users\erikn\.codex\worktrees\af5e\acdream`
|
|
||||||
- Branch: `codex/port-claude-agents`
|
|
||||||
- Handoff code checkpoint: `270f5154`
|
|
||||||
(`feat(runtime): expose dormant placement receipts`)
|
|
||||||
- Immediately preceding residence-owner checkpoint: `4c02ac42`
|
|
||||||
(`feat(runtime): own deferred set-position residence`)
|
|
||||||
- Pure Core SetPosition checkpoint: `e84a388e`
|
|
||||||
(`feat(physics): port canonical retail set-position core`)
|
|
||||||
- No upstream is configured for this worktree branch.
|
|
||||||
- Remotes:
|
|
||||||
- `origin`: `https://git.snakedesert.se/erik/acdream.git`
|
|
||||||
- `github`: `git@github.com:eriknihlen/acdream.git`
|
|
||||||
|
|
||||||
The handoff was written in this same worktree. The next agent should continue
|
|
||||||
there rather than creating a different worktree unless the user explicitly
|
|
||||||
requests it.
|
|
||||||
|
|
||||||
## Worktree hygiene
|
|
||||||
|
|
||||||
The worktree intentionally reports unrelated modifications. Preserve them.
|
|
||||||
Never use `git add -A`, `git reset --hard`, or checkout/revert commands against
|
|
||||||
these paths.
|
|
||||||
|
|
||||||
At the checkpoint, `AGENTS.md` has a real unrelated content diff. The following
|
|
||||||
paths report modified due to existing line-ending/stat noise but have no
|
|
||||||
content diff against the index:
|
|
||||||
|
|
||||||
- `src/AcDream.App/Input/PlayerModeController.cs`
|
|
||||||
- `src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs`
|
|
||||||
- `src/AcDream.App/Rendering/LiveAnimationPresentationContext.cs`
|
|
||||||
- `src/AcDream.App/World/LiveEntityRuntime.cs`
|
|
||||||
- `src/AcDream.Core/Physics/CellArray.cs`
|
|
||||||
- `src/AcDream.Core/Physics/PhysicsBody.cs`
|
|
||||||
- `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs`
|
|
||||||
- `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimePhysicsState.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs`
|
|
||||||
- `tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs`
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/CellTransitTests.cs`
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/Issue133DungeonTeleportPrefixTests.cs`
|
|
||||||
- `tests/AcDream.Runtime.Tests/Gameplay/PlayerMovementControllerTests.cs`
|
|
||||||
- `tools/A8CellAudit/A8CellAudit.csproj`
|
|
||||||
|
|
||||||
Before every commit, stage exact paths and inspect:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git diff --check
|
|
||||||
git diff --cached --check
|
|
||||||
git diff --cached --stat
|
|
||||||
git status --short
|
|
||||||
```
|
|
||||||
|
|
||||||
## What is complete
|
|
||||||
|
|
||||||
### Campaign baseline and issue #273
|
|
||||||
|
|
||||||
- `c24bc571` — retail StepDown support-radius behavior for tight gaps.
|
|
||||||
- `10b55d74` — tight-gap controls and diagnostics.
|
|
||||||
|
|
||||||
### Retail retry and edge/StepDown dispatcher
|
|
||||||
|
|
||||||
- `e5f855ac` — nested per-cell collision retries.
|
|
||||||
- `67d1e9b3` — refreshed-cell retry state.
|
|
||||||
- `4ca7230b` — retained cell across inner retries.
|
|
||||||
- `c559c48d` — retail edge-response ordering.
|
|
||||||
- `4fbd93ec` — edge-slide stop semantics.
|
|
||||||
- `1fd5da67` — StepDown placement validation.
|
|
||||||
- `acec33ec` — StepDown probe state.
|
|
||||||
- `75b6f6b6` — Path-6 collision response.
|
|
||||||
- `d3c0d9ec` — TS-4 production chronology gate.
|
|
||||||
|
|
||||||
These retire AP-3, AP-4, AP-5, AD-53, AD-54, and TS-4 under the tests and
|
|
||||||
research already recorded in the divergence register.
|
|
||||||
|
|
||||||
### Exact cell availability and atomic collision generations
|
|
||||||
|
|
||||||
- `7716c2ee` — retail cell-availability semantics.
|
|
||||||
- `3e0f3b62` — containment-root validation.
|
|
||||||
- `be94bc9b` — atomic collision-generation activation.
|
|
||||||
- `d94145e6` — seal before activation.
|
|
||||||
- `6b28ff99` — starvation-free activation.
|
|
||||||
|
|
||||||
These retire AD-3, AD-4, and AD-6. The active collision world remains visible
|
|
||||||
until a complete replacement generation is atomically committed.
|
|
||||||
|
|
||||||
### Canonical SetPosition Core and Runtime residence owner
|
|
||||||
|
|
||||||
- `e84a388e` ports the pure Core SetPosition transaction.
|
|
||||||
- `4c02ac42` adds `RuntimeSetPositionState`, including:
|
|
||||||
- exact accepted operation ownership;
|
|
||||||
- canonical body/contact/cell/shadow/workset commit;
|
|
||||||
- authored mover retention;
|
|
||||||
- exact-cell and collision-generation wake;
|
|
||||||
- 25-second root/direct-child lost-cell lifetime;
|
|
||||||
- bounded indexed deadline structures;
|
|
||||||
- revisioned ordered Withdraw/Place/Discard host receipts;
|
|
||||||
- cancellation/GUID-reuse/reset/disposal convergence;
|
|
||||||
- structural cell/quaternion validation and bounded scatter work.
|
|
||||||
- `270f5154` adds `RuntimePlacementProjectionChannel`, a public,
|
|
||||||
generation-gated host seam over the one existing receipt owner:
|
|
||||||
- subscribe to ordered immutable receipts;
|
|
||||||
- peek the exact FIFO head;
|
|
||||||
- retry pending receipts without recommitting Runtime state;
|
|
||||||
- acknowledge only the exact current FIFO-head token;
|
|
||||||
- observe pending receipt debt.
|
|
||||||
|
|
||||||
The channel owns no second queue and has no App or Headless production
|
|
||||||
consumer. A source guard pins that dormancy. This is intentional.
|
|
||||||
|
|
||||||
### Validation at the stopping point
|
|
||||||
|
|
||||||
The final channel-only checkpoint passed:
|
|
||||||
|
|
||||||
- Release solution build: 0 errors, 18 existing warnings.
|
|
||||||
- Complete Release suite: 10,279 passed, 4 skipped, 0 failed.
|
|
||||||
- Runtime SetPosition focused tests: 47/47.
|
|
||||||
- App dormancy/ownership guards: 3/3.
|
|
||||||
- `git diff --check`: clean.
|
|
||||||
- Retail-conformance re-review: clean.
|
|
||||||
- Architecture/adversarial re-review: clean.
|
|
||||||
|
|
||||||
## Rejected prototype — do not resurrect it
|
|
||||||
|
|
||||||
A prototype made graphical and headless `PlayerMovementController` instances
|
|
||||||
borrow the canonical `RuntimeEntityRecord.PhysicsBody` immediately and used a
|
|
||||||
snapshot/rollback lease to recover from late construction failures. It was
|
|
||||||
fully removed before `270f5154`.
|
|
||||||
|
|
||||||
The design was rejected because it was failure-atomic only without
|
|
||||||
reentrancy. While the lease was open, a nested SetPosition, remote/projectile
|
|
||||||
bind or update, deletion/GUID replacement, object-clock epoch change, or
|
|
||||||
disposal could establish newer authority. The outer rollback could then erase
|
|
||||||
that newer commit or detach a body already used by another canonical owner.
|
|
||||||
|
|
||||||
The next implementation must use one of these complete solutions:
|
|
||||||
|
|
||||||
1. A Runtime-owned exclusive/versioned controller/body publication
|
|
||||||
transaction respected by **every** canonical body writer, binding path,
|
|
||||||
SetPosition operation, clock epoch transition, deletion, reset, and
|
|
||||||
disposal; or
|
|
||||||
2. Off-canonical preparation followed by one validated atomic Runtime commit
|
|
||||||
that publishes the prepared controller/body relationship without copying
|
|
||||||
stale state over newer authority.
|
|
||||||
|
|
||||||
Because every writer must participate, this belongs to the atomic production
|
|
||||||
route cutover. Do not reintroduce a local snapshot lease or a Commit method
|
|
||||||
that merely checks the body reference at the end.
|
|
||||||
|
|
||||||
## What remains — required execution order
|
|
||||||
|
|
||||||
### Slice 4B2 prerequisite A — real collision-report ownership
|
|
||||||
|
|
||||||
`RuntimePhysicsState.HandleSetPositionCollisions` still returns `false`.
|
|
||||||
Retail `CPhysicsObj::SetPositionInternal` (`0x00515330`) returns the real
|
|
||||||
per-object report/tracking result. Establish the Runtime owner for that result
|
|
||||||
and return it exactly. Do not restore the former environment/object-presence
|
|
||||||
guess.
|
|
||||||
|
|
||||||
Required tests:
|
|
||||||
|
|
||||||
- report/no-report objects;
|
|
||||||
- collided object set ordering and lifetime;
|
|
||||||
- reentrant deletion/reset;
|
|
||||||
- graphical/headless equality;
|
|
||||||
- no report state surviving GUID reuse.
|
|
||||||
|
|
||||||
### Slice 4B2 prerequisite B — exact authored mover preparation
|
|
||||||
|
|
||||||
Every production preparation must pass the full SetPosition request using:
|
|
||||||
|
|
||||||
- Setup's exact ordered authored spheres;
|
|
||||||
- exact scale, including valid zero/presence semantics;
|
|
||||||
- exact StepUp and StepDown heights;
|
|
||||||
- exact flags;
|
|
||||||
- exact cell-local frame and orientation;
|
|
||||||
- current position/vector/state authority versions.
|
|
||||||
|
|
||||||
Do not reconstruct a cylinder from visual radius/height, clamp a positive
|
|
||||||
scale, use the projectile mover as a generic object fallback, or pre-mutate
|
|
||||||
`FullCellId`, `PhysicsBody`, `WorldEntity`, App buckets, or shadows.
|
|
||||||
|
|
||||||
### Slice 4B2 prerequisite C — atomic local controller/body publication
|
|
||||||
|
|
||||||
Implement the complete transaction described in the rejected-prototype note.
|
|
||||||
Graphical and no-window controllers must end with the exact same Runtime body,
|
|
||||||
but construction cannot expose or mutate canonical state before the validated
|
|
||||||
atomic commit.
|
|
||||||
|
|
||||||
Adversarial gates must include:
|
|
||||||
|
|
||||||
- nested construction;
|
|
||||||
- reentrant SetPosition;
|
|
||||||
- remote and projectile binding/update;
|
|
||||||
- deletion and same-GUID new incarnation;
|
|
||||||
- projection-owner replacement;
|
|
||||||
- object-clock epoch change;
|
|
||||||
- reset and disposal;
|
|
||||||
- commit and rollback after replacement;
|
|
||||||
- late graphical camera/shadow/host failure;
|
|
||||||
- late headless prepared-collision failure.
|
|
||||||
|
|
||||||
### Slice 4B2 prerequisite D — presentation-only host projection
|
|
||||||
|
|
||||||
Add one graphical and one headless `IRuntimePlacementObserver` using
|
|
||||||
`GameRuntime.Placements`.
|
|
||||||
|
|
||||||
Host receipt rules:
|
|
||||||
|
|
||||||
- `Withdraw`: remove render/spatial presentation, picking, radar, audio, and
|
|
||||||
targeting while retaining logical Runtime ownership.
|
|
||||||
- `Place`: project only the immutable Runtime-committed frame, then
|
|
||||||
acknowledge the exact token.
|
|
||||||
- `Discard`: discard the older projection revision, then acknowledge it.
|
|
||||||
- A host exception or unavailable backend does not roll Runtime back; retry
|
|
||||||
the same FIFO head.
|
|
||||||
|
|
||||||
`LiveEntityRuntime.RebucketLiveEntity` must become presentation-only. Its
|
|
||||||
current `CommitRebucket` call is a second spatial authority and must be removed
|
|
||||||
as part of the same cutover.
|
|
||||||
|
|
||||||
### Slice 4B2 prerequisite E — collision-prefix quiescence
|
|
||||||
|
|
||||||
Before landblock collision demotion, removal, or replacement:
|
|
||||||
|
|
||||||
1. quiesce the prefix;
|
|
||||||
2. drain/ack the existing placement receipt prefix;
|
|
||||||
3. call `RuntimeSetPositionState.ParkCollisionResidents`;
|
|
||||||
4. commit/withdraw the collision generation atomically;
|
|
||||||
5. wake only exact cell+generation residents.
|
|
||||||
|
|
||||||
Cover `LandblockPhysicsPublisher.DemoteToTerrain`, `RemoveLandblock`,
|
|
||||||
replacement commit, and the headless collision-retirement path. No partially
|
|
||||||
observable collision generation is allowed.
|
|
||||||
|
|
||||||
## Production route cutover
|
|
||||||
|
|
||||||
Cut routes only after all prerequisites above are present. The canonical chain
|
|
||||||
for every route is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
wire acceptance
|
|
||||||
-> BeginAcceptedPlacement exact token
|
|
||||||
-> exact DAT/Setup preparation
|
|
||||||
-> Runtime SetPosition canonical commit or deferred residence
|
|
||||||
-> immutable host projection receipt
|
|
||||||
-> exact host acknowledgement
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1. Initial login and CreateObject
|
|
||||||
|
|
||||||
Current duplicate authority:
|
|
||||||
|
|
||||||
- `DatLiveEntityProjectionMaterializer.MaterializeProjection` immediately
|
|
||||||
positions/rebuckets the world entity.
|
|
||||||
- `PlayerModeController.BuildControllerAndCamera` builds its own body and runs
|
|
||||||
`Resolve`/`ResolvePlacement`.
|
|
||||||
- Headless performs its own initial resolve/placement/body construction.
|
|
||||||
|
|
||||||
Required order:
|
|
||||||
|
|
||||||
1. register identity cellless;
|
|
||||||
2. begin initial/remote-create placement before hydration;
|
|
||||||
3. load exact Setup mover;
|
|
||||||
4. prepare the atomic Runtime controller/body relationship;
|
|
||||||
5. submit canonical SetPosition;
|
|
||||||
6. publish presentation only from `Place`;
|
|
||||||
7. acknowledge, then enable player mode/simulation.
|
|
||||||
|
|
||||||
Tests: outdoor/indoor login, unavailable destination then exact-generation
|
|
||||||
wake, malformed or delayed Setup, one body identity, one enter-world clock
|
|
||||||
reset, no early visible entity, graphical/headless identical snapshots.
|
|
||||||
|
|
||||||
### 2. Local ForcePosition
|
|
||||||
|
|
||||||
Delete the placement authority in `LocalForcePositionTransaction` and the
|
|
||||||
direct `BlipPosition`/pre-commit acknowledgement in
|
|
||||||
`LiveEntityNetworkUpdateController.OnPosition`.
|
|
||||||
|
|
||||||
Required order: accept timestamp and preserve heading; begin
|
|
||||||
`LocalAuthoritative`; canonical SetPosition; host `Place` acknowledgement;
|
|
||||||
then send the outbound Position acknowledgement. A missing destination cell
|
|
||||||
must not acknowledge ACE early.
|
|
||||||
|
|
||||||
Tests: same/cross cell, preserved heading/velocity, missing-cell wake,
|
|
||||||
reentrant newer Position, stale host ack, exactly one outbound ack.
|
|
||||||
|
|
||||||
### 3. Portal transit and materialization
|
|
||||||
|
|
||||||
Remove placement authority from `LocalPlayerTeleportPlacement.Place` and its
|
|
||||||
direct resolve/controller/world-entity/rebucket/spatial mutations.
|
|
||||||
|
|
||||||
Bind a Runtime portal-placement authority to the active
|
|
||||||
`RuntimeWorldTransitState` reveal generation, teleport sequence, exact
|
|
||||||
destination cell, and placement token. Readiness permits submission only.
|
|
||||||
Materialization and simulation release happen only after canonical commit,
|
|
||||||
host projection, and exact acknowledgement. Cancellation/replacement produces
|
|
||||||
`Discard`; a stale generation/sequence/cell/token can never reveal.
|
|
||||||
|
|
||||||
Tests: `/ls`, spell recall, ordinary portal, same-location revisit, missing
|
|
||||||
destination, cancelled/replaced reveal, host throw/retry, no early world reveal
|
|
||||||
or LoginComplete.
|
|
||||||
|
|
||||||
### 4. Remote CreateObject and Position
|
|
||||||
|
|
||||||
Delete `RemoteTeleportController`, `RemoteTeleportPlacement`, their pending
|
|
||||||
dictionary/rollback/lost-cell ownership, and pre-placement
|
|
||||||
`WorldEntity.SetPosition`/rebucket calls.
|
|
||||||
|
|
||||||
Preserve retail `MoveOrTeleport` classification:
|
|
||||||
|
|
||||||
- fresh Teleport timestamp or cellless body: teleport hook then SetPosition;
|
|
||||||
- ordinary nearby grounded update: interpolation remains;
|
|
||||||
- distant update: stop interpolation then SetPosition.
|
|
||||||
|
|
||||||
Accept the timestamp, begin the exact token before hydration/body/App changes,
|
|
||||||
unparent first, run the retail teleport hook when required, submit the exact
|
|
||||||
mover, project after Runtime commit, then re-arm constraints.
|
|
||||||
|
|
||||||
Tests: visible/hidden/parented CreateObject, first Position, Teleport timestamp,
|
|
||||||
near interpolation, >96 m far placement, unloaded indoor destination, racing
|
|
||||||
velocity, delete/GUID reuse during host callback.
|
|
||||||
|
|
||||||
### 5. Projectile authoritative create/corrections
|
|
||||||
|
|
||||||
Remove authoritative placement from App `ProjectileController` and the direct
|
|
||||||
SnapToCell/cell/shadow commit in `RuntimeProjectilePhysicsUpdater`.
|
|
||||||
|
|
||||||
Use `ProjectileAuthoritative` with the same Runtime body and exact projectile
|
|
||||||
Setup sphere for initial create and authoritative corrections. Preserve
|
|
||||||
prediction/component/effect identity. Do not route ordinary per-quantum
|
|
||||||
projectile integration through SetPosition.
|
|
||||||
|
|
||||||
Tests: arrow, bolt, spell projectile, mid-flight correction, unloaded cell,
|
|
||||||
landblock crossing, delete during ack, no duplicate body/projectile/effect.
|
|
||||||
|
|
||||||
### 6. Drops and unparent-to-world
|
|
||||||
|
|
||||||
`InventoryWorldDropProjectionController.TryRecoverUnknownPosition` may create
|
|
||||||
the logical object, but it must enter the same canonical create-placement
|
|
||||||
transaction. Do not expose a stale source position or replay create-time
|
|
||||||
effects.
|
|
||||||
|
|
||||||
Tests: whole item, split stack, new GUID, second drop position, attached child
|
|
||||||
becoming a world root, unavailable destination, newer Position while waiting.
|
|
||||||
|
|
||||||
### 7. Pickup, Parent, and Delete
|
|
||||||
|
|
||||||
Runtime hooks already exist in `RuntimeEntityObjectLifetime`, but the route
|
|
||||||
cutover must ensure pickup/parent/delete cancel the exact active
|
|
||||||
placement/lost-cell family first and publish `Discard`/`Withdraw` before the
|
|
||||||
later entity/inventory delta.
|
|
||||||
|
|
||||||
Tests: pickup during preparation/deferred residence, parent during pending
|
|
||||||
withdrawal, delete during host callback, GUID reuse, reset/disposal ownership
|
|
||||||
convergence.
|
|
||||||
|
|
||||||
### 8. Headless parity
|
|
||||||
|
|
||||||
Delete the independent resolve/placement/direct SetPosition and Blip logic in
|
|
||||||
`HeadlessSessionWorldProjection`. Headless must prepare/commit/ack through the
|
|
||||||
same Runtime operations as graphical presentation. Portal completion also
|
|
||||||
waits for the exact placement acknowledgement.
|
|
||||||
|
|
||||||
Tests: byte-identical login, ForcePosition, portal, missing-cell wake,
|
|
||||||
reconnect, and teardown snapshots.
|
|
||||||
|
|
||||||
## AP-22 — retail-authored collision shapes
|
|
||||||
|
|
||||||
After AP-1/AD-1 production cutover is stable:
|
|
||||||
|
|
||||||
- Make `ShadowShapeBuilder` the single Core authority for prepared Setup
|
|
||||||
primitives.
|
|
||||||
- Preserve authored cylinder order.
|
|
||||||
- If no cylinders exist, preserve authored spheres as spheres.
|
|
||||||
- Mixed data uses retail cylinder-first precedence.
|
|
||||||
- A truly shapeless Setup emits no world shadow.
|
|
||||||
- Remove `Setup.Radius/Height` collision synthesis, `Radius * 2` height guesses,
|
|
||||||
and sphere-to-cylinder coercion.
|
|
||||||
- Cut graphical static, headless static, and live-entity publication over
|
|
||||||
together.
|
|
||||||
- Do not alter transition dummy spheres, sticky/range radius, or projectile
|
|
||||||
mover shapes.
|
|
||||||
|
|
||||||
Automated gates: raw/prepared parity, cylinder order, sphere-only, mixed,
|
|
||||||
shapeless, scale, graphical/headless equality, representative installed DATs,
|
|
||||||
and dropped/portal/sign/door behavior.
|
|
||||||
|
|
||||||
## AD-10 — canonical remote slope projection
|
|
||||||
|
|
||||||
After AP-22:
|
|
||||||
|
|
||||||
- Prove remote movement uses the full `ResolveWithTransition` sweep.
|
|
||||||
- Remove terrain-normal preprojection from `RemoteMotionCombiner`.
|
|
||||||
- Remove Runtime terrain-normal sampling calls and delete the sampler if no
|
|
||||||
longer used.
|
|
||||||
- Let `CTransition::adjust_offset` project against the retained actual contact
|
|
||||||
plane.
|
|
||||||
- Preserve interpolation queues, correction replacement, Hidden behavior,
|
|
||||||
network cadence, and graphical/headless parity.
|
|
||||||
|
|
||||||
Tests must deliberately make terrain normals disagree with BSP/prop contact
|
|
||||||
normals, then cover uphill/downhill motion, seams, stairs, jumping, landing,
|
|
||||||
queue-empty/head-reached boundaries, and two-client observation.
|
|
||||||
|
|
||||||
## Closeout gates
|
|
||||||
|
|
||||||
Do not mark the campaign complete from narrow tests alone.
|
|
||||||
|
|
||||||
Automated:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet build AcDream.slnx -c Release
|
|
||||||
dotnet test AcDream.slnx -c Release --no-build --nologo
|
|
||||||
```
|
|
||||||
|
|
||||||
Also run every focused fixture named in the campaign plan: #273 tight gap,
|
|
||||||
#271 stair side, #269 slope, #265 landing, #185 stairs, #137 sliding normal,
|
|
||||||
#116 head collision, roof/cellar wedge, missing-cell, generation replacement,
|
|
||||||
GUID reuse, graphical/headless parity, and allocation/quiescence gates.
|
|
||||||
|
|
||||||
Connected/visual:
|
|
||||||
|
|
||||||
- login and portal arrival at outdoor, indoor, dungeon, stair-lip, and world
|
|
||||||
edge locations;
|
|
||||||
- repeated `/ls`, spell recall, ordinary portals, same-location revisit, and
|
|
||||||
reconnect;
|
|
||||||
- no early world reveal, outdoor demotion, floor snap, terrain-Z lift, or void;
|
|
||||||
- tight gaps, stairs, steep roofs, ledges, doors, crowds, shallow water, and
|
|
||||||
landblock seams;
|
|
||||||
- dropped objects, portals, signs, doors, and shapeless decorations;
|
|
||||||
- two-client uphill/downhill movement and sloped props;
|
|
||||||
- headless/graphical trace equality and graceful zero-residue teardown.
|
|
||||||
|
|
||||||
Only then retire AP-1, AD-1, AP-22, and AD-10, update the architecture,
|
|
||||||
divergence register, campaign/roadmap/milestones, research notes, durable
|
|
||||||
memory, `CLAUDE.md`, and `AGENTS.md`, and record final rollback SHAs.
|
|
||||||
|
|
||||||
## Review procedure for every remaining behavior commit
|
|
||||||
|
|
||||||
1. Implement one bisectable mechanism and run focused tests.
|
|
||||||
2. Run a retail-conformance reviewer against named retail symbols/addresses.
|
|
||||||
3. Run an architecture/adversarial reviewer against reentrancy, stale
|
|
||||||
sequences, malformed data, GUID reuse, streaming replacement, host failure,
|
|
||||||
reset, and disposal.
|
|
||||||
4. Fix every confirmed finding at its root cause.
|
|
||||||
5. Re-run the same reviewers until clean.
|
|
||||||
6. Run Release build plus the complete Release test suite.
|
|
||||||
7. Update the divergence/docs in the same behavior commit.
|
|
||||||
8. Stage exact paths only and commit.
|
|
||||||
|
|
||||||
## Rollback points
|
|
||||||
|
|
||||||
Newest first:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git revert 270f5154 # dormant public placement receipt channel
|
|
||||||
git revert 4c02ac42 # Runtime SetPosition/lost-cell residence owner
|
|
||||||
git revert e84a388e # pure Core retail SetPosition transaction
|
|
||||||
```
|
|
||||||
|
|
||||||
Earlier campaign commits are individually bisectable and listed in the
|
|
||||||
completed sections above. Revert only the responsible mechanism; do not
|
|
||||||
restore the rejected snapshot lease or revive legacy compensation elsewhere.
|
|
||||||
|
|
||||||
## First action for the next agent
|
|
||||||
|
|
||||||
1. Read this file completely.
|
|
||||||
2. Read `docs/research/2026-07-31-canonical-set-position.md` and the AP-1/AD-1
|
|
||||||
rows in `docs/architecture/retail-divergence-register.md`.
|
|
||||||
3. Confirm `HEAD` contains `270f5154` in the exact worktree above.
|
|
||||||
4. Confirm only `AGENTS.md` has a real unrelated unstaged diff.
|
|
||||||
5. Implement prerequisite A (real Runtime collision-report ownership) as its
|
|
||||||
own reviewed commit.
|
|
||||||
6. Then design prerequisites B/C together so exact mover preparation and the
|
|
||||||
atomic controller/body transaction cannot create another partial ownership
|
|
||||||
state.
|
|
||||||
|
|
@ -1,245 +0,0 @@
|
||||||
# Runtime SetPosition collision-report ownership handoff - 2026-07-31
|
|
||||||
|
|
||||||
## Purpose and exact stopping point
|
|
||||||
|
|
||||||
This handoff records placement Slice 4B2 checkpoint 2: the isolated Runtime
|
|
||||||
owner for retail SetPosition collision tracking and report-result semantics.
|
|
||||||
The checkpoint intentionally stops before authored mover preparation, shared
|
|
||||||
local-controller body publication, graphical/headless placement projection,
|
|
||||||
collision-prefix quiescence, or any production SetPosition route cutover.
|
|
||||||
|
|
||||||
Production behavior is therefore unchanged by this checkpoint. The new owner
|
|
||||||
is populated only by the dormant `RuntimeSetPositionState` and focused tests.
|
|
||||||
AP-1 and AD-1 remain narrowed/open; AP-22 and AD-10 remain open.
|
|
||||||
|
|
||||||
## Exact workspace
|
|
||||||
|
|
||||||
- Worktree: `C:\Users\erikn\.codex\worktrees\af5e\acdream`
|
|
||||||
- Branch: `codex/port-claude-agents`
|
|
||||||
- Starting checkpoint: `ec627c13`
|
|
||||||
(`docs(physics): hand off remaining divergence campaign`)
|
|
||||||
- This handoff belongs to the same behavior commit as the implementation.
|
|
||||||
- No upstream is configured for this worktree branch.
|
|
||||||
- Remotes:
|
|
||||||
- `origin`: `https://git.snakedesert.se/erik/acdream.git`
|
|
||||||
- `github`: `git@github.com:eriknihlen/acdream.git`
|
|
||||||
|
|
||||||
Continue in this worktree unless the user explicitly requests otherwise.
|
|
||||||
`AGENTS.md` has an unrelated pre-existing content diff and must not be staged,
|
|
||||||
restored, or rewritten as part of this checkpoint. Several other paths report
|
|
||||||
line-ending/stat noise without a content diff; stage only the exact paths
|
|
||||||
listed in the final commit.
|
|
||||||
|
|
||||||
## Retail oracle
|
|
||||||
|
|
||||||
The complete readable oracle is
|
|
||||||
[`2026-07-31-runtime-set-position-collision-reporting.md`](2026-07-31-runtime-set-position-collision-reporting.md).
|
|
||||||
The named-retail anchors are:
|
|
||||||
|
|
||||||
- `CPhysicsObj::report_object_collision_end` `0x00510A90`
|
|
||||||
- `CPhysicsObj::report_environment_collision` `0x00512FC0`
|
|
||||||
- `CPhysicsObj::report_object_collision` `0x00513060`
|
|
||||||
- `CPhysicsObj::track_object_collision` `0x00513F10`
|
|
||||||
- `CPhysicsObj::report_collision_start` `0x00513FD0`
|
|
||||||
- `CPhysicsObj::report_collision_end` `0x00514620`
|
|
||||||
- `CPhysicsObj::handle_all_collisions` `0x00514780`
|
|
||||||
- successful `CPhysicsObj::SetPositionInternal(CTransition const*)`
|
|
||||||
`0x00515330`
|
|
||||||
- `CPhysicsObj::leave_world` `0x005155A0`
|
|
||||||
- placement failure in `CPhysicsObj::SetPositionInternal` `0x00515BD0`
|
|
||||||
|
|
||||||
The source is `docs/research/named-retail/acclient_2013_pseudo_c.txt`; the
|
|
||||||
struct authority is `docs/research/named-retail/acclient.h`.
|
|
||||||
|
|
||||||
## What this checkpoint implements
|
|
||||||
|
|
||||||
`RuntimeCollisionReportingState` is the sole per-session owner of:
|
|
||||||
|
|
||||||
- one environment-collision latch per exact `RuntimeEntityKey`;
|
|
||||||
- one ordered object-contact table per exact owner incarnation;
|
|
||||||
- retained peer server GUID, touch time, and ethereal-at-touch state;
|
|
||||||
- static and `ReportAsEnvironment` routing;
|
|
||||||
- asymmetric `IgnoreCollisions` and reciprocal `ReportCollisions` eligibility;
|
|
||||||
- strict ordinary `age > 1.0` and ethereal `age > 0.0` expiry;
|
|
||||||
- force-end-before-callback mutation for reentrant safety;
|
|
||||||
- missing-peer self-only end reports without resolving a later GUID reuse;
|
|
||||||
- exact `Missile | AlignPath | PathClipped` clearing on the canonical record,
|
|
||||||
borrowed body, retained shadow state, and mutation version;
|
|
||||||
- a monotonic immutable report FIFO with observer-failure isolation;
|
|
||||||
- the retail callback-eligibility boolean used by failed placement to choose
|
|
||||||
`Collided` versus `NoValidPosition`;
|
|
||||||
- terminal ownership diagnostics and deterministic session/disposal cleanup.
|
|
||||||
|
|
||||||
Successful dormant SetPosition commits contact, water/walkable and ground
|
|
||||||
edges first, runs reporting next, applies physical response once, and then
|
|
||||||
refloods the shadow. An intervening Vector or Movement update suppresses only
|
|
||||||
the stale physical response; it does not erase collision tracking or reports.
|
|
||||||
Failed placement always supplies retail's `previousContact = false` and
|
|
||||||
`previousOnWalkable = false`, reports once, applies its one response pass, and
|
|
||||||
maps the report result exactly.
|
|
||||||
|
|
||||||
Hidden, teleport/withdrawal, deletion, session reset, and disposal use distinct
|
|
||||||
lifetime edges. Leaving the world force-ends the departing owner's table but
|
|
||||||
retains its environment latch and incoming peer records. Destruction then
|
|
||||||
forgets only the departing owner state. Other owners retain exact-key contacts
|
|
||||||
until their own expiry/force pass and can emit a missing-target end using the
|
|
||||||
preserved server GUID. Hidden and session-clear paths force-end while the old
|
|
||||||
report flags and bodies are still eligible, before state/reset teardown.
|
|
||||||
|
|
||||||
## Architectural boundaries
|
|
||||||
|
|
||||||
- Runtime owns all canonical collision-report state and report-result logic.
|
|
||||||
- Core exposes only the exact successful SetPosition ordering seam and the
|
|
||||||
retained-shadow collision identity required by Runtime.
|
|
||||||
- App and Headless gain no report table, queue, heuristic, or production
|
|
||||||
placement consumer.
|
|
||||||
- Reports are presentation-free and keyed by exact Runtime identity.
|
|
||||||
- Network/update callbacks may re-enter, but every later mutation revalidates
|
|
||||||
current identity, body, and the relevant authority version.
|
|
||||||
- Physical-response velocity authority is deliberately separate from report
|
|
||||||
authority, matching retail's ordering without overwriting a newer vector.
|
|
||||||
|
|
||||||
## Validation and independent review
|
|
||||||
|
|
||||||
The saved final diff passed:
|
|
||||||
|
|
||||||
- combined focused Runtime collision-report and SetPosition tests: 76/76;
|
|
||||||
- complete Runtime project: 562/562;
|
|
||||||
- graphical/headless Runtime-physics ownership and dormancy guards: 4/4;
|
|
||||||
- focused Core SetPosition/contact/response ordering tests: 29/29;
|
|
||||||
- complete Core project: 4,224 passed / 1 intentional skip;
|
|
||||||
- from-source Release solution rebuild: 0 errors and 21 pre-existing test-
|
|
||||||
project nullable/analyzer warnings; this checkpoint introduces none;
|
|
||||||
- complete Release solution: 10,309 passed / 4 intentional skips;
|
|
||||||
- warmed steady-contact refresh: 0 managed bytes across 10,000 calls;
|
|
||||||
- warmed immediate dormant SetPosition commit: still below the existing
|
|
||||||
2,048-byte-per-operation ceiling, with no new captured-delegate cost;
|
|
||||||
- architecture/adversarial re-review: clean after fixing Hidden/session/delete
|
|
||||||
reentrancy, stale shadow-state authority, allocation churn, and batch cost;
|
|
||||||
- retail-conformance re-review: clean against every named address above.
|
|
||||||
|
|
||||||
The final retail re-review found and closed two last ordering defects before
|
|
||||||
sign-off: object collision now snapshots the mover's Missile bit before the
|
|
||||||
source callback and, when that snapshot was set, unconditionally masks the
|
|
||||||
current `Missile | AlignPath | PathClipped` bits afterward. Thus an ordinary
|
|
||||||
callback-added Missile is retained when the mover was not previously a missile,
|
|
||||||
but a callback which clears Missile and re-adds path bits cannot evade the
|
|
||||||
pre-gated retail mask. Environment collision retains retail's post-callback
|
|
||||||
current-Missile test. Successful SetPosition now
|
|
||||||
publishes reports before installing the new stationary-fall counter, applies
|
|
||||||
the physical response next, installs StationaryFall/Stop/Stuck transient bits
|
|
||||||
after response, and only then refloods the shadow.
|
|
||||||
|
|
||||||
The host guard reads both production source trees. It proves App and Headless
|
|
||||||
borrow `GameRuntime.EntityObjects.Physics`, declare no second collision table
|
|
||||||
or return heuristic, and still contain no placement-channel consumer. No
|
|
||||||
connected/live gate is required for this dormant checkpoint because no
|
|
||||||
production route can populate or publish the new report owner.
|
|
||||||
|
|
||||||
## Exact implementation and test paths
|
|
||||||
|
|
||||||
The behavior commit containing this handoff changes exactly these ten code and
|
|
||||||
test paths:
|
|
||||||
|
|
||||||
- `src/AcDream.Core/Physics/PhysicsObjUpdate.cs`
|
|
||||||
- `src/AcDream.Core/Physics/ShadowObjectRegistry.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimePhysicsState.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs`
|
|
||||||
- `tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionReportingStateTests.cs`
|
|
||||||
- `tests/AcDream.App.Tests/Runtime/RuntimePhysicsOwnershipTests.cs`
|
|
||||||
|
|
||||||
The same commit synchronizes the architecture, divergence register, canonical
|
|
||||||
SetPosition research, roadmap, milestones, project memory, prior campaign
|
|
||||||
handoff pointer, retail-oracle note, and this detailed handoff. `AGENTS.md` and
|
|
||||||
the pre-existing line-ending/stat-noise paths are deliberately excluded.
|
|
||||||
|
|
||||||
## Remaining work - required order
|
|
||||||
|
|
||||||
### 1. Exact authored mover preparation - complete 2026-08-01
|
|
||||||
|
|
||||||
The dormant preparation contract is implemented and independently reviewed.
|
|
||||||
It binds the Runtime-owned accepted frame and exact Setup DID, preserves the
|
|
||||||
ordered authored spheres and scale/step semantics, seals the returned command,
|
|
||||||
and forces stale deferred residents through exact re-preparation without
|
|
||||||
pre-mutating canonical state. See
|
|
||||||
[`2026-08-01-runtime-set-position-authored-mover-preparation.md`](2026-08-01-runtime-set-position-authored-mover-preparation.md).
|
|
||||||
|
|
||||||
### 2. Atomic local-controller/body publication - next
|
|
||||||
|
|
||||||
Prepare off-canonical, then perform one Runtime-validated atomic transaction
|
|
||||||
which publishes the exact same body to graphical and no-window controllers.
|
|
||||||
Every body writer, remote/projectile binding, SetPosition operation, clock
|
|
||||||
epoch, deletion, reset and disposal path must participate. Do not resurrect
|
|
||||||
the rejected snapshot/rollback lease documented in the prior handoff.
|
|
||||||
|
|
||||||
### 3. Presentation-only host projection
|
|
||||||
|
|
||||||
Implement graphical and headless observers over the existing dormant placement
|
|
||||||
receipt channel. Withdraw removes presentation/spatial consumers while
|
|
||||||
retaining Runtime identity; Place projects only the immutable committed frame;
|
|
||||||
Discard retires the older revision. Host failure retries the exact FIFO head
|
|
||||||
and never rolls Runtime back.
|
|
||||||
|
|
||||||
### 4. Collision-prefix quiescence and atomic route activation
|
|
||||||
|
|
||||||
Park SetPosition residents before retiring their collision prefix, publish the
|
|
||||||
complete replacement generation, wake exact matching residents, and cut every
|
|
||||||
spawn/Position/portal/projectile/drop/pickup/parent/delete route over together.
|
|
||||||
Only then may AP-1 and AD-1 retire.
|
|
||||||
|
|
||||||
### 5. Remaining campaign slices
|
|
||||||
|
|
||||||
- Port retail-authored object collision shape precedence and retire AP-22.
|
|
||||||
- Remove remote terrain-normal preprojection and let the transition resolver
|
|
||||||
use the retained contact plane, retiring AD-10.
|
|
||||||
- Run the full automated and connected matrix, update all ledgers, and close
|
|
||||||
the remaining physics campaign only with direct evidence.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
This checkpoint is one bisectable commit. Revert the commit containing this
|
|
||||||
file to remove collision-report ownership without disturbing the earlier
|
|
||||||
SetPosition residence and receipt-channel checkpoints. Do not revive the old
|
|
||||||
collision-presence guess or the rejected body snapshot lease.
|
|
||||||
|
|
||||||
Because a Git commit cannot embed its own final hash, resolve the exact
|
|
||||||
checkpoint and revert command without ambiguity using:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$checkpoint = git log -1 --format=%H -- `
|
|
||||||
docs/research/2026-07-31-runtime-set-position-collision-reporting-handoff.md
|
|
||||||
git show --stat $checkpoint
|
|
||||||
git revert $checkpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
Earlier rollback points remain:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git revert 270f5154 # dormant public placement receipt channel
|
|
||||||
git revert 4c02ac42 # Runtime SetPosition/lost-cell residence owner
|
|
||||||
git revert e84a388e # pure Core retail SetPosition transaction
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resume procedure
|
|
||||||
|
|
||||||
1. Continue in
|
|
||||||
`C:\Users\erikn\.codex\worktrees\af5e\acdream` and verify
|
|
||||||
`git branch --show-current` reports `codex/port-claude-agents`.
|
|
||||||
2. Resolve the exact checkpoint with the `git log` command above and confirm
|
|
||||||
it is the current `HEAD` before starting the next behavior slice.
|
|
||||||
3. Read `AGENTS.md`, `docs/architecture/acdream-architecture.md`, this file,
|
|
||||||
the collision-report oracle, the canonical SetPosition research, and the
|
|
||||||
prior remaining-campaign handoff completely.
|
|
||||||
4. Run `git status --short`. Preserve the unrelated `AGENTS.md` content diff
|
|
||||||
and every documented line-ending/stat-noise path. Never stage by blanket.
|
|
||||||
5. Begin only **Exact authored mover preparation**, the first remaining item
|
|
||||||
above. Do not activate production routes, retire AP-1/AD-1, begin AP-22 or
|
|
||||||
AD-10, or resurrect the rejected body snapshot/rollback lease.
|
|
||||||
6. Use exact-path staging and rerun the matching focused projects,
|
|
||||||
`dotnet build AcDream.slnx -c Release --nologo`, and
|
|
||||||
`dotnet test AcDream.slnx -c Release --no-build --nologo` before the next
|
|
||||||
reviewed checkpoint.
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
# Runtime SetPosition collision-report ownership
|
|
||||||
|
|
||||||
**Scope:** placement/streaming Slice 4B2 prerequisite A only. This closes the
|
|
||||||
missing Runtime owner for retail collision tracking and the boolean returned by
|
|
||||||
`CPhysicsObj::handle_all_collisions`. It does **not** activate any graphical or
|
|
||||||
headless production SetPosition route.
|
|
||||||
|
|
||||||
## Named-retail oracle
|
|
||||||
|
|
||||||
Primary sources:
|
|
||||||
|
|
||||||
- `CPhysicsObj::report_object_collision_end` `0x00510A90`
|
|
||||||
- `CPhysicsObj::report_environment_collision` `0x00512FC0`
|
|
||||||
- `CPhysicsObj::report_object_collision` `0x00513060`
|
|
||||||
- `CPhysicsObj::track_object_collision` `0x00513F10`
|
|
||||||
- `CPhysicsObj::report_collision_start` `0x00513FD0`
|
|
||||||
- `CPhysicsObj::report_collision_end` `0x00514620`
|
|
||||||
- `CPhysicsObj::handle_all_collisions` `0x00514780`
|
|
||||||
- `CPhysicsObj::SetPositionInternal(CTransition const*)` `0x00515330`
|
|
||||||
- `CPhysicsObj::leave_world` `0x005155A0`
|
|
||||||
- placement failure path in `CPhysicsObj::SetPositionInternal` `0x00515BD0`
|
|
||||||
- `CPhysicsObj::CollisionRecord`, `EnvCollisionProfile`,
|
|
||||||
`ObjCollisionProfile`, and `AtkCollisionProfile` in
|
|
||||||
`docs/research/named-retail/acclient.h`
|
|
||||||
|
|
||||||
The source text is
|
|
||||||
`docs/research/named-retail/acclient_2013_pseudo_c.txt`. The addresses above
|
|
||||||
are the behavioral authority; the older unnamed chunks remain fallback only.
|
|
||||||
|
|
||||||
### Environment reporting
|
|
||||||
|
|
||||||
```text
|
|
||||||
report_environment_collision(meInContact):
|
|
||||||
reported = false
|
|
||||||
if !colliding_with_environment:
|
|
||||||
if self.ReportCollisions && self.weenie != null:
|
|
||||||
DoCollision(EnvCollisionProfile(self.velocity, meInContact))
|
|
||||||
reported = true
|
|
||||||
colliding_with_environment = true
|
|
||||||
if self.Missile:
|
|
||||||
self.state &= ~(Missile | AlignPath | PathClipped)
|
|
||||||
return reported
|
|
||||||
```
|
|
||||||
|
|
||||||
The latch is independent of callback eligibility. An object with no collision
|
|
||||||
callback still latches its environment contact, and a repeated environment hit
|
|
||||||
returns false. Retail has no environment-end callback. `leave_world` does not
|
|
||||||
clear this latch; the next `handle_all_collisions` call re-arms it only after a
|
|
||||||
non-environment frame.
|
|
||||||
|
|
||||||
### Object reporting and tracking
|
|
||||||
|
|
||||||
```text
|
|
||||||
track_object_collision(other, meInContact):
|
|
||||||
if other.Static:
|
|
||||||
return report_environment_collision(meInContact)
|
|
||||||
|
|
||||||
record = { touched_time = PhysicsTimer.curr_time,
|
|
||||||
ethereal = other.Ethereal }
|
|
||||||
existed = collision_table.clobber(other.id, record)
|
|
||||||
if existed:
|
|
||||||
return false
|
|
||||||
return report_object_collision(other, meInContact)
|
|
||||||
```
|
|
||||||
|
|
||||||
The table insert/refresh precedes callbacks. Duplicate contacts refresh their
|
|
||||||
time but never replay a start callback. DAT/static classification and physics
|
|
||||||
state come from the exact shadow object which produced the collision; object-ID
|
|
||||||
presence is not a valid substitute.
|
|
||||||
|
|
||||||
`report_object_collision` first maps `ReportAsEnvironment` to the environment
|
|
||||||
path. Otherwise:
|
|
||||||
|
|
||||||
- the mover reports only when the other object is not `IgnoreCollisions` and
|
|
||||||
the mover has `ReportCollisions` plus a weenie;
|
|
||||||
- a mover which had Missile set before the source callback unconditionally
|
|
||||||
masks its current `Missile | AlignPath | PathClipped` bits after striking a
|
|
||||||
non-ignored object, even when the callback cleared Missile but re-added path
|
|
||||||
bits; when pre-callback Missile was clear, callback-added Missile is retained;
|
|
||||||
- the reciprocal report occurs only when the other has `ReportCollisions`, the
|
|
||||||
mover is not `IgnoreCollisions`, and the other has a weenie;
|
|
||||||
- the return is true when at least one callback is attempted. It is never a
|
|
||||||
collision-presence boolean.
|
|
||||||
|
|
||||||
### Expiry and end reporting
|
|
||||||
|
|
||||||
`report_collision_end(force)` removes records before dispatching callbacks.
|
|
||||||
This ordering is required for safe reentrancy.
|
|
||||||
|
|
||||||
```text
|
|
||||||
ordinary record: remove when age > 1.0, or force
|
|
||||||
ethereal record: remove when age > 0.0, or force
|
|
||||||
```
|
|
||||||
|
|
||||||
Equality remains alive. A still-resolvable non-`ReportAsEnvironment` peer may
|
|
||||||
receive reciprocal collision-end callbacks. When the peer no longer resolves,
|
|
||||||
the owner can still receive its self-only end using the stored retail object
|
|
||||||
ID. A later incarnation must never satisfy the old contact record.
|
|
||||||
|
|
||||||
### `handle_all_collisions` and SetPosition ordering
|
|
||||||
|
|
||||||
```text
|
|
||||||
handle_all_collisions(info, previousContact, previousOnWalkable):
|
|
||||||
reported = false
|
|
||||||
for other in info.collidedObjects, in encounter order:
|
|
||||||
reported |= track_object_collision(other, previousContact)
|
|
||||||
report_collision_end(force = false)
|
|
||||||
|
|
||||||
if environment latch is already set:
|
|
||||||
latch = info.collided_with_environment
|
|
||||||
else if info.collided_with_environment
|
|
||||||
|| (!previousOnWalkable && self.OnWalkable):
|
|
||||||
reported |= report_environment_collision(previousContact)
|
|
||||||
|
|
||||||
apply retail collision velocity/stationary response
|
|
||||||
return reported
|
|
||||||
```
|
|
||||||
|
|
||||||
Successful `SetPositionInternal(CTransition const*)` commits the resolved
|
|
||||||
cell/frame, Contact/WaterContact/OnWalkable state, and HitGround/LeaveGround
|
|
||||||
edge before `handle_all_collisions`; it ignores the returned boolean and only
|
|
||||||
then replaces/refloods shadows. Collision reports observe the old stationary-
|
|
||||||
fall state; the new counter is installed before physical response, while the
|
|
||||||
StationaryFall/Stop/Stuck transient bits are replaced after response and before
|
|
||||||
shadow reflood. The placement failure path calls
|
|
||||||
`handle_all_collisions(info, false, false)` and maps true to
|
|
||||||
`SetPositionError::Collided` (`4`) and false to `NoValidPosition` (`2`).
|
|
||||||
|
|
||||||
Consequently acdream must keep report/tracking separate from the physical
|
|
||||||
response: failed placement runs both once, while successful Runtime commit
|
|
||||||
runs reporting between the contact/ground commit and shadow reflood without
|
|
||||||
double-applying velocity response.
|
|
||||||
|
|
||||||
## Runtime ownership contract
|
|
||||||
|
|
||||||
The implementation is presentation-free and belongs to the per-session
|
|
||||||
`RuntimePhysicsState` graph. Its invariants are:
|
|
||||||
|
|
||||||
- owner and peer identities are exact `RuntimeEntityKey` values, not server
|
|
||||||
GUID or local ID alone;
|
|
||||||
- each tracked record retains the peer server GUID, touch time in the Runtime
|
|
||||||
simulation-clock domain, and ethereal-at-touch bit;
|
|
||||||
- collided IDs and authored/static ownership are admitted through the exact
|
|
||||||
retained `ShadowObjectRegistry` registration which produced the collision;
|
|
||||||
every dynamic Static/Ethereal/Ignore/ReportAsEnvironment decision then reads
|
|
||||||
the current canonical `PhysicsBody.State`, never a stale shadow snapshot;
|
|
||||||
- immutable reports preserve encounter order and dispatch through a retained,
|
|
||||||
reentrancy-safe FIFO;
|
|
||||||
- callback exceptions are isolated, while the retail report-result boolean is
|
|
||||||
determined by callback eligibility and does not depend on subscribers;
|
|
||||||
- every callback boundary revalidates the exact record/body/authority before
|
|
||||||
any later canonical mutation;
|
|
||||||
- force-end mutates the complete expired set before publishing ends; exact-key
|
|
||||||
admission guards prevent callback reentry from recreating a leaving owner,
|
|
||||||
and session teardown blocks the whole owner batch before its first callback;
|
|
||||||
- one source lifetime token covers a complete precollected end batch, so a
|
|
||||||
callback-accepted delete stops every later peer report even while teardown
|
|
||||||
sidecars remain resolvable;
|
|
||||||
- lifetime forget, session reset, and disposal cannot donate state to GUID
|
|
||||||
reuse;
|
|
||||||
- terminal ownership diagnostics include contact/report state and converge to
|
|
||||||
zero;
|
|
||||||
- graphical and no-window hosts borrow the same Runtime owner. No host owns a
|
|
||||||
second collision table or report-result heuristic.
|
|
||||||
|
|
||||||
The warmed steady-contact refresh path allocates zero managed bytes. Expired
|
|
||||||
contact storage is allocated lazily only after the first actual expiry, and
|
|
||||||
session-batch teardown is linear in owner count.
|
|
||||||
|
|
||||||
## Deliberately deferred
|
|
||||||
|
|
||||||
The canonical SetPosition owner remains dormant in production. The following
|
|
||||||
belong to later 4B2 commits and are not part of this checkpoint:
|
|
||||||
|
|
||||||
- exact ordered Setup spheres, authored scale and step-height preparation;
|
|
||||||
- the atomic shared local-controller body transaction;
|
|
||||||
- presentation-only rebucketing and placement-prefix quiescence;
|
|
||||||
- graphical/headless spawn, Position, portal, projectile, drop, pickup,
|
|
||||||
parent, and delete route cutover.
|
|
||||||
|
|
||||||
AP-1 and AD-1 therefore remain open, narrowed only by removal of the
|
|
||||||
collision-report prerequisite.
|
|
||||||
|
|
@ -1,302 +0,0 @@
|
||||||
# Runtime initial Create residence handoff - 2026-08-01
|
|
||||||
|
|
||||||
> **Status:** this remains the `38fd4b8d` residence-foundation history. The
|
|
||||||
> completed inbound-admission checkpoint and current continuation boundary are
|
|
||||||
> recorded in
|
|
||||||
> [`2026-08-01-runtime-initial-placement-admission-handoff.md`](2026-08-01-runtime-initial-placement-admission-handoff.md).
|
|
||||||
|
|
||||||
## Purpose and exact stopping point
|
|
||||||
|
|
||||||
Commit `38fd4b8dc952236d4b98518c67335026c7815656` adds the dormant Runtime
|
|
||||||
transaction which retains an entity's initial authored CreateObject placement
|
|
||||||
until canonical SetPosition succeeds and the ordered remainder of the Create
|
|
||||||
packet can be adopted. It does not yet cut the production App/Headless Create
|
|
||||||
route over, so AP-1 and AD-1 remain open.
|
|
||||||
|
|
||||||
This is the deliberate clean handoff requested by the user. In plain terms,
|
|
||||||
Runtime now has a tested holding area for a newly created world object while
|
|
||||||
its exact collision placement is being resolved. The object cannot become
|
|
||||||
half-visible, consume later position packets, or be silently replaced during
|
|
||||||
that interval. The next model starts at the executor/cutover boundary; it does
|
|
||||||
not need to repair or redesign this ownership transaction.
|
|
||||||
|
|
||||||
Do not start production cutover from an earlier checkpoint. Do not call this
|
|
||||||
campaign complete: AP-1, AD-1, AP-22, and AD-10 remain open.
|
|
||||||
|
|
||||||
## Exact workspace and Git state
|
|
||||||
|
|
||||||
- Worktree: `C:\Users\erikn\.codex\worktrees\af5e\acdream`
|
|
||||||
- Branch: `codex/port-claude-agents`
|
|
||||||
- Code checkpoint: `38fd4b8dc952236d4b98518c67335026c7815656`
|
|
||||||
- Immediately preceding host-staging checkpoint: `74103f75`
|
|
||||||
- No push or merge is part of this stopping point.
|
|
||||||
|
|
||||||
The worktree intentionally contains unrelated user changes/stat noise. Do not
|
|
||||||
stage, restore, normalize, or rewrite these paths as part of the continuation:
|
|
||||||
|
|
||||||
- `AGENTS.md` (real unrelated content change);
|
|
||||||
- `src/AcDream.App/Input/PlayerModeController.cs`;
|
|
||||||
- `src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs`;
|
|
||||||
- `src/AcDream.App/Rendering/LiveAnimationPresentationContext.cs`;
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs`;
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/CellTransitTests.cs`;
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/Issue133DungeonTeleportPrefixTests.cs`;
|
|
||||||
- `tools/A8CellAudit/A8CellAudit.csproj`.
|
|
||||||
|
|
||||||
The paths after `AGENTS.md` currently have no content diff and are reported
|
|
||||||
because of pre-existing line-ending/stat noise. Always stage exact paths;
|
|
||||||
never use `git add -A`.
|
|
||||||
|
|
||||||
## Placement checkpoint chain
|
|
||||||
|
|
||||||
The current mechanism was built as bisectable commits. The directly relevant
|
|
||||||
chain, oldest first, is:
|
|
||||||
|
|
||||||
- `e84a388e` - pure Core retail SetPosition transaction;
|
|
||||||
- `4c02ac42` - Runtime deferred/lost-cell residence owner;
|
|
||||||
- `270f5154` - dormant placement receipt channel;
|
|
||||||
- `237d1184` - retail SetPosition collision-report owner;
|
|
||||||
- `442cb8f9` - exact authored mover preparation;
|
|
||||||
- `22651c82` - dormant Runtime local physics publication;
|
|
||||||
- `99f867f0` - sealed dormant SetPosition evaluation;
|
|
||||||
- `5785a07b` - dormant SetPosition activation;
|
|
||||||
- `ef436678` - placement acknowledgement ownership;
|
|
||||||
- `74c9b155`, `378ca95a`, `f05ed5c3` - graphical/headless projection seams;
|
|
||||||
- `99bf1751`, `9b0f59bd` - collision-prefix quiescence and atomic replacement;
|
|
||||||
- `0fbc7a1f` - hidden-object SetPosition ownership correction;
|
|
||||||
- `3f800a4a` - authoritative route classification;
|
|
||||||
- `74103f75` - inert App materialization before Runtime placement;
|
|
||||||
- `38fd4b8d` - initial Create residence, continuation FIFO, and adoption.
|
|
||||||
|
|
||||||
## Owned mechanism in `38fd4b8d`
|
|
||||||
|
|
||||||
`RuntimeInitialCreateResidenceState` now owns, per exact entity incarnation:
|
|
||||||
|
|
||||||
- the accepted initial Create frame and exact SetPosition operation;
|
|
||||||
- a cellless logical entity while authored placement is pending;
|
|
||||||
- a monotonic immutable FIFO for fresher Position continuations;
|
|
||||||
- accepted timestamp, position, vector, rotation, placement, and wire payloads;
|
|
||||||
- completion/adoption tokens and a revision which reject stale observers;
|
|
||||||
- exact authority revalidation across generation, identity, Create, position,
|
|
||||||
placement, full-cell, deletion, reset, GUID reuse, and disposal;
|
|
||||||
- reentrant-safe cancellation at the lifetime commit boundary.
|
|
||||||
|
|
||||||
The public legacy registration path is intentionally unchanged. Production
|
|
||||||
behavior remains on the previous route until the continuation executor and
|
|
||||||
all-host cutover land together.
|
|
||||||
|
|
||||||
### Exact behavior now protected
|
|
||||||
|
|
||||||
- Initial/New Create admission is previewed without consuming timestamps;
|
|
||||||
Existing and Stale packets still use the established gates.
|
|
||||||
- No collision generation is guessed. An initial residence can exist only
|
|
||||||
after binding a real, nonzero generation.
|
|
||||||
- Fresh Parent wins over Position, matching the packet's relation priority.
|
|
||||||
- An absent or present-zero position cell remains cellless instead of being
|
|
||||||
fabricated as an outdoor placement.
|
|
||||||
- Later accepted Position packets append to one immutable ordered FIFO. They
|
|
||||||
cannot mutate the original placement operation or bypass it.
|
|
||||||
- A completed but not yet adopted transaction remains exclusive. A later
|
|
||||||
Position revises the retained batch and invalidates the old adoption token;
|
|
||||||
it cannot disappear between completion and acknowledgement.
|
|
||||||
- Placement acknowledgement uses exact identity, operation, generation,
|
|
||||||
position authority, Create integration, full-cell, and placement-commit
|
|
||||||
versions.
|
|
||||||
- Reset first detaches and clears ownership, then publishes cancellation, so a
|
|
||||||
reentrant observer cannot invalidate enumeration or resurrect an owner.
|
|
||||||
- Delete, replacement, pickup, parent, withdrawal, reset, and disposal return
|
|
||||||
cancellation receipts to the caller's safe publication boundary instead of
|
|
||||||
invoking observers before later canonical mutation.
|
|
||||||
- Malformed initial or continuation packets fail before timestamp or canonical
|
|
||||||
state consumption. A corrected packet with the same instance can recover.
|
|
||||||
|
|
||||||
The FIFO stores raw accepted Position facts rather than prematurely choosing
|
|
||||||
a final movement route. That is intentional: contact, animation state, the
|
|
||||||
server-position option, and player distance must be sampled at the same point
|
|
||||||
where retail makes the routing decision.
|
|
||||||
|
|
||||||
## Exact files in `38fd4b8d`
|
|
||||||
|
|
||||||
- `src/AcDream.Core/Physics/PhysicsTimestampGate.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs`
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/MotionSequenceGateTests.cs`
|
|
||||||
- `tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs`
|
|
||||||
- `tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs`
|
|
||||||
|
|
||||||
## Retail order for the next slice
|
|
||||||
|
|
||||||
The next slice must preserve `SmartBox::HandleCreateObject` at `0x00454C80`:
|
|
||||||
|
|
||||||
1. visual description;
|
|
||||||
2. exactly one of Parent, Position, or Pickup relation;
|
|
||||||
3. Movement;
|
|
||||||
4. State;
|
|
||||||
5. Vector;
|
|
||||||
6. Weenie description;
|
|
||||||
7. final resident-cell validity cleanup.
|
|
||||||
|
|
||||||
Position routing must also preserve these named-retail distinctions:
|
|
||||||
|
|
||||||
- a same-incarnation Create position is not equivalent to standalone F748;
|
|
||||||
- ForcePosition performs its own timestamp/parent/placement route;
|
|
||||||
- remote near-contact interpolates, remote far-contact stops interpolation and
|
|
||||||
performs SetPosition, and remote teleport invokes the teleport hook before
|
|
||||||
SetPosition;
|
|
||||||
- local teleport performs SetPosition, then the player-teleported hook, then
|
|
||||||
constrains to the authoritative frame and clears velocity;
|
|
||||||
- local ordinary Position constrains first and interpolates only when the
|
|
||||||
server-position option and contact gate permit it.
|
|
||||||
|
|
||||||
Therefore every retained continuation must include its
|
|
||||||
`RuntimeAcceptedPositionSource`, and executor-time inputs must pin HasAnims,
|
|
||||||
UsePositionFromServer, contact, and distance before mutation. Parent/Pickup and
|
|
||||||
the same-Create Movement -> State -> Vector order must be part of the same
|
|
||||||
synchronous adoption transaction.
|
|
||||||
|
|
||||||
The principal named-retail anchors are:
|
|
||||||
|
|
||||||
- `SmartBox::HandleCreateObject` `0x00454C80`;
|
|
||||||
- `CPhysicsObj::SetPositionInternal` `0x00515330`;
|
|
||||||
- `PhysicsDesc::UnPack` `0x0051DDD0`;
|
|
||||||
- `CPhysicsObj::set_description` `0x00514F40`.
|
|
||||||
|
|
||||||
Use `docs/research/named-retail/acclient_2013_pseudo_c.txt` first and the older
|
|
||||||
Ghidra chunks only as a fallback.
|
|
||||||
|
|
||||||
## Validation and reviews
|
|
||||||
|
|
||||||
The implementation agent and reviewers reported:
|
|
||||||
|
|
||||||
- focused initial-residence and classifier tests: 79/79;
|
|
||||||
- complete Runtime tests: 819/819;
|
|
||||||
- Runtime Release build: zero warnings and errors;
|
|
||||||
- focused Core timestamp tests: 31/31;
|
|
||||||
- retail-conformance review: clean;
|
|
||||||
- architecture/adversarial review: clean;
|
|
||||||
- `git diff --check`: clean.
|
|
||||||
|
|
||||||
Primary-agent final gates after the behavior commit:
|
|
||||||
|
|
||||||
- complete Release solution build: succeeded, 0 errors;
|
|
||||||
- complete Release solution tests: 10,612 passed / 4 intentional skips;
|
|
||||||
- App: 4,027 passed / 3 skips;
|
|
||||||
- Core: 4,242 passed / 1 skip;
|
|
||||||
- Runtime: 819 passed;
|
|
||||||
- Core.Net: 762 passed;
|
|
||||||
- UI abstractions: 543 passed;
|
|
||||||
- Headless: 76 passed;
|
|
||||||
- Content: 124 passed;
|
|
||||||
- Bake: 15 passed;
|
|
||||||
- CLI: 4 passed.
|
|
||||||
|
|
||||||
The build reports 21 pre-existing test-project nullable/analyzer warnings. The
|
|
||||||
checkpoint introduces no build errors or new production warning.
|
|
||||||
|
|
||||||
Both independent reviews initially found real edge cases and the final code
|
|
||||||
includes their root-cause fixes:
|
|
||||||
|
|
||||||
- completed-but-unadopted Position packets could bypass the FIFO;
|
|
||||||
- cancellation callbacks could re-enter before the caller's canonical mutation;
|
|
||||||
- reset could enumerate live dictionaries while a callback mutated them;
|
|
||||||
- adoption did not initially validate every spatial/authority version.
|
|
||||||
|
|
||||||
Final retail-conformance and architecture/adversarial rereviews both passed.
|
|
||||||
No connected visual gate was required because the new API is dormant and no
|
|
||||||
production App or Headless route calls it yet.
|
|
||||||
|
|
||||||
## Production routes intentionally unchanged
|
|
||||||
|
|
||||||
This is the key handoff boundary. At this checkpoint:
|
|
||||||
|
|
||||||
- graphical Create still flows through
|
|
||||||
`LiveEntityHydrationController.OnCreateCore`,
|
|
||||||
`LiveEntityRuntime.RegisterLiveEntity`, and legacy `RegisterEntity`;
|
|
||||||
- graphical materialization still defaults to `LegacyImmediate` rather than
|
|
||||||
the new `AwaitRuntimePlacement` residence;
|
|
||||||
- graphical Position still performs its existing world-position, rebucket,
|
|
||||||
projectile, remote-motion, and shadow work;
|
|
||||||
- headless Create still uses `RuntimeLiveEntitySessionController.OnSpawned`,
|
|
||||||
`HeadlessSessionWorldProjection.ProjectSpawn`, and its independent initial
|
|
||||||
resolve/body construction;
|
|
||||||
- headless Position still uses its existing projection path;
|
|
||||||
- the new Runtime initial-residence API is reached by focused tests only.
|
|
||||||
|
|
||||||
Existing host adapters already observe Runtime placement receipts. Do not add
|
|
||||||
another observer architecture or a second GUID map.
|
|
||||||
|
|
||||||
## Next implementation boundary
|
|
||||||
|
|
||||||
Implement one Runtime continuation executor and exact ordered Create tail,
|
|
||||||
then route graphical and no-window registration through it without a mirror.
|
|
||||||
The executor must be synchronous or retry-idempotent around adoption revision;
|
|
||||||
failure must leave the exact FIFO head retryable. Only after both production
|
|
||||||
hosts and every Create/Position/ForcePosition/parent/pickup route use the same
|
|
||||||
owner may AP-1 and AD-1 retire.
|
|
||||||
|
|
||||||
### Required order for the next model
|
|
||||||
|
|
||||||
1. Add `RuntimeAcceptedPositionSource` to every retained continuation. A
|
|
||||||
same-incarnation Create position and standalone F748 are not interchangeable.
|
|
||||||
2. Implement one Runtime-owned synchronous continuation executor. Capture
|
|
||||||
`UsePositionFromServer`, animation/contact state, and player distance at the
|
|
||||||
retail-equivalent decision point.
|
|
||||||
3. Execute initial placement once, consume its exact host acknowledgement,
|
|
||||||
then drain the continuation FIFO in order with retail's hook ordering.
|
|
||||||
4. Serialize one Create packet as relation
|
|
||||||
(Parent/Position/Pickup), Movement, State, Vector, WeenieDesc, cleanup.
|
|
||||||
5. Keep every side effect exactly-once. If execution can yield, make adoption
|
|
||||||
revision/idempotence explicit so retry cannot replay hooks or position sends.
|
|
||||||
6. Switch graphical and headless registration together to the same Runtime
|
|
||||||
owner. Hosts may project immutable results only; they may not resolve a
|
|
||||||
second placement or create another body.
|
|
||||||
7. Route later Create, Position, ForcePosition, teleport, parent, pickup,
|
|
||||||
withdrawal, delete, remote, projectile, and dropped-item edges through the
|
|
||||||
same owner before deleting legacy paths.
|
|
||||||
8. Run focused tests, full Release build/tests, exact lifecycle/reconnect and
|
|
||||||
nine-stop connected gates, then perform the user visual matrix. Only then
|
|
||||||
retire AP-1 and AD-1.
|
|
||||||
|
|
||||||
Do not begin AP-22 or AD-10 until the production placement cutover is green.
|
|
||||||
|
|
||||||
### Subsequent independent slices
|
|
||||||
|
|
||||||
- **AP-22:** make `ShadowShapeBuilder` the only prepared Setup-shape authority;
|
|
||||||
preserve authored cylinder order, use spheres only when cylinders are absent,
|
|
||||||
allow truly shapeless Setups, and remove radius/height synthesis and sphere-
|
|
||||||
to-cylinder coercion across graphical/headless/live publication.
|
|
||||||
- **AD-10:** remove terrain-normal preprojection from remote motion. Let the
|
|
||||||
canonical transition resolver project against the actual retained contact
|
|
||||||
plane, with tests where terrain and BSP/prop normals deliberately differ.
|
|
||||||
- Run the final connected matrix, synchronize ledgers/docs, and only then close
|
|
||||||
the remaining physics-divergence campaign.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
Revert the behavior checkpoint without disturbing the earlier placement
|
|
||||||
foundation:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git revert 38fd4b8dc952236d4b98518c67335026c7815656
|
|
||||||
```
|
|
||||||
|
|
||||||
The documentation checkpoint containing this file is a separate commit and
|
|
||||||
can be reverted independently if only the handoff text needs correction.
|
|
||||||
|
|
||||||
## Resume checklist
|
|
||||||
|
|
||||||
1. Continue in the exact worktree and branch recorded above.
|
|
||||||
2. Confirm `git rev-parse HEAD` includes both the behavior and documentation
|
|
||||||
checkpoint commits.
|
|
||||||
3. Read this file, `docs/architecture/acdream-architecture.md`,
|
|
||||||
`docs/research/2026-07-31-canonical-set-position.md`, and
|
|
||||||
`docs/research/2026-07-31-runtime-set-position-collision-reporting-handoff.md`.
|
|
||||||
4. Run `git status --short` and preserve every unrelated path listed above.
|
|
||||||
5. Re-run the focused 79-test residence/classifier gate before modifying the
|
|
||||||
transaction.
|
|
||||||
6. Begin only the continuation executor and ordered Create tail. Do not start
|
|
||||||
AP-22/AD-10 or vendor work in the same commit.
|
|
||||||
|
|
@ -1,320 +0,0 @@
|
||||||
# Runtime initial-placement admission handoff - 2026-08-01
|
|
||||||
|
|
||||||
> **Status:** this remains the `30012361` admission-checkpoint history. The
|
|
||||||
> continuation executor this file scoped as "the next implementation
|
|
||||||
> boundary" is complete at `5db3de3c`; the current boundary (production
|
|
||||||
> cutover) is recorded in
|
|
||||||
> [`2026-08-02-runtime-continuation-executor-handoff.md`](2026-08-02-runtime-continuation-executor-handoff.md).
|
|
||||||
|
|
||||||
## Purpose and exact stopping point
|
|
||||||
|
|
||||||
Behavior commit `30012361e12222e8271b1531574257ba910c77cb`
|
|
||||||
completes the bounded Runtime admission checkpoint requested by the user.
|
|
||||||
While an entity's first authored placement is waiting, every later accepted
|
|
||||||
same-incarnation update is preserved in exact arrival order without changing
|
|
||||||
or displaying the entity early.
|
|
||||||
|
|
||||||
In plain terms, Runtime now has a sealed mailbox behind the pending initial
|
|
||||||
placement. Network sequence checks still decide which messages are fresh, but
|
|
||||||
accepted messages wait in that mailbox. The visible/canonical entity remains
|
|
||||||
at its original frozen Create state until a later executor is authorized to
|
|
||||||
apply the mailbox.
|
|
||||||
|
|
||||||
This checkpoint deliberately does **not** implement that executor, switch the
|
|
||||||
graphical or headless production routes, begin AP-22 authored shape work, or
|
|
||||||
begin AD-10 remote slope projection. AP-1 and AD-1 therefore remain open.
|
|
||||||
|
|
||||||
This file supersedes the admission-status portions of
|
|
||||||
`2026-08-01-runtime-initial-create-residence-handoff.md`; that earlier file
|
|
||||||
remains the foundation history for commit `38fd4b8d`.
|
|
||||||
|
|
||||||
## Exact workspace and Git state
|
|
||||||
|
|
||||||
- Worktree: `C:\Users\erikn\.codex\worktrees\af5e\acdream`
|
|
||||||
- Branch: `codex/port-claude-agents`
|
|
||||||
- Behavior checkpoint: `30012361e12222e8271b1531574257ba910c77cb`
|
|
||||||
- Residence foundation: `38fd4b8dc952236d4b98518c67335026c7815656`
|
|
||||||
- Documentation checkpoint: the commit containing this file
|
|
||||||
- No push or merge is part of this checkpoint.
|
|
||||||
|
|
||||||
The worktree intentionally contains unrelated user changes or pre-existing
|
|
||||||
stat/line-ending noise. Do not stage, restore, normalize, or rewrite these
|
|
||||||
paths when continuing:
|
|
||||||
|
|
||||||
- `AGENTS.md` (real unrelated content change);
|
|
||||||
- `src/AcDream.App/Input/PlayerModeController.cs`;
|
|
||||||
- `src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs`;
|
|
||||||
- `src/AcDream.App/Rendering/LiveAnimationPresentationContext.cs`;
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs`;
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/CellTransitTests.cs`;
|
|
||||||
- `tests/AcDream.Core.Tests/Physics/Issue133DungeonTeleportPrefixTests.cs`;
|
|
||||||
- `tools/A8CellAudit/A8CellAudit.csproj`.
|
|
||||||
|
|
||||||
Always stage exact paths. Never use `git add -A` in this worktree.
|
|
||||||
|
|
||||||
## What `30012361` owns
|
|
||||||
|
|
||||||
### One exact pending owner
|
|
||||||
|
|
||||||
`RuntimeInitialCreateResidenceState` owns one transaction per exact
|
|
||||||
`RuntimeEntityKey`, not per server GUID alone. It retains:
|
|
||||||
|
|
||||||
- the deep-frozen initial Create packet and placement operation;
|
|
||||||
- the exact residence token, generation, placement authority, and revision;
|
|
||||||
- a monotonic sequence for accepted continuations;
|
|
||||||
- one immutable, mixed-kind FIFO in original arrival order;
|
|
||||||
- completion/adoption and teardown receipts.
|
|
||||||
|
|
||||||
The accepted continuation kinds are:
|
|
||||||
|
|
||||||
1. same-incarnation Create;
|
|
||||||
2. ObjDesc;
|
|
||||||
3. Parent;
|
|
||||||
4. Pickup;
|
|
||||||
5. Position;
|
|
||||||
6. Movement;
|
|
||||||
7. State;
|
|
||||||
8. Vector.
|
|
||||||
|
|
||||||
There is no coalescing, sorting by message type, or replacement of an earlier
|
|
||||||
accepted FIFO item by a later one.
|
|
||||||
|
|
||||||
### Frozen public state
|
|
||||||
|
|
||||||
While the initial residence is pending:
|
|
||||||
|
|
||||||
- retail timestamp gates advance for accepted updates;
|
|
||||||
- `RuntimeEntityRecord.Snapshot` remains unchanged;
|
|
||||||
- the public accepted-snapshot view remains unchanged;
|
|
||||||
- no entity/object event is published;
|
|
||||||
- no projection acknowledgement callback runs;
|
|
||||||
- parent commitment, world placement, rendering, radar, picking, physics,
|
|
||||||
audio, and other presentation remain unchanged;
|
|
||||||
- every accepted payload is retained as an immutable typed action.
|
|
||||||
|
|
||||||
This is intentional gate-only acceptance. It is not an alternative canonical
|
|
||||||
snapshot and must not grow into one.
|
|
||||||
|
|
||||||
### Immutable payload boundary
|
|
||||||
|
|
||||||
`RuntimeInitialCreateAdmissionFreezer` copies every parser-owned mutable
|
|
||||||
collection that can outlive packet dispatch:
|
|
||||||
|
|
||||||
- EntitySpawn animation-part, texture, and sub-palette arrays;
|
|
||||||
- ObjDesc model arrays;
|
|
||||||
- motion command lists;
|
|
||||||
- Physics Movement raw bytes and motion commands;
|
|
||||||
- Physics child attachments.
|
|
||||||
|
|
||||||
Same-incarnation Create is retained as one atomic envelope. Its actions retain
|
|
||||||
retail's packet-tail order:
|
|
||||||
|
|
||||||
1. AP-119 pre-tail description adaptation;
|
|
||||||
2. ObjDesc;
|
|
||||||
3. exactly one of Parent, Position, or Pickup;
|
|
||||||
4. Movement;
|
|
||||||
5. State;
|
|
||||||
6. Vector;
|
|
||||||
7. Weenie description;
|
|
||||||
8. resident-cell cleanup.
|
|
||||||
|
|
||||||
### Position facts remain raw
|
|
||||||
|
|
||||||
A deferred Position retains the typed packet plus the timestamp disposition
|
|
||||||
and accepted gate facts. It does not prematurely choose interpolation,
|
|
||||||
teleport hooks, or final movement behavior. The new explicit
|
|
||||||
`RuntimePositionConstrainPhase` distinguishes retail's local ordinary
|
|
||||||
constrain-before route from remote/teleport constrain-after routes, but the
|
|
||||||
future executor must still sample the required live inputs at the retail
|
|
||||||
decision point.
|
|
||||||
|
|
||||||
No selected UI target, presentation state, or host-specific route is stored in
|
|
||||||
the admission owner.
|
|
||||||
|
|
||||||
## Missing-parent behavior
|
|
||||||
|
|
||||||
Named retail resolves a nonzero parent before child object lookup and child
|
|
||||||
timestamp admission. Runtime now follows that order:
|
|
||||||
|
|
||||||
- a child Create whose parent is not addressable is stored as the complete raw
|
|
||||||
frozen Create packet;
|
|
||||||
- no child entity record, accepted snapshot, timestamp gate, local ID, event,
|
|
||||||
or residence lease exists yet;
|
|
||||||
- the queue is keyed by parent GUID but each entry also has a monotonic
|
|
||||||
`AdmissionId` which is never reset, preventing reset/reconnect ABA reuse;
|
|
||||||
- a later parent Create may consume only the exact admission token it peeked;
|
|
||||||
- deleting or replacing a still-missing parent does not discard its queued
|
|
||||||
child Create, matching retail's GUID-keyed placeholder behavior;
|
|
||||||
- an exact child Delete removes an equal/older deferred child generation even
|
|
||||||
if no child timestamp gate exists;
|
|
||||||
- generation cleanup preserves an equal or newer deferred child Create and
|
|
||||||
discards only older ownership.
|
|
||||||
|
|
||||||
Raw missing-parent replay and actual child creation belong to the future
|
|
||||||
continuation executor/cutover. They are not performed by this checkpoint.
|
|
||||||
|
|
||||||
## Malformed and saturation behavior
|
|
||||||
|
|
||||||
All structural and capacity checks run before consuming a timestamp gate.
|
|
||||||
|
|
||||||
- Non-finite Vector and Position payloads are rejected without sequence
|
|
||||||
consumption.
|
|
||||||
- A full/saturated continuation owner fails before gate acceptance; there is
|
|
||||||
no fallback to ordinary immediate mutation.
|
|
||||||
- Flattened EntitySpawn projections must exactly agree with the embedded
|
|
||||||
PhysicsDesc for identity, Position, relevant timestamps, parent, and
|
|
||||||
placement.
|
|
||||||
- When PhysicsDesc is absent, every flattened PhysicsDesc projection must also
|
|
||||||
be absent or zero: Position, Setup, Motion, PhysicsState, scale, friction,
|
|
||||||
elasticity, timestamps, parent, and placement.
|
|
||||||
- Instance sequence zero remains legal and is covered on the active pending
|
|
||||||
FIFO path.
|
|
||||||
|
|
||||||
The last rule prevents synthetic or corrupt packets from creating two
|
|
||||||
contradictory placement authorities even though the production parser normally
|
|
||||||
constructs those projections from one source.
|
|
||||||
|
|
||||||
## Lifetime and failure guarantees
|
|
||||||
|
|
||||||
- Delete cancels the matching residence and its FIFO before the exact entity
|
|
||||||
can be reused.
|
|
||||||
- New incarnation/GUID reuse cannot observe or adopt an older incarnation's
|
|
||||||
FIFO.
|
|
||||||
- Session reset/reconnect clears active residence, completed-unadopted batches,
|
|
||||||
deferred raw creates, accepted timestamp ownership, and operation state.
|
|
||||||
- Reentrant teardown callbacks cannot resurrect the detached owner.
|
|
||||||
- Completion/adoption revisions cannot wrap into a valid stale token.
|
|
||||||
- Parent raw-admission IDs cannot wrap or reset into an ABA match.
|
|
||||||
- Every ownership ledger converges to zero on reset/disposal.
|
|
||||||
|
|
||||||
## Named-retail oracle
|
|
||||||
|
|
||||||
The behavior and reviews used these named-retail anchors:
|
|
||||||
|
|
||||||
- `SmartBox::HandleCreateObject` `0x00454C80` - Create packet ordering and
|
|
||||||
missing-parent precondition;
|
|
||||||
- `SmartBox::ProcessObjectNetBlobs` `0x00454B20` - queued packet replay order;
|
|
||||||
- `SmartBox::HandleReceivedPosition` `0x00453FD0` - standalone Position route;
|
|
||||||
- `SmartBox::HandleDeleteObject` `0x00451EA0` - GUID-keyed delete behavior;
|
|
||||||
- `ACCObjectMaint::CreateObject` `0x00558870` - logical object creation;
|
|
||||||
- `CPhysicsObj::set_description` `0x00514F40` - PhysicsDesc application order;
|
|
||||||
- `CPhysicsObj::SetPositionInternal` `0x00515330` - canonical placement.
|
|
||||||
|
|
||||||
Research must continue from
|
|
||||||
`docs/research/named-retail/acclient_2013_pseudo_c.txt`; use the older Ghidra
|
|
||||||
chunks only as a fallback.
|
|
||||||
|
|
||||||
## Files in the behavior checkpoint
|
|
||||||
|
|
||||||
- `src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/ParentAttachmentState.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeInitialCreateAdmissionFreezer.cs`
|
|
||||||
- `src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs`
|
|
||||||
- `src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs`
|
|
||||||
- `tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs`
|
|
||||||
- `tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs`
|
|
||||||
|
|
||||||
## Automated evidence
|
|
||||||
|
|
||||||
Final primary-agent gates on the exact behavior diff:
|
|
||||||
|
|
||||||
- focused initial-residence/classifier tests: **89 passed, 0 failed**;
|
|
||||||
- complete Runtime tests: **829 passed, 0 failed**;
|
|
||||||
- complete Release build: **0 warnings, 0 errors**;
|
|
||||||
- complete Release solution: **10,622 passed, 4 intentional skips**;
|
|
||||||
- `git diff --check`: clean.
|
|
||||||
|
|
||||||
Per-project final solution totals:
|
|
||||||
|
|
||||||
- App: 4,027 passed / 3 skipped;
|
|
||||||
- Bake: 15 passed;
|
|
||||||
- CLI: 4 passed;
|
|
||||||
- Content: 124 passed;
|
|
||||||
- Core.Net: 762 passed;
|
|
||||||
- Core: 4,242 passed / 1 skipped;
|
|
||||||
- Headless: 76 passed;
|
|
||||||
- Runtime: 829 passed;
|
|
||||||
- UI abstractions: 543 passed.
|
|
||||||
|
|
||||||
Independent final results:
|
|
||||||
|
|
||||||
- retail-conformance reviewer: **PASS**;
|
|
||||||
- architecture/adversarial reviewer: **PASS**.
|
|
||||||
|
|
||||||
The reviews explicitly checked exact retail order, gate-only frozen state,
|
|
||||||
missing-parent placeholder lifetime, zero instance, delete/reset/GUID reuse,
|
|
||||||
deep freezing, malformed duplicated projections, capacity preflight,
|
|
||||||
reentrancy, and the absence of executor/cutover work.
|
|
||||||
|
|
||||||
No connected visual gate is required for this checkpoint because production
|
|
||||||
graphical and headless routes remain unchanged and the new owner is exercised
|
|
||||||
only through deterministic Runtime tests.
|
|
||||||
|
|
||||||
## Deliberately unchanged production routes
|
|
||||||
|
|
||||||
At this checkpoint:
|
|
||||||
|
|
||||||
- graphical Create/Position still use the existing App route;
|
|
||||||
- headless Create/Position still use the existing no-window projection route;
|
|
||||||
- no host drains `RuntimeInitialCreateResidenceState.Continuations`;
|
|
||||||
- no raw missing-parent child Create is replayed;
|
|
||||||
- no new gameplay/presentation callback is emitted;
|
|
||||||
- no AP-1 or AD-1 divergence row is retired;
|
|
||||||
- AP-22 and AD-10 are untouched.
|
|
||||||
|
|
||||||
Do not mistake the stored FIFO for completed game behavior. The clean next
|
|
||||||
boundary is the executor that applies it.
|
|
||||||
|
|
||||||
## Next implementation boundary
|
|
||||||
|
|
||||||
Implement only the Runtime continuation executor and retail Create tail.
|
|
||||||
|
|
||||||
The executor must:
|
|
||||||
|
|
||||||
1. consume the exact initial placement acknowledgement once;
|
|
||||||
2. capture executor-time inputs at the retail decision point;
|
|
||||||
3. apply the initial Create tail in retail order;
|
|
||||||
4. drain mixed continuations strictly by retained sequence;
|
|
||||||
5. preserve same-Create atomicity;
|
|
||||||
6. keep the FIFO head retryable if an external host receipt is temporarily
|
|
||||||
unavailable;
|
|
||||||
7. make every hook, timestamp, placement, and event side effect exactly once;
|
|
||||||
8. consume a raw missing-parent Create only through its exact `AdmissionId`;
|
|
||||||
9. abandon safely on delete, reset, replacement, or generation mismatch;
|
|
||||||
10. produce host-independent immutable results rather than calling App or
|
|
||||||
headless presentation directly.
|
|
||||||
|
|
||||||
Do not combine the executor with graphical/headless cutover. After the
|
|
||||||
executor is independently green, the following checkpoint may switch every
|
|
||||||
Create, Position, ForcePosition, Parent, Pickup, withdrawal, remote,
|
|
||||||
projectile, dropped-item, and teardown route across both hosts together.
|
|
||||||
|
|
||||||
Only after executor plus all-host cutover and connected gates pass may AP-1
|
|
||||||
and AD-1 retire. AP-22 and AD-10 remain later independent slices.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
Revert this behavior checkpoint without disturbing the prior residence
|
|
||||||
foundation:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git revert 30012361e12222e8271b1531574257ba910c77cb
|
|
||||||
```
|
|
||||||
|
|
||||||
The documentation checkpoint containing this file is separate and may be
|
|
||||||
reverted independently if only the handoff text needs correction.
|
|
||||||
|
|
||||||
## Resume checklist
|
|
||||||
|
|
||||||
1. Open the exact worktree and branch above.
|
|
||||||
2. Confirm `git log -3 --oneline` contains behavior `30012361` and the
|
|
||||||
documentation commit containing this file.
|
|
||||||
3. Preserve every unrelated dirty path listed above.
|
|
||||||
4. Read this file, `docs/architecture/acdream-architecture.md`,
|
|
||||||
`docs/research/2026-08-01-runtime-initial-create-residence-handoff.md`, and
|
|
||||||
`docs/research/2026-07-31-canonical-set-position.md`.
|
|
||||||
5. Re-run the focused 89-test gate before changing admission/execution code.
|
|
||||||
6. Begin only the continuation executor. Do not begin production cutover,
|
|
||||||
AP-22, AD-10, or vendor work in the same checkpoint.
|
|
||||||
|
|
@ -1,287 +0,0 @@
|
||||||
# Runtime local-player physics publication - 2026-08-01
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This is placement Slice 4B2 checkpoints 4-6. It adds the dormant,
|
|
||||||
presentation-independent transaction which prepares and assigns ownership of
|
|
||||||
one local-player `PhysicsBody` and `PlayerMovementController`, retains an exact
|
|
||||||
post-ownership evaluation lease, and commits the canonical Runtime SetPosition
|
|
||||||
activation in retail order. No App or Headless production route invokes this
|
|
||||||
transaction yet, so graphical and no-window game behavior is unchanged and
|
|
||||||
AP-1/AD-1 remain open until their hosts cut over.
|
|
||||||
|
|
||||||
Checkpoint 6 consumes the prepared placement operation only after the exact
|
|
||||||
body/controller/identity/collision envelope is current. It publishes FullCell,
|
|
||||||
world residence, host, shadow, workset, object-clock, and ordered Place state
|
|
||||||
from that same Runtime-owned dormant body. There is no second body, mirrored
|
|
||||||
gameplay owner, rollback mutation, or presentation callback inside the
|
|
||||||
canonical tail.
|
|
||||||
|
|
||||||
## Ownership contract
|
|
||||||
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState` is the sole owner of unpublished
|
|
||||||
local-player body/controller candidates. Each candidate is bound to a token
|
|
||||||
containing:
|
|
||||||
|
|
||||||
- The exact `RuntimeEntityKey` and authored SetPosition placement token.
|
|
||||||
- A monotonic publication ID.
|
|
||||||
- The nonzero canonical local-player server GUID and exact identity revision.
|
|
||||||
- The record's physics-body and object-clock ownership epochs.
|
|
||||||
- The movement state's controller ownership epoch.
|
|
||||||
- The entity directory's session-lifetime authority.
|
|
||||||
|
|
||||||
Preparation constructs a private controller, body, and object clock. It applies
|
|
||||||
the exact authored cell frame, orientation, Setup sphere list, scale, step
|
|
||||||
heights, and accepted final physics state without mutating the canonical entity,
|
|
||||||
shared object clock, engine/worksets, shadow registry, FullCell, host state, or
|
|
||||||
presentation. The candidate remains explicitly out of world and inactive. No
|
|
||||||
method exposes its controller, body, clock, or another mutable reference while
|
|
||||||
it is owned by the publication transaction.
|
|
||||||
|
|
||||||
This checkpoint accepts only a pristine initial graph: no canonical body,
|
|
||||||
movement controller, physics host, remote motion, projectile, acquisition or
|
|
||||||
binding operation, or remote-placement contract may exist. It cannot replace or
|
|
||||||
upgrade a live graph. The local-player identity must be live, nonzero, and name
|
|
||||||
the same server GUID as the exact entity incarnation.
|
|
||||||
|
|
||||||
Unpublished candidates and ownership-committed dormant controllers reject live
|
|
||||||
movement operations: update, public SetPosition, blip, outbound-position
|
|
||||||
capture, movement/position send tracking, and shared-engine position commit.
|
|
||||||
Only the checkpoint-6 activation transaction may promote `RuntimeOwnedDormant`
|
|
||||||
to `RuntimePublished`; preparation and evaluation never invoke that transition.
|
|
||||||
Once a
|
|
||||||
Runtime-owned dormant or published controller is replaced, reset, or disposed,
|
|
||||||
its terminal retirement state rejects the same operations plus
|
|
||||||
body/configuration mutation and manager acquisition. Publicly constructed
|
|
||||||
legacy controllers keep their existing standalone behavior.
|
|
||||||
|
|
||||||
## Failure-atomic commit
|
|
||||||
|
|
||||||
Commit revalidates every authority after preparation:
|
|
||||||
|
|
||||||
- The entity record is the current incarnation and is not accepted for delete.
|
|
||||||
- The local-player identity still has the token's exact GUID and revision and
|
|
||||||
has not been disposed.
|
|
||||||
- The exact authored SetPosition operation and sealed command remain current.
|
|
||||||
- Session, body, object-clock, and controller ownership epochs still match.
|
|
||||||
- The body/controller/host/remote/projectile graph remains completely pristine,
|
|
||||||
with no acquisition, binding, or remote-placement operation in progress.
|
|
||||||
|
|
||||||
Only after validation completes does the callback-free update-thread tail:
|
|
||||||
|
|
||||||
1. Rebind the candidate controller from its private clock to the record's exact
|
|
||||||
canonical object clock and mark it Runtime-owned but dormant.
|
|
||||||
2. Store the candidate's exact body on the canonical record, advancing the
|
|
||||||
physics ownership epoch once.
|
|
||||||
3. Store the same controller in `RuntimeLocalPlayerMovementState`, advancing the
|
|
||||||
controller ownership epoch once.
|
|
||||||
|
|
||||||
The dormant controller rejects every live/configuration operation after these
|
|
||||||
stores; ownership commit alone cannot tick physics, mutate the canonical clock,
|
|
||||||
or publish an outbound frame. These stores allocate no new gameplay owner,
|
|
||||||
invoke no host or presentation callback, and cannot replay an older
|
|
||||||
incarnation. Replacing SetPosition,
|
|
||||||
changing any accepted physics authority, binding remote/projectile state,
|
|
||||||
replacing body/clock/controller ownership, delete plus GUID reuse, reset, or
|
|
||||||
disposal causes the token to reject. An identity switch away and back also
|
|
||||||
rejects because its revision changed. A rejected or superseded candidate is
|
|
||||||
discarded and cannot perform a later live operation. Reset and disposal converge
|
|
||||||
the publication ledger to zero candidates.
|
|
||||||
|
|
||||||
Repeated stores of the same body/controller do not advance their epochs; real
|
|
||||||
bind, replacement, and unbind edges do. This makes ABA-shaped reference changes
|
|
||||||
observable even if a later value happens to equal an earlier reference.
|
|
||||||
|
|
||||||
## Dormant SetPosition evaluation lease
|
|
||||||
|
|
||||||
Ownership commit now returns one private activation token captured only after
|
|
||||||
the canonical body and controller stores. It binds the exact entity key,
|
|
||||||
authored placement token and sealed command, local identity GUID/revision,
|
|
||||||
session lifetime, and the post-store physics-body, object-clock, and controller
|
|
||||||
ownership epochs. The owner retains the same record, body, controller, and
|
|
||||||
command behind that token; no caller can substitute an equivalent-looking
|
|
||||||
body or rebuild the mover.
|
|
||||||
|
|
||||||
`EvaluateActivation` revalidates that complete lease and calls Core
|
|
||||||
`PhysicsEngine.SetPosition` synchronously with an immutable request. Core's
|
|
||||||
transaction is pure: it returns committed, deferred-cell, or rejected
|
|
||||||
placement data without writing the canonical body, FullCell, clock, spatial
|
|
||||||
worksets, shadows, collision-report owners, host, operation stage, or Place
|
|
||||||
projection. A missing cell therefore leaves the exact body dormant and the
|
|
||||||
authored operation retryable. During evaluation, a valid result likewise
|
|
||||||
remains only an immutable receipt; checkpoint 6's separate commit API consumes
|
|
||||||
that receipt only after revalidating the complete activation envelope.
|
|
||||||
|
|
||||||
Each evaluation carries an append-only, stable-order union of every cell read
|
|
||||||
by the complete Core transaction: the AdjustPosition seed and adjusted cell,
|
|
||||||
visible-child probes (including rejected lateral siblings), rejected
|
|
||||||
portal/building containment probes, transition/compass retries, and every
|
|
||||||
normal or scatter attempt. Rejected probes enter only the authority union and
|
|
||||||
never the final successful shadow/CrossCell footprint. Scatter keeps that union
|
|
||||||
in retained scratch and materializes its
|
|
||||||
immutable receipt exactly once after the final attempt, avoiding quadratic
|
|
||||||
copy/allocation growth at the 64-attempt retail ceiling. The final
|
|
||||||
`CrossCellIds` remains the successful placement's authored
|
|
||||||
shadow footprint; failed scatter probes cannot leak into that commit payload.
|
|
||||||
Runtime seals every distinct queried landblock against the exact collision
|
|
||||||
generation, the global collision-world authority, and the dynamic-shadow
|
|
||||||
mutation revision. An active replacement admission rejects evaluation even
|
|
||||||
before it commits, while begin/cancel, a re-entrant generation commit, or any
|
|
||||||
owner insert/remove/move/state/suspend/reflood mutation invalidates an older
|
|
||||||
receipt.
|
|
||||||
|
|
||||||
Entry restrictions also consult the live `ClientObjectTable` for the resolved
|
|
||||||
house object, owner and complete restriction record, plus the mover's monarch.
|
|
||||||
The receipt therefore seals the exact object-table reference, the engine's
|
|
||||||
monotonic binding epoch, and the table's synchronous mutation revision. Object
|
|
||||||
creation/removal, owner-property, guest-list, or mover-monarch updates invalidate
|
|
||||||
the receipt; a null/fresh replacement and an equal-revision A-B-A binding cycle
|
|
||||||
cannot resurrect it. Retained `ClientObject` owner, monarch, and restriction
|
|
||||||
setters synchronously advance every exact owning table even when callers mutate
|
|
||||||
the object directly rather than re-submit it through `AddOrUpdate`. Replacement,
|
|
||||||
removal, and clear detach that observer exactly, and every
|
|
||||||
`HouseRestrictionRecord` freezes a defensive snapshot of its input guest map so
|
|
||||||
no caller-owned dictionary or mutable downcast can alter entry authority behind
|
|
||||||
the revision.
|
|
||||||
|
|
||||||
`IsEvaluationCurrent` accepts only the newest receipt for the exact activation
|
|
||||||
lease and rejects it after a position/vector/state/object-description/Create
|
|
||||||
authority change, identity revision, body/controller replacement, session or
|
|
||||||
incarnation change, or any sealed collision/shadow authority change.
|
|
||||||
Re-evaluation supersedes the older receipt without mutating world state.
|
|
||||||
Re-entrant reset or delete-plus-GUID-reuse during Core evaluation immediately
|
|
||||||
retires the invalid lease instead of leaving an orphaned dormant graph. An
|
|
||||||
existing activation lease also blocks candidate preparation even if an
|
|
||||||
external owner has already cleared the body/controller references; explicit
|
|
||||||
discard is required before a new candidate can be prepared. Reset and disposal
|
|
||||||
retire the lease, body, and dormant controller and include the pending
|
|
||||||
activation in the ownership convergence ledger.
|
|
||||||
|
|
||||||
## Canonical activation and retail ordering
|
|
||||||
|
|
||||||
The implementation follows the named-retail chain rather than treating
|
|
||||||
SetPosition as a single opaque callback:
|
|
||||||
|
|
||||||
- `CPhysicsObj::SetPosition` at `0x005160C0` owns the outer placement call.
|
|
||||||
- The internal wrapper at `0x00515BD0` evaluates residence and collision.
|
|
||||||
- `CPhysicsObj::SetPositionInternal(CTransition*)` at `0x00515330` commits the
|
|
||||||
accepted frame/contact prefix and later shadow/cell state.
|
|
||||||
- `CPhysicsObj::enter_world` at `0x00516170` is the final live edge.
|
|
||||||
- `CPhysicsObj::leave_world` at `0x005155A0` is the canonical retirement edge.
|
|
||||||
|
|
||||||
Runtime splits that chain into a prepared, callback-free transaction and an
|
|
||||||
ordered notification suffix:
|
|
||||||
|
|
||||||
1. Install the accepted frame and contact prefix on the still-dormant body and
|
|
||||||
perform the first acceleration calculation.
|
|
||||||
2. Open one narrow dormant ground phase and invoke `HitGround` or
|
|
||||||
`LeaveGround`. Movement reapplication may call retail `set_velocity`, but
|
|
||||||
the phase closes with `Active=false`; the body is still out of world, has no
|
|
||||||
host/spatial membership, and its object clock is inactive.
|
|
||||||
3. Synchronize accepted State and Vector authorities, run the post-ground
|
|
||||||
acceleration/sliding phase, and dispatch the already-installed collision
|
|
||||||
batch.
|
|
||||||
4. Revalidate the complete ownership/collision envelope. Accepted State and
|
|
||||||
Vector updates are synchronized; Position, ObjDesc, Create, Setup,
|
|
||||||
incarnation, identity, collision-generation, body, controller, host, or
|
|
||||||
session displacement aborts the old transaction.
|
|
||||||
5. Apply velocity-current physical response and stationary bits, prepare the
|
|
||||||
final shadow mutation and Place receipt, then perform the callback-free
|
|
||||||
FullCell/body/host/controller/spatial/object-clock tail.
|
|
||||||
6. Dispatch exact shadow notifications and the ordered Place projection only
|
|
||||||
after the complete live graph is visible.
|
|
||||||
|
|
||||||
Collision and shadow mutations use explicit prepare/apply/dispatch receipts.
|
|
||||||
Receipt dispatch is exact-once and owner-local, so reverse-order receipts for
|
|
||||||
different owners remain valid while a superseding mutation of the same owner
|
|
||||||
stops the stale suffix. Collision owner states carry the exact SetPosition
|
|
||||||
batch ID. Reentrant Position or newer-batch replacement suppresses remaining
|
|
||||||
reciprocal/environment callbacks, and abort cleanup force-ends/removes only the
|
|
||||||
still-exact old batch, including reverse rows and the environment latch. The
|
|
||||||
combined Runtime physics ownership ledger includes pending collision and
|
|
||||||
shadow SetPosition receipts; teardown cannot report convergence while either
|
|
||||||
receipt remains.
|
|
||||||
|
|
||||||
Candidate construction applies the accepted `PhysicsDesc` values in retail
|
|
||||||
`CPhysicsObj::set_description` order before sealing ownership: final state,
|
|
||||||
friction, clamped elasticity, `set_velocity` (including the 50-unit clamp),
|
|
||||||
and angular velocity. Network acceleration remains parse-only because retail
|
|
||||||
recalculates it from the final physics state. This initial vector bootstrap is
|
|
||||||
required even when the SetPosition receipt's source Vector authority is still
|
|
||||||
current; the later refresh intentionally skips in that case. Collision
|
|
||||||
callbacks may advance State/Vector authority without invalidating the
|
|
||||||
immutable geometry/identity envelope, and a changed Vector authority refreshes
|
|
||||||
the dormant body through the same `set_velocity` path before physical response.
|
|
||||||
|
|
||||||
A deferred-cell commit atomically suspends an authored shadow registration and
|
|
||||||
consumes its notification receipt. Explicit publication discard cancels the
|
|
||||||
exact SetPosition lease and body/controller ownership, while the suspended
|
|
||||||
registration remains owned by the live entity/shadow registry and is reusable
|
|
||||||
by a later activation. A deterministic discard -> generation-ready -> new
|
|
||||||
activation gate proves the same registration restores without stale rows or a
|
|
||||||
pending receipt. Entity/lifetime teardown remains the terminal owner of that
|
|
||||||
suspended registration.
|
|
||||||
|
|
||||||
## Gates
|
|
||||||
|
|
||||||
- Candidate privacy and live-operation rejection.
|
|
||||||
- Pristine-only admission for body, controller, host, remote/projectile,
|
|
||||||
acquisition/binding, and remote-placement ownership.
|
|
||||||
- Exact local-player identity, identity-switch, and disposed-identity rejection.
|
|
||||||
- Exact same-body ownership in entity record and dormant movement controller.
|
|
||||||
- Initial PhysicsDesc velocity, angular velocity, friction, and elasticity
|
|
||||||
bootstrap, including activation with the retail 50-unit velocity clamp.
|
|
||||||
- Dormant rejection after ownership commit plus the controller-level
|
|
||||||
`dormant -> activated -> live` lifecycle contract exercised by checkpoint 6.
|
|
||||||
- No mutation of SetPosition, FullCell, spatial roots, host projections,
|
|
||||||
shadows, worksets, world residence, or presentation during preparation or
|
|
||||||
evaluation; the separately gated activation commit owns those mutations.
|
|
||||||
- Replacement by position, vector, final physics state, object description,
|
|
||||||
CreateObject, remote/projectile/body/clock/controller ownership, and explicit
|
|
||||||
placement cancellation.
|
|
||||||
- Delete plus same-GUID reincarnation.
|
|
||||||
- Candidate replacement, reset, disposal, and ownership convergence.
|
|
||||||
- Publication/activation sequence exhaustion is preflighted before candidate
|
|
||||||
allocation or replacement, leaving no private or canonical owner behind.
|
|
||||||
- Shadow-registry reset invalidates even a prepared, unapplied shapeless
|
|
||||||
transaction which owns no logical rows or pending dispatch receipt.
|
|
||||||
- Pure committed/deferred/rejected SetPosition evaluation with bit-exact
|
|
||||||
body-state snapshots and no canonical, collision-report, projection,
|
|
||||||
clock, FullCell, host, shadow, workset, or operation-stage mutation.
|
|
||||||
- Complete stable-order queried-cell capture across AdjustPosition,
|
|
||||||
visible-child lookup, normal/scatter retries, map-edge/deferred, rejected,
|
|
||||||
committed, and defensive NoCell outcomes. Scatter deliberately retains
|
|
||||||
retail's RNG consumption; only its authority footprint and commit payload
|
|
||||||
are deterministic for a fixed draw sequence.
|
|
||||||
- Newest-receipt selection, active-admission rejection, collision-generation
|
|
||||||
replacement, re-entrant begin/cancel and commit invalidation, plus dynamic
|
|
||||||
shadow insert/move/state/suspend/remove invalidation.
|
|
||||||
- Exact object-table reference/revision/binding authority, including
|
|
||||||
post-evaluation and re-entrant house-object, owner, guest-list, and mover-
|
|
||||||
monarch mutations plus null/fresh/equal-revision ABA replacement. Direct
|
|
||||||
retained-object setters, replacement/removal/clear observer lifetime, shared
|
|
||||||
multi-table ownership, and frozen guest-map input are covered explicitly.
|
|
||||||
- Re-entrant reset and delete/GUID-reuse convergence plus activation-lease
|
|
||||||
overwrite prevention after an external body/controller clear.
|
|
||||||
- Post-ownership position, vector, object-description, Create, identity,
|
|
||||||
body, and controller authority replacement.
|
|
||||||
- Terminal stale-controller rejection after replacement, reset, and disposal.
|
|
||||||
- Body/controller epochs advance only on actual ownership changes.
|
|
||||||
|
|
||||||
The checkpoint-6 focused publication/collision suite passes 129/129, the
|
|
||||||
focused Core shadow transaction suite passes 16/16, and the complete Runtime
|
|
||||||
project passes 695/695 under invariant globalization. The Runtime Release build
|
|
||||||
passes with zero warnings and zero errors. Broader Core/App/solution and
|
|
||||||
connected gates remain for the parent integration checkpoint. Under the
|
|
||||||
machine's Swedish current culture, the three previously known formatting
|
|
||||||
assertions remain unrelated (`0,5` versus `0.5` and localized sky text), so the
|
|
||||||
canonical Runtime gate runs under invariant globalization.
|
|
||||||
|
|
||||||
## Next checkpoint
|
|
||||||
|
|
||||||
Cut the graphical and no-window local-player hosts over to this Runtime-owned
|
|
||||||
activation transaction, then delete their duplicate SetPosition
|
|
||||||
activation/publication paths. The cutover must preserve the same exact body,
|
|
||||||
controller, shadow payload, deferred-cell lease, collision receipt ordering,
|
|
||||||
and graceful teardown proven here; no host may reconstruct or replay the
|
|
||||||
canonical transaction.
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
# Runtime SetPosition authored mover preparation - 2026-08-01
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This is placement Slice 4B2 checkpoint 3. It adds the dormant,
|
|
||||||
presentation-independent preparation contract used to turn an accepted Runtime
|
|
||||||
position into retail's exact `CPhysicsObj::SetPosition` mover input. No App or
|
|
||||||
Headless production route consumes the contract yet, so game behavior is
|
|
||||||
unchanged and AP-1/AD-1 remain open.
|
|
||||||
|
|
||||||
## Retail oracle
|
|
||||||
|
|
||||||
The implementation was checked against the named September 2013 client:
|
|
||||||
|
|
||||||
- `PhysicsDesc::PhysicsDesc` `0x0051D4D0`
|
|
||||||
- `PhysicsDesc::UnPack` `0x0051DDD0`
|
|
||||||
- `CPhysicsObj::set_description` `0x00514F40`
|
|
||||||
- `CPhysicsObj::SetPosition` `0x005160C0`
|
|
||||||
- `SPHEREPATH::init_sphere` `0x0050C670`
|
|
||||||
- `CPartArray::GetNumSphere` `0x00518060`
|
|
||||||
- `CPartArray::GetSphere` `0x00518070`
|
|
||||||
- `CPartArray::GetStepUpHeight` `0x005180D0`
|
|
||||||
- `CPartArray::GetStepDownHeight` `0x005180F0`
|
|
||||||
- `CTransition::init_object` `0x00509E40`
|
|
||||||
- `OBJECTINFO::init` `0x0050CF30`
|
|
||||||
|
|
||||||
SetPosition calls `CTransition::init_object(..., state = 0)` directly. It does
|
|
||||||
not use the ordinary-movement `CPhysicsObj::get_object_info` path. Consequently
|
|
||||||
the SetPosition state carries the player/PK/PKLite/impenetrable classifications
|
|
||||||
(plus acdream's pointer-free entry-restriction carrier), but it does not add
|
|
||||||
Contact, OnWalkable, PathClipped, FreeRotate, or EdgeSlide. Ethereal and
|
|
||||||
`step_down = !Missile` are separate `OBJECTINFO` fields derived from the current
|
|
||||||
physics state.
|
|
||||||
|
|
||||||
## Exact preparation contract
|
|
||||||
|
|
||||||
- Runtime captures the complete accepted server frame under the exact entity,
|
|
||||||
session, position, vector/velocity, wire-state, final-physics-state mutation,
|
|
||||||
object-description, and create-integration authorities. A host cannot
|
|
||||||
substitute a second position.
|
|
||||||
- Collision-world X/Y uses the target landblock's active live-centered offsets;
|
|
||||||
full cell ID, cell-local XYZ, and the complete quaternion remain unchanged.
|
|
||||||
- Setup resolution is bound to the canonical Setup DID. A known but unavailable
|
|
||||||
Setup remains retryable. Resolved-absent is valid only when the canonical
|
|
||||||
object has no Setup. An authored empty Setup remains distinct and still
|
|
||||||
contributes its scaled StepUp/StepDown heights.
|
|
||||||
- The complete ordered Setup sphere list is retained. Core later applies
|
|
||||||
retail's `min(count, 2)` traversal cap. The successfully resolved no-PartArray
|
|
||||||
or zero-sphere arm reaches Core with an empty list, where SetPosition supplies
|
|
||||||
the retail dummy sphere `(0,0,0.1)`, radius `0.1`, scale `1.0`.
|
|
||||||
- Scale precedence is `PhysicsDesc.Scale ?? EntitySpawn.ObjScale ?? 1.0`.
|
|
||||||
Present zero and finite negative values are preserved. Scale is not consumed
|
|
||||||
by a resolved-absent dummy mover.
|
|
||||||
- Every authored command is sealed to the exact preparation operation. Manual,
|
|
||||||
stale, replaced, or merely value-equivalent commands cannot bypass the seal.
|
|
||||||
- A wire-state, final-state mutation (including NoDraw and missile-stop),
|
|
||||||
vector/velocity, description, or create change during a deferred cell wait
|
|
||||||
returns the resident to `AwaitingPreparation`; the stale mover is never
|
|
||||||
replayed when the collision generation wakes.
|
|
||||||
- Preparation mutates no body, clock, FullCell, spatial/shadow registration,
|
|
||||||
bucket, camera, world entity, or presentation resource.
|
|
||||||
|
|
||||||
Legacy direct SetPosition remains a distinct token mode so the dormant slice
|
|
||||||
does not change existing call sites or their warmed allocation ceiling. If a
|
|
||||||
legacy operation becomes deferred and later needs new authored data, the
|
|
||||||
presence of Runtime's preparation authority makes the exact seal mandatory.
|
|
||||||
|
|
||||||
## Ownership and validation
|
|
||||||
|
|
||||||
`RuntimeSetPositionState` owns one exact-key preparation-authority entry only
|
|
||||||
for operations which require authored preparation. The entry dies with the
|
|
||||||
operation on replacement, acknowledgement, cancellation, delete, session
|
|
||||||
reset, or disposal and participates in terminal convergence accounting.
|
|
||||||
|
|
||||||
Preparation-only validation checks the exact cell frame, live-centered world
|
|
||||||
position, values consumed by the first two retail spheres, Setup-derived step
|
|
||||||
heights, line/scatter inputs, and bounded scatter attempts. The legacy direct
|
|
||||||
validator retains its prior behavior, including retail's dummy-sphere and
|
|
||||||
first-two-sphere semantics.
|
|
||||||
|
|
||||||
## Gates and review
|
|
||||||
|
|
||||||
- Focused authored-mover plus SetPosition residence tests: 80/80.
|
|
||||||
- Runtime Release build: zero warnings and zero errors.
|
|
||||||
- Complete Runtime project under invariant culture: 595/595.
|
|
||||||
- Complete Release solution with installed DAT/pak fixtures: 10,342 passed /
|
|
||||||
4 intentional skips.
|
|
||||||
- Retail-conformance review: canonical frame, DID binding, scale/step/sphere
|
|
||||||
behavior, exact SetPosition flags, and deferred wake checked against the
|
|
||||||
named addresses above.
|
|
||||||
- Architecture/adversarial review: command sealing, legacy promotion,
|
|
||||||
replacement, deferred wake, direct compatibility, allocation, reset, GUID
|
|
||||||
reuse, and ownership convergence checked.
|
|
||||||
|
|
||||||
The three ordinary current-culture Runtime failures are pre-existing Swedish-
|
|
||||||
locale formatting assumptions (`0,5` versus `0.5` and localized sky text); the
|
|
||||||
same complete project passes under invariant culture.
|
|
||||||
|
|
||||||
## Next checkpoint
|
|
||||||
|
|
||||||
Implement the dormant atomic local-player physics publication transaction:
|
|
||||||
prepare a private controller/body/clock without canonical mutation, evaluate
|
|
||||||
SetPosition against that candidate, then publish the exact same body relation
|
|
||||||
to `RuntimeEntityRecord` and `RuntimeLocalPlayerMovementState` in one callback-
|
|
||||||
free Runtime commit. App and Headless production activation remains a later
|
|
||||||
checkpoint.
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
# C3c production placement cutover — closeout (2026-08-02)
|
|
||||||
|
|
||||||
Behavior commit: `529e0e9d` (68 files, +5,979/−833, register rows AD-61 +
|
|
||||||
AD-42 refresh in-commit). Plan:
|
|
||||||
[`2026-08-02-placement-cutover.md`](../plans/2026-08-02-placement-cutover.md).
|
|
||||||
Session evidence trail: the campaign scratchpad's `implementer-progress.md`
|
|
||||||
sections `Continuation 1-4`, `C3c-F1`..`C3c-F5`, `C3c-R1` (not committed;
|
|
||||||
summarized here).
|
|
||||||
|
|
||||||
## What shipped
|
|
||||||
|
|
||||||
Both production hosts (graphical + headless) register every initial
|
|
||||||
wire Create through the C0-C3b residence/executor/conductor machinery.
|
|
||||||
One shared `RuntimeFirstEntryDriveController` pumps the local-player and
|
|
||||||
remote conductors from the placement-receipt flow (per-frame graphical,
|
|
||||||
per-tick headless). `MaterializeProjection`/`RebucketLiveEntity` are
|
|
||||||
presentation-only strictly while the initial-create residence is ACTIVE
|
|
||||||
(exact-token check; `ExecutorCompleted` is the presentation-binding
|
|
||||||
receipt); post-residence entities take the full legacy path including
|
|
||||||
retail's `prepare_to_enter_world` (0x00511FA0) clock rebase.
|
|
||||||
`RuntimeLocalPlayerMovementState.Controller`'s setter is sealed; every
|
|
||||||
controller mutation flows through the publication lifecycle. Content-less
|
|
||||||
headless sessions (validated-legal config) keep the pre-flip direct
|
|
||||||
registration until C4/C5 revisit.
|
|
||||||
|
|
||||||
## The five fix slices (each connected-gated inside the cutover)
|
|
||||||
|
|
||||||
- **F1** — live movement-stat + server-physics application moved behind
|
|
||||||
Runtime ownership (`RuntimeMovementStatsApplication`,
|
|
||||||
`ApplyServerPhysicsState`); the post-logout ingest crash on the
|
|
||||||
retired controller is eliminated; `RuntimeMovementSkillProjection`
|
|
||||||
deleted.
|
|
||||||
- **F2** — the login activation wedge (world never revealed): the
|
|
||||||
collision-admission prefix gate factored out of the seal (reentrant
|
|
||||||
commit could yield terminal `RejectedAuthority`), the rearm's
|
|
||||||
generation identity corrected (parked G vs post-retirement G+1), and
|
|
||||||
`PlayerModeAutoEntry` now requires the Runtime-published controller
|
|
||||||
(`IsPlayerControllerReady` was a constant `true` — one early attempt
|
|
||||||
permanently sealed the reveal).
|
|
||||||
- **F3** — landblock-prefix `0`-sentinel replaced by explicit absent-id
|
|
||||||
representation; map-corner landblocks (grid row/col 0, e.g.
|
|
||||||
`0x0000FFFF`) are legal through admission, park/rearm/retire,
|
|
||||||
quiescence, and outdoor shadow seeds.
|
|
||||||
- **F4** — diagnosis only: the nine-stop soak's convergence failure
|
|
||||||
(pendingPublications=1, farBacklog nonzero, landblock/mesh dimensions)
|
|
||||||
is **pre-existing `6b28ff99`** (2026-07-31, "make collision activation
|
|
||||||
starvation-free"): every far publication clones the complete collision
|
|
||||||
world (median ~19.7k leaves / 3.64 ms), so the queue drains ~10
|
|
||||||
landblocks/s and never catches its window. Fix requires an O(changed)
|
|
||||||
clone (structural sharing or per-landblock atomic unit) — a semantics
|
|
||||||
change to that slice's asserted one-leaf-per-step invariant; scheduled
|
|
||||||
as its own slice BEFORE C5 (whose gate matrix includes the soak).
|
|
||||||
- **F5** — local-player first-entry ground contact: retail seeds contact
|
|
||||||
from the first gravity frame's transition touch (`enter_world`
|
|
||||||
0x00516170 carries no seed; local player and remotes share the
|
|
||||||
mechanism via `HandleCreateObject` 0x00454C80). The shared
|
|
||||||
`SpawnPlacementSettler` (moved App→Core) runs at `FinalizeActivation`
|
|
||||||
exactly once; genuinely airborne spawns stay airborne; the outbound
|
|
||||||
contact bit chain is asserted end-to-end. The legacy path's
|
|
||||||
unconditional `Contact|OnWalkable|Active` force-seed (non-retail, no
|
|
||||||
plane) still runs during candidate preparation and is OVERWRITTEN by
|
|
||||||
the faithful settle (register AD-61). Fixes the user-observed
|
|
||||||
standing-cast "You can't do that while in the air!" rejections.
|
|
||||||
|
|
||||||
## Review round R1 (dual Opus: initial FAIL 2+2 MAJOR → delta PASS both)
|
|
||||||
|
|
||||||
Retail MAJORs: the login constraint leash (deleted with the legacy
|
|
||||||
resolve path; re-armed at the committed placement in
|
|
||||||
`FinalizeActivation` — `HandleReceivedPosition` 0x00453FD0 arms on every
|
|
||||||
accepted position) and the post-residence rebucket scope (fixed to
|
|
||||||
exact-token active-residence). Adversarial MAJORs: content-less headless
|
|
||||||
(no drive → legacy registration) and the register rows. Nine minors
|
|
||||||
fixed (owner conversion API with active-residence throw, wire-landblock
|
|
||||||
guards, drive-pending ledger in `IsConverged`, route attach/detach
|
|
||||||
latch, celless conversion for far headless remotes, doc-comment truth,
|
|
||||||
per-incarnation cylinder cache, executor-drain drift model documented +
|
|
||||||
source-pinned); two tracked (#276, #277 in ISSUES).
|
|
||||||
|
|
||||||
## Final gates
|
|
||||||
|
|
||||||
Runtime 1,003; App 4,039/3 skips; Headless 79; complete solution
|
|
||||||
**10,816 / 0 failed / 4 skips** (Release, `-m:1`). Connected
|
|
||||||
lifecycle/reconnect gate **PASS** (`connected-world-gate-20260802-175401`;
|
|
||||||
graceful exits, world-visible, zero airborne-rejection strings; run
|
|
||||||
`-174811` failed on user-interference fingerprint —
|
|
||||||
`activeTeleportCount=1` at the stable checkpoint — and is attributed,
|
|
||||||
not counted). The soak stays red for the pre-existing F4 attribution.
|
|
||||||
|
|
||||||
## Process lessons (carried to memory)
|
|
||||||
|
|
||||||
1. **Report artifacts over marker logs** — three wrong classifications
|
|
||||||
this campaign came from reading route/marker logs instead of
|
|
||||||
`report.json` (the soak "clean route" was Passed=false with 37
|
|
||||||
convergence failures).
|
|
||||||
2. **Log lifetime before absence claims** — a 26-second, 67-line log's
|
|
||||||
silence about a defect proves nothing (the 122749 misread inverted a
|
|
||||||
root-cause classification twice).
|
|
||||||
3. **User observation is the cheapest gate** — the standing-cast
|
|
||||||
airborne rejections and the black-screen reveal were both
|
|
||||||
user-spotted minutes before harness detection.
|
|
||||||
4. **The seal finds the bypasses** — sealing the controller setter
|
|
||||||
surfaced a runtime-mutation bypass (F1) the compile-break audit could
|
|
||||||
not see; expect the same class when sealing any long-lived escape
|
|
||||||
hatch.
|
|
||||||
|
|
@ -1,681 +0,0 @@
|
||||||
# C1 body/controller-publication writer map (2026-08-02)
|
|
||||||
|
|
||||||
Repo: `C:\Users\erikn\.codex\worktrees\af5e\acdream`, branch `codex/port-claude-agents`,
|
|
||||||
HEAD `ae296393`. READ-ONLY research; this file is the only write target.
|
|
||||||
|
|
||||||
Context read: `docs/plans/2026-08-02-placement-cutover.md` (slice C1),
|
|
||||||
`docs/research/2026-07-31-remaining-physics-campaign-handoff.md` (rejected-prototype
|
|
||||||
section, lines 143-168; prerequisite C, lines 203-221), and
|
|
||||||
`docs/research/2026-08-02-cutover-route-inventory.md` route 1 + prerequisite-C
|
|
||||||
section (lines 174-220) + route 8 (headless).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Every writer of `RuntimeEntityRecord.PhysicsBody`
|
|
||||||
|
|
||||||
`PhysicsBody` is `public PhysicsBody? PhysicsBody { get; private set; }`
|
|
||||||
(`src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs:72`). The ONLY mutator is
|
|
||||||
the internal method `SetPhysicsBody(PhysicsBody? body)`
|
|
||||||
(`RuntimeEntityRecord.cs:176-182`):
|
|
||||||
```
|
|
||||||
internal void SetPhysicsBody(PhysicsBody? body)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(PhysicsBody, body)) return;
|
|
||||||
PhysicsBody = body;
|
|
||||||
PhysicsOwnershipEpoch++; // <-- the ONLY place PhysicsOwnershipEpoch is bumped
|
|
||||||
}
|
|
||||||
```
|
|
||||||
So every "writer" is a caller of `.SetPhysicsBody(...)` (all 6 call sites, confirmed
|
|
||||||
by full-repo grep, zero others):
|
|
||||||
|
|
||||||
1. **`RuntimeEntityDirectory.cs:359`** — inside
|
|
||||||
`GetOrCreatePhysicsBody(RuntimeEntityRecord record, Func<incarnation,PhysicsBody> factory)`
|
|
||||||
(need exact surrounding signature — read below). Public/internal API used by
|
|
||||||
the route-1 "SECOND, narrower body-construction duplicate authority" for
|
|
||||||
non-player static-animating physics objects
|
|
||||||
(`DatLiveEntityProjectionMaterializer.cs:1003-1016`, per the route inventory).
|
|
||||||
Guard: only sets if record has no body yet (idempotent-create pattern) — see
|
|
||||||
full read below for exact guard.
|
|
||||||
2. **`RuntimeEntityObjectLifetime.cs:766`** — `Entities.SetPhysicsBody(canonical, null)`
|
|
||||||
inside a teardown method (need to confirm exact method — likely delete/retire
|
|
||||||
path, paired with `Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false)`
|
|
||||||
at line 767 in the SAME method). Clears body on deletion/teardown.
|
|
||||||
3. **`RuntimeLocalPlayerPhysicsPublicationState.cs:405`** — `candidate.Record.SetPhysicsBody(candidate.Body)`
|
|
||||||
inside `Commit(token, out activationToken)` (lines 373-411). **THIS IS THE
|
|
||||||
DORMANT OPTION-2 MECHANISM** — see section 4 below. Guarded by `IsCurrent(candidate)`
|
|
||||||
(epoch/session/identity/null-state re-check, lines 891-922) immediately before,
|
|
||||||
and by `_physics.SetPosition.PrepareDormantLocalActivationOwnership(...)` called
|
|
||||||
first (line 394) as the "seal the exact SetPosition owner before the
|
|
||||||
irreversible no-fail suffix" step — i.e. this call site DOES chain into
|
|
||||||
PrepareDormantLocalActivationOwnership per task 4's target.
|
|
||||||
4. **`RuntimeLocalPlayerPhysicsPublicationState.cs:1025`** — `_entities.SetPhysicsBody(activation.Record, null)`
|
|
||||||
inside `DiscardActivation()` (994-1032), the rollback/teardown path for the
|
|
||||||
SAME dormant mechanism — only fires if `_entities.IsCurrent(activation.Record)`
|
|
||||||
AND `ReferenceEquals(activation.Record.PhysicsBody, activation.Body)` (i.e.
|
|
||||||
never clobbers a body some OTHER newer owner already installed — the exact
|
|
||||||
anti-pattern the rejected prototype failed on).
|
|
||||||
5. **`RuntimePhysicsState.cs:1558`** — `Entities.SetPhysicsBody(record, candidateBody)`
|
|
||||||
— need full read; this is inside the remote/projectile body-binding family
|
|
||||||
(see section 3).
|
|
||||||
6. **`RuntimePhysicsState.cs:1666`** — `Entities.SetPhysicsBody(record, candidate)`
|
|
||||||
— need full read; this is the OTHER binding site, guarded by
|
|
||||||
`PhysicsBodyAcquisitionInProgress` (set true at :1645, cleared at :1676/1678).
|
|
||||||
|
|
||||||
**Writer count: 6 call sites, across 3 files** (`RuntimeEntityDirectory.cs` x1,
|
|
||||||
`RuntimeEntityObjectLifetime.cs` x1, `RuntimeLocalPlayerPhysicsPublicationState.cs` x2,
|
|
||||||
`RuntimePhysicsState.cs` x2).
|
|
||||||
|
|
||||||
## Consumers of `PhysicsOwnershipEpoch`
|
|
||||||
|
|
||||||
Only bumped in one place (`RuntimeEntityRecord.SetPhysicsBody`, above). Consumers
|
|
||||||
(all in `RuntimeLocalPlayerPhysicsPublicationState.cs`) treat it as a
|
|
||||||
compare-and-reject epoch stamped into every token/activation struct:
|
|
||||||
- `RuntimeLocalPlayerPhysicsPublicationToken.PhysicsOwnershipEpoch` (field, :53)
|
|
||||||
captured at `Prepare` time (:314).
|
|
||||||
- `RuntimeLocalPlayerPhysicsActivationToken.PhysicsOwnershipEpoch` (:70) captured
|
|
||||||
as `token.PhysicsOwnershipEpoch + 1UL` (:328) — i.e. the activation token
|
|
||||||
encodes "the epoch AFTER my own commit bumps it", so `IsActivationCurrent`
|
|
||||||
(931-962) and `IsActivationOwnershipEnvelopeCurrent` (717-745) comparing
|
|
||||||
`activation.Record.PhysicsOwnershipEpoch == activation.Token.PhysicsOwnershipEpoch`
|
|
||||||
will FAIL (reject) the instant any OTHER writer (remote/projectile bind, GC
|
|
||||||
clear, non-player static body creation via `RuntimeEntityDirectory` — none of
|
|
||||||
which should ever touch a local-player record, but the check is defense-in-depth)
|
|
||||||
touches the same record's PhysicsBody between prepare and commit.
|
|
||||||
- `IsCurrent(candidate)` (891-922, pre-Commit re-check) also compares
|
|
||||||
`candidate.Record.PhysicsOwnershipEpoch == candidate.Token.PhysicsOwnershipEpoch`
|
|
||||||
(unincremented — i.e. "nobody touched the body between Prepare and Commit").
|
|
||||||
|
|
||||||
**This IS the reentrancy defense the rejected prototype lacked** — see section 6.
|
|
||||||
|
|
||||||
## 2. The two production local-player controller constructions, end to end
|
|
||||||
|
|
||||||
### Graphical: `PlayerModeController.BuildControllerAndCamera`
|
|
||||||
`src/AcDream.App/Input/PlayerModeController.cs:244-525`. Constructor list
|
|
||||||
(52-74) shows it is injected with `RuntimeLocalPlayerMovementState controllerSlot`
|
|
||||||
(the SAME slot type headless writes) — confirms the route-inventory's "open
|
|
||||||
question" (2026-08-02-cutover-route-inventory.md:204-207): **App DOES write
|
|
||||||
`_controllerSlot.Controller = controller` directly, at line 486.** Not a
|
|
||||||
mystery/asymmetry — both hosts write the exact same public setter.
|
|
||||||
|
|
||||||
Steps, in order:
|
|
||||||
1. `_approachCompletions.BeginControllerLifetime()` (250) — App-only approach
|
|
||||||
lifecycle token.
|
|
||||||
2. Capture rollback snapshots: `_camera.CaptureState()` (255),
|
|
||||||
`_shadow.Capture()` (256) — presentation-only.
|
|
||||||
3. `new PlayerMovementController(_physics, playerRecord.ObjectClock, PlayerMovementConstructionOptions.From(_skills.Snapshot))`
|
|
||||||
(259-262) — **uses the PUBLIC constructor**, whose default publication
|
|
||||||
lifecycle is `StandalonePublished` (`PlayerMovementController.cs:617-626`),
|
|
||||||
NOT `CandidatePreparing`/`CreatePublicationCandidate`. This is the key
|
|
||||||
divergence from the dormant mechanism (section 4): this controller never
|
|
||||||
enters the `CandidatePreparing -> CandidateSealed -> RuntimeOwnedDormant ->
|
|
||||||
RuntimePublished` lifecycle at all.
|
|
||||||
4. Builds `MoveToManager`/`EntityPhysicsHost` closures over captured locals
|
|
||||||
(267-346) — presentation-adjacent glue, host-specific.
|
|
||||||
5. `EntityPhysicsHostComposition.SelectStableHostWithoutRebind` (347-350) —
|
|
||||||
canonical-state read (checks `LiveEntityRecord.PhysicsHost`).
|
|
||||||
6. `RuntimeMovementSkillProjection.ApplyTo(_skills, controller)` (366-368).
|
|
||||||
7. `ApplyStepHeights(controller, playerEntity, playerGuid)` (375) — **reads
|
|
||||||
`DatReaderWriter.DBObjs.Setup` directly** (per headless's own comment
|
|
||||||
contrasting itself, `HeadlessSessionWorldProjection.cs:685-689`) — NOT
|
|
||||||
through the prepared-collision/`IPreparedCollisionSource` seam headless
|
|
||||||
uses. Divergence #1.
|
|
||||||
8. `_controllerSlot.BeginMotionPreparation(controller, drainPriorAnimationQueue)`
|
|
||||||
(404-407) — the ONE existing narrow "preparation lease" concept already in
|
|
||||||
`RuntimeLocalPlayerMovementState` (separate from the dormant physics
|
|
||||||
publication state) that lets a synchronous PartArray/type-5 completion
|
|
||||||
reach the candidate `MotionInterpreter` before publish.
|
|
||||||
9. **Duplicate authority** — `_physics.Resolve(...)` (409-413) then
|
|
||||||
`_physics.ResolvePlacement(...)` (422-430) — direct canonical-state-free
|
|
||||||
collision resolve, entirely outside `RuntimeSetPositionState`.
|
|
||||||
10. `controller.PreparePositionForCommit(...)` (434-437),
|
|
||||||
`controller.SetBodyOrientation(...)` (438).
|
|
||||||
11. Camera construction + `_camera.EnterChaseMode(...)` (440-447) —
|
|
||||||
presentation-only, but happens BEFORE the final canonical commit (445-447
|
|
||||||
precede line 482-484) — i.e. camera activation today is NOT gated on a
|
|
||||||
Runtime placement acknowledgement.
|
|
||||||
12. Re-check host stability (449-458) — throws if the host changed during
|
|
||||||
camera activation (defensive, but ad hoc — not an epoch/token check, a
|
|
||||||
bespoke `ReferenceEquals` re-read).
|
|
||||||
13. Shadow sync (`_shadow.SyncPose(...)`, 460-466).
|
|
||||||
14. `EntityPhysicsHostComposition.InstallOrRebind(...)` (472-475) + another
|
|
||||||
`ReferenceEquals` stability re-check (476-480).
|
|
||||||
15. **Duplicate authority — final commit** (482-484):
|
|
||||||
`playerEntity.SetPosition(initial.Position); playerEntity.ParentCellId =
|
|
||||||
initial.CellId; controller.CommitPreparedPosition();` — direct writes to
|
|
||||||
the App-side `WorldEntity`/render sidecar AND `controller`'s own internal
|
|
||||||
frame, bypassing any Runtime `Place` receipt or `RuntimeEntityRecord`
|
|
||||||
write. **`RuntimeEntityRecord.PhysicsBody`/`PhysicsOwnershipEpoch` are
|
|
||||||
NEVER touched anywhere in this method** — `controller.PhysicsBody` (the
|
|
||||||
`_body` field created in step 3) stays a private field of the
|
|
||||||
`StandalonePublished` controller; nothing calls
|
|
||||||
`Entities.SetPhysicsBody(playerRecord, controller.PhysicsBody)`. This
|
|
||||||
means TODAY the canonical `RuntimeEntityRecord.PhysicsBody` slot for the
|
|
||||||
graphical local player is **never populated at all** by this path — a
|
|
||||||
previously-unstated confirmation that `SubmitPreparedPlacement`'s
|
|
||||||
`operation.Record.PhysicsBody is not { } body` requirement (section 3)
|
|
||||||
would REJECT any ordinary (non-initial) SetPosition submitted for the
|
|
||||||
graphical local player today, because no writer ever puts a body on that
|
|
||||||
record. (Route 2's "ForcePosition" duplicate authority,
|
|
||||||
`LocalForcePositionTransaction`, works around this by mutating
|
|
||||||
`PlayerMovementController`'s own body directly via `BlipPosition`, never
|
|
||||||
touching `RuntimeEntityRecord.PhysicsBody` either — internally consistent
|
|
||||||
with each other, both equally disconnected from the canonical record.)
|
|
||||||
16. Slot commits (485-492): `_hostSlot.Host`, `_controllerSlot.Controller =
|
|
||||||
controller` (the public, unguarded setter — bumps `ControllerOwnershipEpoch`
|
|
||||||
unconditionally, see section 4), `_chase.Legacy/Retail`, `_mode.IsPlayerMode
|
|
||||||
= true`.
|
|
||||||
17. `catch`: rolls back camera + shadow only (494-518); does NOT roll back
|
|
||||||
steps 15-16 because those are the LAST lines before `lifetimeCommitted =
|
|
||||||
true` — structurally "hope nothing after this throws" rather than an
|
|
||||||
explicit no-fail invariant.
|
|
||||||
|
|
||||||
**Canonical-state mutations in this method: NONE on `RuntimeEntityRecord`**
|
|
||||||
(no `SetPhysicsBody`, no `SetFullCell`, no object-clock call) — everything
|
|
||||||
mutated is App-local (`WorldEntity`, `PlayerMovementController`'s private
|
|
||||||
body, `RuntimeLocalPlayerMovementState.Controller`,
|
|
||||||
`LocalPlayerPhysicsHostSlot`, camera, shadow). The ONLY canonical-record
|
|
||||||
writes for the local player's initial placement happen earlier in the
|
|
||||||
hydration pipeline (`LiveEntityRuntime.MaterializeLiveEntity`/
|
|
||||||
`RebucketLiveEntity`, route 1 hops 9-11) — entirely disjoint from this method.
|
|
||||||
|
|
||||||
### Headless: `HeadlessSessionWorldProjection.CreateController` + `SynchronizeLocalPlayer`
|
|
||||||
`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:566-655`
|
|
||||||
(read in full).
|
|
||||||
|
|
||||||
`SynchronizeLocalPlayer` (566-615):
|
|
||||||
1. Guards on `record.ServerGuid == _runtime.PlayerIdentity.ServerGuid` and a
|
|
||||||
present `Snapshot.Position` (568-573).
|
|
||||||
2. `_collision.CenterOn(position.LandblockId)` (575) — headless collision-
|
|
||||||
neighborhood readiness, no graphical analog.
|
|
||||||
3. `_runtime.MovementOwner.Controller ?? CreateController(record)` (576-578)
|
|
||||||
— lazy-construct-once via the SAME public `Controller` getter/setter
|
|
||||||
`RuntimeLocalPlayerMovementState` exposes; no reentrancy guard against two
|
|
||||||
concurrent calls both observing `null` (single-threaded host loop makes
|
|
||||||
this safe in practice today, not structurally).
|
|
||||||
4. `_runtime.EntityObjects.Physics.Engine.Resolve(...)` (589-594) then
|
|
||||||
`.ResolvePlacement(...)` (595-605) — **the exact same duplicate-authority
|
|
||||||
shape as graphical step 9**, hardcoded `DefaultRadius`/`DefaultHeight`
|
|
||||||
constants (visible in the call, actual values not read here) instead of
|
|
||||||
`_motionBindings.GetSetupCylinder`.
|
|
||||||
5. `controller.SetPosition(...)` + `controller.SetBodyOrientation(...)`
|
|
||||||
(610-614) — **duplicate final commit**, headless's version of graphical
|
|
||||||
step 15. Also never touches `RuntimeEntityRecord.PhysicsBody`.
|
|
||||||
|
|
||||||
`CreateController` (639-655):
|
|
||||||
1. `new PlayerMovementController(_runtime.EntityObjects.Physics.Engine, record.ObjectClock, PlayerMovementConstructionOptions.From(_runtime.CharacterOwner.MovementSkills.Snapshot))`
|
|
||||||
(642-646) — **same PUBLIC constructor / `StandalonePublished` lifecycle**
|
|
||||||
as graphical step 3.
|
|
||||||
2. `ApplySetupStepHeights(record, controller)` (649, body at 657-691) —
|
|
||||||
**reads via `_preparedCollision.ReadSetupCollision(setupId)`** (668-679),
|
|
||||||
the prepared-asset seam, NOT raw DAT — divergence #1 mirrored (headless
|
|
||||||
uses the "correct"/prerequisite-B-aligned source; graphical does not).
|
|
||||||
**Throws `InvalidDataException`** if the read status isn't `Loaded`
|
|
||||||
(672-675) — propagates uncaught up through `SynchronizeLocalPlayer` ->
|
|
||||||
`ProjectSpawn`/`ProjectPosition` -> the wire-dispatch call chain. This IS
|
|
||||||
gate 10 (late headless prepared-collision failure) manifesting today as an
|
|
||||||
unhandled exception, not a retry.
|
|
||||||
3. `RuntimeMovementSkillProjection.ApplyTo(_runtime.CharacterOwner.MovementSkills, controller)`
|
|
||||||
(650-652).
|
|
||||||
4. `_runtime.MovementOwner.Controller = controller;` (653) — **the exact
|
|
||||||
same public setter graphical step 16 uses.**
|
|
||||||
|
|
||||||
**No headless equivalent of graphical steps 1 (approach lifetime), 8 (motion
|
|
||||||
preparation lease), 11-14 (camera + host-stability re-checks), 17 (camera/
|
|
||||||
shadow rollback)** — headless has no camera/shadow/approach concept at all
|
|
||||||
(confirmed, matches the route inventory's "No headless equivalent of
|
|
||||||
graphical hops 12-14/19").
|
|
||||||
|
|
||||||
### Top divergences between the two hosts (summary)
|
|
||||||
1. **Setup/collision data source**: App reads raw DAT (`ApplyStepHeights` via
|
|
||||||
`_dats`/`_datLock`); headless reads the prepared/baked asset
|
|
||||||
(`ApplySetupStepHeights` via `IPreparedCollisionSource`). Same target
|
|
||||||
values, different pipeline — a real fidelity risk if the two ever diverge
|
|
||||||
(baking staleness).
|
|
||||||
2. **Default cylinder fallback**: App falls back to `0.48f`/`1.835f` inline
|
|
||||||
(`PlayerModeController.cs:416-420`) when `GetSetupCylinder` returns
|
|
||||||
`< 0.05f` radius; headless uses named `DefaultRadius`/`DefaultHeight`
|
|
||||||
constants at the `ResolvePlacement` call site (:599-600) — same intended
|
|
||||||
values, defined in two places.
|
|
||||||
3. **Failure handling**: App's `BuildControllerAndCamera` has an explicit
|
|
||||||
try/catch/rollback for camera+shadow; headless's `CreateController`/
|
|
||||||
`ApplySetupStepHeights` has NO surrounding try/catch — a prepared-collision
|
|
||||||
read failure is a raw unhandled exception today.
|
|
||||||
4. **Presentation surface**: App additionally owns approach-completion
|
|
||||||
lifetime, motion-preparation lease, chase camera, shadow sync — none of
|
|
||||||
which headless has or needs.
|
|
||||||
5. **Neither host touches `RuntimeEntityRecord.PhysicsBody`, `PhysicsOwnershipEpoch`,
|
|
||||||
or any `RuntimeSetPositionState` API** — both are 100% off to the side of
|
|
||||||
the canonical record, confirmed by exhaustive grep (section 1's 6 writer
|
|
||||||
call sites do not include either `PlayerModeController.cs` or
|
|
||||||
`HeadlessSessionWorldProjection.cs`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Every other body binding/consumer
|
|
||||||
|
|
||||||
- **Remote dead-reckoning** (`RuntimeRemotePhysicsUpdater.cs` — flagged
|
|
||||||
protected/dirty, read-only, NOT modified): grep confirms it only READS
|
|
||||||
`record.PhysicsBody` via `ReferenceEquals(record.PhysicsBody, remote.Body)`
|
|
||||||
currency checks (line 817) — it does not call `SetPhysicsBody`. The actual
|
|
||||||
writer for remote motion is `RuntimePhysicsState.SetRemoteMotion`
|
|
||||||
(`RuntimePhysicsState.cs:1446-1570`, full read) — throws
|
|
||||||
`InvalidOperationException` on: binding-already-in-progress (1460-1464),
|
|
||||||
body-would-be-replaced when a body already exists and doesn't match
|
|
||||||
(1487-1492), losing an existing remote-placement contract (1493-1498), or
|
|
||||||
post-callback ownership drift detected via a captured
|
|
||||||
`sessionVersion`/`expectedBody`/`expectedRuntime` triple re-checked after
|
|
||||||
the bind callback (1539-1549, "changed ownership during remote-motion
|
|
||||||
binding"). Calls `Entities.SetPhysicsBody(record, candidateBody)` (:1558)
|
|
||||||
ONLY when `expectedBody is null` (first bind) via `InitializeNewPhysicsBody`
|
|
||||||
(:1556) — i.e. this is throw-on-conflict exclusivity (Option-1 flavor), not
|
|
||||||
epoch/token gating. For a LOCAL PLAYER record this path should never fire
|
|
||||||
(remote motion is for non-local entities) but the guard is defense-in-depth
|
|
||||||
and IS one of the explicit gate checks
|
|
||||||
(`!activation.Record.RemoteMotionBindingInProgress`/`RemoteMotion is null`)
|
|
||||||
the dormant local-publication mechanism re-validates at every stage
|
|
||||||
(section 4).
|
|
||||||
- **Projectile binding**: `RuntimeProjectilePhysicsUpdater.cs` similarly only
|
|
||||||
READS `record.PhysicsBody` (lines 447, 459, `ReferenceEquals` currency
|
|
||||||
checks). The writer is `RuntimePhysicsState.BindProjectile`
|
|
||||||
(`RuntimePhysicsState.cs:1309-1382`, full read) — same throw-on-conflict
|
|
||||||
shape: binding-in-progress (1343-1347), body-mismatch on rebind
|
|
||||||
(1330-1337), must-already-own-canonical-body-before-binding
|
|
||||||
(1348-1352, "projectile must borrow its canonical physics body" — i.e.
|
|
||||||
UNLIKE remote motion, `BindProjectile` requires `record.PhysicsBody` to
|
|
||||||
ALREADY be non-null and matching BEFORE it will bind — it never calls
|
|
||||||
`InitializeNewPhysicsBody`/`SetPhysicsBody` itself for a first-time body;
|
|
||||||
something else (route 5's `ProjectileController.TryBind`,
|
|
||||||
`ProjectileController.cs:176-265` per the route inventory) must construct
|
|
||||||
the body ad hoc first via a DIFFERENT path than `GetOrCreatePhysicsBody`
|
|
||||||
— worth flagging: **this is a 7th, App-side, ad hoc body-construction site
|
|
||||||
not funneled through any of the 6 canonical writer methods** — App's
|
|
||||||
`ProjectileController.TryBind` constructs a body and must be setting it onto
|
|
||||||
the record through some other route (not confirmed by this pass; App-side
|
|
||||||
`ProjectileController.cs` was not read in full — flag as open item, but it
|
|
||||||
is explicitly OUT of C1's local-player scope per the campaign handoff's
|
|
||||||
gate list item "remote and projectile binding/update" being about
|
|
||||||
*interaction with* the local-player transaction, not projectile's own
|
|
||||||
authority).
|
|
||||||
- **`RuntimeSetPositionState.SubmitPreparedPlacement`** (`RuntimeSetPositionState.cs:2224-2274`,
|
|
||||||
full read): requires `operation.Record.PhysicsBody is not { } body` to
|
|
||||||
already be true (line 2254) — i.e. EVERY non-initial-construction
|
|
||||||
SetPosition submission (ForcePosition, portal, remote Position, projectile
|
|
||||||
correction) requires a body to already exist on the record, confirming the
|
|
||||||
6 writer sites in section 1 are the exhaustive set of "who can put the
|
|
||||||
FIRST body on a record." For the local player specifically, only
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState.Commit` (section 4) does this
|
|
||||||
today (dormant, unwired); in PRODUCTION, no writer ever populates
|
|
||||||
`RuntimeEntityRecord.PhysicsBody` for either host's local player (section
|
|
||||||
2 finding) — meaning `SubmitPreparedPlacement` would reject a local-player
|
|
||||||
submission in production today, which is consistent with the route
|
|
||||||
inventory's finding that Route 2 (ForcePosition) and Route 3 (portal) both
|
|
||||||
bypass `RuntimeSetPositionState` entirely via their own duplicate
|
|
||||||
authorities instead.
|
|
||||||
- **`RuntimeSetPositionState.PrepareDormantLocalActivationOwnership`**: see
|
|
||||||
section 4 — the ONE place the local-player-specific dormant body attach
|
|
||||||
happens; requires a pre-opened `Operation` in stage `AwaitingPreparation`
|
|
||||||
from `TryBeginExclusiveAuthoredPlacement`.
|
|
||||||
- **Object-clock epoch transitions**
|
|
||||||
(`RuntimeEntityRecord.SuspendObjectClock`/`ResetObjectClockForEnterWorld`,
|
|
||||||
both `internal`, bump `ObjectClockEpoch`): full call-site grep found BOTH
|
|
||||||
the expected `RuntimeEntityDirectory` wrapper call sites (which run
|
|
||||||
`EnsureKnown(record)` first, `RuntimeEntityDirectory.cs:311-321`) AND
|
|
||||||
**direct unwrapped calls from `src/AcDream.App/World/LiveEntityRuntime.cs`
|
|
||||||
at lines 879, 891, 897, 1258, 3030, 3033** — App calls
|
|
||||||
`record.SuspendObjectClock()`/`record.ResetObjectClockForEnterWorld(...)`
|
|
||||||
straight on the `RuntimeEntityRecord` (accessible because these are
|
|
||||||
`internal` and `AcDream.App` has `InternalsVisibleTo`), bypassing the
|
|
||||||
`RuntimeEntityDirectory` facade's `EnsureKnown` check entirely. This is
|
|
||||||
inside `LiveEntityRuntime`'s `RebucketLiveEntity`-family code (comment
|
|
||||||
references `prepare_to_enter_world`/retail `update_object`'s parent
|
|
||||||
early-out — matches the already-known route-1/prerequisite-D
|
|
||||||
`RebucketLiveEntity` duplicate authority). **Previously-unstated
|
|
||||||
implication for C1**: the SAME record whose `ObjectClockEpoch` the dormant
|
|
||||||
publication mechanism gates on can have its epoch bumped by this
|
|
||||||
direct-call path DURING the window between
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState.Prepare` and `.Commit()`/
|
|
||||||
`.CommitActivation()` if `RebucketLiveEntity` runs concurrently for the
|
|
||||||
SAME entity (e.g. a second CreateObject/Position causing a re-rebucket
|
|
||||||
mid-construction) — the epoch check (`IsCurrent`/`IsActivationOwnershipEnvelopeCurrent`
|
|
||||||
comparing `record.ObjectClockEpoch == token.ObjectClockEpoch`) WOULD catch
|
|
||||||
and reject this correctly (fail-safe), but it confirms the gate is load-
|
|
||||||
bearing against a REAL, already-existing production writer, not a
|
|
||||||
hypothetical.
|
|
||||||
- **Deletion/teardown**: `RuntimeEntityObjectLifetime.TryAcceptDelete`
|
|
||||||
(`RuntimeEntityObjectLifetime.cs:1555+`) calls `Entities.TryDelete` then
|
|
||||||
`Entities.RemoveActive(active)` (1587) — this IMMEDIATELY flips
|
|
||||||
`Entities.IsCurrent(record)` to `false` for that record (removes it from
|
|
||||||
the active-by-guid table), which is the single check
|
|
||||||
`CanPrepare`/`IsCurrent`/`IsActivationCurrent`/every gate in section 4
|
|
||||||
depends on — so a delete landing at any point rejects the in-flight
|
|
||||||
publication transaction on its NEXT check. Full body clear happens later
|
|
||||||
in `RuntimeEntityObjectLifetime.CompleteProjectionRetirement`
|
|
||||||
(:745-771, called from `RetireCanonicalOnly`/the graphical teardown-ack
|
|
||||||
path): `Entities.SetPhysicsBody(canonical, null)` (:766) after
|
|
||||||
`Physics.SetPosition.Forget(canonical, releasePreparedMover: true)` (:753,
|
|
||||||
cancels any in-flight ordinary placement) and
|
|
||||||
`ForgetInitialCreateResidence(canonical)` (:751, cancels any in-flight
|
|
||||||
residence lease) — i.e. deletion cancels BOTH placement-lease families
|
|
||||||
before clearing the body, consistent with prerequisite E's "quiesce before
|
|
||||||
demote" discipline (though for landblock collision, not entity teardown —
|
|
||||||
the pattern rhymes).
|
|
||||||
- **`RuntimePhysicsState` per-frame body access**: `RuntimeOrdinaryPhysicsUpdater.cs`,
|
|
||||||
`RuntimeRemotePhysicsUpdater.cs`, `RuntimeProjectilePhysicsUpdater.cs` each
|
|
||||||
gate their per-tick work on `record.PhysicsBody is not { } body` /
|
|
||||||
`ReferenceEquals(record.PhysicsBody, body)` currency checks (grep-confirmed,
|
|
||||||
e.g. `RuntimeOrdinaryPhysicsUpdater.cs:68,284`) — read-only w.r.t. the
|
|
||||||
`PhysicsBody` reference itself (they mutate the BODY's internal fields
|
|
||||||
every tick, which is expected/normal simulation, not an ownership-slot
|
|
||||||
write). No workset iterates and calls `SetPhysicsBody`. `RuntimePhysicsState.cs`
|
|
||||||
itself has only two visible "workset" mentions (`ClearSpatialWorksets` at
|
|
||||||
:1951, a doc-comment at :1307) — the ordinary/remote/projectile worksets
|
|
||||||
live in their respective `RuntimeXPhysicsUpdater` files, out of this pass's
|
|
||||||
read budget beyond the grep-confirmed read-only currency pattern above.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. `RuntimeSetPositionState.PrepareDormantLocalActivationOwnership` — nucleus or dead end?
|
|
||||||
|
|
||||||
**Definition** (`RuntimeSetPositionState.cs:1026-1052`, full read):
|
|
||||||
```csharp
|
|
||||||
internal void PrepareDormantLocalActivationOwnership(
|
|
||||||
RuntimeEntityRecord record, PhysicsBody body,
|
|
||||||
in RuntimeEntityPlacementToken token)
|
|
||||||
{
|
|
||||||
...
|
|
||||||
if (!token.IsValid
|
|
||||||
|| record.Key != token.Entity
|
|
||||||
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|
|
||||||
|| operation.Token != token
|
|
||||||
|| operation.Stage is not RuntimeEntityPlacementStage.AwaitingPreparation
|
|
||||||
|| !ReferenceEquals(operation.Record, record)
|
|
||||||
|| record.PhysicsBody is not null // <- record must have NO body yet
|
|
||||||
|| !IsCurrent(operation)
|
|
||||||
|| body.InWorld
|
|
||||||
|| (body.TransientState & TransientStateFlags.Active) != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Dormant local activation must bind to the exact current placement owner.");
|
|
||||||
}
|
|
||||||
operation.Body = body;
|
|
||||||
operation.DormantLocalActivation = true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
It THROWS (does not return a status) on any invariant violation — by design a
|
|
||||||
"this should be structurally impossible if the caller validated first"
|
|
||||||
assertion, not a retryable rejection. It requires a PRE-EXISTING placement
|
|
||||||
`Operation` already opened via `TryBeginExclusiveAuthoredPlacement`
|
|
||||||
(`RuntimeSetPositionState.cs:1004-1024`) in stage `AwaitingPreparation` — i.e.
|
|
||||||
it is NOT a standalone entry point; it is ONE STEP inside a larger chain that
|
|
||||||
also needs prerequisite B's mover-preparation authority
|
|
||||||
(`IsExactPreparedPlacementCurrent`, `RuntimeSetPositionState.cs:1318-1340`)
|
|
||||||
satisfied for the SAME token/command before
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState.CanPrepare` will even call it.
|
|
||||||
|
|
||||||
**What it was built for**: it is called from exactly ONE place in the whole
|
|
||||||
repo — `RuntimeLocalPlayerPhysicsPublicationState.Commit`
|
|
||||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:394-397`), as the "seal the
|
|
||||||
exact SetPosition owner before the irreversible no-fail suffix" step,
|
|
||||||
immediately before `candidate.Controller.CommitRuntimeOwnership(...)` and
|
|
||||||
`candidate.Record.SetPhysicsBody(candidate.Body)`. It exists purely to make
|
|
||||||
the PLACEMENT OPERATION (owned by `RuntimeSetPositionState`) and the BODY
|
|
||||||
(owned by `RuntimeEntityRecord`) become mutually aware atomically, so that
|
|
||||||
the SAME operation can later be walked through the full retail SetPosition
|
|
||||||
staged commit (ground phase -> collision dispatch -> response -> final
|
|
||||||
commit) via `TryEvaluateDormantLocalActivation` ->
|
|
||||||
`TryPrepareDormantLocalActivationCommit` ->
|
|
||||||
`TryApplyDormantLocalActivationCommit` ->
|
|
||||||
`TryPrepareDormantLocalActivationFinalCommit` ->
|
|
||||||
`TryApplyDormantLocalActivationFinalCommit`
|
|
||||||
(`RuntimeSetPositionState.cs:2025-2077`, full read of the final-commit
|
|
||||||
method) — the LAST of which is where `_entities.SetFullCell`,
|
|
||||||
`_entities.AdvancePlacementCommit`, `body.InWorld = true`,
|
|
||||||
`_entities.SetPhysicsHost`, `controller.CommitRuntimeActivationFrame()`,
|
|
||||||
`_physics.Engine.UpdatePlayerCurrCell`, `_physics.AcknowledgeSpatialProjection`,
|
|
||||||
`_entities.ResetObjectClockForEnterWorld` (object-clock epoch bump, task 3),
|
|
||||||
and `controller.ActivateRuntimePublication()` (controller goes LIVE) ALL
|
|
||||||
happen in one synchronous, no-branch-for-failure block (:2025-2077), gated
|
|
||||||
immediately before by `IsDormantLocalActivationPrephaseCurrent`/re-validated
|
|
||||||
epoch checks. **This is genuinely the full retail SetPosition commit,
|
|
||||||
already ported, already wired to the same body/controller the dormant
|
|
||||||
publication candidate built.**
|
|
||||||
|
|
||||||
**Verdict: NUCLEUS, not a dead end** — but it is only ONE LOAD-BEARING STEP
|
|
||||||
inside a much larger, ALREADY-COMPLETE mechanism:
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState` (1033 lines,
|
|
||||||
`src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs`)
|
|
||||||
+ its ~15 `RuntimeSetPositionState` dormant-activation methods. Constructed
|
|
||||||
once at `GameRuntime.cs:261` and exposed via
|
|
||||||
`RuntimeLocalPlayerMovementState.PhysicsPublication` (:59-61, itself
|
|
||||||
`internal`, throws if unbound). **Confirmed by exhaustive grep: ZERO
|
|
||||||
production callers in `src/AcDream.App/` or `src/AcDream.Headless/`** — the
|
|
||||||
only callers anywhere are `tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs`.
|
|
||||||
This is, functionally, **Option 2 from the rejected-prototype note ("Off-
|
|
||||||
canonical preparation followed by one validated atomic Runtime commit that
|
|
||||||
publishes the prepared controller/body relationship without copying stale
|
|
||||||
state over newer authority") already built end to end** — complete with:
|
|
||||||
- a `Prepare`/`Commit`/`Discard` triad for the BODY/CONTROLLER pair
|
|
||||||
(analogous to, and reusing, the SAME token-epoch pattern as
|
|
||||||
`RuntimeSetPositionState`'s ordinary placement operations);
|
|
||||||
- a SEPARATE `EvaluateActivation`/`CommitActivation`/`DiscardActivation` triad
|
|
||||||
for actually driving the body through retail SetPosition's staged commit
|
|
||||||
once the body/controller pair is sealed;
|
|
||||||
- re-validation of `PhysicsOwnershipEpoch`, `ObjectClockEpoch`,
|
|
||||||
`ControllerOwnershipEpoch`, `SessionLifetimeVersion`, identity
|
|
||||||
`ServerGuid`+`Revision`, and null/in-progress state for RemoteMotion/
|
|
||||||
Projectile/PhysicsHost/DeleteAcceptedForTeardown at EVERY external entry
|
|
||||||
point (`CanPrepare`, `IsCurrent`, `IsActivationCurrent`,
|
|
||||||
`IsActivationOwnershipEnvelopeCurrent`,
|
|
||||||
`IsCommittedActivationSuffixCurrent`) — this IS the reentrancy defense the
|
|
||||||
rejected snapshot-lease prototype explicitly lacked (see section 6).
|
|
||||||
|
|
||||||
**What is genuinely missing (the real C1 work), given this mechanism already
|
|
||||||
exists:**
|
|
||||||
1. Nobody calls `TryBeginExclusiveAuthoredPlacement` + prerequisite B's
|
|
||||||
`PrepareMover`/`ReadSetupCollision` chain + `PhysicsPublication.Prepare`/
|
|
||||||
`Commit`/`EvaluateActivation`/`CommitActivation` from either host — this IS
|
|
||||||
the wiring gap, exactly like every other route in the cutover.
|
|
||||||
2. **The public `RuntimeLocalPlayerMovementState.Controller` setter
|
|
||||||
(`RuntimeLocalPlayerMovementState.cs:37-50`) remains a live, unguarded
|
|
||||||
escape hatch** — both `PlayerModeController.BuildControllerAndCamera:486`
|
|
||||||
and `HeadlessSessionWorldProjection.CreateController:653` write it
|
|
||||||
directly today, and NOTHING stops either host from continuing to do so
|
|
||||||
even after C1 wires the dormant mechanism, unless that direct-write path
|
|
||||||
is deleted/sealed off (e.g. made `internal` to only
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState`/`CommitRuntimeOwnedController`).
|
|
||||||
The setter has NO epoch/token check on write (`CanCommitRuntimeOwnedController`
|
|
||||||
is a SEPARATE, unused-by-the-setter validation method) — it will happily
|
|
||||||
accept a second unguarded assignment even while a dormant activation is
|
|
||||||
in flight, silently retiring whatever the dormant mechanism just
|
|
||||||
published (`_controller?.RetireRuntimePublication()` at :45, which is a
|
|
||||||
real no-op for anything not currently `RuntimeOwnedDormant`/
|
|
||||||
`RuntimePublished` — see section 6 gate 7). **This is the single most
|
|
||||||
important pre-existing defect C1 must close: the "exclusive" adjective in
|
|
||||||
prerequisite C's "one Runtime-owned exclusive/versioned...transaction"
|
|
||||||
is not yet true while this direct setter remains reachable from hosts.**
|
|
||||||
3. Neither `new PlayerMovementController(physics, objectClock, options)`
|
|
||||||
(public ctor, `StandalonePublished`) call site in the two hosts has been
|
|
||||||
swapped for `PlayerMovementController.CreatePublicationCandidate` — until
|
|
||||||
that swap happens, controllers built by either host never enter the
|
|
||||||
`CandidatePreparing/CandidateSealed/RuntimeOwnedDormant/RuntimePublished`
|
|
||||||
lifecycle the dormant mechanism's gates all key off of.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. `PlayerMovementController` construction requirements
|
|
||||||
|
|
||||||
Constructor needs (from both direct-ctor call sites AND
|
|
||||||
`CreatePublicationCandidate`, `PlayerMovementController.cs:617-690`):
|
|
||||||
- `PhysicsEngine physics` (shared engine reference, both hosts pass their own
|
|
||||||
`RuntimePhysicsState`/`_runtime.EntityObjects.Physics.Engine`).
|
|
||||||
- `RetailObjectQuantumClock? objectClock` — App passes `playerRecord.ObjectClock`
|
|
||||||
(the CANONICAL record's clock, `RuntimeEntityRecord.ObjectClock` at
|
|
||||||
`RuntimeEntityRecord.cs:62`, always non-null per its field initializer);
|
|
||||||
headless passes `record.ObjectClock` identically. The dormant mechanism's
|
|
||||||
`CreatePublicationCandidate` instead passes a THROWAWAY
|
|
||||||
`new RetailObjectQuantumClock()` (`PlayerMovementController.cs:687-688`) at
|
|
||||||
construction time and only swaps in the REAL
|
|
||||||
`candidate.Record.ObjectClock` later, inside `Commit`, via
|
|
||||||
`controller.CommitRuntimeOwnership(candidate.Record.ObjectClock)`
|
|
||||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:403-404` ->
|
|
||||||
`PlayerMovementController.cs:774-786`) — i.e. the dormant candidate is
|
|
||||||
built against a SCRATCH clock so construction can never observe or mutate
|
|
||||||
the canonical record's real clock before the atomic commit swaps it in.
|
|
||||||
This is exactly the "off-canonical preparation" half of Option 2.
|
|
||||||
- `PlayerMovementConstructionOptions` (RunSkill/JumpSkill) — both hosts build
|
|
||||||
via `PlayerMovementConstructionOptions.From(<a RuntimeMovementSkillState
|
|
||||||
snapshot>)`; the dormant mechanism's `Prepare` also takes this as a caller-
|
|
||||||
supplied parameter (`Prepare(..., PlayerMovementConstructionOptions
|
|
||||||
options, ...)`, :191) — no divergence in shape, only in WHERE the skill
|
|
||||||
snapshot is read from (App's local `_skills` field vs. headless's
|
|
||||||
`_runtime.CharacterOwner.MovementSkills` vs. the dormant mechanism taking
|
|
||||||
it as a caller parameter either way).
|
|
||||||
|
|
||||||
What construction MUTATES (beyond the private `_body`): `LocalEntityId`,
|
|
||||||
`StepUpHeight`/`StepDownHeight` (Setup-derived), `SphereList` (Setup-derived,
|
|
||||||
prerequisite B territory), `ObjectScale`, initial position/orientation via
|
|
||||||
`PreparePositionForCommit`/`SetBodyOrientation`, physics state via
|
|
||||||
`ApplyPhysicsState`, `MoveToFactory`/`PositionManager`
|
|
||||||
(`MovementManager`/`MotionInterpreter` wiring). ALL of this is exactly what
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState.Prepare`
|
|
||||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:187-355`) already does against
|
|
||||||
its private `CreatePublicationCandidate`-built controller, reading
|
|
||||||
`command.Physics.StepUpHeight/StepDownHeight/Spheres/Scale/Position/CellId/
|
|
||||||
CellLocalPosition/Orientation` from the CALLER-SUPPLIED
|
|
||||||
`RuntimeSetPositionCommand` (i.e. the command already carries everything
|
|
||||||
prerequisite B's mover-preparation chain produces) rather than reaching into
|
|
||||||
DAT/prepared-collision itself.
|
|
||||||
|
|
||||||
**What an "off-canonical preparation followed by one validated atomic commit"
|
|
||||||
must DEFER** (confirmed by the dormant mechanism's own design, section 4):
|
|
||||||
- The record's REAL `ObjectClock` (use a scratch clock during prep).
|
|
||||||
- `RuntimeEntityRecord.PhysicsBody`/`PhysicsOwnershipEpoch` (never touch the
|
|
||||||
canonical record during prep; only `SetPhysicsBody` inside `Commit`, and
|
|
||||||
only after `PrepareDormantLocalActivationOwnership` succeeds).
|
|
||||||
- `RuntimeLocalPlayerMovementState.Controller`/`ControllerOwnershipEpoch`
|
|
||||||
(only via `CommitRuntimeOwnedController`, never the public setter, during
|
|
||||||
prep).
|
|
||||||
- `body.InWorld`/`TransientState.Active` (explicitly forced false during prep,
|
|
||||||
`Prepare`, :292-293) — the body must not be simulatable until the LATER
|
|
||||||
activation commit flips it (`TryApplyDormantLocalActivationFinalCommit`,
|
|
||||||
`body.InWorld = true` at :2056).
|
|
||||||
- World-residence/host/shadow/camera publication (all deferred to the
|
|
||||||
activation phase / presentation-observer layer, never inside `Prepare`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Adversarial gate list — exact code paths that would race TODAY
|
|
||||||
|
|
||||||
(Campaign handoff's list, `docs/research/2026-07-31-remaining-physics-campaign-handoff.md:210-221`.)
|
|
||||||
For each: what the ALREADY-BUILT dormant mechanism does (if wired) vs. what
|
|
||||||
the CURRENT production direct-construction paths do (today, unwired).
|
|
||||||
|
|
||||||
1. **Nested construction** — Dormant: `CanPrepare` requires `_activation is
|
|
||||||
null` AND `record.PhysicsBody is null` AND `_movement.Controller is null`
|
|
||||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:862-889`) — a second
|
|
||||||
`Prepare` while one is in flight is REJECTED structurally. Today: NEITHER
|
|
||||||
`BuildControllerAndCamera` NOR `CreateController` has any such guard —
|
|
||||||
`CreateController`'s `_runtime.MovementOwner.Controller ?? CreateController(record)`
|
|
||||||
(`HeadlessSessionWorldProjection.cs:576-578`) is a bare null-coalesce, not
|
|
||||||
an atomic test-and-set; only single-threaded host-loop scheduling
|
|
||||||
prevents an actual race today.
|
|
||||||
2. **Reentrant SetPosition** — Dormant: every gate re-checks
|
|
||||||
`PhysicsOwnershipEpoch`/`record.PositionAuthorityVersion` currency.
|
|
||||||
Today: `BuildControllerAndCamera`'s final mutation
|
|
||||||
(`playerEntity.SetPosition`/`ParentCellId`/`CommitPreparedPosition`,
|
|
||||||
PlayerModeController.cs:482-484) has zero epoch check — a same-thread
|
|
||||||
reentrant call (e.g. from a nested wire dispatch) would silently
|
|
||||||
clobber with no detection.
|
|
||||||
3. **Remote and projectile binding/update** — Dormant: `CanPrepare`/
|
|
||||||
`IsCurrent`/`IsActivationCurrent` all check `record.RemoteMotion is null`,
|
|
||||||
`record.Projectile is null`, `!RemoteMotionBindingInProgress`,
|
|
||||||
`!ProjectileBindingInProgress` (defense-in-depth; should never legitimately
|
|
||||||
fire for a local-player record). Today: no such check exists in either
|
|
||||||
host's direct construction path.
|
|
||||||
4. **Deletion and same-GUID new incarnation** — Dormant: `_entities.IsCurrent(record)`
|
|
||||||
checked at every gate; `TryAcceptDelete` -> `RemoveActive` flips this
|
|
||||||
immediately (section 3). Today: `BuildControllerAndCamera`/`CreateController`
|
|
||||||
take a fixed `RuntimeEntityRecord`/`WorldEntity` parameter with NO
|
|
||||||
re-validation against current canonical identity at the final commit.
|
|
||||||
5. **Projection-owner replacement** — Dormant: `_entities.SessionLifetimeVersion
|
|
||||||
== token.SessionGenerationAuthority` checked throughout. Today: no
|
|
||||||
generation check in either direct path.
|
|
||||||
6. **Object-clock epoch change** — Dormant: `ObjectClockEpoch` compared at
|
|
||||||
every gate (section 3/4). Today: no check; AND there is a REAL, live
|
|
||||||
concurrent writer already in production —
|
|
||||||
`LiveEntityRuntime.cs:879/891/897/1258/3030/3033`'s direct
|
|
||||||
`record.SuspendObjectClock()`/`ResetObjectClockForEnterWorld(...)` calls
|
|
||||||
inside the `RebucketLiveEntity` family (section 3) — this is not a
|
|
||||||
hypothetical gate, it is a currently-active call path on the SAME record
|
|
||||||
type.
|
|
||||||
7. **Reset and disposal** — Dormant: `ResetSession()`/`Dispose()` on
|
|
||||||
`RuntimeLocalPlayerMovementState` explicitly cascade into
|
|
||||||
`_physicsPublication?.ResetSession()`/`Dispose()`
|
|
||||||
(`RuntimeLocalPlayerMovementState.cs:244-295`), which tear down
|
|
||||||
candidate/activation state via `ReferenceEquals`-gated clears (never
|
|
||||||
clobbering a newer owner, `DiscardActivation`,
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState.cs:1002-1032`). Today's direct-
|
|
||||||
construction controllers are built via the PUBLIC constructor
|
|
||||||
(`StandalonePublished` lifecycle) — **`RetireRuntimePublication()`
|
|
||||||
(`PlayerMovementController.cs:837-846`) only transitions
|
|
||||||
`RuntimeOwnedDormant`/`RuntimePublished` state; it is a NO-OP for
|
|
||||||
`StandalonePublished` controllers** — a previously-unstated finding: TODAY,
|
|
||||||
`ResetSession()`/`Dispose()`/replacing `.Controller` on either host's
|
|
||||||
directly-built controller produces NO explicit lifecycle transition at
|
|
||||||
all; the controller is simply dropped/GC'd. Not a visible bug today
|
|
||||||
(nothing reads `IsRuntimePublished` for these), but it means today's
|
|
||||||
controllers are invisible to the exact teardown bookkeeping C1's target
|
|
||||||
mechanism relies on.
|
|
||||||
8. **Commit and rollback after replacement** — Dormant: ALL validation
|
|
||||||
happens before the single canonical mutation
|
|
||||||
(`PrepareDormantLocalActivationOwnership`, which itself throws leaving
|
|
||||||
state untouched on failure); everything after is documented as
|
|
||||||
"callback-free, non-allocating, and cannot fail"
|
|
||||||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:391-393`) — no rollback-
|
|
||||||
after-newer-authority path exists BECAUSE nothing after that point can
|
|
||||||
fail by construction. Today: `BuildControllerAndCamera`'s try/catch rolls
|
|
||||||
back camera+shadow only; the final `playerEntity.SetPosition`/
|
|
||||||
`ParentCellId`/`CommitPreparedPosition` triad (482-484) has nothing after
|
|
||||||
it that can throw, so it's accidentally safe today, not structurally
|
|
||||||
guaranteed.
|
|
||||||
9. **Late graphical camera/shadow/host failure** — Today: `BuildControllerAndCamera`
|
|
||||||
DOES handle this (explicit `_camera.RestoreState`/`_shadow.Restore` in the
|
|
||||||
catch block, 494-518) — this is the ONE gate the CURRENT graphical path
|
|
||||||
already handles reasonably. The dormant Runtime-side mechanism has NO
|
|
||||||
camera/shadow concept (presentation-independent by design) — C1 must
|
|
||||||
layer this handling in the PRESENTATION/observer phase (post-Runtime-
|
|
||||||
commit), matching prerequisite D's rule that a host exception must not
|
|
||||||
roll Runtime back, only retry the FIFO head.
|
|
||||||
10. **Late headless prepared-collision failure** — Today:
|
|
||||||
`ApplySetupStepHeights` (`HeadlessSessionWorldProjection.cs:657-691`)
|
|
||||||
throws a raw, uncaught `InvalidDataException` (672-675) if the prepared
|
|
||||||
Setup collision isn't `Loaded` — this propagates up through
|
|
||||||
`CreateController` -> `SynchronizeLocalPlayer` -> `ProjectSpawn`/
|
|
||||||
`ProjectPosition` with NO try/catch anywhere in between (grep-confirmed
|
|
||||||
no surrounding try/catch in `HeadlessSessionWorldProjection.cs`'s these
|
|
||||||
methods) — a genuinely unhandled-exception risk in production headless
|
|
||||||
TODAY, not just a hypothetical C1 gate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary for the C1 contract
|
|
||||||
|
|
||||||
- **Writer count**: 6 confirmed call sites of `RuntimeEntityRecord.SetPhysicsBody`
|
|
||||||
across 3 files (`RuntimeEntityDirectory.cs:359`,
|
|
||||||
`RuntimeEntityObjectLifetime.cs:766`,
|
|
||||||
`RuntimeLocalPlayerPhysicsPublicationState.cs:405,1025`,
|
|
||||||
`RuntimePhysicsState.cs:1558,1666`) — plus a probable 7th App-side ad hoc
|
|
||||||
projectile body-construction site not yet traced to a canonical writer
|
|
||||||
(flagged, out of local-player scope).
|
|
||||||
- The dormant `RuntimeLocalPlayerPhysicsPublicationState` +
|
|
||||||
`RuntimeSetPositionState`'s ~15 dormant-activation methods already
|
|
||||||
implement essentially the COMPLETE Option 2 transaction (off-canonical
|
|
||||||
prepare against a scratch clock/sealed candidate controller, single
|
|
||||||
validated atomic commit, full retail-staged SetPosition activation) with
|
|
||||||
epoch/token/generation/identity re-validation at every external entry
|
|
||||||
point — it has ZERO production callers in either host.
|
|
||||||
- The single largest remaining defect even AFTER wiring: the public
|
|
||||||
`RuntimeLocalPlayerMovementState.Controller` setter is an unguarded escape
|
|
||||||
hatch both hosts currently use directly; it must be sealed (made
|
|
||||||
unreachable from hosts, or itself epoch-gated) for the word "exclusive" in
|
|
||||||
prerequisite C to be true.
|
|
||||||
|
|
@ -1,285 +0,0 @@
|
||||||
# Next-agent prompt — finish the retail placement/collision campaign
|
|
||||||
|
|
||||||
Continue acdream from the 2026-08-03 stabilization checkpoint. The code is
|
|
||||||
modern; behavior must remain retail-faithful. The campaign is playable again,
|
|
||||||
but it is **not closed**.
|
|
||||||
|
|
||||||
## Start here
|
|
||||||
|
|
||||||
Use the merged local `main` worktree:
|
|
||||||
|
|
||||||
```text
|
|
||||||
C:\Users\erikn\source\repos\acdream
|
|
||||||
```
|
|
||||||
|
|
||||||
The completed fixes originated on `codex/port-claude-agents` and are merged
|
|
||||||
into local `main`. Confirm the exact starting commit with `git rev-parse HEAD`
|
|
||||||
and read the operator's handoff message for the merge SHA. Do not reset,
|
|
||||||
clean, or overwrite the main worktree's untracked research/reference files.
|
|
||||||
The original feature worktree remains at:
|
|
||||||
|
|
||||||
```text
|
|
||||||
C:\Users\erikn\.codex\worktrees\af5e\acdream
|
|
||||||
```
|
|
||||||
|
|
||||||
That feature worktree contains protected user-local modifications and is not
|
|
||||||
the preferred continuation workspace.
|
|
||||||
|
|
||||||
Read these files in order before editing:
|
|
||||||
|
|
||||||
1. `CLAUDE.md` and `AGENTS.md`.
|
|
||||||
2. `docs/plans/2026-08-02-placement-cutover.md` — canonical placement-cutover
|
|
||||||
plan and the 2026-08-03 checkpoint.
|
|
||||||
3. This prompt.
|
|
||||||
4. `docs/research/2026-08-02-collision-throughput-handoff/implementer-progress.md`
|
|
||||||
— especially `## P1` and the final stabilization checkpoint.
|
|
||||||
5. `docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md`.
|
|
||||||
6. `docs/research/2026-08-02-c3c-cutover-closeout.md`.
|
|
||||||
7. `docs/ISSUES.md` #269 and #276–#280.
|
|
||||||
8. `docs/architecture/retail-divergence-register.md` rows AP-1, AP-22,
|
|
||||||
AP-131, AD-1, AD-10, AD-60, and TS-28.
|
|
||||||
|
|
||||||
`docs-drafts.md` is now explicitly historical. Do **not** apply it wholesale:
|
|
||||||
it predates the final fixes and incorrectly tries to reuse issue number #280.
|
|
||||||
|
|
||||||
## Binding rules
|
|
||||||
|
|
||||||
- Grep `docs/research/named-retail/acclient_2013_pseudo_c.txt` by named
|
|
||||||
`class::method` before fresh decompilation. Retail behavior is the oracle.
|
|
||||||
- Preserve the modern Runtime-owned, presentation-independent architecture.
|
|
||||||
Graphical and headless hosts must use the same canonical gameplay owners.
|
|
||||||
- Root causes only. Do not add timeouts, grace periods, suppression flags,
|
|
||||||
catch-and-swallow paths, duplicated placement writers, or compatibility
|
|
||||||
bypasses to make a test green.
|
|
||||||
- The user's connected observations are acceptance facts. A green automated
|
|
||||||
test cannot overrule a live regression.
|
|
||||||
- Never use `git add -A`, `git add .`, `git reset --hard`, or
|
|
||||||
`git checkout -- <path>`. Stage exact paths only.
|
|
||||||
- Do not delete or normalize unrelated/untracked worktree content.
|
|
||||||
- Each independent fix must be a bisectable commit whose message records the
|
|
||||||
root cause and evidence.
|
|
||||||
- Update the divergence register and issues in the same commit that changes
|
|
||||||
their truth. Retire a row only when its exact legacy mechanism is gone.
|
|
||||||
- Do not push unless the user explicitly asks.
|
|
||||||
- Connected tests require the user's ACE server at `127.0.0.1:9000`. Close
|
|
||||||
the client gracefully before reconnecting so ACE releases the session.
|
|
||||||
|
|
||||||
## What has been completed
|
|
||||||
|
|
||||||
### C3c and collision-publication checkpoint
|
|
||||||
|
|
||||||
- `529e0e9d` — C3c production first-entry cutover for graphical and headless
|
|
||||||
hosts.
|
|
||||||
- `71604331` — O(changed) per-landblock collision publication checkpoint.
|
|
||||||
Its original commit was deliberately marked WIP after the first feel test;
|
|
||||||
do not treat that old label as the current product status. The following
|
|
||||||
fixes addressed the observed failures.
|
|
||||||
|
|
||||||
### Stabilization fixes, all user-verified where visual behavior applies
|
|
||||||
|
|
||||||
1. `01f4791e` — **retirement-receipt replay loop fixed.**
|
|
||||||
- Root cause: a pending-only live-projection bucket was promoted to a
|
|
||||||
second full landblock cleanup receipt after the first detach had already
|
|
||||||
committed. The duplicate guard threw; a broad resumable path replayed
|
|
||||||
the detach 243 times.
|
|
||||||
- Fix: retain pending identities without manufacturing another receipt;
|
|
||||||
post-commit receipt invariants are terminal, not resumable.
|
|
||||||
- Evidence: focused recenter tests; complete Release suite 10,815 passed /
|
|
||||||
4 skipped; lifecycle report
|
|
||||||
`logs/connected-world-gate-20260802-203751/report.json`; nine-stop report
|
|
||||||
`logs/connected-r6-soak-20260802-204309.report.json` with `Passed=true`,
|
|
||||||
nine checkpoints, zero failures, zero wait cues, zero pending
|
|
||||||
retirements, and no recurrence of the 243x exception.
|
|
||||||
|
|
||||||
2. `670f307c` — **remote placement and targeting share one world frame.**
|
|
||||||
- Root cause: CreateObject positions are landblock-local, but Runtime
|
|
||||||
submitted remote first-entry placement with zero world offset; the local
|
|
||||||
physics host also published a landblock-local origin to targeting.
|
|
||||||
- Fix: Runtime owns the accepted local-player world center, converts remote
|
|
||||||
Create placement before SetPosition, and publishes the local body's world
|
|
||||||
position.
|
|
||||||
- User gate: monsters and statics place correctly; monsters chase and hit
|
|
||||||
the visible player instead of attacking another coordinate.
|
|
||||||
|
|
||||||
3. `1fc529cd` — **distant Use/approach restored.**
|
|
||||||
- Root cause: Runtime object lookup is intentionally non-constructing, so a
|
|
||||||
static door/corpse could enter MoveTo without a physics host and its
|
|
||||||
target snapshot expired at the origin. Startup placement could also
|
|
||||||
leave an impossible pre-PartArray motion suffix ahead of later actions.
|
|
||||||
- Fix: ensure the canonical minimal static host before routing MoveTo and
|
|
||||||
reconcile the startup suffix exactly at presentation attach.
|
|
||||||
- User gate: near and distant object use works, including approach, turn,
|
|
||||||
and use after arrival.
|
|
||||||
|
|
||||||
4. `f24532ad` — **spell, recall, projectile, and static VFX binding fixed.**
|
|
||||||
- Root cause: C3c could create effect/projectile/static-animation sidecars
|
|
||||||
before first SetPosition had bound the entity's mesh, pose, cell, and
|
|
||||||
visibility. One-shot F754/F755 packets were lost and projectiles could
|
|
||||||
inherit a cell-less body.
|
|
||||||
- Fix: exact-incarnation presentation barrier and FIFO replay; retry
|
|
||||||
projectile/static binding on the committed visibility edge; synchronize
|
|
||||||
effect cells on rebucket.
|
|
||||||
- User gate: buffs/protections, recall effects, arrows, combat spell
|
|
||||||
projectiles, portals, and static animation all work.
|
|
||||||
|
|
||||||
5. `175ad6b0` — **login materialization acknowledgement fixed.**
|
|
||||||
- Root cause: ACE creates the local player Hidden and releases that state on
|
|
||||||
LoginComplete. Sending LoginComplete from raw F746 receipt raced first
|
|
||||||
canonical placement and left the purple haze over the character.
|
|
||||||
- Fix: one completion callback from Runtime's local first-entry terminal
|
|
||||||
edge; content-less headless retains its only truthful accepted-Create
|
|
||||||
edge.
|
|
||||||
- User gate: ordinary login no longer leaves the purple haze; recall still
|
|
||||||
has the intended materialization presentation.
|
|
||||||
|
|
||||||
### Latest focused verification
|
|
||||||
|
|
||||||
After the final fix, these passed:
|
|
||||||
|
|
||||||
- 90 focused App effect/projectile/static-animation scheduler tests.
|
|
||||||
- Two focused Runtime login-completion tests.
|
|
||||||
- The exact live-entity cell-tracking regression.
|
|
||||||
- All 79 Headless tests.
|
|
||||||
- `dotnet build AcDream.slnx -c Release --no-restore` with zero errors
|
|
||||||
(existing warnings remain).
|
|
||||||
|
|
||||||
The long complete suite and connected nine-stop soak were **not rerun after
|
|
||||||
the final four stabilization commits**. The P1 soak proves P1's binary, not
|
|
||||||
the final campaign binary.
|
|
||||||
|
|
||||||
## What remains — execute in this order
|
|
||||||
|
|
||||||
### 1. Reconcile the six selected-fixture failures
|
|
||||||
|
|
||||||
A broad selected run after cleanup exposed:
|
|
||||||
|
|
||||||
- five failures in `LiveEntityRuntimeTests`, associated with the still-open
|
|
||||||
placement/cell cutover;
|
|
||||||
- one old `RuntimeLiveEntitySessionControllerTests` remote-first-entry fixture
|
|
||||||
that provides an empty collision source while the production contract now
|
|
||||||
requires truthful collision admission.
|
|
||||||
|
|
||||||
Re-run these two classes first and record the exact test names and assertions.
|
|
||||||
Classify each as either a real product failure or a stale fixture. If stale,
|
|
||||||
update the fixture to provide the same valid prepared collision neighborhood
|
|
||||||
as production; never weaken the product contract or merely change expected
|
|
||||||
values. If real, fix the owning production mechanism and add a smaller
|
|
||||||
regression test.
|
|
||||||
|
|
||||||
Suggested first commands:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:ACDREAM_PAK_PATH='C:\Users\erikn\Documents\Asheron''s Call\acdream.pak'
|
|
||||||
dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~LiveEntityRuntimeTests -m:1
|
|
||||||
dotnet test tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~RuntimeLiveEntitySessionControllerTests -m:1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Finish C4: remaining authoritative placement routes
|
|
||||||
|
|
||||||
The plan still marks routes 2–7 open:
|
|
||||||
|
|
||||||
- route 2: ForcePosition;
|
|
||||||
- route 3: portal placement through `RuntimeWorldTransitState` and
|
|
||||||
`RuntimePortalPlacementAuthority`;
|
|
||||||
- route 4: remote Create/Position, deleting the remaining
|
|
||||||
`RemoteTeleportController`/inline MoveOrTeleport duplicate;
|
|
||||||
- route 5: authoritative projectile correction;
|
|
||||||
- route 6: drops and split-recovery marking;
|
|
||||||
- route 7: pickup, parent-detach, and delete/recreate residue.
|
|
||||||
|
|
||||||
Inventory every current writer before editing. For each route, prove:
|
|
||||||
|
|
||||||
- one Runtime SetPosition transaction owns accepted frame, exact cell,
|
|
||||||
collision result, shadow/workset membership, and deferred-cell lifetime;
|
|
||||||
- App only projects the committed result;
|
|
||||||
- graphical and headless hosts use the same command and state path;
|
|
||||||
- stale sequences, delete/GUID reuse, missing cells, portal generations, and
|
|
||||||
replacement collision generations cannot commit old state;
|
|
||||||
- no route reconstructs from a stale spawn or uses the legacy outdoor demote/
|
|
||||||
terrain-Z lift.
|
|
||||||
|
|
||||||
Resolve #276 when the spawn settler's resolved `CellId` becomes authoritative.
|
|
||||||
Resolve #277 with a real service-window/celless lifecycle instead of relying
|
|
||||||
on ACE's current broadcast radius.
|
|
||||||
|
|
||||||
### 3. Fix #280: destination prefetch before portal reveal
|
|
||||||
|
|
||||||
Current behavior waits only a hard-coded radius-one (3x3) outdoor
|
|
||||||
neighborhood, while the visible configured world extends farther. The user
|
|
||||||
can see distant terrain continue building after portal exit.
|
|
||||||
|
|
||||||
Port the retail mechanism, not a larger magic number:
|
|
||||||
|
|
||||||
- `CellManager::PreFetchCells @ 0x00455820`;
|
|
||||||
- `LScape::PreFetchCells @ 0x00505660`;
|
|
||||||
- `CLandBlock::PreFetchCells` and `CLandBlockInfo::PreFetchCells`;
|
|
||||||
- `SmartBox::UseTime @ 0x00455410` while `blocking_for_cells`;
|
|
||||||
- the `TAS_TUNNEL_CONTINUE` resume/reveal order.
|
|
||||||
|
|
||||||
Use the quality/view-distance configured destination window. Hold one
|
|
||||||
generation-scoped reservation across terrain, buildings/statics, EnvCells,
|
|
||||||
render publication, composite textures, and collision. Keep portal UI and
|
|
||||||
wait cue responsive. Never reveal early because of a timeout, and do not wait
|
|
||||||
for an impossible terminal marker for all dynamic ACE objects.
|
|
||||||
|
|
||||||
Acceptance: repeated login, `/ls`, spell recall, and portals at every quality
|
|
||||||
setting reveal no constructing terrain, missing nearby statics/buildings,
|
|
||||||
unready interiors, missing composites, or absent nearby collision. Dynamic
|
|
||||||
monsters/items may still arrive later from ACE.
|
|
||||||
|
|
||||||
### 4. C5 closeout and live gates
|
|
||||||
|
|
||||||
After steps 1–3:
|
|
||||||
|
|
||||||
1. Delete every superseded placement writer and compatibility projection.
|
|
||||||
2. Run focused Runtime/Core/App tests for every route.
|
|
||||||
3. Run the complete Release solution suite with the installed pak.
|
|
||||||
4. Run the exact lifecycle/reconnect route.
|
|
||||||
5. Run the canonical nine-stop soak on the **final binary**. Read
|
|
||||||
`report.json`, not marker output. Required: `Passed=true`, zero failures,
|
|
||||||
every canonical checkpoint, `waitCueShown=false`, zero pending
|
|
||||||
publication/retirement/reveal debt, graceful exit, and no render-shadow
|
|
||||||
mismatch. Diagnose any real failure; do not rerun past it.
|
|
||||||
6. Perform two-client observation for remote creation, chase/attack, doors,
|
|
||||||
drops/pickups, portal departure/arrival, arrows, and spells.
|
|
||||||
7. Ask the user for the remaining #269/#278 slope-glide comparison at the
|
|
||||||
known impassable slope.
|
|
||||||
|
|
||||||
Only then retire AP-1, AD-1, AP-131, and the legacy half of AD-60 and close
|
|
||||||
the corresponding placement issues.
|
|
||||||
|
|
||||||
### 5. Finish the original physics-divergence campaign
|
|
||||||
|
|
||||||
After placement C5 is green:
|
|
||||||
|
|
||||||
- **AP-22:** make `ShadowShapeBuilder` the sole authority for authored Setup
|
|
||||||
collision shapes. Preserve cylinder order; use authored spheres when there
|
|
||||||
are no cylinders; cylinder-first for mixed data; truly shapeless means no
|
|
||||||
shadow. Remove invented `Setup.Radius` cylinders, `Radius * 2` heights, and
|
|
||||||
sphere-to-cylinder coercion across graphical, headless, static, and live
|
|
||||||
paths.
|
|
||||||
- **AD-10:** prove remote motion uses the full transition sweep, remove
|
|
||||||
terrain-normal preprojection, and let `CTransition::adjust_offset` project
|
|
||||||
against the actual retained contact plane. Preserve interpolation,
|
|
||||||
correction replacement, Hidden state, and network cadence.
|
|
||||||
- Run the final movement/collision matrix and update the divergence ledger,
|
|
||||||
architecture, roadmap, milestones, memory, `CLAUDE.md`, and `AGENTS.md`.
|
|
||||||
Resume vendor Slice 5 only after this campaign is genuinely closed.
|
|
||||||
|
|
||||||
## Required deliverable
|
|
||||||
|
|
||||||
For every remaining item report:
|
|
||||||
|
|
||||||
- observed failure and deterministic reproduction;
|
|
||||||
- retail/reference evidence with named functions and addresses;
|
|
||||||
- root cause in plain language plus file/line evidence;
|
|
||||||
- exact fix and why it preserves Runtime ownership;
|
|
||||||
- tests added or corrected;
|
|
||||||
- commit SHA;
|
|
||||||
- complete build/test/connected-gate numbers;
|
|
||||||
- user visual result where required;
|
|
||||||
- divergence/issue rows retired, narrowed, or left open.
|
|
||||||
|
|
||||||
Finish with an explicit list of anything still open. Do not describe the
|
|
||||||
campaign as complete while any C4 route, #280, final-binary soak, AP-22,
|
|
||||||
AD-10, or required user visual gate remains.
|
|
||||||
|
|
@ -1,411 +0,0 @@
|
||||||
# O(changed) collision clone — design note
|
|
||||||
|
|
||||||
**Phase:** research + design only. No production edits, nothing staged, no probes left
|
|
||||||
behind. Worktree `C:\Users\erikn\.codex\worktrees\af5e\acdream`, branch
|
|
||||||
`codex/port-claude-agents`, HEAD `c52ce14a`.
|
|
||||||
|
|
||||||
**Problem:** the collision-generation staging clone is O(resident world) per landblock
|
|
||||||
publication, so loading an N-landblock ring costs O(N²). The far ring never converges.
|
|
||||||
C3c made it user-visible (late monster pop-in, extended/stuck portal space, portal-exit
|
|
||||||
pop-in, failing nine-stop soak) but did not cause it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## (a) What the one-leaf-per-step invariant actually protects
|
|
||||||
|
|
||||||
### It is a frame-time bound. Nothing else.
|
|
||||||
|
|
||||||
The whole-world copy did not arrive with `6b28ff99`. It arrived one commit earlier, in
|
|
||||||
`be94bc9b` "fix(physics): activate collision generations atomically" (2026-07-31), as a
|
|
||||||
**synchronous** copy performed in a single call at admission:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// be94bc9b, PhysicsEngine.CreateCollisionStagingCopy
|
|
||||||
foreach ((uint id, LandblockPhysics landblock) in _landblocks)
|
|
||||||
staging._landblocks[id] = landblock;
|
|
||||||
staging.ShadowObjects.CopyCollisionStateFrom(ShadowObjects, stagingCache);
|
|
||||||
```
|
|
||||||
|
|
||||||
`6b28ff99` "make collision activation starvation-free" replaced that with
|
|
||||||
`CollisionStagingBuilder` (`src/AcDream.Core/Physics/PhysicsEngine.cs:785-941`), which
|
|
||||||
performs the *same* copy chopped into single leaves across frames. The retired AD-6 row
|
|
||||||
states the purpose verbatim
|
|
||||||
(`docs/architecture/retail-divergence-register.md:113`):
|
|
||||||
|
|
||||||
> "Admission captures the active root in O(1); a stable landblock/owner slot suffix
|
|
||||||
> materializes non-target leaves incrementally, **so resident-world size cannot become a
|
|
||||||
> synchronous clone spike**."
|
|
||||||
|
|
||||||
The committed test says the same thing three ways
|
|
||||||
(`tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs:921-991`):
|
|
||||||
|
|
||||||
| Assertion | line | What it pins |
|
|
||||||
|---|---|---|
|
|
||||||
| `Assert.InRange(admissionAllocation, 1L, 128L*1024L)` | :973 | admission allocates a constant |
|
|
||||||
| `Assert.Equal(0, prepared.Engine.LandblockCount)` | :974 | admission copies no resident landblock |
|
|
||||||
| `Assert.InRange(step.WorkUnits, 0, 1)` | :983 | **the copy is chopped to one leaf per host step** |
|
|
||||||
| `Assert.True(advances > residentLandblocks)` | :988 | it really walked the resident world |
|
|
||||||
| `Assert.Equal(residentLandblocks, prepared.Engine.LandblockCount)` | :989 | the draft ends up holding the whole world |
|
|
||||||
|
|
||||||
So the invariant protects **hitch avoidance**: a dense resident world must not produce
|
|
||||||
one long synchronous copy inside a single update step. It is a *scheduling* property
|
|
||||||
asserted as a *mechanism*, which is why the batching lever tripped it.
|
|
||||||
|
|
||||||
### What it does NOT protect
|
|
||||||
|
|
||||||
- **Not concurrent-reader isolation.** That is `CollisionWorldStateSlot.TransferTo`'s
|
|
||||||
single `Volatile.Write` (`src/AcDream.Core/Physics/CollisionWorldState.cs:66-84`).
|
|
||||||
And a full threading audit of every writer and reader of `CollisionWorldState` found
|
|
||||||
**no concurrent reader or writer exists**: `GameWindow` runs one Silk.NET loop thread;
|
|
||||||
`UpdateFrameOrchestrator.Tick` runs `_streaming.Tick()` → `DrainAndApply` and then the
|
|
||||||
live/physics/camera phases strictly sequentially on that thread; the only background
|
|
||||||
workers (`LandblockStreamer` worker thread, `EnvCellRenderer` `Parallel.ForEach`,
|
|
||||||
`ObjectMeshManager` `Task.Run`) never touch `PhysicsDataCache` / `CellGraph` /
|
|
||||||
`ShadowObjectRegistry` / `PhysicsEngine` — grep of `LandblockBuildFactory.cs` and
|
|
||||||
`LandblockMesh.cs` for those types returns zero hits. Every one of
|
|
||||||
`BeginCollisionAdmission` (:2040), `PrepareCollisionGeneration` (:2092),
|
|
||||||
`AdvanceCollisionGenerationPreparation` (:2123), `StageCollisionAssets` (:2218),
|
|
||||||
`AdvanceCollisionGenerationSeal` (:2298), `CommitCollisionGeneration` (:2382),
|
|
||||||
`CancelCollisionGeneration` (:2139) passes through
|
|
||||||
`RuntimePhysicsState.EnsureCollisionMutationThread` (:2857-2869). The
|
|
||||||
`ConcurrentDictionary` choices are load-bearing only for *single-threaded*
|
|
||||||
mutate-while-enumerating (`PhysicsDataCache.cs:958-984`, and the seal cursor holding a
|
|
||||||
live enumerator across frames at :1081-1160) — the cross-thread rationale in the
|
|
||||||
`CellGraph.cs:17` and `PhysicsDataCache.cs:14-20` doc comments is **stale after
|
|
||||||
6b28ff99**.
|
|
||||||
- **Not admission fairness.** That is the separate journal/coalescing machinery
|
|
||||||
(`RuntimePhysicsState.cs:475-529`, research doc step 5) — the other half of what
|
|
||||||
"starvation-free" meant. It is orthogonal to the leaf metering and stays.
|
|
||||||
|
|
||||||
### What the pre-6b28ff99 mechanism did
|
|
||||||
|
|
||||||
`be94bc9b`'s commit was a **delta apply**, not a root swap:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// be94bc9b, PhysicsEngine.CommitLandblockReplacement — deleted by 6b28ff99
|
|
||||||
DataCache.CommitLandblockReplacement(replacement.DataCache); // O(changed)
|
|
||||||
_landblocks[replacement.LandblockId] = replacement.Landblock;
|
|
||||||
ShadowObjects.CommitLandblockReplacement(replacement.Shadows);
|
|
||||||
```
|
|
||||||
|
|
||||||
`6b28ff99` replaced those three lines with
|
|
||||||
`stagingCache.CollisionWorld.TransferTo(activeCache.CollisionWorld)`
|
|
||||||
(`PhysicsEngine.cs:304-321`). **That is the change that made the clone load-bearing.**
|
|
||||||
Before it, the clone was a build sandbox; after it, the clone *is* the world that gets
|
|
||||||
published, so every leaf not cloned is a leaf deleted from the world.
|
|
||||||
|
|
||||||
Before `be94bc9b` the client mutated the active maps in place across many frames — the
|
|
||||||
genuinely non-equivalent state the research doc describes ("the active `PhysicsDataCache`,
|
|
||||||
`CellGraph`, `PhysicsEngine`, buildings, static shadows, and retained-object refloods
|
|
||||||
changed at different cursors", `docs/research/2026-07-31-atomic-collision-generation.md:12-16`).
|
|
||||||
**The atomicity requirement is "the multi-frame build must not be observable", not "the
|
|
||||||
whole world must be swapped".** A delta applied inside one synchronous update-thread call
|
|
||||||
satisfies it.
|
|
||||||
|
|
||||||
### The cost is worse than F4 measured
|
|
||||||
|
|
||||||
F4 attributed median 19,736 / p90 32,135 / max 38,021 leaves and median 3.64 ms per
|
|
||||||
publication to the staging clone. The **seal** does the same walk again: the replacement
|
|
||||||
builder holds live enumerators over four `_staging` maps *and* four `_active` maps
|
|
||||||
(`PhysicsDataCache.cs:1081, 1092, 1103, 1114, 1125, 1136, 1147, 1158`) plus two in
|
|
||||||
`CellGraph.cs:184, 203`, using `CapturePrefixOne` / `CaptureRemovalOne` — a full scan of
|
|
||||||
each map to find O(target) keys. Real per-publication cost is therefore roughly **2–3×
|
|
||||||
resident world**, not 1×. **Fixing only the clone leaves O(N²) in the seal.** Any design
|
|
||||||
that does not also scope the removal capture is not a fix.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## (b) Candidate designs
|
|
||||||
|
|
||||||
### D1 — Structural sharing (persistent/immutable `CollisionWorldState`)
|
|
||||||
|
|
||||||
Replace the ~20 mutable maps with persistent maps (HAMT / `ImmutableDictionary`) so a
|
|
||||||
staging clone shares unchanged subtrees and copies only the changed path.
|
|
||||||
|
|
||||||
- **Blast radius:** every read and write site of every map in `CollisionWorldState`,
|
|
||||||
`PhysicsDataCache`, `CellGraph`, `ShadowObjectRegistry`, `PhysicsEngine`.
|
|
||||||
- **Invariant changes:** none semantically; the root swap survives unchanged, so the
|
|
||||||
atomicity story is untouched.
|
|
||||||
- **Throughput:** admission O(1), clone O(1), commit O(changed · log N). Excellent on the
|
|
||||||
copy axis.
|
|
||||||
- **Why rejected:** it pays for the copy with the *query*. A HAMT probe is several times a
|
|
||||||
`Dictionary` probe and allocates on write; the resolver runs thousands of these per
|
|
||||||
frame at 30 Hz. Slice I's entire thesis is flat, integer-indexed, zero-allocation
|
|
||||||
collision (`docs/plans/2026-07-25-modern-runtime-slice-i.md`; I1 measured 0 B/resolve).
|
|
||||||
D1 optimizes the rare operation at the expense of the hot one and fights the I-series
|
|
||||||
architecture head-on.
|
|
||||||
|
|
||||||
### D1b — Landblock-sliced root (per-prefix immutable slice + small map)
|
|
||||||
|
|
||||||
Regroup the root so each landblock's cells / flat cells / EnvCells / buildings / terrain /
|
|
||||||
outdoor cells / `LandblockPhysics` live in one immutable `LandblockCollisionSlice`, and
|
|
||||||
the root becomes `Dictionary<prefix, slice>` (~625 entries). Commit = one dictionary
|
|
||||||
write per prefix.
|
|
||||||
|
|
||||||
- **Blast radius:** every keyed read becomes mask + two probes; the seal's removal scans
|
|
||||||
collapse to "old slice vs new slice". The **shadow registry does not partition** —
|
|
||||||
`ShadowEntityCells`, `ShadowEntityShapes`, `ShadowEntityRegistrations`,
|
|
||||||
`ShadowOwnerVersions` are owner-keyed and owners legitimately span prefixes (that is
|
|
||||||
the whole retained-owner problem), so the shadow half needs a separate mechanism.
|
|
||||||
- **Invariant changes:** the atomic unit becomes the slice; the root swap disappears.
|
|
||||||
- **Throughput:** O(changed) by construction, and atomic even for a hypothetical
|
|
||||||
concurrent reader.
|
|
||||||
- **Verdict:** this is the right answer *if* concurrent readers existed. They do not.
|
|
||||||
Keep it on the shelf as the migration target should the runtime ever go multi-threaded;
|
|
||||||
do not pay its refactor cost now.
|
|
||||||
|
|
||||||
### D2 — Per-landblock atomic unit: restore the delta apply *(recommended)*
|
|
||||||
|
|
||||||
`CommitLandblockReplacement` drains the **already-existing**
|
|
||||||
`PhysicsEngine.LandblockReplacementApplyCursor` (`PhysicsEngine.cs:568-777`) against the
|
|
||||||
**active** root inside one synchronous call, instead of `TransferTo`. The staging root
|
|
||||||
becomes empty-at-admission (target content only); `CollisionStagingBuilder` phases 0–8 are
|
|
||||||
deleted.
|
|
||||||
|
|
||||||
The delta record already exists and is already tested: `PreparedPhysicsDataCacheLandblock`
|
|
||||||
(`PhysicsDataCache.cs:1255-1270`) is exactly lists of key/value pairs to install and lists
|
|
||||||
of ids to remove, all target-scoped. The apply cursor already handles removals, installs,
|
|
||||||
terrain, the 0x40 synthesized outdoor cells, the landblock itself, and yields the reflood
|
|
||||||
owner ids to the caller at phase 12 (`PhysicsEngine.cs:702-712`). Today it is used to
|
|
||||||
rebase a committed peer delta into a *later draft*; pointing its `destination` at the
|
|
||||||
active engine is a constructor argument, not new machinery.
|
|
||||||
|
|
||||||
**What breaks, honestly:**
|
|
||||||
|
|
||||||
1. *Readers mid-query* — nothing. Single-threaded, evidenced above. A drained cursor
|
|
||||||
inside one call is indivisible with respect to every reader that exists.
|
|
||||||
2. *Re-entrancy* — real, and the audit flagged it: `OwnerMutated` /
|
|
||||||
`OwnerPrefixMembershipChanged` (`ShadowObjectRegistry.cs:86-87`) can fire mid-delta.
|
|
||||||
Precedent already exists: the commit brackets itself with
|
|
||||||
`_suppressCollisionOwnerJournal = true` (`RuntimePhysicsState.cs:2491-2501`). Extend
|
|
||||||
that bracket to cover the whole apply.
|
|
||||||
3. *Cross-frame enumerators* — the seal holds live enumerators over the **active** maps
|
|
||||||
across frames (`PhysicsDataCache.cs:1092, 1136, 1158`). A delta apply now mutates the
|
|
||||||
maps those enumerators walk. `ConcurrentDictionary` will not throw, but the observed
|
|
||||||
set is unspecified. **O1 below removes those enumerators entirely**, which is why O1
|
|
||||||
must land first.
|
|
||||||
4. *The retirement machinery* — `LandblockRetirementCursor` (`PhysicsEngine.cs:348-...`)
|
|
||||||
currently retires from an off-side draft. Same cursor, destination becomes the active
|
|
||||||
root, still drained in one call.
|
|
||||||
5. *The reflood context* — the seal currently computes retained-owner refloods against a
|
|
||||||
full staging world. With an empty staging root that context is gone, so the reflood
|
|
||||||
moves to the commit call, against the now-current active world. **That is precisely
|
|
||||||
retail**: `CObjCell::init_objects` (0x0052B420) → `CPhysicsObj::recalc_cross_cells`
|
|
||||||
(0x00515A30), already the retail anchor cited on the AD-6 row.
|
|
||||||
6. *The peer-rebase / journal apparatus* — with no snapshot there is nothing to rebase.
|
|
||||||
`EnqueueCommittedRebase` (`RuntimePhysicsState.cs:563-578, 2502-2507`) and most of the
|
|
||||||
journal become dead. Delete them in the same slice; do not leave dead invariants
|
|
||||||
guarding a deleted mechanism.
|
|
||||||
|
|
||||||
- **Throughput:** per publication ≈ target payload (~70–200 leaves at the measured
|
|
||||||
~184 ns/leaf) + the owners touching the target, versus today's ~2–3 × 20,000. Roughly
|
|
||||||
**300× less work per publication**, and — decisively — **independent of resident-world
|
|
||||||
size**, so total ring load goes O(N²) → O(N). At the failing run's numbers that is
|
|
||||||
~13.7 M leaf copies for a 625-landblock ring down to ~44 K.
|
|
||||||
|
|
||||||
### D3 — Adjacency-scoped clone (the tempting middle ground) — **rejected as unsafe**
|
|
||||||
|
|
||||||
Copy only leaves in the target's 3×3 landblock neighbourhood. One predicate change in
|
|
||||||
`CopyOneOutsideTarget` (`PhysicsEngine.cs:949-961`); clone drops ~20,000 → ~630 and
|
|
||||||
becomes O(1) in world size.
|
|
||||||
|
|
||||||
Rejected for a structural reason worth stating plainly: **while commit is a whole-root
|
|
||||||
transfer, "clone less" means "delete more."** Anything not copied into the draft is absent
|
|
||||||
from the root that replaces the world. A partial clone is therefore a silent world-erasure
|
|
||||||
bug, not a perf tuning knob. Only after commit becomes a delta does bounded context become
|
|
||||||
safe — at which point D2 has already removed the need for it. It also leaves the seal's
|
|
||||||
O(world) scans untouched, so O(N²) survives regardless.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## (c) Recommendation
|
|
||||||
|
|
||||||
**Take D2, in three landable slices, with O1 first.**
|
|
||||||
|
|
||||||
Rationale in one line: the delta-apply commit path is not a new invention — it is the
|
|
||||||
mechanism that shipped in `be94bc9b` and was deleted by `6b28ff99` to buy an atomicity
|
|
||||||
guarantee against concurrent readers that do not exist; restoring it makes the cost
|
|
||||||
O(changed) by construction and moves the client *toward* retail's `init_objects` shape,
|
|
||||||
not away from it.
|
|
||||||
|
|
||||||
### Invariant-test replacement
|
|
||||||
|
|
||||||
Delete from `DenseResidentWorldAdmissionIsConstantAndMaterializesOneLeafPerStep`
|
|
||||||
(`tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs:921-991`) the three
|
|
||||||
assertions that pin the clone itself — `:983` `Assert.InRange(step.WorkUnits, 0, 1)`,
|
|
||||||
`:988` `Assert.True(advances > residentLandblocks)`, `:989`
|
|
||||||
`Assert.Equal(residentLandblocks, prepared.Engine.LandblockCount)`. They assert the exact
|
|
||||||
mechanism being removed.
|
|
||||||
|
|
||||||
Replace with `CollisionPreparationCostIsIndependentOfResidentWorldSize` — a strictly
|
|
||||||
stronger invariant, because it pins the *property* (bounded, world-size-independent work)
|
|
||||||
rather than a mechanism:
|
|
||||||
|
|
||||||
```
|
|
||||||
Run the full admission → preparation → seal → commit sequence twice,
|
|
||||||
at residentLandblocks = 32 and residentLandblocks = 256.
|
|
||||||
|
|
||||||
Assert total preparation advances(32) == total preparation advances(256) // O(changed)
|
|
||||||
Assert total seal WorkUnits(32) == total seal WorkUnits(256) // closes the seal scans
|
|
||||||
Assert every step.WorkUnits <= K // K = retained per-step bound
|
|
||||||
Assert admissionAllocation in [1, 128 KiB] // kept from :973
|
|
||||||
Assert prepared.Engine.LandblockCount == 0 after preparation completes // stronger than :974:
|
|
||||||
// the draft now holds ONLY the target
|
|
||||||
```
|
|
||||||
|
|
||||||
Add two more:
|
|
||||||
|
|
||||||
- `CommitAppliesOneLandblockDeltaInASingleCall` — the engine-mutating
|
|
||||||
`CommitCollisionGeneration` call drains the apply cursor to `Completed` before it
|
|
||||||
returns; the active world holds no target-prefix content before it and the complete
|
|
||||||
target after it, with no observable intermediate.
|
|
||||||
- `CommitTimeRefloodMatchesPrecomputedReflood` — for a fixed scenario, the owner set and
|
|
||||||
each owner's resulting cross-cell set after a commit-time reflood are **equal** to what
|
|
||||||
the pre-change staged reflood produced. This is the proof that D2 is a scheduling
|
|
||||||
change and not a semantics change, and it is the test that makes the perf framing in
|
|
||||||
(d) legitimate.
|
|
||||||
|
|
||||||
**Keep unchanged:** every `Assert.InRange(seal.WorkUnits, 0, 1)` at `:558, :667, :747,
|
|
||||||
:848, :1797, :2020, :2318, :2428, :3033` — the seal stays metered; the zero-managed-byte
|
|
||||||
commit assertions (the delta lists are built during seal, so the apply must still be
|
|
||||||
allocation-free); and `CommittedPreparationRevokesItsStagingCollisionRoot` (`:1664`) in
|
|
||||||
spirit — the staging root must still be revoked after commit, it simply no longer becomes
|
|
||||||
the active root.
|
|
||||||
|
|
||||||
### Migration plan
|
|
||||||
|
|
||||||
| Slice | Change | Gate |
|
|
||||||
|---|---|---|
|
|
||||||
| **O1** | Per-prefix installed-key ledger in `CollisionWorldState`, maintained by the install/remove paths. Rewrite the seal's ten full-map scans (`PhysicsDataCache.cs:1081-1160`, `CellGraph.cs:184, 203`) to enumerate that set. Removes the cross-frame active-map enumerators. **Behaviour-identical; a pure win that lands alone.** | existing suites green + the new seal-independence assertion |
|
|
||||||
| **O2** | `PhysicsEngine.CommitLandblockReplacement` drains `LandblockReplacementApplyCursor` against the active root instead of `TransferTo`. Extend the `_suppressCollisionOwnerJournal` bracket over the whole apply. Retirement cursor destination → active root. | focused Runtime physics suite + connected lifecycle gate |
|
|
||||||
| **O3** | Empty staging root: delete `CollisionStagingBuilder` phases 0–8. Move retained-owner reflood into the commit call (retail `init_objects` → `recalc_cross_cells`). Delete the now-dead peer-rebase/journal paths and their tests. | full ladder below |
|
|
||||||
|
|
||||||
### Gate ladder (O3 closeout)
|
|
||||||
|
|
||||||
1. **Focused:** Runtime physics collision-generation suite, App
|
|
||||||
`LandblockPhysicsPublisherTests`, Headless `HeadlessSessionHostTests`.
|
|
||||||
2. **Complete Release solution:** baseline to match or beat is **10,808 passed / 0 failed
|
|
||||||
/ 4 skips** (`-m:1`, `ACDREAM_PAK_PATH`).
|
|
||||||
3. **Connected lifecycle/reconnect gate:** signature must hold — `Passed=true`,
|
|
||||||
`Failures=[]`, both sessions `ExitCode=0`, zero render-shadow mismatches, zero pending
|
|
||||||
deltas, graceful exits.
|
|
||||||
4. **Nine-stop soak must reach `Passed: true` with `Failures: []`.** The failing run is
|
|
||||||
`logs/connected-r6-soak-20260802-143157.report.json` (37 failures, `Passed: false`);
|
|
||||||
the passing baseline is `logs/connected-r6-soak-20260727-004942.report.json`
|
|
||||||
(`Passed: true`, commit `a9a822f2`). Concrete acceptance, per checkpoint:
|
|
||||||
|
|
||||||
| Key | Failing run | Required |
|
|
||||||
|---|---|---|
|
|
||||||
| `resources.streamingWork.deferredCompletions` | 92–501 at 8/9 stops | `0` at all 9 |
|
|
||||||
| `resources.streamingWork.farBacklog` | same values | `0` at all 9 |
|
|
||||||
| `resources.streamingWork.pendingPublications` | `1` at 8/9 | `0` at all 9 |
|
|
||||||
| `resources.streamingWork.deferredAdoptedCpuBytes` | 1.5–8.5 MB | `0` at all 9 |
|
|
||||||
| `resources.streamingWork.oldestDeferredAgeMilliseconds` | 37,764–69,728 | `0` |
|
|
||||||
| `resources.loadedLandblocks` | 124–533 | **625** at the eight outdoor stops |
|
|
||||||
| `reveal.waitCueShown` | `true` at 6/9 | `false` at all 9 — *this is the user-reported "extended/stuck portal space"* |
|
|
||||||
| `streamingWork.lifetimeFrameOverrunCount` | 1,706 | materially lower |
|
|
||||||
| `streamingWork.maximumOperationStage` | `"publication-index-physics"` | must no longer name this stage |
|
|
||||||
|
|
||||||
**`aerlinthe` (sequence 4) is the control, not a target.** It is the one indoor
|
|
||||||
destination and the one stop that is already clean in the failing run (374/173 vs the
|
|
||||||
baseline's 374/176, every streaming counter `0`) — precisely because an indoor
|
|
||||||
destination streams few landblocks, so O(N²) never bites. It must stay clean; do not
|
|
||||||
expect it to reach 625.
|
|
||||||
|
|
||||||
5. **Frame time must not regress — and must recover.** Route-level `cpuUs` from
|
|
||||||
`frame-history-summary.json`, microseconds:
|
|
||||||
|
|
||||||
| | p50 | p95 | p99 | p999 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| baseline `20260727` | 9,730 | 41,262 | 44,875 | 63,474 |
|
|
||||||
| failing `20260802` | 17,001 | 47,434 | 57,061 | 102,876 |
|
|
||||||
|
|
||||||
Gate on the sharper per-checkpoint window numbers: **`checkpointWindows[].metrics.cpuUs`
|
|
||||||
p99 within +10 % of the `20260727` baseline at every stop.** The worst offenders are
|
|
||||||
`caul-plateau` 101,408 → target ≈ 47,821; `caul-return` 103,309 → ≈ 46,938;
|
|
||||||
`caul-baseline` 108,148 → ≈ 43,664; `sawato-baseline` 49,748 → ≈ 10,592. Frame count
|
|
||||||
should recover toward the baseline's 35,492 frames / 498.5 s from the failing run's
|
|
||||||
25,965 / 583.9 s.
|
|
||||||
|
|
||||||
6. **Do NOT gate on these — retest only after convergence.** `trackedGpuBytes` (62 MB
|
|
||||||
failing vs 474 MB baseline), `meshRenderData` 588 vs 607, `meshEstimatedBytes`
|
|
||||||
233.8 MB vs 268.6 MB, and the inverted CPU mesh-cache hit ratio
|
|
||||||
(3,666 hits / 5,689 misses vs 20,825 / 6,845) are all far-ring-never-converged
|
|
||||||
artifacts of the same mechanism. F4 already reached this conclusion; a leak
|
|
||||||
investigation before convergence is restored will chase a ghost.
|
|
||||||
|
|
||||||
### Register / plan bookkeeping (same commit as the code)
|
|
||||||
|
|
||||||
- **`docs/architecture/retail-divergence-register.md:113`** — the retired AD-6 row
|
|
||||||
describes the deleted mechanism verbatim ("one shared off-side `CollisionWorldState`",
|
|
||||||
"one zero-managed-byte volatile root transfer", the journal, the peer rebases). A
|
|
||||||
retired row still documents what shipped; leaving it describing a deleted clone is
|
|
||||||
exactly the out-of-sync failure the register rules forbid. Rewrite it to the delta-apply
|
|
||||||
mechanism. The row's retail anchor is already
|
|
||||||
`CObjCell::init_objects` → `recalc_cross_cells` (0x0052b420 / 0x00515a30) — the new
|
|
||||||
mechanism is **closer** to that anchor, so no new deviation row is created.
|
|
||||||
- **Judgment call for the implementer, do not assume:** the *original* AD-6 deviation was
|
|
||||||
"Per-LANDBLOCK shadow re-flood on hydration vs retail per-CELL `recalc_cross_cells`"
|
|
||||||
(`be94bc9b` register diff). If O3's commit-time reflood is again per-landblock rather
|
|
||||||
than per-cell, decide explicitly whether AD-6 must be un-retired or a successor row
|
|
||||||
added, and record the decision. Flagged, not decided here.
|
|
||||||
- **`docs/research/2026-07-31-atomic-collision-generation.md`** — steps 2, 3, 5, 6, 7, 8
|
|
||||||
and most of the "Deterministic evidence" list describe the clone / journal / rebase.
|
|
||||||
Rewrite in the same commit.
|
|
||||||
- **`memory/project_collision_port.md`** — the 37-line block `6b28ff99` added is now wrong.
|
|
||||||
- **`claude-memory/project_physics_collision_digest.md`** — add two DO-NOT-RETRY entries:
|
|
||||||
(1) *"Do not re-introduce a whole-world staging clone. The atomic unit is the landblock
|
|
||||||
delta applied in one update-thread call; the runtime is single-threaded and the root
|
|
||||||
swap buys nothing."* (2) *"Batching N leaves per staging step is not a fix — measured
|
|
||||||
1.8× at N=256, not convergence, and it trips the committed invariant test."*
|
|
||||||
- **`docs/ISSUES.md`** — F4 established this regression is not in the C3c diff, so it
|
|
||||||
needs its own issue id (the C3c smoke-test commit `c52ce14a` filed #279 for a different
|
|
||||||
finding). File it, and reference it from the O1/O2/O3 commit messages.
|
|
||||||
- **Rollback:** each slice lands as one commit with its own recorded `git revert` SHA,
|
|
||||||
per the Modern Runtime convention.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## (d) Perf-work framing
|
|
||||||
|
|
||||||
**This is modern-runtime infrastructure, not retail-scoped behaviour work.** The delta
|
|
||||||
apply installs the *identical* `PreparedPhysicsDataCacheLandblock` content that
|
|
||||||
`TransferTo` publishes today — the same cells, flat cells, EnvCell topology, buildings,
|
|
||||||
terrain, synthesized outdoor cells, landblock, and owner set. Only the path by which that
|
|
||||||
content reaches the active root changes, and only the amount of work done to get there.
|
|
||||||
Collision results, contact planes, walkable polygons, membership, and therefore game feel
|
|
||||||
are bit-identical.
|
|
||||||
|
|
||||||
The project's render-perf-not-faithfulness-gated rule
|
|
||||||
(`claude-memory/feedback_render_perf_not_faithfulness_gated.md`) applies: throughput work
|
|
||||||
that is pixel- and feel-identical does not need a retail-behaviour gate. But because this
|
|
||||||
is collision, the acceptance bar is still the connected gates plus the user's visual pass
|
|
||||||
— green unit tests prove nothing about a streaming convergence bug.
|
|
||||||
|
|
||||||
Two guards keep the framing honest:
|
|
||||||
|
|
||||||
1. **The direction of travel is toward retail, not away.** Retail hydrates a cell
|
|
||||||
synchronously in `CObjCell::init_objects` and refloods the objects associated with it
|
|
||||||
via `recalc_cross_cells`. A per-landblock delta applied in one update-thread call is
|
|
||||||
the streaming-shaped version of exactly that. The whole-world clone was the adaptation;
|
|
||||||
removing it retires an adaptation rather than adding one.
|
|
||||||
2. **The one thing that could change feel is reflood timing** — owners near the target
|
|
||||||
re-flooding at commit rather than from a pre-computed staged set.
|
|
||||||
`CommitTimeRefloodMatchesPrecomputedReflood` (above) is the specific test that turns
|
|
||||||
that from an assumption into evidence. If that test cannot be made to pass, the perf
|
|
||||||
framing is void and the slice needs a behaviour gate.
|
|
||||||
|
|
||||||
**Explicitly not a workaround.** Per the no-workarounds rule, note what this is *not*: no
|
|
||||||
suppression flag, no grace period, no budget loosening, no early-return guard at the
|
|
||||||
symptom. The root cause is an algorithm that is quadratic in resident-world size, and the
|
|
||||||
fix is to make it linear by restoring the per-landblock atomic unit the mechanism had
|
|
||||||
before `6b28ff99`.
|
|
||||||
|
|
||||||
### Measure before and after
|
|
||||||
|
|
||||||
A stripped-after probe should count, per publication: (clone leaves, seal leaves, apply
|
|
||||||
leaves) and wall-clock for each. F4 measured only the clone (median 19,736 / p90 32,135 /
|
|
||||||
max 38,021 leaves, median 3.64 ms, 1,584 preparations = 8.53 s CPU in one 4-minute capped
|
|
||||||
session). The seal was never measured and D2 must beat both. Expected after O3: clone
|
|
||||||
leaves 0, seal leaves ≈ target payload, apply leaves ≈ target payload, total per
|
|
||||||
publication well under 100 µs and flat as the ring fills.
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
# Docs-commit drafts — collision publication-throughput fix (O1/O2/O3)
|
|
||||||
|
|
||||||
> **HISTORICAL DRAFT — DO NOT APPLY WHOLESALE (2026-08-03).** The O1/O2/O3
|
|
||||||
> implementation landed in `71604331` and the user-visible stabilization
|
|
||||||
> fixes continued through `175ad6b0`. This draft predates that work, assigns
|
|
||||||
> issue number #280 to the collision clone even though #280 now canonically
|
|
||||||
> tracks incomplete portal-destination prefetch, and names ledger edits that
|
|
||||||
> must be re-audited against the final production tree. It remains only as
|
|
||||||
> research evidence. Use `NEXT-AGENT-PROMPT.md`, the campaign plan, and the
|
|
||||||
> live divergence register for current work.
|
|
||||||
|
|
||||||
Drafted per contract; NOT applied to the repo. Apply in the docs commit after
|
|
||||||
code review. Register judgment executed as pinned: AD-6 stays retired with a
|
|
||||||
successor note; the residual timing/order compression gets a NEW row (AD-62).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. `docs/architecture/retail-divergence-register.md`
|
|
||||||
|
|
||||||
### 1a. Append to the retired ~~AD-6~~ row (line 113), at the end of column 2
|
|
||||||
|
|
||||||
> **Successor note (2026-08-02, collision publication-throughput fix
|
|
||||||
> O1/O2/O3):** the whole-world staging clone, the owner-mutation journal, the
|
|
||||||
> peer-rebase/retirement cursors, and the zero-managed-byte whole-root
|
|
||||||
> transfer this row describes were deleted. The shipped mechanism is now the
|
|
||||||
> per-landblock delta commit this row's retail anchor always pointed at:
|
|
||||||
> admission captures an O(1) empty target-only staging root
|
|
||||||
> (`PhysicsEngine.CollisionStagingBuilder`), the seal enumerates one prefix's
|
|
||||||
> installed keys through the `CollisionWorldState` per-prefix ledgers, and
|
|
||||||
> `PhysicsEngine.CommitLandblockReplacement` drains the sealed delta into the
|
|
||||||
> ACTIVE root in one synchronous update-thread call, recalculating every
|
|
||||||
> associated owner's cross-cells against the live world
|
|
||||||
> (`ShadowObjectRegistry.ApplyCommittedOwnerReplacement` +
|
|
||||||
> `RefloodPrefixOwnersAfterReplacement`; retail `CObjCell::init_objects`
|
|
||||||
> 0x0052b420 → `CPhysicsObj::recalc_cross_cells` 0x00515a30). Equivalence is
|
|
||||||
> pinned by `CommitTimeRefloodMatchesPrecomputedReflood`; world-size
|
|
||||||
> independence by `CollisionPreparationCostIsIndependentOfResidentWorldSize`.
|
|
||||||
> Residual timing/order compression vs retail: AD-62.
|
|
||||||
|
|
||||||
### 1b. New row AD-62 (residual timing/order compression), adaptation class
|
|
||||||
|
|
||||||
| AD-62 | **Adaptation.** Commit-time collision reflood granularity/order: retail runs `CObjCell::init_objects` per CELL at cell hydration and `CPhysicsObj::recalc_cross_cells` per object as each cell loads; acdream runs the equivalent once per LANDBLOCK replacement inside the single synchronous activation call, walking the sealed owner list then the live prefix-owner slots (per-landblock granularity matches the streaming unit, same compression `ShadowObjectRegistry.RefloodLandblock` has always carried). An owner becoming target-associated mid-publication refloods at activation (the prefix-slot sweep) rather than at its own cell's hydration instant; a stationary owner adjacent to the target whose flood would only change through building/EnvCell bridges can carry frame-stale cross-cells between the seal capture and the activation sweep (movers self-heal per `SetPositionInternal`). | `src/AcDream.Core/Physics/PhysicsEngine.cs` (`CommitLandblockReplacement`); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`ApplyCommittedOwnerReplacement`, `RefloodPrefixOwnersAfterReplacement`) | Late/stale cross-cell rows for a non-moving seam object for a few frames around a landblock publication — an object collidable through a wall seam or briefly not collidable where new topology landed | Low | `CObjCell::init_objects` 0x0052b420; `CPhysicsObj::recalc_cross_cells` 0x00515a30; `CPhysicsObj::SetPositionInternal` tail 0x00515330 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. `claude-memory/project_physics_collision_digest.md` — DO-NOT-RETRY additions
|
|
||||||
|
|
||||||
> - **Do not re-introduce a whole-world staging clone for collision
|
|
||||||
> generations.** The atomic unit is the landblock delta applied in one
|
|
||||||
> update-thread call (`PhysicsEngine.CommitLandblockReplacement`); the
|
|
||||||
> runtime is single-threaded and a root swap buys nothing. The clone made
|
|
||||||
> ring load O(N²) (the C3c late-monster-pop-in / stuck-portal-space soak
|
|
||||||
> failure, issue #280). Deleted 2026-08-02.
|
|
||||||
> - **Batching N staging-clone leaves per step is not a fix** — measured 1.8×
|
|
||||||
> at N=256, not convergence, and it trips the committed one-work-unit seal
|
|
||||||
> invariant. The fix was removing the clone, not tuning it.
|
|
||||||
|
|
||||||
## 3. `docs/research/2026-07-31-atomic-collision-generation.md` — update
|
|
||||||
|
|
||||||
Add a banner at the top:
|
|
||||||
|
|
||||||
> **SUPERSEDED IN PART (2026-08-02).** Steps 2 (whole-world staging clone), 3
|
|
||||||
> (owner-mutation journal write-through), 5 (journal coalescing/compaction), 6
|
|
||||||
> (peer rebases), 7 (draft retirement cursors), and 8 (whole-root transfer)
|
|
||||||
> describe machinery deleted by the collision publication-throughput fix
|
|
||||||
> (O1/O2/O3). The atomicity requirement they served — "the multi-frame build
|
|
||||||
> must not be observable" — is now met by one synchronous per-landblock delta
|
|
||||||
> apply under prefix quiescence with commit-time owner refloods against the
|
|
||||||
> live world (retail init_objects → recalc_cross_cells). The admission
|
|
||||||
> fairness half (quiescence, prefix mutation permissions, ordered activation)
|
|
||||||
> is unchanged and still accurate. Deterministic-evidence entries that name
|
|
||||||
> the journal/rebase/retirement tests refer to tests deleted with the
|
|
||||||
> machinery; their replacements are
|
|
||||||
> `CollisionPreparationCostIsIndependentOfResidentWorldSize`,
|
|
||||||
> `CommitAppliesOneLandblockDeltaInASingleCall`, and
|
|
||||||
> `CommitTimeRefloodMatchesPrecomputedReflood`.
|
|
||||||
|
|
||||||
## 4. `memory/project_collision_port.md`
|
|
||||||
|
|
||||||
Remove/replace the 37-line block `6b28ff99` added (the starvation-free clone
|
|
||||||
description) with a pointer to the new mechanism (same content as 1a).
|
|
||||||
|
|
||||||
## 5. `docs/ISSUES.md` — file the regression as its own issue
|
|
||||||
|
|
||||||
> - **#280 — OPEN → fixed pending review: collision staging clone made ring
|
|
||||||
> load O(N²)** (filed 2026-08-02). The per-publication whole-world staging
|
|
||||||
> clone (be94bc9b synchronous, 6b28ff99 metered) plus the seal's full-map
|
|
||||||
> scans cost ~2-3× resident world per landblock publication, so an
|
|
||||||
> N-landblock ring cost O(N²) and the far ring never converged. F4 measured
|
|
||||||
> median 19,736 leaves / 3.64 ms per publication; C3c made it user-visible
|
|
||||||
> (late monster pop-in, extended/stuck portal space, portal-exit pop-in,
|
|
||||||
> nine-stop soak failure 20260802-143157) but did not cause it. Fix: O1
|
|
||||||
> per-prefix installed-key ledger; O2 per-landblock delta commit
|
|
||||||
> (restores be94bc9b's O(changed) apply); O3 empty staging root +
|
|
||||||
> commit-time reflood (retail init_objects → recalc_cross_cells) + journal/
|
|
||||||
> rebase/retirement machinery deleted. Reference the O1/O2/O3 commits here
|
|
||||||
> when they land.
|
|
||||||
|
|
||||||
## 6. Milestones/roadmap
|
|
||||||
|
|
||||||
No phase-table change needed: this is Modern Runtime infrastructure follow-up
|
|
||||||
inside the active campaign context; the C3c smoke-test findings list in
|
|
||||||
ISSUES (#278 additions) should get items (d)/(e)/(f)-class re-observed after
|
|
||||||
the soak gate passes.
|
|
||||||
|
|
||||||
## 7. Commit-message notes for the slice commits
|
|
||||||
|
|
||||||
- O1: `fix(physics): #280 O1 - per-prefix installed-key ledger; seal scans and
|
|
||||||
landblock removals become O(prefix keys)` — behavior-identical; new test
|
|
||||||
CollisionSealWorkIsIndependentOfResidentWorldSize.
|
|
||||||
- O2: `fix(physics): #280 O2 - restore per-landblock delta commit (be94bc9b
|
|
||||||
shape) at PhysicsEngine.CommitLandblockReplacement` — notes: staging-slot
|
|
||||||
owner-list widening (direct-staged owners), staging-root revoke, zero-byte
|
|
||||||
commit asserts → O(target payload) bounds (commit-time reflood + dictionary
|
|
||||||
node inserts allocate; world-size independence pinned by the O3 test).
|
|
||||||
- O3: `fix(physics): #280 O3 - empty staging root; commit-time reflood
|
|
||||||
(init_objects → recalc_cross_cells); delete journal/rebase/retirement
|
|
||||||
machinery` — 9 mechanism tests deleted, 2 contract tests added.
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
# Grep sweep — deleted collision machinery (2026-08-02, against the WIP O1-O3 tree)
|
|
||||||
|
|
||||||
Produced by an adversarial-review subagent (completed after the review
|
|
||||||
round was halted). Verdict summary: the deletions are CLEAN — no
|
|
||||||
surviving consumer of the journal/peer-rebase/draft-retirement/staging-
|
|
||||||
clone machinery, and no post-`Revoke()` dereference path found. Two
|
|
||||||
actionable leftovers and a stale-docs catalog for whoever lands the
|
|
||||||
collision work.
|
|
||||||
|
|
||||||
## Actionable
|
|
||||||
|
|
||||||
1. `CollisionWorldStateSlot.TransferTo` (`CollisionWorldState.cs:270-281`)
|
|
||||||
is fully DEAD — zero callers incl. tests. Delete it (comments at
|
|
||||||
`PhysicsEngine.cs:311/:379` reference it historically and are
|
|
||||||
accurate).
|
|
||||||
2. Two test names are stale terminology with live bodies:
|
|
||||||
`RuntimePhysicsStateTests.cs:1730`
|
|
||||||
(`PostCommitOwnerMutationWinsOverQueuedPeerRebase`) and `:2264`
|
|
||||||
(`PendingOrActivePeerRebaseCannotResurrectRetiredLandblock`) — rename
|
|
||||||
when touched.
|
|
||||||
|
|
||||||
## Confirmed DEAD (zero refs in src/tests/tools/docs)
|
|
||||||
|
|
||||||
- `LandblockRetirementCursor`/`Step`/`CreateLandblockRetirementCursor`
|
|
||||||
- `CollisionStagingBuilder.Advance/.WorkUnits/.Completed/.SuppressLandblock`
|
|
||||||
- `CopyOneOutsideTarget`
|
|
||||||
- Owner journal: `_collisionOwnerJournal`, `EnqueueCommittedRebase`,
|
|
||||||
write-through, draft retirement
|
|
||||||
|
|
||||||
## Confirmed LIVE (name overlap, different concept — do not "clean up")
|
|
||||||
|
|
||||||
- `InstallLandblockClone` (`PhysicsEngine.cs:576,673`) — the per-landblock
|
|
||||||
delta-apply installer, not the old clone loop.
|
|
||||||
- `MirrorOwnerFrom` (`ShadowObjectRegistry.cs:1961,2011,2079`) —
|
|
||||||
repurposed for the O3 commit-time reflood.
|
|
||||||
- `OwnerMutated`/`OwnerPrefixMembershipChanged` events — general-purpose,
|
|
||||||
unrelated to the deleted journal.
|
|
||||||
- `RetryDeferred` (`RuntimeSetPositionState.cs:4103` et al.) — the
|
|
||||||
deferred-SetPosition subsystem, unrelated.
|
|
||||||
- `LandblockReplacementBuilder` `.WorkUnits/.Advance/.Completed` — the
|
|
||||||
live seal builder, not the deleted staging builder.
|
|
||||||
|
|
||||||
## Post-Revoke audit
|
|
||||||
|
|
||||||
`Revoke()` has exactly one call site (`PhysicsEngine.cs:381`, end of
|
|
||||||
`CommitLandblockReplacement`). `MarkCommitted` (`RuntimePhysicsState.cs:
|
|
||||||
372-380`) + `LandblockPhysicsPublisher.cs:505/:1177-1183` guards mean no
|
|
||||||
production or test site dereferences a revoked slot. `prepared.Engine`/
|
|
||||||
`DataCache` remain unguarded by design (ObjectDisposedException is the
|
|
||||||
intended revoked behavior).
|
|
||||||
|
|
||||||
## Stale docs/comments that now describe the DELETED design (rewrite when
|
|
||||||
landing the collision work)
|
|
||||||
|
|
||||||
- `docs/architecture/acdream-architecture.md:504-566` — full section on
|
|
||||||
journal/write-through/rebase/root-transfer: STALE, needs rewrite to
|
|
||||||
the per-landblock delta commit.
|
|
||||||
- `memory/project_collision_port.md:50-88` — same content class, STALE.
|
|
||||||
- `docs/research/2026-07-31-atomic-collision-generation.md` — whole file
|
|
||||||
documents the deleted mechanism with no supersession note (the
|
|
||||||
prepared banner is in docs-drafts.md).
|
|
||||||
- `src/AcDream.Core/Physics/PhysicsDataCache.cs:126-130` XML doc —
|
|
||||||
describes the deleted one-leaf-per-step materialization.
|
|
||||||
- `src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs:621-625`
|
|
||||||
comment — "zero-work root transfer" no longer true.
|
|
||||||
- Correctly archival (no action): `retail-divergence-register.md:113`
|
|
||||||
(~~AD-6~~ retired entry); the placement-cutover plan + C3c closeout
|
|
||||||
(they name the clone as the known problem being fixed).
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
# P1 — origin-recenter retirement-receipt loop
|
|
||||||
|
|
||||||
## Observed failure
|
|
||||||
|
|
||||||
`launch-feeltest-oclone.log` contains 243 consecutive failures with this
|
|
||||||
shape:
|
|
||||||
|
|
||||||
```text
|
|
||||||
streaming: origin-recenter preparation will resume:
|
|
||||||
InvalidOperationException: Landblock 0xC85AFFFF already has a full
|
|
||||||
retirement receipt.
|
|
||||||
```
|
|
||||||
|
|
||||||
The stack is `StreamingController.TryAdvanceOriginRecenterPreparation` →
|
|
||||||
`LandblockPresentationPipeline.DetachAllForOriginRecenter` →
|
|
||||||
`LandblockRetirementCoordinator.AdoptDetachedFull`.
|
|
||||||
|
|
||||||
## Root cause
|
|
||||||
|
|
||||||
An ordinary full retirement detaches every landblock-owned presentation
|
|
||||||
resource first, then parks surviving live entities in
|
|
||||||
`GpuWorldState._pendingByLandblock` while the exact cleanup ticket advances
|
|
||||||
asynchronously (`GpuWorldState.DetachLandblock`, around lines 1188–1314).
|
|
||||||
|
|
||||||
The origin-recenter swap incorrectly treated every pending-only live bucket
|
|
||||||
as another landblock presentation generation (`GpuWorldState.cs`, former
|
|
||||||
lines 1352–1353). It therefore emitted a second full cleanup receipt for the
|
|
||||||
same already-retired generation. `LandblockRetirementCoordinator` correctly
|
|
||||||
rejected that duplicate at lines 416–425. Because spatial detachment had
|
|
||||||
already committed, the broad retry catch in
|
|
||||||
`StreamingController.TryAdvanceOriginRecenterPreparation` then repeated the
|
|
||||||
detach against the changed state every frame.
|
|
||||||
|
|
||||||
The pre-fix regression test
|
|
||||||
`OriginRecenterAdoption_PendingOnlyLiveProjectionDoesNotCreateSecondFullReceipt`
|
|
||||||
failed because the recenter returned one receipt for the pending-only bucket.
|
|
||||||
|
|
||||||
## Retail and reference boundary
|
|
||||||
|
|
||||||
Retail destroys one concrete landblock owner synchronously:
|
|
||||||
`CLandBlock::destroy_static_objects` (`0x0052FA50`) leaves and deletes the
|
|
||||||
landblock's static objects; `CLandBlock::Destroy` (`0x0052FAA0`) releases its
|
|
||||||
buildings and landblock data; `CLandBlock::release_all` (`0x0052FCF0`)
|
|
||||||
releases the landblock's object and visibility ownership. A live object
|
|
||||||
parked outside a loaded landblock is not a second `CLandBlock` and therefore
|
|
||||||
cannot create a second landblock-destruction transaction.
|
|
||||||
|
|
||||||
The extracted WorldBuilder reference follows the same ownership boundary:
|
|
||||||
`ObjectRenderManagerBase` removes an actual `_landblocks` entry before
|
|
||||||
`UnloadLandblockResources`, and `PortalRenderManager` only unloads a removed
|
|
||||||
`PortalLandblock`. Neither treats an independently parked object as a new
|
|
||||||
landblock resource owner.
|
|
||||||
|
|
||||||
Acdream retains its approved asynchronous adaptation: the first exact
|
|
||||||
receipt owns cleanup, while the live projection survives spatial recentering.
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
- `GpuWorldState.DetachAllForOriginRecenter` no longer creates retirement
|
|
||||||
receipts from `_pendingByLandblock` alone. Pending live identities are
|
|
||||||
still captured from `_projectionLocations`, cleared atomically, and
|
|
||||||
re-parked unchanged.
|
|
||||||
- A landblock that also owns loaded, pending-render, pending-near, tier, or
|
|
||||||
bounds state still receives its exact full receipt.
|
|
||||||
- A receipt-ledger invariant thrown after spatial detachment is now surfaced
|
|
||||||
as a committed `StreamingMutationException`; it is terminal rather than
|
|
||||||
falsely logged as resumable work.
|
|
||||||
- The genuine duplicate-receipt guard remains unchanged.
|
|
||||||
|
|
||||||
## Deterministic evidence
|
|
||||||
|
|
||||||
- The new pending-only regression failed before the source fix and passes
|
|
||||||
afterward.
|
|
||||||
- `OriginRecenter_PendingOnlyLiveProjectionKeepsItsExistingRetirementOwner`
|
|
||||||
drives the production recenter/controller sequence and proves the origin
|
|
||||||
commits while the first cleanup ticket remains pending.
|
|
||||||
- `OriginRecenter_CommittedReceiptInvariantFailsFastInsteadOfReplayingDetach`
|
|
||||||
proves a genuine post-detach ledger violation surfaces once rather than
|
|
||||||
entering a frame-by-frame retry loop.
|
|
||||||
- The complete `OriginRecenter` focused group passes 20/20.
|
|
||||||
|
|
||||||
## Gate evidence
|
|
||||||
|
|
||||||
- Release build: 0 errors (21 pre-existing warnings).
|
|
||||||
- Complete Release suite: 10,815 passed, 0 failed, 4 skipped.
|
|
||||||
- Connected lifecycle/reconnect gate:
|
|
||||||
`logs/connected-world-gate-20260802-203751/report.json` — `Passed=true`.
|
|
||||||
- Connected nine-stop soak:
|
|
||||||
`logs/connected-r6-soak-20260802-204309.report.json` — `Passed=true`,
|
|
||||||
`Failures=[]`, graceful exit, all 9 canonical checkpoints present, no wait
|
|
||||||
cue, no pending landblock retirement, no reveal invariant failure, and no
|
|
||||||
render-shadow mismatch.
|
|
||||||
- The soak artifacts contain zero occurrences of
|
|
||||||
`already has a full retirement receipt`; the captured failing session had
|
|
||||||
243.
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# User feel-test observations — O-slice tree (2026-08-02 ~20:10, uncommitted)
|
|
||||||
|
|
||||||
Axioms; they override the gate numbers. Log: launch-feeltest-oclone.log.
|
|
||||||
|
|
||||||
1. MONSTERS STILL POP IN while running past — the O-slice did NOT fix the
|
|
||||||
user-visible symptom despite the soak's publication convergence.
|
|
||||||
2. MONSTERS SPAWNED MID-AIR far ahead at a newly-entered area.
|
|
||||||
3. STATICS ("stabs") PLACED INCORRECTLY — visibly wrong static placement.
|
|
||||||
4. User: "This is not how retail worked. I could see monsters way in
|
|
||||||
front of me."
|
|
||||||
5. DOOR APPROACH REGRESSED: using a door no longer walks the character
|
|
||||||
to it first. NOTE: likely a COMMITTED C3c regression, not O-slice —
|
|
||||||
prime suspect is PlayerModeController's conditional MoveTo bind
|
|
||||||
(`if (controller.MoveTo is { } moveTo)` — the flip only binds
|
|
||||||
approach callbacks IF the Runtime-owned MoveToManager already exists;
|
|
||||||
the legacy path CREATED it via factory at attach). If Runtime's
|
|
||||||
MakeMoveToManager runs after player-mode attach (or never for this
|
|
||||||
flow), MoveToComplete/approach never wires. Triage first in the C3c
|
|
||||||
fix slice; check whether the 175401 gate probe ever exercised a
|
|
||||||
door/use-approach (suspect: no coverage).
|
|
||||||
|
|
||||||
SMOKING GUN (log): 243x "streaming: origin-recenter preparation will
|
|
||||||
resume: System.InvalidOperationException: Landblock <id> already has a
|
|
||||||
full retirement receipt." — continuous catch-retry loop during origin
|
|
||||||
recenter. Both reviewers redirected with this; the implementer's
|
|
||||||
"exposed pre-existing" retirement classification is under re-judgment.
|
|
||||||
The catch-and-resume wrapper is itself suspect as a pre-existing
|
|
||||||
symptom-swallower (no-silent-catch rule).
|
|
||||||
|
|
||||||
STATUS: O-slice commit ON HOLD until every observation is explained.
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
# User in-game observations — 2026-08-02 (~15:40, during the F4 diagnostic run)
|
|
||||||
|
|
||||||
Axioms per the retail-oracle rule. User will do a full test session once
|
|
||||||
the current work passes; these are the pre-session signals.
|
|
||||||
|
|
||||||
1. AIRBORNE-WHILE-STANDING (severe, flip-suspect): repeated
|
|
||||||
"[System] You can't do that while in the air!" +
|
|
||||||
"You can't do that. (error 0x042C)" x4 + one "WeenieError 0x001D" when
|
|
||||||
trying to cast while standing still. Suspect: the conductor placement
|
|
||||||
path lacks the legacy spawn path's #270 settle sweep (contact from the
|
|
||||||
compressed first gravity frame) -> outbound contact state says
|
|
||||||
airborne. Routed to F4 as a lead (unified-hypothesis check); if F4's
|
|
||||||
stuck item is not the player, this becomes its own slice (F5) BEFORE
|
|
||||||
the C3c commit — casting is core gameplay and blocks the smoke test.
|
|
||||||
2. MATERIALIZATION HAZE RE-FIRING while standing still (flip-suspect):
|
|
||||||
purple haze re-triggers around the character. Plausibly the visible
|
|
||||||
face of the soak's pendingPublications=1 stuck item if that item is
|
|
||||||
the local player. Routed to F4.
|
|
||||||
3. NO SLIDE ALONG IMPASSABLE SLOPES: walking into too-steep terrain does
|
|
||||||
not glide laterally. Likely pre-existing open issue #269 (Campaign P
|
|
||||||
slope-slide residual). Verify pre-existence during the review/closeout;
|
|
||||||
do not fold into C3c unless evidence says the flip changed it.
|
|
||||||
4. /ls DOES NOT WORK: unclear which command surface (chat slash command?).
|
|
||||||
Triage at the session; low priority.
|
|
||||||
|
|
||||||
Review-focus implication: retail reviewer must verify the flip preserves
|
|
||||||
the legacy spawn path's contact seeding (#270) semantics; adversarial
|
|
||||||
reviewer must verify the placement publication for the local player
|
|
||||||
actually completes and is reaped.
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
# Runtime initial-placement continuation executor handoff - 2026-08-02
|
|
||||||
|
|
||||||
## Purpose and exact stopping point
|
|
||||||
|
|
||||||
Behavior commit `5db3de3c7ab2c6350d11af7f34b852464fc1e0f9` implements the
|
|
||||||
Runtime continuation executor: the missing mechanism that, once an entity's
|
|
||||||
initial authored placement is acknowledged, adopts that placement exactly
|
|
||||||
once, applies the retail Create tail, replays deferred missing-parent work,
|
|
||||||
and drains the admission checkpoint's mixed continuation FIFO in exact
|
|
||||||
arrival order with retail route decisions made at execution time. The
|
|
||||||
residence system built by `38fd4b8d` (residence/FIFO) and `30012361`
|
|
||||||
(admission) is now COMPLETE as a mechanism: an entity can enter the world
|
|
||||||
through it and every packet accepted while its placement was pending is
|
|
||||||
applied exactly once, in order, with retail semantics.
|
|
||||||
|
|
||||||
This checkpoint deliberately does NOT cut the graphical or headless
|
|
||||||
production routes over — `RuntimeLiveEntitySessionController.cs` (headless)
|
|
||||||
and App's `LiveEntityRuntime` still call legacy `RegisterEntity`, and no host
|
|
||||||
calls `Execute`. It does not begin AP-22 or AD-10 and does not retire
|
|
||||||
AP-1/AD-1. The executor is exercised by deterministic Runtime tests only, so
|
|
||||||
no connected visual gate was required.
|
|
||||||
|
|
||||||
This file supersedes the executor-boundary portions of
|
|
||||||
[`2026-08-01-runtime-initial-placement-admission-handoff.md`](2026-08-01-runtime-initial-placement-admission-handoff.md).
|
|
||||||
|
|
||||||
## Exact workspace and Git state
|
|
||||||
|
|
||||||
- Worktree: `C:\Users\erikn\.codex\worktrees\af5e\acdream`
|
|
||||||
- Branch: `codex/port-claude-agents`
|
|
||||||
- Behavior checkpoint: `5db3de3c7ab2c6350d11af7f34b852464fc1e0f9`
|
|
||||||
- Documentation checkpoint: the commit containing this file
|
|
||||||
- The same eight unrelated dirty paths as the admission handoff remain
|
|
||||||
intentionally unstaged; never stage by blanket.
|
|
||||||
- No push or merge is part of this checkpoint.
|
|
||||||
|
|
||||||
## What the executor owns
|
|
||||||
|
|
||||||
`RuntimeInitialCreateContinuationExecutor` (constructed inside
|
|
||||||
`RuntimeEntityObjectLifetime` beside the residence state; internal
|
|
||||||
`InitialCreateExecution`; generation bound through `BindEventContext`) owns,
|
|
||||||
per exact `RuntimeEntityKey` + lease:
|
|
||||||
|
|
||||||
- the synchronous, retry-idempotent `Execute` transaction:
|
|
||||||
`Complete` -> `AdoptCompletedPlacement` (consumes the acknowledged initial
|
|
||||||
placement exactly once, resolving the `HasRetainedCompletion` deadlock so
|
|
||||||
later authored placements for the key can begin, with `PlacementAdopted`
|
|
||||||
keeping the completed entry current) -> AfterEnterWorld hook request
|
|
||||||
(local player, exactly once) -> deferred replay -> strict-sequence FIFO
|
|
||||||
drain -> `ConsumeExecuted` release (adoption-revision-checked; `Revised`
|
|
||||||
re-drains only the tail);
|
|
||||||
- per-continuation applies through gate-less instance seams on
|
|
||||||
`InboundPhysicsStateController` (`ApplyAccepted*Snapshot`) that read and
|
|
||||||
write the ONE snapshot store — the legacy fused paths are re-expressed as
|
|
||||||
gate + the same shared merge bodies, so there is no drift and no second
|
|
||||||
canonical snapshot;
|
|
||||||
- execution-time Position routing via
|
|
||||||
`RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`
|
|
||||||
with live inputs: the retained wire packet's own `IsGrounded` bit as the
|
|
||||||
server-asserted contact (never the local body), the data-driven
|
|
||||||
`(MotionTableId ?? Physics?.MotionTableId)` animation proxy (AP-130), live
|
|
||||||
distance/`UsePositionFromServer`, and the record's committed cell — driving
|
|
||||||
authored placements for SetPosition routes through the canonical
|
|
||||||
`RuntimeSetPositionState` Begin/Watch/resume lifecycle with a retryable
|
|
||||||
`AwaitingContinuationPlacement` yield (a contention flavor with no pending
|
|
||||||
token means "retry Execute later");
|
|
||||||
- atomic `SameIncarnationCreate` envelopes: per-stage index idempotency,
|
|
||||||
buffered publication flushed in stage order after the final stage (AD-59),
|
|
||||||
the object-table apply via the accepted-spawn seam (result observed; a
|
|
||||||
nested replacement abandons), and the three-branch resident-cell
|
|
||||||
disposition (TS-63);
|
|
||||||
- missing-parent replay, both flavors, keyed by parent GUID exactly as
|
|
||||||
retail's `QueueBlobForObject`/`ProcessObjectNetBlobs`: raw child Creates
|
|
||||||
AND queued accepted parent relations, drained in the initial tail with
|
|
||||||
whole-bucket atomic detach, per-entry exception containment
|
|
||||||
(`ReplayFailureCount`/`LastReplayFailure`), typed outcomes
|
|
||||||
(Registered/ReDeferred/Rejected; ParentApplied/DeferredAwaitingParent/
|
|
||||||
DiscardedStaleParent), and cancellation-aware restore windows whose tokens
|
|
||||||
record every cancellation fired while a batch is detached (ABA-safe;
|
|
||||||
a cleared token restores nothing);
|
|
||||||
- the field-masked executor baseline: each apply re-syncs ONLY the tracked
|
|
||||||
fields its own mutations moved, before publication, so external mutations
|
|
||||||
are detected in every quiet window and publish-callback;
|
|
||||||
- one shared abandonment routine on every non-retryable exit: forgets any
|
|
||||||
pending continuation placement (cancellation published), retires the
|
|
||||||
residence through the lifetime choke point, discards progress, returns a
|
|
||||||
typed status — the combined ownership ledger (residences, executor
|
|
||||||
progress, deferred buckets, replay windows, placement watches) converges,
|
|
||||||
and residence retirement notifies the executor
|
|
||||||
(`BindRetirementNotification`);
|
|
||||||
- an ordered immutable execution receipt/trace carrying every fact a cutover
|
|
||||||
host needs: per-action kind/sequence/stage, Position route facts
|
|
||||||
(disposition, constrain phase, teleport-hook phase, stop-interpolation,
|
|
||||||
zero-velocity, preserve-heading, send-position-immediately, unparent),
|
|
||||||
replay outcomes, and resident-cell dispositions.
|
|
||||||
|
|
||||||
## Retail anchors proven this slice
|
|
||||||
|
|
||||||
Beyond the admission handoff's eight anchor functions:
|
|
||||||
|
|
||||||
- retail's local-ordinary interpolate gate is
|
|
||||||
`UsePositionFromServer && wire-contact` — `PositionPack` bit 0x4 →
|
|
||||||
`has_contact` (pseudo-C 284654) → `UnpackPositionEvent` arg5 (93092) →
|
|
||||||
the gate at 93044. The earlier research note's "isForce" reading was a
|
|
||||||
misnomer disproven during review; the shipped classifier was correct.
|
|
||||||
- `ProcessObjectNetBlobs` detaches the whole per-GUID bucket before
|
|
||||||
dispatching (93617 → 93649) — mirrored by the detach/restore windows.
|
|
||||||
- missing-parent relations are QUEUED by parent GUID (standalone parent
|
|
||||||
handler 0x004535D0: lookup 92312, queue 92326; `QueueBlobForObject`
|
|
||||||
0x005092D0's GUID-keyed placeholder bucket 271082-271088) — never
|
|
||||||
discarded; the round-4 discard was overturned on this evidence.
|
|
||||||
- `HandleReceivedPosition`'s `HasAnims` gate (92992) is animation-queue
|
|
||||||
presence (`CSequence::has_anims` = non-empty list), anchoring AP-130.
|
|
||||||
- the same-incarnation tail order and resident-cell cleanup
|
|
||||||
(93865..93943) are mirrored stage-for-stage, with the claimedCell==0
|
|
||||||
destruction branch proven structurally unreachable for admitted envelopes
|
|
||||||
(every envelope carries a WeenieDescription by shape).
|
|
||||||
|
|
||||||
## Divergence register
|
|
||||||
|
|
||||||
Rows filed in the behavior commit: **AD-59** (envelope buffered live-record
|
|
||||||
events), **AD-60** (executor canonical cell semantics — wire positions never
|
|
||||||
directly commit residency), **AP-130** (HasAnims MotionTableId proxy),
|
|
||||||
**AP-131** (legacy Position merge's unconditional placement-frame/parent-
|
|
||||||
clear flags — retired by construction at cutover), **AP-132** (parent
|
|
||||||
incarnation gating vs retail's pointer-only GUID replay), **TS-62** (no live
|
|
||||||
ConstrainTo binding in the dormant slice — trace-only), **TS-63**
|
|
||||||
(resident-cell abandonment/delegation split). AP-1 and AD-1 remain open
|
|
||||||
until the cutover. Issue **#275** tracks the post-cutover unification of the
|
|
||||||
legacy Position path onto the classifier.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
- Focused executor/residence/classifier gate: **161/161**.
|
|
||||||
- Complete Runtime project: **903/903** (829 baseline + 74 slice tests).
|
|
||||||
- Complete Release solution: **10,696 passed / 4 intentional skips / 0
|
|
||||||
failed** (`-m:1`, installed `acdream.pak`); Release build 0 errors,
|
|
||||||
21 pre-existing test-project warnings.
|
|
||||||
- `git diff --check` clean; the eight unrelated dirty paths untouched.
|
|
||||||
- Independent reviews (both read-only, both required to PASS): the
|
|
||||||
retail-conformance reviewer and the architecture/adversarial reviewer each
|
|
||||||
ran four passes across five implementation rounds. Finding classes fixed
|
|
||||||
at root cause along the way: wire-vs-body contact source; two-store
|
|
||||||
snapshot divergence; WeenieDescription wholesale-overwrite; non-converging
|
|
||||||
abandonment; reentrant mid-drain residence retirement; the
|
|
||||||
acknowledged-completion leak that would have blocked all future placements
|
|
||||||
for a key; per-field baseline blessing; replay exception containment and
|
|
||||||
detached-batch resurrection; and the stale-parent discard overturned in
|
|
||||||
favor of retail's queue-by-parent-GUID replay. Final verdicts: RETAIL
|
|
||||||
REVIEW: PASS; ARCHITECTURE REVIEW: PASS (three residual NOTEs, all
|
|
||||||
defense-in-depth observations, none blocking).
|
|
||||||
|
|
||||||
## Production routes intentionally unchanged
|
|
||||||
|
|
||||||
Graphical Create still flows through `LiveEntityRuntime.RegisterEntity`;
|
|
||||||
headless still uses `RuntimeLiveEntitySessionController`'s legacy
|
|
||||||
`RegisterEntity`; no production code calls
|
|
||||||
`RegisterEntityWithInitialResidence` or `Execute`. The residence+executor
|
|
||||||
system is a complete, reviewed, dormant mechanism awaiting the cutover.
|
|
||||||
|
|
||||||
## Next implementation boundary — the production cutover
|
|
||||||
|
|
||||||
Route graphical AND headless registration through the residence+executor
|
|
||||||
owner together, then every Create, Position, ForcePosition, Parent, Pickup,
|
|
||||||
withdrawal, delete, remote-movement, projectile-correction, and dropped-item
|
|
||||||
edge through the same transaction. Hosts project immutable Runtime results
|
|
||||||
only; they may not resolve a second placement or create another body. Delete
|
|
||||||
the legacy duplicate paths only after parity tests pass (this retires AP-131
|
|
||||||
and closes #275 by construction). Run the exact lifecycle/reconnect and
|
|
||||||
canonical nine-stop connected routes, two-client observation, and the user
|
|
||||||
visual matrix. Only then retire AP-1 and AD-1.
|
|
||||||
|
|
||||||
Cutover-specific notes from this slice:
|
|
||||||
|
|
||||||
- The execution receipt carries every route fact a host must bind — the
|
|
||||||
constrain phases and stop-interpolation/zero-velocity flags (TS-62), the
|
|
||||||
teleport-hook phases, and the send-position-immediately echo.
|
|
||||||
- `AwaitingContinuationPlacement` has two flavors: pending token (host must
|
|
||||||
prepare/submit/acknowledge the placement, then retry Execute) and
|
|
||||||
contention (no pending token; retry Execute after the conflicting
|
|
||||||
operation resolves).
|
|
||||||
- The dormant placement path's 1,880-bytes/operation allocation budget
|
|
||||||
(2,048 cap) remains the standing 4B2 activation blocker for
|
|
||||||
frame-frequency traffic; resolve or budget it before the cutover routes
|
|
||||||
high-frequency Position traffic through the owner.
|
|
||||||
|
|
||||||
After the cutover: **AP-22** (authored collision shapes;
|
|
||||||
`ShadowShapeBuilder` sole authority), then **AD-10** (remote contact-plane
|
|
||||||
projection), then the final automated + connected matrix and ledger
|
|
||||||
synchronization close the campaign; vendor Slice 5 resumes after.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git revert 5db3de3c7ab2c6350d11af7f34b852464fc1e0f9
|
|
||||||
```
|
|
||||||
|
|
||||||
The documentation checkpoint containing this file is separate and may be
|
|
||||||
reverted independently. Do not revert the `38fd4b8d`/`30012361` foundation
|
|
||||||
beneath it without a separately proven defect.
|
|
||||||
|
|
||||||
## Resume checklist
|
|
||||||
|
|
||||||
1. Continue in the exact worktree/branch above; confirm `git log` contains
|
|
||||||
`5db3de3c` and the documentation commit containing this file.
|
|
||||||
2. Preserve the eight unrelated dirty paths; never `git add -A`.
|
|
||||||
3. Read this file, the admission handoff, and
|
|
||||||
`docs/architecture/acdream-architecture.md`.
|
|
||||||
4. Re-run the focused gate before modifying execution code:
|
|
||||||
the Residence + Classifier + Executor filter must report 161/161.
|
|
||||||
5. Begin ONLY the production cutover checkpoint. Do not fold AP-22, AD-10,
|
|
||||||
or vendor work into it.
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
# CPhysicsObj::set_description @ 0x00514F40 — three FPU-elided gates recovered
|
|
||||||
|
|
||||||
## Verification chain
|
|
||||||
|
|
||||||
1. **Binary/PDB pairing**: `py tools/pdb-extract/check_exe_pdb.py "C:/Users/erikn/Downloads/acclient.exe"`
|
|
||||||
→ `=== MATCH: this exe pairs with our acclient.pdb ===` (GUID
|
|
||||||
`9e847e2f-777c-4bd9-886c-22256bb87f32`, linker timestamp
|
|
||||||
2013-09-06T00:17:56Z). Confirmed before any byte reads.
|
|
||||||
2. **PE section mapping** (hand-parsed via a one-off script,
|
|
||||||
`scratchpad/pe_read.py`): image base `0x00400000`;
|
|
||||||
`.text` VA=0x00401000 RawPtr=0x00001000;
|
|
||||||
`.rdata` VA=0x00792000 RawPtr=0x00392000 (holds the FP constants below).
|
|
||||||
VA→file-offset: `file_off = raw_ptr + (VA - image_base - section_virt_addr)`.
|
|
||||||
3. Raw bytes of the function (`0x00514F40`–`0x00515153`) were dumped and
|
|
||||||
hand-disassembled instruction-by-instruction, cross-checked line-by-line
|
|
||||||
against `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines
|
|
||||||
283130–283251 (function body) so every address in the trace lines up
|
|
||||||
with a named pseudo-C statement.
|
|
||||||
4. **Ghidra MCP**: not available this session — no CodeBrowser open on
|
|
||||||
port 8080/8081 (both probes returned empty). Not needed; binary + ACE
|
|
||||||
agreement below is already two independent confirmations.
|
|
||||||
5. **ACE cross-check**: `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs`
|
|
||||||
(main checkout, not the af5e worktree — ACE isn't vendored there),
|
|
||||||
`set_description`, lines 3557–3568. ACE's C# independently reproduces
|
|
||||||
all three predicates exactly as decoded from the binary below. Binary
|
|
||||||
is the ground truth per project policy; ACE here is 100% consistent
|
|
||||||
with it, so no conflict to adjudicate.
|
|
||||||
|
|
||||||
Confidence: **byte-certain** for all three. Every constant was read
|
|
||||||
directly from `.rdata`, every comparison/jump opcode was decoded from
|
|
||||||
the raw instruction stream, and the result matches ACE's independent
|
|
||||||
port line-for-line.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conditional 1 — friction OUTER gate (pseudo-C line 283219, VA 0x0051505a)
|
|
||||||
|
|
||||||
### Bytes
|
|
||||||
|
|
||||||
```
|
|
||||||
0051504f: d9 46 68 FLD DWORD PTR [ESI+0x68] ; ST(0) = (double)esi->friction (PhysicsDesc.friction @ +0x68)
|
|
||||||
00515052: dc 15 10 46 79 00 FCOM QWORD PTR [0x00794610] ; compare ST(0) vs constant, no pop (value reused below)
|
|
||||||
00515058: df e0 FNSTSW AX
|
|
||||||
0051505a: f6 c4 05 TEST AH, 0x05 ; mask = C0(bit0) | C2(bit2)
|
|
||||||
0051505d: 7b 15 JNP 0x00515074 ; jump (skip friction block) iff PF=0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Constant
|
|
||||||
|
|
||||||
VA `0x00794610` (.rdata, file offset `0x00394610`), 8 bytes:
|
|
||||||
`00 00 00 00 00 00 00 00` → **`0.0` (double, exact)**.
|
|
||||||
|
|
||||||
### Decoding the jump
|
|
||||||
|
|
||||||
`TEST AH,0x05` ANDs AH with the C0|C2 status bits, then the parity flag
|
|
||||||
(PF) reflects the parity of that AND result. Case table for
|
|
||||||
`FCOM esi->friction, 0.0` (ST0=friction):
|
|
||||||
|
|
||||||
| relation | C0 | C2 | C3 | AH&0x05 | popcount | PF |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| friction > 0.0 | 0 | 0 | 0 | 0x00 | 0 | 1 |
|
|
||||||
| friction < 0.0 | 1 | 0 | 0 | 0x01 | 1 | **0** |
|
|
||||||
| friction == 0.0 | 0 | 0 | 1 | 0x00 | 0 | 1 |
|
|
||||||
| unordered (NaN) | 1 | 1 | 1 | 0x05 | 2 | 1 |
|
|
||||||
|
|
||||||
`JNP` (jump on PF=0) only fires for the strict `<` case. So the jump
|
|
||||||
(which SKIPS the whole friction reassignment block, landing at the
|
|
||||||
shared cleanup at `0x00515074`) is taken **only when `friction < 0.0`**;
|
|
||||||
every other case (`>= 0.0`, and — as an accepted compiler-quirk
|
|
||||||
edge case irrelevant to real game data — unordered/NaN) falls through
|
|
||||||
into the block.
|
|
||||||
|
|
||||||
### Recovered predicate
|
|
||||||
|
|
||||||
```c
|
|
||||||
// outer gate: proceed to the friction-assignment logic only when friction is non-negative
|
|
||||||
if (esi->friction >= 0.0f) {
|
|
||||||
// ... inner compare (Conditional 2) ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conditional 2 — friction INNER compare (pseudo-C line 283226, VA 0x0051506a)
|
|
||||||
|
|
||||||
### Bytes
|
|
||||||
|
|
||||||
```
|
|
||||||
0051505f: dc 15 c0 28 79 00 FCOM QWORD PTR [0x007928c0] ; compare ST(0)=friction vs constant, no pop
|
|
||||||
00515065: df e0 FNSTSW AX
|
|
||||||
00515067: f6 c4 41 TEST AH, 0x41 ; mask = C0(bit0) | C3(bit6)
|
|
||||||
0051506a: 74 08 JZ 0x00515074 ; jump (skip assignment) iff (AH&0x41)==0
|
|
||||||
0051506c: d9 9f bc 00 00 00 FSTP DWORD PTR [EDI+0xbc] ; this->friction = friction (field @ +0xbc), pops ST(0)
|
|
||||||
```
|
|
||||||
|
|
||||||
Pseudo-C had already fully rendered the C0/C2/C3 synthetic-byte
|
|
||||||
construction for this one (only the final `test ah,0x41`→bool
|
|
||||||
collapse was marked unimplemented), so the byte read is a
|
|
||||||
confirmation rather than a fresh recovery.
|
|
||||||
|
|
||||||
### Constant
|
|
||||||
|
|
||||||
VA `0x007928c0` (.rdata, file offset `0x003928c0`), 8 bytes:
|
|
||||||
`00 00 00 00 00 00 f0 3f` → **`1.0` (double, exact; IEEE-754 bit
|
|
||||||
pattern `0x3FF0000000000000`)**.
|
|
||||||
|
|
||||||
### Decoding the jump
|
|
||||||
|
|
||||||
Mask `0x41` = C0(below) | C3(equal). `JZ` (jump when the TEST result
|
|
||||||
is zero, i.e. neither bit set) skips the assignment when friction is
|
|
||||||
strictly `>` 1.0. Falls through (assigns `this->friction`) when
|
|
||||||
`friction <= 1.0` (below-or-equal family, exactly as flagged in the
|
|
||||||
task). This is the canonical `jbe` idiom.
|
|
||||||
|
|
||||||
### Recovered predicate
|
|
||||||
|
|
||||||
```c
|
|
||||||
// inner compare: only assign if friction also passes the upper bound
|
|
||||||
if (esi->friction <= 1.0f)
|
|
||||||
this->friction = esi->friction;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Combined (conditionals 1+2)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (esi->friction >= 0.0f && esi->friction <= 1.0f)
|
|
||||||
this->friction = esi->friction;
|
|
||||||
```
|
|
||||||
|
|
||||||
This is byte-for-byte what ACE's port does at
|
|
||||||
`PhysicsObj.cs:3557-3558`: `if (desc.Friction >= 0.0f && desc.Friction <= 1.0f) Friction = desc.Friction;`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conditional 3 — translucency gate (pseudo-C line 283240, VA 0x0051509f)
|
|
||||||
|
|
||||||
### Bytes
|
|
||||||
|
|
||||||
```
|
|
||||||
0051508b: d9 44 24 24 FLD DWORD PTR [ESP+0x24] ; ST(0) = (float)translucency (local copy of esi->translucency, field @ esi+0x70)
|
|
||||||
0051508f: d8 1d 80 6a 7c 00 FCOMP DWORD PTR [0x007c6a80] ; compare ST(0) vs constant, WITH pop (single precision, reg field=3)
|
|
||||||
00515095: 8b d1 MOV EDX, ECX
|
|
||||||
00515097: 89 97 b8 00 00 00 MOV [EDI+0xb8], EDX ; this->translucencyOriginal = translucency (unconditional)
|
|
||||||
0051509d: df e0 FNSTSW AX
|
|
||||||
0051509f: f6 c4 44 TEST AH, 0x44 ; mask = C2(bit2) | C3(bit6)
|
|
||||||
005150a2: 7b 15 JNP 0x005150b9 ; jump (skip live-translucency apply) iff PF=0
|
|
||||||
005150a4: ... ; fallthrough: this->translucency = translucency; PartArray propagation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Constant
|
|
||||||
|
|
||||||
VA `0x007c6a80` (.rdata, file offset `0x003c6a80`), 4 bytes:
|
|
||||||
`00 00 00 00` → **`0.0f` (single-precision float, exact)**. Note this
|
|
||||||
compare is single-precision (`d8`/`FCOMP m32`), unlike the two
|
|
||||||
friction compares above which are double-precision (`dc`/`FCOM m64`) —
|
|
||||||
matches the pseudo-C's `((long double)0f)` literal notation (the `f`
|
|
||||||
suffix is BN flagging a float-typed constant) versus `((long
|
|
||||||
double)0.0)` for the friction case.
|
|
||||||
|
|
||||||
### Decoding the jump
|
|
||||||
|
|
||||||
Case table for `FCOMP translucency, 0.0f` (ST0=translucency), mask
|
|
||||||
`0x44` = C2(unordered) | C3(equal):
|
|
||||||
|
|
||||||
| relation | C0 | C2 | C3 | AH&0x44 | popcount | PF |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| translucency > 0.0 | 0 | 0 | 0 | 0x00 | 0 | 1 |
|
|
||||||
| translucency < 0.0 | 1 | 0 | 0 | 0x00 | 0 | 1 |
|
|
||||||
| translucency == 0.0 | 0 | 0 | 1 | 0x40 | 1 | **0** |
|
|
||||||
| unordered (NaN) | 1 | 1 | 1 | 0x44 | 2 | 1 |
|
|
||||||
|
|
||||||
`JNP` (PF=0) fires **only** for the exact-equal-to-zero case. So the
|
|
||||||
jump — which skips applying live `translucency`/PartArray propagation,
|
|
||||||
leaving only the unconditional `translucencyOriginal` write — is taken
|
|
||||||
**only when `translucency == 0.0f`**. Every other case (`>0`, `<0`,
|
|
||||||
and unordered/NaN as a compiler-quirk edge case) falls through and
|
|
||||||
applies.
|
|
||||||
|
|
||||||
### Recovered predicate
|
|
||||||
|
|
||||||
```c
|
|
||||||
// translucencyOriginal is ALWAYS written (this happens before the gate, unconditionally)
|
|
||||||
this->translucencyOriginal = translucency;
|
|
||||||
|
|
||||||
// live translucency + PartArray propagation only when translucency is non-zero
|
|
||||||
if (translucency != 0.0f)
|
|
||||||
{
|
|
||||||
this->translucency = translucency;
|
|
||||||
if (this->part_array != 0)
|
|
||||||
CPartArray::SetTranslucencyInternal(this->part_array, translucency);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Matches ACE's port at `PhysicsObj.cs:3562-3568` exactly:
|
|
||||||
```csharp
|
|
||||||
TranslucencyOriginal = desc.Translucency;
|
|
||||||
if (desc.Translucency != 0.0f)
|
|
||||||
{
|
|
||||||
Translucency = desc.Translucency;
|
|
||||||
if (PartArray != null)
|
|
||||||
PartArray.SetTranslucencyInternal(desc.Translucency);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary table
|
|
||||||
|
|
||||||
| # | Gate | Predicate (apply-when) | Constant | Cert. |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 1 | friction outer | `friction >= 0.0f` | `0.0` (double) @ VA 0x00794610 | byte-certain |
|
|
||||||
| 2 | friction inner | `friction <= 1.0f` | `1.0` (double) @ VA 0x007928c0 | byte-certain |
|
|
||||||
| 3 | translucency | `translucency != 0.0f` | `0.0f` (float) @ VA 0x007c6a80 | byte-certain |
|
|
||||||
|
|
||||||
All three: no unresolved cases. The only caveat on all three is a
|
|
||||||
decompiler/compiler-codegen edge case around NaN (unordered operands
|
|
||||||
fall into the "true"/apply bucket rather than IEEE-strict "always
|
|
||||||
false"), which is a documented quirk of this exact MSVC x87 codegen
|
|
||||||
pattern and not something the retail struct's `float` fields would
|
|
||||||
ever hit in practice (friction/translucency are authored data, never
|
|
||||||
NaN).
|
|
||||||
|
|
||||||
## Port note (C3b, `RuntimeRemoteBodyDescription.cs`)
|
|
||||||
|
|
||||||
acdream's friction port uses `f >= 0.0f && f <= 1.0f`, which deliberately
|
|
||||||
SKIPS NaN rather than reproducing the unordered-goes-to-apply codegen quirk
|
|
||||||
tabled above — the sanctioned modern-boundary deviation this doc
|
|
||||||
pre-declared. Elasticity's setter port (`!(e >= 0f)` first arm) and
|
|
||||||
translucency's `!= 0.0f` gate both route NaN exactly as the binary does
|
|
||||||
(0f and apply respectively); ACE's `set_elasticity` sends NaN to 0.1f and
|
|
||||||
is divergent from the binary on that edge.
|
|
||||||