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:
parent
542dcfc384
commit
f02b995e4f
21 changed files with 1669 additions and 263 deletions
|
|
@ -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 < MinQuantum → return (too short — skip)
|
||||
/// if dVar1 <= 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue