Cockpit: the CONFIG-MODE weapon-regrouping session + the live-play fire fixes (tasks #6+#11)

TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and
live-verified (hold-configure -> tap fire button -> toggle membership ->
release-commit):
- MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton
  @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept)
  + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/
  MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch).
- Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration /
  +0x40,+0x44 AddOrErase evt/dir.  No "secondary vtable @0050f498", no
  "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP).
  L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the
  NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon
  latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO
  mapper implements the toggle (Thrustmaster no-ops = can't regroup).
- MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID
  (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the
  destination to &fireImpulse (@004b99a8).
- ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table;
  buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale
  was wrong).  Finding [T1]: the shipped binary NEVER enables this gauge (no
  SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable.
- Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon);
  BT_CONFIG_TEST scripts a headless verify.  @004afbc4 corrected to its real
  Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups).
- Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries
  (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong.

TASK #11 -- the user-reported live-play regressions (both real bugs):
- PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the
  victim on its OWN 0.3s cadence whenever fire was held -- desynced once the
  authentic recharges landed.  Retired (the real impact visual flows from
  each discharge via the messmgr SubmitExplosion).
- WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window
  (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike
  jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness,
  and the weapon sticks in Loading at level ~10000 forever (observed live).
  Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick
  -- also fixes the I^2R integral over-heating generators on big steps.
  NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap).
- Duty-cycle measured (max-rate autofire): generators equilibrate in the
  degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker
  never trips solo, PPCs sustain ~11.8-damage full-charge fire.  The slow
  kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's
  counters (this regrouping UI, generator reassignment, coolant valves) are
  the reconstruction queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 19:39:35 -05:00
co-authored by Claude Fable 5
parent 4e63a7b6c3
commit 092408041c
26 changed files with 748 additions and 209 deletions
+116 -71
View File
@@ -644,6 +644,7 @@ int gBTMissileTrigger = 0; // channel 3: missiles (key 3 / CTRL)
static int gBTLaserKey = 0; // raw key states (set by the keyboard poll)
static int gBTPPCKey = 0;
static int gBTMissileKey = 0;
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
// Damage: each shot dispatches a REAL Entity::TakeDamageMessage to the target. Now
// that the damage zones are constructed (mech.cpp Pass-3 zone build), the engine base
@@ -1524,6 +1525,10 @@ void
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0;
gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
// task #6: HOLD 'G' opens the config session on the selected
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
// held, the fire keys TOGGLE its group membership.
gBTConfigKey = focused && (pAsync('G') & dn) ? 1 : 0;
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
// damage dispatcher + the beam-visual keepalive)
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey) ? 1 : 0;
@@ -3001,6 +3006,34 @@ void
gBTHudPrimary = ((int)targetReticle.reticleElementMask & 0x20) != 0;
}
// task #6 ORDER FIX: the scripted block must run BEFORE the fire-push
// block below -- it writes gBTLaserKey/gBTConfigKey, which the earlier
// keyboard poll zeroes each frame while the window is unfocused; the
// old placement AFTER the push block meant the push never saw the
// scripted press.
// task #6 scripted verify (BT_CONFIG_TEST=1, headless): drives the same
// gBTConfigKey/gBTLaserKey states the keyboard would --
// t=[8,11) hold the configure button (session open)
// t=[9.5,9.7) one Trigger press edge -> ChooseButton toggle
// t>=13 0.2s Trigger pulses -> does the weapon fire now?
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_CONFIG_TEST"))
{
// frame-count driven (this env throttles background windows to a
// few fps -- wall/sim seconds are useless for scripting)
static int s_cfgFrame = 0;
++s_cfgFrame;
(void)dt;
// Schedule LATE (frame 2000+): the sim ticks many frames per second
// and the first-run schedule (30-120) fired before the enemy spawn
// completed -- targetInArc false -> the fire press never pushed.
gBTConfigKey = (s_cfgFrame >= 2000 && s_cfgFrame < 2600) ? 1 : 0;
int fireOn = (s_cfgFrame >= 2300 && s_cfgFrame < 2320) ? 1 : 0; // ONE press edge
if (s_cfgFrame >= 3000)
fireOn = ((s_cfgFrame / 60) & 1); // fire pulses after exit
gBTLaserKey = fireOn;
}
// E8: pulse the three fire channels per frame (1,0,1,0...) so each
// weapon sim's CheckFireEdge sees clean rising edges. UNCONDITIONAL --
// NOT inside the enemy block: firing needs only the world PICK (task
@@ -3067,6 +3100,77 @@ void
}
}
// task #6 dev harness: the config-session BRACKET. 'G' edge ->
// dispatch ConfigureMappables (id 9) press/release to the selected
// weapon -- the exact ReceiverDataMessageOf<ControlsButton> payload
// (+-(element+1)) the pod's streamed aux-panel EventMapping delivers.
// Everything downstream is the AUTHENTIC chain: EnterConfiguration
// flips the global mode to Mapping(0x8000) (normal fire mappings go
// dormant for the hold), the fire keys deliver ChooseButton (id 10)
// through the temp 0x8000 event mappings pushed by the SAME live-mode
// button path above, and the release restores NonMapping(0x10000).
if ((Entity *)this == application->GetViewpointEntity())
{
static int s_prevCfg = 0;
if (gBTConfigKey != s_prevCfg)
{
s_prevCfg = gBTConfigKey;
Subsystem *weapon = 0;
const char *slotEnv = getenv("BT_CONFIG_SLOT");
int wantSlot = (slotEnv != 0) ? atoi(slotEnv) : -1;
for (int s = 1; s < GetSubsystemCount(); ++s) // slot 0 = the MAPPER, never a weapon
{
Subsystem *sub = GetSubsystem(s);
if (sub == 0)
continue;
// Pick by WEAPON classID (0xBC8 Emitter / 0xBCA ProjectileWeapon /
// 0xBCE GaussRifle / 0xBD0 MissileLauncher / 0xBD4 PPC). The old
// (simulationFlags & 0x8) heuristic matched the MAPPER at slot 0:
// its MakeViewpointEntity resource is stack-built with only
// name/classID/size set, so its flags are STACK GARBAGE -- and a
// config message dispatched to the mapper lands in the mapper's
// OWN id space (the aux-preset Fail trap) = the abort popup.
int cid = (int)sub->GetClassID();
if (wantSlot >= 0 ? (s == wantSlot)
: (cid == 0xBC8 || cid == 0xBCA || cid == 0xBCE
|| cid == 0xBD0 || cid == 0xBD4))
{
weapon = sub;
break;
}
}
if (weapon == 0 && getenv("BT_FIRE_LOG"))
{
DEBUG_STREAM << "[config] NO WEAPON matched (count="
<< GetSubsystemCount() << ")" << std::endl;
for (int s2 = 0; s2 < GetSubsystemCount() && s2 < 33; ++s2)
{
Subsystem *p = GetSubsystem(s2);
if (p != 0)
DEBUG_STREAM << " [cfg-scan] " << s2 << " " << p->GetName()
<< " flags=0x" << std::hex << (int)p->simulationFlags
<< std::dec << std::endl;
}
}
if (weapon != 0)
{
const int kElem = 0x0e; // an aux-panel element (as authored)
ReceiverDataMessageOf<ControlsButton> msg(
9 /*MechWeapon::ConfigureMappablesMessageID (mechweap.hpp)*/,
sizeof(ReceiverDataMessageOf<ControlsButton>),
(ControlsButton)(gBTConfigKey ? (kElem + 1) : -(kElem + 1)));
weapon->Dispatch(&msg);
if (getenv("BT_FIRE_LOG"))
DEBUG_STREAM << "[config] " << (gBTConfigKey ? "ENTER" : "EXIT")
<< " session on " << weapon->GetName()
<< " mode now=0x" << std::hex
<< (int)application->GetModeManager()->GetModeMask()
<< std::dec << std::endl;
}
}
}
{
// The VICTIM under the boresight (task #46: any peer mech, not just
// the solo gEnemyMech). Range/log/fire all key off THIS mech.
@@ -3115,11 +3219,6 @@ void
gAimHits = 0; gAimNoRay = 0; gAimGround = 0;
}
if (gFireCooldown > 0.0f)
gFireCooldown -= dt;
const int fireWanted = gBTDrive.fireForced || gBTDrive.fire;
// Resolve the "explode" effect resource once.
if (gExplodeReady == 0)
{
@@ -3143,72 +3242,18 @@ void
}
}
// FIRE + DAMAGE: only vs a MECH under the boresight (terrain/sky picks
// draw beams but deal no damage). Dispatch routes to the picked victim
// -- for a REPLICANT this is a cross-pod hit: Entity::Dispatch reroutes
// the TakeDamageMessage to the owning master over the wire (task #46).
if (fireWanted && victim != 0 && targetInArc && gFireCooldown <= 0.0f
&& anyWeaponInRange && gExplodeReady == 1)
{
gFireCooldown = kFireCooldown;
++gShotCount;
// Impact = the PICKED HULL POINT -> the STEP-6 cylinder lookup
// resolves the zone under the boresight (aimed fire).
Point3D impact = hotPoint;
Origin exp_origin = ((Mech *)victim)->localOrigin;
exp_origin.linearPosition = impact; // at the hit point
// IMPACT FRAME: -Z aimed from the victim at the shooter (the .PFX
// hit spray convention; yaw = atan2(ddx, ddz)). [T0]
exp_origin.angularPosition =
EulerAngles(0.0f, (Scalar)atan2((double)ddx, (double)ddz), 0.0f);
Explosion::MakeMessage exp_message(
Explosion::MakeMessageID,
sizeof(Explosion::MakeMessage),
(Entity::ClassID)RegisteredClass::ExplosionClassID,
EntityID::Null,
gExplodeRes,
Explosion::DefaultFlags,
exp_origin,
victim->GetEntityID(), // entity hit
GetEntityID()); // shooter (this mech)
Explosion *shot = Explosion::Make(&exp_message);
if (shot)
{
Register_Object(shot);
DEBUG_STREAM << "[fire] SHOT #" << gShotCount
<< " -> explosion at target (range=" << range << ")\n" << std::flush;
}
// --- DAMAGE (real, STEP 6): dispatch UNAIMED (zone == -1) with the
// beam's world entry point. Mech::TakeDamageMessageHandler
// resolves the zone from the cylinder table; the base handler
// routes it to Mech__DamageZone::TakeDamage. Dispatch on a
// REPLICANT is rerouted by Entity::Dispatch to the owning
// master over the wire -- the cross-pod damage path.
// DAMAGE DELIVERY MOVED (task #8): the AUTHENTIC submitter is
// each firing weapon's own SendDamageMessage (@004b9728, called
// from Emitter::FireWeapon with the authored per-shot
// damagePortion + damageForce + impact) into the
// SubsystemMessageManager. This block keeps only the
// presentation roles: the boresight pick feeds the target
// slots the weapons read (mech+0x388/0x37C/0x38C), the [fire]
// beam-entry explosion visual above, and the shot log. The
// flat kShotDamage build + shooter-side score moved with the
// submission (mechweap.cpp).
if (getenv("BT_FIRE_LOG") && ((Mech *)victim)->damageZoneCount > 0)
{
DEBUG_STREAM << "[damage] beam hit " << (void*)victim
<< (((Mech *)victim)->GetInstance() == ReplicantInstance
? " (REPLICANT -> cross-pod via the weapon submit)" : "")
<< "\n" << std::flush;
}
// DEATH effects fire at the VICTIM's own death transition
// (UpdateDeathState, task #42) -- MP-correct: the master runs it.
}
// THE PHANTOM-SHOT BLOCK IS RETIRED (task #11, user-reported): this
// bring-up stand-in spawned an explosion at the victim on its OWN
// 0.3s cadence whenever the fire key was down -- matched the old
// one-frame recharge, but once the AUTHENTIC electrical model
// landed (task #10: 2-5s recharges + the generator thermal breaker)
// it kept painting hits while no weapon was discharging ("enemy
// shows fire even though I'm not producing lasers"). The authentic
// impact visual flows from each REAL discharge: Emitter::FireWeapon
// -> SendDamageMessage -> SubsystemMessageManager explosion
// bundling -> SubmitExplosion (messmgr.cpp). This block now keeps
// only its presentation roles above (boresight pick -> the target
// slots the weapons read, and the [target] log).
}
}