fix(interaction): restore distant use after runtime cutover
Runtime GetObjectA lookup became intentionally non-constructing, so static doors and corpses entered MoveToObject without a physics host and their target snapshot timed out at the origin. Ensure the canonical minimal host exists before routing the server move. Runtime first-entry also grounds the local player before graphical PartArray attachment. That could leave an unmatched startup CMotionInterp node ahead of all later use and cast motion. Drain matched PartArray entries first, then retire only the impossible pre-attach suffix at the presentation attach boundary. Add focused regressions for static-target host materialization and attach-order reconciliation. User verified near and distant object use in the connected client; focused App tests pass 3/3.
This commit is contained in:
parent
670f307c84
commit
1fc529cdcb
4 changed files with 183 additions and 1 deletions
|
|
@ -334,6 +334,17 @@ internal sealed class PlayerModeController :
|
|||
controller.Motion.DefaultSink =
|
||||
new MotionTableDispatchSink(sequencer);
|
||||
sequencer.Manager.HandleEnterWorld();
|
||||
// C3c constructs and grounds the Runtime-owned player before
|
||||
// graphical PartArray presentation attaches. Ground entry can
|
||||
// therefore enqueue CMotionInterp work while no animation sink
|
||||
// exists; that work has no matching PartArray completion and
|
||||
// would permanently sit ahead of later MoveTo/cast motions.
|
||||
// First let the newly attached PartArray drain every matching
|
||||
// animation above, then retire only the unmatched pre-attach
|
||||
// interpreter suffix. In retail both owners exist together,
|
||||
// so this suffix cannot arise; this is the split-lifetime
|
||||
// reconciliation at their single attach boundary.
|
||||
controller.Motion.HandleExitWorld();
|
||||
}
|
||||
|
||||
var legacyCamera = new ChaseCamera { Aspect = _viewport.Aspect };
|
||||
|
|
|
|||
|
|
@ -417,7 +417,17 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
if (update.MotionState.MovementType == 6
|
||||
&& path.TargetGuid is { } tgtGuid
|
||||
&& _liveEntities is { } liveMoveEntities
|
||||
&& liveMoveEntities.TryGetInteractionEligibleEntity(tgtGuid, out var tgtEnt))
|
||||
&& liveMoveEntities.TryGetInteractionEligibleEntity(tgtGuid, out var tgtEnt)
|
||||
// Retail resolves the CPhysicsObj itself here. Even a static,
|
||||
// animation-less object (door, chest, corpse, NPC prop) can
|
||||
// therefore accept AddVoyeur and immediately publish its
|
||||
// position. C3c moved the local player's GetObjectA seam into
|
||||
// Runtime, where lookup is intentionally non-constructing;
|
||||
// eagerly materialize the existing canonical minimal host at
|
||||
// this App composition boundary before PerformMovement asks
|
||||
// Runtime for it. Without this, MoveToObject arms but receives
|
||||
// no target snapshot and times out at (0,0,0).
|
||||
&& ResolvePhysicsHost(tgtGuid) is not null)
|
||||
{
|
||||
ms.Type = AcDream.Core.Physics.MovementType.MoveToObject;
|
||||
ms.ObjectId = tgtGuid;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,32 @@ public sealed class C3cF2AutoEntryWiringTests
|
|||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayerPresentationAttach_DrainsMatchedAnimationsBeforeStartupMotionSuffix()
|
||||
{
|
||||
string source = ReadSource("Input", "PlayerModeController.cs");
|
||||
|
||||
int attach = source.IndexOf(
|
||||
"controller.Motion.DefaultSink =",
|
||||
StringComparison.Ordinal);
|
||||
int animationDrain = source.IndexOf(
|
||||
"sequencer.Manager.HandleEnterWorld();",
|
||||
attach,
|
||||
StringComparison.Ordinal);
|
||||
int unmatchedMotionDrain = source.IndexOf(
|
||||
"controller.Motion.HandleExitWorld();",
|
||||
animationDrain,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.True(attach >= 0, "The player animation sink was not attached.");
|
||||
Assert.True(
|
||||
animationDrain > attach,
|
||||
"Matching PartArray animations must drain after attaching the sink.");
|
||||
Assert.True(
|
||||
unmatchedMotionDrain > animationDrain,
|
||||
"Only the unmatched pre-attach interpreter suffix may drain last.");
|
||||
}
|
||||
|
||||
private static string ReadSource(params string[] relativePath)
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.Physics;
|
||||
|
||||
public sealed class LiveEntityMotionRuntimeControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void MoveToObject_StaticTargetWithoutHost_MaterializesCanonicalMinimalHost()
|
||||
{
|
||||
const uint targetGuid = 0x70000091u;
|
||||
const uint cellId = 0x01010001u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
0x0101FFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
var runtime = LiveEntityRuntimeFixture.Create(
|
||||
spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||
runtime.RegisterAndMaterializeProjection(Spawn(targetGuid, cellId));
|
||||
Assert.False(runtime.TryGetPhysicsHost(targetGuid, out _));
|
||||
|
||||
var origin = new LiveWorldOriginState();
|
||||
origin.Recenter(1, 1);
|
||||
var controller = new LiveEntityMotionRuntimeController(
|
||||
runtime,
|
||||
new PhysicsDataCache(),
|
||||
static () => null,
|
||||
new SelectionState(),
|
||||
origin);
|
||||
var movement = new MovementManager(new MotionInterpreter());
|
||||
var update = new WorldSession.EntityMotionUpdate(
|
||||
Guid: 0x50000001u,
|
||||
MotionState: new CreateObject.ServerMotionState(
|
||||
Stance: 0x3D,
|
||||
ForwardCommand: null,
|
||||
MovementType: 6,
|
||||
MoveToParameters: 0x203u,
|
||||
MoveToSpeed: 1f,
|
||||
MoveToRunRate: 1f,
|
||||
MoveToPath: new CreateObject.MoveToPathData(
|
||||
TargetGuid: targetGuid,
|
||||
OriginCellId: cellId,
|
||||
OriginX: 10f,
|
||||
OriginY: 10f,
|
||||
OriginZ: 5f,
|
||||
DistanceToObject: 0.6f,
|
||||
MinDistance: 0f,
|
||||
FailDistance: 15f,
|
||||
WalkRunThreshold: 15f,
|
||||
DesiredHeading: 0f,
|
||||
Bitfield: 0x203u)),
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 2,
|
||||
ServerControlSequence: 3,
|
||||
IsAutonomous: false);
|
||||
|
||||
Assert.True(controller.RouteServerMoveTo(movement, cellId, update));
|
||||
Assert.True(runtime.TryGetPhysicsHost(targetGuid, out var targetHost));
|
||||
Assert.IsType<EntityPhysicsHost>(targetHost);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid, uint cellId)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
cellId,
|
||||
10f,
|
||||
10f,
|
||||
5f,
|
||||
1f,
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
Position: 1,
|
||||
Movement: 1,
|
||||
State: 1,
|
||||
Vector: 1,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 1,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 1,
|
||||
Instance: 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.Static,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: null,
|
||||
SoundTableId: null,
|
||||
PhysicsScriptTableId: null,
|
||||
Parent: null,
|
||||
Children: null,
|
||||
Scale: null,
|
||||
Friction: null,
|
||||
Elasticity: null,
|
||||
Translucency: null,
|
||||
Velocity: null,
|
||||
Acceleration: null,
|
||||
AngularVelocity: null,
|
||||
DefaultScriptType: null,
|
||||
DefaultScriptIntensity: null,
|
||||
Timestamps: timestamps);
|
||||
return new WorldSession.EntitySpawn(
|
||||
Guid: guid,
|
||||
Position: position,
|
||||
SetupTableId: 0x02000001u,
|
||||
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
|
||||
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
|
||||
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
BasePaletteId: null,
|
||||
ObjScale: null,
|
||||
Name: "static target",
|
||||
ItemType: null,
|
||||
MotionState: null,
|
||||
MotionTableId: null,
|
||||
PhysicsState: (uint)PhysicsStateFlags.Static,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue