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

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