diff --git a/emulator/render-bridge/pod_render_eject.conf b/emulator/render-bridge/pod_render_eject.conf new file mode 100644 index 00000000..5cc936a7 --- /dev/null +++ b/emulator/render-bridge/pod_render_eject.conf @@ -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 + diff --git a/restoration/source410/BT/HEAT.HPP b/restoration/source410/BT/HEAT.HPP index 2311ce1f..e465ddb7 100644 --- a/restoration/source410/BT/HEAT.HPP +++ b/restoration/source410/BT/HEAT.HPP @@ -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(); } diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index 543b27c3..ff949da0 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -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 eject_press( + Mech::EjectPilotMessageID, + sizeof(ReceiverDataMessageOf), + 1 + ); + Dispatch(&eject_press); + } + // // BT_PRESS_GEN=1..4: SelectGenerator at the first Emitter // (the generator panel re-tap); BT_PRESS_SEEK: ToggleSeekVoltage // at the first Emitter (the seek dial). diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index 1e1675c3..3661f747 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -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 diff --git a/restoration/source410/BT/MECH4.NOTES.md b/restoration/source410/BT/MECH4.NOTES.md index 79b0f33a..93520491 100644 --- a/restoration/source410/BT/MECH4.NOTES.md +++ b/restoration/source410/BT/MECH4.NOTES.md @@ -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. diff --git a/restoration/source410/BT/MECHMPPR.CPP b/restoration/source410/BT/MECHMPPR.CPP index 59db8ef4..fc422361 100644 --- a/restoration/source410/BT/MECHMPPR.CPP +++ b/restoration/source410/BT/MECHMPPR.CPP @@ -96,6 +96,7 @@ MechControlsMapper::MechControlsMapper( ) { duckCommand = 0; + ejectLatch = 0; Check(owner); // // Prime the published control inputs to neutral. Per-frame interpretation diff --git a/restoration/source410/BT/MECHMPPR.HPP b/restoration/source410/BT/MECHMPPR.HPP index ab7d1311..0d1365fd 100644 --- a/restoration/source410/BT/MECHMPPR.HPP +++ b/restoration/source410/BT/MECHMPPR.HPP @@ -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 //