fix(physics): route remote Positions on contact, not walkability (AP-140)
The two gates that decide whether an accepted remote Position is interpolated
or hard-snapped read `Airborne`, which is `!Body.OnWalkable` — WALKABILITY.
Retail reads CONTACT: InterpolationManager::adjust_offset @0x00555D30 gates its
entire body on `transient_state & 1` @0x00555D52, so a retail body in contact
with a non-walkable face still interpolates.
The two predicates disagree in exactly one state — in contact, not on walkable
ground — which 204d0ae0 turned from unreachable into ordinary. Before it, the
per-tick forge made every non-airborne remote walkable by construction, so the
disagreement could not occur.
Both gates now read `!Body.InContact`: ApplyRemoteContactRouting's flight
carve-out and OnPosition's player-remote arm.
`Airborne` is deliberately NOT re-derived from CONTACT. That would perturb all
five of its writers and contradict a pinned assertion in
RemoteTeleportPlacementTests.Apply_PendingGroundToSteepContact_ (InContact:
true, OnWalkable: false -> Assert.True(remote.Airborne)); a previous
implementer attempted it and correctly backed out rather than editing the
assertion. This narrower shape touches no existing test.
AP-140's register row is retired in this commit, as the row itself specified.
Honest scope: this is a faithfulness fix, not a visible one. ACE derives its
IsGrounded flag with the same floor_z test, so during a slide it almost
certainly reports not-grounded, the classifier returns NoPositionOperation, and
neither arm is taken. Expect no observable change against ACE.
Suite 11,027 passed / 4 skipped / 0 failed (baseline 11,023).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f4f2579575
commit
2eb39a0250
6 changed files with 273 additions and 33 deletions
File diff suppressed because one or more lines are too long
|
|
@ -945,14 +945,22 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
/// arm selection testable and must not be collapsed to a bool.</summary>
|
||||
internal enum RemoteContactArm : byte
|
||||
{
|
||||
/// <summary>The body was airborne. Hard-snapped, exactly as before
|
||||
/// route 4a. Gated on <c>remote.Airborne</c> ALONE — the wire contact
|
||||
/// bit is never read here. The common case is the landing packet, but
|
||||
/// a not-in-contact packet also reaches this arm whenever the
|
||||
/// classification is one route 4a does not own (null, cell-less
|
||||
/// <summary>The body was in free flight — NOT in contact with any
|
||||
/// surface. Hard-snapped. Gated on the body's own
|
||||
/// <c>Body.InContact</c> ALONE (AP-140, retired 2026-08-04) — the wire
|
||||
/// contact bit is never read here. The common case is the landing
|
||||
/// packet, but a not-in-contact packet also reaches this arm whenever
|
||||
/// the classification is one route 4a does not own (null, cell-less
|
||||
/// <c>SetPosition</c>, or rejected), because the
|
||||
/// <c>IsAirborneNoOperation</c> early return fires only for
|
||||
/// classifications it does own. That matches pre-4a behaviour.</summary>
|
||||
/// classifications it does own. That matches pre-4a behaviour.
|
||||
///
|
||||
/// <para>The name is retained for continuity with route 4a; read
|
||||
/// "airborne" here as retail's CONTACT_TS-clear, not as the client
|
||||
/// <c>RemoteMotion.Airborne</c> flag, which is
|
||||
/// <c>!Body.OnWalkable</c> — a strictly WIDER set. The two disagree on
|
||||
/// a body in contact with a non-walkable face (a steep slide), which
|
||||
/// retail interpolates.</para></summary>
|
||||
AirborneSnap,
|
||||
|
||||
/// <summary>Route 4a's near InterpolateTo branch.</summary>
|
||||
|
|
@ -1008,7 +1016,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
/// packet classifies <c>Interpolate</c>, so if the 4a test came first it
|
||||
/// would ENQUEUE a body that must PLANT, and a creature knocked off a
|
||||
/// ledge would glide down over a packet interval. The player-remote caller
|
||||
/// reaches this method only with <c>Airborne == false</c> (its landing
|
||||
/// reaches this method only with <c>Body.InContact == true</c> (its landing
|
||||
/// block sits ahead of its routing and returns), so the carve-out is inert
|
||||
/// there and the two callers stay one decision.
|
||||
/// </para>
|
||||
|
|
@ -1046,7 +1054,29 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// TEMPORARY — strip with the ACDREAM_PROBE_REMOTE_SLIDE family.
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.BeginRemoteSlideAttribution(
|
||||
canonical.ServerGuid);
|
||||
if (remote.Airborne)
|
||||
// AP-140 (retired 2026-08-04): retail's predicate for exactly this
|
||||
// decision is CONTACT, not walkability.
|
||||
// `InterpolationManager::adjust_offset` @0x00555D30 gates its ENTIRE
|
||||
// body on `physics_obj->transient_state & 1` @0x00555D52 — and
|
||||
// TransientState bit 0 is `CONTACT_TS` (acclient.h:3690), NOT
|
||||
// `ON_WALKABLE_TS` (0x2). A retail body in contact with a non-walkable
|
||||
// face therefore still walks toward its queued waypoint. This gate used
|
||||
// to read `remote.Airborne`, which is `!Body.OnWalkable` — WALKABILITY,
|
||||
// a strictly wider set. The two disagree on precisely one state (in
|
||||
// contact, not on walkable ground), and Bug B (`204d0ae0`) turned that
|
||||
// state from unreachable into ordinary by deleting the per-tick
|
||||
// `TransientState |= Contact | OnWalkable` forge that had made every
|
||||
// non-airborne remote walkable by construction. A remote sliding on a
|
||||
// steep roof lives in it, and was being hard-snapped at UpdatePosition
|
||||
// cadence instead of interpolated.
|
||||
//
|
||||
// `Airborne` itself is deliberately NOT re-derived from CONTACT: it has
|
||||
// five writers, all spelling `!Body.OnWalkable`, and the per-tick
|
||||
// updater's ground-clamp branch plus
|
||||
// `RemoteTeleportPlacementTests.Apply_PendingGroundToSteepContact_…`
|
||||
// both depend on the walkability reading. Only the two ROUTING gates
|
||||
// move (this one and `OnPosition`'s player-remote landing block).
|
||||
if (!remote.Body.InContact)
|
||||
{
|
||||
// Verbatim from the pre-4a branch, queue deliberately NOT
|
||||
// cleared: the arc integrates locally (K-fix15), and clearing
|
||||
|
|
@ -1059,19 +1089,16 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// review round.
|
||||
//
|
||||
// Do NOT restate this as "the queue is already empty here" — it
|
||||
// is not. Nothing that sets Airborne clears the queue except the
|
||||
// teleport hook's StopInterpolating: neither the 0xF74E
|
||||
// VectorUpdate (OnVector, below) nor any of the five
|
||||
// `Airborne = !Body.OnWalkable` sites do. Those five are
|
||||
// SettleSpawnedRemoteContact (this file),
|
||||
// RemoteTeleportPlacement.Apply,
|
||||
// RuntimeSetPositionState's canonical placement commit, and
|
||||
// RuntimeRemotePhysicsUpdater's two — the SetPositionInternal
|
||||
// commit in Tick and the TickHidden resolve. A walking NPC can
|
||||
// enqueue a near waypoint and then step off a lip, arriving here
|
||||
// with a populated queue. That is exactly why the landing clear
|
||||
// exists, and a reader who believes the queue is empty here could
|
||||
// delete it.
|
||||
// is not. Nothing that CLEARS the body's CONTACT bit clears the
|
||||
// queue: it is dropped by the per-tick sweep's SetPositionInternal
|
||||
// commit (and by TickHidden's resolve) whenever the contact plane
|
||||
// stops being valid, and by nothing else — the teleport hook's
|
||||
// StopInterpolating is the only thing that empties the queue on a
|
||||
// leave-ground edge, and the 0xF74E VectorUpdate (OnVector, below)
|
||||
// does not. A walking NPC can enqueue a near waypoint and then step
|
||||
// off a lip, arriving here with a populated queue. That is exactly
|
||||
// why the landing clear exists, and a reader who believes the queue
|
||||
// is empty here could delete it.
|
||||
remote.Body.Position = worldPos;
|
||||
remote.Body.Orientation = rotation;
|
||||
return new RemoteContactRouting(
|
||||
|
|
@ -2145,16 +2172,32 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
}
|
||||
|
||||
// ── LANDING TRANSITION ────────────────────────────────────────
|
||||
// First IsGrounded=true UP while the client still considers the
|
||||
// body airborne (`!Body.OnWalkable`, now derived by the per-tick
|
||||
// SetPositionInternal commit rather than latched here).
|
||||
// First IsGrounded=true UP while the body is still in FREE
|
||||
// FLIGHT — not in contact with any surface (`!Body.InContact`,
|
||||
// derived by the per-tick SetPositionInternal commit from the
|
||||
// sweep's contact plane).
|
||||
// Hard-snap to the authoritative landing position and clear the
|
||||
// interpolation queue (an airborne remote's Positions hard-snap
|
||||
// and never enqueue, so any pre-arc waypoints are stale).
|
||||
// interpolation queue (a free-flying remote's Positions
|
||||
// hard-snap and never enqueue, so any pre-arc waypoints are
|
||||
// stale).
|
||||
// `rmState.Airborne` is deliberately NOT cleared here: the next
|
||||
// tick derives it from the sweep, which is the only thing that
|
||||
// can tell walkable ground from a steep face.
|
||||
//
|
||||
// AP-140 (retired 2026-08-04): this gate read `rmState.Airborne`
|
||||
// (`!Body.OnWalkable` — WALKABILITY). Retail's predicate for
|
||||
// exactly this snap-vs-interpolate decision is CONTACT:
|
||||
// `InterpolationManager::adjust_offset` @0x00555D30 gates its
|
||||
// entire body on `transient_state & 1` @0x00555D52, and bit 0 is
|
||||
// `CONTACT_TS` (acclient.h:3690), not `ON_WALKABLE_TS` (0x2). A
|
||||
// remote in contact with a NON-walkable face — sliding down a
|
||||
// steep roof, ordinary since Bug B `204d0ae0` deleted the
|
||||
// per-tick walkability forge — was taking this hard snap at
|
||||
// UpdatePosition cadence; it now falls through to the grounded
|
||||
// routing below and interpolates, as retail does. See the twin
|
||||
// gate in ApplyRemoteContactRouting for why `Airborne` itself is
|
||||
// left alone.
|
||||
//
|
||||
// Bug B (2026-08-04) — the twin of the per-tick forge. This
|
||||
// block used to additionally zero the body velocity, assert
|
||||
// `Contact | OnWalkable`, invoke MovementManager::HitGround, and
|
||||
|
|
@ -2173,7 +2216,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// • retail never toggles GRAVITY_PS on a ground edge — see the
|
||||
// per-tick commit's comment.
|
||||
// What remains is AP-87's acdream-only snap, unchanged.
|
||||
if (rmState.Airborne)
|
||||
if (!rmState.Body.InContact)
|
||||
{
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
|
|
@ -2269,8 +2312,8 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
// is the SAME shared entry point the NPC arm below calls —
|
||||
// retail's disassembly makes no `this == player` distinction
|
||||
// on any of these branches. The player arm reaches it only
|
||||
// with Airborne == false (the landing block above returns), so
|
||||
// the airborne carve-out inside is inert here.
|
||||
// with Body.InContact == true (the landing block above
|
||||
// returns), so the free-flight carve-out inside is inert here.
|
||||
bool willBeDrTicked = WillAdvanceRemoteMotion(update.Guid, rmState);
|
||||
RemoteContactRouting playerRouting = ApplyRemoteContactRouting(
|
||||
_remotePlacementDrive,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,48 @@ public sealed class LiveEntityNetworkBranchRoutingTests
|
|||
source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AP-140 (retired 2026-08-04): the SECOND accepted-Position routing
|
||||
/// gate — <c>OnPosition</c>'s player-remote landing block — must
|
||||
/// select the hard snap on retail's CONTACT predicate
|
||||
/// (<c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its
|
||||
/// whole body on <c>transient_state & 1</c> @0x00555D52, and bit 0
|
||||
/// is <c>CONTACT_TS</c>), not on the client <c>Airborne</c> flag,
|
||||
/// which is <c>!Body.OnWalkable</c> — WALKABILITY, a strictly wider
|
||||
/// set that also captures a remote sliding on a steep face.
|
||||
///
|
||||
/// <para>
|
||||
/// A source pin rather than a behavioural fixture for the reason this
|
||||
/// class already documents: the controller's dependency set is
|
||||
/// composition-only. The twin gate inside
|
||||
/// <c>ApplyRemoteContactRouting</c> — a static method, so reachable —
|
||||
/// IS covered behaviourally, in
|
||||
/// <c>LiveEntityNetworkRemoteSteadyStateIntegrationTests</c>. Restore
|
||||
/// <c>if (rmState.Airborne)</c> here and this test fails.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PlayerRemoteLandingSnapSelectsOnContactNotOnWalkability()
|
||||
{
|
||||
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
||||
|
||||
Assert.Contains(
|
||||
"if (!rmState.Body.InContact)",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
// `rmState.Airborne` survives as a WRITE target and in prose (the
|
||||
// block's own comment explains why it is deliberately not cleared
|
||||
// there); what must never come back is reading it as the gate.
|
||||
Assert.DoesNotContain(
|
||||
"if (rmState.Airborne)",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"if (remote.Airborne)",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string ReadSource(string fileName)
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
|
|
|
|||
|
|
@ -392,7 +392,13 @@ public sealed class LiveEntityNetworkRemoteFarSnapIntegrationTests
|
|||
fixture.AllowDestination();
|
||||
(RuntimeEntityRecord record, RemoteMotion remote, PhysicsBody body) =
|
||||
fixture.AddRemote(0x70004008u, Destination);
|
||||
// AP-140 (retired 2026-08-04): "mid-arc" is retail's CONTACT_TS-clear
|
||||
// (`InterpolationManager::adjust_offset` gates on `transient_state & 1`
|
||||
// @0x00555D52), not the client `Airborne` walkability flag. Both are
|
||||
// set here so the test states free flight in the terms the gate now
|
||||
// reads AND the terms it used to.
|
||||
remote.Airborne = true;
|
||||
remote.Body.TransientState = TransientStateFlags.Active;
|
||||
var target = new Vector3(60f, 10f, 7f);
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
|
|
|
|||
|
|
@ -161,7 +161,12 @@ public sealed class LiveEntityNetworkRemoteSteadyStateIntegrationTests
|
|||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005001u, new Vector3(12f, 14f, 7f));
|
||||
remote.Body.Position = new Vector3(10f, 10f, 5f);
|
||||
// AP-140 (retired 2026-08-04): free flight is retail's CONTACT_TS-clear
|
||||
// (`InterpolationManager::adjust_offset` gates on `transient_state & 1`
|
||||
// @0x00555D52), not the client `Airborne` walkability flag. Both are
|
||||
// stated so the test reads the same before and after that change.
|
||||
remote.Airborne = true;
|
||||
remote.Body.TransientState = TransientStateFlags.Active;
|
||||
var landing = new Vector3(10.5f, 10f, 5f); // 0.5 m — well within 4 m.
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
|
|
@ -211,6 +216,137 @@ public sealed class LiveEntityNetworkRemoteSteadyStateIntegrationTests
|
|||
Assert.Equal(new Vector3(10f, 10f, 5f), remote.Body.Position);
|
||||
}
|
||||
|
||||
// ── AP-140 (retired 2026-08-04): the gate's predicate is CONTACT ────────
|
||||
|
||||
/// <summary>
|
||||
/// The divergence AP-140 recorded, as behaviour. A remote in CONTACT with
|
||||
/// a NON-walkable face — sliding down a steep roof — is
|
||||
/// <c>Airborne == true</c> by the client flag's own definition
|
||||
/// (<c>!Body.OnWalkable</c>), and used to be hard-snapped at
|
||||
/// UpdatePosition cadence. Retail interpolates it:
|
||||
/// <c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its entire
|
||||
/// body on <c>transient_state & 1</c> @0x00555D52, and bit 0 is
|
||||
/// <c>CONTACT_TS</c> (acclient.h:3690), not <c>ON_WALKABLE_TS</c> (0x2).
|
||||
///
|
||||
/// <para>
|
||||
/// This is the ONE state on which the two predicates disagree, so it is
|
||||
/// the only test that can discriminate the fix: point the gate back at
|
||||
/// <c>remote.Airborne</c> and this test alone fails, with
|
||||
/// <c>AirborneSnap</c> and a moved body.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SteepContactBody_InterpolatesInsteadOfSnapping()
|
||||
{
|
||||
using var fixture = new RemotePlacementDriveFixture();
|
||||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005003u, new Vector3(12f, 14f, 7f));
|
||||
var before = new Vector3(10f, 10f, 5f);
|
||||
remote.Body.Position = before;
|
||||
// Contact with a face too steep to stand on: CONTACT set, ON_WALKABLE
|
||||
// clear — exactly what the sweep's SetPositionInternal commit derives
|
||||
// on a 52-degree roof since Bug B (204d0ae0) deleted the per-tick
|
||||
// walkability forge that used to make this state unreachable.
|
||||
remote.Body.TransientState =
|
||||
TransientStateFlags.Active | TransientStateFlags.Contact;
|
||||
Assert.True(remote.Body.InContact);
|
||||
Assert.False(remote.Body.OnWalkable);
|
||||
// The client flag agrees with its five writers, all `!Body.OnWalkable`.
|
||||
remote.Airborne = !remote.Body.OnWalkable;
|
||||
Assert.True(remote.Airborne);
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
||||
fixture.Drive,
|
||||
record,
|
||||
remote,
|
||||
Classify(hasContact: true, playerDistance: 10f),
|
||||
before + new Vector3(0.5f, 0f, 0f),
|
||||
Quaternion.Identity,
|
||||
willBeDrTicked: true);
|
||||
|
||||
Assert.Equal(
|
||||
LiveEntityNetworkUpdateController.RemoteContactArm
|
||||
.SteadyStateInterpolate,
|
||||
routing.Arm);
|
||||
// The interpolate arm enqueues a waypoint and leaves the body to the
|
||||
// per-tick catch-up; the snap arm would have written it directly.
|
||||
Assert.Equal(before, remote.Body.Position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The regression guard on the other side of the same gate: a genuinely
|
||||
/// free-flying remote — no contact with anything — must still take the
|
||||
/// hard snap. Retail's <c>adjust_offset</c> writes nothing at all with
|
||||
/// <c>CONTACT_TS</c> clear, so the queued waypoint cannot advance the
|
||||
/// body and the authoritative position is the only thing that can.
|
||||
/// Widen the gate to "always interpolate" and this test fails.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FreeFlightBodyWithNoContact_StillSnaps()
|
||||
{
|
||||
using var fixture = new RemotePlacementDriveFixture();
|
||||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005004u, new Vector3(12f, 14f, 7f));
|
||||
remote.Body.Position = new Vector3(10f, 10f, 5f);
|
||||
remote.Body.TransientState = TransientStateFlags.Active;
|
||||
Assert.False(remote.Body.InContact);
|
||||
remote.Airborne = !remote.Body.OnWalkable;
|
||||
var landing = new Vector3(10.5f, 10f, 5f);
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
||||
fixture.Drive,
|
||||
record,
|
||||
remote,
|
||||
Classify(hasContact: true, playerDistance: 10f),
|
||||
landing,
|
||||
Quaternion.Identity,
|
||||
willBeDrTicked: true);
|
||||
|
||||
Assert.Equal(
|
||||
LiveEntityNetworkUpdateController.RemoteContactArm.AirborneSnap,
|
||||
routing.Arm);
|
||||
Assert.Equal(landing, remote.Body.Position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unchanged third state: contact with WALKABLE ground. Both
|
||||
/// predicates agree here, and both before and after AP-140's fix the
|
||||
/// packet interpolates. Pinned so a future edit cannot "simplify" the
|
||||
/// gate into something that only satisfies the two tests above.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WalkableGroundedBody_IsUnchangedAndStillInterpolates()
|
||||
{
|
||||
using var fixture = new RemotePlacementDriveFixture();
|
||||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005005u, new Vector3(12f, 14f, 7f));
|
||||
var before = new Vector3(10f, 10f, 5f);
|
||||
remote.Body.Position = before;
|
||||
remote.Body.TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
remote.Airborne = !remote.Body.OnWalkable;
|
||||
Assert.False(remote.Airborne);
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
||||
fixture.Drive,
|
||||
record,
|
||||
remote,
|
||||
Classify(hasContact: true, playerDistance: 10f),
|
||||
before + new Vector3(0.5f, 0f, 0f),
|
||||
Quaternion.Identity,
|
||||
willBeDrTicked: true);
|
||||
|
||||
Assert.Equal(
|
||||
LiveEntityNetworkUpdateController.RemoteContactArm
|
||||
.SteadyStateInterpolate,
|
||||
routing.Arm);
|
||||
Assert.Equal(before, remote.Body.Position);
|
||||
}
|
||||
|
||||
private static void AssertRenderPoseSuppressed(
|
||||
RuntimeAuthoritativePositionRoute? route)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -96,7 +96,21 @@ internal sealed class RemotePlacementDriveFixture : IDisposable
|
|||
Orientation = Quaternion.Identity,
|
||||
LastUpdateTime = 1d,
|
||||
State = PhysicsStateFlags.Gravity,
|
||||
TransientState = TransientStateFlags.Active,
|
||||
// AP-140 (retired 2026-08-04): the remote stands on the flat
|
||||
// source landblock, so the sweep's SetPositionInternal commit
|
||||
// would derive CONTACT (`contact_plane_valid` @0x00515430) and
|
||||
// ON_WALKABLE (`contact_plane.N.z >= floor_z`
|
||||
// @0x00515465-0x0051548E) for it. Both bits now have to be
|
||||
// present, because the accepted-Position routing gates read
|
||||
// retail's CONTACT predicate
|
||||
// (`InterpolationManager::adjust_offset` @0x00555D52) rather than
|
||||
// the client `Airborne` walkability flag: a body with a bare
|
||||
// `Active` transient state is in FREE FLIGHT and every routing
|
||||
// test would take the free-flight snap arm. Tests that want free
|
||||
// flight clear these explicitly.
|
||||
TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable,
|
||||
};
|
||||
body.SnapToCell(SourceCell, body.Position, body.Position);
|
||||
Lifetime.Entities.SetPhysicsBody(record, body);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue