From 05392e227bcbe98a27bc79ba803b2ef9e10d314e Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 3 Aug 2026 14:24:32 -0500 Subject: [PATCH] BT410 5.3.102: ProcessCollision -- trees fall, walls do not, and both sides of a ram hurt The per-contact responder (@004abb40), overriding the engine's protected virtual per the MECH4.NOTES.md decode: THE SEPARATING GATE: a contact whose relative velocity points away from the surface ((normal . rel) < -1e-4) is skipped entirely -- no damage while pulling apart. MECH-VS-MECH: another mech takes the collision damage too, zone -1, its own cylinder resolving where the ram landed. Both sides of a ram hurt, and both route through the 5.3.100 economy, so a ram rattles rather than executes. THE CRUSHABLE SENTINEL: a CulturalIcon takes its crunch dispatch, and one WITHOUT the StoppingCollisionVolume flag then sets the amount to exactly 0.00123f -- Simulate's response block reads that value as "the move stands", restores the post-snap saves, and drives on through. Trees fall; walls do not. THE CONTACT ACCUMULATOR feeds the audio-bound collisionState indicator (0->1 fires the impact sound once per contact, not per frame) and captures the impact speed for the authored volume/brightness scales. [T3 simplification, tagged in source: the authentic InitialHit/Slide split keys StaticBounce's OUT coefficients against an unidentified literal @0x4ac048.] LIVE, one arena run, all three behaviours in the wild and zero deaths: [crash] CRUNCH (crushable) at (371.551,-102.862) <- drove through it [colldmg] rattle 1.72691 pts in 3 sub-hits [crash] BLOCK dmg=47969.6 iv2=932.659 KNOCKDOWN <- wall still walls [crash] BLOCK dmg=630 iv2=12.5 (x5, free taps) <- grind still free STAGED remainder, named in MECH4.NOTES.md: the gyro crunch feed (torque 0.4 / impulse 0.2 -- the gyro is a feel-wave stub), and the authentic InitialHit/Slide split pending the @0x4ac048 literal. Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/MECH.CPP | 201 +++++++++++++++++++++++++++++- restoration/source410/BT/MECH.HPP | 18 +++ 2 files changed, 218 insertions(+), 1 deletion(-) diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index b6907c95..4ef9bdd3 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -59,6 +59,9 @@ #if !defined(RANDOM_HPP) # include // Random -- the collision-rattle draw #endif +#if !defined(CULTURAL_HPP) +# include // CulturalIcon -- the crunch / crushable sentinel +#endif #include // DamageLookupTable -- the cylinder hit table #include // Player::VehicleDeadMessage -- the death notification #include // BTPlayer::ScoreMessage -- the kill credit @@ -256,6 +259,7 @@ Mech::Mech( legResetLatch = 0; bodyResetLatch = 0; limpModeOverride = 0; + collisionTemporaryState = 0; { const char *fl = getenv("BT_FORCE_LIMP"); if (fl != NULL && (*fl == '3' || *fl == '4')) @@ -781,6 +785,166 @@ Mech::~Mech() // Entity handler which routes damageZones[zone]->TakeDamage. //############################################################################# // +// +//############################################################################# +// ProcessCollision (binary @004abb40) -- the per-contact collision +// responder, overriding the engine's protected virtual. The base head is +// Mover::ProcessCollision verbatim (MOVER.CPP:1354); what the Mech adds is +// WHO it hit: +// +// * A separating contact -- one whose relative velocity points away from +// the surface -- is skipped entirely (the -1e-4 gate). No damage while +// pulling apart. +// +// * Another MECH takes the collision damage too (zone -1: the victim's +// own cylinder resolves it). Both sides of a ram hurt. +// +// * A CulturalIcon takes the crunch, and a CRUSHABLE one (no +// StoppingCollisionVolume flag) then sets the amount to the 0.00123f +// WALK-THROUGH SENTINEL -- Simulate's response block reads exactly that +// value as "the move stands": the mech drives THROUGH the prop while +// the prop takes the damage. Trees fall, walls do not. +//############################################################################# +// +void + Mech::ProcessCollision( + Scalar time_slice, + BoxedSolidCollision &collision, + const Point3D &old_position, + Damage *damage + ) +{ + Check(this); + Verify(time_slice > 0.0f); + Check(&collision); + Check_Pointer(damage); + + Scalar + penetration; + + if ( + !collisionVolume->ProcessCollision( + collision, + worldLinearVelocity, + lastCollisionList, + &damage->surfaceNormal, + &penetration) + ) + { + return; + } + + Max_Clamp(penetration, time_slice); + Scalar + ratio = penetration / time_slice, + elasticity = elasticityCoefficient, + friction = frictionCoefficient; + + Simulation + *owner_simulation = collision.GetTreeVolume()->GetOwningSimulation(); + + damage->damageAmount = + StaticBounce( + old_position, + time_slice, + ratio, + damage->surfaceNormal, + &elasticity, + minimumBounceSpeed, + &friction); + + Entity + *other = (Entity *)owner_simulation; + + if (other != NULL && other->IsDerivedFrom(Mover::ClassDerivations)) + { + Vector3D + relative; + relative.Subtract( + worldLinearVelocity, + ((Mover *)other)->GetWorldLinearVelocity()); + if (damage->surfaceNormal * relative < -1.0e-4f) + { + return; + } + if (other->IsDerivedFrom(Mech::ClassDerivations)) + { + Damage + ram = *damage; + ram.impactPoint = Point3D( + (collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f, + (collision.collisionSlice.minY + collision.collisionSlice.maxY) * 0.5f, + (collision.collisionSlice.minZ + collision.collisionSlice.maxZ) * 0.5f); + + Entity::TakeDamageMessage + hit( + Entity::TakeDamageMessageID, + sizeof(Entity::TakeDamageMessage), + GetEntityID(), + -1, + ram + ); + other->Dispatch(&hit); + } + } + + if (other != NULL && other->IsDerivedFrom(CulturalIcon::ClassDerivations)) + { + Logical + stopping = + ((CulturalIcon *)other)->IsStoppingCollisionVolume(); + + // + // The separating gate against a STATIC icon (its velocity is zero, + // which the 1995 compiler inlined away). + // + if (damage->surfaceNormal * worldLinearVelocity < -1.0e-4f) + { + return; + } + + // + // The crunch goes out BEFORE the sentinel overwrites the amount. + // + { + Damage + crunch = *damage; + crunch.impactPoint = Point3D( + (collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f, + (collision.collisionSlice.minY + collision.collisionSlice.maxY) * 0.5f, + (collision.collisionSlice.minZ + collision.collisionSlice.maxZ) * 0.5f); + + Entity::TakeDamageMessage + hit( + Entity::TakeDamageMessageID, + sizeof(Entity::TakeDamageMessage), + GetEntityID(), + -1, + crunch + ); + other->Dispatch(&hit); + } + + if (!stopping) + { + damage->damageAmount = 0.00123f; + } + } + + // + // The contact-state accumulator. [T3 simplification, per the decomp's + // dominant branch: the authentic InitialHit(1)/Slide(2) split keys the + // StaticBounce OUT coefficients against an unidentified literal + // (@0x4ac048); a resolved contact IS an impact, so accumulate 1 and + // capture the impact speed for the audio scales.] + // + if (collisionTemporaryState == 0) + { + collisionTemporaryState = 1; + collisionSpeed = worldLinearVelocity.Length(); + } +} + // //############################################################################# // DistributeCollisionDamage (binary @0049ffcc, reached ONLY from the damage @@ -1741,11 +1905,17 @@ void { Vector3D impact_velocity = worldLinearVelocity; + Vector3D + post_snap_velocity = worldLinearVelocity; + Point3D + post_snap_position = localOrigin.linearPosition; BoxedSolidCollisionList *collisions = GetCurrentCollisions(); Damage collision_damage; + collisionTemporaryState = 0; + if (collisions != NULL) { ProcessCollisionList( @@ -1753,7 +1923,36 @@ void &collision_damage); } - if (collision_damage.damageAmount > 0.0f) + // + // 0->1 on a fresh contact fires the impact sound; SetState only + // fires on change, so a pressed-against contact plays once. + // + collisionState.SetState((unsigned)collisionTemporaryState); + + if (collision_damage.damageAmount == 0.00123f) + { + // + // The crushable-icon WALK-THROUGH sentinel (ProcessCollision): + // the move STANDS -- restore the post-snap saves and undo the + // bounce. The prop took its crunch in the dispatch; the mech + // drives on through. (The gyro crunch feed -- torque 0.4 along + // the normalized force, upward impulse 0.2, binary + // @4aa81e/@4aa86c -- is STAGED with the gyro feel wave.) + // + worldLinearVelocity = post_snap_velocity; + localOrigin.linearPosition = post_snap_position; + localToWorld = localOrigin; + MoveCollisionVolume(); + collision_damage.damageAmount = 0.0f; + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[crash] CRUNCH (crushable) at (" + << post_snap_position.x << "," + << post_snap_position.z << ")" << endl << flush; + } + } + else if (collision_damage.damageAmount > 0.0f) { worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f); localOrigin.linearPosition = frame_start_position; diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index 9e911224..d43657f0 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -386,6 +386,24 @@ void DistributeCollisionDamage(Damage *damage); + // + // The per-contact collision responder (binary @004abb40) -- the + // override of the engine's protected virtual. Base resolution plus: + // the separating-contact gate, the mech-vs-mech ram dispatch, the + // CulturalIcon crunch, and the 0.00123f walk-through sentinel for + // crushable props. See MECH4.NOTES.md for the decode. + // + void + ProcessCollision( + Scalar time_slice, + BoxedSolidCollision &collision, + const Point3D &old_position, + Damage *damage); + + int collisionTemporaryState; // binary mech+0x44c -- the frame's + // contact accumulator; pushed into + // collisionState once per frame + static Scalar BodyClipFinished( Mech *mech,