fix(retail): rotation rate, useability gate, retail toast strings
Two retail divergences fixed from the 2026-05-16 faithfulness audit
(Commit A of the plan at docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md).
1. Rotation rate ignored HoldKey.Run. Retail's CMotionInterp::
apply_run_to_command (decomp 0x00527be0 line 305098) multiplies
turn_speed by run_turn_factor (1.5, PDB-named symbol at 0x007c8914)
when input is TurnRight/TurnLeft under HoldKey.Run. Effective
running rotation is 50% faster (~135°/s vs walking ~90°/s).
Our keyboard A/D and ApplyAutoWalkOverlay used a fixed walking
rate.
New: RemoteMoveToDriver.TurnRateFor(running) helper. Keyboard
path passes input.Run; auto-walk overlay passes
_autoWalkInitiallyRunning. The walking-rate base
(BaseTurnRateRadPerSec = π/2) is unchanged; TurnRateRadPerSec
constant is preserved as the walking-rate alias for callers
that don't have run/walk state (NPC remotes).
2. IsUseableTarget gated on `useability & USEABLE_REMOTE (0x20)`,
which was stricter than retail. Per ItemUses::IsUseable
(acclient_2013_pseudo_c.txt:256455) cross-referenced with 4
call sites, retail's IsUseable() semantic is `_useability != 0`.
But visually retail's USEABLE_NO (1) entities don't approach
either, because ACE never broadcasts MovementType=6 for them.
Our client installs a speculative auto-walk BEFORE the server
responds, so we'd visibly approach + face signs before the
wire packet was rejected.
Pragmatic fix: block USEABLE_UNDEF (0) AND USEABLE_NO (1) in
IsUseableTarget — slightly stricter than retail's
IsUseable but matches retail's user-visible behaviour
("R on sign does nothing"). Documented in the doc-comment so
a future implementer knows the gap.
3. New IsPickupableTarget gate for F-key path — requires
USEABLE_REMOTE (0x20) bit. Null-useability fallback for
BF_CORPSE + small-item ItemTypes (preserves M1 ground-item
pickup flow when ACE seed DB doesn't publish useability).
4. R-key (UseCurrentSelection) upfront gate now ALWAYS uses
IsUseableTarget. R is conceptually "use" with smart-routing
to pickup as a downstream optimization. F-key (SendPickUp)
uses IsPickupableTarget directly.
5. Retail toast strings on block, centralised in new
src/AcDream.Core/Ui/RetailMessages.cs:
- "The X cannot be used" (data 0x007e2a70, sprintf 0x00588ea4)
fires on UseCurrentSelection / SendUse gate block.
- "The X can't be picked up!" (sprintf 0x00587353) fires on
SendPickUp non-pickupable block.
- "You cannot pick up creatures!" (data 0x007e22b4) fires on
SendPickUp creature block (was previously silent).
- Plus 4 inactive retail strings ready for future call sites:
CannotBeUsedWith (two-target Use), CannotBePickedUp (formal
pickup variant), CannotBeUsedWhileOnHook_HooksOff +
CannotBeUsedWhileOnHook_NotOwner (housing). All cite their
retail data addresses + runtime sprintf addresses.
6. ProbeUseabilityFallbackEnabled diagnostic (env var
ACDREAM_PROBE_USEABILITY_FALLBACK=1) logs every time the
null-useability fallback fires. Settles whether the
fallback for creature + BF_DOOR/LIFESTONE/PORTAL/CORPSE
entries in ACE's seed DB without useability is hot code
or theoretical defense.
Test coverage:
- +3 RemoteMoveToDriverTests cover TurnRateFor walking/running/back-compat.
- +7 RetailMessagesTests cover each retail string with retail anchor.
- +1 CreateObjectTests TryParse_WeenieFlagsUsable_ReadsUseableNoValue
pins parser correctness for USEABLE_NO=1.
- 294/294 Core.Net pass; 24/24 new+touched Core tests pass.
- Pre-existing baseline of 8 Physics test failures unchanged
(BSPStepUp + MotionInterpreter regression noise from prior
sessions; out of scope here).
Deferred to a separate session per user direction:
- Click area = indicator-rect retail fidelity. Retail's picker
uses per-part CGfxObj.drawing_sphere + polygon refine
(0x0054c740); ours uses single Setup.SelectionSphere ray-
intersect. The rect corners are dead zones today. Three fix
options analyzed: screen-space rectangle hit-test, sqrt(2)
sphere inflation, polygon refine Stage B.
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
f4f4143ac0
commit
e0d5d271f3
9 changed files with 2040 additions and 38 deletions
|
|
@ -502,7 +502,12 @@ public sealed class PlayerMovementController
|
|||
// MathF.Min(|delta|, maxStep) naturally clamps the final
|
||||
// fractional step to exactly delta, so we land on the
|
||||
// target heading without overshoot.
|
||||
float maxStep = RemoteMoveToDriver.TurnRateRadPerSec * dt;
|
||||
// 2026-05-16 — retail-faithful turn rate. Auto-walk knows
|
||||
// its run/walk decision from _autoWalkInitiallyRunning
|
||||
// (set at BeginServerAutoWalk based on initial distance vs
|
||||
// WalkRunThreshold). Running rotation is 50% faster per
|
||||
// run_turn_factor at retail 0x007c8914.
|
||||
float maxStep = RemoteMoveToDriver.TurnRateFor(_autoWalkInitiallyRunning) * dt;
|
||||
Yaw += MathF.Sign(delta) * MathF.Min(MathF.Abs(delta), maxStep);
|
||||
while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI;
|
||||
while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI;
|
||||
|
|
@ -637,10 +642,21 @@ public sealed class PlayerMovementController
|
|||
}
|
||||
|
||||
// ── 1. Apply turning from keyboard + mouse ────────────────────────────
|
||||
// 2026-05-16 — retail-faithful turn rate.
|
||||
// Anchor: docs/research/named-retail/acclient_2013_pseudo_c.txt
|
||||
// - CMotionInterp::apply_run_to_command 0x00527be0
|
||||
// multiplies turn_speed by run_turn_factor (1.5) under
|
||||
// HoldKey.Run on TurnRight/TurnLeft commands.
|
||||
// - Base rate ±π/2 rad/s comes from add_motion 0x005224b0
|
||||
// with HasOmega-cleared MotionData fallback.
|
||||
// Effective: walking ≈ 90°/s, running ≈ 135°/s.
|
||||
// Previously: WalkAnimSpeed*0.5 ≈ 89.4°/s — coincidentally
|
||||
// close to retail walking but no run differentiation.
|
||||
float keyboardTurnRate = RemoteMoveToDriver.TurnRateFor(input.Run);
|
||||
if (input.TurnRight)
|
||||
Yaw -= MotionInterpreter.WalkAnimSpeed * 0.5f * dt; // ~90°/s
|
||||
Yaw -= keyboardTurnRate * dt;
|
||||
if (input.TurnLeft)
|
||||
Yaw += MotionInterpreter.WalkAnimSpeed * 0.5f * dt;
|
||||
Yaw += keyboardTurnRate * dt;
|
||||
Yaw -= input.MouseDeltaX * MouseTurnSensitivity;
|
||||
// Wrap yaw to [-PI, PI] so it doesn't grow unbounded.
|
||||
while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue