docs: scope C4 route 5 (projectile authoritative placement)
Route 5 is RuntimePositionEntityKind.Projectile + ProjectileAuthoritative, and the entity kind turns out to be behaviourally inert in the classifier — ClassifyAcceptedPosition has exactly one EntityKind test (LocalPlayer, :349); Projectile and Remote fall through identical code from :393, differing only in OperationKind. Route 5 is an execution/ownership consolidation, not a classification change. Roughly half already shipped: the Create half and the residence-window Position half are canonical and live. What remains is the post-residence accepted Position, short-circuited before the classifier at LiveEntityNetworkUpdateController.cs:1428-1448 — ~180 non-comment lines. Retail has no missile branch: HandleReceivedPosition @0x00453FD0 and MoveOrTeleport @0x00516330 route a projectile through the identical remote arm @0x0045414D, ConstrainTo @0x00454272 IS armed for projectiles, and for an in-flight missile with a cell and no teleport/contact retail does nothing (return 0 @0x0051636D). This forecloses the plausible "projectiles are special" implementation before anyone writes it. Records that route 5 must land AFTER 4b-2 — it widens RuntimeRemotePlacementDriveController.OwnsPlacement, which excludes ProjectileAuthoritative today, leaving the far and teleport/cell-less branches with no owner. Names the gate problem honestly: ACE never sends UpdatePosition for a missile (the one site is commented out at WorldObject_Tick.cs:333-334), so the Position half is unreachable in ordinary play and has no cheap live trigger. The gate covers the Create half and regressions; the four dispositions are test-gated. Corrects a prior inventory claim: CommitProjectileCell is not ad hoc, it routes into the shared CommitCanonicalCell (RuntimePhysicsState.cs:1376-1397). The bypass is SnapToCell plus the InWorld/shadow tail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d5bdc35598
commit
07e979393b
1 changed files with 604 additions and 0 deletions
604
docs/research/2026-08-04-c4-route-5-scoping.md
Normal file
604
docs/research/2026-08-04-c4-route-5-scoping.md
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
# C4 route 5 — projectile authoritative placement: scoping (2026-08-04)
|
||||
|
||||
Research only. Nothing implemented, nothing edited under `src/` or `tests/`.
|
||||
All reads are against the clean tree at `d5bdc355` (`git status --porcelain`
|
||||
empty at the time of reading; route 4b-2 was in flight in another agent's
|
||||
session and had not yet written).
|
||||
|
||||
**Headline: route 5 is much smaller than route 4, and roughly half of it has
|
||||
already shipped.** The Create half and the residence-window Position half are
|
||||
canonical today (C3b/C3c). What remains is one execution seam, three deletions,
|
||||
and two policy decisions that the campaign has not yet made. It should NOT
|
||||
split. But one of those policy decisions — the near-`Interpolate` branch — has
|
||||
no executable machinery in acdream at all, and if the contract does not settle
|
||||
it up front the implementer will reproduce route 4b-2's "deleted the only
|
||||
handler for a live branch" failure exactly.
|
||||
|
||||
---
|
||||
|
||||
## 1. What route 5 actually is
|
||||
|
||||
### 1.1 The classifier surface
|
||||
|
||||
`RuntimePositionEntityKind.Projectile`
|
||||
(`src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs:13`)
|
||||
is route 5's entity kind. It has exactly **one** behavioural effect anywhere in
|
||||
the classifier:
|
||||
|
||||
- `OperationKind` (`:559-570`) maps it to
|
||||
`RuntimeSetPositionOperationKind.ProjectileAuthoritative` (`:567-568`).
|
||||
|
||||
That is all. `ClassifyCreate` (`:205-273`) gives a Projectile the identical
|
||||
`SetPosition` + `InitialCreateFlags` route as any non-local entity — the only
|
||||
`EntityKind` test in that method is `is RuntimePositionEntityKind.LocalPlayer`
|
||||
at `:263-266`, for the teleport hook. `ClassifyAcceptedPosition` (`:308-473`)
|
||||
has a single `LocalPlayer` branch at `:349`; **Projectile and Remote fall
|
||||
through the same code from `:393` onward and are byte-for-byte identical in
|
||||
disposition, flags, `StopInterpolating`, and `ConstrainPhase`.** The existing
|
||||
test `RuntimeAuthoritativePositionRouteClassifierTests.cs:418-436`
|
||||
(`ProjectilePosition_UsesRemoteMoveOrTeleportClassification`) pins exactly
|
||||
that.
|
||||
|
||||
So the dispositions route 5 owns for an accepted Position are the same four
|
||||
route 4 owns, discriminated only by `OperationKind ==
|
||||
ProjectileAuthoritative`:
|
||||
|
||||
| Branch | Condition (classifier line) | Runs SetPosition? |
|
||||
|---|---|---|
|
||||
| `SetPosition` | `TeleportAdvanced \|\| CommittedCellId == 0` (`:399`) | yes |
|
||||
| `NoPositionOperation` | `!effectiveContact` (`:425`) | no |
|
||||
| `Interpolate` | contact, `PlayerDistance < 96 m` (`:454-459`) | no |
|
||||
| `SetPositionSimple` | contact, `PlayerDistance >= 96 m` (`:454-459`) | yes |
|
||||
|
||||
**Consequence worth stating plainly: route 5 is not a classification change at
|
||||
all.** Whether a projectile's accepted Position is tagged `Projectile` or
|
||||
`Remote`, the classifier returns the same route. Route 5 is an *execution and
|
||||
ownership* consolidation — which owner runs the route, and which ledger the
|
||||
operation lands in.
|
||||
|
||||
### 1.2 What is ALREADY canonical
|
||||
|
||||
**(a) The Create half — DONE, live in production.**
|
||||
`RuntimeInitialCreateResidenceState.Begin` decides the kind at `:591-595`:
|
||||
|
||||
```
|
||||
RuntimePositionEntityKind entityKind = isLocalPlayer
|
||||
? RuntimePositionEntityKind.LocalPlayer
|
||||
: (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0
|
||||
? RuntimePositionEntityKind.Projectile
|
||||
: RuntimePositionEntityKind.Remote;
|
||||
```
|
||||
|
||||
This is the **only** production producer of `RuntimePositionEntityKind.Projectile`
|
||||
in the tree (verified by grep across `src/` and `tests/`). The first-entry
|
||||
conductor accepts it explicitly:
|
||||
`RuntimeRemoteFirstEntryState.cs:311-313` admits `RemoteAuthoritative
|
||||
or ProjectileAuthoritative`. And App has already been cut over —
|
||||
`DatLiveEntityProjectionMaterializer.cs:848-865` deliberately **skips**
|
||||
`_projectiles.TryBind` while `initialResidenceActive`, with a comment naming
|
||||
the exact bug that forced it ("the conductor then correctly rejected the
|
||||
unexpected owner and the missile remained permanently cell-less"). `TryBind`
|
||||
is retried on the committed projection-visible edge
|
||||
(`ProjectileController.cs:870-897`), by which point the canonical body exists
|
||||
and has been placed by the conductor.
|
||||
|
||||
**(b) The residence-window Position half — DONE, dormant-but-wired.**
|
||||
`RuntimeInitialCreateContinuationExecutor.ApplyPositionAction:1892` recovers
|
||||
the kind from the lease's `OperationKind`
|
||||
(`EntityKindOf`, `:2571-2579`, which maps `ProjectileAuthoritative` →
|
||||
`Projectile` at `:2577-2578`) and classifies through the one shared request
|
||||
builder `RuntimeAcceptedPositionRouteRequests.Build`
|
||||
(`:1914-1928`). A Position arriving while a missile's initial residence is open
|
||||
is already fully canonical.
|
||||
|
||||
Note the executor's own recorded gap at `:2019-2026`: for
|
||||
`Interpolate`/`NoPositionOperation`/`AwaitFreshPosition` it emits a typed trace
|
||||
and returns — *"Binding to the live interpolation owner is cutover work."* That
|
||||
gap is shared with route 4 and is one half of the policy question in §5 below.
|
||||
|
||||
### 1.3 What is still a duplicate authority
|
||||
|
||||
**The post-residence (steady-state) accepted Position for a live projectile.**
|
||||
Entry point:
|
||||
|
||||
- `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:1428-1448` —
|
||||
inside `OnPosition`, positioned AFTER the remote-teleport hook (`:1417-1426`)
|
||||
and BEFORE route 4a's classification (`:1473-1481`). It calls
|
||||
`_projectileController.ApplyAuthoritativePosition(...)` and **returns when it
|
||||
returns `true`**, so a projectile never reaches the classifier at all today.
|
||||
- `src/AcDream.App/Physics/ProjectileController.cs:492-590` —
|
||||
`ApplyAuthoritativePosition` (two overloads + doc comment, 99 lines).
|
||||
Validates, then delegates.
|
||||
- `src/AcDream.Runtime/Physics/RuntimeProjectilePhysicsUpdater.cs:301-424` —
|
||||
the actual authority (124 lines): `body.Orientation = orientation` (`:345`),
|
||||
`body.SnapToCell(fullCellId, worldPosition, cellLocalPosition)` (`:346`),
|
||||
`body.State = record.FinalPhysicsState` (`:347`), a velocity commit
|
||||
(`:348-358`), `CommitProjectileCell` (`:370-375`), the presentation
|
||||
acknowledgement (`:387`), and the `InWorld`/`Activate`/shadow-sync tail
|
||||
(`:390-422`).
|
||||
|
||||
`CommitProjectileCell` itself is **not** a bypass — `RuntimePhysicsState.cs:1376-1397`
|
||||
routes it into the shared `CommitCanonicalCell` (`:2138-2160`), the same
|
||||
canonical-cell writer the remote path uses. The 2026-08-02 inventory called it
|
||||
"ad hoc"; that is wrong and should be corrected in the contract. The bypass is
|
||||
`body.SnapToCell` + the `InWorld`/shadow tail running outside the
|
||||
`RuntimeSetPositionState` transaction, not the cell commit.
|
||||
|
||||
### 1.4 Adjacent, NOT route 5
|
||||
|
||||
Per the inventory and confirmed by reading:
|
||||
|
||||
- **Per-quantum integration stays out of `SetPosition`.**
|
||||
`RuntimeProjectilePhysicsUpdater.TryBegin`/`Complete` (`:37-205`), driven by
|
||||
`ProjectileController.Tick` (`:640-766`) and `LiveEntityAnimationScheduler`
|
||||
(`:403-426`). `Complete`'s `SnapToCell` at `:145-152` plus `CommitProjectileCell`
|
||||
at `:158-163` is the simulation commit, not an authoritative placement.
|
||||
The 1,880 B/op → 944 B/op placement budget (C2, plan `:155-170`) is per
|
||||
*operation*; projectile integration runs per object quantum on every missile
|
||||
in flight. **Do not touch it.**
|
||||
- **`ApplyAuthoritativeVector` (`RuntimeProjectilePhysicsUpdater.cs:207-250`)
|
||||
and `ApplyAuthoritativeState` (`:252-299`)** are not placement authorities.
|
||||
Vector already goes through `_physics.TryCommitAuthoritativeVector` (`:242`).
|
||||
State's one pose write is `body.SnapToCell(record.FullCellId, body.Position,
|
||||
...)` at `:286-296` — a re-normalize of the frame into the *already canonical*
|
||||
cell on the Missile-bit rising edge, not a move. The inventory's "reduce to
|
||||
acknowledge-only" framing overstates what is there.
|
||||
|
||||
---
|
||||
|
||||
## 2. Retail truth
|
||||
|
||||
Verify each yourself; every anchor below was read in
|
||||
`docs/research/named-retail/acclient_2013_pseudo_c.txt` during this pass.
|
||||
|
||||
### 2.1 There is no projectile branch. At all.
|
||||
|
||||
`SmartBox::UnpackPositionEvent` @0x004542C0 resolves the object from
|
||||
`CObjectMaint` (@0x004542F2) and calls `HandleReceivedPosition` (@0x00454358)
|
||||
with no state/kind test.
|
||||
|
||||
`SmartBox::HandleReceivedPosition` @0x00453FD0:
|
||||
- FORCE_POSITION early return @0x0045400C requires `arg2 == this->player`.
|
||||
- `unset_parent` @0x00454129 unconditional; `SetPlacementFrame` @0x00454142
|
||||
gated on `!HasAnims`.
|
||||
- `if (arg2 != this->player)` @0x0045414D → `MoveOrTeleport` @0x00454254 →
|
||||
**`ConstrainTo` @0x00454272 inside `if (MoveOrTeleport(...) != 0)`, anchored
|
||||
to `&arg2->m_position` read live (post-move)** → `return`.
|
||||
|
||||
A missile is not `this->player`, so **a projectile takes the identical remote
|
||||
arm.** No `state & Missile` test exists anywhere in this function.
|
||||
|
||||
`CPhysicsObj::MoveOrTeleport` @0x00516330:
|
||||
```
|
||||
if (!newer_event(POSITION_TS-ish gate)) return 0; // @0x00516364/@0x0051636D
|
||||
if (newer_event(TELEPORT_TS, arg3) || this->cell == 0) { // @0x00516386
|
||||
teleport_hook @0x005163EF; SetFlags(0x1012) @0x00516414;
|
||||
SetPosition @0x00516420; return 1; // @0x00516438
|
||||
}
|
||||
if (arg4 != 0) { // @0x0051638E — the contact bit
|
||||
if (player_distance < 96f) // @0x00516390-@0x00516399
|
||||
{ InterpolateTo(this, arg2, IsMovingTo(this)) @0x005163AF; return 1; }
|
||||
if (position_manager) StopInterpolating @0x005163CB;
|
||||
SetPositionSimple(this, arg2, 1) @0x005163D9; return 1; // @0x005163E8
|
||||
}
|
||||
return 0; // @0x0051636D
|
||||
```
|
||||
`SetPositionSimple` @0x005162B0 with `arg3 != 0` builds `0x1012`
|
||||
(`Teleport|Slide|SendPositionEvent`, @0x005162C4) — the classifier's
|
||||
`AuthoritativeTeleportFlags`.
|
||||
|
||||
**Answers to the specific questions asked:**
|
||||
|
||||
- *Does a projectile's authoritative Position take the same `MoveOrTeleport`
|
||||
path as a remote?* **Yes, identically.** Same function, same branches, no
|
||||
discriminator.
|
||||
- *What flags?* Teleport/cell-less → `0x1012` via the explicit `SetFlags`
|
||||
@0x00516414. Far snap → `0x1012` via `SetPositionSimple(…, 1)` @0x005162C4.
|
||||
Near and airborne → no `SetPosition` at all.
|
||||
- *Is `ConstrainTo` armed for projectiles?* **Yes** — @0x00454272, on any
|
||||
nonzero `MoveOrTeleport` return, with no kind test. It is NOT armed on the
|
||||
airborne no-op (return 0 @0x0051636D). `CPhysicsObj::ConstrainTo` @0x00510520
|
||||
calls `MakePositionManager` first, so retail *creates* the manager on demand
|
||||
for a missile.
|
||||
- *Is `StopInterpolating` called?* **Only on the far branch**, and only if a
|
||||
`position_manager` already exists (@0x005163C9-@0x005163CB). A missile that
|
||||
has never been near-interpolated has none, so the call is skipped.
|
||||
|
||||
### 2.2 `player_distance` is maintained for missiles
|
||||
|
||||
`CPhysicsObj::update_object` @0x00515D10 computes `player_vector` from
|
||||
`Position::get_offset` @0x00515D5B and stores `player_distance` @0x00515D95 for
|
||||
every active, unparented, in-cell object — which includes every in-flight
|
||||
missile. So retail's near/far test is meaningful for projectiles.
|
||||
|
||||
(BN artifact note: the decomp shows `player_distance` taking `player_vector.x`
|
||||
@0x00515D7B-@0x00515D95. That is the standard x87-elision artifact for a vector
|
||||
magnitude; acdream's Euclidean `PlayerDistance` is the right reading. Flagging
|
||||
it so nobody "fixes" it to `.x`.)
|
||||
|
||||
### 2.3 `MoveOrTeleport` discards the velocity argument
|
||||
|
||||
`MoveOrTeleport(this, arg2, arg3, arg4, arg5)` declares
|
||||
`arg5 = AC1Legacy::Vector3 const*` and **never references it** in the
|
||||
decompiled body @0x00516330-@0x00516438. `HandleReceivedPosition` threads
|
||||
`arg6` into it @0x00454254 and does nothing else with it. Retail's velocity
|
||||
authority is `set_velocity` via VectorUpdate, not the Position event.
|
||||
|
||||
*Confidence:* the parameter is textually unreferenced. I did not byte-verify
|
||||
against `refs/acclient.exe` that BN did not elide a use. Treat as **strong but
|
||||
not byte-confirmed**; it matters only because acdream currently *does* commit a
|
||||
velocity here (§3, item 4).
|
||||
|
||||
### 2.4 What retail actually does to an in-flight missile — mostly nothing
|
||||
|
||||
Chain the three facts: an in-flight missile has a non-null cell, no advancing
|
||||
TELEPORT_TS, and no ground contact. `arg4 == 0` → `return 0` @0x0051636D.
|
||||
**Nothing is written, and `ConstrainTo` is skipped.**
|
||||
|
||||
The contact bit's provenance is ACE `PositionPack.BuildFlags`
|
||||
(`references/ACE/Source/ACE.Server/Network/Structure/PositionPack.cs:72-73`):
|
||||
`IsGrounded` is set from `TransientState & OnWalkable`. A flying missile is not
|
||||
`OnWalkable`.
|
||||
|
||||
*Not established:* I did not observe a live missile Position packet, because
|
||||
(see §6) ACE does not send one. The claim "an in-flight missile's Position
|
||||
packet would carry `IsGrounded == false`" is an inference from ACE's flag
|
||||
definition, not a measurement.
|
||||
|
||||
---
|
||||
|
||||
## 3. The duplicate authority to delete
|
||||
|
||||
Exact deletion targets, with the fabricated values called out.
|
||||
|
||||
| # | Site | Lines | What it is |
|
||||
|---|---|---|---|
|
||||
| 1 | `src/AcDream.Runtime/Physics/RuntimeProjectilePhysicsUpdater.cs:301-424` | 124 | The real authority: `SnapToCell` `:346`, velocity commit `:348-358`, `CommitProjectileCell` `:370-375`, `InWorld`/`Activate`/shadow tail `:390-422` |
|
||||
| 2 | `src/AcDream.App/Physics/ProjectileController.cs:492-590` | 99 | Two `ApplyAuthoritativePosition` overloads + doc; validation, currency closures, render-pose projection `:583-586` |
|
||||
| 3 | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:1428-1448` | 21 | The early-return call site that keeps projectiles out of the classifier |
|
||||
|
||||
Raw total **244 lines**; roughly **180 non-comment**.
|
||||
|
||||
**Fabricated values on those paths:**
|
||||
|
||||
- `LiveEntityNetworkUpdateController.cs:1442-1443`:
|
||||
`acceptedSpawn.Physics?.Velocity ?? System.Numerics.Vector3.Zero`. This is
|
||||
the exact `?? Vector3.Zero` shape route 4b-2's contract calls out, and it is
|
||||
worse than an inert default: it is passed into
|
||||
`RuntimeProjectilePhysicsUpdater.cs:348-358`, which commits it as the body's
|
||||
authoritative velocity whenever `VelocityAuthorityVersion` matches. A Position
|
||||
packet with no `PhysicsDesc.Velocity` therefore **zeroes an in-flight
|
||||
missile's velocity**. Retail's `MoveOrTeleport` does not consume its velocity
|
||||
argument at all (§2.3).
|
||||
- No duplicated distance/threshold constants exist on the projectile path —
|
||||
`MaxPhysicsDistance = 96f` and `BodySnapThreshold = 4f` live only in the
|
||||
classifier (`:194`) and route 4a's seam
|
||||
(`RuntimeRemoteSteadyStatePosition.cs:43`) plus the surviving 4b legacy far
|
||||
halves. Route 5 introduces no new copy and must not.
|
||||
|
||||
**Not a deletion target, despite appearances:**
|
||||
|
||||
- `ProjectileController.CanAcceptPositionPayload` (`:102-126`). It is called
|
||||
unconditionally at `LiveEntityNetworkUpdateController.cs:1205-1208` for
|
||||
**every** entity (it returns `true` for non-projectiles via `!= false`), so it
|
||||
is the shared admission validator, not a projectile authority. It also does
|
||||
**not** subsume item 1's inner validation: it checks `update.Position` and
|
||||
`update.Velocity`, while `ApplyAuthoritativePosition` additionally checks the
|
||||
origin-translated `worldPosition` and a *different* velocity
|
||||
(`acceptedSpawn.Physics?.Velocity`).
|
||||
- `ProjectileController.TryBind`'s create branch (`:222-267`) and its
|
||||
`entity.SetPosition`/`ParentCellId`/`RebucketLiveEntity` tail (`:269-291`).
|
||||
This *is* a placement authority — `GetOrCreatePhysicsBody` with a
|
||||
`SnapToCell` seeded straight from the CreateObject wire frame
|
||||
(`:258-264`) — but it is now the **fallback** for the paths the residence
|
||||
conductor does not own: `initialResidenceActive == false`
|
||||
(`DatLiveEntityProjectionMaterializer.cs:857`) and the late-classification
|
||||
route `ApplyAuthoritativeState` → `TryBind` (`ProjectileController.cs:488`),
|
||||
where an ordinary remote gains the Missile bit mid-life. Deleting it belongs
|
||||
to route 5 only if route 5 first proves those two paths unreachable or
|
||||
supplies a canonical replacement. **My recommendation: leave it, record it,
|
||||
and let C5's deletion pass own it** — see §7 trap T8.
|
||||
|
||||
---
|
||||
|
||||
## 4. Prediction / correction
|
||||
|
||||
This is where the subtle regression lives, and it is a real one.
|
||||
|
||||
The projectile prediction model (J5.6):
|
||||
`RuntimeProjectile.PredictionAuthorityVersion` (`RuntimeProjectile.cs:36-39`)
|
||||
is bumped by `InvalidatePrediction()`. It is bumped in exactly three places,
|
||||
all in `RuntimeProjectilePhysicsUpdater`: `ApplyAuthoritativeVector:241`,
|
||||
`ApplyAuthoritativeState:272`, and **`ApplyAuthoritativePosition:342`**.
|
||||
|
||||
That version is the **sole** cancellation mechanism for an in-flight split
|
||||
quantum:
|
||||
|
||||
- `TryBegin` captures it into the commit (`:86-87`).
|
||||
- `Complete` re-checks it via `IsSpatialCurrent`/`IsIdentityCurrent`
|
||||
(`:114-122`, `:131-140`, `:164-174`, `:200-204`, `:440-450`).
|
||||
- App re-checks it too, in `ProjectileController.IsCurrentQuantumIdentity`
|
||||
(`:850-859`).
|
||||
|
||||
And the quantum is genuinely split across other work:
|
||||
`LiveEntityAnimationScheduler.cs:403-426` calls `TryBeginQuantum` at `:404-408`,
|
||||
runs `_animationHooks.Capture` at `:411`, then `CompleteQuantum` at `:418-422`.
|
||||
`ProjectileController.AdvanceQuantum` (`:773-782`) is the fused variant for
|
||||
un-animated missiles.
|
||||
|
||||
**The trap:** if route 5 replaces `ApplyAuthoritativePosition` with a
|
||||
`RuntimeSetPositionState` route and does not invalidate the projectile's
|
||||
prediction version at the same point, then (a) an already-begun quantum's
|
||||
`Complete` will no longer abort, and will write the pre-correction integrated
|
||||
pose over the freshly committed placement, and (b) the version stops advancing
|
||||
on this channel, weakening the currency checks that the Vector and State
|
||||
channels still rely on. The fix is trivial once seen — the contract must
|
||||
require the replacement to invalidate prediction at the *same* point in the
|
||||
sequence retail's `SetPosition` would clobber the body — but it is invisible if
|
||||
you only read the classifier.
|
||||
|
||||
*Not established:* whether the network drain can actually land between `:408`
|
||||
and `:418` in a single frame (both are on the update thread; the wire pump is a
|
||||
separate frame phase, and `_animationHooks.Capture` can dispatch effect
|
||||
callbacks). I could not settle re-entrancy by reading alone. The mechanism —
|
||||
`InvalidatePrediction` being the only cancel — is established regardless, and
|
||||
that is enough to make the requirement binding.
|
||||
|
||||
**Second prediction interaction:** retail's near branch is
|
||||
`InterpolateTo(this, arg2, IsMovingTo(this))` @0x005163AF, and
|
||||
`CPhysicsObj::IsMovingTo` @0x0050EB10 returns nonzero only when a
|
||||
`MovementManager` exists and is moving-to. A missile has no MovementManager, so
|
||||
retail passes 0. Any acdream near-branch policy that wants to be retail-shaped
|
||||
must pass `isMovingTo: false` for projectiles.
|
||||
|
||||
---
|
||||
|
||||
## 5. The two policy decisions the contract must make
|
||||
|
||||
Both are of the shape the 4b-2 contract names: *"deleting the legacy block
|
||||
removes the only handler for a live classification."* Neither can be deferred
|
||||
into a code comment.
|
||||
|
||||
### 5.1 `Interpolate` (near, in contact) has NO executable machinery
|
||||
|
||||
Route 4a's seam is `RuntimeRemoteSteadyStatePosition.ApplyInterpolate`
|
||||
(`:113-149`) and it takes a `RemoteMotion` — it drives `remote.Interp`
|
||||
(the interpolation queue) and `remote.Body`. `TryArmConstraintAfterOperation`
|
||||
(`:165-180`) requires `remote.Host` to reach `host.PositionManager.ConstrainTo`.
|
||||
|
||||
A projectile has **none of that**. `RuntimeProjectile` (`RuntimeProjectile.cs:17-40`)
|
||||
is `{ Body, CollisionSphere, PredictionAuthorityVersion }`. No
|
||||
`EntityPhysicsHost`, no `PositionManager`, no `InterpolationManager`, no
|
||||
`ConstraintManager`. A pure missile also never acquires a `RemoteMotion` —
|
||||
that is created on the 0xF74C motion path, and
|
||||
`ProjectileController.HandlesMovement` (`:599-602`) plus
|
||||
`LiveEntityAnimationScheduler.cs:256-257,336` keep the two owners disjoint
|
||||
while the Missile bit is set. (Coexistence *is* representable —
|
||||
`ProjectileController.Tick:756` handles `record.RemoteMotionRuntime is
|
||||
RemoteMotion` — but it is the adopted-body case, not the ordinary arrow/bolt.)
|
||||
|
||||
So the contract must choose, explicitly:
|
||||
|
||||
1. **Build retail's `PositionManager` chain for projectiles.** Retail-exact
|
||||
(`ConstrainTo` @0x00510520 does `MakePositionManager` on demand). Also the
|
||||
only way to be exact about `StopInterpolating` @0x005163CB and the leash
|
||||
@0x00454272. Large: a new interpolation/constraint owner for a body type
|
||||
that has none, plus its interaction with the per-quantum stepper. **If this
|
||||
is chosen, it is route 5b and route 5 splits.**
|
||||
2. **State an acdream divergence: a projectile's near-`Interpolate` and its
|
||||
`ConstrainTo` are no-ops**, with a register row citing @0x005163AF and
|
||||
@0x00454272 and the reason (no PositionManager exists for a ballistic body;
|
||||
the classifier's own `ConstrainPhase` is honoured for the placement branches
|
||||
only). Cheap, honest, and — given §6 — has no observable production effect.
|
||||
|
||||
**My recommendation is (2)**, with the row filed in the same commit. Choosing
|
||||
(1) for a branch that ACE never triggers would be building machinery for a
|
||||
packet that does not exist.
|
||||
|
||||
### 5.2 The placement dispositions have no owner
|
||||
|
||||
`RuntimeRemotePlacementDriveController.OwnsPlacement`
|
||||
(`src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:274-278`)
|
||||
requires:
|
||||
|
||||
```
|
||||
route.OperationKind is RuntimeSetPositionOperationKind.RemoteAuthoritative
|
||||
&& route.Disposition is SetPosition or SetPositionSimple
|
||||
&& (route.SetPositionFlags & PhysicsSetPositionFlags.Teleport) != 0
|
||||
```
|
||||
|
||||
`ProjectileAuthoritative` is **excluded**. So the moment route 5 stops
|
||||
short-circuiting at `LiveEntityNetworkUpdateController.cs:1432`, the
|
||||
`SetPosition` (teleport / cell-less) and `SetPositionSimple` (far) branches for
|
||||
a projectile have *no handler at all*.
|
||||
|
||||
The controller is otherwise entirely kind-agnostic: the service window, the
|
||||
per-entity `_pending` map, the `_awaitingAcknowledgement` ledger, the
|
||||
refuse-not-park decision, and `DetachRoute`'s cancellation all read only the
|
||||
record and the token. Widening the first clause to
|
||||
`is RemoteAuthoritative or ProjectileAuthoritative` (plus its class/method doc)
|
||||
is ~10 lines and is the right move. Do **not** clone a sibling controller.
|
||||
|
||||
**Sequencing constraint:** that file is route 4b-2's active edit surface, and
|
||||
route 4b-3 will touch it again. Route 5 must land *after* 4b-2, and its
|
||||
contract should say so.
|
||||
|
||||
---
|
||||
|
||||
## 6. The connected gate — and an honest problem with it
|
||||
|
||||
**ACE never sends an `UpdatePosition` for a missile.** The only physics-tick
|
||||
site that would is commented out:
|
||||
`references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:333-334`
|
||||
(`/*if (PhysicsObj.IsGrounded) SendUpdatePosition();*/`), inside the branch
|
||||
explicitly gated on `(PhysicsObj.State & PhysicsState.Missile) != 0` at `:265`.
|
||||
Every live `SendUpdatePosition` / `GameMessageUpdatePosition` caller in
|
||||
`ACE.Server` is a Player, Creature/Monster, Pet, GamePiece, inventory move, or
|
||||
an admin command (grepped exhaustively). The projectile classes broadcast
|
||||
`GameMessageVectorUpdate` (`SpellProjectile.cs:238`) and `GameMessageSetState`
|
||||
(`:229`) on impact — never a Position.
|
||||
|
||||
So **route 5's authoritative-Position half is unreachable in ordinary play
|
||||
against ACE**, and there is no cheap live trigger: the one admin path
|
||||
(`AdminCommands.cs:4772`, move-selected-object-to-me) requires selecting a
|
||||
projectile inside its ~5 s `ProjectileTimeout` lifetime.
|
||||
|
||||
The contract must say this rather than invent a gate. Concretely:
|
||||
|
||||
**What the user CAN observe (the Create half + no-regression):**
|
||||
1. In peace mode with a bow: fire an arrow at a target ~20 m away. The arrow
|
||||
must appear at the launch point immediately, fly a clean arc, and vanish on
|
||||
impact.
|
||||
2. Cast a Force Bolt / Flame Bolt at a target across a landblock boundary
|
||||
(stand near a seam, target something on the other side). The bolt must not
|
||||
freeze, teleport, or vanish at the seam.
|
||||
3. Fire indoors, in a dungeon: the projectile must respect the EnvCell and
|
||||
impact on the wall rather than passing through.
|
||||
4. Fire at a target at bow max range (~80 m) and confirm the projectile does
|
||||
not stall or snap.
|
||||
5. After each impact, walk through the impact point. **An invisible collider
|
||||
there is the loud regression** — it means a projectile's shadow/cell state
|
||||
survived its retirement.
|
||||
|
||||
**Regression signatures:** projectile spawns at the origin or at the player's
|
||||
feet instead of the launch point; hangs motionless; disappears on frame 1;
|
||||
appears only after it has already travelled; leaves an invisible-but-solid
|
||||
body (the #184 signature); an arrow that visibly *slows or stops* mid-flight
|
||||
(that would be the §3 velocity-zeroing path, which route 5 removes — so if it
|
||||
is present today, it should stop).
|
||||
|
||||
**What the gate cannot cover:** the four accepted-Position dispositions. Those
|
||||
must be gated by focused Runtime + App tests, with the ACE citation above
|
||||
recorded as the reason. Do not let a green connected gate be presented as
|
||||
evidence for the Position half.
|
||||
|
||||
---
|
||||
|
||||
## 7. Traps
|
||||
|
||||
**T1 — the near-`Interpolate` branch loses its only handler.** §5.1. Highest
|
||||
risk. Today `ApplyAuthoritativePosition` handles *every* projectile Position
|
||||
shape by hard-correcting. Route 5 replaces it with four dispositions, two of
|
||||
which (`Interpolate`, `NoPositionOperation`) have no projectile executor. The
|
||||
airborne one is correct as a no-op (retail returns 0). The near one is not, and
|
||||
silently dropping it is the same failure as 4b-2's `null`/`Rejected*` trap.
|
||||
|
||||
**T2 — `OwnsPlacement` excludes `ProjectileAuthoritative`.** §5.2. The far and
|
||||
teleport/cell-less branches have no owner the instant the short-circuit is
|
||||
removed.
|
||||
|
||||
**T3 — prediction invalidation.** §4. `InvalidatePrediction()` is the only
|
||||
mechanism that aborts an in-flight split quantum; the SetPosition route does
|
||||
not call it.
|
||||
|
||||
**T4 — the entity kind is decided ONCE, at Create.**
|
||||
`RuntimeInitialCreateResidenceState.cs:591-595` reads
|
||||
`record.FinalPhysicsState & Missile` at Create time and freezes it into the
|
||||
lease's `OperationKind`. But the Missile bit changes at runtime: ACE clears it
|
||||
on impact (`SpellProjectile.cs:229` broadcasts the new state), and
|
||||
`ApplyAuthoritativeState` → `TryBind` (`ProjectileController.cs:473-488`) is the
|
||||
path where an ordinary object *becomes* a missile. Meanwhile
|
||||
`RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition:622` hardcodes
|
||||
`RuntimePositionEntityKind.Remote`. Route 5 must define one per-packet
|
||||
discriminator from the canonical `FinalPhysicsState`, and the remote arm and
|
||||
the projectile arm must be provably mutually exclusive on the same input —
|
||||
otherwise both or neither will own a packet at the moment the bit flips.
|
||||
Because the classifier is disposition-identical for the two kinds (§1.1), a
|
||||
mis-tag is *silent*: the route is right and only the ledger/owner is wrong.
|
||||
|
||||
**T5 — `ApplyAuthoritativePosition` returns `true` on an invalid payload.**
|
||||
`ProjectileController.cs:550-554` and `RuntimeProjectilePhysicsUpdater.cs:329-339`
|
||||
both `return true` — "handled, ignore" — so the packet does *not* fall through
|
||||
to the generic remote path. Their validation is **not** subsumed by
|
||||
`CanAcceptPositionPayload` (§3). Any replacement must preserve the swallow, or
|
||||
an invalid projectile packet starts being routed as an ordinary remote.
|
||||
|
||||
**T6 — carrying AP-87 onto a branch that does not need it.** Same shape as
|
||||
4b-2's warning. AP-87's `bodyToTarget > 4 m` / `!willBeDrTicked` guards
|
||||
(`RuntimeRemoteSteadyStatePosition.cs:43,130-138`) exist to stop an unplaced
|
||||
*remote* body being enqueued into an interpolation queue. A projectile has no
|
||||
queue. Do not copy them in. Equally: do not delete the near-branch copies —
|
||||
those are 4a's and still load-bearing.
|
||||
|
||||
**T7 — routing per-quantum integration through `SetPosition`.** The one
|
||||
explicit prohibition carried from the original route-5 requirement, and the
|
||||
allocation budget is the reason. `RuntimeProjectilePhysicsUpdater.Complete`
|
||||
(`:94-205`) runs per quantum per in-flight missile.
|
||||
|
||||
**T8 — deleting `TryBind`'s create branch as "obviously superseded".**
|
||||
`ProjectileController.cs:222-267` looks like dead legacy now that C3c owns
|
||||
Create, but it is still the live path for `initialResidenceActive == false`
|
||||
(`DatLiveEntityProjectionMaterializer.cs:857`) and for late Missile
|
||||
classification (`ProjectileController.cs:488`). Same class as the 4a review's
|
||||
R1 (a "duplicate" that turned out to be the only handler for a real case).
|
||||
|
||||
**T9 — a second snapshot store for the same state.** The `_pending` /
|
||||
`_awaitingAcknowledgement` maps in
|
||||
`RuntimeRemotePlacementDriveController` are keyed by `RuntimeEntityKey` and are
|
||||
already per-entity. Adding a projectile-specific pending map alongside them
|
||||
would recreate the two-stores-for-one-state shape the campaign has hit twice.
|
||||
Widen the existing controller; do not add a parallel one.
|
||||
|
||||
**T10 — the register.** No projectile placement row exists anywhere in
|
||||
`docs/architecture/retail-divergence-register.md` (grepped). Route 5 introduces
|
||||
at least one deviation (§5.1 option 2) and retires at least one (the velocity
|
||||
zeroing, §3). Both rows land in the same commit as the behaviour.
|
||||
|
||||
---
|
||||
|
||||
## 8. Line budget, and whether route 5 splits
|
||||
|
||||
Calibration: route 4a = 364 production lines; 4b-1 = 230 + a 57-line park fix;
|
||||
4b-2 budgeted at 350-500.
|
||||
|
||||
| Item | Non-comment production lines |
|
||||
|---|---|
|
||||
| Delete the three sites in §3 | ~180 removed |
|
||||
| Runtime projectile steady-state seam (airborne no-op + the §5.1 policy + prediction invalidation) | 60-100 |
|
||||
| Projectile classification entry (extend `ClassifyRemoteAcceptedPosition` with a kind parameter, or a sibling) | 10-35 |
|
||||
| Widen `OwnsPlacement` + doc | ~10 |
|
||||
| App call-site rewrite in `OnPosition` (classify → own → skip generic → project committed placement) | 40-70 |
|
||||
| **Total added** | **120-215** |
|
||||
|
||||
**Estimate: 250-400 non-comment production lines touched, of which 120-215 are
|
||||
new.** Test work is the larger share as usual — roughly 400-700 lines (Runtime
|
||||
seam tests per disposition, the prediction-invalidation test, a *behavioural*
|
||||
App test proving the generic path no longer runs for a projectile).
|
||||
|
||||
**Route 5 should NOT split — conditional on §5.1 resolving to option 2.**
|
||||
If the contract chooses to build a `PositionManager`/`InterpolationManager`
|
||||
chain for projectiles, that is a separate 5b landing of its own and the
|
||||
estimate above does not cover it.
|
||||
|
||||
**Sequencing: route 5 lands after 4b-2 and, preferably, after 4b-3**, because
|
||||
it edits `RuntimeRemotePlacementDriveController` and
|
||||
`LiveEntityNetworkUpdateController.OnPosition` — both of which 4b-2/4b-3 are
|
||||
rewriting heavily. A parallel route-5 landing would be exactly the
|
||||
"coupled plan slices in parallel" failure the project has already recorded.
|
||||
|
||||
---
|
||||
|
||||
## 9. Explicitly not established
|
||||
|
||||
1. **Whether the network pump can land a packet between
|
||||
`TryBeginQuantum` and `CompleteQuantum`** (§4). Would be settled by an
|
||||
`ACDREAM_PROBE_*` counter on a straddled `Complete` returning false, or by
|
||||
reading the frame graph's phase ordering end to end.
|
||||
2. **Whether retail's `MoveOrTeleport` genuinely ignores its velocity
|
||||
argument**, or BN elided a use (§2.3). Would be settled by
|
||||
`tools/pdb-extract` byte-decode of 0x00516330-0x00516438, or a cdb
|
||||
breakpoint at @0x00454254 dumping `arg6` and comparing the object's velocity
|
||||
before and after.
|
||||
3. **Whether an in-flight missile's Position packet would carry
|
||||
`IsGrounded == false`** (§2.4) — inferred from ACE's
|
||||
`PositionPack.cs:72-73`, never observed, because ACE does not emit the
|
||||
packet.
|
||||
4. **Whether `initialResidenceActive == false` is actually reachable for a
|
||||
Missile-state top-level Create in the graphical host** (§3, T8). It is
|
||||
plainly reachable in the content-less headless path
|
||||
(C3c: "content-less headless keeps pre-flip direct registration") and for
|
||||
Parented/PickedUp Creates. For a graphical top-level missile Create it
|
||||
depends on `RuntimeInitialCreateResidenceState.CanAcceptCreate` (`:559-573`)
|
||||
never refusing, which I did not prove.
|
||||
5. **Whether any retail server ever sent a projectile Position.** Only ACE was
|
||||
checked. Retail's client clearly supports it (§2.1); acdream targets ACE.
|
||||
Loading…
Add table
Add a link
Reference in a new issue