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:
parent
b26f84cc69
commit
7b7ffcd278
36 changed files with 3884 additions and 761 deletions
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 30–50×); 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 17–26 days focused work, 3–5 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 0–5 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.
|
||||
|
|
|
|||
|
|
@ -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" — 🔵 (~3–4 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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue