From 807fdb5f7f7318d776314194a9265c6a49bbb294 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 30 Jul 2026 23:57:38 +0200 Subject: [PATCH] fix #270: retry spawn placement until first success (lost-cell recovery analog) + attributed probes The creation-only spawn placement could no-op or fail during the login flood (cell id not yet hydrated / streaming collision not resident) with nothing retrying - monsters created in that window stayed airborne-flagged forever and their action animations remained refused. Retail's answer to 'object addressed before its cell exists' is the CObjectMaint lost-cell list (GotoLostCell): park, re-place when the cell is available. The UM dispatch path now retries SeedRemoteSpawnPlacement while the body has never been successfully placed (no contact AND no stored plane); one success ends the retries. Probes: [SPAWN-PLACE] logs each placement outcome (guid/cell/ok/ contact/walkable); [UM-ACT]/[MT-FAIL] now carry the owning guid via MotionInterpreter.DiagnosticGuid (probe-identity-attribution lesson) plus the body's live contact/walkable flags. Co-Authored-By: Claude Opus 5 --- .../LiveEntityMotionRuntimeController.cs | 2 ++ .../LiveEntityNetworkUpdateController.cs | 25 +++++++++++++++---- src/AcDream.Core/Physics/MotionInterpreter.cs | 16 +++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs b/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs index db8a635b..37d631ab 100644 --- a/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs +++ b/src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs @@ -45,6 +45,8 @@ internal sealed class LiveEntityMotionRuntimeController rm.Sink = new AcDream.Core.Physics.Motion.MotionTableDispatchSink(sequencer); rm.Motion.DefaultSink = rm.Sink; } + // #270 probe identity — strip with the probes. + rm.Motion.DiagnosticGuid = serverGuid; // #174 (2026-07-05): the RemoveLinkAnimations seam is retail // CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to // CPartArray::HandleEnterWorld 0x00517d70 → diff --git a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs index d71387ea..c7a5ca44 100644 --- a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs +++ b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs @@ -190,6 +190,12 @@ internal sealed class LiveEntityNetworkUpdateController | AcDream.Core.Physics.ObjectInfoState.EdgeSlide : AcDream.Core.Physics.ObjectInfoState.EdgeSlide, movingEntityId: entity.Id); + if (AcDream.Core.Physics.PhysicsDiagnostics.DumpMotionEnabled) + { + Console.WriteLine( + $"[SPAWN-PLACE] guid={serverGuid:X8} cell=0x{cellId:X8} ok={placement.Ok} " + + $"contact={placement.InContact} walkable={placement.OnWalkable}"); + } if (!placement.Ok) return; // unplaceable — stays airborne, like a failed retail placement @@ -720,15 +726,24 @@ internal sealed class LiveEntityNetworkUpdateController update.Guid); remote.Body.Orientation = entity.Rotation; remote.Body.Position = entity.Position; - // #270: run the retail spawn placement so the fresh body has - // real ground contact BEFORE the funnel below dispatches this - // packet's actions — a first-ever-UM attack swing needs - // contact_allows_move true to animate. + } + // #270: run the retail spawn placement so the body has real ground + // contact BEFORE the funnel below dispatches this packet's actions — + // an attack swing needs contact_allows_move true to animate. Retried + // (not creation-only) while the body has never been successfully + // placed: creation during the login flood can precede streaming + // residency or the entity's cell hydration, and retail's own answer + // to "object addressed before its cell exists" is the + // CObjectMaint lost-cell list — park it, re-place when the cell is + // available. The no-contact-and-no-plane predicate is exactly + // "never placed"; one success ends the retries. + if (!remote.Body.InContact && !remote.Body.ContactPlaneValid) + { SeedRemoteSpawnPlacement( remote, update.Guid, entity, - entity.Position, + remote.Body.Position, // Interior live entities carry ParentCellId; outdoor live // entities carry the outdoor landcell in EffectCellId (see // WorldEntity's cell-field docs). 0 → helper no-ops. diff --git a/src/AcDream.Core/Physics/MotionInterpreter.cs b/src/AcDream.Core/Physics/MotionInterpreter.cs index a176564b..0e6ed006 100644 --- a/src/AcDream.Core/Physics/MotionInterpreter.cs +++ b/src/AcDream.Core/Physics/MotionInterpreter.cs @@ -678,6 +678,14 @@ public sealed class MotionInterpreter : IMotionDoneSink /// public Action? RemoveLinkAnimations { get; set; } + /// + /// #270 diagnostic identity (2026-07-30): the owning entity's server + /// guid, stamped by the binding layer so [UM-ACT]/[MT-FAIL] probe lines + /// are attributable (probe-identity-attribution lesson). 0 = unset. + /// Strip with the #270 probes. + /// + public uint DiagnosticGuid { get; set; } + /// /// R3-W4 no-op seam standing in for retail /// CPhysicsObj::InitializeMotionTables, called from @@ -2779,7 +2787,7 @@ public sealed class MotionInterpreter : IMotionDoneSink if (PhysicsDiagnostics.DumpMotionEnabled) { Console.WriteLine( - $"[UM-ACT] cmd=0x{a.Command:X8} stamp={incoming} stored={stored} " + $"[UM-ACT] guid={DiagnosticGuid:X8} cmd=0x{a.Command:X8} stamp={incoming} stored={stored} " + $"newer={newer} auton={a.Autonomous} localSkip={IsLocalPlayer && a.Autonomous}"); } @@ -3038,8 +3046,10 @@ public sealed class MotionInterpreter : IMotionDoneSink if (!dispatchOk && PhysicsDiagnostics.DumpMotionEnabled) { Console.WriteLine( - $"[MT-FAIL] motion=0x{motion:X8} speed={p.Speed:F2} " - + $"style=0x{InterpretedState.CurrentStyle:X8} substate=0x{InterpretedState.ForwardCommand:X8}"); + $"[MT-FAIL] guid={DiagnosticGuid:X8} motion=0x{motion:X8} speed={p.Speed:F2} " + + $"style=0x{InterpretedState.CurrentStyle:X8} substate=0x{InterpretedState.ForwardCommand:X8} " + + $"contact={PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)} " + + $"walkable={PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable)}"); } if (!dispatchOk)