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>
135 lines
6.6 KiB
Markdown
135 lines
6.6 KiB
Markdown
# 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.
|