acdream/docs/research/2026-08-03-c4-route-2-visual-gate.md
Erik 9966b53174 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>
2026-08-03 18:46:36 +02:00

6.6 KiB

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.