feat(app): stage live entities before runtime placement

This commit is contained in:
Erik 2026-08-01 18:43:23 +02:00
parent 3f800a4aec
commit 74103f75b5
2 changed files with 187 additions and 3 deletions

View file

@ -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