User report: 'You should be face to face with the NPC before sending
use. So first is rotation, when you are facing, then using.' and
'it does not face it completely.'
Two changes:
1. Split alignment thresholds in ApplyAutoWalkOverlay:
walkAligned (30°) — gate for synthesised Forward+Run motion
during far-range approach; body walks
while finishing residual turn within 30°.
aligned (5°) — gate for arrival-fire. Final facing
before the auto-walk ends and the action
re-sends. Matches retail's tight pre-Use
rotation tolerance.
Within-arrival check still requires alignment; without alignment
the body holds in turn-only mode regardless of distance.
2. Defer wire Use/PickUp packet for CLOSE-range targets. SendUse
and SendPickUp now check IsCloseRangeTarget(guid): if the player
is already within the target's use-radius, we install the
speculative overlay, set _pendingPostArrivalAction, and RETURN
without sending the wire packet. AutoWalkArrived fires after the
local rotation completes (alignment within 5°); the existing
re-send handler then fires SendUse with isRetryAfterArrival=true,
sending the wire packet at that moment. Effect: rotate first,
THEN Use — the NPC/door/item only sees the action after the
character has turned to face it.
Far-range path unchanged: send immediately, ACE auto-walks,
arrival re-sends.
Filed #69: turn animation (leg/arm cycle while pivoting). The body
now rotates but doesn't play the TurnLeft/TurnRight cycle the user
wants to see. Separate scope — needs motion-interpreter integration.
10 Hz heartbeat (301281d) made ACE see us in-radius before its
MoveToChain timeout; user confirmed doors work now. Closing #67 — root
cause was 1 Hz position outbound on our side, not anything door-
specific. Same fix unblocked door + NPC.
Two visible refinements:
1. Turn-first gate. User report: 'when I use from far range, I should
face that object and then start moving. Now it starts running
before facing is complete.'
ApplyAutoWalkOverlay now suppresses Forward motion when the
heading delta to the target is > 30°. Body turns IN PLACE first,
then walks forward once roughly aligned. Within the 30° band the
body walks while finishing the residual turn. Matches the user-
observed retail rhythm.
2. Arrival margin shrunk 0.2 m → 0.05 m. User report: 'NPC dialogue
fires, but still a bit too close. In retail it fires from a longer
range.' With the 10 Hz heartbeat the server-side Player.Location
tracks us within ~100 ms, so the bigger safety margin is no longer
needed — only a tiny epsilon to absorb the sub-tick race between
local arrival fire and the next outbound packet.
Filed #68: remote players' running animation doesn't transition to
Ready on auto-walk arrival when observed from acdream. Separate
visual bug — server-side action completes correctly; just the cycle
on the dead-reckoned remote body doesn't flip back to idle.
Margin trim:
Previous: min(0.5, threshold * 0.4) — for 3 m NPC arrived at 2.5 m
New: min(0.2, threshold * 0.2) — for 3 m NPC arrives at 2.8 m
User feedback: 'compared to retail, it fires too close. In retail
it fires from a longer range.' Smaller margin matches that — still
safely inside ACE's strict WithinUseRadius but closer to the boundary.
Tight pickup radii (0.6 m item) now arrive at 0.48 m (was 0.36 m).
Filed issues:
#67 Door Use action doesn't complete after auto-walk arrival.
NPC dialogue fires correctly post-flush-AP+re-send, but
doors still go silent — need to investigate door-specific
state requirements in ACE's Door.ActOnUse or our wire
payload differences.
#66 Rotation: local player flips back after auto-walk arrival
(observed from retail observer); NPCs don't turn to face
the player when used. Both rooted in missing MovementType=8
TurnToObject handling. Supersedes #65 (which was local-only)
with a unified rotation-handling phase scope.
Phase B.5 (ground-item pickup, close-range path) shipped and
visual-verified 2026-05-14 at Holtburg. M1 demo target 4/4 ("pick up
an item") met.
New ship-handoff doc captures the 5-commit history including the
post-visual-test PickupEvent (0xF74A) wire-handler fix that closes
the local-despawn gap.
Roadmap and CLAUDE.md updated to reflect the ship + the new follow-up
issues:
- #63 (MEDIUM) — server-initiated MoveToObject auto-walk not
honored; blocks double-click pickup + out-of-range F. Filed as
candidate Phase B.6. holtburger has the reference implementation.
- #64 (LOW) — local-player pickup animation does not render
(retail observers see it correctly). Likely a self-echo filter
dropping UpdateMotion(Pickup) on the local player.
Carry-overs from B.4c (#61 link-cycle flash, #62 PARTSDIAG null-guard)
unchanged.
Final whole-branch code review (Opus) surfaced two Important post-merge
follow-ups + a one-word inaccuracy in the handoff doc:
- #59: tighten WorldPicker per-entity Setup.Radius (M1-deferred; the
ServerGuid==0 invariant is load-bearing and worth documenting before
L.2d's CBuildingObj port lands).
- #60: port retail's obstruction_ethereal downstream path so combat-HUD
contact reporting works for ethereal creatures (M2-combat).
- handoff: corrected "Added a _entitiesByServerGuid reverse-lookup" to
"Used the pre-existing _entitiesByServerGuid" — the dict has existed
since Phase 6.6/6.7; slice 1c used it, didn't add it.
Review verdict: branch ready to merge to main.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L.2g slice 1 is CODE-COMPLETE: parser + registry mutator + WorldSession
dispatcher + GameWindow subscriber (4 commits: 2459f28, d538915,
536a608, 108e386). Build clean, 6 new tests pass, baseline-stable
across the full suite. Per-commit + final integration code reviews
all approved.
Visual verification deferred: while running the Holtburg-doorway test,
Phase B.4's outbound Use handler turned out to be unwired. The wire
builders (InteractRequests.BuildUse), classes (SelectionState,
WorldPicker), input-action enums, and keybindings all exist — but
GameWindow.OnInputAction has no case for SelectDblLeft, so the click
silently does nothing. The inbound L.2g chain we just landed can't
fire until something sends an outbound Use.
This commit captures the handoff + reframes next-session work:
* docs/research/2026-05-12-l2g-slice1-shipped-handoff.md (NEW)
Full evidence: 4 shipped commits, end-to-end code flow, B.4
discovery explanation, 4 minor + 1 Important review notes
(the Important one is a test-coverage gap that the B.4b visual
test will settle automatically), reproducibility recipe,
next-session pick.
* CLAUDE.md
"Currently in Phase L.2" paragraph: L.2g slice 1 code shipped;
visual test deferred to B.4b. Next-phase-candidates list:
L.2g slice 1 (now done) replaced with the B.4b candidate
pointing at the slice scope.
* docs/plans/2026-04-29-movement-collision-conformance.md
L.2g section gains a "Current shipped slice (2026-05-12):" table
listing the 4 commits.
* docs/plans/2026-05-12-milestones.md
M1 phase-list updated: L.2g slice 1 (code) shipped; B.4 renamed
"B.4 / B.4b" with the gap-discovery note + B.4b shape.
* docs/ISSUES.md
New issue #57 (HIGH) for the B.4 interaction-handler gap.
Promoted to Phase B.4b; will close as
DONE (promoted to Phase B.4b) when B.4b's design spec lands.
* Memory file project_interaction_pipeline.md (in personal
memory dir, not in this commit) updated to reflect reality.
Next session: Phase B.4b (~30-50 LOC, 1-2 subagent dispatches,
~30 min). Subscribe SelectDblLeft -> WorldPicker.Pick ->
InteractRequests.BuildUse -> _liveSession.SendGameMessage. Same
Holtburg-doorway visual test verifies both L.2g slice 1 and B.4b
in one pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roadmap:
- Status header now reads "Between phases" with C.1.5b in the recent-wins
bullet list.
- New SHIPPED row in the table (C.1.5b) summarising both slices, the
reality discovery about EnvCell statics, and the 4 visual-verified sites.
- The "IN FLIGHT — C.1.5b" sub-bullet under Phase C flipped to SHIPPED.
ISSUES.md:
- #56 removed from Active.
- #56 added to Recently closed with full commit chain (1e3c33b spec+plan,
f3bc15e SetupPartTransforms helper, 11521f4 ParticleHookSink part-transform,
5ca5827 activator refactor, 8735c39 GpuWorldState fire-sites), resolution
notes, and the visual-verification record.
CLAUDE.md:
- "Currently in flight" pointer replaced by a "Currently between phases"
marker + a C.1.5b shipped paragraph that's parallel to the existing
C.1.5a entry.
- "After C.1.5b" → "Next phase candidates".
Visual verification 2026-05-12: portal swirl matches retail extent + lateral
spread (no ground-burial); Holtburg Inn fireplace flames; cottage chimney
smoke; spell-cast particles on +Acdream — all match retail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual verification at the Holtburg Town network portal passed for the
slice's mechanism: 10-hook portal script fires end-to-end with correct
color, persistence, orientation, and multi-emitter dispatch. After the
334f0c6 rotation-seed fix, the swirl is oriented correctly along the
portal's facing instead of world-NS.
Known limitation surfaced during verification and filed as issue #56:
ParticleHookSink ignores CreateParticleHook.PartIndex, so all 10 of the
portal's emitters collapse to the entity root position + identity-rotated
offset, producing a compressed and partly-ground-buried swirl instead of
the multi-tier shape retail renders. Mechanism is correct; per-part
transform handling is the next vfx-pipeline concern (will affect every
multi-emitter PES — slice 2 chimneys/fireplaces in particular).
Documentation changes:
- docs/ISSUES.md: new #56 entry with the captured entity guids
(0x7A9B405B / 0x7A9B4080), script ids (0x3300126D / 0x3300067A),
symptom data, root-cause hypothesis, file pointers, and acceptance
criterion. Notes the blocks-slice-2 relationship explicitly.
- docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md §9:
new limitation #1 documenting the verified PartIndex collapse symptom.
- docs/plans/2026-04-11-roadmap.md: new "C.1.5a" row in the shipped
table referencing the spec, plan, and #56 caveat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final code review of slice 1 flagged one Important issue (the spec's
"zero cost when off" claim for the surface-dump path is technically
violated — _uploadMetadata always writes one dict entry per upload
regardless of env var) plus minor doc/consistency gaps. Applied:
1. Spec §5 "Cost when off": dropped the "Zero" claim; replaced with
"Negligible — one Dictionary write per upload (~30-50 KB at Holtburg)
plus a hash-table write per upload. Expensive work (file I/O,
histogram construction) is still env-gated." This matches reality.
2. Baseline doc §5: rewrote from "Raw logs (scratch, can be deleted)"
referencing files that were never preserved in this worktree, to
"Reproducing the measurements" with the actual PowerShell launch
commands. Honest about the raw logs not being kept; the captured
medians in section 2 are the canonical record.
3. New issue #55 filed in docs/ISSUES.md — static-entity slow path
reports ~1.45M meshMissing/5s at r4 standstill, drops to ~0 when
walking. LOW severity (no visible regression), hypothesis points
at a "permanently-missing entity gets re-classified every frame"
pattern that Tier 1 cache doesn't cover.
4. Roadmap shipped table: renamed "N.6.1" row to "N.6 slice 1" to
match every other artifact's naming. Search-discoverability fix.
None of these change the slice's conclusion or next-phase
recommendation (C.1.5 first, then reduced-scope slice 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EntityClassificationCache keyed by (entityId, landblockHint) tuple lands
per spec docs/superpowers/specs/2026-05-10-issue-53-tier1-cache-design.md
+ plan docs/superpowers/plans/2026-05-10-issue-53-tier1-cache.md.
Perf result (horizon-safe preset + High quality, AMD Radeon RX 9070 XT
@ 1440p): entity dispatcher cpu_us median ~1200 us, p95 ~1500 us. Down
from ~3500m / ~4000p95 pre-Tier-1. ~66% / ~63% reduction. Well under
the A.5 spec budget (median <= 2.0 ms, p95 <= 2.5 ms). No BUDGET_OVER
flag across 30s+ standstill captures.
Visual gate cleared after 4 bug-fix iterations:
- 71d0edc: namespace stab Ids globally (cross-LB id collision)
- 95ebbf3: key cache by (entityId, landblockHint) tuple (defensive)
- c55acdc: skip cache populate when classification is incomplete
- f928e66: incomplete-entity flag must persist across same-entity tuples
User-confirmed visually via +Acdream test character: NPCs animate,
multi-part static buildings render fully (no airborne geometry, no
Z-fighting, no missing parts, no wrong textures), Nullified Statue of
a Drudge on top of the Foundry renders all parts, trees outside
Holtburg render with branches present.
Closes the post-A.5 polish phase. Issues #52, #54, #53 all closed.
Tests: 1711 passing, 8 pre-existing physics/input failures unchanged.
N.5b sentinel: 112/112 throughout.
Memory: ~/.claude/projects/.../memory/project_tier1_cache.md +
feedback_cache_per_tuple_pattern.md capture the audit-gap and per-tuple-
vs-per-entity recurring trap for future cache work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move ISSUE #54 to Recently closed referencing commit `bf31e59`. Drop
#54 from CLAUDE.md "Currently in flight" — only #53 (Tier 1 retry)
remains open in the post-A.5 polish phase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move ISSUE #52 from Active to Recently closed with full root-cause writeup
referencing commit `e40159f`. Strip lifestone reference from CLAUDE.md
"Currently in flight"; remaining post-A.5 polish scope is #53 (Tier 1
retry) + #54 (JobKind plumbing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document Phase N.5b shipping (terrain on the modern rendering path via
Path C — `TerrainModernRenderer` mirrors WB's `TerrainRenderManager`
pattern but consumes acdream's `LandblockMesh.Build` so retail's
`FSplitNESW` formula stays in lockstep with physics + visual mesh).
Changes:
- `docs/plans/2026-04-11-roadmap.md` — add N.5b row to the Shipped
table; promote N.5b's "Phases ahead" entry to ✓ SHIPPED with the
Path C resolution + perf reality check; refresh N.6 scope to note
Terrain has joined the modern path (legacy `Texture2D` retirement
scope narrows to Sky + Debug); update top-of-doc Status line.
- `docs/ISSUES.md` — close issue #51 (WB terrain-split formula
divergence). Move from OPEN to "Recently closed" with the Path C
resolution: never adopted WB's formula; modern dispatcher uses
retail's via `LandblockMesh.Build`. References `da56063` (the
black-terrain fix that landed within the N.5b ship chain).
- `CLAUDE.md` — add `TerrainModernRenderer.cs` to the WB integration
cribs list with the GL_INVALID_OPERATION caveat (use uvec2 +
`sampler2DArray(handle)` constructor, NOT direct
`uniform sampler2DArray` + `glProgramUniformHandleARB`). Update
the "Currently in flight" preamble: N.6 builds on N.5 + N.5b;
add an N.5b shipped paragraph linking the perf baseline doc.
- `docs/plans/2026-05-09-phase-n5b-perf-baseline.md` — new doc
capturing the radius=5 Holtburg perf measurement (modern 6.4-7.0
µs median vs legacy 1.5 µs — modern is ~4× SLOWER on CPU at
radius=5). Documents the spec acceptance criterion #5 amendment,
the architectural wins that DO hold (zero glBindTexture/frame,
constant-cost dispatch as A.5 raises radius, per-LB frustum cull),
and the three high-value gotchas surfaced during implementation.
User-memory updates (outside repo, not in this commit):
- `memory/project_phase_n5b_state.md` — full N.5b state file with
the three gotchas captured.
- `memory/MEMORY.md` — index entry pointing at the state file.
Build: dotnet build green. No code changes in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final cross-cutting review of N.5 found that Task 15's deletion of
mesh_instanced.vert/.frag left InstancedMeshRenderer orphaned —
ACDREAM_USE_WB_FOUNDATION=0 silently rendered terrain+sky only with
no entities. The SHIP commit's "[x] ACDREAM_USE_WB_FOUNDATION=0 still
works" claim was inaccurate.
Resolution: formal retirement of the legacy renderer path within N.5
instead of deferring to N.6.
Deleted:
- src/AcDream.App/Rendering/InstancedMeshRenderer.cs
- src/AcDream.App/Rendering/StaticMeshRenderer.cs
- src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs
GameWindow simplified — capability detection is unconditional, missing
bindless throws NotSupportedException with a clear message at startup.
WbDrawDispatcher + mesh_modern shader load are mandatory after init.
No escape hatch.
GpuWorldState simplified — WbFoundationFlag.IsEnabled guards on
AddLandblock/RemoveLandblock removed; adapter calls are unconditional
when the adapter is non-null.
PendingSpawnIntegrationTests updated — WbFoundationFlag.ForTestsOnly_ForceEnable
static ctor removed (flag is gone; adapter calls are unconditional).
The ApplyLoadedTerrain physics-data loop was also simplified: the
EnsureUploaded sub-loop that fed InstancedMeshRenderer is gone;
_pendingCellMeshes is now explicitly cleared to prevent unbounded
accumulation (the worker thread still populates it, but WB handles
EnvCell geometry through its own pipeline).
Spec §2 Decision 5 + §10 Out-of-Scope updated. Plan ship-amendment
section added. Roadmap updated (N.5 ships with retirement; N.6 scope
narrowed to perf-only). CLAUDE.md "WB integration cribs" updated.
Perf baseline doc updated. WbDrawDispatcher class summary docstring
corrected to describe the as-shipped SSBO + multi-draw-indirect path.
ISSUES.md #51 updated (terrain not in N.5 scope; deferred to N.7).
Bindless support is now a hard requirement. Modern desktop GPUs
universally expose GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters;
if a user hits the NotSupportedException, that's a real bug report
worth investigating, not a silent fallback.
Build: 0 errors, 0 warnings. Tests: 71/71 (Wb+MatrixComposition+TextureCacheBindless filter).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit during N.3 follow-up uncovered that WB's TerrainUtils
CalculateSplitDirection uses a different math expression than
retail's FSplitNESW (the AC2D-cited polynomial 0x0CCAC033 etc that
our visual terrain mesh and physics already share). Substituting
TerrainSurface.SampleZ with WB's GetHeight in isolation would
re-introduce the triangle-Z hover bug from earlier work — physics
and visual mesh would pick different diagonals on disputed cells.
Updates:
- ISSUE #51 documents the divergence with file references and the
research that's needed when N.5 picks this up.
- Roadmap N.2 entry flags the dependency on N.5 and the reasoning
("not low-risk after all").
N.1's conformance proved slope-filtering equivalence (boolean
walkable verdict), not formula equivalence. The lesson is captured
in memory (feedback_wb_migration_formulas.md, not in-repo).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds Phase N.1 to "Phases already shipped" table at top of roadmap,
updates the Phase N section to mark N.1 with checkmark SHIPPED status,
and files the known road-edge-tree cosmetic difference at landblock
0xA9B1 in ISSUES.md as issue #50 follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#48. Trees on sloped cells visibly hovered above the visible
terrain because GameWindow.SampleTerrainZ (the bilinear fallback used
during scenery hydration before physics registers a landblock) had
its diagonal arms swapped — used the SEtoNW triangle test on SWtoNE
cells and vice versa. The ACDREAM_DUMP_SCENERY_Z=1 diagnostic showed
every scenery line ran through the bilinear path (streaming race),
so on hilly terrain scenery was placed at a Z up to ~1.5 m off from
the visible mesh.
Latent since ff325ab (2026-04-17 "feat(ui): debug overlay + refined
input controls" carrying along the upgrade). That commit reached for
WorldBuilder TerrainUtils.GetHeight as the secondary oracle and
re-derived the triangle-pair tests; the named-retail / ACE algorithm
in TerrainSurface.SampleZ (used by the physics path / player Z) was
always correct, so player feet stayed flush — the two paths just
disagreed and only scenery noticed.
Fix:
- TerrainSurface.InterpolateZInTriangle (private static) — single
source of truth for the triangle pick + barycentric Z, sourced
from FUN_00532a50 / ACE LandblockStruct.ConstructPolygons.
- TerrainSurface.SampleZFromHeightmap (public static) — heightmap-
byte-array variant for the scenery hydration fallback. Both this
and TerrainSurface.SampleZ (instance) now delegate to the same
InterpolateZInTriangle.
- GameWindow.SampleTerrainZ — thin wrapper over the new static.
- TerrainSurfaceTests.SampleZFromHeightmap_AgreesWithInstance_AcrossWholeLandblock
asserts both sampler paths agree at 1500 sample points across both
diagonals, so future drift gets caught.
The ACDREAM_DUMP_SCENERY_Z=1 diagnostic in BuildSceneryEntitiesForStreaming
is kept committed (env-var gated, zero cost when off) — useful for
the related #49 scenery (X, Y) placement investigation filed in the
same commit.
Visual verified at Holtburg landblock 0xA9B30001 2026-05-06: the
formerly floating 32 m pines (setups 0x020002D3 / 0x020002D9) now
sit flush on the visible terrain mesh.
Test baseline: dotnet test reports the same 8 pre-existing motion /
BSP step-up failures as the handoff doc warned about — no new
failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keep local physics authoritative at the retail 30 Hz MinQuantum, but expose a render-only position that lerps between completed physics ticks for the player mesh and chase-camera target. Network outbound continues to use the discrete physics position.
Also make the visually confirmed #47 humanoid close-detail DIDDegrade path default-on, with ACDREAM_RETAIL_CLOSE_DEGRADES=0 left as a diagnostic opt-out.
Verification: dotnet build AcDream.slnx -c Debug; focused #38 interpolation tests passed; visual confirmed smooth 2026-05-06. Full dotnet test AcDream.slnx -c Debug --no-build still has the known 8 AcDream.Core.Tests baseline failures.
Co-authored-by: Codex <codex@openai.com>
A few specific scenery GfxObjs render with their trunk base ~0.5-1.5m
above the terrain mesh while the vast majority sit flush. Per-GfxObj-
id ⇒ deterministic across instances. Player Z snap is unaffected.
Side-by-side with retail confirmed the same species place flush there.
Filed with three competing hypotheses: per-GfxObj origin convention
(some tree meshes authored with origin at bbox-center vs trunk-base),
physics-vs-bilinear terrain Z mismatch on NE↔SW-cut cells, or the
same DIDDegrade close-detail story as #47 applied to scenery.
Detailed cut-and-paste handoff for the next agent at
docs/research/2026-05-06-issue-48-handoff.md — covers the diagnostic
dump that disambiguates the hypotheses with one log line per
offending tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Humanoid bodies (Setup 0x02000001 + heritage variants) rendered visibly
flat / bulky vs retail because we drew the base GfxObj id from Setup /
AnimPartChange directly. Retail's CPhysicsPart::LoadGfxObjArray
(0x0050DCF0) treats that base id as the entry point to a DIDDegrade
table; close/player rendering uses Degrades[0].Id, which is the
higher-detail mesh that carries bicep / deltoid / shoulder geometry.
ACViewer also has this bug — it was the key signal it isn't acdream-
specific. Both clients drew the LOD-3 base mesh (e.g. 14 verts / 17
polys for Aluvian Male upper arm 0x01000055), missing the close-
detail variant (0x01001795: 32 verts / 60 polys).
Adds GfxObjDegradeResolver that walks the table with safe fallbacks
at every step. Wired in GameWindow after AnimPartChange application
and before texture-change resolution so texture overrides match the
resolved mesh's surfaces. Gated by ACDREAM_RETAIL_CLOSE_DEGRADES=1
and scoped to humanoid setups (34 parts with >=8 null-sentinel
attachment slots) while the fix bakes — the change is harmless on
non-humanoid setups (resolver falls back to base when no degrade
table) but we hold the broader sweep until LOD distance plumbing
lands.
User confirmed visually 2026-05-06: bicep, deltoid, and back-muscle
definition match retail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filed #47 in docs/ISSUES.md — humanoid characters using Setup 0x02000001
(players + Woodsman + other Aluvian NPCs) render visibly bulkier and less
shape-defined than retail's view. Drudges and other monster setups render
identically. Independent of equipment (naked +Je still shows it).
Investigation this session ruled out 0xF625 ObjDescEvent drops (real bug,
fixed in e471527, but doesn't explain shape), HiddenParts overlap,
ParentIndex walking (animation frames are setup-root coords already),
and player-specific data flow (NPCs using same setup affected too).
Diagnostic infrastructure landed alongside the issue (env-var-gated, no
runtime cost when off):
- ACDREAM_DUMP_CLOTHING=1 now also prints:
- setup.Parts.Count, flatten.Count, APC count on header
- ParentIndex[] and DefaultScale[] arrays
- IdleFrame per-part Origin + Orientation (first 17 parts)
- per-part EMIT line: gfx, subMeshes count, triangle count
- TOTAL triangle / meshRef counts per entity
This is what nailed down "all 34 parts emit" + "animation frames are
setup-root not parent-local" + "humans get setup-wide 180°-Z rotation
that drudges don't" — saved hours next session.
Open hypotheses for #47 next session: per-face vs smoothed vertex
normals (per-vertex normals from dat may be face-style for human
GfxObjs but smooth for monsters), low cell ambient leaving back faces
flat-shadowed, missing MSAA on the GL window.
#45 — closed by commit e9e080d. PlayerMovementController hands a raw
localAnimSpeed (1.0 slow / runRate fast); UpdatePlayerAnimation now
scales sidestep cycles by WalkAnimSpeed/SidestepAnimSpeed × 0.5 to
match ACE's BroadcastMovement formula. User-verified.
#46 — filed. Retail clients observing acdream's local +Acdream
character see visibly blippy / laggy movement. Local acdream view of
the same character is fine; acdream observing retail-driven
characters is also fine (after #39 / #45). The degradation is
specifically on the OUTBOUND path. Likely culprits ranked: AutoPos
heartbeat cadence (acdream's fixed 200 ms is suspect per
project_retail_motion_outbound memory), MoveToState send conditions,
sequence counters, or absent HasVelocity on UPs. Verification approach
documented (two retail clients + one acdream side-by-side; cdb
breakpoint count of MovementManager::unpack_movement on retail
observer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#45 — local +Acdream slow-strafe walking renders too slow. User
observed during fix#5 visual verify of #39: the observer-side fix
landed, then the user noticed the matching animation on the local
player was also playing at sub-walk cadence. Likely the same
SidestepAnimSpeed (1.25) vs WalkAnimSpeed (3.12) mismatch as fix#5
but on the local UpdatePlayerAnimation path. Filed for separate
investigation.
#39 — added "Progress 2026-05-06" section listing the five-commit
fix sequence (8fa04af → 863d96b → bb026b7 → 2653b30 → cc62e1c →
349ba65), the wire-level finding that retail genuinely does NOT
broadcast on Shift toggle (refuting the earlier confused trace
analysis), the user-verified working cases (1/2/4/5) and the
residual items (latency from 500ms UM grace, direction-flip cases
3/6/7 not yet explicitly verified).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual-verify of commit 8fa04af in launch-39-candidate.log refutes the
static-analysis hypothesis that retail does not broadcast UMs on
HoldKey-only changes. The log shows:
- [FWD_WIRE] for retail actor 0x50000001 contains many direct Walk↔Run
transitions (0x44000007 ↔ 0x45000005). ACE IS sending UMs on Shift
toggle.
- [SETCYCLE] fires correctly per UM; Sequencer.CurrentMotion cycles
through Walk / Run / Turn / Sidestep correctly per [UM_RAW].
- [UPCYCLE_PLAYER] never fired — UM grace correctly suppressed it
(UMs at >2 Hz, well within 500 ms grace).
- User reports legs visually stuck in walking animation despite the
wire/sequencer saying Run.
Conclusion: bug is downstream of Sequencer.CurrentMotion — same as
2026-05-03 hypothesis F. Most likely _currNode lands on the walk-to-run
transition link after SetCycle (`currNodeIsCyclic=False` confirmed in
[SCFULL]) and Advance does not progress past it to the cycle.
The candidate fix code (LastUMTime, ApplyPlayerLocomotionRefinement,
hysteresis constants, un-gated call site) is left in place — harmless
because UM grace blocks the velocity-fallback path while UMs arrive,
and the infrastructure may be useful for cases #2–#7 if those need
velocity fallback. But it does not close case #1.
Updates ISSUES.md #39 with refuted hypothesis + new evidence + next
step pointer. findings-static.md gains "Visual-verify result" §
documenting the diagnostic dump and recommending the next investigation
target be AnimationSequencer.Advance queue-handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a player-remote velocity-fallback path to ApplyServerControlledVelocityCycle
so that when retail (the actor) toggles Shift while holding W and acdream is
the observer, the visible leg cycle switches Run↔Walk within ~200–500 ms even
though no fresh UM arrives. Static analysis (ACE GameActionMoveToState +
MovementData.cs auto-upgrade + acdream's prior diag traces) suggests retail
does NOT broadcast a fresh MoveToState on HoldKey-only changes — acdream's
UMs handle direction-key changes and our local +Acdream's transitions, but
retail-driven actors leave the cycle stuck.
Changes (all in src/AcDream.App/Rendering/GameWindow.cs):
- New RemoteMotion.LastUMTime field, stamped in OnLiveMotionUpdated
- ApplyServerControlledVelocityCycle: removed inner IsPlayerGuid gate;
routes player remotes to new ApplyPlayerLocomotionRefinement
- ApplyPlayerLocomotionRefinement (forward-direction only):
- 500 ms UM grace window (UMs win when fresh)
- Forward-direction-only (low byte 0x05 / 0x07)
- Hysteresis: Run → Walk demote at < 4.5 m/s; Walk → Run promote > 5.5 m/s
- Skip SetCycle when neither motion ID nor speedMod changed meaningfully
- [UPCYCLE_PLAYER] diag gated on ACDREAM_REMOTE_VEL_DIAG=1
- Outer call site in OnLivePositionUpdated un-gated (!IsPlayerGuid removed);
per-remote routing now lives inside the function
Scope: case #1 (Run↔Walk forward) only. Cases #2–#7 (backward, sidestep
speed-buckets, direction-flips) remain deferred — PlanFromVelocity is
forward-only and its NPC-tuned thresholds (RunThreshold=1.25) do not
separate player Walk (~2.5 m/s) from player Run (~9 m/s); a TTD trace
of retail's per-direction algorithm should ground the wider fix.
ISSUES.md #39 updated with progress; investigation-prompt.md and a new
findings-static.md committed under
docs/research/2026-05-06-locomotion-cycle-transitions/ (the prompt was
authored on a parallel branch in commit 7a38da3 and is brought into this
worktree here so the next session can find it without branch-hopping).
Build clean. The 8 pre-existing test failures on this branch
(BSPStepUpTests.C3_Path6_AirborneMoverHitsSteepSlope, MotionInterpreter
WalkBackward GetMaxSpeed, etc.) are unrelated to this change — verified
by running them with the diff stashed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#42 — moved from OPEN to DONE in place (rich investigation log preserved
below the new Resolution block). The originally-listed mechanisms (H1
slope-driven AdjustOffset projection, H2 step-down probe, H3 EdgeSlide)
were all RULED OUT by the first evidence run; root cause was self-
collision in FindObjCollisions, not in-sweep mechanism choice. Added
forward-pointer to retail's CObjCell::find_obj_collisions self-skip
(named-retail acclient_2013_pseudo_c.txt:308931).
#43 — new entry in Recently closed for the slope staircase on grounded
player remotes. Diagnosis: PositionManager.ComputeOffset's seqVel-only
fallback returned flat-Z motion because anim cycles bake Z=0 body-local,
producing visible 5 Hz Z stepping at the server-UP cadence. Fix: project
the fallback onto the local terrain plane (mirrors retail's
CTransition::adjust_offset contact-plane projection at the queue-empty
boundary). Verified via 9193 queue-empty-with-non-zero-offset.Z ticks
across a 34m vertical traversal.
Both diagnostic env-vars kept in tree for future regression hunts:
ACDREAM_AIRBORNE_DIAG=1 and ACDREAM_SLOPE_DIAG=1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the elated-aryabhata-208d5e branch into main. 7 commits implementing
the L.3 retail-faithful remote-entity motion port:
de129bc M1 Fresh InterpolationManager port + retail spec
40d88b9 M2 Queue routing for player-remote UPs + entity-position sync
2365c8c M3 Animation root motion fallback for idle queue
d57ace0 M6 Cleanup — dead fields + stale env-var references
c26bbbb M4 Jump-CellId fix + #42 filed
b37b713#42 root cause confirmed via A/B test
5cc2812 Handoff prompt for #42 PhysicsEngine investigation
User-verified visual checks: smooth body chase on running/walking/strafing,
no per-UP rubber-band, no slope staircase, NPCs pathing correctly, jumps
land cleanly. Two follow-up issues filed:
#41 sub-decimeter steady-state blips (velocity-synthesis residual; LOW)
#42 airborne XY drift on jumps (PhysicsEngine.ResolveWithTransition
depenetration; root cause confirmed; deep-dive prompt at
docs/research/2026-05-05-issue-42-handoff.md)
Replaces the env-var-gated experimental path (ACDREAM_INTERP_MANAGER=1)
which was marked DO-NOT-ENABLE — the env-var no longer toggles anything.
NPCs and airborne player remotes still use the legacy path; only grounded
player remotes route through the new retail-faithful queue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A/B-tested 2026-05-05 with user observing retail-controlled remote:
- With CellId fix removed: jumps render with geometrically-correct
XY (no drift) but body falls through the floor.
- With CellId fix applied: jumps land cleanly but arc shows ~1 m
horizontal offset; snaps back on next UM.
Confirms the drift originates inside ResolveWithTransition, not from
wire data, local Euler error, or stale velocity. CellId fix kept in
place because falling through the floor is more disruptive than
~1 m visual jitter that resolves on next input.
#42 updated with the verified diagnosis, three ranked-by-probability
hypotheses for the in-sweep mechanism (initial-overlap depenetration
along non-+Z terrain normal is the leading candidate), three matching
fix paths, and a deterministic repro recipe for the next session.
The right next step is investigating PhysicsEngine.ResolveWithTransition
and comparing against retail's CTransition::find_valid_position
(docs/research/named-retail/) — out of scope for the L.3 motion port,
files as a follow-up PhysicsEngine bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CellId fix:
L.3 M2 introduced OnLivePositionUpdated player-remote routing that
returned without setting `rmState.CellId = p.LandblockId`. The legacy
path always set this (formerly at line 3601). Airborne player remotes
fall through to the legacy TickAnimations path which gates
ResolveWithTransition on `rm.CellId != 0`; without the cell-id update
the sphere sweep was skipped, K-fix15 landing detection never fired,
and the body fell through the floor on jumps.
Fix: set `rmState.CellId = p.LandblockId` early in the M2 player-remote
branch (after orientation snap, before any return).
User-verified 2026-05-05: jumps now land cleanly with sequencer
leaving Falling on landing.
#42 filed:
Visual verification of M4 also exposed a ~1 m horizontal drift on
stationary jumps (body arcs through the air offset from actor's actual
position; lands at offset; snaps back on next UM). User confirms this
is pre-existing, masked by the legacy path's hard-snap-on-every-UP
behavior that M2 explicitly removed per retail spec
(03-up-routing.md § 3 "AIRBORNE NO-OP"). Filed as #42 with three
candidate fix paths (pragmatic legacy-restore, root-cause investigation,
or hybrid soft-correction).
M5 NPCs verified clean (legacy path unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores PositionManager.ComputeOffset call in TickAnimations player-
remote branch. M2 was queue-only (body chases server but stops between
UPs after head reached); M3 adds the retail REPLACE behavior:
- Queue active and not reached → catch-up vector (REPLACES anim).
- Queue empty or head reached → anim root motion (seqVel × dt rotated
by body.Orientation) drives translation between UPs.
- Blip-to-tail still fires on fail_count > 3.
Mirrors retail UpdatePositionInternal @ 0x00512c30 per
docs/research/2026-05-04-l3-port/05-position-manager-and-partarray.md
§ 6: PositionManager::adjust_offset OVERWRITES local frame's origin
with catch-up when active; otherwise no-op (anim root motion stands).
User-verified 2026-05-05: "Best implementation we have had so far.
Running works, walking works, strafing works."
Closes#40 (env-var path regression — replaced wholesale).
Files #41 for residual sub-decimeter blips: velocity-synthesis magnitude
(RunAnimSpeed × adjustedSpeed) overshoots server pace slightly, queue
walks it back every UP. Within retail's DesiredDistance / MinDistance
tolerances; not a correctness bug. Fix path requires porting
add_motion @ 0x005224b0 and cdb-tracing retail's actual
CSequence::velocity magnitude.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five parallel agents + dat probes ruled out:
- byte-level decode primitive (matches ACViewer)
- polygon emission (no ST_DOUBLE / Surface.Type & 6 issues)
- per-PART texture-override scoping (correctly per-MeshRef'd)
- SubPalette indexing convention (full-size 2048 palettes, *8 wire un-pack
is single-applied)
Smoking gun: for +Acdream the server sends 10 SubPaletteSwap ranges that
overlay palette indices [0..320), [576..1024), [1392..1488), [1728..1920).
The complement — [320..576), [1024..1392), [1488..1728), [1920..2048) —
is NOT overlaid. Base palette 0x0400007E at those indices has
red/skin tones. Coat texture UVs sampling those non-overlaid indices
render as visible "skin stub at top of coat".
Either ACE sends incomplete SubPaletteSwap data, or retail does extra
client-side ClothingTable computation we (and ACE) don't.
Diagnostic harness now lives at tools/InspectCoatTex/Program.cs;
GameWindow's DUMP_CLOTHING also probes runtime SubPalette dat sizes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-of-session cleanup of the 2026-05-03 remote-motion debug session.
Documentation:
- CLAUDE.md: add ⚠️ DO-NOT-ENABLE warning for ACDREAM_INTERP_MANAGER=1
in the diagnostic env-vars list. Add an "Outbound motion wire format"
section documenting acdream's WalkForward+HoldKey.Run encoding (which
ACE auto-upgrades to RunForward on relay) so future sessions don't
re-derive it.
- docs/ISSUES.md: file two new issues:
* #39 — Run↔Walk cycle transition not visible on observed
retail-driven player remotes when watched from acdream. Root cause
located: ApplyServerControlledVelocityCycle is gated by
IsPlayerGuid, excluding the exact case where ACE doesn't broadcast
a UM (shift toggle while direction key held). Fix sketch ~10
lines, separate commit. Cross-references the four-agent
investigation prompt.
* #40 — ACDREAM_INTERP_MANAGER=1 env-var path regressed. Documents
why (e94e791 conflated MoveOrTeleport with update_object), the
visible symptoms (staircase Z, position blips), and why
Commit B (039149a)'s ResolveWithTransition port was insufficient
(env-var path also clears body.Velocity → no horizontal Euler
motion → sweep input is queue catch-up only, which stair-steps).
Fix path = separate L.3 follow-up to re-integrate PositionManager
additively on top of the legacy chain.
Code:
- GameWindow.cs:6042: prepend a ⚠️ REGRESSED warning block at the top
of the env-var per-frame branch so anyone reading the code is
immediately aware not to enable it. Cross-references ISSUES.md #40.
- AnimationSequencer.cs: re-throttle [SCFAST]/[SCFULL] diagnostics to
0.5s per instance (rolled back from A.1's unthrottled experiment).
Already served its purpose; throttled is enough for steady-state
diagnostics. Restore _lastSetCycleDiagTime field.
No behavior change for any current launch (env-var unset = legacy
path unchanged). Build green; baseline test failures (8) unchanged
from prior commit, none introduced by this session.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes a multi-bug knot in player motion outbound + remote inbound,
discovered via cdb live trace of retail (2026-05-01) and follow-up
visual verification.
Outbound (acdream → ACE):
- JumpAction velocity is BODY-LOCAL, not world (per retail
CPhysicsObj::get_local_physics_velocity at 0x00512140 + ACE
Player.HandleActionJump's set_local_velocity call). Was sending
world; observers saw jump rotated by player yaw.
- Capture get_jump_v_z BEFORE LeaveGround() — the latter resets
JumpExtent to 0, after which get_jump_v_z returned 0. Was sending
Z=0 in every JumpAction.
- Backward/strafe-left jumps lost their horizontal velocity because
LeaveGround → get_state_velocity returns zero for non-canonical
motion (faithful to retail's FUN_00528960; retail papers over via
adjust_motion translation, not yet ported). Compute the correct
body-local launch velocity from input directly and push it back
into the body so local prediction matches what we send.
- IsRunning HoldKey was gated on `input.Run && input.Forward`, so
strafe-run and backward-run incorrectly broadcast as walk to
observers — ACE then animated walk + dead-reckoned at walk speed
while server position moved at run speed (visible as observer
lag). Fixed: gate on any active directional axis.
- AutonomousPosition heartbeat 0.2s → 1.0s to match holtburger's
AUTONOMOUS_POSITION_HEARTBEAT_INTERVAL and the ~1Hz observed in
retail trace.
- Heartbeat now fires while in-world regardless of motion state
(matches holtburger + retail's transient_state-based gate, not
motion-based). Pre-fix the at-rest heartbeat was suppressed.
Inbound (ACE → acdream, remote retail player):
- Remote backward walk arrives as cmd=WalkForward + speed=-1.91
(retail's adjust_motion'd form). Two bugs were stacking:
1. AnimationSequencer fast-path returned without updating when
sign(speedMod) flipped while motion stayed equal — kept playing
forward at old positive framerate. Fixed: bypass fast-path on
sign change so the full re-setup runs.
2. GameWindow clamped negative speedMod to 1.0 when stuffing
InterpretedState.ForwardSpeed, making get_state_velocity
produce forward velocity. Fixed: pass speedMod through verbatim
so the dead-reckoning body translates backward.
Issue #38 filed: 30Hz physics tick produces a chase-camera smoothness
regression at 60+ FPS render. Standard render-time interpolation is
the recommended fix (separate phase).
Findings + comparison vs retail/holtburger:
docs/research/2026-05-01-retail-motion-trace/findings.md
docs/research/2026-05-01-retail-motion-trace/fixes.md
TODO: port retail's adjust_motion (FUN_00528010) properly so
get_state_velocity works for all directions natively — would let us
drop the workaround in PlayerMovementController jump path and the
clamp in GameWindow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Filed as #37 after a ~3 hr investigation that ruled out animation source,
backface culling/winding, palette overlay, and head-GfxObj polygons.
Confirmed:
- Stub is from part 9 (upper torso/coat) post-AnimPartChange (gfx 0x0100120D)
- Part 9's both surfaces ARE matched by our 2 TextureChanges
- Server data complete; composition formula matches ACME + retail decomp
Untested hypothesis space (next session):
- Texture decode chain (compare our SurfaceDecoder vs ACME TextureHelpers)
- Polygon-to-surface index off-by-one on part 9
- Multi-layer texture composition AC may do
- UV mapping bug
Diagnostic env vars committed to source for next-session reuse:
- ACDREAM_HIDE_PART=N — hide specific humanoid part to localize bugs
- ACDREAM_NO_CULL=1 — disable backface culling
- ACDREAM_DUMP_CLOTHING=1 — dump APC + TC + per-part Surface chain coverage
Bug was originally reported as "head/neck protrudes forward"; the apparent
forward shift turned out to be an optical illusion from the missing
coat collar. Math + cdb-ground-truth + ACME comparison confirmed the
head placement is correct retail-faithful — see #37 for the long write-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A 2026-04-30 cdb live trace of retail acclient.exe attached during
sky observation revealed the missing infrastructure behind acdream's
three open sky bugs (#2 lightning, #28 aurora, #29 cloud density).
Live trace over 24,576 GameSky::Draw frames:
GameSky::Draw = 24,576 (60 Hz render rate)
GameSky::UseTime = 12,288 (30 Hz — half rate)
GameSky::CreateDeletePhysicsObjects = 12,288 (30 Hz, MinQuantum gate)
CPhysicsObj::CallPES = 372 (~150/min average)
CallPESHook::Execute = 372 (1:1 with CallPES)
CreateParticleHook::Execute = 62 (15 initial + 47 burst)
CPhysicsObj::create_particle_emitter = 62 (matches CreateParticleHook)
Three concrete findings:
1. Retail HAS persistent particle emitters on celestial / sky objects.
15 created at cell load; dynamically spawned on region/weather/time
transitions (the trace caught a +47 burst on one such transition).
2. The PES script-hook system drives existing emitters periodically.
`CallPESHook::Execute` fires script-scheduled actions which call
`CPhysicsObj::CallPES` 1:1, ~150 times per minute.
3. Earlier research saying "GameSky doesn't read pes_id" was correct
in scope but missed that the dispatch chain runs through the
script-hook system (not from inside GameSky directly).
`CelestialPosition.pes_id` is consumed downstream.
Files a new issue #36 that consolidates implementation work for
all three sky bugs into one porting effort. Adds cross-references
in #2, #28, #29 pointing at #36.
Decomp anchors for next session:
CallPESHook::Execute @ 0x00526e20
CreateParticleHook::Execute @ 0x00526ec0
CPhysicsObj::CallPES @ 0x00511af0
CPhysicsObj::create_particle_emitter @ 0x0050f360
GameSky::CreateDeletePhysicsObjects @ 0x005073c0
CelestialPosition.pes_id @ struct offset +0x004
Memory file `project_retail_debugger.md` updated separately with the
sky-PES breakthrough so future sessions inherit the context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three intertwined changes from a single investigation session driven by
attaching cdb to a live retail acclient.exe (v11.4186, Sept 2013 EoR
build) and tracing what retail actually DOES on the steep-roof wedge
scenario the user reported in acdream.
═══════════════════════════════════════════════════════════
1. L.5 — physics-tick MinQuantum gate (PlayerMovementController)
═══════════════════════════════════════════════════════════
Retail's CPhysicsObj::update_object subdivides per-frame dt into 1/30 s
sized integration steps and SKIPS entirely when accumulated dt is below
MinQuantum. Live trace evidence:
update_object = 40,960 calls
UpdatePhysicsInternal = 25,087 calls (61%)
i.e., 39% of update_object calls return early via the MinQuantum gate.
Retail's effective physics tick rate is 30Hz even at 60+ Hz render.
acdream's PlayerMovementController bypassed the existing PhysicsBody.
update_object and called UpdatePhysicsInternal(dt) directly each render
frame, which compressed bounce-energy / gravity-tangent accumulation
into half the time and amplified our steep-roof wedge dynamics.
Fix: add `_physicsAccum` accumulator. Integrate only when accumulated
dt ≥ MinQuantum (clamped to MaxQuantum to bound stale-frame jumps).
HugeQuantum drops accumulated time to discard truly stale frames
(debugger break, GC pause). Render still runs at full rate; only the
physics step is gated.
═══════════════════════════════════════════════════════════
2. Phase 3 reset retail-faithful kill_velocity (TransitionTypes)
═══════════════════════════════════════════════════════════
Retail's reset path (acclient_2013_pseudo_c.txt:273231-273239) gates
kill_velocity on `last_known_contact_plane_valid`:
if (last_known_valid == 0) {
set_collision_normal(step_up_normal); return COLLIDED;
}
kill_velocity(this);
last_known_valid = 0;
return COLLIDED;
Earlier in this session I deviated to "unconditional kill_velocity" as
a hypothesis-driven wedge fix. The live trace then showed the
deviation CAUSED a different wedge by zeroing V every frame, leaving
the body with no tangent momentum to escape (V = (0,0,0) for 169
consecutive frames while position pre/resolved frozen). The retail-
faithful gate is restored.
Note: the gate rarely fires in normal airborne play because our L.2.4
proximity guard clears last_known_valid soon after the body separates
from its remembered floor. Live retail trace also showed
kill_velocity = 0 hits over an entire play session — same behavior. So
acdream's kill_velocity is correct as ported now.
The supporting ObjectInfo.VelocityKilled flag + StopVelocity wiring +
PhysicsEngine.ResolveWithTransition consumer that actually zeros
body.Velocity when the flag is set — these were a no-op stub before
this session and are now correctly wired. Retail anchor:
OBJECTINFO::kill_velocity → CPhysicsObj::set_velocity({0,0,0}, 0) at
acclient_2013_pseudo_c.txt:274467-274475.
═══════════════════════════════════════════════════════════
3. Retail debugger toolchain (#35)
═══════════════════════════════════════════════════════════
When the question is "what does retail actually DO at runtime?" — not
"what does retail's code SAY" — the decomp at docs/research/named-retail/
is invaluable but doesn't capture state interactions across frames.
This commit ships infrastructure to attach Windows' cdb.exe to a live
retail acclient.exe with full PDB symbols and capture state at any
breakpoint.
- tools/pdb-extract/check_exe_pdb.py — reads any PE's CodeView entry
and reports MATCH / MISMATCH against refs/acclient.pdb's GUID.
Always run before attaching cdb. The matching v11.4186 build's
GUID is 9e847e2f-777c-4bd9-886c-22256bb87f32.
- tools/pdb-extract/dump_pdb_info.py — dumps a PDB's expected
build timestamp + GUID + age. Used to figure out which acclient.exe
build pairs with our PDB.
CLAUDE.md gets a Step -1 in the development workflow ("ATTACH cdb
TO RETAIL when behavior is the question, not code") and a full
"Retail debugger toolchain" section with the workflow, sample .cdb
script structure, and watchouts (PDB names use snake_case for some
classes / PascalCase for CPhysicsObj; ; is cdb's command separator;
killing cdb kills the debuggee; high-hit-rate breakpoints lag the game).
memory/project_retail_debugger.md captures the workflow + key findings
so future sessions inherit the toolchain by reading project memory.
═══════════════════════════════════════════════════════════
4. BSPQuery Path 6 slide-tangent restored (b1af56e behavior)
═══════════════════════════════════════════════════════════
After this session's retail-strict experiments showed that retail-
faithful Path 6 (SetCollide + Phase 3 reset chain) produces a
"lands on roof in falling animation, can't slide off" half-state in
acdream — because our acdream port of step_up_slide / cliff_slide is
incomplete for grounded-on-steep movement — the L.4 slide-tangent
deviation from commit b1af56e is restored as the pragmatic ship state.
The deviation: when an airborne sphere hits a polygon whose normal Z
is below FloorZ (≈ 0.6642, slope > ~49°), project the move along the
steep face to remove the into-wall displacement, set CollisionNormal +
SlidingNormal, return Slid. Body never gets ContactPlane on the steep
poly, never gets the half-state, slides off the slope under gravity's
tangent contribution.
Retail-strict requires the deeper step_up_slide / cliff_slide audit
(filed under #32). Until that lands, slide-tangent is the right
deviation — produces user-acceptable "slide off the roof" behavior.
═══════════════════════════════════════════════════════════
Test status: 833/833 green.
Refs:
acclient_2013_pseudo_c.txt:283950 (CPhysicsObj::update_object)
acclient_2013_pseudo_c.txt:273231-273239 (Phase 3 reset path)
acclient_2013_pseudo_c.txt:274467-274475 (OBJECTINFO::kill_velocity)
acclient_2013_pseudo_c.txt:323783-323821 (BSPTREE::find_collisions Path 6)
Closes#35. Updates #32 with L.4/L.5 status.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase L.4 closes the "stuck in falling animation on a steep roof" bug
the user reported on 2026-04-30 ("I jump up, I land on it. It should not
even let me land, should just slide with a falling animation"). After
this commit the body no longer sticks to a steep roof when jumping
into it — it slides along the slope while keeping the falling animation.
Two pieces:
1. BSPQuery Path 6 steep-poly slide
When an airborne sphere hits a polygon whose world normal Z is below
FloorZ (≈ 0.6642, slope > ~49°), the previous flow was:
Path 6 SetCollide → Path 4 set_walkable → ContactPlane committed →
body "lands" on the steep poly with Contact bit + falling animation.
This left the player stuck mid-slope because OnWalkable was cleared
but Contact stayed set.
The new branch detects the steep normal in Path 6 BEFORE SetCollide
is called. Instead of entering the landing path, it removes the
into-wall component of the move (project onto the steep face), sets
CollisionNormal + SlidingNormal, and returns Slid. Same shape as
Path 5's step-up fallback and CylinderCollision. The resolver retries;
the sphere is now outside the poly; FindCollisions returns OK;
ValidateTransition commits the slid position. ContactPlane is never
set, so the body stays airborne with falling animation.
2. PlayerMovementController L.3a-bounce carve-out + Inelastic stop
Re-enables the velocity-reflection bounce when the contact normal is
upward-facing but steeper than walkable (0 < N.Z < FloorZ). The base
L.3a rule suppresses bounce on landing transitions to avoid micro-
bounce on flat terrain; that suppression also stuck the player to
too-steep roofs they shouldn't land on. This carve-out re-enables
the reflection specifically for the steep upward case.
Also lands related L.2c precipice / edge-slide work that was in flight:
- TransitionTypes EdgeSlideAfterStepDownFailed: walkable-poly-steep
cliff route + steep-ContactPlane cliff route ordering, so that
CliffSlide fires when the stored walkable polygon itself is too
steep (Path 4 had previously accepted it as a "landing" via the
permissive LandingZ threshold).
- CliffSlide reference-normal selection: prefer LastWalkable, fall back
to LastKnownContactPlane only when walkable, else use world-up. This
prevents the cross(steepN, steepN) = 0 degenerate case that left the
cliff slide as a no-op when both current and last-known were steep.
- Phase 2 / step-down branch / edge-slide branch / cliff-slide
diagnostic helpers gated on ACDREAM_DUMP_EDGE_SLIDE / ACDREAM_DUMP_STEEP_ROOF.
- Two new airborne-mover regression tests in BSPStepUpTests +
PhysicsEngineTests covering wall-slide and edge tangent motion.
DEVIATION FROM RETAIL — DOCUMENTED FOR FOLLOW-UP
The Path 6 steep slide is NOT what retail does. Retail's flow on the
same hit is:
Path 6 SetCollide (no steep check) → Path 4 find_walkable returns
nothing for steep → Phase 3 reset path: restore_check_pos +
kill_velocity → return COLLIDED → validate_transition reverts CheckPos
to CurPos and forces OK.
Net retail behavior: position reverts to pre-failed-move (typically
just below the roof in the common jump-up case), velocity zeroed,
gravity rebuilds Z next frame, body falls back down naturally with
the falling animation. The "freeze" framing I used earlier was wrong;
in the typical case retail just bounces the body off and lets gravity
take over.
Strict retail behavior would match the user's intent better in the
common case AND avoid the bounce-energy-accumulation we saw with the
slide-tangent approach (V grew to ~50 m/s in continuous-contact frames).
However, retail's behavior degenerates in the edge case of an overhead
landing onto a steep slope (body would freeze mid-air above the roof).
This commit ships the slide-tangent fix as an interim "much better"
state per user verification on 2026-04-30. Follow-up work to match
retail strictly: revert Path 6 steep-slide, audit Phase 3 reset to
ensure kill_velocity (matching OBJECTINFO::kill_velocity ->
CPhysicsObj::set_velocity({0,0,0}, 0)) actually fires, and re-test.
Refs:
- acclient_2013_pseudo_c.txt:323784-323821 (Path 6 SetCollide)
- acclient_2013_pseudo_c.txt:273191-273239 (Phase 3 reset path)
- acclient_2013_pseudo_c.txt:272563-272596 (validate_transition revert)
- acclient_2013_pseudo_c.txt:274467-274475 (kill_velocity)
- acclient_2013_pseudo_c.txt:282699-282715 (handle_all_collisions bounce)
Tests: 833/833 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port the first retail precipice-slide slice from named retail/ACE: terrain and BSP walkable hits now preserve polygon vertices, failed step-down edges back-probe to rediscover the walkable polygon, and edge-slide can run precipice/cliff slide instead of only hard-stopping.
Adds pseudocode anchors plus regression coverage for terrain polygon context and loaded-terrain boundary edge-slide.
Co-authored-by: Codex <codex@openai.com>