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
|
|
@ -181,12 +181,14 @@ internal sealed class ProjectileController
|
|||
// animation workset, classification adopts that same body instead
|
||||
// of replacing it or replaying CreateObject vectors.
|
||||
body = sharedBody;
|
||||
uint currentCellId = record.FullCellId;
|
||||
Vector3 currentCellLocal = CellLocalFromWorld(
|
||||
body.Position,
|
||||
currentCellId,
|
||||
liveCenterX,
|
||||
liveCenterY);
|
||||
// Retail has one CPhysicsObj, whose Position owns both objcell_id
|
||||
// and the cell-local frame. The graphical record's FullCellId is
|
||||
// only the later projection receipt and is legitimately still
|
||||
// zero while a residence-managed Create awaits presentation.
|
||||
// Validate and adopt the canonical body frame itself; successful
|
||||
// classification projects that same cell into the sidecar below.
|
||||
uint currentCellId = body.CellPosition.ObjCellId;
|
||||
Vector3 currentCellLocal = body.CellPosition.Frame.Origin;
|
||||
if (!IsFinite(body.Position)
|
||||
|| !IsFinite(body.Velocity)
|
||||
|| !IsFinite(body.Omega)
|
||||
|
|
@ -268,7 +270,17 @@ internal sealed class ProjectileController
|
|||
entity.SetPosition(body.Position);
|
||||
entity.Rotation = body.Orientation;
|
||||
entity.ParentCellId = canonicalCellId;
|
||||
if (!_liveEntities.RebucketLiveEntity(record.ServerGuid, canonicalCellId)
|
||||
// Classification can run from the projection-visible callback after
|
||||
// Runtime has already installed this exact cell. Re-entering Rebucket
|
||||
// from that callback would supersede the outer projection transaction
|
||||
// merely to write the same bucket again. Only perform a spatial move
|
||||
// when classification is actually changing residence.
|
||||
bool alreadyProjectedInCanonicalCell = record.IsSpatiallyProjected
|
||||
&& record.FullCellId == canonicalCellId;
|
||||
if ((!alreadyProjectedInCanonicalCell
|
||||
&& !_liveEntities.RebucketLiveEntity(
|
||||
record.ServerGuid,
|
||||
canonicalCellId))
|
||||
|| !_liveEntities.TryGetRecord(record.ServerGuid, out var currentRecord)
|
||||
|| !ReferenceEquals(currentRecord, record)
|
||||
|| !ReferenceEquals(currentRecord.WorldEntity, entity)
|
||||
|
|
@ -848,10 +860,43 @@ internal sealed class ProjectileController
|
|||
|
||||
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
||||
{
|
||||
if (record.ProjectileRuntime is not RuntimeProjectile runtime
|
||||
|| record.WorldEntity is not { } entity
|
||||
if (record.WorldEntity is not { } entity
|
||||
|| !_liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|| !ReferenceEquals(current, record))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (visible
|
||||
&& record.ProjectileRuntime is null
|
||||
&& record.PhysicsBody is not null
|
||||
&& (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0)
|
||||
{
|
||||
// A residence-managed Create builds its one CPhysicsObj before
|
||||
// presentation, but Runtime intentionally withholds the committed
|
||||
// cell frame until enter_world completes. Materialization may
|
||||
// therefore observe the body while it is still cell-less and its
|
||||
// eager TryBind correctly refuses that incomplete frame. The
|
||||
// projection-visible edge is the first point at which both the
|
||||
// canonical body and its authoritative placement are guaranteed
|
||||
// to be committed, so retry classification here instead of
|
||||
// fabricating a CreateObject-frame fallback.
|
||||
Setup? setup = _setupResolver?.Resolve(
|
||||
entity.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
{
|
||||
int liveCenterX = _origin?.CenterX ?? 0;
|
||||
int liveCenterY = _origin?.CenterY ?? 0;
|
||||
_ = TryBind(
|
||||
record,
|
||||
setup,
|
||||
_lastFiniteGameTime,
|
||||
liveCenterX,
|
||||
liveCenterY);
|
||||
}
|
||||
}
|
||||
|
||||
if (record.ProjectileRuntime is not RuntimeProjectile runtime
|
||||
|| !ReferenceEquals(current.ProjectileRuntime, runtime))
|
||||
{
|
||||
return;
|
||||
|
|
@ -933,20 +978,6 @@ internal sealed class ProjectileController
|
|||
&& float.IsFinite(value.Y)
|
||||
&& float.IsFinite(value.Z);
|
||||
|
||||
private static Vector3 CellLocalFromWorld(
|
||||
Vector3 worldPosition,
|
||||
uint cellId,
|
||||
int liveCenterX,
|
||||
int liveCenterY)
|
||||
{
|
||||
int landblockX = (int)((cellId >> 24) & 0xFFu);
|
||||
int landblockY = (int)((cellId >> 16) & 0xFFu);
|
||||
return worldPosition - new Vector3(
|
||||
(landblockX - liveCenterX) * 192f,
|
||||
(landblockY - liveCenterY) * 192f,
|
||||
0f);
|
||||
}
|
||||
|
||||
private bool TryGetCurrent(
|
||||
uint serverGuid,
|
||||
out LiveEntityRecord record,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue