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>
31 KiB
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 toRuntimeSetPositionOperationKind.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— insideOnPosition, positioned AFTER the remote-teleport hook (:1417-1426) and BEFORE route 4a's classification (:1473-1481). It calls_projectileController.ApplyAuthoritativePosition(...)and returns when it returnstrue, 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 theInWorld/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 byProjectileController.Tick(:640-766) andLiveEntityAnimationScheduler(:403-426).Complete'sSnapToCellat:145-152plusCommitProjectileCellat:158-163is 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) andApplyAuthoritativeState(:252-299) are not placement authorities. Vector already goes through_physics.TryCommitAuthoritativeVector(:242). State's one pose write isbody.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 insideif (MoveOrTeleport(...) != 0), anchored to&arg2->m_positionread 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
MoveOrTeleportpath as a remote? Yes, identically. Same function, same branches, no discriminator. - What flags? Teleport/cell-less →
0x1012via the explicitSetFlags@0x00516414. Far snap →0x1012viaSetPositionSimple(…, 1)@0x005162C4. Near and airborne → noSetPositionat all. - Is
ConstrainToarmed for projectiles? Yes — @0x00454272, on any nonzeroMoveOrTeleportreturn, with no kind test. It is NOT armed on the airborne no-op (return 0 @0x0051636D).CPhysicsObj::ConstrainTo@0x00510520 callsMakePositionManagerfirst, so retail creates the manager on demand for a missile. - Is
StopInterpolatingcalled? Only on the far branch, and only if aposition_manageralready 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.Zeroshape route 4b-2's contract calls out, and it is worse than an inert default: it is passed intoRuntimeProjectilePhysicsUpdater.cs:348-358, which commits it as the body's authoritative velocity wheneverVelocityAuthorityVersionmatches. A Position packet with noPhysicsDesc.Velocitytherefore zeroes an in-flight missile's velocity. Retail'sMoveOrTeleportdoes not consume its velocity argument at all (§2.3).- No duplicated distance/threshold constants exist on the projectile path —
MaxPhysicsDistance = 96fandBodySnapThreshold = 4flive 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 atLiveEntityNetworkUpdateController.cs:1205-1208for every entity (it returnstruefor 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 checksupdate.Positionandupdate.Velocity, whileApplyAuthoritativePositionadditionally checks the origin-translatedworldPositionand a different velocity (acceptedSpawn.Physics?.Velocity).ProjectileController.TryBind's create branch (:222-267) and itsentity.SetPosition/ParentCellId/RebucketLiveEntitytail (:269-291). This is a placement authority —GetOrCreatePhysicsBodywith aSnapToCellseeded 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 routeApplyAuthoritativeState→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:
TryBegincaptures it into the commit (:86-87).Completere-checks it viaIsSpatialCurrent/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:
- Build retail's
PositionManagerchain for projectiles. Retail-exact (ConstrainTo@0x00510520 doesMakePositionManageron demand). Also the only way to be exact aboutStopInterpolating@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. - State an acdream divergence: a projectile's near-
Interpolateand itsConstrainToare no-ops, with a register row citing @0x005163AF and @0x00454272 and the reason (no PositionManager exists for a ballistic body; the classifier's ownConstrainPhaseis 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):
- 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.
- 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.
- Fire indoors, in a dungeon: the projectile must respect the EnvCell and impact on the wall rather than passing through.
- Fire at a target at bow max range (~80 m) and confirm the projectile does not stall or snap.
- 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
- Whether the network pump can land a packet between
TryBeginQuantumandCompleteQuantum(§4). Would be settled by anACDREAM_PROBE_*counter on a straddledCompletereturning false, or by reading the frame graph's phase ordering end to end. - Whether retail's
MoveOrTeleportgenuinely ignores its velocity argument, or BN elided a use (§2.3). Would be settled bytools/pdb-extractbyte-decode of 0x00516330-0x00516438, or a cdb breakpoint at @0x00454254 dumpingarg6and comparing the object's velocity before and after. - Whether an in-flight missile's Position packet would carry
IsGrounded == false(§2.4) — inferred from ACE'sPositionPack.cs:72-73, never observed, because ACE does not emit the packet. - Whether
initialResidenceActive == falseis 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 onRuntimeInitialCreateResidenceState.CanAcceptCreate(:559-573) never refusing, which I did not prove. - Whether any retail server ever sent a projectile Position. Only ACE was checked. Retail's client clearly supports it (§2.1); acdream targets ACE.