docs(research): #47 handoff prompt for next-session agent
Self-contained pickup brief for the bulky-humanoid bug. Has: - the bug + acceptance criterion - everything ruled out this session (with evidence) - starting facts confirmed via diagnostics - 4 ranked hypotheses (per-vertex normals → ambient → MSAA → frame composition) with concrete tests for each - diagnostic env vars + their output shapes - the CLAUDE.md grep-named-first workflow - files most likely to need edits - live test workflow (env vars, expected entities in Holtburg) - constraints (don't break drudges / scenery / +Acdream local view) Designed to drop straight into a fresh agent's prompt window.
This commit is contained in:
parent
e697a9ad1e
commit
8d7cad5b14
1 changed files with 287 additions and 0 deletions
287
docs/research/2026-05-06-issue-47-handoff.md
Normal file
287
docs/research/2026-05-06-issue-47-handoff.md
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
# Issue #47 handoff — humanoid Setup 0x02000001 renders bulky vs retail
|
||||||
|
|
||||||
|
**Use this whole document as the prompt** when handing off to a fresh agent.
|
||||||
|
Everything they need to pick up cold is below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The bug, in one paragraph
|
||||||
|
|
||||||
|
acdream renders any character that uses Setup `0x02000001` (Aluvian Male)
|
||||||
|
with a visibly **bulkier, less shape-defined silhouette** than the same
|
||||||
|
character viewed in retail's AC client. Specifically: shoulders look
|
||||||
|
smoother / rounder where retail has pointier shoulder pads; the back lacks
|
||||||
|
contour ("flat back"); arms appear puffier. The bug applies equally to
|
||||||
|
players (`+Acdream`, `+Je`) and humanoid NPCs using the same setup
|
||||||
|
(`Woodsman`, `Sedor Wystan the Blacksmith`, `Thelnoth Cort the Healer`,
|
||||||
|
others). It is **independent of equipment** — `+Je` stripped naked still
|
||||||
|
shows the bulky shape. It is **specific to Setup 0x02000001** — drudges
|
||||||
|
(setup `0x020007DD`) and other monster setups render identically to retail
|
||||||
|
through the same pipeline. See ISSUES.md `#47` for the full filing.
|
||||||
|
|
||||||
|
## Acceptance criterion
|
||||||
|
|
||||||
|
Side-by-side screenshots of the same humanoid (player or NPC) viewed from
|
||||||
|
the same approximate angle in acdream and retail show **matching silhouette
|
||||||
|
and shape definition** — pointy shoulders where retail has them, contoured
|
||||||
|
back, no "puffy" arms. User confirms visually. NPCs, drudges, and scenery
|
||||||
|
must continue to render correctly (no regressions).
|
||||||
|
|
||||||
|
## What's already been ruled out (don't redo these)
|
||||||
|
|
||||||
|
1. **0xF625 ObjDescEvent appearance updates being dropped.** Was a real
|
||||||
|
bug for skin/hair colors. Fixed in commit `e471527`. Does NOT affect
|
||||||
|
the bulky-shape issue (persists with the fix in place AND with no
|
||||||
|
equipment).
|
||||||
|
2. **Position-pop on equip toggle.** Side effect of the appearance fix,
|
||||||
|
also resolved in `e471527`. Doesn't affect shape.
|
||||||
|
3. **Clothing/armor overlapping the base body** (the HiddenParts
|
||||||
|
hypothesis). User stripped `+Je` naked; bulky shape persists.
|
||||||
|
4. **`ParentIndex` hierarchy not walked in `SetupMesh.Flatten`.** Setup
|
||||||
|
`0x02000001` has a real hierarchy
|
||||||
|
(`-1, -1, 1, 2, 3, -1, 5, 6, 7, 0, 9, 10, 11, 12, 13, 14, 15, 0, ...`)
|
||||||
|
but a parent-walk implementation produced **no visible change**.
|
||||||
|
Confirms AC's idle animation frames are already in setup-root
|
||||||
|
coordinates, not parent-local.
|
||||||
|
5. **Equipment / wielded items.** No equipment on `+Je` and bug persists.
|
||||||
|
6. **Player-specific data flow.** Humanoid NPCs using the same setup
|
||||||
|
(Woodsman et al) show the same bug.
|
||||||
|
7. **Silent GfxObj load failures or polygon drops.** `ACDREAM_DUMP_CLOTHING=1`
|
||||||
|
confirmed every one of the 34 parts emits triangles (`EMIT part=NN
|
||||||
|
gfx=0xXX subMeshes=N tris=N`); total ~648-700 tris per character.
|
||||||
|
|
||||||
|
## What's confirmed (use this as starting facts)
|
||||||
|
|
||||||
|
- `Setup.Parts.Count = 34` for `0x02000001`. `flatten.Count = 34`.
|
||||||
|
`AnimPartChanges = 34..38` depending on equipment. All match.
|
||||||
|
- Idle animation frames place parts at sensible humanoid Z-heights
|
||||||
|
(`head Z=1.587, mid-body Z=0.5-1.0, ground Z=0.085`).
|
||||||
|
- All 34 per-part orientations are nearly identical: 180° around -Z
|
||||||
|
axis (`W≈0, Z≈-1`). This is a setup-wide coordinate-flip convention.
|
||||||
|
Drudges have varied per-part orientations — different layout.
|
||||||
|
- `setup.DefaultScale.Count = 0` for both humans and drudges → all parts
|
||||||
|
use `Vector3.One` scale.
|
||||||
|
- Same fragment shader (`mesh.frag`) is used for humans and drudges.
|
||||||
|
Per-pixel diffuse with interpolated `vWorldNormal`.
|
||||||
|
|
||||||
|
## Top hypotheses, ordered by likely payoff
|
||||||
|
|
||||||
|
### Hypothesis A — per-face vs smoothed vertex normals
|
||||||
|
|
||||||
|
**Strongest candidate.** AC's dat stores ONE normal per `SWVertex`. If
|
||||||
|
human-character GfxObjs (e.g. `0x01001212`, `0x0100004B`-`0x01000059`)
|
||||||
|
were authored with **per-face flat normals** (each vertex's normal copied
|
||||||
|
from its triangle's face normal) while monster GfxObjs were authored with
|
||||||
|
**smoothed normals** (averaged across adjacent faces), acdream's
|
||||||
|
`Vector3.Normalize(sw.Normal)` would produce flat shading on humans and
|
||||||
|
smooth shading on monsters. The screenshots strongly support this — retail
|
||||||
|
characters look smooth-shaded, acdream characters look facet-edged.
|
||||||
|
|
||||||
|
User said "not shaders" but they may not realize per-vertex normal
|
||||||
|
*style* is part of the shader pipeline.
|
||||||
|
|
||||||
|
**Test:** in `src/AcDream.Core/Meshing/GfxObjMesh.cs:142`, replace the
|
||||||
|
direct `sw.Normal` read with a smooth normal computed per-load via
|
||||||
|
face-adjacency accumulation:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Pre-pass: for each polygon, compute face normal; accumulate onto each vertex.
|
||||||
|
// Post-pass: normalize.
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify retail does this — see `docs/research/deepdives/r13-dynamic-lighting.md:107`
|
||||||
|
for the `v.normal += t.face_normal` pattern, and
|
||||||
|
`docs/plans/2026-04-13-rendering-rebuild.md:50` for the
|
||||||
|
`AdjustPlanes: face-normal accumulation + per-vertex lighting` note (terrain context but
|
||||||
|
same math applies to characters).
|
||||||
|
|
||||||
|
If smooth-normals fixes humans and ALSO doesn't break drudges (because
|
||||||
|
drudge dat normals were already smooth, computing them again gives the
|
||||||
|
same answer modulo precision), this is the bug.
|
||||||
|
|
||||||
|
### Hypothesis B — cell ambient too low
|
||||||
|
|
||||||
|
Back-facing surfaces (the parts of the character not lit by the directional
|
||||||
|
sun) fall to `uCellAmbient` in `mesh.frag`. If ambient is very dark, the
|
||||||
|
back of any character looks uniformly black — reading as "flat" because no
|
||||||
|
detail variation is visible. Retail likely has higher ambient that lets
|
||||||
|
unlit surfaces still show their geometry through subtle gradients.
|
||||||
|
|
||||||
|
**Test:** dump `uCellAmbient` UBO values during a player render and compare
|
||||||
|
to retail's behaviour. Try bumping ambient temporarily and see if back-
|
||||||
|
detail emerges.
|
||||||
|
|
||||||
|
### Hypothesis C — anti-aliasing
|
||||||
|
|
||||||
|
acdream's GL window may not have MSAA enabled. Without it, polygon edges
|
||||||
|
visibly stair-step, exaggerating the faceted look at low triangle counts
|
||||||
|
(~700 tris per character). Retail likely has AA on by default.
|
||||||
|
|
||||||
|
**Test:** check the Silk.NET window creation code for `Samples`/MSAA
|
||||||
|
config. Try enabling `Samples=4` and re-render.
|
||||||
|
|
||||||
|
### Hypothesis D — orientation composition order or sign
|
||||||
|
|
||||||
|
The 180°-around-Z rotation on every part is unusual. If acdream applies
|
||||||
|
it correctly but retail applies it differently (e.g. as a post-multiply
|
||||||
|
or with the inverse), parts could be subtly mis-positioned in ways that
|
||||||
|
read as "bulky" rather than "broken". My investigation didn't fully rule
|
||||||
|
this out — `parent-walk` was a no-op, but a *single-level* orientation
|
||||||
|
composition discrepancy might be invisible without comparing actual
|
||||||
|
post-transform vertex positions to retail.
|
||||||
|
|
||||||
|
**Test:** attach cdb to retail (see CLAUDE.md "Retail debugger toolchain"),
|
||||||
|
break in `Frame::combine` (`0x518FD0`) with a player guid, dump the
|
||||||
|
resulting `Frame` for parts 0, 9, 16. Compare to acdream's per-part
|
||||||
|
world matrices (add a diagnostic).
|
||||||
|
|
||||||
|
## Diagnostic infrastructure already built
|
||||||
|
|
||||||
|
All env-var-gated, no runtime cost when off:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_DUMP_CLOTHING = "1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Prints, for every humanoid spawn (gate: `setup.Parts.Count >= 10`):
|
||||||
|
|
||||||
|
- Header: `setup.Parts.Count`, `flatten.Count`, `APC` count
|
||||||
|
- `ParentIndex[N]: -1,-1,1,2,3,...` array
|
||||||
|
- `DefaultScale[N]: ...` array
|
||||||
|
- `IdleFrame.Frames[N]:` per-part `Origin` + `Orientation` (first 17 parts)
|
||||||
|
- `EMIT part=NN gfx=0xXXXXXXXX subMeshes=N tris=N` per part
|
||||||
|
- `TOTAL tris=N meshRefs=N` per entity
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_DUMP_APPEARANCE = "1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Prints structured decode of every `0xF625 ObjDescEvent` (mid-session
|
||||||
|
appearance change) — SubPalettes, TextureChanges, AnimPartChanges.
|
||||||
|
|
||||||
|
Source: `src/AcDream.App/Rendering/GameWindow.cs` around `dumpClothing` /
|
||||||
|
`OnLiveEntitySpawnedLocked`, and `src/AcDream.Core.Net/WorldSession.cs`
|
||||||
|
`DumpAppearanceEnabled`.
|
||||||
|
|
||||||
|
## Workflow (per `CLAUDE.md`)
|
||||||
|
|
||||||
|
This is AC-specific behavior. Follow the mandatory workflow:
|
||||||
|
|
||||||
|
1. **Step 0 — grep named first.**
|
||||||
|
`grep "Class::method" docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||||
|
for any function name from a hypothesis. The Sept 2013 PDB is named —
|
||||||
|
most things have real names. Don't decompile fresh until you've grep'd.
|
||||||
|
2. **Step 1 — cross-reference.** Check ACE / ACME / ACViewer / Chorizite /
|
||||||
|
AC2D / holtburger as appropriate. The reference hierarchy in CLAUDE.md
|
||||||
|
spells out which ref is authoritative for each domain. For rendering:
|
||||||
|
ACME `WorldBuilder.Tests/ClientReference.cs` is the closest oracle.
|
||||||
|
3. **Step 2 — pseudocode.** Write the algorithm in plain language under
|
||||||
|
`docs/research/*_pseudocode.md` before porting.
|
||||||
|
4. **Step 3 — port faithfully.** Match retail line-by-line, same variable
|
||||||
|
names, same control flow. No "improvements".
|
||||||
|
5. **Step 4 — conformance test.** Write a test using a captured wire body
|
||||||
|
or known dat values as golden output.
|
||||||
|
6. **Step 5 — visual verification.** User confirms in-client.
|
||||||
|
|
||||||
|
If grep-named-first turns up nothing relevant, the function is likely
|
||||||
|
in the unnamed minority. Fall back to Ghidra chunks under
|
||||||
|
`docs/research/decompiled/` keyed by address.
|
||||||
|
|
||||||
|
## Files most likely to need edits
|
||||||
|
|
||||||
|
- `src/AcDream.Core/Meshing/GfxObjMesh.cs` — polygon emission, vertex
|
||||||
|
normal handling at line 142. **Hypothesis A lives here.**
|
||||||
|
- `src/AcDream.Core/Meshing/SetupMesh.cs` — per-part transform
|
||||||
|
composition. The parent-walk experiment lives here (and was reverted —
|
||||||
|
see git history if you want to revisit it).
|
||||||
|
- `src/AcDream.App/Rendering/Shaders/mesh.frag` — per-pixel lighting
|
||||||
|
equation. **Hypothesis B lives here** if ambient is the cause.
|
||||||
|
- `src/AcDream.App/Rendering/Shaders/mesh.vert` — normal transform
|
||||||
|
(`mat3(uModel) * aNormal`). Watch for non-uniform scale issues if
|
||||||
|
any future change touches scale.
|
||||||
|
- The Silk.NET window setup code (search for `IWindow.Create` /
|
||||||
|
`WindowOptions`) — **Hypothesis C lives here** if MSAA needs enabling.
|
||||||
|
|
||||||
|
## Test workflow (live verification)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Always: kill stale + 3-5s wait (ACE session lingers briefly).
|
||||||
|
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||||
|
Start-Sleep -Seconds 4
|
||||||
|
|
||||||
|
$env:ACDREAM_DUMP_CLOTHING = "1" # verbose per-spawn diagnostics
|
||||||
|
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||||
|
$env:ACDREAM_LIVE = "1"
|
||||||
|
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||||
|
$env:ACDREAM_TEST_PORT = "9000"
|
||||||
|
$env:ACDREAM_TEST_USER = "testaccount"
|
||||||
|
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||||
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
||||||
|
Tee-Object -FilePath "launch_issue47.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
The user's `+Acdream` character logs in at Holtburg. Nearby:
|
||||||
|
- Other player(s) like `+Je` (driven from a parallel retail acclient)
|
||||||
|
- Humanoid NPCs (Woodsman, Sedor Wystan the Blacksmith, Thelnoth Cort
|
||||||
|
the Healer, Sontella Dagroff the Bowyer, Archmage Cindrue, Monyra the
|
||||||
|
Jeweler, Ecutha the Tailor) — most use Setup 0x02000001
|
||||||
|
- Drudges and slinkers nearby (different setup, render correctly — use
|
||||||
|
as control)
|
||||||
|
- The Foundry's "Nullified Statue of a Drudge" — different setup, used
|
||||||
|
for prior diagnostic work
|
||||||
|
|
||||||
|
User has the retail acclient open in parallel for side-by-side comparison.
|
||||||
|
Visual verification is the acceptance test — there's no automated way to
|
||||||
|
say "this looks like retail."
|
||||||
|
|
||||||
|
## Constraints / don't-break
|
||||||
|
|
||||||
|
- Do **not** break drudge / monster rendering. They're correct now.
|
||||||
|
- Do **not** break scenery (terrain, buildings, statues). They're correct.
|
||||||
|
- Do **not** break the local `+Acdream` (own-character) view either —
|
||||||
|
same pipeline so any rendering change applies to it.
|
||||||
|
- Tests must stay green: `dotnet test AcDream.slnx` should report 0
|
||||||
|
failures. There are 8 pre-existing motion test failures in
|
||||||
|
`AcDream.Core.Tests` that are NOT yours — don't try to fix them in
|
||||||
|
this work.
|
||||||
|
- Build must stay green: `dotnet build AcDream.slnx -c Debug`.
|
||||||
|
|
||||||
|
## When to stop and ask
|
||||||
|
|
||||||
|
Per CLAUDE.md, ask only for:
|
||||||
|
- Visual verification (user looking at the client)
|
||||||
|
- Genuine architectural disagreements with the roadmap
|
||||||
|
- Hard-to-reverse destructive actions
|
||||||
|
|
||||||
|
Otherwise act. Don't ask "should I continue?".
|
||||||
|
|
||||||
|
## References to consult
|
||||||
|
|
||||||
|
- `references/ACME/WorldBuilder/Editors/Landscape/StaticObjectManager.cs` —
|
||||||
|
ACME's mesh hydration; same stack (Silk.NET) as us, used as our
|
||||||
|
closest "should look right" oracle.
|
||||||
|
- `references/ACViewer/ACViewer/Physics/PartArray.cs:614` (`UpdateParts`)
|
||||||
|
and `references/ACViewer/ACViewer/Physics/Animation/AFrame.cs:43`
|
||||||
|
(`Combine`) — frame composition math.
|
||||||
|
- `references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Networking.cs:48`
|
||||||
|
(`SerializeUpdateModelData`) and `:978` (`AddBaseModelData`) — what
|
||||||
|
ACE puts on the wire for character appearance.
|
||||||
|
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` — Sept 2013 PDB
|
||||||
|
with 18,366 named functions. **Grep this FIRST.**
|
||||||
|
- `docs/plans/2026-04-13-rendering-rebuild.md` — earlier rendering
|
||||||
|
rebuild plan with notes on `AdjustPlanes` (terrain, but the
|
||||||
|
face-normal accumulation pattern is the smooth-normal pattern).
|
||||||
|
- `docs/research/deepdives/r13-dynamic-lighting.md` — has the smooth-
|
||||||
|
normal accumulation pseudocode at line 107.
|
||||||
|
|
||||||
|
## Final note
|
||||||
|
|
||||||
|
This is a **rendering-fidelity** issue, not a wire-protocol one. The
|
||||||
|
network data is correct (the previous session's `0xF625` work confirmed
|
||||||
|
it). The bug is in how acdream interprets that data into pixels.
|
||||||
|
|
||||||
|
The smoothest path is probably Hypothesis A (smooth normals), one
|
||||||
|
contained change in `GfxObjMesh.Build`, gated behind a feature flag for
|
||||||
|
A/B testing, then verified live by the user. If that doesn't fix it,
|
||||||
|
move to B (ambient), then C (MSAA), then D (frame composition with cdb
|
||||||
|
trace).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue