feat(physics): C4 route 2 — ForcePosition through the canonical placement
A local-player ForcePosition had TWO independent writers for one accepted
packet: LocalForcePositionTransaction snapped the physics body
(PlayerMovementController.BlipPosition, a raw SnapToCell with no collision
resolve), while LiveEntityNetworkUpdateController's generic tail separately
wrote position/cell/rotation to the render WorldEntity from the raw wire and
rebucketed it. Two stores, one packet — the divergence class 670f307c fixed on
the remote path. The outbound AutonomousPosition ack also fired BEFORE any
canonical commit existed: we told ACE "got it, I'm here" before deciding where
"here" was, and the trailing isCurrent() could only suppress the continuation,
never recall the packet.
RuntimeAcceptedPositionDriveController is now the one Runtime-owned seam. Both
hosts call the identical TryExecuteAcceptedLocalPosition; App and headless
project the committed result through the existing placement projection sink
(LiveEntityRuntime.TryApplyRuntimePlacementPlace already performed the same
four writes, from committed state rather than a wire guess).
Retail: SmartBox::HandleReceivedPosition @0x00453FD0's FORCE_POSITION branch is
get_heading -> Frame::set_heading -> SmartBox::BlipPlayer @0x00453940 -> stamp
POSITION_TS -> SendPositionEvent @0x00454091 -> return @0x0045409D. BlipPlayer
is CPhysicsObj::SetPositionSimple @0x005162B0 with flags 0x1012
(Teleport|Slide|SendPositionEvent) — a real collision-resolving SetPosition,
not a snap. The pinned classifier already encoded this exactly.
Named behaviour changes:
* The ack is now an OUTPUT of the committed route, fired strictly after the
canonical commit and exactly once per accepted force packet.
* The ForcePosition route no longer re-arms the constraint leash. The force
branch returns at 0x0045409D, ahead of all three ConstrainTo sites
(0x00454272, 0x0045418A, 0x004541EC); the old re-arm cited retail's "Player,
normal" branch, which BlipPlayer is not on. The teleport, CommitPreparedPosition
and first-entry callers legitimately still constrain and are untouched.
* A force correction that terminates WITHOUT committing still sends its
position event and is not retried — retail's BlipPlayer discards
SetPositionSimple's SetPositionError return and acks unconditionally.
A single _pending funnel owns the in-flight placement, deciding on the token's
PositionAuthorityVersion against the record's: equal -> clear; advanced with the
newest accepted event still a force -> re-issue, re-classified; advanced to an
ordinary Apply -> clear, since newer server truth owns that pose. This closes a
double-apply/double-ack and a silently-dropped correction that two earlier
iterations of this slice each introduced.
AD-62 records the residual: a ForcePosition our async collision publication
cannot carry to a committed placement is not re-applied. Retail has no park —
its world is fully resident and its placement synchronous — so the state is
unreachable there. AP-131 is NOT retired; its legacy Position caller is route 4.
Deleted: LocalForcePositionTransaction, PlayerMovementController.BlipPosition,
HeadlessSessionWorldProjection.BlipLocalPlayer.
Gates: complete Release solution 10,858 passed / 4 skipped / 0 failed (baseline
10,844/4/0). Two independent Opus reviews (retail-conformance and
architecture/adversarial) PASS on the final diff after three FAIL rounds; every
intermediate state was fully green, so the suite caught none of the four real
defects. Connected acceptance is NOT run: nothing a user can do makes ACE emit
a ForcePosition without retail's @pklite, which acdream does not implement — see
docs/research/2026-08-03-c4-route-2-visual-gate.md.
Known gap, recorded not claimed: the plan's acceptance item 2 is unmet. The App
double-write check is a source pin, and "the committed projection moves the
render entity" is uncovered at any layer (#292). Filed alongside: #286-#291,
#293-#296.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
22a5c95400
commit
9966b53174
25 changed files with 4292 additions and 195 deletions
135
docs/research/2026-08-03-c4-route-2-visual-gate.md
Normal file
135
docs/research/2026-08-03-c4-route-2-visual-gate.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# C4 route 2 — ForcePosition: connected visual gate (2026-08-03)
|
||||
|
||||
The user-facing acceptance test for route 2. Route 2 is code-complete and
|
||||
suite-green before this runs; this document is the hand-off.
|
||||
|
||||
## Why the obvious recipe does NOT work
|
||||
|
||||
The route-2 contract's acceptance line says *"ACE `@teleport`-style
|
||||
displacement"*. That is wrong about which route it exercises, and following it
|
||||
would have produced a false pass.
|
||||
|
||||
Verified in ACE this session: `PositionPack`'s constructor
|
||||
(`references/ACE/Source/ACE.Server/Network/Structure/PositionPack.cs:36-57`)
|
||||
advances `ObjectTeleport` when `adminMove == true` (lines 49-52) and only ever
|
||||
*reads* `ObjectForcePosition` (line 54). Every admin move command —
|
||||
`@teleto`, `@teletome`, `@teleloc`, `@movetome` — routes through
|
||||
`Player_Location.cs:654 Teleport()` / `SendUpdatePosition(true)` and therefore
|
||||
advances **TELEPORT_TS, not FORCE_POSITION_TS**. Those commands exercise
|
||||
**route 3**, not route 2.
|
||||
|
||||
`SequenceType.ObjectForcePosition` is advanced at exactly **two** places in the
|
||||
whole ACE tree:
|
||||
|
||||
1. `references/ACE/Source/ACE.Server/WorldObjects/Player.cs:1148` — the PK Lite
|
||||
entry collision bump.
|
||||
2. `references/ACE/Source/ACE.Server/WorldObjects/Player_Tick.cs:488` — the
|
||||
anti-cheat z-position rubber-band.
|
||||
|
||||
(2) requires the server to believe you are fly-hacking — same landblock, a
|
||||
claimed Z more than 10 units above your last ground contact, more than a second
|
||||
after your last jump, Jump skill under 1000, and still flagged airborne. It is
|
||||
not reachable by legitimate play and there is no command path into it. Do not
|
||||
build the gate on it.
|
||||
|
||||
So (1) is the gate.
|
||||
|
||||
## The only reliable lever is deferred — what that means
|
||||
|
||||
The reachable trigger is the PK Lite entry-collision bump, which requires
|
||||
retail's `@pklite`. That is a **client** command, not a server one — ACE has no
|
||||
`pklite` text-command handler, so typing `@pklite` into chat today forwards as
|
||||
inert text. Retail's client turns it into a game action instead:
|
||||
|
||||
- `ClientCommunicationSystem::DoPKLite` @`0x0057A490`
|
||||
(`acclient_2013_pseudo_c.txt:390106`) — rejects with error `0x507` when
|
||||
`ACCWeenieObject::IsPlayerKiller` @`0x0058C910` is true (PK bit `0x20` OR
|
||||
PKLite bit `0x2000000`), otherwise calls
|
||||
- `CM_Character::Event_EnterPKLite` @`0x006A13F0` (line 680071) — a 12-byte
|
||||
parameterless game action, opcode `0x28F`, no payload.
|
||||
|
||||
acdream does not implement it, and **the user deferred implementing it
|
||||
(2026-08-03).** Scoping is preserved in this session's research so it can be
|
||||
picked up cheaply: ~40 lines of production code across `ClientCommandId`,
|
||||
`RetailClientCommandCatalog`, `ClientCommandRequests.BuildParameterless`,
|
||||
`WorldSession`, `ClientCommandController`, `LiveSessionCommandRouter`, and
|
||||
`LiveSessionRuntimeFactory` — every pattern already exists, including
|
||||
`WeenieError 0x0507`.
|
||||
|
||||
**Consequence, stated plainly: route 2's own behaviour is NOT visually
|
||||
verifiable in this campaign.** Nothing a user can do makes ACE emit a
|
||||
ForcePosition. Route 2's acceptance therefore rests on its automated Runtime /
|
||||
App / Headless tests and the complete Release suite. The connected pass below
|
||||
is a **regression check on the blast radius**, not acceptance of the new route.
|
||||
Do not record it as the latter.
|
||||
|
||||
## What the connected pass actually covers
|
||||
|
||||
Launch acdream in **Release** with `ACDREAM_RETAIL_UI=1` against the local ACE
|
||||
at `127.0.0.1:9000`.
|
||||
|
||||
Route 2 deletes `PlayerMovementController.BlipPosition` and
|
||||
`HeadlessSessionWorldProjection.BlipLocalPlayer`, removes App's generic-tail
|
||||
double-write for the local player, and re-routes the local player's accepted
|
||||
placement through the canonical Runtime SetPosition transaction. So the things
|
||||
to confirm are that ordinary play is untouched:
|
||||
|
||||
- Ordinary running, turning, walk/run toggle. No tethering, no rubber-band, no
|
||||
drift against the server.
|
||||
- A jump and a landing.
|
||||
- Walking through a doorway into an interior and back out.
|
||||
- A portal recall, and a portal into a dungeon.
|
||||
- Two-client observation: `+Acdream` seen from the retail client moves smoothly
|
||||
and lands where acdream shows it.
|
||||
|
||||
Anything wrong there is route 2's fault even though route 2 did not intend to
|
||||
touch it.
|
||||
|
||||
## Optional cheap shot (may not fire)
|
||||
|
||||
The second ACE call site is the anti-cheat z-position rubber-band
|
||||
(`Player_Tick.cs:459-490`). It needs: same landblock as your last ground
|
||||
contact, a claimed Z more than 10 units above it, more than a second since your
|
||||
last jump, Jump skill under 1000, and the server still flagging you airborne.
|
||||
There is no command path into it and normal play cannot satisfy it (falling
|
||||
makes the Z delta negative, and walking up terrain keeps refreshing the ground
|
||||
position), but a `@teleloc` straight up ~15 units within the same landblock is
|
||||
a two-minute experiment with a definitive tell: ACE's console logs
|
||||
`z-pos hacking detected for +Acdream` at `Player_Tick.cs:486` immediately
|
||||
before it force-bumps you.
|
||||
|
||||
If that line appears, you have a genuine ForcePosition and the checks below
|
||||
apply. If it does not, the route is untested by observation — which is the
|
||||
expected outcome.
|
||||
|
||||
**If a ForcePosition does occur, must be true:**
|
||||
- You end up at the corrected position and stay there. No visible double-apply,
|
||||
no snap-then-yank-back over one or two frames.
|
||||
- **Your facing does not change.** Retail's force branch replaces the
|
||||
destination heading with your current heading before placing
|
||||
(`HandleReceivedPosition` @`0x00453FD0`, `Frame::set_heading` at
|
||||
`0x00454068`).
|
||||
- Exactly **one** outbound `AutonomousPosition` for the correction.
|
||||
|
||||
**The two named behaviour changes:**
|
||||
|
||||
1. **The ack now fires after the canonical commit, not before it.** Previously
|
||||
the client told ACE "got it, I'm here" before deciding where "here" was.
|
||||
Symptom of a regression: ACE re-sending corrections, or a visible fight
|
||||
between client and server position after the bump.
|
||||
|
||||
2. **The ForcePosition route no longer re-arms the constraint leash.** Retail's
|
||||
force branch returns at `0x0045409D`, ahead of all three `ConstrainTo` call
|
||||
sites (`0x00454272`, `0x0045418A`, `0x004541EC`); our old `BlipPosition`
|
||||
re-armed the leash citing a branch it was not on. #167 was a leash bug, so
|
||||
this is the change most worth your eyes if you get a bump. Symptom of a
|
||||
problem: after the bump, movement feels tethered, rubber-bands back toward
|
||||
the pre-bump spot, or the leash trips on ordinary running shortly after.
|
||||
|
||||
## Known, deliberately unchanged
|
||||
|
||||
Retail's `SmartBox::PlayerPositionUpdated` (@`0x00453870`) gates its
|
||||
`set_viewer` / `LScape::update_viewpoint` re-seat on
|
||||
`distance >= GetAutonomyBlipDistance` (`0x004538C0-0x004538E2`). We publish the
|
||||
render root unconditionally. Not changed in this slice; recorded here so the
|
||||
next reader does not rediscover it as a bug.
|
||||
Loading…
Add table
Add a link
Reference in a new issue