Commit graph

2201 commits

Author SHA1 Message Date
Erik
c072b73686 docs(R2): record Q3+Q4 shipped + R3-W0 prep in the Phase R plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:42:02 +02:00
Erik
3b9d9bb6be feat(R2-Q4): adapter cutover — SetCycle/PlayAction rehosted on PerformMovement; Fix B / fast-path / stop-anim fallback / G17 gate DELETED
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>
2026-07-02 21:41:27 +02:00
Erik
cd0289bea2 docs(R3-W0): ambiguity pins A1-A10 — all textually resolved, adversarially verified
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>
2026-07-02 21:24:46 +02:00
Erik
a6235a36f5 test(R2-Q4a): pre-cutover adapter trace goldens
11 scripted scenarios (spawn idle, idle→walk link, walk→run cyclic-to-
cyclic, fast re-speed, backward-walk remap, stop-settle fallback, emote
action, K-fix18 skip-link, turn modifier, style change, link-drain
sentinel count) snapshotting the CSequence core's list state after every
SetCycle/PlayAction against the LEGACY adapter (fast-path + Fix B +
stop-anim fallback + G17 gate). These are the parity bar for the Q4
PerformMovement cutover — intentional post-cutover changes get
EXPECTED-DIFF annotations, everything else must stay byte-identical.

Notable pre-cutover behavior captured: the S6 link-before-link stacking
(old reversed link kept mid-drain while the settle link appends behind).

Adds the internal AnimationSequencer.Core test seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:12:15 +02:00
Erik
aa65990a1d feat(R2-Q3): verbatim MotionTableManager — the pending-animation queue (closes H3, H15-core)
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>
2026-07-02 21:01:50 +02:00
Erik
8eff397801 docs(R3-W-1): CMotionInterp-completion research base — decomp extraction + ACE cross-ref + port work-list
Workflow-produced R3 research (3 docs, 3,061 lines):
- r3-motioninterp-decomp.md: verbatim pseudo-C + anchors for the full R3
  scope — pending_motions lifecycle (add_to_queue 0x00527b80, MotionDone
  0x00527ec0), DoMotion 0x00528d20 + PerformMovement 0x00528e80, the whole
  jump family, HitGround 0x00528ac0 / LeaveGround 0x00528b00 (stale
  0x00529710 doc-comment corrected), enter_default_state, MovementManager
  relay surface, struct anchors, constants inventory. Negative results
  (IsAnimating / HandleUpdateTarget / CMotionInterp::HandleEnterWorld NOT
  in retail) explicitly recorded.
- r3-ace-motioninterp.md: ACE MotionInterp/MovementManager/MotionNode map
  with flagged ACE-isms (jump_is_allowed L747 NPE typo, Falling-vs-Fallen
  boundary discrepancy).
- r3-port-plan.md: 10 pinned ambiguities (headline: motion_allows_jump
  0x48 polarity INVERTED in the BN annotation — ranges are a BLOCKLIST;
  apply_current_movement dispatch gate IsThePlayer vs IsCreature), 19
  itemized gaps J1-J19, keep-list, 7-commit sequence W0-W7 ending in the
  local-player unification, exact IMotionDoneSink wiring spec vs R2 §4.

Precondition paragraph updated at vaulting: Q2 committed 98f58db9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:56:16 +02:00
Erik
615cd4dd74 docs(R2): record Q0-Q2 progress in the Phase R plan
Q0 pins dc54a3e4, Q1 MotionState 2345da30, Q2 CMotionTable 98f58db9.
Remaining R2 work: Q3 MotionTableManager, Q4 adapter cutover, Q5
RemoteMotionSink deletion, Q6 register sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:52:48 +02:00
Erik
98f58db913 feat(R2-Q2): verbatim CMotionTable — the GetObjectSequence dispatcher
CMotionTable (Core/Physics/Motion, 692 lines) wrapping the dat
MotionTable, per r2-motiontable-decomp.md + Q0 pins:

- GetObjectSequence (0x00522860): entry guards; modifier-class no-op
  fast path; Branch 1 style-change (exit link + style link +
  default_style double-hop + re_modify); Branch 2 cycle (UNCONDITIONAL
  default-style retry per label_522ae6, is_allowed gate, same-substate
  re-speed fast path = ChangeCycleSpeed + SubtractMotion(old) +
  CombineMotion(new), clear-modifiers bit0, direct-link vs !SameSign
  double-hop, A2 signedSpeed, outgoing-modifier re-registration,
  re_modify, outTicks per A3); Branch 3 action (direct link OR the
  4-layer out-and-back with the base cycle re-added at the OLD
  substate_mod; outTicks WITHOUT ACE's double-count, A4-#1); Branch 4
  modifier (PHYSICS-ONLY CombineMotion + AddModifier stop-then-re-add —
  the AP-73 retirement mechanism).
- get_link (A1 pin: either-negative -> swapped keys — the adapter's
  field-validated port re-homed), is_allowed (Bitfield & 2; A5
  CONFIRMED on DatReaderWriter 2.1.7), re_modify (deep-copy snapshot
  termination bound), StopSequenceMotion, SetDefaultState,
  DoObjectMotion/StopObjectMotion/StopObjectCompletely (A4-#4 return).
- Free functions: AddMotion (UNCONDITIONAL velocity/omega set — the
  G17 core), CombineMotion/SubtractMotion (physics-only),
  ChangeCycleSpeed (verbatim incl. the A4-#2 retail gap), SameSign.

44 conformance tests pinning H1/H4/H5/H7/H8/H10-H14 + all Q0 pins,
incl. the run-while-turning gated-cycle -> Branch-4 physics-only test
(retail's actual turn-blend mechanism) and missing-cycle ->
sequence-untouched (retires the HasCycle fallback rationale).

Implemented by a dedicated agent against the Q2 spec; diff + key
branches reviewed, suite re-verified (3934 green) before commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:26 +02:00
Erik
2345da30e9 feat(R2-Q1): verbatim MotionState (gap H2)
Style/Substate/SubstateMod + the two independent MotionList chains with
retail's exact disciplines: modifier PUSH-FRONT stack
(add_modifier_no_check 0x00525ff0; add_modifier 0x00526340 refusing
duplicates AND the current base substate; remove_modifier by node;
clear_modifiers) and action TAIL-APPEND FIFO (add_action 0x005260a0;
remove_action_head 0x00526120 returning the popped motion, 0 when
empty; clear_actions). Deep-copy ctor clones both chains (Q0-pins
A4-#5: re_modify's snapshot is a termination bound, never shared).

11 discipline-table tests. Next: Q2 (CMotionTable + free functions —
the GetObjectSequence dispatcher).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:31:16 +02:00
Erik
dc54a3e41f docs(R2-Q0): motion-table research base + ambiguity pins
- 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>
2026-07-02 20:29:26 +02:00
Erik
a987cad182 feat(R1-P6): root-motion Frame seam + dead-API removal — R1 COMPLETE
- 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>
2026-07-02 20:24:01 +02:00
Erik
9147344a6f feat(R1-P5): AnimationSequencer rehosted on the verbatim CSequence core
The adapter keeps its full public surface (every consumer compiles
unchanged) while the internals move to Motion.CSequence:

DELETED (legacy invented mechanisms, per the R1 gap map + Phase R
mandate): the AnimNode class + adapter queue/_currNode/_firstCyclic/
_framePosition, the ACE-fabricated FrameEpsilon boundary math, the
safety=64 advance cap, ClearCyclicTail surgery, the stale-head
_currNode relocation block, per-node IsLooping/Velocity/Omega, the
adapter-side AdvanceToNextAnimation/ApplyPosFrame/ExecuteHooks, the
per-node !IsLooping AnimationDone gate (now the core's list-structure
head != first_cyclic gate, G5).

REHOSTED: SetCycle rebuild = RemoveCyclicAnims (+ClearAnimations for
K-fix18) + retail add_motion appends (framerate-only AnimData scaling,
G10 loop membership: the LAST appended node is the cyclic tail) + Fix B
via RemoveAllLinkAnimations (the core's snap-forward IS Fix B's
behavior); Advance = core.Update(dt, null) with hooks flowing through
an IAnimHookQueue adapter into the existing ConsumePendingHooks drain;
fast path = core.MultiplyCyclicAnimationFramerate + the adapter-level
velocity rescale (R2's change_cycle_speed composite stand-in, G13).
Kept byte-identical: adjust_motion remap, GetLink + stop-anim fallback,
K-fix18, velocity synthesis, SlerpRetailClient, the #61 render-side
clamp, SCFAST/SCFULL diagnostics.

Tests: 2 updated with decomp citations — the reverse-start boundary now
pins retail's bare-int HighFrame+1 (0x00525c80, no epsilon, G1); the
root-motion accumulation test pins the inert-stub state pending P6's
Frame wiring (G7). All 44 sequencer tests + the 56 core tests + full
suite green (3346).

Implemented by a dedicated agent against the P5 spec; diff + tests
reviewed, suite re-verified before commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:20:58 +02:00
Erik
658b91d8aa feat(R1-P4): verbatim update_internal / update / advance_to_next_animation
The CSequence frame-advance core (gaps G3/G4/G5/G6/G8/G9/G19),
ACE-verified skeleton + retail constants + the P0-pinned leftover carry:

- update_internal (0x005255d0): frame_number += framerate*dt; overshoot
  clamps to the RAW high/low frame with leftover-time computation +
  animDone; the pose/physics/hook triple fires for EVERY crossed integer
  frame (ascending fwd / descending rev, strict > < boundaries, NO
  epsilon, NO safety cap); AnimDone is a LIST-STRUCTURE gate (head !=
  first_cyclic) queuing the global AnimDoneHook; iterative loop carries
  the leftover into the next node (P0 pin — a lag spike fast-forwards
  through multiple queued nodes in one tick).
- advance_to_next_animation (0x005252b0): four pose ops per transition
  (subtract1 outgoing @ current frame + residual physics; step — fwd
  wraps to first_cyclic, REVERSE wraps to the LIST TAIL, asymmetric by
  design; reseed from the incoming direction-aware boundary; combine
  incoming + physics). Pose ops run in BOTH directions — ACE's
  framerate-sign gates are ACE-isms, decomp is unconditional modulo the
  degenerate-framerate guard.
- update (0x00525b80): non-empty -> update_internal + apricot; empty +
  frame -> accumulated-physics free motion (G8).
- execute_hooks (0x00524830): direction filter (Both or match) QUEUING
  into the IAnimHookQueue host seam (stands in for CPhysicsObj.anim_hooks
  + the AnimDoneHook singleton; drain placement moves in R6). Null part
  frame guarded (documented safe divergence vs retail's latent deref).
- FrameOps.Combine/Subtract1: the AFrame pose composition (verified
  math from the pre-R1 port, now against DatReaderWriter Frame).

10 conformance tests: single-tick goldens, exact-integer boundary
sit (the old #61 flash class), cyclic wrap without AnimDone,
link->cycle fast-forward proving hook order (link hooks -> ANIMDONE ->
cycle hooks) AND the carry, reverse descending hooks with the OOB
high+1 start, reverse tail-wrap, zero-framerate physics-only,
empty-list free motion, pos-frame root motion into the caller Frame,
apricot trim via update. Full suite 3346 green.

R1 remaining: P5 (adapter cutover — AnimationSequencer rehosted on the
core, legacy epsilon/stale-head/safety-cap DELETED) + P6 (root-motion
wiring + API narrowing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:05:04 +02:00
Erik
5138b8fb01 feat(R1-P3): verbatim apply_physics + Frame rotate/grotate
- CSequence.ApplyPhysics (0x00524ab0): copysign semantics — magnitude
  from the quantum arg, sign from the sign-source arg (call sites pass
  1/framerate + signed elapsed); origin += velocity*signed, then
  rotate(omega*signed).
- FrameOps.GRotate (0x005357a0): axis-angle quaternion (angle=|v|,
  half-angle sin/cos) PREMULTIPLIED onto the orientation — incremental
  rotation in WORLD space; |v|^2 < F_EPSILON^2 skipped.
- FrameOps.Rotate (0x004525b0): local rotation vector mapped through
  the frame's local->global rotation, then GRotate.

7 numeric conformance tests (copysign matrix, global-space composition,
local->global mapping equivalence, epsilon gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:53:32 +02:00
Erik
778744bf3e feat(R1-P2): verbatim CSequence container + list surgery
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>
2026-07-02 19:50:19 +02:00
Erik
1371c2a14c feat(R1-P0/P1): CSequence research base + verbatim AnimSequenceNode
P0 — research + pins: full CSequence-family verbatim extraction (1756
lines, per-function raw pseudo-C + cleaned flow, decomp line anchors),
ACE cross-reference (9 ranked divergences; headline: retail frame_number
is x87 long double — ACE's float is the worst case, our double the best
available; ACE's frame-boundary epsilon is an ACE fabrication, NOT
retail), current-sequencer map, and the R1 gap map (20 gaps, 13 keeps,
P0-P6 port order). Pinned the one decomp ambiguity (leftover-time carry
after advance_to_next_animation — ACE reading adopted; cdb confirmation
protocol recorded, non-blocking).

P1 — AnimSequenceNode verbatim (gap G1/G2/G16/G18):
- direction-aware BARE-INT boundary pair (get_starting_frame 0x00525c80 /
  get_ending_frame 0x00525cb0): reverse starts at high+1, ends at low —
  NO epsilon;
- multiply_framerate (0x00525be0) swaps low/high on negative factor;
- set_animation_id (0x00525d60) retail clamp order (high<0 -> num-1;
  low>=num -> num-1; high>=num -> num-1; low>high -> high=low);
- ctors with retail defaults (30f/-1/-1; AnimData copy + clamp);
- get_pos_frame null out-of-range (retail; ACE returns identity),
  floor double overload; get_part_frame same discipline;
- NO per-node IsLooping/Velocity/Omega — loop membership is list
  structure, physics accumulators live on the sequence (G16).

22 conformance tests (clamp table, boundary mirror table, swap
round-trip, bounds/floor semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:45:56 +02:00
Erik
cae56afc82 docs(R): Phase R plan of record — retail motion+animation ground-up reconstruction
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>
2026-07-02 19:21:28 +02:00
Erik
4bfcd2735e fix(L.2g-S2b): turn/sidestep axes checked BEFORE overlay classification in RemoteMotionSink
The first S2b cut classified turn/sidestep dispatches (Modifier-class
0x65xxxxxx) into the generic overlay router before the axis checks ran —
turns never reached the cycle pick OR the ObservedOmega seed. S2 smoke
symptoms: turn-in-place showed no animation (orientation snapping only)
and running-in-circles had no client-side rotation between UPs.
Log evidence: 30 [TURN_WIRE] events, zero turn [SETCYCLE]s
(launch-s2-smoke.log).

Harness gap noted: the conformance suite stops at the funnel->sink
boundary; extending golden coverage through the sink (SETCYCLE-level
fixtures) is scheduled with the R-phase rewrite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:19:46 +02:00
Erik
67506ce988 feat(L.2g-S2b): wire remote entities onto the CMotionInterp funnel (DEV-1 integration)
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>
2026-07-02 18:59:00 +02:00
Erik
7b0cbbda2c feat(L.2g-S2a): CMotionInterp inbound funnel in Core + live-retail conformance harness (DEV-1 core)
Port the inbound funnel verbatim into MotionInterpreter:
- MoveToInterpretedState (0x005289c0): raw-state style adopt, FLAT
  copy_movement_from overwrite, apply, then action replay under the
  15-bit server_action_stamp wraparound gate (local player skips its
  own autonomous echoes).
- ApplyInterpretedMovement (0x00528600): my_run_rate cache from
  RunForward speed, then retail dispatch order style -> forward-or-
  Falling -> sidestep(-stop) -> turn(-stop), turn early-return.
- DispatchInterpretedMotion (0x00528360): contact-gated sink dispatch
  (IInterpretedMotionSink = the GetObjectSequence backend the App
  implements); blocked non-action motions take the apply-only path —
  retail's real mechanism behind K-fix17's 'airborne remotes keep
  their cycle' empirical guard.
- contact_allows_move REWRITTEN VERBATIM from the real 0x00528240
  (pseudo-C 305471): Falling/0x40000011 + turns always allowed,
  non-creature weenies + no-gravity bypass, else Contact+OnWalkable.
  The previous body conflated jump_charge_is_allowed (0x00527a50)
  posture checks — the 2026-06-04 deep-dive divergence, now retired.
  Six tests that pinned the misattributed behavior corrected
  (grounded posture does NOT block motion; airborne accepts
  Falling/turns only).
- IWeenieObject.IsCreature (default true) for the gate's non-creature
  bypass.

Conformance harness (the user-requested 'prove it equals retail'
apparatus, layer 1): RetailObserverTraceConformanceTests parses the
LIVE cdb trace of a retail observer (Fixtures/l2g-observer-trace.log,
captured via tools/cdb/l2g-observer.cdb) into 183 golden cases —
each [MTIS] input state replayed through our funnel must produce
retail's exact [DIM] dispatch sequence. 183/183 conformant, including
the airborne jump case (replayed contact-free; pre-gate vs post-gate
accounting + HitGround second-pass truncation documented in-test).
13 synthetic funnel tests cover the branches the trace missed
(actions, stamps, longjump, sidestep).

GameWindow integration (S2b) follows; funnel not yet wired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:50:09 +02:00
Erik
97e098bf91 docs(L.2g-S2): verbatim inbound-funnel pseudocode + observer cdb trace script
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>
2026-07-02 18:33:23 +02:00
Erik
a2f8104cbf feat(L.2g-S5): delete player pace-inference layer; close #39 (DEV-2)
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>
2026-07-02 16:10:28 +02:00
Erik
cb74e64343 feat(L.2g-S1): retail movement-event staleness gate (DEV-6)
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>
2026-07-02 15:44:06 +02:00
Erik
fb3ee0544a docs(L.2g): inbound motion deviation map + campaign registration
/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>
2026-07-02 15:20:46 +02:00
Erik
b60b9b4a21 docs: handoff for inbound animation+position verbatim retail port
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>
2026-07-02 13:49:01 +02:00
Erik
d34721fa94 feat(D6.2b): send retail-faithful raw forward_speed=1.0 on the wire
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>
2026-07-02 10:36:46 +02:00
Erik
4740750649 docs(D6.2a): record user smoke sign-off
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>
2026-07-02 10:15:47 +02:00
Erik
0f099bb652 feat(D6.2a): port CMotionInterp motion normalization + unify local velocity/turn/jump
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>
2026-07-01 08:57:17 +02:00
Erik
4ed278369b docs(D6): CMotionInterp motion-normalization pseudocode + unification design
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>
2026-07-01 08:30:25 +02:00
Erik
f271a49e6a docs(L.2b): record ACE smoke + user visual sign-off for the wire-parity slice
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>
2026-07-01 07:46:59 +02:00
Erik
78e163a41e feat(L.2b): outbound movement wire parity — RawMotionState default-difference, JumpPack, contact byte
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>
2026-06-30 22:30:01 +02:00
Erik
2c8620ea94 feat(L.1b): dual MotionCommand catalog — AceModern runtime + Retail2013 conformance
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>
2026-06-30 22:13:36 +02:00
Erik
33ad231d18 docs(L.2b/L.1b): movement wire-parity slice design + 2026-06-26 audits
Imports the two 2026-06-26 movement/animation retail-parity research docs
(audit D1-D12 + ACE-vs-2013 command-catalog gap) and adds the design spec
for the first implementation slice: dual command catalog (AceModern runtime
default + full-extraction Retail2013 conformance) + outbound wire parity
(RawMotionState default-difference packing D1, contact|standingLongjump
trailing byte D3, retail JumpPack layout D4). Decomp-verbatim, tests-first;
D6 motion-interpreter input-state construction explicitly deferred.

Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md
Phases: L.2b (movement wire/contact authority) + L.1b (command router).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:34:33 +02:00
Erik
ca94b479bf Merge claude/hopeful-maxwell-214a12 — D.2b UI Studio + faithful importer + Character window
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
2026-06-26 12:33:51 +02:00
Erik
ea42c6d303 docs: record D.2b UI Studio + importer dat-fidelity + Character window (shipped this session)
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>
2026-06-26 12:18:03 +02:00
Erik
9408c062d2 docs: deep handoff for the multi-window UI mockup stage
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>
2026-06-26 12:17:27 +02:00
Erik
4b5961ec04 fix(studio): Character window polish — XP-label left-align, smaller icons/tighter rows, selection bar full-width + top/bottom
Item 1 — XP-next label left-alignment: the "XP for next level:" label (child of the
XP meter) was not left-aligned with the "Total Experience (XP):" caption above it.
Fixed by computing the meter's x-offset at bind time and setting xpLabel.Left =
TotalXpLabel.Left - meter.Left, plus Centered=false/RightAligned=false.

Item 2 — icon size / row height: attribute-row icons reduced from 24px → 16px,
row height from 30px → 22px. The 9 rows are now compact and tightly packed matching
the retail reference (2026-06-26). Row font (18px dat) still fits the 22px row.

Item 3 — selection bar: UiClickablePanel gains UseSelectionBars (default false) and
SelectionBarHeight (default 3px). When UseSelectionBars=true and BackgroundSprite is
set, OnDraw draws the sprite as a thin horizontal bar at the TOP edge (y=0) and BOTTOM
edge (y=H-barH) of the row — full panel width, no left/right end-caps, UV-tiled
horizontally (u1=Width/nativeW). Falls back to the base UiPanel fill (BackgroundColor
or full-stretch sprite) when UseSelectionBars=false. AddRow sets UseSelectionBars=true
on all attribute/vital rows so the selection highlight shows as retail-style bars.
Sprite 0x06001397 is 300×32 px; at 3px bar height the UV crop shows the sprite's
top 3px (top bar) and bottom 3px (bottom bar). Temp pre-select for screenshot
verification was added then removed before this commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 12:01:42 +02:00
Erik
ad4ed51d6b feat(ui): importer Fix 5 — build meter text children; drop character XP injection hacks
LayoutImporter.BuildWidget gains a UiMeter-specific branch after the
ConsumesDatChildren gate: Type-3 slice containers (already consumed by
DatWidgetFactory.BuildMeter to extract sprite ids) are skipped, but all
other children (Type-12 UIElement_Text overlays) are built normally,
registered in byId, and attached as UiElement children of the meter.

This fixes the XP meter (0x10000236): its two Type-12 children
  0x10000237 "XP for next level:" label
  0x10000238 XP-to-next-level value
are now real UiText widgets in the tree, findable via FindElement.

CharacterStatController.Bind now uses FindElement + LinesProvider binding
instead of injecting runtime AddChild nodes. The two previously-injected
UiText overlays are removed; the controller binds Padding=0, ClickThrough,
RightAligned on the dat-origin widgets instead.

New constants XpNextLabelId + XpNextValueId replace the old comment block.

TAB GROUPS (SKIPPED — documented): UiText.ConsumesDatChildren flipping is
NOT safe globally (risks chat/vitals) and the page-visibility pass in
CharacterStatController depends on tab group Children.Count==0. Tab sprite
injection onto layout.Root stays and is explicitly commented as deliberate.

VITALS SAFE: health/stamina/mana meters have only Type-3 children → new
loop finds nothing → zero UiElement children added → byte-identical render.
VitalsController.BindMeter AddChild(number) pattern unchanged.

Tests: 3 new LayoutImporterTests (slice-only, Fix-5 text children,
vitals regression guard) + 3 CharacterStatControllerTests (label bound,
value bound, missing children no-throw). 715 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 11:35:19 +02:00
Erik
1b9dd6c7a8 docs(ui): Fix 4 — dat UIState model has no Group visibility encoding; CharacterStatController hacks are correct
Investigation (2026-06-26): the character footer three sibling groups (0x10000240/241/247)
all carry DefaultState=0x10000011 (StatManagement_Footer_Default) — the dat does NOT
differentiate them by visibility.  Parent 0x1000022F has States {Default, Text, Meter}
with PassToChildren=true, but each child group registers all three states with
IncorporationFlags=None and Media=0, so state-propagation produces no visual change
on any group.  Retail gmStatManagementUI uses hardcoded element-id dispatch for
0x10000240/241/247 — the groups are never hidden/shown via the dat state mechanism
(decomp gmStatManagementUI::GetFooterTitleLabel @0x004f0170).

Tab-page containers (0x1000022B/22C/10000539) have DefaultState=Undef — no visibility
encoding.  Tab visibility is managed purely at runtime by gmTabUI.

IncorporationFlags (DatReaderWriter): {None, PassToChildren, X, Y, Width, Height,
ZLevel} — no Visible flag.  The importer cannot set initial Group visibility from
the dat; the controller is the correct and only place.  CharacterStatController
HideAllById footer pass and ContainsWidget page-visibility walk are retail-faithful
and must be kept.

No logic changes — comment-only documentation.  Build green, 710 App tests green.
Studio screenshots: character footer shows State-A by default with no overlap;
vitals and toolbar unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 11:19:03 +02:00
Erik
ed626f4250 docs: file #150 — thread Fix C per-element dat-font resolver into GameWindow's 4 import sites
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>
2026-06-26 10:54:16 +02:00
Erik
26d5c101e4 docs: add 2026-06-26 character window retail reference screenshot notes
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>
2026-06-26 10:32:47 +02:00
Erik
a0d33956f6 feat(ui): importer Fix C — per-element dat FontDid resolver (studio path); character level uses its retail font
DatWidgetFactory.Create now accepts an optional fontResolve: Func<uint,UiDatFont?>
parameter. When supplied and the element has a non-zero FontDid, the element
receives its own dat font instead of the shared global datFont fallback.
Null = original single-font behavior (the live GameWindow path passes null —
provably unchanged). LayoutImporter.Build/BuildFromInfos/Import all thread
the optional resolver down to the factory.

RenderStack gains a lazy font cache (ConcurrentDictionary, pre-seeded with
VitalsDatFont + LargeDatFont) and a ResolveDatFont(uint) method. StudioWindow
wires stack.ResolveDatFont into LayoutSource so every studio import gets
per-element fonts. GameWindow import calls left passing null (follow-up todo).

CharacterStatController font-hack cleanup (diagnosed via one-shot console dump
then removed):
- Name (0x10000231): dat FontDid = 18px font — remove datFont override (null)
- Heritage/PkStatus: dat FontDid = 14px fonts — remove override
- LevelCaption: dat FontDid = 16px — remove override (same font, no visual change)
- Level (0x1000023B): dat FontDid = 36px (the big retail gold font) — was forced
  to rowDatFont/LargeDatFont (18px); now drops to null so the dat 36px font drives
- TotalXpLabel/TotalXp: dat FontDid = 16px — remove override
- FooterTitle (0x1000024E): dat FontDid = 20px — remove datFont override
- KEEP: synthesized elements (XP meter overlays, 9 attribute rows, tab sprites)
  still use datFont directly since they have no dat origin

All Label/LabelTwoLine/LabelLeft/LabelProvider helpers updated: null = keep
build-time dat font; non-null = controller explicit override (backward-compat).

8 new tests in DatWidgetFactoryFontResolveTests:
- null resolver → DatFont == global datFont
- FontDid=0 → resolver not called
- resolver returns null → fallback to global datFont
- resolver called with element's FontDid
- controller DatFont override wins after build
- LayoutImporter.Build threads fontResolve to factory
- meter element fires resolver for non-zero FontDid
- BuildFromInfos without fontResolve param = original behavior

Build + all 710 App tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:30:13 +02:00
Erik
6e0be4bd34 feat(ui): importer carries dat FontColor (0x1B) onto text widgets; character colors from dat where present
LayoutImporter.ReadState now reads Properties 0x1B (ColorBaseProperty, ARGB bytes) and stores the normalized Vector4 in ElementInfo.FontColor (nullable). ElementReader.Merge propagates it with the same non-null-derived-wins rule as FontDid and HJustify. DatWidgetFactory.BuildText seeds UiText.DefaultColor from FontColor when present.

Diagnosis for LayoutDesc 0x2100002E: ALL 12 header and footer text elements carry NO dat color. Every color is runtime set by CharacterStatController. Comments added at each callsite. No hardcoded colors deleted.

Tests added: 3 ElementReader FontColor Merge + 3 DatWidgetFactory DefaultColor. 702 passed, 0 failed. Screenshots: character window, vitals, toolbar all unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 10:15:55 +02:00
Erik
41430420b3 feat(ui): importer carries dat justification (0x14/0x15) onto text widgets; drop character footer-title height hack
LayoutImporter.ReadState now reads Properties[0x14] (HorizontalJustification,
EnumBaseProperty: 0=Left, 1=Center, 3/5=Right) and Properties[0x15]
(VerticalJustification: 2=Top, 4=Bottom; else Center) into two new ElementInfo
fields HJustify/VJustify. Merge propagates them with the same non-default-wins
rule used for FontDid.

DatWidgetFactory.BuildText applies the resolved justify at build time:
HJustify=Center sets Centered=true, HJustify=Right sets RightAligned=true,
VJustify=Top/Bottom sets VerticalJustify. Controllers that FindElement and set
those properties afterward continue to override - backward-compat preserved.

UiText gains VerticalJustify (Top/Center/Bottom, default Center). The Centered
and RightAligned single-line paths call UiText.VOffset() for the Y coordinate,
so VJustify.Top renders text at y=Padding rather than the fixed (H-lh)/2 center.

CharacterStatController: footer title (0x1000024E, H=55 dat box) previously
used Height=18 + Anchors=None to prevent center-vertical overlap with line-1/2.
Diagnostic confirmed dat says HJustify=Center, VJustify=Center. The hack is
replaced with one minimal explicit override: VerticalJustify=Top on the
already-Centered element. Text now renders at top of the 55px box natively.
Centered=false and RightAligned=false hand-sets removed where dat supplies them.

Dat justify values (studio diagnostic, 2026-06-26):
  0x1000024E footer title: HJustify=Center, VJustify=Center
  0x10000235/0x10000243/0x10000245 value fields: HJustify=Right
  header name/heritage/pk/level: HJustify=Center

+13 tests: ElementReader Merge propagation; DatWidgetFactory BuildText
justify application + controller-override backward-compat; UiText VOffset.
696 passed, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 10:05:36 +02:00
Erik
8f222f7506 fix(studio): Character window — level number uses large dat font (18px) for better retail match
Retail's m_pLevelText (0x1000023B) element is a plain UIElement_Text, not a
sprite-digit widget — the "large gold" appearance comes from the dat font
size on that specific element. Since 0x40000001 (18px) is the largest font
confirmed in client_portal.dat and there is no per-element font override in
our LayoutImporter, use rowDatFont (18px) for the level value so it fills
more of the 65×50 element and approaches the retail appearance.

Investigation: namedretail grep gmStatManagementUI + UpdateCharacterInfo
(0x004f0770) confirmed Type-12 UIElement_Text + SetText(L"%d"). No NumberSprite
or DigitSprite widget type exists in retail — the diff is purely font size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 02:31:54 +02:00
Erik
dfb4c81133 fix(studio): Character window — selected-row highlight uses retail bar sprite
Replaces the translucent-gold BackgroundColor tint with sprite 0x06001397
(Button state 6 — the retail dark horizontal bars) for the selected-row
background. When spriteResolve is provided to Bind(), clicking a row now
applies BackgroundSprite=0x06001397 + SpriteResolve on that row and clears
it on all others. Falls back to HighlightBg tint when no resolver is passed
(tests, or contexts without GL).

UiPanel.BackgroundSprite / SpriteResolve added so any panel can host a
sprite background in place of the solid BackgroundColor rect. HandleRowClick
updated to accept spriteResolve and apply the sprite or tint branch
depending on availability. Two new tests verify the sprite / deselect paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 02:26:46 +02:00
Erik
a236dc33ac fix(studio): Character window — larger row text + tighter attribute rows
- RowHeight reduced 44→30px to pack the 9 rows tighter, matching retail's denser list
- Attribute row name/value text now uses Font 0x40000001 (MaxCharHeight=18px) instead of
  the default 0x40000000 (16px); both fonts are in client_portal.dat (confirmed 2026-06-26)
- RenderStack gains LargeDatFont field; RenderBootstrap.Create loads both fonts
- FixtureProvider passes LargeDatFont as rowDatFont to CharacterStatController.Bind
- CharacterStatController.Bind gains rowDatFont? parameter (falls back to datFont);
  passed to BuildAttributeRows so row UiText elements use the larger font
- Full solution tests green (681/683 App + 1579/1581 Core + 343 Net + 425 UI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 02:22:00 +02:00
Erik
405b0d5077 fix(studio): Character window polish — name white, XP captions, selected-title white, Infinity! cost
- Name label color changed from gold to white (retail "Horan" is white; 2026-06-26 ref)
- Level caption now provides 2 lines ("Character" / "Level") so neither truncates in the 65px element
- "Total Experience (XP):" caption (TotalXpLabelId 0x10000234) now visible; fixed Padding=0 on
  LabelLeft so the dat-font line is not clipped by the bottom-pin scroll math in small-height elements
- "XP for next level:" caption + value injected as UiText children ON the UiMeter (0x10000236);
  the meter's ConsumesDatChildren=true only gates the importer — AddChild at runtime works fine;
  framework draws children after OnDraw so the text overlays the red bar faithfully
- Selected footer title (State B) renders WHITE per retail spec; State A title stays body/parchment
- Maxed attribute (raise cost = 0) shows "Infinity!" in Experience To Raise line per retail spec
- 5 new tests; 1 existing LevelCaption test updated to expect 2 lines; 681/683 pass (full suite green)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 02:16:11 +02:00
Erik
31666c0e85 fix(studio): Character footer — title was centered in the full-height (H=55) box, overlapping the lines
A position dump (the apparatus, after 5 speculative passes) showed the footer title element is H=55
(the whole footer box) in the dat; centering "Select an Attribute to Improve" in it landed the text
dead-center (~y572), painting over "Skill Credits Available:" (y565) and "Unassigned Experience:"
(y582). Constrain the title to a thin line (H=18) at the footer top + drop its anchors so the
per-frame stretch doesn't re-grow it. All three State-A lines now read cleanly. Diagnosed from data,
not guessing — exactly the apparatus-over-cdb call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 01:04:43 +02:00
Erik
5aa65dbd43 fix(D.2b): Character window — tab bar sprites on root + footer State-A all 3 lines
BUG 1 (tab bar): Tab group elements (0x10000228/229/538) are UiText with
ConsumesDatChildren=true so their 3 button children are consumed at import.
Fix: inject 3 sprite UiTexts per tab as CHILDREN OF LAYOUT ROOT at absolute
tab rects, ZOrder=8/9 so they draw over dat-imported UiTexts (ZOrder=1-3).
Original tab groups hidden. Active tab (Attributes) gold; inactive parchment.

BUG 2 (footer): Three root causes, all fixed.
  (a) _byId stores LAST registered copy per id: stateA (0x10000240) was
      the Titles-page copy, hidden by the page-visibility pass. Fixed by
      walking root.Children to find the Attributes page (contains NameId)
      then FindInSubtree for stateA within that subtree.
  (b) Attributes-page stateB/stateC siblings (stacked at y=545) were still
      Visible=True, drawing over stateA line-1/line-2. Fixed with
      HideAllById walking the Attributes page subtree for ids 241/247.
  (c) Footer label elements (H=17-18px, Padding=4f) were routed through
      UiTexts scroll path: bottom-pinned baseY ended above the top clip
      boundary, silently blanking all text. Fixed: LabelProvider sets
      Padding=0f for directly-bound footer single-line labels.

UiDatElement.ElementId exposes _info.Id for subtree id-based walks.
676 tests pass, vitals panel unaffected (regression screenshot clean).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 22:59:44 +02:00