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:
Erik 2026-08-03 12:10:21 +02:00
parent 1fc529cdcb
commit f24532adf3
11 changed files with 417 additions and 50 deletions

View file

@ -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()
{