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>
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:
references/ACE/Source/ACE.Server/WorldObjects/Player.cs:1148— the PK Lite entry collision bump.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 error0x507whenACCWeenieObject::IsPlayerKiller@0x0058C910is true (PK bit0x20OR PKLite bit0x2000000), otherwise callsCM_Character::Event_EnterPKLite@0x006A13F0(line 680071) — a 12-byte parameterless game action, opcode0x28F, 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:
+Acdreamseen 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_headingat0x00454068). - Exactly one outbound
AutonomousPositionfor the correction.
The two named behaviour changes:
-
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.
-
The ForcePosition route no longer re-arms the constraint leash. Retail's force branch returns at
0x0045409D, ahead of all threeConstrainTocall sites (0x00454272,0x0045418A,0x004541EC); our oldBlipPositionre-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.