The funnel's retail-ordered dispatches now go directly into the entity's
motion-table stack via Motion/MotionTableDispatchSink (Core):
ApplyMotion → PerformMovement(InterpretedCommand), StopMotion →
PerformMovement(StopInterpretedCommand). No axis collection, no
single-cycle priority pick, no Commit pass, no HasCycle probe, no
Run→Walk→Ready fallback chain — GetObjectSequence 0x00522860 +
is_allowed decide (closes H4, H5-callers, H11-callers, H15, H17-carry).
Run-while-turning now blends for real: the turn is a Branch-4 modifier
combined over the untouched run substate (AP-73 DELETED from the
register).
AnimationSequencer gains the public PerformMovement passthrough (lazy
initialize_state + locomotion velocity synthesis on success — AP-75;
remote body translation via PositionManager.ComputeOffset depends on
CurrentVelocity) and InitializeState(); HasCycle DELETED (the miss
hazard is structurally gone — GetObjectSequence checks the cycle before
any surgery; new conformance test pins sequence+state untouched on a
miss).
GameWindow: sink swap with the TurnApplied/TurnStopped ObservedOmega
callbacks (H17 carried verbatim — register AP-76, retire R6); spawn +
door sites run retail's enter-world order (initialize_state installs
the table default, the wire's initial motion dispatches unguarded — the
L.1c fallback chains deleted); despawn drains the pending queue via
HandleExitWorld (MotionDone success:false per entry, 0x0051bda0).
5 new MotionTableDispatchSink conformance tests (lazy init, Branch-4
turn blend + callback, Case-B stop unwind, Case-A stop re-drive,
miss no-op). Full suite green: 3,462 passed.
Live smoke vs ACE: in-world, NPC emote/Ready UMs dispatching through
the new path, [MOTIONDONE] completions firing across entities — first
live proof of the Q3→Q4→Q5 chain. Zero exceptions.
Registers: AP-73 deleted; AP-76 added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AnimationSequencer becomes a thin shim over the verbatim R2 stack:
SetCycle dispatches the style change (Branch 1) then the motion (Branch
2/3/4) through MotionTableManager.PerformMovement; PlayAction is the
same dispatch (actions rebuild via Branch 3; modifiers are Branch 4
physics-only combine_motion — the AP-73 turn-blend mechanism now runs
for real). CurrentStyle/CurrentMotion/CurrentSpeedMod are read-only
mirrors of MotionState (post-adjust_motion, signed mod). Lazy
initialize_state on first drive (retail lazy-create analog) keeps
undriven sequencers do-nothing.
DELETED legacy inventions (net -648 lines): the adapter fast-path
(replaced by Branch-2 fast re-speed: change_cycle_speed +
subtract/combine_motion), Fix B + IsLocomotionCycleLowByte (replaced by
remove_redundant_links on the pending queue — the retail mechanism the
2026-05-03 cdb trace pointed at), the stop-anim low-byte fallback
(replaced by get_link's reversed-key double-hop — retail's actual
backward-walk settle plays the windup link REVERSED), GetLink/BuildNode/
EnqueueMotionData + the G17 HasVelocity/HasOmega gate (add_motion sets
unconditionally), MultiplyCyclicFramerate + the G13 composite,
PlayAction's insert-before-tail machinery, SCFAST/SCFULL diagnostics.
KEPT at the adapter (register rows): the boundary adjust_motion remap +
locomotion velocity/omega synthesis (AP-75, retire R3-W6/R6), K-fix18
skip-link as post-dispatch RemoveAllLinkAnimations (AP-74, retire
R3-W4 when LeaveGround fires it).
GameWindow queue-drain wiring (the §4 G6 seam): each drained AnimDone
hook → manager.AnimationDone(true) (retail AnimDoneHook::Execute
0x00526c20 → Hook_AnimDone 0x0050fda0 chain), UseTime once per tick
(0x00517d57/0x00517d67); IMotionDoneSink bound to an
ACDREAM_DUMP_MOTION recorder (AD-36 — R3 rebinds to
MotionInterpreter.MotionDone).
Conformance: the 11-scenario pre-cutover trace suite (a6235a36) replays
green with six EXPECTED-DIFF annotations (double-hop walk-run routing,
reversed settle links, Branch-4 physics-only modifiers, Branch-1
style-change links, post-adjust mirrors) — everything unannotated is
byte-identical. Legacy suites repaired by a dedicated agent (fixtures
gain the retail-mandatory StyleDefaults + class-bit-tagged ids;
reflection state-poking replaced with real dispatch; zero category-D
regressions — the suspected cursor bug was a fixture class-bit gap),
plus two vacuously-passing tests fixed. Register: stale IA-4 deleted
(R1-P1 ported the negative-factor swap).
Full suite green: 3,458 passed (374+425+713+1946).
Closes H6, H7, H8, H9, H10-adapter, H16-wiring (r2-port-plan.md §3 Q4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Workflow-produced pin pass over the R3 decomp extraction (3 independent
raw re-readers + adversarial refuters on the two load-bearing pins +
synthesis). No pin was refuted; none blocks on cdb.
Headlines:
- A1: motion_allows_jump 0x005279e0 is a BLOCKLIST (0 = pass, 0x48 =
blocked) — the BN extraction's whitelist annotation was inverted
(corrected in-place in §3a + §10). Retail blocks FALLEN 0x40000008
and PASSES Falling 0x40000015; ACE mis-transcribed the exact-id term
as Falling (one-off slip — ACE's own charge gates use Fallen).
Definitive literal-uint blocklist table recorded.
- A3: the raw-vs-interpreted dual-dispatch gate is IsThePlayer (vtable
slot +0x14, bound via the ACCWeenieObject vftable dump @0x007e3ea0),
NOT ACE's IsCreature — in all four functions. Anti-artifact proof:
HitGround/LeaveGround nearby call the +0x2c IsCreature slot, so BN
distinguishes the slots locally. Copying ACE's gate would send
remotes down apply_raw_movement against an empty raw state.
- A4: MovementParameters absolute-mask table pinned from acclient.h's
own bitfield struct; retail ctor default 0x1EE0F has can_charge
CLEAR (ACE sets it true) and walk_run_threshold 15.0 (ACE 1.0).
- A5/A6: both jump-velocity epsilons are 0.000199999995f (acdream's
0.001 must change); get_leave_ground_velocity's fallback matrix is
GLOBAL→LOCAL (index-pattern match against Frame::globaltolocalvec).
- A10: definitive error-code table from an exhaustive return-site
sweep, incl. a second 0x24 site (DoInterpretedMotion action-class
contact block) absent from the plan row.
- Adjacent: move_to_interpreted_state's apply_current_movement arg2 is
a garbled allowJump = (motion_allows_jump(old fwd) == 0) — polarity
trap for W-commits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports retail's MotionTableManager (0x0051bxxx; r2-motiontable-decomp.md
§11) above the Q2 CMotionTable: add_to_queue 0x0051bfe0 (append +
immediate collapse), remove_redundant_links 0x0051bf20 (TAIL-ANCHORED
single scan, NOT ACE's restructured loop — cycle-tail block mask
0xb0000000 = STYLE|MODIFIER|ACTION, style-tail 0x70000000 =
CYCLE|MODIFIER|ACTION; the decomp's prose gloss of those masks was
imprecise, the literal constants are ported), truncate_animation_list
0x0051bca0 (zero NumAnims IN PLACE from the tail through the matched
node's successor + CSequence.RemoveLinkAnimations), AnimationDone
0x0051bce0 (counter-driven countdown chain: one call can pop MULTIPLE
entries; action-class pops MotionState's action FIFO; drained-list
counter reset), CheckForCompletedMotions 0x0051be00 / UseTime (zero-tick
sweep, success hardcoded true), initialize_state 0x0051c030 (0x41000003
sentinel), HandleEnterWorld/HandleExitWorld drains (MotionDone
success:false; enter also strips link anims), PerformMovement 0x0051c0b0
(error codes 7 / 0x43 / 0; StopCompletely queues the Ready sentinel
UNCONDITIONALLY).
IMotionDoneSink is the R2 seam standing in for CPhysicsObj::MotionDone —
R3 binds MotionInterpreter.MotionDone (r2-port-plan.md §4 contract).
This queue + remove_redundant_links IS the retail mechanism our old
'Fix B' rapid-motion collapse approximated — Q4 deletes Fix B and routes
SetCycle/PlayAction through PerformMovement.
47 conformance tests: countdown-chain tables, truncate blocked/allowed
matrices for both masks, zero-tick vs counter sweep, world drains, the
walk-run-walk-run collapse trace, the 2026-05-03 walk-to-run golden
(both nodes queued, truncate not firing), PerformMovement error matrix.
Register: AD-34 extended (pending_animations managed LinkedList);
AD-35 added (NotHandled sentinel vs retail's dead-code pointer-leak
default case).
Implemented by a dedicated agent against the committed Q3 spec; diff
scope, mask semantics, truncation range, counter reset, and
PerformMovement paths independently verified against the decomp raw
text before commit. Build + full suite green (3,447 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- r2-motiontable-decomp.md: 1,603-line verbatim extraction — full
CMotionTable (GetObjectSequence all 4 class branches, get_link,
is_allowed, re_modify, StopSequenceMotion, SetDefaultState, wrappers),
the free functions (add/combine/subtract_motion, change_cycle_speed,
same_sign), all 16 MotionTableManager members (pending_animations,
add_to_queue, remove_redundant_links with the 0xb0000000/0x70000000
block masks, truncate, AnimationDone vs CheckForCompletedMotions,
PerformMovement with the 0x41000003 stop sentinel), MotionState's
full modifier-stack/action-FIFO cast, verbatim struct layouts +
constants table. BN mistypings identified (SurfInfo lookups are
style_defaults/links hashes).
- r2-ace-motiontable.md: ACE cross-ref with the two-tracker headline
(MotionTableManager UPSTREAM of MotionInterp — never merged) + 5
flagged ACE oddities.
- r2-port-plan.md: 17 gaps (H1-H17), keep list, Q0-Q6 commit sequence,
the MotionDone->R3 boundary contract.
- Q0-pins.md: A1/A2 pinned to ACE's reading (three corroborations;
cdb confirmation folds into the next live session), A3 outTicks
decode, A4 ACE-oddity adjudications (the Action-branch double-count
is an ACE bug — do not copy), A5 Bitfield check at Q2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Advance(dt, Frame? rootMotionFrame) overload: retail's actual root-
motion contract (CSequence::update(quantum, Frame*) 0x00525b80) —
every crossed frame's pos_frame combines into the caller's Frame plus
the sequence velocity/omega via apply_physics. This is the seam R6's
retail per-tick order (CPartArray.Update -> adjust_offset ->
Frame.combine) consumes.
- DELETED: ConsumeRootMotionDelta + the dead adapter accumulator fields
(zero external callers; gap-map API-migration table).
- Root-motion test now asserts REAL accumulation through the wired
Frame path (replaces the P5 inert-stub pin).
- Phase R plan: R1 stage marked SHIPPED with its commit trail.
Full suite green (3346). R1 done: P0 research/pins -> P1 node -> P2
container -> P3 physics -> P4 advance core -> P5 adapter cutover ->
P6 wiring. Next: R2 (GetObjectSequence + MotionTableManager; extraction
workflow already running).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CSequence (Core/Physics/Motion): anim-node list with retail's exact
cursor semantics —
- append_animation (0x00525510): first_cyclic slides to the JUST-
APPENDED node on EVERY call (the cyclic tail is always the last
appended node); curr_anim seeds to head + get_starting_frame only
when null; unresolvable anims discarded (G10);
- remove_cyclic_anims (0x00524e40): removed curr_anim snaps BACK to
prev at get_ending_frame (or 0.0); first_cyclic = new tail;
- remove_link_animations/remove_all_link_animations (0x00524be0/
0x00524ca0): removed curr_anim snaps FORWARD to first_cyclic at
get_starting_frame (G11);
- apricot (0x00524b40, PDB-verified retail name): consumed-head trim
bounded by curr_anim AND first_cyclic;
- clear (0x005255b0) resets placement fields too — raw body is
authority over the gap map's G20 note;
- sequence-level velocity/omega with set/combine/subtract (G12);
- multiply_cyclic_animation_fr touches framerates ONLY (G13 —
velocity rescale belongs to R2's change_cycle_speed composite);
- placement frame family + floored accessors (G14).
Register: AD-33 (double vs x87 long double frame_number, G15),
AD-34 (managed LinkedList vs intrusive DLList).
17 list-surgery state-table tests; 39 total R1 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User mandate 2026-07-02: complete new movement + animation system,
verbatim retail equivalent, all entity classes, inbound + outbound, no
frozen code, no bandaids. Executed as a staged verbatim reconstruction
(R1 CSequence -> R8 cutover audit), each stage harness-gated with the
proven cdb-golden technique; legacy paths DELETED at each cutover.
Supersedes L.2g S3-S6; absorbs shipped S1/S2/S5 as components.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OnLiveMotionUpdated's remote branch (368 lines of bulk-copy + cycle
picker + per-axis DoInterpretedMotion + command-list router) collapses
to: build InboundInterpretedState from the wire (retail UnPack defaults;
MoveTo packets feed the PlanMoveToStart seed as the forward command) ->
MotionInterpreter.MoveToInterpretedState(ims, RemoteMotionSink) ->
sink.Commit().
RemoteMotionSink (new, App): receives the funnel's gate-passed
dispatches in retail order; the axis-priority pick (fwd > side > turn),
Run->Walk->Ready missing-cycle fallback, overlay routing, ObservedOmega
seeding, and diag lines are MOVED VERBATIM from the pre-S2 block.
Register row AP-73 documents the single-cycle composition approximation
(retail blends modifiers via re_modify — DEV-9, retires with S3/S6).
Retail-verbatim behavior changes:
- Absent stance now defaults to NonCombat 0x8000003D (retail UnPack
default, S0-trace-verified) instead of keep-current.
- Remote command lists flow through the funnel's 15-bit action-stamp
gate — retail's actual mechanism for ACE's re-bundled stale entries;
the old skip-SubState router workaround is now local-player-only.
- Airborne cycle preservation is the funnel's contact_allows_move gate
(K-fix17's guard semantics, now from the retail mechanism).
- Stops ride the same path: empty UM -> flat copy -> Ready dispatch ->
get_state_velocity 0 (DEV-3 core; the 300ms stop-detection fallback
stays until S6 verifies NPC unification).
Full suite green (3290). Live smoke next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pseudocode for the S2 port (unpack_movement case 0 / move_to_interpreted_state
/ apply_current_movement / apply_interpreted_movement / DoInterpretedMotion),
anchored on decomp lines + validated against a LIVE cdb trace of a retail
observer (per-UM DIM order confirmed: style -> forward -> sidestep-stop ->
turn-stop; empty UM = wholesale Ready stop).
Also settles the packer question: RawMotionState::Pack (0x0051ed10) is pure
static-default-difference — outbound L.2b port already verbatim; the
empty-vs-explicit walk variance between captures is driver-client state,
handled identically by the wholesale apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
S0 wire probe (live capture, retail actor via ACE) refuted the premise
the #39 machinery was built on: walk<->run Shift toggles arrive as
EXPLICIT UMs (0x0005 <-> 0x0007@runRate) because retail's default-
difference packing baselines forward_command against Ready — W-held is
always packed, and ACE re-emits it unconditionally with the holdKey
upgrade (MovementData.cs:104-119). The frequent flags=0 autonomous UMs
are genuine keys-released / heading-only states; retail applies them as
full stops (InterpretedMotionState ctor 0x0051e8d0 defaults Ready +
move_to_interpreted_state 0x005289c0 flat copy).
Retail has NO pace->animation adaptation anywhere in its inbound
pipeline (two decomp dives + ACE cross-check, deviation map DEV-2), and
the refinement layer's 0.2s-grace re-promotion after legitimate Ready
UMs was itself the observed Ready<->Run thrash / rubber-band component.
Deleted: ApplyPlayerLocomotionRefinement, UmGraceSeconds,
PlayerRunPromoteSpeed/PlayerRunDemoteSpeed, RemoteMotion.LastUMTime,
the synth-player refinement call in OnLivePositionUpdated. Player-remote
cycles are now UM-driven only, exactly like retail. NPC PlanFromVelocity
path untouched (S6 unifies).
S0 findings + S1 live validation (0 false UM_STALE drops across 280 UMs)
recorded in docs/research/2026-07-02-inbound-motion-deviation-map.md.
fix#39: closed — root-cause narrative corrected, machinery removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port retail's three-stamp inbound gate for 0xF74C UpdateMotion:
- MotionSequenceGate (Core/Physics): CPhysicsObj::is_newer (0x00451ad0)
wraparound u16 compare, verbatim per ACE PhysicsObj.is_newer (the BN
pseudo-C setcc returns are garbled; ACE + branch structure are the
oracle). Gates: INSTANCE_TS at dispatch (stale incarnation drops
before any stamp is touched), MOVEMENT_TS strictly-newer (stamped
BEFORE the server-control check, per CPhysics::SetObjectMovement
0x00509690), SERVER_CONTROLLED_MOVE_TS drop-when-stored-newer.
- Seed from CreateObject's PhysicsDesc timestamp block (index 1 =
ObjectMovement now parsed; ACE WorldObject_Networking.cs:411-420
order) — without seeding, entities whose movement sequence is past
0x8000 at spawn would drop every UM against a zero stamp.
Adopt-on-first / advance-only-after, so the #138 rehydrate replay of
retained spawns cannot regress live stamps.
- UpdateMotion + EntitySpawn now carry instance/movement/serverControl
sequences + isAutonomous (was parsed-past; isAutonomous feeds the
S2 funnel's last_move_was_autonomous). Gate wired at the top of
OnLiveMotionUpdated before any state mutation; [UM_STALE] diag under
ACDREAM_DUMP_MOTION / ACDREAM_REMOTE_VEL_DIAG; gate dropped with the
entity on DeleteObject.
Register: AD-32 added (adopt-newer-incarnation instead of retail's
QueueBlobForObject); TS-26 updated (UM side closed, UP side open).
Deviation map: docs/research/2026-07-02-inbound-motion-deviation-map.md.
19 new gate tests + parser coverage; full suite 3276 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/investigate deliverable for the inbound (remote-entity) animation+position
retail-parity effort. 10 deviations (DEV-1..10) mapped and adversarially
verified against the named retail decomp + ACE port + current code (9
confirmed, 1 refuted-and-corrected).
Headline: the #39-era UP-pace->cycle inference layer's premise ('wire goes
silent on Shift toggle') is refuted at both oracles — retail sends a fresh
MoveToState on HoldRun toggle while moving (0x006b37a8) and ACE rebroadcasts
every MoveToState unconditionally (GameActionMoveToState.cs:36); retail has
NO pace->animation adaptation anywhere (position error is absorbed solely by
the InterpolationManager chase, already ported verbatim in L.3).
Registers sub-lane L.2g in the roadmap: port the CMotionInterp inbound funnel
verbatim for all remote entity classes, slices S0-S6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fresh-session handoff for the INBOUND (remote-entity) motion arc. Captures the
symptom (walk<->run transition without stopping = interpreter reacts too slowly
-> animation+position desync compounding through turns; plus sliding + stop
position errors), the root-cause hypothesis (acdream lacks retail's CMotionInterp/
CSequence motion-transition state machine: pending_motions/MotionDone), the
pre-paid research (2026-06-04 sequencer deep-dive, 2026-06-26 audit D7-D12), the
decomp anchors, the inbound code map, the /investigate-then-port plan, the
live-capture setup, and the discipline reminders (report-only, surgical
sequencer integration, decomp-verbatim). Prior outbound/local arc shipped clean
through d34721fa.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The outbound MoveToState now sends the RAW forward_speed 1.0 (omitted by
default-difference packing) instead of the pre-computed runRate, matching what
retail's client sends.
Settled the open question with a live echo-test: acdream sends forward_speed=1.0,
ACE broadcasts back RunForward @ runRate (not 1.0), and a retail observer saw
+Acdream run at full pace. So ACE RECOMPUTES the broadcast run speed from the
character's run skill and auto-upgrades WalkForward+HoldKey.Run -> RunForward for
observers. The earlier PlayerMovementController comment citing ACE
MovementData.cs ("ACE relays the speed, so the wire must send run_rate") was
wrong; corrected.
Changes:
- PlayerMovementController section 6: forward wire command is WalkForward @ 1.0
(+ HoldKey.Run when running); LocalAnimationCommand still carries RunForward for
the local cycle. Backward already 1.0. The MotionStateChanged detection now
keys the walk<->run toggle off HoldKey + LocalAnimationCommand (forward_speed is
constant 1.0); the forward_speed comparison is retained but never fires.
- GameWindow: reverted the D6.2b echo-test one-liner; the wire RawMotionState
takes forward_speed from result (now 1.0).
The wire and the D6.2a local velocity are now both raw-1.0. Threading them onto
one shared RawMotionState (removing section-6's duplicate build) is a
behavior-neutral cleanup follow-up.
Full suite green (3255). Docs: roadmap + pseudocode doc updated with the
echo-test finding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strafe-left moves + symmetric, backward outpaces strafe (retail-faithful at
high run skill), jump travels lateral, turn feel unchanged, no crash/rejection.
Closes the visual-verification acceptance for the D6.2a velocity/turn/jump
unification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports retail's raw->interpreted motion normalization and routes the local
player's velocity through it, so backward/strafe come from the retail source
instead of hand-mirrored controller code (retires register TS-22 + UN-5).
MotionInterpreter (decomp-verbatim, Ghidra/ACE-cross-checked):
- adjust_motion (0x00528010): WalkBackward->WalkForward (x-0.649999976),
TurnLeft->TurnRight (x-1), SideStepLeft->SideStepRight (x-1), sidestep scale
0.5*(WalkAnimSpeed/SidestepAnimSpeed), RunForward early-return, per-channel
holdkey fallback to CurrentHoldKey.
- apply_run_to_command (0x00527be0): WalkForward->RunForward when speed>0 +
UNCONDITIONAL *runRate; TurnRight *1.5; SideStepRight *runRate clamp +/-3.0.
- apply_raw_movement (0x005287e0): copy raw->interpreted, adjust the 3 channels
with per-channel hold keys.
- WalkAnimSpeed unified to the retail-exact 3.11999989f (was 3.12f).
PlayerMovementController: build ONE RawMotionState from MovementInput at
forward_speed=1.0 (apply_run_to_command applies the run rate — a pre-scaled
speed would double-scale), run apply_raw_movement, then take grounded + jump
velocity straight from get_state_velocity (now correct for all directions) and
drive the keyboard turn omega from the interpreted turn_speed (BaseTurnRate x
turn_speed = the same pi/2 formula the remote path uses; numerically identical
to the old TurnRateFor). Deleted the hand-mirrored backward(x-0.65) /
strafe(x+-1.25) formulas in both the grounded and jump blocks. Mouse turn and
the auto-walk path are untouched.
Retail-faithful behavior changes (confirm in smoke): strafe is now
1.25 * ~1.248 * runRate = ~1.56*runRate, clamped via SideStepSpeed<=3.0 so
|v.X|<=3.75 (was 1.25*runRate, no scale/clamp); backward is unchanged
(3.12*0.65*runRate); backward/strafe-left velocity now comes from the pipeline
instead of returning zero. Forward run pace unchanged (4.0*runRate).
Tests: MotionNormalizationTests (17, adjust_motion/apply_run_to_command) +
MotionVelocityPipelineTests (10, apply_raw_movement->get_state_velocity golden
velocities incl. the strafe clamp + turn speeds). Full suite green (3255).
Register: TS-22 + UN-5 deleted (retired); TS-34 added (adjust_motion creature
guard is a no-op — IWeenieObject has no IsCreature; correct for the only caller,
the player). Pseudocode: docs/research/2026-07-01-d6-motion-interp-pseudocode.md.
NEXT D6.2b: the echo-gated wire forward_speed=1.0 question (evidence suggests
ACE relays rather than recomputes; verified via the smoke echo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Understand-phase output for D6: verbatim decomp + faithful pseudocode for
adjust_motion (0x00528010), apply_run_to_command (0x00527be0),
get_state_velocity (0x00527d50), apply_raw_movement (0x005287e0), all
cross-checked against the retail decomp (ACE not checked out in this worktree;
its constants match the decomp). Includes the acdream integration map (the
hand-mirrored backward/strafe bypass D6 replaces), the retail-faithful behavior
changes (strafe ~20% faster + 3.0 clamp), and the locked design decisions:
full RawMotionState unification (wire+velocity+turn), forward_speed=1.0 on run
verified via smoke echo, turn ported to interpreted omega (feel unchanged,
AP-9 base rate stays).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Login/run/turn/RunLock accepted by ACE (player motion echoed, no rejection),
NPCs animate including inbound MoveTo type-7 and TurnToHeading type-9 without
crash, graceful exit. Closes the visual-verification acceptance item for the
L.2b outbound wire-parity slice (D1/D3/D4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports the three retail outbound-movement packers verbatim (decomp-derived
golden bytes, confirmed via the Ghidra bridge, cross-checked vs holtburger):
- D1 — RawMotionState::Pack (0x0051ed10): new AcDream.Core.Physics.RawMotionState
data type (11 fields + actions, retail defaults) + RawMotionStatePacker that
sets a flag bit only when the field DIFFERS from its default. MoveToState.Build
now takes a RawMotionState instead of presence-based nullable params, so the
over-sent forwardSpeed=1.0 / currentHoldKey=None / default per-axis holdkeys are
no longer emitted. num_actions packs into bits 11-15 (not "bits 11-31").
- D3 — MoveToStatePack::Pack (0x005168f0) trailing byte =
(standingLongjump ? 0x02 : 0) | (contact ? 0x01 : 0); explicit contact/
standingLongjump params (standingLongjump=false honestly until the feature lands).
- D4 — JumpAction rewritten to retail JumpPack::Pack (0x00516d10): extent,
velocity, full Position, four u16 timestamps, align. Removed the spurious
objectGuid/spellId u32s; Position is now packed (it was absent). Body 56 bytes.
- Position::Pack (0x005a9640) / Frame::Pack (0x00535130) verified already-correct
(cellId, origin xyz, quaternion wxyz); locked with a golden test, no change.
GameWindow callers adapted minimally: build the RawMotionState from the existing
MovementResult values (behavior preserved except the intended D1 omissions) and
pass cellId/position/rotation to the Jump send. Pre-existing MotionInterpreter
placeholder struct RawMotionState renamed LegacyRawMotionState (D6/Phase-2 scope,
pure rename) to free the name for the retail-faithful type.
D5 audit: confirmed a real divergence — retail SendMovementEvent (0x006b4680)
stamps only last_sent_position_time after an MTS while SendPositionEvent
(0x006b4770) stamps all three; acdream's NotePositionSent stamps all three on
both paths. Left unchanged (comments added at both call sites), recorded as
register TS-33, deferred to a dedicated cadence-port slice.
Tests: RawMotionStatePackTests / MoveToStateGoldenTests / JumpActionTests /
PositionPackTests + updated MoveToStateTests / AutonomousPositionTests. Full
suite green (Core.Net.Tests 372, full solution 3228 passed / 4 pre-existing skips).
Register: TS-24/TS-25 refreshed (packer now supports actions/style; runtime
emission still deferred), TS-33 added. Roadmap L.2b shipped note added.
Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md (2-6)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Splits MotionCommandResolver's single ACE-modern lookup into an
IMotionCommandCatalog seam with two implementations:
- AceModernCommandCatalog (runtime default): built cleanly from the
DatReaderWriter MotionCommand enum (mirrors ACE + local DAT MotionTables)
with a documented class-priority tiebreak. The old blind 0x016E-0x0197
per-range override is DELETED — verified the ACE matrix (LifestoneRecall
0x0153 to 0x10000153, MarketplaceRecall 0x0166, AllegianceHometownRecall
0x0171, OffhandSlashHigh 0x0173) resolves correctly straight from the enum
with no override. Stale shift-start comment corrected to SnowAngelState
(0x43000115 to 0x43000118), not AllegianceHometownRecall.
- Retail2013CommandCatalog (conformance/reference): full verbatim extraction
of command_ids[0x198] at 0x007c73e8 (acclient_2013_pseudo_c.txt:1017259-1017667),
direct wire-low to full index lookup. 408 entries, anchors verified against
source ([0x150]=0x10000150, [0x153]=0x09000153, [0x197]=0x10000197).
MotionCommandResolver.ReconstructFullCommand stays a static facade delegating
to an AceModern singleton — all ~10 runtime callers unchanged.
Removing the override exposed a pre-existing bug: CombatAnimationPlanner's
late-combat command block uses 2013 numbering, not ACE/DRW. Corrected the one
catalog test assertion pinned to the override's output (wire 0x0170 to
0x09000170 IssueSlashCommand, its true DRW identity) and filed the planner bug
as #159 rather than silently patching out-of-scope code.
Tests: +catalog matrices (ACE/2013), class-priority collisions, boundary
cases, real-DAT availability (gap-doc hit counts reproduced). Build + full
suite green (Core.Tests 1718, no regressions).
Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md (1)
Research: docs/research/2026-06-26-ace-vs-2013-motion-command-gap.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI Studio (preview panels through the production renderer), importer dat-fidelity (Fix A/B/C/4/5:
the importer carries dat font/justification/color; boundary look=importer / state=runtime), and the
Character window Attributes tab (reads as retail). 3062 tests green.
Handoff: docs/research/2026-06-26-mockup-stage-handoff.md.
# Conflicts:
# docs/ISSUES.md
# docs/architecture/retail-divergence-register.md
Roadmap: extend D.5-remaining ledger with three new shipped entries (UI Studio,
importer Fix A/B/C, Character window 0x2100002E). Milestone M5: update D.2b
status paragraph to name all shipped sub-phases including the studio, importer
boundary discovery (look=importer / state=runtime), and Character window with
deferred polish pointer (#151).
ISSUES: add #151 (Character window deferred polish — LOW, filed 2026-06-26;
user accepted Attributes tab as good-enough, polish items to be enumerated later).
Issues #149 (studio inventory all-black) and #150 (GameWindow font resolver) were
already present.
Divergence register: add AP-69 (per-element dat-font resolver is studio-only;
GameWindow passes null to the four Import sites until #150 lands). No row for
Fix-4 UIState-group activation (runtime-faithful, not a deviation) or for the
tab-sprite injection / footer-state hiding (same).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers: what's done (UI Studio, faithful importer Fix A/B/C/4/5, Character window), the
look-vs-runtime boundary, the mockup goal + approach (host the production UiHost live),
must-fix issues (#149 inventory, #150 GameWindow fonts), patterns/lessons, reference map,
and first concrete steps. For the next agent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to Fix C (a0d3395): the per-element FontDid resolver is studio-only; GameWindow
passes null. Captured the turnkey fix (ConcurrentDictionary + GetOrAdd closure over UiDatFont.Load,
mirroring RenderStack.ResolveDatFont) + the 4 import sites + the CharacterStatController auto-benefit note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transcribed from user's retail screenshots during the character-window
polish session; documents name-white, large-gold-level, XP captions,
Infinity! cost, and the selected-row bar sprite — used as the acceptance
target for Fix A/B/C.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gap 1 — Header captions:
- 0x1000023A ("Character Level") above the level value: bound via LabelLeft().
- 0x10000234 ("Total Experience (XP):") left of total XP: bound via LabelLeft().
- 0x10000237/0x10000238 (XP-to-level label + value) are children of the UiMeter
(0x10000236, ConsumesDatChildren=true) and cannot be bound — documented in
code comments; XP meter fill still bound via Fill=XpFraction.
Gap 2 — Tab bar sprites:
- Tab group elements 0x10000228/229/538 are Type-12 UIElement_Text in the dat
(ConsumesDatChildren=true), so the three button children (left-cap, center,
right-cap) are consumed at import and absent from the widget tree. Old
SetTabState/SetButtonStateRecursive found no UiButton children to set.
- Fix: AddTabSprites() injects three UiText sprite-children per group using
the known RenderSurface ids confirmed from the retail UI layout dump:
Open (active) 0x06005D92/0x06005D94/0x06005D96
Closed (inactive) 0x06005D93/0x06005D95/0x06005D97
Source: dump nodes 0x10000439/0x100000E9/0x10000215 in layout 0x2100002E,
state_id 11=Closed, 12=Open per gmTabUI::SetActive(bool).
Gap 3 — Footer legibility:
- The shared footer child ids (0x1000024E etc.) appear in THREE footer-state
groups (A/B/C). ImportedLayout._byId stores the LAST duplicate = narrower
State B/C copies (145px labels). Fix: hide State B/C groups (footerB/footerC
Visible=false), walk State A container (0x10000240) positionally to bind the
wider State A labels (195px). FooterLine1Label now reads "Skill Credits
Available:" and FooterLine2Label reads "Unassigned Experience:" at full width.
Tests: 3 old tab-state tests (SetButtonStateRecursive expectation) replaced by
4 new sprite-injection tests + 2 caption-binding tests. Full suite: 676 pass,
0 fail (was 673 pass after 3 failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified root cause (oracle-first) of the user's "stretched + less-detailed"
outdoor terrain: acdream tiles the base ground texture once per 24m landcell
(terrain_modern.frag TILE=1.0) and never applies retail's landscape DETAIL
texture overlay (dat TerrainTex.DetailTextureId / DetailTexTiling).
Decomp-cited mechanism (landPolyDraw detail UV = curr_detail_tiling*baseUV
pc:702299; 10->50 depth fade pc:702263; TEXOP_MODULATE pc:425099; single
landscape scalar via TexMerge::GetDetailTiling pc:263852). Filtering + 512^2
resolution ruled out.
Fix attempt (parallel detail texture array + MODULATE2X + distance fade) built
and ran but rendered the ground black (detail array sampled ~0); reverted.
Handles verified NOT swapped -- the bug is detail-texture DATA (gray fill /
DetailTextureId decode). Filed with full debugging steps for a fresh pass.
Also recorded the DO-NOT-RETRY: the first investigation agent FABRICATED a base
`TexTiling` dat field that does not exist (only DetailTexTiling).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Element map for LayoutDesc 0x2100002E (the tabbed Attributes/Skills/Titles window) +
the importer-mount reality + the StringTable verdict + the follow-up list. Grounds the
CharacterStatController work in 0e644b5 and the remaining refinements.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rendered all 26 dump windows through the studio's --screenshot mode + a
System.Drawing contact-sheet montage. The generic dump LayoutSource renders
every panel with no crash; fidelity tracks static-chrome (in the dump) vs
runtime content (slots/stats/appraise, not in the dump). Static-rich panels
(vitals/toolbar/character/map/radar/effects/chat…) render great; runtime-only
windows (examine) are correctly blank; inventory shows backdrop+frame but its
nested sub-window chrome is sparse (next investigation). screenshots gitignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds Task 4b: a second load path for the UI Studio that reads the committed
retail UI layout dump (docs/research/2026-06-25-retail-ui-layout-dump.json)
and renders any of the 26 retail windows as a static sprite hierarchy.
New files:
- src/AcDream.App/Studio/UiDumpModel.cs — POCOs + System.Text.Json parse of
the dump (UiDump, DumpPanel, DumpNode, DumpRect, DumpStateSet, DumpImage,
DumpState + UiDumpModel static helpers: Parse, ListSlugs, PickImageId).
- src/AcDream.App/Studio/DumpLayout.cs — DumpLayout.Load(path, slug, resolve,
out err): parses the dump, finds the panel by slug, builds a UiElement tree.
Internal DumpSpriteElement draws its sprite via DrawSprite (not reusing
UiDatElement — avoids the ElementInfo/StateMedia dat-import dependency for
this static mockup). DumpGroupElement is a transparent container for Group
nodes. Rect basis is ABSOLUTE in the dump (verified: inventory root at
absolute x=500 and its children also start at x≈500 — child offset from
parent is 0–50px, not 500px); DumpLayout subtracts parent rect to produce
parent-relative Left/Top for each child.
- tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs — 5 tests covering:
inventory load with 0x100001D5 check + >= 40 nodes, unknown slug → null+err,
root at origin, children are parent-relative, all 26 slugs smoke-load.
Modified files:
- StudioOptions: adds DumpSlug + DumpFile fields; --dump <slug> and
--dump-file <path> args; ResolveDumpFile() walks up to the solution root
to find the default dump JSON (mirrors ConformanceDats.SolutionRoot()).
--dump suppresses the default vitals layout so the two modes are exclusive.
- StudioWindow.OnLoad: when DumpSlug is set, loads via DumpLayout (no
FixtureProvider, no controllers — static structure only); else falls
through to the existing LayoutSource + FixtureProvider path.
Results: DumpLayoutTests 5/5 passed; full AcDream.App.Tests 609 passed, 2
skipped (same as before); dotnet build green, 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A standalone dev tool that renders any acdream UI panel through the
PRODUCTION renderer (UiHost/LayoutImporter/dat-sprite path + the WB mesh
pipeline for the 3-D doll), with an ImGui click-to-inspect inspector,
sample-data fixtures, markup hot-reload + write-back, and render-config
sliders. Collapses the edit→build→login→F12→eyeball loop into edit→glance.
Full v1 scope (user pre-approved): both sources (dat LayoutDesc id +
markup file), full fixtures, editable inspector + doll-camera sliders,
live doll. Seven isolated units; the highest-risk step (extracting a
shared RenderBootstrap from GameWindow.OnLoad) is sequenced first behind
a "game still renders" gate, with a studio-local-duplicate fallback. The
drag-drop designer is deferred to phase 2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The paperdoll doll now matches retail: correct held pose, framing, and
facing. Three decomp-sourced fixes closed the visual gate.
Pose: cdb-confirmed m_didAnimation = 0x030003C0 (gmPaperDollUI), played
once + HELD (set_sequence_animation framerate=0, RedressCreature
0x004a3c22). Dumping the dat showed the 29-frame anim has only two
distinct keyframes — frame 0 (transitional, bent arm) and frames 1..28
(byte-identical: the settled stance, arms down + leg back) — so
ApplyPaperdollPose applies the LAST frame statically (no looping).
Camera: ported verbatim from UIElement_Viewport::SetCamera (decomp
0x004a5a39). position (0.12,-2.4,0.88); direction (0,0,0) => IDENTITY
view frame => look straight down +Y, ZERO yaw; FOV pi/4 (CreatureMode
ctor default 0x004543cf); ambient 0.3. The prior hand-tune aimed the
camera at mid-body, adding a ~2deg yaw that turned the doll's face away
— full-body framing comes from eye-height + FOV, not aiming.
Heading: retail Frame::set_heading(h) (0x00535e40) builds facing
(sin h, cos h); System.Numerics CreateFromAxisAngle(+Z, +h) rotates the
body's default +Y forward to (-sin h, cos h) — the X-lean was MIRRORED
(~22deg), the real cause of the turned-away face. Negate the angle to
land on retail's facing.
Wrap-up: stripped the temporary O/P pose-frame stepper + Slice2
diagnostics; divergence register AP-66 reworded, AP-67 (RTT doll render
vs in-cell CreatureMode::Render) + AP-68 (per-race UpdateForRace
unimpl) added; DollCameraTests pinned to the retail values + a zero-yaw
guard. tools/cdb/paperdoll-pose.cdb = the pose-DID capture script.
Build + full suite green (Core 1579 / Core.Net 343 / App 597 / UI 425).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the 134 D.2b UI commits onto the physics/collision development line so
main can fast-forward. Today's #149 (BSP-less static collision) + #150 (open
doors fully passable) + the full collision/streaming/dense-town-FPS arc meet
the paperdoll/inventory work.
# Conflicts:
# docs/ISSUES.md
# docs/architecture/retail-divergence-register.md
An OPEN door (ETHEREAL_PS 0x4 set on Use) still stopped the player with a
residual threshold block after the collision sweep, despite swinging open
visually.
The resolver runs two collision passes per step: the main sweep and a
step-down (foot-sphere "is there floor?") sub-pass. acdream tested the
ethereal door in BOTH; the main pass cleared it (BSPQuery Path 1 + the
Layer-2 override) but the step-down pass had no escape, leaving a Collided
result at the sill -- the "can-sized cylinder on the ground threshold".
Retail's CPhysicsObj::FindObjCollisions (pc:276795-276806) SKIPS an ethereal
target when sphere_path.step_down != 0 -- it only tests it in the main pass.
So an open door is fully passable everywhere; the swung panel's position is
irrelevant (ethereal = no collision; the swing animation is purely visual).
Port that branch verbatim: an ethereal-for-this-test target (target state &
0x4, OR mover-ethereal vs a non-static target) is `continue`d when
sp.StepDown is set.
Live-verified (user confirmed): the door now blocks 0x while open (0x1000C),
still blocks while closed (0x10008); pre-fix it blocked 217x while open.
Core suite green (1595/0).
(An earlier "animate the door collision" theory was wrong and dropped -- if
collision tracked the swung panel you'd bump the panel in its open position,
which retail does not do. The user caught it.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Town props placed as landblock stabs whose ONLY collision is a Setup
CylSphere/Sphere (no physics BSP) registered ZERO collision shapes and were
walk-through -- torches, braziers, lamp-posts, candle-stands, posts.
Root: the ISSUES #83 / A1.6 gate `!_isLandblockStab` skipped Setup
cyl/sphere registration for ALL landblock stabs, on the false assumption
"landblock stabs collide via BSP only (retail CBuildingObj)." That
over-broadened -- it also killed collision for BSP-less stabs.
Retail's CPhysicsObj::FindObjCollisions (@0x0050f050) uses binary dispatch:
the object's physics BSP if it HAS one (HAS_PHYSICS_BSP_PS 0x10000), ELSE its
CSetup CylSpheres/Spheres -- never both. Confirmed via live retail cdb on the
Holtburg torch (Setup 0x020005D8 at world (105.99,17.17)): FindObjCollisions
target num_cylsphere=1, cyl h=2.2 -- a cylsphere, exact-matching the dat
(cylSphere r=0.2 h=2.2); the StabList confirms stab[95]=0x020005D8 there.
Fix: gate the Setup cyl/sphere registration on `entityBsp == 0` instead of
stab-ness. Preserves #83's anti-doubling (stab WITH a BSP -> BSP-only) while
restoring collision for BSP-less stabs. Other landblock entities on this path
(scenery -- tree-trunk cylspheres) are unaffected.
Live-verified: torch + candle/brazier family block now; ~115 cyl/sphere
Setups register across streamed landblocks. Core suite green (1595/0).
The earlier selection-sphere hypothesis was WRONG and is reverted -- the cdb's
r=0.48 sphere was the player/NPC body (every body sphere is ~0.48), not the
torch. The correct cdb method: capture the TARGET at FindObjCollisions (not
`this` in CSphere::intersects_sphere) and confirm by position + Setup-id.
(Issue numbered #149 to stay clear of main's #148; this worktree branched
before main's #145-148 were added.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part A (D4): CObjCell::check_entry_restrictions (pc:309576) gate is omitted from
FindEnvCollisions. Investigation confirmed the gate requires restriction_obj (per-cell
access-lock entity id) + weenie-object-table dispatch (CanMoveInto / CanBypassMoveRestrictions)
— neither exist in acdream. CellPhysics has no restriction_obj field; DatReaderWriter
models no per-cell access locks. Gap is inert in all dev content (ACE starter area has
no access-locked env cells). Documented as AP-50 in the retail divergence register.
Part B (W1): Replace the hardcoded-false smell in BspOnlyDispatch with named internal
helpers PvpExempt() and MissileIgnore() that return false with retail oracle citations
(pc:276808-276841 and pc:274385 respectively). BspOnlyDispatch now folds all three
terms in retail's exact predicate structure. Behavior is byte-identical in M1.5 scope
(both stubs false ⇒ reduces to HAS_PHYSICS_BSP_PS check alone, same as before).
A6.P7 door dispatch unchanged.
Tests: 3 new guard tests in A6P7DispatchRulesTests — W1_PvpExempt_ReturnsFalseInM15Scope,
W1_MissileIgnore_ReturnsFalseInM15Scope, W1_BspOnlyDispatch_DoorStateStillDispatchesBspOnly.
Suite: 1590 pass / 0 fail / 2 skip (was 1587 + 3 = 1590).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retail oracle greps confirmed:
- CSphere::intersects_sphere @ 0x00537ae4 (pc:321692): the ethereal branch
is `void __thiscall` — all paths return void (no COLLIDED). The function
performs a proximity check only; no blocking result is produced.
- CCylSphere::intersects_sphere @ 0x0053b4a0 (pc:324573): same void-return
pattern — ethereal branch calls collides_with_sphere (check only, no slide),
all returns are void = passable.
Change: added `if (sp.ObstructionEthereal) return TransitionState.OK` at the
top of SphereCollision and CylinderCollision in TransitionTypes.cs, mirroring
the void-return semantics of both retail functions. The existing per-object
clear at pc:276989 (line 2837) still fires after the early OK return.
Before this fix: an ethereal-alone NPC/ghost with a Cylinder or Sphere shadow
shape would BLOCK the player (regression introduced when Task 3 made ETHEREAL-
alone fall through ShouldSkip instead of instant-skipping). After: all three
shape types — BSP (via BSPQuery Path 1), Sphere, and Cylinder — correctly pass
through when obstruction_ethereal is set.
Tests: added 4 tests to ObstructionEtherealTests.cs verifying:
- Ethereal Cylinder → passable (sweep passes through, no CollisionNormalValid)
- Ethereal Sphere → passable (same)
- Non-ethereal Cylinder → still blocks (regression guard)
- Non-ethereal Sphere → still blocks (regression guard)
Full Core suite: 1584 pass, 0 fail, 2 skip (pre-existing dat skips).
Pseudocode doc updated with confirmed cyl/sphere ethereal contracts and the
complete set/clear/consume flow summary.
Retail refs:
- CSphere::intersects_sphere @ 0x00537ae4 / acclient_2013_pseudo_c.txt:321692
- CCylSphere::intersects_sphere @ 0x0053b4a0 / acclient_2013_pseudo_c.txt:324573
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retail CPhysicsObj::FindObjCollisions (0x0050f050) only instant-skips when
BOTH ETHEREAL_PS (0x4) AND IGNORE_COLLISIONS_PS (0x10) are set (pc:276782).
ETHEREAL-alone sets sphere_path.obstruction_ethereal=1 (pc:276806) and
continues to the shape dispatch. BSPTREE::find_collisions (0x0053a496) routes
Path 1 (sphere_intersects_solid) when the flag is set (pc:323742): the open
door has no solid leaf at the doorway, so the test returns OK → player passes
through. CEnvCell::find_env_collisions (0x0052c144) clears the flag first so
ENV walls are never weakened (pc:309580, "D5 clear").
Changes:
- CollisionExemption.ShouldSkip: require BOTH bits for Gate-1 early-out
(previously ETHEREAL alone returned true — the AD-7 shim). Divergence
register row AD-7 deleted.
- SpherePath: add ObstructionEthereal field (mirrors retail
SPHEREPATH.obstruction_ethereal).
- FindObjCollisionsInternal loop: set sp.ObstructionEthereal=(target&0x4)!=0
before shape dispatch; clear it after (per-object clear pc:276989).
Also clear at the null-BSP continue site to keep flag clean.
- FindEnvCollisions: clear sp.ObstructionEthereal=false at top (D5 clear
pc:309580) — ENV cell walls are always solid.
- BSPQuery.FindCollisions Path 1: change `obj.Ethereal` (ObjectInfo.Ethereal,
always false — dead code) to `path.ObstructionEthereal`. Gate now correctly
mirrors retail pc:323742: PLACEMENT_INSERT || obstruction_ethereal.
Consume site change (BSPQuery.cs before/after):
BEFORE: if (path.InsertType == InsertType.Placement || obj.Ethereal)
AFTER: if (path.InsertType == InsertType.Placement || path.ObstructionEthereal)
Mirrors retail pc:323742 exactly. obj.Ethereal was dead code (ObjectInfo.Ethereal
is never set true anywhere); the correct flag is SpherePath.ObstructionEthereal.
Tests: 1580 pass / 0 fail / 2 skip (was 1576/0/2 + 4 new in ObstructionEtherealTests.
CellarUp, CornerFlood, DoorCollision, HouseExitWalk all green — no wall regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setup.Spheres were previously coerced to short cylinders (CylHeight=2*r),
which is geometrically wrong: a cylinder has flat caps; a sphere does not.
This ported CSphere::intersects_sphere (0x00537A80) so sphere-typed shadow
entries are tested as spheres — 3-D distance, no height clamping.
Changes:
- ShadowObjectRegistry.cs: added ShadowCollisionType.Sphere (enum value 2).
The BuildFloodSpheres anyCyl dedup at :232 is unaffected: only Cylinder
sets anyCyl=true; Sphere shapes fall through to the BSP-fallback path
(anyCyl=false → included), which is correct.
- ShadowShapeBuilder.cs: FromSetup now emits ShadowCollisionType.Sphere
(CylHeight=0) for Setup.Spheres instead of a short Cylinder.
- CollisionPrimitives.cs: added SweptSphereHitsSphere — quadratic swept
solve ported from ACE Sphere.cs::FindTimeOfCollision, which is a C# port
of retail's CSphere::intersects_sphere @ 0x00537A80. Sign convention
confirmed against the decomp: retail negates the root to produce a
forward t ∈ (0,1].
- TransitionTypes.cs: added Sphere narrow-phase branch between BSP and
Cylinder in FindObjCollisionsInCell; uses 3-D distance for overlap
(not XY-only). Added SphereCollision() method implementing the 3-D
wall-slide response. Updated diagnostic logging at :2734 to cover Sphere.
- Updated ShadowShapeBuilderTests for new Sphere type assertion.
- New SphereIntersectsSphereConformanceTests: 9 geometrically-anchored
cases (head-on, tangent, perpendicular-miss, lateral-near-miss,
sweep-away, beyond-step, degenerate-zero-sweep, already-overlapping,
vertical-sweep).
Retail oracle: CSphere::intersects_sphere @ 0x00537A80 (named-retail);
ACE Sphere.cs::FindTimeOfCollision (C# port, cross-confirmed).
Build: 0 errors, 10 warnings (pre-existing).
Tests: 1576 pass / 0 fail / 2 skip (1578 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retail oracle: CPartArray::InitParts@0x00517F40, CGfxObj::Serialize@0x00534970 (physics_bsp
gated on serialized-flags bit-0), CPhysicsPart::find_obj_collisions@0x0050D8D0 (returns OK /
passable when physics_bsp==null). Render-mesh bounds never enter collision.
Changes:
- GameWindow.cs: delete the ~200-line VISUAL mesh-bounds collision block (the
isPhantomSetup / isPhantomGfxObj locals + the if-block computing worldMin/worldMax
AABB + the ShadowObjects.Register call that capped and registered the synthetic
cylinder). Also removes dead counter variables scHaveBounds/scRegistered/scNoBounds/
scTooThin; trims the ProbeBuildingEnabled summary line accordingly.
- PhysicsDataCache.cs: delete IsPhantomGfxObjSource (the predicate that only existed
to fence the mesh-AABB synthesis; the "phantom" concept is now the default — no DAT
shape means no registration, verbatim with retail).
- PhysicsDataCachePhantomSourceTests.cs: deleted (tested the removed method).
- ShadowShapeBuilderShapeSourceTests.cs: new guard test — a Setup with parts but
hasPhysicsBsp=false and no CylSpheres/Spheres yields an empty shape list, locking
the DAT-only rule in the builder.
- retail-divergence-register.md: AP-2 row deleted (divergence retired).
Objects with no DAT physics shape (no CylSpheres, no Spheres, no part with a
PhysicsBSP) now register no collision shape and are passable, verbatim with retail.
Objects with real DAT shapes (BSP parts, CylSpheres) are unaffected.
dotnet build green, 22/22 tests passing (ShadowShapeBuilderShapeSourceTests +
CellarUpTrajectoryReplay + CornerFlood + Issue147ArwicBuildings replay harnesses).
Visual gate pending: walk Holtburg + open world; objects that become passable must
match retail (DAT has no physics shape — trees with real CylSpheres still solid).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19-agent verified audit of how retail decides which objects collide vs
acdream's per-channel filters. Confirms the user's "1 fix for all collision"
intuition: acdream already has retail's two-layer shape (per-cell shadow
registration + query-time exemption); the divergences are now narrow and
enumerable, not a scattered filter mess.
Audit (docs/research/2026-06-24-collision-inclusion-audit.md): 6 confirmed
deviations (D1 mesh-AABB phantom HIGH, D2 ETHEREAL-alone, D3 no sphere
primitive, D4 entry-restrictions, D5 obstruction_ethereal absent, D8 cell-
transform stale cache) + 2 refuted by the adversarial pass (D6 placement-
insert present in BSPQuery; D7 terrain pass-through is the #135/#138
streaming-gap, retail does it too). Loader verified faithful: CacheGfxObj
reads PhysicsBSP gated on HasPhysics; the mesh-AABB is a pure additive
non-faithful layer.
Design (docs/superpowers/specs/2026-06-24-unified-collision-inclusion-design.md):
"1 fix" = one DAT-only shape authority (delete mesh-AABB), one query
predicate, four faithful channels kept distinct (retail keeps find_env/
find_building/find_obj separate), one per-apply rebase invariant. 5
independently-gated slices. Retires register rows AP-2 + AD-7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>