fix(vfx): bind effects after canonical placement
C3c created graphical effect, projectile, and static-animation sidecars before Runtime finished the entity's first SetPosition. One-shot F754/F755 packets could be discarded, projectiles could adopt a cell-less body, and animated statics could compete for body ownership. Keep effects behind an exact-incarnation presentation barrier, retry projectile/static binding on the committed visibility edge, and keep effect cells synchronized with canonical rebuckets. User verified spell, recall, arrow, projectile, portal, and static presentation; 90 focused App tests and the Release build pass.
This commit is contained in:
parent
1fc529cdcb
commit
f24532adf3
11 changed files with 417 additions and 50 deletions
|
|
@ -107,7 +107,7 @@ public sealed class ProjectileControllerTests
|
|||
Assert.Null(record.AnimationRuntime);
|
||||
var remote =
|
||||
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.SnapToCell(CellA, entity.Position, entity.Position);
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
|
||||
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
|
||||
|
|
@ -1006,6 +1006,8 @@ public sealed class ProjectileControllerTests
|
|||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
var remote =
|
||||
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
remote.Body.SnapToCell(CellA, entity.Position, entity.Position);
|
||||
|
||||
record.FinalPhysicsState = MissileState;
|
||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||
|
|
@ -1081,7 +1083,10 @@ public sealed class ProjectileControllerTests
|
|||
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
|
||||
remote.Body.set_velocity(new Vector3(8f, 0f, 0f));
|
||||
remote.Body.Omega = new Vector3(0f, 0f, 3f);
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.SnapToCell(
|
||||
startCell,
|
||||
entity.Position,
|
||||
new Vector3(191f, 10f, 50f));
|
||||
remote.Body.State = record.FinalPhysicsState;
|
||||
PhysicsBody body = remote.Body;
|
||||
|
||||
|
|
@ -1142,7 +1147,7 @@ public sealed class ProjectileControllerTests
|
|||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||
var remote =
|
||||
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
|
||||
remote.Body.Position = entity.Position;
|
||||
remote.Body.SnapToCell(CellA, entity.Position, entity.Position);
|
||||
remote.Body.Orientation = entity.Rotation;
|
||||
|
||||
Assert.True(float.IsNaN(record.Snapshot.Physics!.Value.Velocity!.Value.X));
|
||||
|
|
@ -1181,6 +1186,55 @@ public sealed class ProjectileControllerTests
|
|||
Assert.Equal(MissileState, remote.Body.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResidencePlacementVisibility_RetriesProjectileBindingAfterBodyFrameCommits()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
var remote = fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
|
||||
|
||||
Assert.Null(record.ProjectileRuntime);
|
||||
Assert.True(fixture.Live.WithdrawLiveEntityProjection(Guid));
|
||||
|
||||
remote.Body.Orientation = Quaternion.Identity;
|
||||
remote.Body.set_velocity(new Vector3(10f, 0f, 0f));
|
||||
remote.Body.SnapToCell(CellA, entity.Position, entity.Position);
|
||||
|
||||
Assert.True(fixture.Live.RebucketLiveEntity(Guid, CellA));
|
||||
|
||||
Assert.NotNull(record.ProjectileRuntime);
|
||||
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
|
||||
Assert.True(record.ProjectileRuntime.Body.InWorld);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SharedBodyCell_IsCanonicalBeforePresentationCellIsProjected()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
LiveEntityRecord record = fixture.Spawn(instance: 1);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
var remote = fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
|
||||
|
||||
Assert.True(fixture.Live.WithdrawLiveEntityProjection(Guid));
|
||||
record.CanonicalLandblockId = 0u;
|
||||
record.FullCellId = 0u;
|
||||
Assert.Equal(0u, record.FullCellId);
|
||||
remote.Body.Orientation = Quaternion.Identity;
|
||||
remote.Body.set_velocity(new Vector3(10f, 0f, 0f));
|
||||
remote.Body.SnapToCell(CellA, entity.Position, entity.Position);
|
||||
|
||||
Assert.True(fixture.Controller.TryBind(
|
||||
record,
|
||||
ProjectileSetup(),
|
||||
currentTime: 1.0,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1));
|
||||
|
||||
Assert.Equal(CellA, record.FullCellId);
|
||||
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SharedRemoteMotionCell_DelegatesToCanonicalLiveRecord()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -239,6 +239,50 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
|||
Assert.Equal(referenceFrames[0], frames[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingLiveStaticOwner_BindsCanonicalRuntimeOwnerOnFirstTick()
|
||||
{
|
||||
var loader = new Loader();
|
||||
loader.Add(AnimationId, TwoFrameAnimation());
|
||||
Setup setup = MakeSetup();
|
||||
WorldEntity entity = MakeEntity(serverGuid: 0x70000001u);
|
||||
var sequencer = new AnimationSequencer(
|
||||
setup,
|
||||
new MotionTable(),
|
||||
loader);
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Orientation = entity.Rotation,
|
||||
};
|
||||
body.SnapToCell(0x01010001u, entity.Position, Vector3.Zero);
|
||||
LiveEntityAnimationState animation =
|
||||
LiveState(entity, setup, sequencer);
|
||||
int resolutions = 0;
|
||||
var scheduler = new RetailStaticAnimatingObjectScheduler(
|
||||
loader,
|
||||
(_, _) => { },
|
||||
(_, _, _) => { },
|
||||
resolveLiveOwner: candidate =>
|
||||
{
|
||||
Assert.Same(entity, candidate);
|
||||
resolutions++;
|
||||
return (animation, body);
|
||||
});
|
||||
Assert.True(scheduler.Register(entity, new ScriptActivationInfo(
|
||||
ScriptId: 0,
|
||||
PartTransforms: entity.IndexedPartTransforms,
|
||||
PartAvailability: entity.IndexedPartAvailable,
|
||||
Setup: setup,
|
||||
DefaultAnimationId: AnimationId,
|
||||
UsesStaticAnimationWorkset: true)));
|
||||
|
||||
scheduler.Tick(0.02f);
|
||||
scheduler.Tick(0.02f);
|
||||
|
||||
Assert.Equal(1, resolutions);
|
||||
Assert.True(scheduler.TryTakePreparedFramesForTest(OwnerId, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LivePhysicsStaticOwner_DiscardsLongNonResidentIntervalOnReentry()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using AcDream.Core.Net.Messages;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -129,6 +130,31 @@ public sealed class EntityEffectControllerTests
|
|||
return entity;
|
||||
}
|
||||
|
||||
public LiveEntityRecord AwaitInitialPresentation(
|
||||
uint guid = Guid,
|
||||
ushort generation = 1,
|
||||
EntityEffectProfile? profile = null)
|
||||
{
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, generation);
|
||||
RuntimeEntityRecord canonical = Assert.IsType<RuntimeEntityRecord>(
|
||||
Runtime.RegisterLiveEntity(spawn).Canonical);
|
||||
WorldEntity entity = Assert.IsType<WorldEntity>(
|
||||
Runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
spawn.Position!.Value.LandblockId,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: exact =>
|
||||
exact.EffectProfile = profile ?? LiveProfile(),
|
||||
out LiveEntityRecord? record,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement));
|
||||
Assert.Same(entity, record!.WorldEntity);
|
||||
Assert.False(record.IsSpatiallyProjected);
|
||||
Assert.False(record.IsSpatiallyVisible);
|
||||
Assert.True(Controller.PrepareLiveEntityOwner(guid));
|
||||
return record;
|
||||
}
|
||||
|
||||
public static EntityEffectProfile LiveProfile(
|
||||
uint tableDid = TableDid,
|
||||
uint rawDefaultType = RawType,
|
||||
|
|
@ -187,6 +213,32 @@ public sealed class EntityEffectControllerTests
|
|||
Assert.Equal(1, fixture.Runner.ActiveScriptCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeInitialPlacementWindow_DefersPacketsUntilPresentationBinds()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
LiveEntityRecord record = fixture.AwaitInitialPresentation();
|
||||
|
||||
fixture.Controller.HandleDirect(new PlayPhysicsScript(Guid, DirectDid));
|
||||
fixture.Controller.HandleTyped(
|
||||
new PlayPhysicsScriptType(Guid, RawType, 0.5f));
|
||||
|
||||
Assert.Equal(2, fixture.Controller.PendingPacketCount);
|
||||
Assert.Equal(0, fixture.Runner.ActiveScriptCount);
|
||||
|
||||
// Runtime has committed the first frame and the graphical placement
|
||||
// receipt is now publishing its pose/resources.
|
||||
record.FullCellId = 0x01010001u;
|
||||
record.IsSpatiallyProjected = true;
|
||||
record.IsSpatiallyVisible = true;
|
||||
Assert.True(fixture.Controller.OnPresentationBound(record));
|
||||
Assert.Equal(0, fixture.Controller.PendingPacketCount);
|
||||
Assert.Equal(2, fixture.Runner.ActiveScriptCount);
|
||||
|
||||
fixture.Runner.Tick(0.0);
|
||||
Assert.Equal([1u, 2u], EmitterIds(fixture.Sink));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadyOwnerReceivesDirectAndTypedPacketsImmediately()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2190,16 +2190,23 @@ public sealed class LiveEntityRuntimeTests
|
|||
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
||||
var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010022u));
|
||||
runtime.MaterializeLiveEntity(guid, 0x01010022u, id => Entity(id, guid));
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010022u,
|
||||
id => Entity(id, guid))!;
|
||||
|
||||
runtime.RebucketLiveEntity(guid, 0x0102FFFFu);
|
||||
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
Assert.Equal(0x01010022u, record.FullCellId);
|
||||
Assert.Equal(0x0102FFFFu, record.CanonicalLandblockId);
|
||||
Assert.Equal(0x01010022u, entity.ParentCellId);
|
||||
Assert.Equal(0x01010022u, entity.EffectCellId);
|
||||
|
||||
runtime.RebucketLiveEntity(guid, 0x01020033u);
|
||||
Assert.Equal(0x01020033u, record.FullCellId);
|
||||
Assert.Equal(0x01020033u, entity.ParentCellId);
|
||||
Assert.Equal(0x01020033u, entity.EffectCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue