feat(physics): integrate live projectile runtime
Attach the retail projectile driver to canonical LiveEntityRecord ownership, sharing one PhysicsBody and full-cell identity with RemoteMotion regardless of creation order. Apply timestamp-gated state, vector, and position corrections in place, preserve active ordinary-body behavior when Missile clears, and keep renderer, effects, shadows, and spatial buckets synchronized across loaded/pending transitions. Validate malformed packets before canonical timestamp mutation, validate adopted bodies from their current frame rather than stale CreateObject data, serialize late Setup resolution with streaming DAT reads, and preserve classification across clock anomalies. Retain shadow registrations through temporary leave-world residence while logical teardown remains generation-scoped. Add 31 App lifecycle/adversarial tests plus Core shadow suspension coverage, and synchronize the retail pseudocode, architecture, milestones, roadmap, and durable physics memory. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
f02b995e4f
commit
a51ebc66e9
12 changed files with 2559 additions and 32 deletions
|
|
@ -21,8 +21,22 @@ public interface ILiveEntityRemoteMotionRuntime
|
|||
PhysicsBody Body { get; }
|
||||
}
|
||||
|
||||
/// <summary>Marker for the projectile runtime added by the projectile phase.</summary>
|
||||
public interface ILiveEntityProjectileRuntime { }
|
||||
/// <summary>
|
||||
/// Optional seam for a remote-motion component that consumes the canonical
|
||||
/// full-cell identity owned by <see cref="LiveEntityRecord"/>. The runtime
|
||||
/// binds this for every production remote, regardless of whether projectile
|
||||
/// classification arrives before or after the MovementManager.
|
||||
/// </summary>
|
||||
public interface ILiveEntityCanonicalCellConsumer
|
||||
{
|
||||
void BindCanonicalCell(Func<uint> read, Action<uint> write);
|
||||
}
|
||||
|
||||
/// <summary>Projectile physics state owned by a live object.</summary>
|
||||
public interface ILiveEntityProjectileRuntime
|
||||
{
|
||||
PhysicsBody Body { get; }
|
||||
}
|
||||
|
||||
/// <summary>Marker for the DAT-driven effect profile added by the effect phase.</summary>
|
||||
public interface ILiveEntityEffectProfile { }
|
||||
|
|
@ -96,6 +110,15 @@ public sealed class LiveEntityRecord
|
|||
public bool WorldSpawnPublished { get; internal set; }
|
||||
public LiveEntityProjectionKind ProjectionKind { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cell used when a streamed landblock asks live objects to restore their
|
||||
/// spatial projection. Once materialized, the canonical runtime cell wins;
|
||||
/// the retained server snapshot is only a first-materialization source.
|
||||
/// </summary>
|
||||
public uint ProjectionCellId => WorldEntity is not null
|
||||
? FullCellId
|
||||
: Snapshot.Position?.LandblockId ?? FullCellId;
|
||||
|
||||
internal void RefreshDerivedState(bool refreshPosition = true)
|
||||
{
|
||||
if (refreshPosition && Snapshot.Position is { } position)
|
||||
|
|
@ -480,8 +503,38 @@ public sealed class LiveEntityRuntime
|
|||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record))
|
||||
throw new InvalidOperationException($"Cannot bind remote motion before live entity 0x{serverGuid:X8} exists.");
|
||||
if (record.ProjectileRuntime is { } projectile
|
||||
&& !ReferenceEquals(projectile.Body, runtime.Body))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} already owns a different projectile physics body.");
|
||||
}
|
||||
record.RemoteMotionRuntime = runtime;
|
||||
record.PhysicsBody = runtime.Body;
|
||||
if (runtime is ILiveEntityCanonicalCellConsumer cellConsumer)
|
||||
{
|
||||
// Capture the incarnation, not only its GUID. A callback retained
|
||||
// by an old MovementManager must never mutate a replacement that
|
||||
// later reuses the same server GUID.
|
||||
LiveEntityRecord incarnation = record;
|
||||
cellConsumer.BindCanonicalCell(
|
||||
read: () =>
|
||||
_recordsByGuid.TryGetValue(serverGuid, out var current)
|
||||
&& ReferenceEquals(current, incarnation)
|
||||
&& ReferenceEquals(current.RemoteMotionRuntime, runtime)
|
||||
? current.FullCellId
|
||||
: 0u,
|
||||
write: cellId =>
|
||||
{
|
||||
if (cellId != 0
|
||||
&& _recordsByGuid.TryGetValue(serverGuid, out var current)
|
||||
&& ReferenceEquals(current, incarnation)
|
||||
&& ReferenceEquals(current.RemoteMotionRuntime, runtime))
|
||||
{
|
||||
RebucketLiveEntity(serverGuid, cellId);
|
||||
}
|
||||
});
|
||||
}
|
||||
_remoteMotionByGuid[serverGuid] = runtime;
|
||||
}
|
||||
|
||||
|
|
@ -495,7 +548,61 @@ public sealed class LiveEntityRuntime
|
|||
return false;
|
||||
_remoteMotionByGuid.Remove(serverGuid);
|
||||
record.RemoteMotionRuntime = null;
|
||||
record.PhysicsBody = null;
|
||||
if (record.ProjectileRuntime is null)
|
||||
record.PhysicsBody = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetProjectileRuntime(uint serverGuid, ILiveEntityProjectileRuntime runtime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot bind projectile physics before live entity 0x{serverGuid:X8} is materialized.");
|
||||
}
|
||||
if (record.RemoteMotionRuntime is { } remote
|
||||
&& !ReferenceEquals(remote.Body, runtime.Body))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} already owns a different remote-motion physics body.");
|
||||
}
|
||||
if (record.ProjectileRuntime is { } projectile
|
||||
&& !ReferenceEquals(projectile.Body, runtime.Body))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} already owns a different projectile physics body.");
|
||||
}
|
||||
|
||||
record.ProjectileRuntime = runtime;
|
||||
record.PhysicsBody = runtime.Body;
|
||||
}
|
||||
|
||||
public bool TryGetProjectileRuntime(
|
||||
uint serverGuid,
|
||||
out ILiveEntityProjectileRuntime runtime)
|
||||
{
|
||||
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
&& record.ProjectileRuntime is { } found)
|
||||
{
|
||||
runtime = found;
|
||||
return true;
|
||||
}
|
||||
|
||||
runtime = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ClearProjectileRuntime(uint serverGuid)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.ProjectileRuntime is null)
|
||||
return false;
|
||||
|
||||
record.ProjectileRuntime = null;
|
||||
if (record.RemoteMotionRuntime is null)
|
||||
record.PhysicsBody = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue