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
|
|
@ -198,6 +198,9 @@ public sealed class ProjectilePhysicsStepper
|
|||
body.Position = beginPosition;
|
||||
body.Orientation = beginOrientation;
|
||||
|
||||
ObjectInfoState moverFlags = body.State.HasFlag(PhysicsStateFlags.PathClipped)
|
||||
? ObjectInfoState.PathClipped
|
||||
: ObjectInfoState.None;
|
||||
var resolved = _physics.ResolveWithTransition(
|
||||
beginPosition,
|
||||
candidatePosition,
|
||||
|
|
@ -208,7 +211,7 @@ public sealed class ProjectilePhysicsStepper
|
|||
stepDownHeight: 0f,
|
||||
isOnGround: previousOnWalkable,
|
||||
body: body,
|
||||
moverFlags: ObjectInfoState.PathClipped,
|
||||
moverFlags: moverFlags,
|
||||
movingEntityId: movingEntityId,
|
||||
localSphereOrigin: sphere.LocalOrigin,
|
||||
beginOrientation: beginOrientation,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public sealed class ShadowObjectRegistry
|
|||
{
|
||||
private readonly Dictionary<uint, List<ShadowEntry>> _cells = new();
|
||||
private readonly Dictionary<uint, List<uint>> _entityToCells = new(); // for deregistration
|
||||
private readonly HashSet<uint> _suspendedEntities = new();
|
||||
|
||||
/// <summary>
|
||||
/// A6.P4 door fix (2026-05-24): per-entity original shape list, used by
|
||||
|
|
@ -326,6 +327,33 @@ public sealed class ShadowObjectRegistry
|
|||
reg.State, reg.Flags, seedCellId, reg.IsStatic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an entity from every cell collision list while retaining the
|
||||
/// exact registration and shape payload needed to restore it later.
|
||||
/// This is the registry counterpart of retail
|
||||
/// <c>CPhysicsObj::remove_shadows_from_cells</c> during temporary
|
||||
/// leave-world/pending-cell residence; it is deliberately not logical
|
||||
/// teardown.
|
||||
/// </summary>
|
||||
public bool Suspend(uint entityId)
|
||||
{
|
||||
if (!_entityReg.ContainsKey(entityId))
|
||||
return false;
|
||||
|
||||
if (_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
{
|
||||
foreach (uint cellId in cellIds)
|
||||
{
|
||||
if (_cells.TryGetValue(cellId, out var list))
|
||||
list.RemoveAll(entry => entry.EntityId == entityId);
|
||||
}
|
||||
_entityToCells.Remove(entityId);
|
||||
}
|
||||
|
||||
_suspendedEntities.Add(entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BR-7 streaming hook — re-run the flood for every entity whose seed
|
||||
/// cell or current cell set touches <paramref name="landblockId"/>'s
|
||||
|
|
@ -343,6 +371,9 @@ public sealed class ShadowObjectRegistry
|
|||
|
||||
foreach (var kvp in _entityReg)
|
||||
{
|
||||
if (_suspendedEntities.Contains(kvp.Key))
|
||||
continue;
|
||||
|
||||
if ((kvp.Value.SeedCellId & 0xFFFF0000u) == lbPrefix)
|
||||
{
|
||||
toReflood.Add(kvp.Key);
|
||||
|
|
@ -407,6 +438,11 @@ public sealed class ShadowObjectRegistry
|
|||
/// </summary>
|
||||
public void UpdatePhysicsState(uint entityId, uint newState)
|
||||
{
|
||||
// Suspended dynamic objects have no cell rows, but their retained
|
||||
// registration must still receive authoritative state changes.
|
||||
if (_entityReg.TryGetValue(entityId, out var retainedRegistration))
|
||||
_entityReg[entityId] = retainedRegistration with { State = newState };
|
||||
|
||||
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
return; // not registered — no-op
|
||||
|
||||
|
|
@ -438,15 +474,17 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
_entityShapes.Remove(entityId);
|
||||
_entityReg.Remove(entityId);
|
||||
_suspendedEntities.Remove(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove all entities belonging to a landblock. With flood-driven
|
||||
/// registration an entity's cells can span landblock prefixes; entries
|
||||
/// under OTHER prefixes survive, and the entity is fully dropped only
|
||||
/// when no cells remain (its owner despawns it via
|
||||
/// <see cref="Deregister"/> in the normal path — retail's per-object
|
||||
/// remove_shadows_from_cells, Ghidra 0x00511230).
|
||||
/// registration an entity's cells can span landblock prefixes, so entries
|
||||
/// under other prefixes survive. A static registration ends when its final
|
||||
/// streamed cell disappears. A dynamic/server-live registration retains
|
||||
/// its exact payload with zero cell rows so a later load can reflood it;
|
||||
/// only its logical owner calls <see cref="Deregister"/> (retail's
|
||||
/// per-object remove_shadows_from_cells, Ghidra 0x00511230).
|
||||
/// </summary>
|
||||
public void RemoveLandblock(uint landblockId)
|
||||
{
|
||||
|
|
@ -473,8 +511,16 @@ public sealed class ShadowObjectRegistry
|
|||
foreach (var eid in entitiesToRemove)
|
||||
{
|
||||
_entityToCells.Remove(eid);
|
||||
_entityShapes.Remove(eid);
|
||||
_entityReg.Remove(eid);
|
||||
// A streamed-out server-live object is still logically alive.
|
||||
// Preserve dynamic registration/shape payload for the reload
|
||||
// reflood; static owners end with their landblock.
|
||||
if (!_entityReg.TryGetValue(eid, out var registration)
|
||||
|| registration.IsStatic)
|
||||
{
|
||||
_entityShapes.Remove(eid);
|
||||
_entityReg.Remove(eid);
|
||||
_suspendedEntities.Remove(eid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue