BT410 5.3.125: the pilot punches out -- the effectiveness census and EjectPilot complete the authentic message roster, 6 of 6

The combat-effectiveness census (@0049fa1c, the master perf's opening
call) runs at the top of every Simulate frame: a mech is combat-
INEFFECTIVE when its working weapons fall below the ctor-hardcoded
minimum of 2 (+0x448, part_012.c:15749 -- a weapon works when not
destroyed and not a dry launcher, fire state 7 == our NoAmmoState
exactly), when no generator survives (state != 1 and generator state !=
GeneratorFailed -- the binary's +0x210 != 4, our enum value matching),
when the aggregate bank's coolant fraction drops below 0.05 (@0049fb50),
or when limping without the crouch cell held.  Census corrections over
the first read: 0xBBE is the AGGREGATE HEATSINK BANK (coolant fraction,
not generator charge) and 0xBC1 is the GENERATOR (count, not coolant
units) -- both re-pinned through our own classID map.

EjectPilot (@0049f854, id 0x19) consumes it: only a CRIPPLED mech that
is not already dying may eject -- mode 10 (EJECTED, the reason
IsMechDestroyed tests >= 9), the mapper's eject latch (+0x258), and a
self-dispatched explosive TakeDamage (type 2, zone -1 through the
cylinder resolver at the mech's own position).  Staged with the decode
recorded: the damage amount (the binary reads a bound cell through
mapper+0x208 -- no game-side writer exists, the mapper binding-loop
decode will name it) and the score notify (@00429078 + app+0x20 vcall,
event code 5).

Soak (pod_render_eject.conf, BT_FORCE_CRIPPLED + BT_PRESS_EJECT): the
census reads the real roster (weapons=6 generators=4 bank=1.0), the
press runs the whole chain -- eject, vital kill through the cylinder
table, VehicleDead, +5s respawn, HEALED at a fresh drop zone --
crash-sig 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-07 13:39:24 -05:00
co-authored by Claude Fable 5
parent 5769208ec0
commit 7b57f6b059
7 changed files with 337 additions and 9 deletions
@@ -0,0 +1,86 @@
# pod_render_norio.conf -- pod_render_rec with the RIO serial port OFF.
#
# The emulator log shows a steady stream of serial1 RX OVERRUN errors on
# the RIO pipe, and controls post their events at HighEventPriority
# UNCONDITIONALLY (CONTROLS.HPP:250) -- so a chattering RIO port would
# flood a priority the background pump always serves first, starving the
# priority-0 renderer events the load gate waits on. This conf tests that
# by removing the port entirely. Everything else is identical to the rec
# conf, so a launch here and a hang there isolates the RIO.
#
[sdl]
output=opengl
# higher,higher not highest: HIGH_PRIORITY_CLASS starved the host desktop;
# with the retry patches a rare dropout self-recovers (see gauge_rio.conf).
priority=higher,higher
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
# match the EMU8000s' native rate (no resample) and buffer ~60ms so brief
# emulation-thread stalls (RIO retry recovery) don't audibly chop
rate=44100
blocksize=1024
prebuffer=60
[serial]
# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's
# few-ms ACK deadline is met; plasma display on COM2 (real pod has both).
# VWE fork namedpipe backend (com0com/realport retired -- COM1/COM2 gone):
# DOSBox = pipe client (retry), vRIO/vPLASMA apps = servers; an unconnected
# pipe behaves as an unplugged cable so the mission still runs. serialnamedpipe.h
serial1=disabled
serial2=namedpipe pipe:vplasma
# live UNBUFFERED game output: DOS char devices are not buffered, so
# redirecting stdout to COM3 lands every line immediately. A normal
# '> file' redirect stays 0 bytes until the process exits, which hides
# all progress on a run that does NOT crash.
serial3=file file:C:\VWE\TeslaRel410\emulator\render-bridge\podlog.txt
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
c:
cd \REL410\BT
set VIDEOFORMAT=svga
rem production pod card init (PARAMETR.BAT:181-186): DIAGNOSE + AWEUTIL per
rem card -- AWEUTIL /S does the EMU8000 bring-up and DRAM detect the HMI SOS
rem driver relies on; skipping it left the cards uninitialized (silent).
rem aweutil /s SKIPPED for now: it verifies the AWE32 GM ROM, which the
rem emulated cards lack (hangs in a retry loop) -- restore once the ROM is
rem dumped from a real card. diagnose /s kept (passes, sets mixer config).
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO
rem attached; stdout redirected so mission-load progress survives kills.
set BT_JOINTS=1
set L4VIEWEXT=1
set BT_STACK_LOG=1
set BT_MECH_LOG=1
set BT_PRESS_EJECT=1
set BT_FORCE_CRIPPLED=1
set BT_GAUGE_LOG=1
set BT_FORCE_THROTTLE=0
set BT_FORCE_TURN=0
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r s n p
32rtm.exe -x
BTL4REC.EXE -egg testarn.egg > COM3
echo GAME-RC=%errorlevel% >> RC.TXT
32rtm.exe -u
echo ALPHA1-RUN-DONE
pause
+4
View File
@@ -417,6 +417,10 @@
//
int
GetCoolantAvailable() const { Check(this); return coolantAvailable; }
Scalar
GetCoolantLevel() const { Check(this); return coolantLevel; }
Scalar
GetThermalCapacity() const { Check(this); return thermalCapacity; }
Subsystem *
ResolveCoolingMaster() { Check(this); return linkedSinks.Resolve(); }
+195
View File
@@ -91,6 +91,7 @@ const Receiver::HandlerEntry
MESSAGE_ENTRY(Mech, SetBurningState),
MESSAGE_ENTRY(Mech, ClearBurningState),
MESSAGE_ENTRY(Mech, BalanceCoolant),
MESSAGE_ENTRY(Mech, EjectPilot),
MESSAGE_ENTRY(Mech, DuckRequest)
};
@@ -315,6 +316,9 @@ Mech::Mech(
realMaxSpeed = 0.0f; // the 0x15 share triplet (+0x7a0/4/8)
realMaxSpeedKnown = 0;
realMaxSpeedSent = 0;
combatIneffective = 0; // the @0049fa1c census cell (+0x414)
minimumWeaponCount = 2; // AUTHENTIC ctor constant (+0x448,
// part_012.c:15749: param_1[0x112] = 2)
collisionVolumeState.Initialize(2);
collisionVolumeState.SetLevel(1);
standingVolumeHeight = 0.0f;
@@ -969,6 +973,177 @@ void
}
}
//
//#############################################################################
// UpdateCombatEffectiveness -- binary @0049fa1c, the master perf's opening
// call. The binary keys the bank on classID 0xBBE and the generators on
// 0xBC1; we use the equivalent derivations ([T1] cosmetic). A dry missile
// launcher (fire state 7 = NoAmmoState, the binary's +0x364 read) does not
// count as a working weapon. BT_FORCE_CRIPPLED=1 pins the flag for the
// eject harness.
//#############################################################################
//
void
Mech::UpdateCombatEffectiveness()
{
Check(this);
int working_weapons = 0;
int working_generators = 0;
Scalar bank_fraction = 0.0f;
for (int s = 2; s < subsystemCount; ++s)
{
Subsystem *sub = subsystemArray[s];
if (sub == NULL)
{
continue;
}
if (sub->IsDerivedFrom(AggregateHeatSink::ClassDerivations))
{
HeatSink *bank = (HeatSink *)sub;
if (bank->GetThermalCapacity() > 0.0f)
{
bank_fraction =
bank->GetCoolantLevel() / bank->GetThermalCapacity();
}
}
else if (sub->IsDerivedFrom(Generator::ClassDerivations))
{
if (
(int)sub->GetSimulationState() != 1 &&
((Generator *)sub)->GeneratorStateOf()
!= Generator::GeneratorFailed
)
{
++working_generators;
}
}
else if (sub->IsDerivedFrom(MechWeapon::ClassDerivations))
{
if ((int)sub->GetSimulationState() != 1)
{
if (
sub->IsDerivedFrom(MissileLauncher::ClassDerivations) &&
((MechWeapon *)sub)->GetWeaponState()
== MechWeapon::NoAmmoState
)
{
// a dry launcher is not a working weapon
}
else
{
++working_weapons;
}
}
}
}
int limping_uncrouched = 0;
{
int mode = MovementMode();
if (mode == 3 || mode == 4)
{
MechControlsMapper *census_mapper =
(MechControlsMapper *)subsystemArray[0];
if (census_mapper == NULL || census_mapper->DuckCommand() == 0)
{
limping_uncrouched = 1;
}
}
}
int ineffective =
(
working_weapons < minimumWeaponCount ||
working_generators == 0 ||
bank_fraction < 0.05f ||
limping_uncrouched
) ? 1 : 0;
{
static int s_forceCrippled = -1;
if (s_forceCrippled < 0)
{
s_forceCrippled = (getenv("BT_FORCE_CRIPPLED") != NULL) ? 1 : 0;
}
if (s_forceCrippled)
{
ineffective = 1;
}
}
if (ineffective != combatIneffective && getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[census] combatIneffective -> " << ineffective
<< " (weapons=" << working_weapons
<< " generators=" << working_generators
<< " bank=" << bank_fraction << ")" << endl << flush;
}
combatIneffective = ineffective;
}
//
//#############################################################################
// EjectPilotMessageHandler -- binary @0049f854 (authentic name from the
// handler table). The pilot can only punch out of a CRIPPLED mech
// (mech+0x414, the census) that is not already dying. The eject: mode 10
// (EJECTED -- the reason IsMechDestroyed tests >= 9), the mapper's eject
// latch, and a self-dispatched explosive TakeDamage (type 2, zone -1 ->
// the cylinder resolver). STAGED pieces, recorded: the binary's damage
// amount reads *(*(mapper+0x208) + 0x1c) -- a bound cell whose identity
// needs the mapper ctor's binding-loop decode -- we use a guaranteed-kill
// constant; and the score notify (@00429078 accessor + the app+0x20 vcall,
// event code 5) lands with the score-plumbing wave.
//#############################################################################
//
void
Mech::EjectPilotMessageHandler(Receiver::Message *message)
{
Check(this);
Check_Pointer(message);
if (((int *)(message + 1))[0] <= 0 || combatIneffective == 0)
{
return;
}
if (Mech::InDeathTransition(this))
{
return;
}
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[eject] pilot out -- mech " << GetEntityID()
<< " ejects (crippled)" << endl << flush;
}
SetSimulationState(10);
MechControlsMapper *eject_mapper =
(MechControlsMapper *)subsystemArray[0];
if (eject_mapper != NULL)
{
eject_mapper->ejectLatch = 1;
}
Damage ejection;
ejection.damageType = Damage::ExplosiveDamageType; // binary: 2
ejection.damageAmount = 1.0e6f; // [T1] staged kill amount
ejection.damageForce = Vector3D(0.0f, 0.0f, 0.0f);
ejection.impactPoint = localOrigin.linearPosition;
Entity::TakeDamageMessage
eject_hit(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(),
-1,
ejection
);
Dispatch(&eject_hit);
}
//
//#############################################################################
// RedistributeCoolantShares -- binary @0049f788: total the priorities, give
@@ -1712,6 +1887,12 @@ void
{
Check(this);
//
// The effectiveness census is the master perf's FIRST call
// (@0049fa1c, before everything else, dead or alive).
//
UpdateCombatEffectiveness();
//
//-----------------------------------------------------------------------
// DEATH (binary UpdateDeathState @mech4, the once-per-death transition):
@@ -2295,6 +2476,20 @@ void
Dispatch(&balance_press);
}
//
// BT_PRESS_EJECT: one EjectPilot press (0x19) through the normal
// dispatch (pair with BT_FORCE_CRIPPLED=1 -- a healthy mech
// refuses the eject).
//
if (getenv("BT_PRESS_EJECT"))
{
ReceiverDataMessageOf<int> eject_press(
Mech::EjectPilotMessageID,
sizeof(ReceiverDataMessageOf<int>),
1
);
Dispatch(&eject_press);
}
//
// BT_PRESS_GEN=1..4: SelectGenerator<N> at the first Emitter
// (the generator panel re-tap); BT_PRESS_SEEK: ToggleSeekVoltage
// at the first Emitter (the seek dial).
+23
View File
@@ -704,6 +704,13 @@
void
RedistributeCoolantShares();
//
// The combat-effectiveness census (binary @0049fa1c; see the
// member block). Runs at the top of every Simulate frame.
//
void
UpdateCombatEffectiveness();
DamageLookupTable*
GetDamageLookupTable() const { Check(this); return damageLookupTable; }
@@ -982,6 +989,20 @@
int realMaxSpeedKnown;
int realMaxSpeedSent;
//
// The combat-effectiveness census (binary @0049fa1c -> mech+0x414,
// recomputed at the top of every master frame). INEFFECTIVE when:
// working weapons < minimumWeaponCount (mech+0x448, ctor-hardcoded
// 2; a weapon works when not destroyed and not a dry launcher --
// fire state NoAmmoState), OR no working generator (not destroyed,
// generator state != GeneratorFailed -- the binary's +0x210 != 4),
// OR the bank's coolant fraction < 0.05 (@0049fb50), OR limping
// without the crouch cell held. Consumer: the EjectPilot gate --
// you can only punch out of a crippled mech.
//
int combatIneffective;
int minimumWeaponCount;
void
DuckRequestMessageHandler(Receiver::Message *message);
void
@@ -992,6 +1013,8 @@
ClearBurningStateMessageHandler(Receiver::Message *message);
void
BalanceCoolantMessageHandler(Receiver::Message *message);
void
EjectPilotMessageHandler(Receiver::Message *message);
void
SetStandingCollisionVolume(); // binary @004ac04c... see .CPP
+21 -9
View File
@@ -316,12 +316,24 @@ Still to land from the roster:
live-verified streamed binding); revisit if a non-condenser valve
binding ever surfaces. ValveSetting attribute still publishes
coolantFlowScale (the displayed fractions are unchanged).
* **EjectPilot @0049f854** (head read): gated on payload > 0 AND
**mech+0x414** -- THE EFFECTIVENESS-CENSUS CONSUMER FOUND: you can only
eject a CRIPPLED mech (@0049fa1c's weapons/coolant/generator/limp
census) -- and !InDeathTransition; notifies via an app+0x2c accessor
(@00429078) + app+0x20 vcall (score/event, code 5, ownerID +0x18c);
sets **MODE 10 = EJECTED** (which is WHY IsMechDestroyed tests >= 9:
9 killed, 10 ejected); then builds a large effect/explosion struct --
tail still unread. Mode roster now: 0 default, 2 burning, 3/4 gimp
L/R, 5-8 falls, 9 killed, 10 ejected; writers of 5-8 still open.
* **EjectPilot @0049f854 -- LANDED 5.3.125** with the census
(@0049fa1c -> combatIneffective, the master perf's opening call).
Census truths, all pinned: 0xBBE = the AGGREGATE HEATSINK BANK (its
coolant fraction +0x12c/+0x128 < 0.05 @0049fb50 = crippled term; my
earlier "generator charge" reading was wrong), 0xBC1 = GENERATOR
(working = state != 1 && generator state != 4 = GeneratorFailed --
our enum matches), weapons by RTTI with dry launchers excluded
(+0x364 == 7 == our NoAmmoState exactly), minimum weapon count =
mech+0x448 = CTOR-HARDCODED 2 (part_012.c:15749), limping without
the crouch cell completes the roster of crippled terms. The eject:
mode 10 + mapper ejectLatch (+0x258) + self-dispatched explosive
TakeDamage (type 2, zone -1 -> cylinder resolver, impact = own
position). STAGED, recorded: the damage amount (binary reads
*(*(mapper+0x208)+0x1c), a bound/cached pointer with NO game-side
writer in the decomp -- needs the mapper ctor binding-loop decode)
and the score notify (@00429078 accessor + app+0x20 vcall, event
code 5 -- same poster idiom as the player-side @004c1044 throttle).
Dev kit: BT_FORCE_CRIPPLED=1 + BT_PRESS_EJECT=1
(pod_render_eject.conf). THE AUTHENTIC MESSAGE ROSTER IS NOW 6/6.
Mode roster: 0 default, 2 burning, 3/4 gimp L/R, 5-8 falls, 9
killed, 10 ejected; writers of 5-8 still open.
+1
View File
@@ -96,6 +96,7 @@ MechControlsMapper::MechControlsMapper(
)
{
duckCommand = 0;
ejectLatch = 0;
Check(owner);
//
// Prime the published control inputs to neutral. Per-frame interpretation
+7
View File
@@ -54,6 +54,13 @@
{ Check(this); return duckCommand; }
int duckCommand;
//
// The ejection latch (binary mapper+0x258, right beside the duck
// cell): EjectPilot sets it 1 as the pilot punches out; the cockpit
// side reads it (consumer still to be identified).
//
int ejectLatch;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//