fix(combat): apply server attack motions locally

Route accepted non-autonomous local UpdateMotion state through retail's wholesale interpreted-motion funnel, so ACE-selected melee and missile actions use the normal motion queue and action-stamp gate. Share nullable wire conversion with remotes and remove the local direct command replay.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 13:34:55 +02:00
parent 8ec0944a1c
commit 9958458318
6 changed files with 275 additions and 135 deletions

View file

@ -496,6 +496,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **✓ SHIPPED — Wave 4.4d toolbar launchers + Use/Examine.** All seven authored launchers are discovered through their DAT panel-id attribute; mounted Inventory/Character panels toggle through the registry and the other five buttons are ghosted. Typed window-visibility events own Highlight/Normal state. Use and Examine act on selection or enter their one-shot retail target modes, with exact Appraise wire dispatch and retail cursors. Warning-free App build and 4,747-pass / 5-skip Release suite are green; the connected live gate passed 2026-07-11. AP-101 is narrowed to the remaining ammo-number display.
- **Wave 4.4e implemented — exact missile ammo number (live gate pending).** Pure Core reproduces retail's thrown-weapon-vs-separate-ammo resolution over the ordered player equipment list and its zero-to-one count normalization. Relevant equipment/stack events update authored missile indicator `0x10000194` with the DAT font. Warning-free Release build and 4,754-pass / 5-skip suite are green; AP-101 is retired.
- **M2 held-object parenting shipped and live-gated 2026-07-11.** The combat toggle now ports `GetDefaultCombatMode` over ordered equipped contents, so a bow requests Missile instead of the old hardcoded Melee. CreateObject preserves parent/placement/timestamp fields, `0xF749` ParentEvent is handled, and `EquippedChildRenderController` renders the weapon as a separate child composed from the animated hand part + holding frame + child placement frame. Live gate passed: bow selected missile stance, rendered in-hand, followed animation, unequipped cleanly, and melee remained correct. App Release builds with zero warnings; the full 4,765-pass / 5-skip suite is green. AP-111 is retired; research: `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`.
- **M2 local attack receive funnel implemented 2026-07-11; live gate pending.** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository``ClientObjectTable` / `ItemInstance``ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.

View file

@ -454,6 +454,10 @@ include dungeons.
- **L.1c held-object foundation (implemented and live-gated 2026-07-11)** — exact
equipment-aware default combat mode (bow→Missile, caster→Magic) plus retail
CreateObject/ParentEvent child attachment and per-frame hand-follow rendering.
- **L.1c local attack receive path (implemented 2026-07-11; live gate pending)**
local non-autonomous mt-0 UpdateMotion now uses retail's wholesale interpreted
funnel and action-stamp gate, so ACE's server-selected melee/missile action
drives the player's motion table; direct command-list replay is removed.
- **L.1b** — Command router + motion-state cleanup (prereq for L.1c).
**Freeze on landing:**

View file

@ -0,0 +1,81 @@
# Local combat UpdateMotion — retail receive pseudocode
Date: 2026-07-11
## Question
Who chooses and plays the local player's melee or missile attack motion?
## Named-retail oracle
- `ClientCombatSystem::ExecuteAttack` at `0x0056BB70` sends the targeted
melee/missile request and updates combat-repeat state. It does not choose a
concrete swing command and does not play an animation locally.
- `CPhysics::SetObjectMovement` at `0x00509690` rejects a local autonomous
echo, but accepts a newer non-autonomous movement event, stores
`last_move_was_autonomous = false`, calls `unpack_movement`, then returns 1
so `CommandInterpreter::LoseControlToServer` runs.
- `MovementManager::unpack_movement` at `0x00524440`, case 0, unpacks an
`InterpretedMotionState`, calls `move_to_interpreted_state`, then applies the
sticky-object and standing-long-jump trailers.
- `CMotionInterp::move_to_interpreted_state` at `0x005289C0` copies the state
wholesale, reapplies style/forward/side/turn, and dispatches fresh command
list entries under the 15-bit `server_action_stamp` gate. The player skips
only command entries whose autonomous bit is set.
## ACE cross-reference
ACE's `Player_Melee.DoSwingMotion` selects the actual maneuver from the combat
table, constructs a `Motion`, marks the target sticky, and broadcasts it.
Missile attacks similarly enqueue the server-selected aim action, followed by
reload and Ready. `MovementData.IsAutonomous` documents the wire split as
client-initiated `true`, server-initiated `false`.
## Retail pseudocode
```text
on local attack input:
send TargetedMeleeAttack or TargetedMissileAttack
do not invent or play a swing locally
on UpdateMotion for local player:
apply movement and server-control sequence gates
if event.isAutonomous:
drop the entire event
player.lastMoveWasAutonomous = false
interrupt current move
unstick
apply outer style if it changed
if movementType is MoveTo/TurnTo:
route to MoveToManager
return
if movementType == InterpretedMotionState:
ims = constructor defaults (NonCombat, Ready, speeds 1)
unpack every present field over ims
move_to_interpreted_state(ims)
copy every axis wholesale
apply style, forward, sidestep, turn
for each fresh command-list entry:
if local player and entry.autonomous: skip
dispatch entry through the motion table
apply sticky target trailer
write standingLongJump, including false
command interpreter loses control to server
next local input edge takes autonomous control back
```
## acdream correction
The local branch already had the autonomous-event rejection and the
interrupt/style/MoveTo routing, but deliberately skipped the mt-0 wholesale
copy. It then replayed `Commands[]` directly through
`AnimationCommandRouter`, bypassing retail's action-stamp and local-autonomous
gates. The correction routes local mt-0 through the existing
`MotionInterpreter.MoveToInterpretedState` and persistent motion-table sink,
exactly like remotes, and removes the direct replay. A shared App-layer factory
now owns nullable-wire-to-constructor-default conversion for both branches.