feat(app): stage live entities before runtime placement
This commit is contained in:
parent
3f800a4aec
commit
74103f75b5
2 changed files with 187 additions and 3 deletions
|
|
@ -30,6 +30,18 @@ public enum LiveEntityProjectionKind
|
|||
Attached,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects whether App may publish a newly constructed sidecar immediately or
|
||||
/// must keep it non-spatial until the canonical Runtime SetPosition receipt is
|
||||
/// projected. The latter is the production CreateObject path; the former is a
|
||||
/// temporary compatibility seam for callers not yet cut over in Slice 4B2.
|
||||
/// </summary>
|
||||
internal enum LiveEntityMaterializationResidence
|
||||
{
|
||||
LegacyImmediate,
|
||||
AwaitRuntimePlacement,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logical-resource seam coordinated by <see cref="LiveEntityRuntime"/>.
|
||||
/// Spatial bucketing is deliberately absent: registering or removing meshes,
|
||||
|
|
@ -249,6 +261,8 @@ public sealed class LiveEntityRecord
|
|||
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
|
||||
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
||||
public bool ResourcesRegistered { get; internal set; }
|
||||
internal LiveEntityMaterializationResidence? MaterializationResidence
|
||||
{ get; set; }
|
||||
public bool IsSpatiallyProjected { get; internal set; }
|
||||
public bool IsSpatiallyVisible { get; internal set; }
|
||||
internal ulong ProjectionMutationVersion { get; set; }
|
||||
|
|
@ -578,10 +592,20 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
Func<uint, WorldEntity> factory,
|
||||
LiveEntityProjectionKind projectionKind,
|
||||
Action<LiveEntityRecord>? initializeProjection,
|
||||
out LiveEntityRecord? record)
|
||||
out LiveEntityRecord? record,
|
||||
LiveEntityMaterializationResidence residence =
|
||||
LiveEntityMaterializationResidence.LegacyImmediate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
if (residence is LiveEntityMaterializationResidence
|
||||
.AwaitRuntimePlacement
|
||||
&& projectionKind is not LiveEntityProjectionKind.World)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Only a top-level world projection can await canonical Runtime placement.",
|
||||
nameof(projectionKind));
|
||||
}
|
||||
record = null;
|
||||
if (_isClearing
|
||||
|| _sessionClearPendingFinalization
|
||||
|
|
@ -605,6 +629,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
{
|
||||
record = _projections.AddMaterializing(expectedCanonical);
|
||||
createdSidecar = true;
|
||||
record.MaterializationResidence = residence;
|
||||
initializeProjection?.Invoke(record);
|
||||
}
|
||||
catch
|
||||
|
|
@ -618,6 +643,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
throw;
|
||||
}
|
||||
}
|
||||
else if (record.MaterializationResidence != residence)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8}/{expectedCanonical.Incarnation} "
|
||||
+ $"cannot change its materialization residence from "
|
||||
+ $"{record.MaterializationResidence} to {residence}.");
|
||||
}
|
||||
|
||||
if (record.WorldEntity is null)
|
||||
{
|
||||
|
|
@ -653,6 +685,10 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
|
||||
record.WorldEntity = entity;
|
||||
// Runtime-placement residence is sticky for this incarnation and
|
||||
// must suppress draw before arbitrary resource callbacks observe
|
||||
// or re-enter the sidecar.
|
||||
RefreshPresentation(record);
|
||||
_isRegisteringResources = true;
|
||||
try
|
||||
{
|
||||
|
|
@ -726,6 +762,18 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
RefreshPresentation(record);
|
||||
WorldEntity materialized = record.WorldEntity!;
|
||||
if (residence is LiveEntityMaterializationResidence
|
||||
.AwaitRuntimePlacement)
|
||||
{
|
||||
// Resource construction is a logical App-side ownership edge, not
|
||||
// world residence. Runtime's immutable Place receipt is the only
|
||||
// path which may install the accepted frame and spatial bucket.
|
||||
record.IsSpatiallyProjected = false;
|
||||
record.IsSpatiallyVisible = false;
|
||||
RefreshSpatialPresentationIndexes(record);
|
||||
RefreshPresentation(record);
|
||||
return materialized;
|
||||
}
|
||||
if (!RebucketLiveEntity(serverGuid, fullCellId)
|
||||
|| !_projections.TryGet(expectedCanonical, out LiveEntityRecord? current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|
|
@ -746,6 +794,14 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
return false;
|
||||
if (record.MaterializationResidence is
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement)
|
||||
{
|
||||
// The private Runtime Place path below performs a presentation-
|
||||
// only bucket update. This legacy API also commits canonical
|
||||
// Runtime residence and cannot touch a cut-over incarnation.
|
||||
return false;
|
||||
}
|
||||
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
bool wasProjected = record.IsSpatiallyProjected;
|
||||
|
|
@ -3031,8 +3087,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
return;
|
||||
|
||||
PhysicsStateFlags state = record.FinalPhysicsState;
|
||||
entity.IsDrawVisible =
|
||||
(state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
|
||||
bool residenceVisible = record.MaterializationResidence is not
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement
|
||||
|| record.IsSpatiallyProjected;
|
||||
entity.IsDrawVisible = residenceVisible
|
||||
&& (state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
|
||||
|
||||
bool interactionVisible = record.IsSpatiallyVisible
|
||||
&& record.ProjectionKind is LiveEntityProjectionKind.World
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue