From c5537a1247178f8ebfc4dfd182b9b70285867610 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Tue, 28 Jul 2026 14:02:41 -0500 Subject: [PATCH] BT_SELF_DAMAGE: kill your own pilot in solo, so the RESPAWN family is bench-testable at last Nothing could kill the LOCAL pilot on a bench. BT_MP_FORCE_DMG only targets REPLICANTS (mech4.cpp:4884 skips anything else), a solo dummy never shoots back, and every other damage path needs a second live pod. So the entire respawn family -- #70 torso twist after respawn, #22 ammo/weapons not resetting, #55 coolant/heat/generator restore, #57 the respawn latch -- could only ever be tested by asking a playtester to die. That is why they are all still open or awaiting-verification. BT_SELF_DAMAGE= dispatches an unaimed TakeDamage at our OWN mech through the same virtual Entity::Dispatch a real beam hit uses, so the cylinder hit-location lookup, the zone cascade, the vital-subsystem kill and the whole authentic death -> 5s -> DropZone -> Mech::Reset cycle all run for real. No state is poked. =60 kills a fresh MadCat in about a minute. It LATCHES OFF at the first death. That matters more than it sounds: live damage knocks the power bus down, which is exactly the signal a respawn test wants to read. The first run of this harness produced a torso electrical state flapping 4 <-> 1 and I nearly filed it as #70 -- it was the harness still shooting me. One death, then silence, then measure. FIRST RESULT -- #70 does NOT reproduce in solo, but something near it does. After the respawn the MadCat's torso recovers fully: rate back to 0.872665 (the authored 50 deg/s), elec=4 Ready. Twist is not broken by a respawn. However the post-respawn mission shows INTERMITTENT power dropouts that a pristine mission never shows: pristine control (no damage, no death): 48/48 samples elec=4, ZERO dips post-respawn (harness silent): 95/97 elec=4, 2 dips to elec=1 A dip zeroes effectiveTwistRate (torso.cpp:570), so a pilot mid-turn feels the twist cut out -- plausibly what "torso twist stops working after a respawn" actually is: not a permanent stop but a stutter. Both dips are transient and the tail of the run is steady, so it is not a stuck state. Cause not yet found; the watched subsystem's own electrical level (wElec) dips with it, while the generator holds a full 10000V against a 5000V brownout threshold -- so it is NOT the brownout path in PowerWatcher::UpdateWatch. Filed here as the next thread to pull, with the harness that makes it reproducible in one solo run. Co-Authored-By: Claude Opus 5 (1M context) --- game/reconstructed/mech4.cpp | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 21e2d2e..4e28719 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -5541,6 +5541,49 @@ void gBTGenSelKey = pulse ? s_gsID : 0; } + // SELF-DAMAGE HARNESS (BT_SELF_DAMAGE=, off by default). + // The whole RESPAWN family -- torso twist after respawn (#70), ammo/weapon + // reset (#22), coolant/heat/generator restore (#55), the respawn latch + // (#57) -- was untestable on a bench because nothing can kill the LOCAL + // pilot in solo: BT_MP_FORCE_DMG only targets REPLICANTS, and a solo + // dummy never shoots back. This dispatches an unaimed TakeDamage at our + // OWN mech once a second, through the SAME virtual Entity::Dispatch path + // a real beam hit uses -- so the cylinder hit-location lookup, the zone + // cascade, the vital-subsystem kill and the authentic death/respawn cycle + // all run for real. Nothing is poked directly. + // e.g. BT_SELF_DAMAGE=40 kills a fresh mech in well under a minute. + // ONE DEATH ONLY: latch off as soon as we die, so everything observed + // AFTER the respawn is the respawn's own doing and not the harness still + // shooting us. (Live damage knocks the power bus down, which is exactly + // the signal a respawn test is trying to read.) + static int s_sdSpent = 0; + if (IsMechDestroyed()) + s_sdSpent = 1; + if ((Entity *)this == application->GetViewpointEntity() + && getenv("BT_SELF_DAMAGE") && !s_sdSpent + && !IsMechDestroyed() && damageZoneCount > 0) + { + static float s_sdAccum = 0.0f; + s_sdAccum += dt; + if (s_sdAccum >= 1.0f) + { + s_sdAccum = 0.0f; + Damage dmg; + dmg.damageType = Damage::ExplosiveDamageType; + float want = (float)atof(getenv("BT_SELF_DAMAGE")); + dmg.damageAmount = (want > 0.0f) ? want : 20.0f; + dmg.burstCount = 1; + dmg.impactPoint = localOrigin.linearPosition; + Entity::TakeDamageMessage td( + Entity::TakeDamageMessageID, + sizeof(Entity::TakeDamageMessage), + GetEntityID(), -1 /*unaimed -> cylinder table resolves the zone*/, dmg); + DEBUG_STREAM << "[selfdmg] " << dmg.damageAmount + << " to own mech (unaimed)" << std::endl << std::flush; + Dispatch(&td); + } + } + // task #13 scripted verify (BT_VALVE_TEST=1): one MoveValve press at // frame ~600 -- Condenser1's valveState cycles 1 -> 5, so the flow // redistribution gives it 5/10 of the total coolant flow ([valve] log