fix #270: settle sweep at spawn placement - contact from the compressed first gravity frame

The [SPAWN-PLACE] probe showed placements succeeding with contact=False:
find_placement_pos validates the spot but the sphere sits a few cm above
the floor with no touch. Retail gains spawn contact from the FIRST
GRAVITY FRAME (every CPhysicsObj simulates, falls, touches); our
stationary remotes never run a physics frame. The seed now compresses
that settle: a short downward ResolveWithTransition from the server
position snaps the body onto the floor, and its touch grants the contact
plane + CONTACT/ON_WALKABLE via the verbatim commit. No floor within
reach = stays airborne, exactly like retail's fall.

The retry predicate now watches the CONTACT transient (the flag
contact_allows_move reads) instead of ContactPlaneValid - a DR-tick
writeback can set plane DATA from last-known state without real contact,
which is why the heavy attackers in the retry-session log got exactly
one placement attempt and then 250+ refused Falling dispatches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-31 00:21:03 +02:00
parent 807fdb5f7f
commit 21b3a3f3d3

View file

@ -178,43 +178,51 @@ internal sealed class LiveEntityNetworkUpdateController
height = 1.835f; height = 1.835f;
} }
ResolveResult placement = _physicsEngine.ResolvePlacement( var moverFlags = IsPlayerGuid(serverGuid)
? AcDream.Core.Physics.ObjectInfoState.IsPlayer
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide;
// Retail's spawn contact comes from the FIRST GRAVITY FRAME, not the
// placement itself: every retail CPhysicsObj simulates, so a freshly
// placed creature falls the few centimetres onto the floor and the
// transition's touch grants the contact plane. Our stationary remotes
// never run a physics frame (the DR tick resolves only movers), so
// the settle is compressed here: a short downward sweep from the
// server position. Its touch handler produces exactly the state
// retail's first frame would (position snapped onto the floor,
// contact plane + CONTACT/ON_WALKABLE committed below). A sweep that
// finds no floor (true airborne spawn) leaves the body airborne.
AcDream.Core.Physics.ResolveResult settle = _physicsEngine.ResolveWithTransition(
worldPos, worldPos,
worldPos + new System.Numerics.Vector3(0f, 0f, -0.5f),
cellId, cellId,
radius, sphereRadius: radius,
height, sphereHeight: height,
stepUpHeight: 0.4f, stepUpHeight: 0.4f,
stepDownHeight: 0.4f, stepDownHeight: 0.4f,
moverFlags: IsPlayerGuid(serverGuid) isOnGround: false,
? AcDream.Core.Physics.ObjectInfoState.IsPlayer body: remote.Body,
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide moverFlags: moverFlags,
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide,
movingEntityId: entity.Id); movingEntityId: entity.Id);
if (AcDream.Core.Physics.PhysicsDiagnostics.DumpMotionEnabled) if (AcDream.Core.Physics.PhysicsDiagnostics.DumpMotionEnabled)
{ {
Console.WriteLine( Console.WriteLine(
$"[SPAWN-PLACE] guid={serverGuid:X8} cell=0x{cellId:X8} ok={placement.Ok} " $"[SPAWN-PLACE] guid={serverGuid:X8} cell=0x{cellId:X8} ok={settle.Ok} "
+ $"contact={placement.InContact} walkable={placement.OnWalkable}"); + $"contact={settle.InContact} walkable={settle.OnWalkable} "
+ $"z={worldPos.Z:F3}->{settle.Position.Z:F3}");
} }
if (!placement.Ok) if (!settle.Ok || !settle.InContact)
return; // unplaceable — stays airborne, like a failed retail placement return; // no floor within reach — stays airborne like retail's fall
remote.Body.Position = placement.Position; remote.Body.Position = settle.Position;
remote.Body.ContactPlaneValid = placement.InContact;
if (placement.InContact)
{
remote.Body.ContactPlane = placement.ContactPlane;
remote.Body.ContactPlaneCellId = placement.ContactPlaneCellId;
remote.Body.ContactPlaneIsWater = placement.ContactPlaneIsWater;
remote.Body.GroundNormal = placement.ContactPlane.Normal;
}
AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition( AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition(
remote.Body, remote.Body,
placement.InContact, settle.InContact,
placement.OnWalkable, settle.OnWalkable,
placement.CollisionNormalValid, settle.CollisionNormalValid,
placement.CollisionNormal, settle.CollisionNormal,
previousContact: false, previousContact: false,
previousOnWalkable: false, previousOnWalkable: false,
remote.Movement.HitGround, remote.Movement.HitGround,
@ -727,17 +735,19 @@ internal sealed class LiveEntityNetworkUpdateController
remote.Body.Orientation = entity.Rotation; remote.Body.Orientation = entity.Rotation;
remote.Body.Position = entity.Position; remote.Body.Position = entity.Position;
} }
// #270: run the retail spawn placement so the body has real ground // #270: run the retail spawn settle so the body has real ground
// contact BEFORE the funnel below dispatches this packet's actions — // contact BEFORE the funnel below dispatches this packet's actions —
// an attack swing needs contact_allows_move true to animate. Retried // an attack swing needs contact_allows_move true to animate. Retried
// (not creation-only) while the body has never been successfully // (not creation-only) while the body lacks the CONTACT transient
// placed: creation during the login flood can precede streaming // (the flag contact_allows_move reads — NOT ContactPlaneValid, which
// residency or the entity's cell hydration, and retail's own answer // a DR writeback can set from last-known plane data without real
// to "object addressed before its cell exists" is the // contact): creation during the login flood can precede streaming
// CObjectMaint lost-cell list — park it, re-place when the cell is // residency or cell hydration, and retail's own answer to "object
// available. The no-contact-and-no-plane predicate is exactly // addressed before its cell exists" is the CObjectMaint lost-cell
// "never placed"; one success ends the retries. // list — park it, re-place when the cell is available. One grounded
if (!remote.Body.InContact && !remote.Body.ContactPlaneValid) // settle ends the retries; a genuinely airborne remote (mid-jump
// player) fails the floor probe harmlessly until it lands.
if (!remote.Body.InContact)
{ {
SeedRemoteSpawnPlacement( SeedRemoteSpawnPlacement(
remote, remote,