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:
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -96,6 +96,7 @@ MechControlsMapper::MechControlsMapper(
|
||||
)
|
||||
{
|
||||
duckCommand = 0;
|
||||
ejectLatch = 0;
|
||||
Check(owner);
|
||||
//
|
||||
// Prime the published control inputs to neutral. Per-frame interpretation
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user