fix(gameplay): reconcile wield ownership and target facing

Preserve PlayerDescription inventory/equipment ownership across authoritative manifest replacement, make weapon switching and combat/UI consumers read the same canonical object state, and carry the complete outbound player position frame across landblocks.

Route target-facing and mouse-look through the shared MovementManager and MotionInterpreter completion owner. Match retail input aggregation, toggle ordering, turn/sidestep remapping, per-axis hold keys, and synchronous movement publication without render-only heading state.

Initialize the live streaming origin from the first accepted canonical player Position, defer other projections until that origin exists, and retain logical entity identity through hydration.

Advance the project ledger from completed M2 to active M3, synchronize CLAUDE.md/AGENTS.md and durable memory, and record the next cast-lifecycle, spellbook/enchantment, and two-client portal gates.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 08:19:23 +02:00
parent b26f84cc69
commit 7b7ffcd278
36 changed files with 3884 additions and 761 deletions

View file

@ -46,7 +46,7 @@ Copy this block when adding a new issue:
## #217 — Character windows did not receive live 64-bit experience updates
**Status:** IN-PROGRESS — implementation complete; connected visual gate pending
**Status:** DONE — 2026-07-13, connected gate user-confirmed
**Severity:** HIGH
**Filed:** 2026-07-13
**Component:** retained UI / character sheet / net / player state
@ -79,6 +79,9 @@ the value element `0x10000235` is explicitly right-aligned.
right-aligned total and red progress fill; Skills immediately shows the same
server-authoritative unassigned XP available for raises.
**Gate:** Passed in the starter-dungeon item-giving session: earned XP,
right-aligned Total Experience/progress, and Unassigned Experience all updated.
---
## #213 — Retail client commands were sent to ACE as chat text
@ -184,18 +187,19 @@ the combat button and grave key briefly selected Melee before ACE restored
NonCombat, so the character could not enter combat.
**Root cause:** PlayerDescription recorded equipped entries with their equip
mask but no player ownership/container index. Live WieldObject updates did
index the same item under the player. `ToggleLiveCombatMode` reads the player's
ordered contents for retail `GetObjectAtLocation`, so it missed login equipment,
defaulted to Melee, and sent a mode incompatible with ACE's equipped missile
weapon. ACE correctly returned authoritative NonCombat.
mask but no player ownership/container index. The combat-mode lookup therefore
missed login equipment, defaulted to Melee, and sent a mode incompatible with
ACE's equipped missile weapon. ACE correctly returned authoritative NonCombat.
**Resolution:** PlayerDescription equipment now uses the same
contained-by-wielder projection as live WieldObject while preserving its wire
order, equip mask, and layering priority. The production combat planner now
selects Missile for the login-equipped crossbow. The connected gate completed
NonCombat → Missile → NonCombat → Missile, including a Jump press, without a
server rejection.
**Resolution:** PlayerDescription preserves login equipment ownership and the
production combat planner selects Missile for the login-equipped crossbow. A
2026-07-14 retail-conformance refinement also corrected live WieldObject: its
payload is exactly `(itemGuid, equipLocation)`, and confirmation produces
`ContainerId=0`, `WielderId=player`. Equipment consumers now use
`GetEquippedBy`, which unifies that authoritative state with the temporary
optimistic pre-confirm projection instead of treating equipment as backpack
contents. The connected gate completed NonCombat → Missile → NonCombat →
Missile, including a Jump press, without a server rejection.
**Research:** `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`

View file

@ -212,7 +212,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, spellbook/effects/favorite spell bars, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic `gmCombatUI` now covers the active M2 melee/missile loop; Wave 10 lands each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI | Named `gm*UI::PostInit` methods and LayoutDesc catalog |
| ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` |
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, server-response queueing, and auto-repeat, but omits `StartAttackRequest`'s `FinishJump`/`MaybeStopCompletely` command-interpreter calls and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The M2 attack contract and authored basic panel are live; the remaining seams require the jump/movement command owner and a distinct Recklessness treatment rather than UI-local guesses | Starting an attack while charging a jump or deliberately moving may not stop/cancel exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, `MaybeStopCompletely`, server-response queueing, and auto-repeat, but still omits `StartAttackRequest`'s `FinishJump` call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |
| ~~AP-114~~ | **RETIRED 2026-07-14 (protection-effect corrective gate)** — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail `Always2D` classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | `src/AcDream.App/Rendering/ParticleRenderer.cs`; `RetailParticleGeometryClassifier.cs`; `particle_mesh.vert/.frag` | — | — | `CPhysicsPart::Draw @ 0x0050D7A0`; `CPhysicsPart::Always2D @ 0x0050D8A0`; `ParticleEmitter::SetInfo @ 0x0051CE90`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` |
@ -245,7 +245,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| TS-30 | Chat DAT elements `0x10000522``0x10000525` render but have no controller semantics; the older claim that they are numbered in-window filter tabs is **unproven** | `src/AcDream.App/UI/Layout/ChatWindowController.cs` | Named retail proves separately filtered main/floaty chat windows, not an in-window numbered-tab model. Wave 5 must live/DAT-confirm these element roles before assigning behavior | The controls may be inert today, but inventing tab switching could be a larger divergence than leaving an unconfirmed role inactive | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; correction in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
| TS-31 | **NARROWED 2026-07-13**`/squelch`, `/unsquelch`, `/filter`, `/unfilter`, and `/messagetypes` send the exact modification events and consume the authoritative retail `SquelchDB`; incoming `ChatLog` lines are not yet filtered through that database, and clickable name-tag social actions remain absent | `src/AcDream.Core/Social/SquelchState.cs`; `src/AcDream.Core.Net/Messages/SocialStateMessages.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core/Chat/ChatLog.cs` | Command/state transport is complete; enforcement belongs at the shared inbound-chat boundary so both backends remain identical | A squelch appears in the list and persists server-side but matching incoming lines can still render; contextual name actions remain unavailable | `SquelchDB::UnPack @ 0x006B1900`; `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
| TS-33 | Outbound position-send tracker over-stamped after MoveToState: `NotePositionSent` writes last-sent position + cell + contact-plane after BOTH the MTS and AP sends, but retail's `SendMovementEvent` (0x006b4680) stamps ONLY `last_sent_position_time` after an MTS — only `SendPositionEvent` (0x006b4770, the AP path) stamps all three. Broader: acdream gates APs on a plain interval (`HeartbeatDue`) where retail uses `ShouldSendPositionEvent` (0x006b45e0 — interval OR cell-change OR contact-plane-change), AND acdream's frame-changed diff compares POSITION only (`ApproxPositionEqual`) where retail's `Frame::is_equal` compares the full frame incl. ORIENTATION — a stationary heading change (R4-V5: the MoveToManager's `HandleTurnToHeading` arrival snap, `set_heading(send:true)`) never triggers an AP, so the server keeps the stale facing until the player next moves. Masked against ACE (ACE rotates server-side on its own mt-8/9 / close-range-use paths and broadcasts the result) | `src/AcDream.App/Rendering/GameWindow.cs:8331` (MTS `NotePositionSent`); `src/AcDream.App/Input/PlayerMovementController.cs` (`ApproxPositionEqual` + the heartbeat diff); the player MoveToManager `setHeading` seam in `EnterPlayerModeNow` (the `send` flag's would-be consumer) | D5 audit-only in the L.2b wire-parity slice; the full cadence port (`ShouldSendPositionEvent` gate + split MTS/AP stamping + full-frame `Frame::is_equal` diff) is a dedicated follow-up slice (R7 outbound) | AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends, and a stationary server-commanded turn leaves observers with stale facing until the next movement; on a real network this shifts the position-correction rhythm | `CommandInterpreter::SendMovementEvent` 0x006b4680, `SendPositionEvent` 0x006b4770, `ShouldSendPositionEvent` 0x006b45e0, `Frame::is_equal` (pc:700263) |
| TS-33 | **NARROWED 2026-07-15** — full AP tracker semantics are ported: MTS stamps time only; AP stamps complete cell-local Position + contact plane + time; `ShouldSendPositionEvent` compares cell/contact inside the interval and the complete Frame including orientation afterward. Residual: acdream's single update path snapshots the AP predicate, emits a same-update MTS first when input changed, then AP. Retail proves `UseTime` performs Should→AP, but MTS originates in separate input callbacks; their relative same-tick callback/wire order is not yet traced | `src/AcDream.App/Rendering/GameWindow.cs` (outbound MTS/AP blocks); `src/AcDream.App/Input/PlayerMovementController.cs` (ported tracker) | Preserve the pre-existing acdream wire order until a focused retail packet/breakpoint trace establishes input callback versus `UseTime`; do not infer it from `UseTime` alone | In the rare update where both packets are due, ACE may observe their position timestamps/action sequences in the opposite order from retail, shifting only that correction tick; stationary target-facing is live-gated because full-frame orientation now publishes | `CommandInterpreter::UseTime` 0x006B3BF0; `SendMovementEvent` 0x006B4680; `SendPositionEvent` 0x006B4770; `ShouldSendPositionEvent` 0x006B45E0; `Frame::is_equal` 0x00424C30 |
| TS-40 | Retail's `physics_obj->cell` ("placed in the world") is proxied by the explicit `PhysicsBody.InWorld` flag — set by `SnapToCell` (local player placement) and `RemoteMotion` construction (remotes exist only for world entities); consumed by `CMotionInterp`'s detached-object link-strip guards (`if (cell == 0) RemoveLinkAnimations`, raw @305627). Replaces the UNREGISTERED `CellPosition.ObjCellId == 0` proxy, which only the local player ever seeded (#145 `SnapToCell`), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug | `src/AcDream.Core/Physics/PhysicsBody.cs` (`InWorld`); `src/AcDream.Core/Physics/MotionInterpreter.cs` (3 guard sites) | acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists | A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag | `CMotionInterp::DoInterpretedMotion` 0x00528360 tail @305627; `CPhysicsObj::RemoveLinkAnimations` |
| TS-35 | `PhysicsBody.IsFullyConstrained` is a stub property (default `false`, never set by any physics code), read by `jump_is_allowed`'s verbatim `IsFullyConstrained` gate (raw 305524-305525) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`IsFullyConstrained`) | R3-W3 needed the read site to port `jump_is_allowed`'s full chain. **R5-V1 CORRECTED the mechanism** (the earlier "per-cell contact-plane / doorway-jamming" guess was WRONG): the write side is the **ConstraintManager server-position rubber-band leash** — armed by `SmartBox::HandleReceivedPosition` on every inbound server position, `IsFullyConstrained` = `max*0.9 < offset`. R5-V1 ported `ConstraintManager` (`src/AcDream.Core/Physics/Motion/ConstraintManager.cs`) but does NOT arm it (no acdream `SmartBox` + two x87 distance constants BN elided) — so this read stays false. Arming = issue #167 | A body retail would consider fully constrained (still rubber-banding toward a server position inside the tight leash) never refuses the jump (0x47) — a jump succeeds mid-rubber-band where retail blocks it. Low practical risk (the leash band is tight + short-lived) | `CPhysicsObj::IsFullyConstrained` 0x0050ec60 → `ConstraintManager::IsFullyConstrained` 0x005560d0; `jump_is_allowed` 0x005282b0; arming `SmartBox::HandleReceivedPosition` 0x00453fd0 (issue #167) |
| TS-37 | RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had `contact_allows_move` (0x00528240) arm `StandingLongJump` as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; `ChargeJump` (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`contact_allows_move`, `ChargeJump`) | N/A — retired | N/A — retired | `CMotionInterp::charge_jump` 0x005281c0 @305448 |

View file

@ -1,11 +1,34 @@
# acdream — strategic roadmap
**Status:** Living document. Updated 2026-05-19. **Between phases.** **Since the last header update:** Indoor walkable-plane BSP port FOUNDATION shipped (6 commits, `ff548b9``f845b22`) but visual verification failed — cellar descent, 2nd-floor walking, single-floor cottage regressions all confirm the shipped fix doesn't address the user-reported indoor bugs. Root cause now diagnosed as deeper than originally thought: `TryFindIndoorWalkablePlane` exists as a Phase 2 stop-gap that retail doesn't have an analog for. Retail retains ContactPlane state across frames; we re-synthesize per frame. Foundation work (BSP walker + probe + tests) remains useful; next phase needs to port retail's ContactPlane retention mechanism and likely eliminate `TryFindIndoorWalkablePlane` entirely. Handoff: [`docs/research/2026-05-19-indoor-walkable-plane-bsp-port-shipped-handoff.md`](../research/2026-05-19-indoor-walkable-plane-bsp-port-shipped-handoff.md). ISSUES #83 remains OPEN with deeper diagnosis. **Earlier:** Indoor cell rendering Phase 1 (diagnostics) + Phase 2 (fix) shipped — root cause was a one-line WB bug at `ObjectMeshManager.cs:1223` (blind `TryGet<Setup>` on GfxObj-prefixed stab ids threw `ArgumentOutOfRangeException` which WB's outer catch silently swallowed, causing 26/123 Holtburg cells to fail upload). Identified via diagnostic chain (5 `[indoor-*]` probes + a `ContinueWith` exception surfacer + a `ConsoleErrorLogger` injected into WB), fixed with a Setup-prefix guard. User visually confirmed floors render. Surfaced 9 pre-existing indoor bugs filed in `docs/ISSUES.md`. **Earlier:** C.1.5b shipped (issue #56 per-part transforms for multi-emitter PES + `EntityScriptActivator` extended to dat-hydrated EnvCell statics & exterior stabs — portal swirl, inn fireplace flames, cottage chimney smoke, spell-cast particles all match retail). post-A.5 polish completed (#52 lifestone, #54 JobKind, #53 Tier 1 cache); N.6 slice 1 shipped (gpu_us fix + radius=12 perf baseline, conclusion CPU dominates GPU 3050×); C.1.5a shipped (portal PES wiring; surfaced #56 → resolved in C.1.5b).
**Status:** Living document. Updated 2026-07-15. **M3 active.** M2's connected demo loop is complete: melee and missile weapon switching, target-facing, combat animation and damage/death presentation, loot/inventory, stack operations, and giving an item to a world target have all passed user gates. The projectile/PhysicsScript/particle/portal foundation is implemented through its Step 9 automated hardening gate; single-client missile and protection visuals pass, while the final two-client portal-out/materialization observer gate remains open. The immediate work order is an F.4/L.1d retail-conformance audit against that shipped foundation, implementation of the remaining cast lifecycle, then F.5 spellbook and active-enchantment UI.
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
---
## Current program: Phase W — Unified Cell Graph (UCG)
## Current program: M3 — Cast a spell
**Outcome:** Cast a projectile spell at a hostile target, see the complete
turn/windup/release/projectile/impact lifecycle, self-cast a buff and see its
active enchantment, then recall through the DAT-driven portal presentation.
**Next concrete work, in order:**
1. Reconcile current E.5 wire, M2/M3 projectile/VFX, motion, and enchantment
state against named-retail F.4/L.1d; record only the proven remaining gaps.
2. Port and conformance-test those cast-state and animation lifecycle gaps,
including facing, release, interruption/fizzle, recoil, and authoritative
completion.
3. Ship F.5's retained spellbook and active-enchantment surfaces over the
existing shared state/command seam.
4. Run the pending two-client portal-out/materialization observer visual gate
before M3 closes.
Opportunistic UI/physics fixes are no longer the work-order driver. They enter
the active slice only when they block this demo or represent a severe regression.
---
## Historical program: Phase W — Unified Cell Graph (UCG)
**[SUPERSEDED — G.3 dungeon work shipped; M1.5 next step = #138 round-trip
gate + #145-residual. See the shipped table + `docs/plans/2026-05-12-milestones.md`
@ -497,16 +520,16 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **✓ SHIPPED — Wave 4.4c exact selected-health policy.** Core ports `ObjectIsAttackable @ 0x0056A600`; CreateObject preserves second-header PetOwner; toolbar player/pet composition is exact. Self, pets, and Free-PK creatures now query health while friendly NPCs and attackable non-creatures remain name-only. AP-46 retired. The world/radar policy matrix and exact DAT paperdoll body-map + upper-item-priority route passed live 2026-07-11.
- **✓ 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.
- **Wave 4.5 implemented — retail primary-weapon switching (corrective live gate pending).** Inventory double-click now routes through a focused `AutoWieldController`: the occupied `0x03500000` weapon-ready group is returned to the player container, followed when necessary by an incompatible shield or mismatched ammo; each blocker waits for authoritative `InventoryPutObjInContainer` before the next step, then `GetAndWieldItem` equips the requested bow/sword/caster/two-hander. The transaction remains inventory-busy through the exact authoritative `WieldObject` acknowledgement, independent of the object table's rollback counter, matching retail's waiting state and preventing rapid switches from overlapping or remaining stuck. `CreateObject` retains exact `AmmoType` for compatibility. The same transaction runs in peace and war; ACE owns the latter's old-stance → peace → new-stance motion chain. Player PropertyInt 40 updates `CombatState`, keeping toolbar/combat UI synchronized with ACE's selected Melee/Missile/Magic stance. Research: `docs/research/2026-07-12-retail-weapon-switch-pseudocode.md`.
- **Wave 4 inventory drag visuals implemented — corrective live visual gate pending.** `IconComposer` now preserves retail's separate underlay-free `m_pDragIcon` for cursor tracking across inventory, paperdoll, and toolbar bindings. Physical source cells keep their full icon in place and reveal authored ghost mesh `0x0600109A` from drag begin through every release/cancel path, with the persistent selection square above the mesh; shortcut aliases remain unghosted. `ItemList_DragOver` destination states are split exactly: contents-grid placement uses the green accept circle `0x060011F9`, while side-bag/main-pack container drop-in uses the green arrow `0x060011F7`. AP-47 retired. Research: `docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md`.
- **Wave 4.6 item giving implemented 2026-07-13; starter-dungeon live gate pending.** SmartBox drag release now preserves the exact release coordinates, world-picks the target, and runs the existing retail `ItemHolder::AttemptPlaceIn3D` policy. NPC/creature targets send exact `GiveObjectRequest 0x00CD(target,item,amount)` bytes, selected partial stacks use the shared split quantity, and the object table waits for ACE's authoritative remove/stack-size response. `PlayerDescription.Options1` supplies the real `DragItemOnPlayerOpensSecureTrade` option. Issue #216; research: `docs/research/2026-07-13-retail-give-item-pseudocode.md`.
- **✓ Wave 4.5 — retail primary-weapon switching (live-gated 2026-07-15).** Inventory double-click now routes through a focused `AutoWieldController`: the occupied `0x03500000` weapon-ready group is returned to the player container, followed when necessary by an incompatible shield or mismatched ammo; each blocker waits for authoritative `InventoryPutObjInContainer` before the next step, then `GetAndWieldItem` equips the requested bow/sword/caster/two-hander. The transaction remains inventory-busy through the exact authoritative `WieldObject` acknowledgement, independent of the object table's rollback counter, matching retail's waiting state and preventing rapid switches from overlapping or remaining stuck. `CreateObject` retains exact `AmmoType` for compatibility. The same transaction runs in peace and war; ACE owns the latter's old-stance → peace → new-stance motion chain. Player PropertyInt 40 updates `CombatState`, keeping toolbar/combat UI synchronized with ACE's selected Melee/Missile/Magic stance. Research: `docs/research/2026-07-12-retail-weapon-switch-pseudocode.md`.
- **✓ Wave 4 inventory drag visuals (live-gated 2026-07-15).** `IconComposer` now preserves retail's separate underlay-free `m_pDragIcon` for cursor tracking across inventory, paperdoll, and toolbar bindings. Physical source cells keep their full icon in place and reveal authored ghost mesh `0x0600109A` from drag begin through every release/cancel path, with the persistent selection square above the mesh; shortcut aliases remain unghosted. `ItemList_DragOver` destination states are split exactly: contents-grid placement uses the green accept circle `0x060011F9`, while side-bag/main-pack container drop-in uses the green arrow `0x060011F7`. AP-47 retired. Research: `docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md`.
- **✓ Wave 4.6 item giving (starter-dungeon live gate passed 2026-07-13).** SmartBox drag release now preserves the exact release coordinates, world-picks the target, and runs the existing retail `ItemHolder::AttemptPlaceIn3D` policy. NPC/creature targets send exact `GiveObjectRequest 0x00CD(target,item,amount)` bytes, selected partial stacks use the shared split quantity, and the object table waits for ACE's authoritative remove/stack-size response. `PlayerDescription.Options1` supplies the real `DragItemOnPlayerOpensSecureTrade` option. Issue #216; research: `docs/research/2026-07-13-retail-give-item-pseudocode.md`.
- **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`.
- **Missile/portal VFX campaign Steps 05 implemented and independently reviewed 2026-07-14.** The retail oracle and packet fixtures are pinned, complete `PhysicsDesc` plus F754/F755 parsing and nine-channel gates are shipped, and App now has one canonical `LiveEntityRuntime` record per server-object incarnation. Logical register/unregister is separate from spatial rebucketing, so landblock churn and attached equipment retain identity without replaying renderer/script creation or reconstructing from stale spawn data. Canonical materialized and visible target/radar views are distinct; pickup/parent leave-world preserves owners while pausing root simulation; spawn publication is once per incarnation. `GpuWorldState` is spatial-only for live objects. Production PhysicsScript and Animation loading shares a narrow `DatCollection`-backed compatibility reader for retail's inherited blocking-particle payload, including mesh-side preloading; the proven concurrent-safe DatReaderWriter 2.1.7 read path avoids blocking update-thread effects behind streaming. Typed-table selection preserves DAT order and retail's first `intensity <= Mod` boundary, and live `PhysicsDesc` effect defaults replace rather than fall back to Setup. The Step 4 scheduler is now one serial FIFO per owner with duplicate stacking, catch-up dispatch, deterministic delayed `CallPES`, complete hook-router fan-out, update-frame clock publication, retail cell/Frozen eligibility, and structural rejection of malformed zero-time recursive DAT chains without rejecting valid timed weather loops. `EntityEffectController` retains pre-create F754/F755 in one mixed per-GUID FIFO, resolves local identity only through `LiveEntityRuntime`, replays once only after the canonical owner is fully ready and in-world, drops later plays while that existing owner is cell-less, routes attached-child updates through the eligible parent, and replaces/clears the live SoundTable on every PhysicsDesc application. Step 5 publishes current rigid root/indexed-part poses after animation and recursively composed held-child transforms, keeping render-only Setup scale out of particle and holding-location anchors; it then drains animation hooks, owner PhysicsScripts, moving emitters/lights, and particle simulation in fixed order. Normal/blocking/anonymous logical emitter identity is exact, including Stop retaining a blocking ID until final-particle retirement; moving and held Setup lights follow their current roots and share retail's PhysicsState Lighting/SetLight transition state; world-released versus parent-local particle behavior is preserved; missing emitter DAT produces an actionable diagnostic and no invented fallback. Drawable meshes no longer collapse stable Setup-part indices; nested attachments update parent-before-child, recursively withdraw on ancestor pose loss, and recover the complete descendant chain only on real publication edges. Loaded↔pending projection edges skip particle update/draw while retaining original absolute creation times and logical identity, and withdraw light presentation; re-entry evaluates elapsed state once without backlog emission or recreating an effect owner. Scripts, particles, lights, translucency, audio, and teardown all share canonical `WorldEntity.Id`; static allocators fail fast before namespace wrap. IA-7, AD-14, TS-10, TS-11, TS-12, TS-13, and AP-67 are retired; AD-32 now covers only the remaining non-effect, non-Parent pre-create packet families, and AD-43 registers the corrupt-DAT zero-time-cycle safety boundary.
- **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`.
- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
- **✓ M2 local attack receive funnel (live-gated 2026-07-15).** 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`.
- **✓ M2 basic retail combat bar (live-gated 2026-07-15).** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
- **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets. Confirmation reuse now ports retail `DialogFactory` contexts, queue groups/priority, fresh DAT roots, property results, callbacks/close notices, and server abort handling; `/die`, gameplay request tuples, and guarded item-use prompts share its type-1 LayoutDesc `0x2100003C` presenter. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`, `docs/research/2026-07-13-retail-dialog-factory-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.
- **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate pending). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout.
- **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate passed). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout.
- **✓ 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.
- **✓ D.5.3 — Toolbar selected-object display (issue #141) SHIPPED.** Exact health/player/pet/Free-PK policy, selected-item mana (`0x100001A2`), stack entry (`0x100001A3`), stack slider (`0x100001A4`), formatted count/name, shared split quantity, click-to-use, peace/war, health/name, and paperdoll self/upper-item selection all passed live by 2026-07-11. Exact raw `ShortCutData` preservation is already shipped in Wave 4.2; the eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase.

View file

@ -2,16 +2,13 @@
**Status:** Living document. Created 2026-05-12.
**Sits above:** [`docs/plans/2026-04-11-roadmap.md`](2026-04-11-roadmap.md) (the strategic phase index).
**Currently working toward:** **M2 — "Kill a drudge."** **M1.5 — Indoor
world feels right LANDED 2026-07-10** (pinned writeup in the M1.5 section
below): the building/cellar demo (prior sessions) AND the full dungeon
round-trip (enter via portal → navigate → exit to the outdoor world) are
both user-gated. Dungeon support (full Phase G.3) shipped — render / stream
/ teleport-in / collide / light / doors, each individually gated
(#133/#137/#95/#79/#93/#80 CLOSED; #138 closed by the round-trip gate).
Carried forward as post-M1.5 follow-ups (NOT M1.5 blockers): the far-town
teleport-OUT arrival cascade (**#145-residual, REOPENED** —
capture-harness-first) and #116 slide-response feel-debt.
**Currently working toward:** **M3 — "Cast a spell." M2 — "Kill a drudge"
LANDED 2026-07-15.** The complete connected melee/missile, death/loot,
inventory, and item-giving demo is user-gated. The shared projectile/effect
foundation is hardened through Step 9 and its single-client visual gate passes.
Next is the F.4/L.1d retail cast-lifecycle gap audit, then F.5 spellbook and
active enchantments. Carried: the two-client portal/observer VFX gate,
#145-residual, #116 slide-response, R6/TS-42, and Track MP0.
---
@ -426,13 +423,21 @@ unblocks that).
---
### M2 — "Kill a drudge" — 🔵 ACTIVE (M1.5 landed 2026-07-10)
### M2 — "Kill a drudge" — ✅ LANDED 2026-07-15
**Demo scenario:** Equip a sword. Walk to a drudge. Swing. See "You hit
Drudge for 12 slashing damage (87%)" in chat. Watch the swing animation
play. Drudge dies, drops loot. Pick up the loot. Open the inventory panel
and see it.
**Landed:** The connected Release client now completes that entire loop. Both
melee and missile weapons switch through server-authoritative inventory
transactions; attacks face the selected hostile target before release; combat
animation, damage text, persistent death, auto-target, loot, inventory/stack
operations, paperdoll/toolbar interaction, and world-item giving have passed
user gates. Work discovered outside this demo remains explicitly carried below
rather than keeping the milestone open indefinitely.
**First port target when M2 starts (per the M2 combat-math research memo,
`docs/research/2026-06-04-combat-math-deep-dive.md`):**
`CombatMath.ComputeDamage` — damage-calc + armor-resists are port-ready
@ -454,7 +459,7 @@ 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 inventory weapon switching (implemented 2026-07-12; live gate pending)** —
- **L.1c inventory weapon switching (implemented and live-gated 2026-07-15)** —
inventory double-click ports retail's two-stage `AutoWield`: return the occupied
primary weapon to the main pack, then any incompatible shield/ammo, waiting
for every server move confirmation before wielding the requested
@ -463,7 +468,7 @@ include dungeons.
acknowledgement independently of rollback bookkeeping, and prevents rapid
switches from overlapping or remaining stuck; ACE owns stance-specific motions and its PropertyInt 40 update
now drives the client `CombatState`.
- **M2 item giving (implemented 2026-07-13; starter-dungeon live gate pending)** —
- **M2 item giving (implemented and starter-dungeon live-gated 2026-07-13)** —
a physical inventory drag released over a 3-D creature now ports
`ItemHolder::AttemptPlaceIn3D` through the exact `GiveObjectRequest 0x00CD`
packet, including selected partial-stack quantities and authoritative-only
@ -565,11 +570,11 @@ include dungeons.
modes retain the billboard path. This restores the four-piece apex of
Armor/protection shield effects. The final two-client visual gate is still
pending.
- **L.1c local attack receive path (implemented 2026-07-11; live gate pending)** —
- **L.1c local attack receive path (implemented and live-gated 2026-07-15)** —
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.
- **M2 basic combat bar (implemented 2026-07-11; live gate pending)** — authored
- **M2 basic combat bar (implemented and live-gated 2026-07-15)** — authored
`gmCombatUI` LayoutDesc `0x21000073` now pops for Melee/Missile and shares one
press/hold/release attack-request owner across DAT buttons and keybindings.
Retail x86 resolves full charge to 1.0 s (0.8 s dual wield), also retiring the
@ -614,13 +619,21 @@ a game.
---
### M3 — "Cast a spell" — 🔵 (~34 weeks after M2)
### M3 — "Cast a spell" — 🔵 ACTIVE (started 2026-07-15)
**Demo scenario:** Cast Flame Bolt at a drudge. Watch the cast animation,
the projectile, the impact. Self-cast a buff (Strength Self). See the
enchantment in a buff list. Recall to lifestone — full recall animation,
correct teleport, correct re-spawn.
**Starting position:** E.5 already supplies the cast wire and authoritative
spell/enchantment state, while the M2/M3 campaign supplies traveling physical
and spell missiles, DAT PhysicsScripts, live part-attached particles/lights,
Hidden/UnHide, and recall motion/effects. Representative projectiles and
protection effects pass the single-client visual gate. M3 begins with a focused
named-retail gap audit of F.4/L.1d—not a rewrite of this foundation—then fills
the spellbook/active-enchantment UI and completes the two-client portal gate.
**Phases to ship:**
- **F.4** — Spell cast state machine (buffs + recalls first, projectile
spells second).

View file

@ -0,0 +1,377 @@
# Wield ownership and targeted-action facing pseudocode
Date: 2026-07-14
## Sources
- Named retail `ItemHolder::DetermineUseResult @ 0x00588460`
- Named retail `ACCWeenieObject::UIAttemptWield @ 0x0058D590`
- Named retail `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0`
- Named retail `ACCWeenieObject::SetPlayerWieldLocation @ 0x0058D8C0`
- Named retail `ACCObjectMaint::ViewObjectContents @ 0x00558A70`
- Named retail `gmPaperDollUI::ServerSaysMoveItem @ 0x004A64A0`
- Named retail `gmToolbarUI::RecvNotice_ServerSaysMoveItem @ 0x004BF2B0`
- Named retail WieldObject dispatch `0x0055B27C..0x0055B2B5`
- Named retail `ClientCombatSystem::ExecuteAttack @ 0x0056BB70`
- Named retail `ClientCombatSystem::StartAttackRequest @ 0x0056C040`
- Named retail `CameraSet::ToggleMouseLook @ 0x00457490`
- Named retail `CameraSet::MouseLookHandler @ 0x00458D80`
- Named retail `CameraSet::Rotate @ 0x00458310`
- Named retail `CommandInterpreter::HandleMouseMovementCommand @ 0x006B3870`
- Named retail `CommandInterpreter::StopListHeadMovement @ 0x006B35A0`
- Named retail `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`
- Named retail `MovementManager::unpack_movement @ 0x00524440`
- Named retail `MoveToManager::TurnToObject @ 0x005297D0`
- Named retail `CommandInterpreter::SendMovementEvent @ 0x006B4680`
- Named retail `CommandInterpreter::SendPositionEvent @ 0x006B4770`
- Named retail `CommandInterpreter::ShouldSendPositionEvent @ 0x006B45E0`
- Named retail `CommandInterpreter::UseTime @ 0x006B3BF0`
- Named retail `Frame::is_equal @ 0x00424C30`
- Named retail `Frame::is_quaternion_equal @ 0x00424C70`
- Named retail `Position::Pack @ 0x005A9640`
- ACE `Player_Missile.HandleActionTargetedMissileAttack` and
`Creature.Rotate/TurnToObject` (interpretation aid; retail remains the oracle)
## Wield request and authoritative ownership
`UIAttemptWield` does not rewrite the item's public weenie fields. It sends the
request and records the one outstanding inventory request:
```text
UIAttemptWield(item, location):
if player is not ready for an inventory request:
return
if item is a split stack and a partial split is selected:
send StackableSplitToWield(item.id, location, splitSize)
return
send GetAndWieldItem(item.id, location)
prevRequestObjectID = item.id
prevRequest = Wield
prevRequestTime = now
```
`DetermineUseResult` uses the authoritative `wielderID`, not container
membership, to decide whether a player-owned weapon needs to be wielded:
```text
if item is owned by the player
and (item.combatUse != None or item is a caster or item has WieldOnUse)
and item.wielderID != player.id:
return WieldLeft when WieldLeft is set, otherwise WieldRight
```
The server move callback updates container, wielder, and location as one
authoritative state transition:
```text
ServerSaysMoveItem(item, newContainer, placement, newWielder, newLocation):
oldContainer = item.containerID
oldWielder = item.wielderID
oldLocation = item.location
remove item from oldContainer.contents, when present
item.containerID = newContainer
add item to newContainer.contents at placement, when present
if newWielder == player.id:
SetPlayerWieldLocation(item, newLocation)
item.wielderID = player.id
else if oldWielder == player.id:
SetPlayerWieldLocation(item, None)
item.wielderID = 0
item.location = newLocation
clear the matching outstanding inventory request
notify inventory UI with old/new container, slot, wielder, and location
```
Paperdoll and toolbar consumers test the complete old and new placement. A
post-mutation item plus old/new container alone is insufficient: confirmed
equipment has `ContainerId=0`, so an unwield into a side bag would otherwise
lose the old player wielder/location and fail to clear the visible slot.
Retail ViewContents is a different operation:
```text
ViewObjectContents(container, contentProfile):
clear container.itemsList and container.containersList
rebuild those ordered lists from ContentProfile
do not mutate any child containerID, wielderID, or location
```
PlayerDescription therefore initializes login inventory ownership explicitly;
later ViewContents packets replace only the viewed list projection. A later
authoritative move or delete evicts the GUID from every stale viewed-list
projection before applying its new canonical membership. Only move, wield, and
put-in-3D events publish placement transitions.
PlayerDescription assigns its complete packed InventoryPlacement list directly,
preserving wire order. Later live `SetPlayerWieldLocation` calls insert the new
placement at the head, and `GetObjectAtLocation` scans head-to-tail. Combat mode
and ammo selection must preserve this mixed lifecycle rather than sorting
confirmed equipment by GUID.
The retail 0x22 and 0x23 dispatchers publish `ServerSaysMoveItem_s` even when
the item GUID has not resolved to a weenie yet. The notice carries the GUID and
zero old placement plus the raw new placement; it must not be suppressed or
manufacture a retained stub. This lets UI transaction owners observe the exact
server boundary despite CreateObject/message reordering.
The WieldObject event itself contains exactly two words. Its dispatch supplies
the local player identity and an empty container to `ServerSaysMoveItem`:
```text
WieldObject(itemGuid, equipLocation):
item = FindObject(itemGuid)
ServerSaysMoveItem(
item,
newContainer = 0,
placement = 0,
newWielder = GetPlayerID(),
newLocation = equipLocation,
isServerMove = true)
```
Conformance consequence: a bow moved from the paper doll into a backpack must
have `WielderId == 0` after the authoritative PutObjectInContainer event. If it
retains the player's ID, the next double click is classified as already wielded
and no GetAndWieldItem request is sent.
## Targeted missile facing ownership
Retail's client combat UI does not invent a local pre-attack rotation:
```text
ClientCombatSystem::ExecuteAttack(target, height, power):
validate target and player readiness
send Event_TargetedMissileAttack(target.id, height, power)
```
The authoritative movement response is consumed through the ordinary movement
funnel:
```text
MovementManager::unpack_movement(server movement):
interrupt current movement
apply the wire stance when it changed
case TurnToObject:
unpack object id, heading, and turn parameters
physicsObject.TurnToObject(object id, parameters)
```
ACE independently confirms the expected server sequence for the local test
environment:
```text
HandleActionTargetedMissileAttack(target):
rotateTime = Rotate(target)
wait rotateTime
LaunchMissile(target)
Rotate(target):
broadcast server-controlled TurnToObject(target)
calculate the retail motion-table turn duration
```
Therefore missile facing belongs in the shared inbound TurnToObject execution
path. Delaying the outbound missile attack request or synthesizing a bow-specific
turn would be a retail divergence. Targeted spell facing needs its own retail
casting-path proof before making the same claim.
The inbound turn also depends on the server knowing the player's current
heading. ACE calculates its turn duration from the server-side `Location`
orientation. If a stationary client turn never publishes its quaternion, ACE
can believe the player already faces the target, schedule a zero-duration turn,
and launch before the ordinary inbound TurnToObject visibly completes.
Retail keeps that authority synchronized through the full position-event
cadence:
```text
CommandInterpreter.UseTime():
if ShouldSendPositionEvent():
SendPositionEvent()
ShouldSendPositionEvent():
if lastSentTime + 1 second >= now:
return cell changed OR contact plane changed
return cell changed OR NOT Frame.is_equal(lastFrame, player.frame)
Frame.is_equal(a, b):
return origin components differ by <= 0.0002 world units
AND quaternion W/X/Y/Z components differ by < 0.0002
SendMovementEvent():
send MoveToState(player.m_position)
lastSentTime = now
// do not replace last frame or contact plane
SendPositionEvent():
send AutonomousPosition(player.m_position)
lastSentTime = now
lastSentPosition = player.m_position // cell + origin + orientation
lastSentContactPlane = player.contact_plane
```
Quaternion comparison is component-wise; retail does not canonicalize the
equivalent stored `q` and `-q` representations during this comparison. Plane
comparison uses `<= 0.0002` for the three normal components but `< 0.0002` for
the distance component. `UseTime` proves the internal Should→AP order, while
MTS originates in separate input handlers; their relative same-tick callback
and wire order is not established by these functions and remains the narrow
TS-33 trace item. This is the shared correction for melee, missile, use, and
spell targeting; there is no weapon-specific pre-turn or artificial attack
delay.
## Instant mouse-look is player movement, not a render-only yaw
Retail's middle-mouse instant look does not mutate the visible character frame
behind the movement system. The camera owner translates the mouse into ordinary
turn motions and the command interpreter publishes those motions to the server:
```text
CameraSet.ToggleMouseLook(active):
mouseLookActive = active
commandInterpreter.SetMouseLookActive(active)
commandInterpreter.MovePlayer(CameraInstantMouseLook, active, 1, mouse=true, holdRun=true)
if !active:
commandInterpreter.ApplyCurrentMovement()
commandInterpreter.SendMovementEvent()
// Toggle does not update CameraSet.m_LastServerMessage.
CInputManager_WIN32.GetInput():
accumulate all raw device records into the frame's cursor displacement
if the cursor position changed:
lastInputEvent = now
CallMouseLookHandler(totalDx, totalDy) once
else if mouseLookActive && now > lastInputEvent + 0.2 seconds:
CallMouseLookHandler(0, 0)
CameraSet.MouseLookHandler(mouseX, mouseY):
if raw mouseX == 0 && raw mouseY == 0:
StopDrift()
if now > m_LastServerMessage + 0.5 seconds:
m_LastServerMessage = now
SendMovementEvent()
low-pass filter the one accumulated raw sample
multiply by configured input sensitivity and 0.0666666701
on horizontal zero: reset mouselook_x_extent
on horizontal nonzero: increment mouselook_x_extent
only after five consecutive nonzero samples:
CameraSet.Rotate(filtered horizontal input)
Rotate publishes only when its own invocation crosses the same strict
m_LastServerMessage + 0.5-second boundary
CameraSet.Rotate(horizontal input):
direction = horizontal input selects TurnLeft or TurnRight
speed = 2 * filtered camera adjustment for this frame
speed = min(speed, 1.5)
commandInterpreter.MovePlayer(direction, start=true, speed, mouse=true, holdRun=true)
if last mouse movement event + 0.5 seconds < now:
commandInterpreter.SendMovementEvent()
CommandInterpreter.HandleMouseMovementCommand(command):
TakeControlFromServer()
remember command as mouse-origin movement
MovePlayer(command.motion, start=true, command.speed, mouse=true, holdRun=true)
SendMovementEvent()
CommandInterpreter.StopListHeadMovement():
if the active command came from the mouse:
MovePlayer(command.motion, start=false, command.speed, mouse=true, holdRun=true)
CommandInterpreter.TakeControlFromServer():
if controlled_by_server and autonomy_level != 0 and player is alive:
controlled_by_server = false
player.last_move_was_autonomous = true
player.StopCompletely()
StopInterpolating()
SetHoldRun()
ApplyCurrentMovement()
```
`ApplyCurrentMovement` means the complete device snapshot: global run state,
forward/back, sidestep, and turn. `CameraInstantMouseLook` remaps a held
keyboard turn into the sidestep channel with that axis's `HoldKey.Run`, even
when the global movement mode is walk. Both the synchronous toggle packet and
later movement-edge packets must serialize the canonical remapped raw channel;
reconstructing sidestep from only the physical strafe keys loses A/D after MMB
is already active. `RawMotionState.CurrentHoldKey` is serialized independently
of active axes, so an idle run/walk change cannot be inferred from “is moving.”
Attack entry also clears client movement before the request:
```text
ClientCombatSystem.StartAttackRequest(...):
attack_request_in_progress = true
requested_attack_power = 1
FinishJump()
commandInterpreter.MaybeStopCompletely()
current_build_is_automatic = false
build and send the attack request
CommandInterpreter.MaybeStopCompletely():
if player movement is not currently controlled by the server:
StopCompletely()
```
The architectural invariant is therefore one heading with one owner: the
character seen on screen, the local physics orientation, the raw outbound turn
state, and ACE's authoritative player orientation all advance through the same
movement command. A mouse callback that writes `PlayerMovementController.Yaw`
directly violates that invariant. It creates the exact failure mode observed in
the bow test: the client can appear 180 degrees away while ACE retains the old
heading, computes a zero-duration `Rotate(target)`, and launches immediately.
ACE 1.76.4751 commit `7233b0e57db9a8566954d8dfbc2128c84e64c4ff`
cross-checks the consequence: `HandleActionTargetedMissileAttack` calls
`Rotate(target)` and delays by the returned turn time; `Creature.Rotate`
computes that delay from the server-side heading and broadcasts
`TurnToObject`. ACE is an interpretation aid here; the client mechanism and
ordering above come from named retail.
## Outbound position frame
Retail never reconstructs protocol coordinates from a renderer origin:
```text
SendMovementEvent():
MoveToStatePack(rawMotionState, &player.m_position, ...)
SendMoveToStateEvent(pack)
SendPositionEvent():
if Position.IsValid(player.m_position):
AutonomousPositionPack(&player.m_position, ...)
SendAutonomousPositionEvent(pack)
Position.Pack():
write objcell_id
Frame.Pack(frame) // landblock-local origin + orientation
```
`CPhysicsObj::m_position` is the canonical carried pair `(cell id, local
Frame.Origin)`. In acdream the equivalent is `PhysicsBody.CellPosition`.
`_liveCenterX/Y` is a render/streaming translation and must not participate in
outbound messages.
The captured regression was exact: the player crossed from A9B1 to A9B2 and
the logical entity's projection was recovered. That recovery re-ran the login
origin initialization, moving `_liveCenterY` by one block while the existing
world position remained in the A9B1 render frame. Reconstructing local Y then
sent `214.56` instead of the carried `22.56`, a 192 m displacement. The fix has
two invariants:
```text
first accepted canonical player Position in CreateObject or Position update:
initialize render/streaming origin once, before world translation
non-player projection before that Position:
retain canonical live record, defer render projection
spatial projection recovery, rebucketing, or appearance mutation afterward:
never initialize or move the session origin
every outbound movement/position/jump packet:
serialize PlayerMovementController.CellPosition directly
```