feat(retail): Commit B — retail-faithful AP cadence + screen-rect picker
Retires divergences flagged in the 2026-05-16 faithfulness audit: 1. AP cadence. Replaces the 1 Hz idle / 10 Hz active flat heartbeat with a diff-driven model gated on `Contact && OnWalkable` (acclient_2013_pseudo_c.txt:700327 SendPositionEvent). Sends on position or cell change while grounded on walkable, plus a 1 sec heartbeat; suppressed entirely airborne. PlayerMovementController exposes `NotePositionSent(pos, cellId, now)` which GameWindow stamps after each AutonomousPosition / MoveToState send — mirrors retail's shared `last_sent_position_time` between SendPositionEvent (0x006b4770) and SendMovementEvent (0x006b4680). Known divergence from retail: ours is per-frame-while-moving, retail's effective rate is ~1 Hz during smooth motion (cell/plane checks). Filed as #74, blocked by #63 — when #63 lands we revert to retail's narrower gate. 2. Workaround retirement. Removes TinyMargin (0.05 m inside arrival) and the AP-flush before re-send (`SendAutonomousPositionNow`). The diff-driven cadence makes both obsolete. Close-range turn-first deferred Use is kept (it IS retail — ACE Player_Move.cs:66-87 mirrors retail's CreateMoveToChain pre-callback rotation), renamed `OnAutoWalkArrivedSendDeferredAction` to clarify it's a FIRST send. `isRetryAfterArrival` parameter dropped. 3. Far-range Use/PickUp retry. Restored — was load-bearing, not the "redundant cleanup" the Group 2 audit thought. Issue #63 means ACE drops the first Use as too-far without re-polling on subsequent APs; the arrival re-send is what makes far-range Use complete. Logs include `(queued for arrival re-send pending #63)` to make this explicit. Removes when #63 closes. 4. Screen-rect picker. New `AcDream.Core.Selection.ScreenProjection` helper shared by `WorldPicker` and `TargetIndicatorPanel`. The `Setup.SelectionSphere` projects to a screen-space square (retail anchor `SmartBox::GetObjectBoundingBox` 0x00452e20); picker hit-tests the mouse pixel against the same rect the indicator draws, inflated by 8 px (`TriangleSize`). Guarantees what-you-see is what-you-click — including rect corners that were dead zones under the old ray-sphere picker. Per-type radius (1.0/1.6/2.0 m) and vertical-offset (0.2/0.9/1.0/1.5 m) heuristic lambdas retired; `IsTallSceneryGuid` deleted; `EntityHeightFor` trimmed to 1.5 m × scale defensive default. No defensive sphere synth — entities without a baked `SelectionSphere` are skipped, matching retail's `GfxObjUnderSelectionRay` (0x0054c740). 5. Rotation rate run multiplier (Commit A precursor). `TurnRateFor(running)` helper applies retail's `run_turn_factor = 1.5f` (PDB-named 0x007c8914) under HoldKey.Run, matching `apply_run_to_command` at 0x00527be0 (line 305098). Effective: walking ≈ 90°/s, running ≈ 135°/s. Keyboard A/D + ApplyAutoWalkOverlay both use it. 6. Useability gate (Commit A precursor). `IsUseableTarget` corrected to `useability != 0` per `ItemUses::IsUseable` at 256455 — ANY non-zero passes (USEABLE_NO=1, USEABLE_CONTAINED=8, etc.), not just the USEABLE_REMOTE bit. Cross-checked against 4 call sites in retail (ItemHolder::UseObject 0x00588a80, DetermineUseResult 0x402697, UsingItem 0x367638, disable-button-state 0x198826). Added `ProbeUseabilityFallbackEnabled` diagnostic (`ACDREAM_PROBE_USEABILITY_FALLBACK=1`) to measure how often the creature/BF_DOOR fallback fires for ACE-seed-DB entities with null useability. CLAUDE.md updated with the graceful-shutdown rule for relaunch: Stop-Process bypasses the logout packet, leaving ACE's session marked logged-in for ~3+ min. CloseMainWindow() sends WM_CLOSE so the shutdown hook runs and the logout packet reaches ACE. Tests: +3 ScreenProjectionTests + 6 WorldPickerRectOverloadTests = +9. Core.Net 294/294 pass; Core 1073/1081 (8 pre-existing Physics failures unchanged). Visual-verified 2026-05-16: rotation rate, useability, screen-rect click area, double-click + R-key + F-key Use/PickUp at short and long range — dialogue/door/pickup fire on arrival. Filed follow-ups #70 (triangle apex/size DAT sprite), #71 (picker Stage B polygon refine), #72 (cdb omega.z probe), #73 (retail-message sweep pattern), #74 (per-frame AP chattier than retail — blocked by #63). Old ray-sphere `WorldPicker.Pick(origin, direction, ...)` overload kept for back-compat; no callers in acdream proper. Plan: docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2bc3a9e99
commit
b5da17db76
10 changed files with 1348 additions and 573 deletions
168
docs/ISSUES.md
168
docs/ISSUES.md
|
|
@ -46,6 +46,174 @@ Copy this block when adding a new issue:
|
|||
|
||||
# Active issues
|
||||
|
||||
## #74 — AP cadence is per-frame-while-moving, more chatty than retail
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (works; just sends ~60× the packets retail would during smooth motion)
|
||||
**Filed:** 2026-05-16
|
||||
**Component:** physics / net cadence
|
||||
|
||||
**Description:** The diff-driven AP cadence shipped in Commit B fires
|
||||
`HeartbeatDue` on **any** position change each frame while grounded
|
||||
on walkable (effective ~60 Hz during smooth movement) and a 1 Hz
|
||||
heartbeat when idle. Retail's `ShouldSendPositionEvent`
|
||||
(`acclient_2013_pseudo_c.txt:700233`) only sends during the
|
||||
sub-interval when cell or contact-plane changes, and only sends the
|
||||
1 Hz heartbeat if `(cellId, frame)` changed since `last_sent` —
|
||||
truly idle = 0 Hz. So retail during continuous smooth movement is
|
||||
effectively 1 Hz (cell crosses + plane changes don't happen every
|
||||
frame); we are ~60 Hz.
|
||||
|
||||
**Root cause / status:** Deliberate ACE-targeted choice. The
|
||||
per-frame cadence is load-bearing for ACE's `WithinUseRadius` poll
|
||||
to see the player arrive at a target during local speculative
|
||||
auto-walk (issue #63's workaround chain). Going to 1 Hz would
|
||||
re-introduce the arrival-lag bug for far-range Use/PickUp.
|
||||
|
||||
**Files:** [PlayerMovementController.cs:1240-1275](src/AcDream.App/Input/PlayerMovementController.cs)
|
||||
— the `HeartbeatDue = groundedOnWalkable && (positionChanged || intervalElapsed)`
|
||||
gate.
|
||||
|
||||
**Acceptance:** Either (a) fix issue #63 so we honor ACE's
|
||||
`MoveToObject` server-side, removing the need for the per-frame
|
||||
cadence, then revert to retail's `cell-or-plane-change || (interval && frame-change)`
|
||||
shape (~5 LOC change); or (b) document this as a permanent
|
||||
divergence and update commit messages / code comments to match.
|
||||
|
||||
**Estimated scope:** Small (~5 LOC + commit-message rewrite) once
|
||||
#63 is fixed. Currently blocked by #63.
|
||||
|
||||
---
|
||||
|
||||
## #73 — Retail-message centralization plan — per-feature string sweeps
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (per-feature work, not infrastructure)
|
||||
**Filed:** 2026-05-16
|
||||
**Component:** ui / retail messages
|
||||
|
||||
**Description:** Commit A added `AcDream.Core.Ui.RetailMessages` as
|
||||
the home for retail-decomp-sourced UI strings (`CannotBeUsed`,
|
||||
`CantBePickedUp`, `CannotPickUpCreatures`). The retail decomp has
|
||||
~750 more user-facing strings we'll need over time — combat misses,
|
||||
spell fizzles, vendor dialogs, "you do not have enough" etc. Rather
|
||||
than bulk-port them once, port per-feature as the feature lands:
|
||||
when wiring vendor purchase, sweep vendor strings into
|
||||
`RetailMessages.Vendor.*`; when wiring spell-cast feedback, sweep
|
||||
`RetailMessages.Spell.*`.
|
||||
|
||||
**Status:** No infrastructure work pending. Pattern is established;
|
||||
new strings get added to `RetailMessages.cs` with retail anchor
|
||||
comments at the call site that triggered the need.
|
||||
|
||||
**Files:** [RetailMessages.cs](src/AcDream.Core/Ui/RetailMessages.cs)
|
||||
— class-level doc comment already describes the per-feature sweep
|
||||
pattern.
|
||||
|
||||
**Acceptance:** Each phase / feature that adds new user-facing
|
||||
strings sweeps its retail-anchor strings into `RetailMessages` and
|
||||
calls them by name rather than literal-in-place. Closing condition:
|
||||
"all M1 demo strings are in RetailMessages" or similar per-milestone
|
||||
gate, decided when M1 ships.
|
||||
|
||||
---
|
||||
|
||||
## #72 — Confirm Humanoid TurnRight/TurnLeft `omega.z` base rate via cdb
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (current ±π/2 fallback matches all corroborating
|
||||
evidence; cdb probe would settle the open question for good)
|
||||
**Filed:** 2026-05-16
|
||||
**Component:** physics / rotation / research
|
||||
|
||||
**Description:** Commit A's rotation rate uses
|
||||
`BaseTurnRateRadPerSec = π/2` based on the documented
|
||||
`AnimationSequencer.cs:734-741` claim that the Humanoid motion table
|
||||
ships TurnRight/TurnLeft with `HasOmega` cleared (forcing the
|
||||
convention fallback). The constant has 3 corroborating sources but
|
||||
the actual dat content was never dumped — and the run-multiplier
|
||||
`run_turn_factor = 1.5` at retail `0x007c8914` from
|
||||
`apply_run_to_command` (decomp 0x00527be0) likewise hasn't been
|
||||
verified live.
|
||||
|
||||
**Acceptance:** Set a cdb breakpoint on `CSequence::set_omega`
|
||||
(`acclient_2013_pseudo_c.txt` — find exact symbol address) while
|
||||
holding A or D in a retail client. Capture the `omega.z` argument
|
||||
value walking, then running. If `±π/2` walking and `±π/2 × 1.5 ≈ 2.356`
|
||||
running, close as confirmed. If different, file as a regression and
|
||||
fix the constants in
|
||||
[RemoteMoveToDriver.cs](src/AcDream.Core/Physics/RemoteMoveToDriver.cs).
|
||||
|
||||
**Estimated scope:** ~30 min cdb session + 1 commit if confirmed,
|
||||
or +small fix if different. Not blocking M1.
|
||||
|
||||
---
|
||||
|
||||
## #71 — WorldPicker Stage B — polygon refine for retail-accurate clicks
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (Stage A — screen-rect picker — is sufficient for M1)
|
||||
**Filed:** 2026-05-16
|
||||
**Component:** selection / picker
|
||||
|
||||
**Description:** Retail's mouse picker does two-tier sphere-then-polygon
|
||||
selection (`acclient_2013_pseudo_c.txt:0x0054c740`
|
||||
`Render::GfxObjUnderSelectionRay`):
|
||||
1. Per-part sphere reject via `CGfxObj::drawing_sphere`.
|
||||
2. Polygon-accurate refine via `CPolygon::polygon_hits_ray` on every
|
||||
visual polygon; closest-t polygon hit wins over any sphere hit.
|
||||
|
||||
Commit B's Stage A
|
||||
([WorldPicker.cs](src/AcDream.Core/Selection/WorldPicker.cs)) does
|
||||
screen-space rect hit-test against the projected
|
||||
`Setup.SelectionSphere` (matching the indicator rect, deliberately
|
||||
broader than the visible mesh polygons). Stage B would tighten clicks
|
||||
to the visible mesh — under-pick what looks like empty space inside
|
||||
the rect, catch visible mesh that pokes past the sphere boundary
|
||||
(creature outstretched arm, sign edge).
|
||||
|
||||
**Acceptance:** Pipe per-part GfxObj visual polygons through a
|
||||
`PickPolygonProvider` interface (don't duplicate mesh decoding —
|
||||
hook the existing `ObjectMeshManager` cached data). Two-tier in
|
||||
`WorldPicker.Pick`: sphere reject → polygon scan → polygon hit
|
||||
dominates sphere hit. Acceptance test: visible-mesh accuracy on
|
||||
Holtburg sign, Royal Guard outstretched bow arm, inn-door wood
|
||||
frame edges.
|
||||
|
||||
**Estimated scope:** Medium (~4-6 hours). Defer until visual
|
||||
verification surfaces a Stage A miss in real play. The user
|
||||
confirmed 2026-05-16 that "I can click on longer ranges now so
|
||||
good" — Stage A is enough for M1's "click an NPC" demo.
|
||||
|
||||
---
|
||||
|
||||
## #70 — Triangle apex/size — final retail-feel UX pass
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (cosmetic — indicator already retail-anchored, this is final-feel polish)
|
||||
**Filed:** 2026-05-16
|
||||
**Component:** ui / target indicator
|
||||
|
||||
**Description:** Per 2026-05-16 user feedback during the
|
||||
`SelectionSphere` indicator ship, the triangle apex direction
|
||||
(flipped to point inward at the target) and sprite size (currently
|
||||
8 px legs) are heuristic visual choices. Retail uses an actual DAT
|
||||
sprite from `UIRegion::GetChild(0x1000003a/3b/3c)` — the bitmap
|
||||
shape and size come from the dat, not constants.
|
||||
|
||||
**Acceptance:** Extract the retail triangle sprite from the dat
|
||||
(probably via `tools/UiLayoutMockup` or a new `DatSpriteProbe`) and
|
||||
either (a) blit the exact bitmap, or (b) pick a procedural size +
|
||||
shape that matches it pixel-for-pixel at standard zoom.
|
||||
|
||||
**Files:** [TargetIndicatorPanel.cs](src/AcDream.App/UI/TargetIndicatorPanel.cs)
|
||||
— `TriangleSize` constant + the four `AddTriangleFilled` calls.
|
||||
|
||||
**Estimated scope:** Small (~1-2 hours, mostly dat exploration).
|
||||
Not blocking M1.
|
||||
|
||||
---
|
||||
|
||||
## #69 — Local player rotation isn't animated (no leg/arm cycle while pivoting)
|
||||
|
||||
**Status:** OPEN
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue