fix(physics): #307 — PreviousTeleport was always 0 on the live Position path
Shipped defect in route 2 (9966b531), found while reviewing route 4a.
`InboundPhysicsStateController.TryApplyPosition` built its AcceptedPhysicsTimestamps
via `Current(gate, teleportAdvanced: ...)`, omitting `previousTeleport`, which
defaulted to a literal 0. The only site that populated it was the deferred
initial-create path — which is why the continuation executor was correct and
every newer consumer was not.
Consequence in shipped code: route 2 feeds this into
`ValidAcceptedAuthority`, which requires Previous == Accepted for a
ForcePosition. Any local player whose TELEPORT_TS is nonzero — anyone who has
portalled or recalled this session — had the authority rejected and the force
correction SILENTLY DROPPED. The user's @pklite acceptance was genuine but
narrow: that character had not teleported, so the stamp was still 0.
Second latent consequence: with an accepted stamp >= 0x8000, wrap-safe
TeleportRegressed also fires against the 0 and rejects ordinary Apply positions,
not just ForcePosition.
The fix captures `previousTeleport = gate.TeleportTimestamp` BEFORE
`TryAcceptPositionEvent` mutates it, matching the shape the deferred path
already used. Ordering is the whole point: capturing after would make
Previous == Accepted unconditionally, so ValidAcceptedAuthority's check would
pass vacuously — the symptom would disappear while the semantics broke.
Also removes the footgun that allowed it. `Current`'s parameter is now
`ushort? previousTeleport = null` resolving to `gate.TeleportTimestamp`, so the
eleven non-Position channels — none of which can move TELEPORT_TS — get
"previous == current" by omission rather than a literal 0 that is
indistinguishable from a genuine "never teleported".
Consumer audit: only TryApplyPosition was defective. The two route-2 call sites
trace back to it; the RuntimeEntityObjectLifetime sites source from
TryAcceptDeferredPosition and were already correct.
Tests discrimination-verified by reverting the argument to 0: the stamp test
fails Expected 10 / Actual 0, and the classifier test fails Expected
SetPositionSimple / Actual RejectedAuthority — the shipped defect reproduced
exactly.
Gates: complete Release solution 10,935 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b633b10967
commit
19d9509497
3 changed files with 206 additions and 4 deletions
|
|
@ -624,8 +624,16 @@ public sealed class InboundPhysicsStateController
|
|||
return false;
|
||||
}
|
||||
|
||||
// #307: TryAcceptPositionEvent MUTATES gate.TeleportTimestamp, so the
|
||||
// pre-event value must be captured BEFORE the call - the exact shape
|
||||
// TryAcceptDeferredPosition already used. Reading it afterwards would
|
||||
// yield the accepted value and make Previous == Accepted vacuously
|
||||
// true; omitting it (the original defect) left it 0, which made every
|
||||
// downstream ValidAcceptedAuthority ForcePosition check fail for any
|
||||
// entity whose TELEPORT_TS had ever advanced.
|
||||
ushort previousTeleport = gate.TeleportTimestamp;
|
||||
bool advancesTeleport = PhysicsTimestampGate.IsNewer(
|
||||
gate.TeleportTimestamp,
|
||||
previousTeleport,
|
||||
update.TeleportSequence);
|
||||
disposition = gate.TryAcceptPositionEvent(
|
||||
update.InstanceSequence,
|
||||
|
|
@ -636,7 +644,8 @@ public sealed class InboundPhysicsStateController
|
|||
timestamps = Current(
|
||||
gate,
|
||||
teleportAdvanced: disposition is PositionTimestampDisposition.Apply
|
||||
&& advancesTeleport);
|
||||
&& advancesTeleport,
|
||||
previousTeleport: previousTeleport);
|
||||
accepted = ApplyAcceptedPosition(
|
||||
old,
|
||||
update,
|
||||
|
|
@ -1113,17 +1122,28 @@ public sealed class InboundPhysicsStateController
|
|||
0,
|
||||
spawn.InstanceSequence);
|
||||
|
||||
/// <summary>
|
||||
/// #307: <paramref name="previousTeleport"/> is the TELEPORT_TS value the
|
||||
/// gate held BEFORE the event this call is stamping. Only the two Position
|
||||
/// entry points can move that channel, so they pass their own pre-event
|
||||
/// capture; every other channel leaves it untouched and therefore gets
|
||||
/// "previous == current" by omission. The parameter deliberately has NO
|
||||
/// zero default: a literal 0 is a legal, common TELEPORT_TS, so a silent
|
||||
/// zero is indistinguishable from a genuine "never teleported" and reads
|
||||
/// as a spurious teleport regression to
|
||||
/// <c>RuntimeAuthoritativePositionRouteClassifier</c>.
|
||||
/// </summary>
|
||||
private static AcceptedPhysicsTimestamps Current(
|
||||
PhysicsTimestampGate gate,
|
||||
bool teleportAdvanced = false,
|
||||
ushort previousTeleport = 0) => new(
|
||||
ushort? previousTeleport = null) => new(
|
||||
gate.InstanceTimestamp,
|
||||
gate.ServerControlledMoveTimestamp,
|
||||
gate.TeleportTimestamp,
|
||||
gate.ForcePositionTimestamp,
|
||||
teleportAdvanced,
|
||||
TeleportHookRequired: false,
|
||||
previousTeleport);
|
||||
previousTeleport ?? gate.TeleportTimestamp);
|
||||
|
||||
private static WorldSession.EntitySpawn MergeUntimestampedCreate(
|
||||
WorldSession.EntitySpawn retained,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue