feat(physics): port retail projectile integration

Add a pure Core projectile driver for retail clock quanta, final-state acceleration, 50-unit velocity clamping, world-space angular integration, full-3D AlignPath, and oriented Setup-local sphere sweeps. Preserve exact success versus set_frame-only failure semantics, independent Contact/OnWalkable state, and full-cell identity across all landblock directions.

Port OBJECTINFO::missile_ignore with explicit weenie/creature metadata, remove the invented transition-step cap, retain PathClipped without arming PerfectClip, and keep authoritative target identity optional. Add malformed-shape/clock, thin-wall, terrain, target-exclusion, frame-spike, failure-state, and boundary conformance coverage.

Retire TS-2 and synchronize the architecture, milestones, roadmap, research oracle, and durable physics memory.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 11:43:11 +02:00
parent 542dcfc384
commit f02b995e4f
21 changed files with 1669 additions and 263 deletions

View file

@ -106,7 +106,9 @@ public sealed class PhysicsBody
public const float SmallVelocitySquared = SmallVelocity * SmallVelocity;
public const float DefaultFriction = 0.95f;
public const float MinQuantum = 1.0f / 30.0f; // ~0.0333 s
public const float MaxQuantum = 0.1f; // 10 fps lower bound
// Matching-client disassembly resolves the named lift's stripped global to
// 0.2 seconds. update_object consumes larger gaps as repeated 0.2 s quanta.
public const float MaxQuantum = 0.2f;
public const float HugeQuantum = 2.0f; // discard stale dt
// ── struct fields ──────────────────────────────────────────────────────
@ -177,6 +179,27 @@ public sealed class PhysicsBody
InWorld = true; // retail: enter_world / set_cell assigns physics_obj->cell
}
/// <summary>
/// Commits only the frame while retaining the current cell identity. This
/// is retail <c>CPhysicsObj::set_frame</c> at <c>0x00514090</c>, used by
/// <c>UpdateObjectInternal</c> when a transition fails: the integrated
/// candidate frame is kept, but <c>Position.objcell_id</c> is not changed
/// and no outdoor canonicalization is performed.
/// </summary>
public void SetFrameInCurrentCell(Vector3 worldPosition, Quaternion orientation)
{
Vector3 delta = worldPosition - _position;
_position = worldPosition;
Orientation = orientation;
if (CellPosition.ObjCellId != 0)
{
CellPosition = new Position(
CellPosition.ObjCellId,
new CellFrame(CellPosition.Frame.Origin + delta, orientation));
}
}
// Mirror a world-position translation into the cell-relative frame. Velocity is
// frame-invariant under translation, so the same delta applies to the local origin.
// AdjustToOutside then recomputes the cell index from the local (intra-landblock 24 m
@ -552,32 +575,35 @@ public sealed class PhysicsBody
// velocity += acceleration * dt (done unconditionally in decompile)
Velocity += Acceleration * dt;
// Angular integration: apply omega rotation.
// omega * dt gives the angle-axis delta rotation.
float omegaLen = Omega.Length();
if (omegaLen > 1e-6f)
// Angular integration: Frame::grotate receives the WORLD-space
// rotation vector omega*dt and premultiplies its quaternion onto the
// frame (0x005357A0; called at UpdatePhysicsInternal 0x00510935).
// Its F_EPSILON gate is applied to the rotation vector, not omega.
Vector3 rotation = Omega * dt;
float rotationLenSq = rotation.LengthSquared();
if (rotationLenSq >= PhysicsGlobals.EpsilonSq)
{
float angle = omegaLen * dt;
Quaternion deltaRot = Quaternion.CreateFromAxisAngle(Omega / omegaLen, angle);
Orientation = Quaternion.Normalize(Quaternion.Multiply(Orientation, deltaRot));
float angle = MathF.Sqrt(rotationLenSq);
Quaternion deltaRot = Quaternion.CreateFromAxisAngle(rotation / angle, angle);
Orientation = Quaternion.Normalize(Quaternion.Multiply(deltaRot, Orientation));
}
}
// ── FUN_00515020 ───────────────────────────────────────────────────────
// ── CPhysicsObj::update_object 0x00515D10 ───────────────────────────────
/// <summary>
/// Per-frame top-level driver. Computes dt from the wall clock versus
/// LastUpdateTime, clamping to [MinQuantum, HugeQuantum], then calls
/// calc_acceleration and UpdatePhysicsInternal.
/// LastUpdateTime, discards invalid gaps, and advances the consumed
/// PhysicsTimer time through fixed maximum quanta plus one remainder.
///
/// Decompiled logic (FUN_00515020):
/// Decompiled logic (<c>CPhysicsObj::update_object</c> 0x00515D10):
/// if parent-attached (offset +0x40 != 0) → return
/// dVar1 = currentTime - LastUpdateTime
/// if dVar1 &lt; MinQuantum → return (too short — skip)
/// if dVar1 &lt;= EPSILON → update timestamp and return
/// if dVar1 > HugeQuantum → update timestamp and return (stale — discard)
/// while dVar1 > MaxQuantum: simulate MaxQuantum step, subtract
/// if dVar1 > MinQuantum: simulate remainder
/// LastUpdateTime = currentTime
/// LastUpdateTime = consumed PhysicsTimer time
///
/// The caller passes currentTime; the object does not read a global clock
/// directly in this port so tests can drive the clock explicitly.
@ -586,9 +612,13 @@ public sealed class PhysicsBody
{
double deltaTime = currentTime - LastUpdateTime;
// dt too small — nothing to simulate yet
if (deltaTime < MinQuantum)
// Retail drops only a true micro-fragment. A larger remainder at or
// below MinQuantum is retained by leaving LastUpdateTime unchanged.
if (deltaTime <= PhysicsGlobals.EPSILON)
{
LastUpdateTime = currentTime;
return;
}
// Stale / first frame — just consume the time without simulating
if (deltaTime > HugeQuantum)
@ -597,9 +627,12 @@ public sealed class PhysicsBody
return;
}
// Sub-step: break large dt into MaxQuantum chunks
double physicsTime = LastUpdateTime;
// Sub-step: break large dt into MaxQuantum chunks.
while (deltaTime > MaxQuantum)
{
physicsTime += MaxQuantum;
calc_acceleration();
UpdatePhysicsInternal(MaxQuantum);
deltaTime -= MaxQuantum;
@ -608,10 +641,11 @@ public sealed class PhysicsBody
// Simulate the remainder
if (deltaTime > MinQuantum)
{
physicsTime += deltaTime;
calc_acceleration();
UpdatePhysicsInternal((float)deltaTime);
}
LastUpdateTime = currentTime;
LastUpdateTime = physicsTime;
}
}