//===========================================================================// // File: btl4gau2.cpp // // Project: BattleTech Brick: Gauge Renderer Manager // // Contents: Cockpit instrument library, part 2 -- composite panel gauges // // (see btl4gau2.hpp). // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // 02/22/96 CPB Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1996, Virtual World Entertainment, Inc. All rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary (BTL4OPT.EXE) and RE-HOSTED onto the // authentic 1995 engine (MUNGA GAUGE/GAUGREND, MUNGA_L4 L4GAUGE) for the // BC++4.52 build. Behaviour follows the Ghidra pseudo-C @004c6798..@004c9b50 // (+ VehicleSubSystems::Make @004cbaf0); per-method @ADDR evidence is cited // inline. This TU links between btl4gaug.obj and btl4gau3.obj (BTL4.MAK). // // DATA BINDINGS (the re-host rule: bind by NAME through the engine's // Simulation::GetAttributePointer, or through a documented accessor on the // reconstructed subsystems -- NEVER a raw 1995 byte offset): // LIVE PercentDone / TriggerState / WeaponState / RearFiring / ChargeLevel // -- published by name (mechweap.cpp / emitter.cpp attribute tables). // LIVE generator voltage / number / on-lamp -- PoweredSubsystem:: // ResolveVoltageSource() -> Generator::MeasuredVoltage() / // GetGeneratorNumber(), and the PUBLIC Generator::outputVoltage / // generatorOn members (powersub.hpp). // LIVE destroyed / failed -- Simulation::GetSimulationState() == // MechSubsystem::DestroyedState, MechSubsystem:: // GetSubsystemDamageLevel() >= 1.0 (the PPC-dial polarity truth // source, mechdmg.cpp DistributeCriticalHit). // LIVE jam -- MechWeapon::GetWeaponState() == MechWeapon::JammedState // (retires the binary's raw *(subsys+0x364) read). // NAME-DEFERRED CurrentTemperature / DegradationTemperature / // FailureTemperature / CoolantMassLeakRate / HeatLoad / // CoolantAvailable / CurrentSeekVoltageIndex / Min / Max / // SeekVoltage -- bound by name; the source410 heat / powersub / // seek attribute waves do not publish them yet, so the resolve // misses and the widget reads the unbound zero cell. Each binding // goes LIVE automatically the day its table row lands. // INERT (see the stub ledger at each site) -- seek-voltage response // sampler, cooling-loop lamp frame, linked-sink temperatures, // ammo count / feed state, aux-screen number / placement / label. // // Recovered constant-pool values: // 0x43660000=230.0f / 0x433b0000=187.0f SeekVoltageGraph x/y scale // 0x461c3c00=9999.0f BecameActive "force redraw" sentinel // 0x463b8000=12000.0f generator max voltage // 0x42c80000=100.0f HeatSinkCluster numeric scale (_DAT_004c8df0) // _DAT_004c92e0=0.99f warning threshold // _DAT_004c6bdc=1200.0f / _DAT_004c6be0=12000.0f seek-curve step/limit // ld80 @004c6bd0 / @004c6d74 = 8.3333333333333e-05 (exactly 1/12000) -- // the voltage->y normaliser (y = v * (1/12000) * yScale) // #include #pragma hdrstop #if !defined(BTL4GAU2_HPP) # include #endif #if !defined(BTL4MODE_HPP) # include #endif #if !defined(L4CTRL_HPP) # include #endif #if !defined(L4WRHOUS_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(MECHSUB_HPP) # include #endif #if !defined(POWERSUB_HPP) # include #endif #if !defined(MECHWEAP_HPP) # include #endif #if !defined(PROJWEAP_HPP) # include #endif #if !defined(HEAT_HPP) # include #endif #include static const Scalar SeekVoltageXScale = 230.0f; // 0x43660000 static const Scalar SeekVoltageYScale = 187.0f; // 0x433b0000 static const Scalar RedrawSentinel = 9999.0f; // 0x461c3c00 static const Scalar GeneratorMaxVoltage = 12000.0f; // 0x463b8000 static const Scalar HeatNumericBase = 100.0f; // 0x42c80000 // SeekVoltageGraph plot constants (capstone disasm of @004c6934): static const Scalar SeekCurveStep = 1200.0f; // _DAT_004c6bdc (10 segments) static const Scalar SeekCurveLimit = 12000.0f; // _DAT_004c6be0 (y-axis full scale) static const Scalar SeekCurveYNorm = (Scalar)(1.0 / 12000.0); // ld80 @004c6bd0/@004c6d74 // Recovered .data tuning constants (read from the optimised image). static const Scalar HeatLoadScale = 100.0f; // _DAT_004c8df0 static const Scalar WeaponWarnThreshold = 0.99f; // _DAT_004c92e0 // //############################################################################# // UNBOUND-FEED CELLS. Every engine widget copies through its value pointer // each frame (GaugeConnectionDirectOf hard-verifies and derefs the source // at construction -- a NULL feed is a crash, not a blank). Any binding whose // attribute row / accessor is not reconstructed yet is pointed at one of // these zero cells instead: the widget draws, the value is static, and the // binding site documents what will light it up. The cells are read-only to // every consumer. //############################################################################# // static Scalar UnboundScalarSource = 0.0f; static int UnboundIntegerSource = 0; // // FUN_0041bfc0 == the engine's name-keyed attribute-pointer resolve. The // authentic Simulation::GetAttributePointer(const char*) (SIMULATE.CPP:444) // already returns NULL on a miss, so no NullAttribute normalisation is // needed here -- only the NULL-subsystem guard. // static void * AttributePointerOf( void *subsystem, const char *name ) { if (subsystem == NULL) { return NULL; } return ((Subsystem *)subsystem)->GetAttributePointer(name); } // // Feed resolvers: attribute by name, or the unbound zero cell on a miss. // (The composite ctors use the RAW AttributePointerOf only where the binary // itself gated behaviour on a missing attribute.) // static Scalar * ScalarAttributeOf( void *subsystem, const char *name ) { Scalar *pointer = (Scalar *)AttributePointerOf(subsystem, name); return (pointer != NULL) ? pointer : &UnboundScalarSource; } static int * IntegerAttributeOf( void *subsystem, const char *name ) { int *pointer = (int *)AttributePointerOf(subsystem, name); return (pointer != NULL) ? pointer : &UnboundIntegerSource; } // //############################################################################# // Accessor bridges onto the reconstructed subsystems (replace the WinTesla // port's extern "BT*" bridge functions with direct reads through the // documented public interfaces). //############################################################################# // // // The binary's IsDerivedFrom(0x50f4bc) gate (FUN_0041a1a4): PoweredSubsystem's // ClassDerivations. Gates the power-bus children (the 1995 ctor gated them on // the "InputVoltage" attribute resolving, which is the same population). // static Logical IsPoweredSubsystemDerived(Subsystem *subsystem) { if (subsystem == NULL) { return False; } return subsystem->IsDerivedFrom(PoweredSubsystem::ClassDerivations); } // // The powering generator, resolved through the NAMED connection (powersub.hpp // ResolveVoltageSource) -- the databinding-rule replacement for the binary's // ResolveLink(AttributePointerOf("InputVoltage")) walk. NULL when the // subsystem is unpowered, the tap is unresolved, or the source is not a // Generator. // static Generator * ResolvedGeneratorOf(void *subsystem) { Subsystem *source; if (!IsPoweredSubsystemDerived((Subsystem *)subsystem)) { return NULL; } source = ((PoweredSubsystem *)subsystem)->ResolveVoltageSource(); if (source == NULL || !source->IsDerivedFrom(Generator::ClassDerivations)) { return NULL; } return (Generator *)source; } // // The destroyed test == binary *(subsystem+0x40) == 1: the simulation state // word, 1 == MechSubsystem::DestroyedState (the same hard-failure gate the // weapon simulations check). // static Logical SubsystemDestroyed(Subsystem *subsystem) { if (subsystem == NULL) { return False; } return subsystem->GetSimulationState() == MechSubsystem::DestroyedState; } // // PPC-dial POLARITY truth source (kept on re-host): a subsystem is FAILED // when its own damage zone saturates (DistributeCriticalHit destroys at // >= 1.0, mechdmg.cpp) -- read through the public accessor. // static Scalar SubsystemDamageLevelOf(Subsystem *subsystem) { if (subsystem == NULL || !subsystem->IsDerivedFrom(MechSubsystem::ClassDerivations)) { return 0.0f; } return ((MechSubsystem *)subsystem)->GetSubsystemDamageLevel(); } // // The jam test (retires the binary's raw *(subsys+0x364)==5 -- our recon // publishes the weapon-state alarm through GetWeaponState, mechweap.hpp). // static Logical WeaponJammedOf(Subsystem *subsystem) { if (subsystem == NULL || !subsystem->IsDerivedFrom(MechWeapon::ClassDerivations)) { return False; } return ((MechWeapon *)subsystem)->GetWeaponState() == MechWeapon::JammedState ? True : False; } // // INERT STUB -- the seek-voltage RESPONSE sampler. The 1995 subsystem // published a voltage-response virtual (binary subsystem vtbl slot 15: // Emitter @004bb42c = sqrt(seekPower(v)/2.0e8), Myomers @004b8f94 = // sqrt(speed(v)*3.6/350)). Neither response model is reconstructed on the // source410 subsystems, so the plot samples a flat response: the graph still // performs its box-clear / erase duties (the shared Eng bit-plane's top-box // eraser, Gitea #10 finding A) and the destroyed bitmap still shows, but the // curve is a stand-in until the emitter/myomer response wave lands. // static Scalar SeekResponseOf( Subsystem *, Scalar ) { return 0.0f; } //########################################################################### // File-private GaugeConnection classes for the composite-cluster lamps. // Each derives GaugeConnection, stores dst/src, and overrides Update() // (the per-frame sampler Gauge::Update drives -- GAUGE.CPP:569). //########################################################################### // // The cooling-loop lamp's frame: the loop number of the Condenser this // subsystem's sink is plumbed to, or 0 ("OFF") when it has no coolant, // no loop master, or a master that is not a Condenser. // static int CoolingLoopFrameOf(void *source) { Subsystem *subsystem = (Subsystem *)source; if (subsystem == NULL || !subsystem->IsDerivedFrom(HeatSink::ClassDerivations)) { return 0; } HeatSink *sink = (HeatSink *)subsystem; if (sink->GetCoolantAvailable() != 1) { return 0; } Subsystem *master = sink->ResolveCoolingMaster(); if (master == NULL || !master->IsDerivedFrom(Condenser::ClassDerivations)) { return 0; } return ((Condenser *)master)->GetCondenserNumber(); } // // CoolingLoopConnection -- @004c3134 ctor / @004c31a0 sampler (vt 0x518ea8). // The binary: when the source subsystem's coolant loop is available // (HeatSink coolantAvailable == 1) it follows the linked cooling master (the // "HeatSink" link == linkedSinks) and shows its Condenser loop number // (condenserNumber == the image-strip frame); otherwise 0. // // LIVE (5.3.36): heat.hpp now publishes GetCoolantAvailable() / // ResolveCoolingMaster() / GetCondenserNumber(), so the lamp follows the // binary exactly -- the same shape as PowerSourceConnection below. A sink // with no coolant, no loop master, or a master that is not a Condenser // reads frame 0 ("OFF"), which is what the box showed for every panel // while this was stubbed. // class CoolingLoopConnection : public GaugeConnection { public: CoolingLoopConnection(int *destination, void *source): GaugeConnection(0), source(source), destination(destination) {} void Update() // @004c31a0 { *destination = CoolingLoopFrameOf(source); } protected: void *source; // @0x10 this[4] int *destination; // @0x14 this[5] }; // // PowerSourceConnection -- @004c31ec ctor / @004c3258 sampler (vt 0x518e9c). // The binary resolved the subsystem's InputVoltage link and read the // generator's number (1..4 -> the btpbus generator-letter frame A..D; // 0 == OFF). LIVE: resolved through the named voltageSource connection // (ResolveVoltageSource / GetGeneratorNumber, powersub.hpp) -- the // databinding-rule rewrite that fixed the frozen bus lamps. // class PowerSourceConnection : public GaugeConnection { public: PowerSourceConnection(int *destination, void *source): GaugeConnection(0), source(source), destination(destination) {} void Update() // @004c3258 { Generator *generator = ResolvedGeneratorOf(source); *destination = (generator != NULL) ? generator->GetGeneratorNumber() : 0; } protected: void *source; // @0x10 int *destination; // @0x14 }; // // GeneratorVoltageConnection -- @004c3288 ctor / @004c32f4 sampler (vt // 0x518e90). The binary resolved the subsystem's InputVoltage link and // copied its output voltage; 0 if unresolved. Drives the eng-page // generator-voltage bar (ScalarBarGauge) + the MyomerCluster seek graph. // LIVE: ResolveVoltageSource()->MeasuredVoltage() (powersub.hpp). // class GeneratorVoltageConnection : public GaugeConnection { public: GeneratorVoltageConnection(Scalar *destination, void *source): GaugeConnection(0), source(source), destination(destination) {} void Update() // @004c32f4 { Generator *generator = ResolvedGeneratorOf(source); *destination = (generator != NULL) ? generator->MeasuredVoltage() : 0.0f; } protected: void *source; // @0x10 Scalar *destination; // @0x14 }; //########################################################################### //########################################################################### // SeekVoltageGraph @004c6798 // (fully reconstructed from the capstone disasm of @004c6798/@004c6920/ // @004c6934/@004c6be4/@004c6c30/@004c6c6c -- Ghidra had dropped every // x87 argument.) //########################################################################### //########################################################################### // // @004c6798 -- ctor (vtable PTR_FUN_0051a1fc). GraphicGauge base, AddRefs // the "destroyed" bitmap, resolves the four SeekVoltage attribute POINTERS // off the weapon subsystem BY NAME (a miss reads NULL and gates the tick / // cursor draws -- the emitter/myomer seek rows are not published by the // source410 tables yet, so today they gate CLOSED), sets the view XOR draw // op + the port extent (0x97,0x80,0x17d,0x13b) and stashes the 230x187 plot // scale. // SeekVoltageGraph::SeekVoltageGraph( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, Entity *subsystem_in, AttributeAccessor *seek_voltage_pointer, const char *destroyed_image, Logical draw_cursor, const char *identification_string ): GraphicGauge(rate, mode_mask, renderer_in, 0xFFFF /*owner*/, graphics_port_number, identification_string) { Check_Pointer(this); //------------------------------------------------------------ // Retain the persistent resource string and AddRef the bitmap // (the callers pass string literals; released in the dtor). //------------------------------------------------------------ destroyedImage = (char *)destroyed_image; ((L4Warehouse *)renderer_in->warehousePointer)-> bitMapBin.Get(destroyedImage); // AddRef //------------------------------------------------------------ // The four seek attributes, by NAME (NULL on a miss -- gated). //------------------------------------------------------------ currentSeekIndexPtr = (int *)AttributePointerOf(subsystem_in, "CurrentSeekVoltageIndex"); // this[0x25] minSeekIndexPtr = (int *)AttributePointerOf(subsystem_in, "MinSeekVoltageIndex"); // this[0x26] maxSeekIndexPtr = (int *)AttributePointerOf(subsystem_in, "MaxSeekVoltageIndex"); // this[0x27] seekVoltageTablePtr = (Scalar *)AttributePointerOf(subsystem_in, "SeekVoltage"); // this[0x28] //------------------------------------------------------------ // The live-cursor voltage source; Execute derefs it every // frame, so a missed binding is normalised to the zero cell. //------------------------------------------------------------ liveVoltage = (Scalar *)seek_voltage_pointer; // this[0x29] if (liveVoltage == NULL) { liveVoltage = &UnboundScalarSource; } subsystem = (Subsystem *)subsystem_in; // this[0x2A] //------------------------------------------------------------ // @004c6798: SetOperation(Xor) + SetPositionWithinPort -- the // XOR op is what lets DrawTicks/DrawCursorBox erase by redraw. //------------------------------------------------------------ localView.SetOperation(GraphicsDisplay::Xor); // vtbl+0x0C localView.SetPositionWithinPort(0x97, 0x80, 0x17d, 0x13b); // vtbl+0x08 xScale = SeekVoltageXScale; // this[0x2C] yScale = SeekVoltageYScale; // this[0x2D] drawCursor = draw_cursor; // this[0x2F] destroyedShown = 0; // this[0x2E] // // (The binary leaves this[0x2B]/[0x30..0x32] uninitialised -- the // activation sentinel forces the first replot, which seeds them before // any read. Seeded here for determinism.) // previousVoltage = RedrawSentinel; // this[0x2B] cursorX = -1; // this[0x30] cursorY = -1; // this[0x31] tickIndexShown = -1; // this[0x32] Check_Fpu(); } // // @004c68ac -- dtor. Release the destroyed bitmap; base chain implicit. // SeekVoltageGraph::~SeekVoltageGraph() { Check(this); L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; warehouse->bitMapBin.Release(destroyedImage); } Logical SeekVoltageGraph::TestInstance() const { return GraphicGauge::TestInstance(); } // // @004c6920 -- BecameActive: previousVoltage = 9999.0f (poison the cached // response so the next Execute clears + replots the whole box -- THE top-box // eraser on a page switch; the binary body writes ONLY this[0x2B]). // void SeekVoltageGraph::BecameActive() { previousVoltage = RedrawSentinel; // @0xAC force full redraw } // // @004c6be4 -- the full-box erase: SetOperation(Replace), SetColor(0), // MoveToAbsolute(0,0), DrawFilledRectangleToAbsolute(1000,1000) -- clipped to // the view = the whole (0x97,0x80)-(0x17d,0x13b) top data box painted black. // NOTE: leaves the view in Replace mode (the polyline plot relies on that; // Execute restores Xor). // void SeekVoltageGraph::ClearGraph() { localView.SetOperation(GraphicsDisplay::Replace); // vtbl+0x0C localView.SetColor(0); // vtbl+0x18 localView.MoveToAbsolute(0, 0); // vtbl+0x24 localView.DrawFilledRectangleToAbsolute(1000, 1000); // vtbl+0x48 } // // @004c6c30 -- the live cursor: an 8x8 filled square centred on // (cursorX,cursorY), drawn in the view's current (Xor) op so a second call // erases it. // void SeekVoltageGraph::DrawCursorBox() { localView.MoveToAbsolute(cursorX - 4, cursorY - 4); // vtbl+0x24 localView.DrawFilledRectangleToRelative(8, 8); // vtbl+0x4C } // // @004c6c6c -- the per-gear tick marks (XOR). For each seek index i in // [*min..*max]: V = seekVoltage[i]; x = Round(response(V) * xScale); // y = Round(V * (1/12000) * yScale) [ld80 @004c6d74]. The gear the // highlight was last drawn at (this[0x32]) gets the full L (axis->point-> // axis); the rest get 10-pixel axis stubs. Called in pairs to move the // highlight (XOR erase + redraw). // void SeekVoltageGraph::DrawTicks() { int i, last; // // (Guard kept from the decode: the binary derefs min/max unguarded -- // its call sites only guarantee current+table non-null. On the re-host // the seek attributes may legitimately be unpublished, so the guard is // load-bearing.) // if (minSeekIndexPtr == NULL || maxSeekIndexPtr == NULL || seekVoltageTablePtr == NULL) { return; } last = *maxSeekIndexPtr; // this[0x27] for (i = *minSeekIndexPtr; i <= last; i++) // this[0x26] { Scalar gearVoltage = seekVoltageTablePtr[i]; // this[0x28][i] int x = Round(SeekResponseOf(subsystem, gearVoltage) * xScale), y = Round(gearVoltage * SeekCurveYNorm * yScale); if (i == tickIndexShown) // this+0xC8 { localView.MoveToAbsolute(0, y); // vtbl+0x24 localView.DrawLineToAbsolute(x, y); // vtbl+0x30 localView.DrawLineToAbsolute(x, 0); } else { localView.MoveToAbsolute(0, y); localView.DrawLineToAbsolute(10, y); localView.MoveToAbsolute(x, 0); localView.DrawLineToAbsolute(x, 10); } } } // // @004c6934 -- Execute (the seek-voltage response plot; full x87 recovery). // // DESTROYED branch (simulationState == DestroyedState): once, clear the box // and draw the centred "destroyed" bitmap (edestryd.pcc) at // ((Round(xScale)-w)/2, (Round(yScale)-h)/2), color 0xFF. // // LIVE branch: // * on the destroyed->alive edge, call BecameActive (own vtbl slot) to // poison the cache; // * change-test: sample the response at 12000 V and compare with the cached // this[0x2B]; on change (or the 9999 activation sentinel): CLEAR // (@004c6be4 -- the ghost eraser), SetColor(0xFF), MoveTo(0,0), then the // polyline v = 0,1200,...,10800; reset the cursor slots to -1, restore // SetOperation(Xor), and draw the gear ticks at the current index; // * tick maintenance: when the current seek index moved, XOR-erase the old // highlight + redraw at the new (two DrawTicks calls); // * cursor (draw_cursor pages only): XOR-erase the old box (if any), sample // at the LIVE voltage (*this[0x29]) and draw the new 8x8 box. // void SeekVoltageGraph::Execute() { Scalar yStep, response, v; if (SubsystemDestroyed(subsystem)) { if (destroyedShown == 0) { destroyedShown = 1; // this[0x2E] ClearGraph(); // @004c6be4 L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; BitMap *image = warehouse->bitMapBin.Get(destroyedImage); if (image != NULL) // (guard; binary derefs unchecked) { localView.MoveToAbsolute( // vtbl+0x24 centre the bitmap (Round(xScale) - image->Data.Size.x) >> 1, (Round(yScale) - image->Data.Size.y) >> 1); localView.SetColor(0xff); // vtbl+0x18 localView.DrawBitMap(0, image, // vtbl+0x54 0, 0, image->Data.Size.x - 1, image->Data.Size.y - 1); } warehouse->bitMapBin.Release(destroyedImage); } return; } if (destroyedShown != 0) { destroyedShown = 0; BecameActive(); // own vtbl slot (@004c6920) } yStep = SeekCurveYNorm * yScale; // fld tbyte @004c6bd0 * this[0x2D] //------------------------------------------------------------ // change-test: the response curve's value at max voltage. //------------------------------------------------------------ response = SeekResponseOf(subsystem, GeneratorMaxVoltage); if (previousVoltage != response) // this[0x2B] { previousVoltage = response; ClearGraph(); // @004c6be4 -- erases the WHOLE box localView.SetColor(0xff); // vtbl+0x18 (Replace mode from the clear) localView.MoveToAbsolute(0, 0); // vtbl+0x24 for (v = 0.0f; v < SeekCurveLimit; v += SeekCurveStep) { localView.DrawLineToAbsolute( // vtbl+0x30 Round(SeekResponseOf(subsystem, v) * xScale), Round(v * yStep)); } cursorX = -1; // this[0x30] cursorY = -1; // this[0x31] localView.SetOperation(GraphicsDisplay::Xor); // vtbl+0x0C back to Xor if (currentSeekIndexPtr != NULL && seekVoltageTablePtr != NULL) { tickIndexShown = *currentSeekIndexPtr; // this[0x32] DrawTicks(); // @004c6c6c } } //------------------------------------------------------------ // gear-change tick maintenance (XOR pair: erase old, draw new). //------------------------------------------------------------ if (currentSeekIndexPtr != NULL && seekVoltageTablePtr != NULL) { int currentIndex = *currentSeekIndexPtr; if (currentIndex != tickIndexShown) { DrawTicks(); // erase at the old index tickIndexShown = currentIndex; DrawTicks(); // draw at the new } } //------------------------------------------------------------ // live-voltage cursor (drawCursor pages: the emitter/PPC graphs). //------------------------------------------------------------ if (drawCursor != 0) // this[0x2F] { Scalar live; if (cursorX >= 0) { DrawCursorBox(); // XOR-erase the old box } live = *liveVoltage; // *this[0x29] cursorX = Round(SeekResponseOf(subsystem, live) * xScale); cursorY = Round(live * yStep); DrawCursorBox(); // @004c6c30 } } //########################################################################### // ConfigMapGauge @004c6d80 //########################################################################### // // The 4-slot state table (DAT_00518eb4, PE-recovered {y, button} pairs; the // button numbers resolve to the authentic L4CTRL.HPP names). Blit x = 0xc. // struct ConfigMapSlot { int y; int button; }; static const ConfigMapSlot configMapSlot[4] = { { 0x0d, LBE4ControlsManager::ButtonJoystickPinky }, // 0x45 { 0x25, LBE4ControlsManager::ButtonJoystickThumbLow }, // 0x46 { 0x3d, LBE4ControlsManager::ButtonJoystickTrigger }, // 0x40 { 0x55, LBE4ControlsManager::ButtonJoystickThumbHigh }, // 0x47 }; // // @004c6d80 -- ctor (vtable PTR_FUN_0051a1b8). GraphicGauge base; AddRefs the // joystick base bitmap "btjoy.pcc" + the four config-state pixmaps // cm_off/cm_other/cm_only/cm_both.pcc; sets the port origin (x,y); // linkedEntity = 0 (armed later by the LinkToEntity broadcast). // ConfigMapGauge::ConfigMapGauge( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, Entity *subsystem_in, const char *identification_string ): GraphicGauge(rate, mode_mask, renderer_in, 0 /*owner*/, graphics_port_number, identification_string) { int i; Check_Pointer(this); joystickImage = (char *)"btjoy.pcc"; // @0x98 this[0x26] stateImage[0] = (char *)"cm_off.pcc"; // @0x9C this[0x27] stateImage[1] = (char *)"cm_other.pcc"; // @0xA0 this[0x28] stateImage[2] = (char *)"cm_only.pcc"; // @0xA4 this[0x29] stateImage[3] = (char *)"cm_both.pcc"; // @0xA8 this[0x2A] localView.SetOrigin(x, y); // vtbl+0x10 (this+0x48) // // @0x94 is NOT a colour and @004c6ee0 is NOT an uncalled SetColor -- it // is the virtual GaugeBase::LinkToEntity override (vtbl slot 9, verified // against the binary vtable @0051a1b8). The engine broadcasts // LinkToEntity(viewpointEntity) to every gauge when the viewpoint binds, // which arms this gate -- the trigger-config joystick DOES render in the // shipped game. // linkedEntity = NULL; // @0x94 this[0x25] subsystem = subsystem_in; // @0x90 this[0x24] L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; warehouse->bitMapBin.Get(joystickImage); // AddRef for (i = 0; i < 4; i++) { warehouse->pixelMap8Bin.Get(stateImage[i]); // AddRef } } // // @004c6e54 -- dtor: release btjoy bitmap + 4 pixmaps; base chain implicit. // ConfigMapGauge::~ConfigMapGauge() { int i; Check(this); L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; warehouse->bitMapBin.Release(joystickImage); for (i = 0; i < 4; i++) { warehouse->pixelMap8Bin.Release(stateImage[i]); } } Logical ConfigMapGauge::TestInstance() const { return GraphicGauge::TestInstance(); } // // @004c6ee0 -- LinkToEntity (GaugeBase vtbl slot 9): linkedEntity (this[0x25]) // = the viewpoint entity. Broadcast by GaugeRenderer::LinkToEntity when the // viewpoint binds; arms the Execute gate. // void ConfigMapGauge::LinkToEntity(Entity *entity) // @004c6ee0 { linkedEntity = entity; } // // @004c6ef4 -- BecameActive: force a full redraw (dirty=1, previousState=-1). // void ConfigMapGauge::BecameActive() { int i; dirty = True; // @0xBC this[0x2F] for (i = 0; i < 4; i++) { previousState[i] = -1; // @0xAC this[0x2B..0x2E] } } // // @004c6f1c -- Execute. Once linked (linkedEntity != 0): on the dirty flag, // blit the base joystick bitmap; then for each of the 4 config-map slots read // the fire button's map state and, if changed, blit the matching cm_* pixmap. // // The sampler is LBE4ControlsManager::buttonGroup[btn].GetMapState (the // engine ControlsUpdateManager, CONTROLS.HPP): unmapped=0 / mappedByOthers=1 // / mappedByMe=2 / both=3 == the cm_off/cm_other/cm_only/cm_both frame index // -- the per-weapon lamp column over the btjoy joystick image. The feed the // streamed direct mapping registered is the weapon's TriggerState attribute // (== &fireImpulse, published by name in mechweap.cpp) with a 0 message id; // the mode is the authentic BTL4ModeManager::ModeNonMapping (== the binary's // 0x10000 literal -- the persistent groups). // void ConfigMapGauge::Execute() { int slot; if (linkedEntity == NULL) // @004c6f28: not yet linked to the viewpoint { return; } L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; if (dirty) { BitMap *base = warehouse->bitMapBin.Get(joystickImage); dirty = False; if (base != NULL) { localView.SetColor(0xFF); // vtbl+0x18 // // issue #42 (doubled joystick), kept on re-host: the binary // repositions to the view ORIGIN before the blit. Without it the // FIRST draw worked (fresh cursor) and every forced redraw (the // DISPLAY page round-trip's BecameActive) blitted at the state // loop's last cursor (0xc, 0x55) -- the offset ghost joystick. // localView.MoveToAbsolute(0, 0); // vtbl+0x24 localView.DrawBitMapOpaque(0 /*background*/, 0 /*rotation*/, base); } warehouse->bitMapBin.Release(joystickImage); } LBE4ControlsManager *controls = (LBE4ControlsManager *)application->GetControlsManager(); if (controls == NULL || subsystem == NULL) { return; } // // The mapping feed: the weapon's fire-button destination. (The stored // pointer's dynamic type is Subsystem -- cast back before use.) // void *destination = AttributePointerOf((Subsystem *)subsystem, "TriggerState"); for (slot = 0; slot < 4; slot++) { int state = controls->buttonGroup[configMapSlot[slot].button].GetMapState( destination, // &fireImpulse (Receiver *)(Subsystem *)subsystem, // the subsystem itself 0, // message id (weapons use 0) (ModeMask)BTL4ModeManager::ModeNonMapping); // the persistent groups if (state != previousState[slot]) { PixelMap8 *pix; previousState[slot] = state; pix = warehouse->pixelMap8Bin.Get(stateImage[state]); if (pix != NULL) { localView.MoveToAbsolute(0xc, configMapSlot[slot].y); // vtbl+0x24 localView.DrawPixelMap8(0 /*opaque*/, 0 /*rotation*/, pix); } warehouse->pixelMap8Bin.Release(stateImage[state]); } } } //########################################################################### // OneOfSeveral data-driven lamps @004c70a4 / @004c7160 //########################################################################### // // @004c70a4 -- AnimatedSubsystemLamp ctor (vtable PTR_FUN_0051a174): // OneOfSeveral (fromImageStrip=1) + a CoolingLoopConnection feeding the // INHERITED `selected` frame from the subsystem's cooling-loop state. // Otherwise inherits OneOfSeveral::Execute. // // COLOR FIX kept (verified vs binary @004c70a4 / caller @004c8140): the // binary passes bg=0xff, fg=0 (the MFD on-colour) -- an earlier transcript // dropped them to 0,0 and the loop/gen boxes drew black-on-black. // AnimatedSubsystemLamp::AnimatedSubsystemLamp( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, const char *image, int columns, int rows, void *subsystem, const char *identification_string ): OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number, x, y, True /*fromImageStrip*/, image, 0xff, 0, columns, rows, identification_string) { GaugeConnection *connection; // // SHADOW-FIELD FIX kept: `selected` is the INHERITED OneOfSeveral member // (@0xAC) -- a local redeclaration froze the lamp at frame 0. // selected = 0; connection = new CoolingLoopConnection(&selected, subsystem); // @004c3134 Register_Object(connection); AddConnection(connection); } AnimatedSubsystemLamp::~AnimatedSubsystemLamp() // @004c7134 (base chain) { } // // @004c7160 -- AnimatedSourceLamp ctor (vtable PTR_FUN_0051a130): identical // shape but a PowerSourceConnection (@004c31ec) drives the frame from the // resolved power source's generator number (the btpbus A..D letter lamp). // AnimatedSourceLamp::AnimatedSourceLamp( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, const char *image, int columns, int rows, void *source, const char *identification_string ): OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number, x, y, True, image, 0xff, 0, columns, rows, identification_string) { GaugeConnection *connection; selected = 0; connection = new PowerSourceConnection(&selected, source); // @004c31ec Register_Object(connection); AddConnection(connection); } AnimatedSourceLamp::~AnimatedSourceLamp() // @004c71f0 { } //########################################################################### // ScalarBarGauge @004c721c / @004c72ac //########################################################################### // // @004c721c -- the "slotOf" variant (vtable PTR_FUN_0051a0e4). Chains the // engine BarGraphBitMapScalar with no auto-connection (value=NULL -- the // engine ctor documents that a derived object then builds the connection), // then adds a GeneratorVoltageConnection writing the inherited // WipeGaugeScalar::currentValue (@0xB4) from the resolved voltage source. // Generator-voltage bar, range 0..12000. dtor @004c733c = base chain. // ScalarBarGauge::ScalarBarGauge( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int left, int bottom, int right, int top, const char *tile_image, int foreground_color, int background_color, WipeGaugeScalar::Direction direction, Scalar min, Scalar max, void *voltage_source, const char *identification_string ): BarGraphBitMapScalar(rate, mode_mask, renderer_in, 0 /*owner*/, graphics_port_number, left, bottom, right, top, tile_image, foreground_color, background_color, direction, min, max, (Scalar *)NULL /*no auto-connection*/, identification_string) { GaugeConnection *connection; connection = new GeneratorVoltageConnection(¤tValue, voltage_source); // @004c3288 Register_Object(connection); AddConnection(connection); } // // @004c72ac -- the plain-Scalar variant. Same bar body but a // GaugeConnectionDirectOf from a DIRECT Scalar* (the generator's own // output-voltage member) instead of a GeneratorVoltageConnection. // GeneratorCluster is its caller. Disambiguated from the void* overload by // the Scalar* argument (exact match here, std-conversion to void*). // ScalarBarGauge::ScalarBarGauge( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int left, int bottom, int right, int top, const char *tile_image, int foreground_color, int background_color, WipeGaugeScalar::Direction direction, Scalar min, Scalar max, Scalar *value_source, const char *identification_string ): BarGraphBitMapScalar(rate, mode_mask, renderer_in, 0 /*owner*/, graphics_port_number, left, bottom, right, top, tile_image, foreground_color, background_color, direction, min, max, (Scalar *)NULL /*no auto-connection*/, identification_string) { GaugeConnection *connection; connection = new GaugeConnectionDirectOf(0, ¤tValue, value_source); Register_Object(connection); AddConnection(connection); } ScalarBarGauge::~ScalarBarGauge() // @004c733c (base chain) { } //########################################################################### // shared cluster helpers //########################################################################### // // FUN_004c2ec4 -- draw a clipped port-background bitmap: MoveTo(x,y), fetch // the tile from the BitMap cache, opaque-blit it, release. (Used by the // cluster ctors to paint the panel's static frame image into the MFD port.) // static void DrawPortBackground( GraphicsView *view, L4Warehouse *warehouse, int x, int y, const char *tile ) { BitMap *bmp; view->MoveToAbsolute(x, y); // vtbl+0x24 bmp = warehouse->bitMapBin.Get(tile); if (bmp != NULL) { view->DrawBitMapOpaque(0 /*background*/, 0 /*rotation*/, bmp); } warehouse->bitMapBin.Release(tile); } // // A default per-child load-distribution rate (the binary ctors call the // NextNthTierGaugeRate allocators FUN_004442b8/dc/290). // static inline GaugeRate ChildRate() { return Gauge::NextThirdTierGaugeRate(); } // //############################################################################# // AUX-SCREEN ASSIGNMENT (RECONSTRUCTION STAND-IN). The 1995 build read the // authored auxScreenNumber / auxScreenPlacement / auxScreenLabel off the // powered subsystem (binary sub+0x1dc/0x1e0/0x1e4, streamed from // PoweredSubsystem__SubsystemResource). Our reconstructed subsystems free // the resource block after construction (MECHSUB.HPP `resource` -- NEVER // deref) and keep NO live copy of the aux fields, so: // * the screen NUMBER is assigned SEQUENTIALLY in roster order over the // displayable subsystem classes (deterministic, and identical between // VehicleSubSystems::Make and PrepEngrScreen::BecameActive); // * the strip-image PLACEMENT has no source -> the panel backdrop child is // not built; // * the label .pcc has no source -> the label blits are skipped. // The ClassID dispatch itself is authentic. All three feeds go live when an // aux-screen wave lands live members on PoweredSubsystem. //############################################################################# // static Logical IsPanelSubsystem(Subsystem *subsystem) { switch (subsystem->GetClassID()) { case RegisteredClass::SensorClassID: // 0xBC3 case RegisteredClass::MyomersClassID: // 0xBC6 case RegisteredClass::EmitterClassID: // 0xBC8 case RegisteredClass::PPCClassID: // 0xBD4 case RegisteredClass::ProjectileWeaponClassID: // 0xBCD case RegisteredClass::MissileLauncherClassID: // 0xBD0 case RegisteredClass::GaussRifleClassID: // 0xBCE (warned-unsupported) return True; default: return False; } } // // The AUTHORED aux screen (binary sub+0x1dc). PoweredSubsystem caches the // streamed block at ctor time (its resource memory dies with the Mech // ctor's stream buffer), so the panels land on the screens the artists // assigned -- not in roster order. A non-powered panel subsystem (or a // model that authored nothing) falls back to the sequential walk. // static int AuxScreenAssignmentOf( Entity *mech, Subsystem *subsystem ) { int i, position = 0, count = mech->GetSubsystemCount(); if (subsystem->IsDerivedFrom(PoweredSubsystem::ClassDerivations)) { int authored = ((PoweredSubsystem *)subsystem)->GetAuxScreenNumber(); if (authored > 0) { return authored; } } for (i = 1; i < count; i++) // slot 0 = the controls mapper { Subsystem *candidate = mech->GetSubsystem(i); if (candidate == NULL || !IsPanelSubsystem(candidate)) { continue; } ++position; if (candidate == subsystem) { return position; // 1-based aux screen } } return 0; } //########################################################################### //########################################################################### // GeneratorCluster (config keyword "GeneratorCluster" -- the 4 generator // engineering panels, buttons 9-12 / GeneratorA..D; vtable PTR_FUN_0051a0a0) // Make @004c7368 / ctor @004c746c / dtor @004c7778. //########################################################################### //########################################################################### // // CFG shape (L4GAUGE.CFG:5046): NO leading rate token; p[0] = the mode mask // (the panel's own rate is hard-wired 0xFFFF). KEYWORD + PARAMETER LIST ARE // PARSE-CRITICAL -- the interpreter assigns opcodes by table position and // Save()s each parameter after the opcode byte. // MethodDescription GeneratorCluster::methodDescription = { "GeneratorCluster", GeneratorCluster::Make, { { ParameterDescription::typeModeMask, NULL }, // p[0] mode mask (ModeAlwaysActive) { ParameterDescription::typeString, NULL }, // p[1] generator name (GeneratorA..D) { ParameterDescription::typeString, NULL }, // p[2] background pcc { ParameterDescription::typeColor, NULL }, // p[3] extra/leak colorA { ParameterDescription::typeString, NULL }, // p[4] temperature tile { ParameterDescription::typeColor, NULL }, // p[5] temp bg color { ParameterDescription::typeColor, NULL }, // p[6] temp fill color { ParameterDescription::typeString, NULL }, // p[7] voltage tile { ParameterDescription::typeColor, NULL }, // p[8] voltage fg color { ParameterDescription::typeString, NULL }, // p[9] leak pcc { ParameterDescription::typeColor, NULL }, // p[10] leak colorB { ParameterDescription::typeString, NULL }, // p[11] lamp A pcc { ParameterDescription::typeString, NULL }, // p[12] lamp B pcc { ParameterDescription::typeColor, NULL }, // p[13] lamp on color { ParameterDescription::typeColor, NULL }, // p[14] lamp off color PARAMETER_DESCRIPTION_END } }; // // @004c7368 -- Make. Resolve the named generator (config p[1], e.g. // "GeneratorA"); warn + bail if absent. Native Make pattern: plain new + // Register_Object (the binary's operator new(0xb4) + in-place construct). // Logical GeneratorCluster::Make( int display_port_index, Vector2DOf position, Entity *entity, GaugeRenderer *gauge_renderer ) { ParameterDescription *p = methodDescription.parameterList; Check(gauge_renderer); Subsystem *subsystem = entity->FindSubsystem(p[1].data.string); if (subsystem == NULL) { DEBUG_STREAM << "Subsystem " << p[1].data.string << " not found\n"; return False; } GeneratorCluster *gauge = new GeneratorCluster( p[0].data.modeMask, // mode (ModeAlwaysActive) (L4GaugeRenderer *)gauge_renderer, 0, // owner_ID (binary hard-wires 0) display_port_index, // graphics_port_number position.x, position.y, // panel origin subsystem, // resolved generator p[2].data.string, // background image p[3].data.color, // extra/leak colorA p[4].data.string, // temperature tile p[5].data.color, // temp bg color p[6].data.color, // temp fill color p[7].data.string, // voltage tile p[8].data.color, // voltage fg color p[9].data.string, // leak image p[10].data.color, // leak colorB p[11].data.string, // lampA image p[12].data.string, // lampB image p[13].data.color, // lamp on color p[14].data.color, // lamp off color "GeneratorCluster"); Register_Object(gauge); return True; } // // @004c746c -- ctor (vtable PTR_FUN_0051a0a0). GraphicGauge base (own rate // hard-wired 0xFFFF; mode from config). Resolves the generator's feeds, // interns + AddRefs the panel background pixmap, builds the 5 child gauges // into this panel's port at offsets relative to the panel origin (x,y). // // FEEDS: outputVoltage / generatorOn are PUBLIC Generator members // (powersub.hpp:413/411) -- LIVE. The temperatures and the coolant leak are // bound by NAME (unpublished by the source410 heat wave -> unbound cells // until it lands). The leak gauge's `third` (binary *(subsys+0x150), a raw // 1995-layout read) is INERT 0 -- the field is stored-but-unused by the base // BitMapInverseWipe::Execute, so the constant is behaviour-neutral. // GeneratorCluster::GeneratorCluster( ModeMask mode_mask, L4GaugeRenderer *renderer_in, int owner_ID, int graphics_port_number, int x, int y, Subsystem *subsystem_in, const char *background_image, int extra_color, const char *temp_tile, int temp_bg_color, int temp_fill_color, const char *voltage_tile, int voltage_fg_color, const char *leak_image, int leak_color_b, const char *lampA_image, const char *lampB_image, int lamp_on_color, int lamp_off_color, const char *identification_string ): GraphicGauge((GaugeRate)Gauge::gaugeRate_A, mode_mask, renderer_in, owner_ID, graphics_port_number, identification_string) { Check_Pointer(this); L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; GaugeRate rate1 = ChildRate(), // VertTwoPartBar rate2 = ChildRate(), // ScalarBarGauge rate3 = ChildRate(), // LeakGauge rate4 = ChildRate(); // both lamps Scalar *currentTemp = ScalarAttributeOf(subsystem_in, "CurrentTemperature"), *degradeTemp = ScalarAttributeOf(subsystem_in, "DegradationTemperature"), *failTemp = ScalarAttributeOf(subsystem_in, "FailureTemperature"), *coolantLeak = ScalarAttributeOf(subsystem_in, "CoolantMassLeakRate"); // // LIVE generator feeds through the public members (a config typo could // name a non-generator, so the derivation is checked). // Scalar *outputVolt = &UnboundScalarSource; int *generatorOn = &UnboundIntegerSource; if (subsystem_in != NULL && subsystem_in->IsDerivedFrom(Generator::ClassDerivations)) { outputVolt = &((Generator *)subsystem_in)->outputVoltage; generatorOn = &((Generator *)subsystem_in)->generatorOn; // binary subsys+0x1D4 } localView.SetOrigin(x, y); // vtbl+0x10 secondaryColor = leak_color_b; // @0x94 (stored; unread here) backgroundImage = nameCopy(background_image); // @0x90 (config string is transient) warehouse->pixelMap8Bin.Get(backgroundImage, False); // AddRef //------------------------------------------------------------ // child 1: vertical two-part temperature bar //------------------------------------------------------------ temperatureBar = new VertTwoPartBar(rate1, mode_mask, renderer_in, graphics_port_number, x + 5, y + 0x29, x + 0xC, y + 0x5C, temp_tile, temp_bg_color, temp_fill_color, extra_color, currentTemp, degradeTemp, failTemp, "VertTwoPartBar"); Register_Object(temperatureBar); //------------------------------------------------------------ // child 2: generator-voltage bar 0..12000 (@004c72ac variant) //------------------------------------------------------------ voltageBar = new ScalarBarGauge(rate2, mode_mask, renderer_in, graphics_port_number, x + 0x18, y + 0x29, x + 0x1C, y + 0x5C, voltage_tile, voltage_fg_color, extra_color, WipeGaugeScalar::wipeUp, 0.0f, GeneratorMaxVoltage, outputVolt, "GeneratorVoltage(scalar)"); Register_Object(voltageBar); //------------------------------------------------------------ // child 3: coolant-leak inverse-wipe (third INERT, frames=3) //------------------------------------------------------------ leakGauge = new BitMapInverseWipe(rate3, mode_mask, renderer_in, graphics_port_number, x, y + 0x27, leak_image, extra_color, leak_color_b, 0 /*third: binary *(subsys+0x150); stored-unused*/, 3 /*frames*/, coolantLeak, "LeakGauge"); Register_Object(leakGauge); //------------------------------------------------------------ // child 4/5: two status lamps (generatorOn -- binary subsys+0x1D4) //------------------------------------------------------------ lampA = new TwoState(rate4, mode_mask, renderer_in, (unsigned)owner_ID, graphics_port_number, x + 0xC, y + 2, lampA_image, lamp_on_color, lamp_off_color, generatorOn, "TwoState"); Register_Object(lampA); lampB = new TwoState(rate4, mode_mask, renderer_in, (unsigned)owner_ID, graphics_port_number, x + 2, y + 2, lampB_image, lamp_on_color, lamp_off_color, generatorOn, "TwoState"); Register_Object(lampB); if (getenv("BT_VIS_LOG")) DEBUG_STREAM << "[gen] port=" << graphics_port_number << " x=" << x << " y=" << y << " lampA='" << lampA_image << "' at " << (x + 0xC) << "," << (y + 2) << " lampB='" << lampB_image << "' at " << (x + 2) << "," << (y + 2) << " on=" << *generatorOn << " col0=" << lamp_on_color << " col1=" << lamp_off_color << "\n" << flush; subsystem = subsystem_in; // @0xAC _reserved0xB0 = 0; // @0xB0 (binary leaves uninit; zeroed) Check_Fpu(); } // // @004c7778 -- dtor. Release the background pixmap + free the interned name. // The 5 children are NOT deleted here (faithful to the binary: each // self-registered with the renderer via the base Gauge ctor and is owned // there -- deleting would double-free with the renderer chain teardown). // GeneratorCluster::~GeneratorCluster() { Check(this); L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; warehouse->pixelMap8Bin.Release(backgroundImage); Unregister_Pointer(backgroundImage); delete[] backgroundImage; backgroundImage = NULL; } // // BecameActive -- paint the panel's static background pixmap. MUST override // the default (which self-inactivates the panel); a real paint keeps the // panel active so its 5 self-registered child gauges keep drawing. // void GeneratorCluster::BecameActive() { L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; PixelMap8 *bg = warehouse->pixelMap8Bin.Get(backgroundImage); // already AddRef'd by the ctor if (bg != NULL) { localView.MoveToAbsolute(0, 0); // vtbl+0x24 localView.DrawPixelMap8(True, 0, bg, 0, 0, // vtbl+0x5C (opaque, full image) bg->Data.Size.x - 1, bg->Data.Size.y - 1); } warehouse->pixelMap8Bin.Release(backgroundImage); // balance this Get } // // Execute -- the panel itself has no per-frame content (the temp / voltage / // leak / lamp children each Execute independently), but Execute MUST be // overridden: the engine base Gauge::Execute is Fail("not overridden") // (GAUGE.CPP:598; the 1995 base was a no-op). // void GeneratorCluster::Execute() { } //########################################################################### //########################################################################### // PrepEngrScreen @004c7b30 Make / @004c7bf0 ctor //########################################################################### //########################################################################### // // FUN_004c79c8 -- draw the three status-label cells (+ optional top erase // box). Each cell blits its label .pcc, or blanks the cell. The absolute // cell rectangles are the binary's exact coordinates. (The C-cell compound // blank: the binary uses an extra GraphicsView relative-fill (99,10); a // plain fill at the same cell is the marked faithful-enough substitute.) // static void DrawStatusCells( GraphicsView *view, L4Warehouse *warehouse, int topBox, const char *A, const char *B, const char *C ) { if (topBox) { view->SetColor(0); view->MoveToAbsolute(0x97, 0x80); view->DrawFilledRectangleToAbsolute(0x17d, 0x13b); } if (A == NULL) { view->SetColor(0); view->MoveToAbsolute(0x15a, 0x145); view->DrawFilledRectangleToAbsolute(0x17f, 0x159); } else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x15a, 0x145, A); } if (B == NULL) { view->SetColor(0); view->MoveToAbsolute(0x85, 0x58); view->DrawFilledRectangleToAbsolute(0x17f, 0x7c); } else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x85, 0x58, B); } if (C == NULL) { view->SetColor(0); view->MoveToAbsolute(0x2a, 0x34); view->DrawFilledRectangleToAbsolute(0x9f, 0x2d); // (compound-blank substitute) } else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x2a, 0x34, C); } } // // CFG shape (L4GAUGE.CFG:4595): mode, int screen(1..12), then 7 .pcc names // (font + 6 labels). 9 params. PARSE-CRITICAL. // MethodDescription PrepEngrScreen::methodDescription = { "prepEngr", PrepEngrScreen::Make, { { ParameterDescription::typeModeMask, NULL }, // p[0] mode (ModeMFD*Eng*) { ParameterDescription::typeInteger, NULL }, // p[1] screen number 1..12 { ParameterDescription::typeString, NULL }, // p[2] font helv42.pcc -> statusImage[0] { ParameterDescription::typeString, NULL }, // p[3] escnd.pcc -> statusImage[1] { ParameterDescription::typeString, NULL }, // p[4] edmg.pcc -> statusImage[2] { ParameterDescription::typeString, NULL }, // p[5] bteslev.pcc -> statusImage[3] { ParameterDescription::typeString, NULL }, // p[6] bteunjm.pcc -> statusImage[4] { ParameterDescription::typeString, NULL }, // p[7] espeed.pcc -> statusImage[5] { ParameterDescription::typeString, NULL }, // p[8] btesrnge.pcc -> statusImage[6] PARAMETER_DESCRIPTION_END } }; // // @004c7b30 -- Make. Validate the screen number (p[1], 1..12) then // construct. The GraphicGaugeBackground base self-registers with the // renderer, so BecameActive fires whenever its Eng screen is selected. // Logical PrepEngrScreen::Make( int display_port_index, Vector2DOf /*position -- PrepEngr draws at absolute coords*/, Entity *entity, GaugeRenderer *gauge_renderer ) { ParameterDescription *p = methodDescription.parameterList; int screen = p[1].data.integer; if (screen < 1 || screen > 12) { DEBUG_STREAM << "PrepEngr: screen number out of range " << screen << "\n"; return False; } PrepEngrScreen *gauge = new PrepEngrScreen( p[0].data.modeMask, // mode (ModeMFD*Eng*) (L4GaugeRenderer *)gauge_renderer, 0, // owner_ID (binary hard-wires 0) display_port_index, // graphics_port_number entity, // mech screen, // screen number p[2].data.string, p[3].data.string, p[4].data.string, p[5].data.string, p[6].data.string, p[7].data.string, p[8].data.string, "PrepEngr"); Register_Object(gauge); return True; } // // @004c7bf0 -- ctor. GraphicGaugeBackground base; store screen + mech (FIX // kept: an early transcript SWAPPED them); intern the 7 image names (the // config strings are transient) and pre-load (AddRef) each from the cache. // PrepEngrScreen::PrepEngrScreen( ModeMask mode_mask, L4GaugeRenderer *renderer_in, unsigned int owner_ID, int graphics_port_number, Entity *mech_in, int screen_number, const char *img0, const char *img1, const char *img2, const char *img3, const char *img4, const char *img5, const char *img6, const char *identification_string ): GraphicGaugeBackground(mode_mask, renderer_in, owner_ID, graphics_port_number, identification_string) { int i; const char *source[7]; Check_Pointer(this); screenNumber = screen_number; // @0x6C mech = mech_in; // @0x70 source[0] = img0; source[1] = img1; source[2] = img2; source[3] = img3; source[4] = img4; source[5] = img5; source[6] = img6; L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; for (i = 0; i < 7; i++) { if (source[i] != NULL) { statusImage[i] = nameCopy(source[i]); warehouse->bitMapBin.Get(statusImage[i]); // pre-load/AddRef } else { statusImage[i] = NULL; } } } // // @004c7d14 -- dtor. Release the 7 pre-loaded images + free their name // copies; the localView + base dtor run implicitly. // PrepEngrScreen::~PrepEngrScreen() { int i; Check(this); L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; for (i = 0; i < 7; i++) { if (statusImage[i] != NULL) { warehouse->bitMapBin.Release(statusImage[i]); Unregister_Pointer(statusImage[i]); delete[] statusImage[i]; statusImage[i] = NULL; } } } // // @004c7e48 -- BecameActive. Find the subsystem assigned to this screen and // paint its labels, dispatched on ClassID. NON-inactivating (a real paint; // does not chain the base) -> the paint-on-activation contract. // // STAND-INS at this site (see the aux-screen ledger above): // * screen match = the sequential roster assignment; // * numeric #2 (the linked heat-sink number, binary sink+0x1d4 == // Condenser::condenserNumber -- protected, no accessor) draws a static 0; // * the subsystem label bitmap (binary sub+0x224 name -> @(0x125,0x172)) // is skipped -- no live auxScreenLabel. // void PrepEngrScreen::BecameActive() { int i, count; const char *font; if (mech == NULL) { return; } L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; font = statusImage[0]; // helv42.pcc if (font == NULL) { return; } count = mech->GetSubsystemCount(); for (i = 1; i < count; i++) // skip slot 0 (controls mapper) { Subsystem *sub = mech->GetSubsystem(i); if (sub == NULL || !IsPanelSubsystem(sub)) { continue; } if (AuxScreenAssignmentOf(mech, sub) != screenNumber) { continue; } //---- numeric #1 : the screen number, 2 digits @(0xE9,0x176) { NumericDisplay num(warehouse, 0xe9, 0x176, font, 2, NumericDisplay::unsignedFormat, 0, 0xff); num.Draw(&localView, (Scalar)screenNumber); } //---- numeric #2 : linked heat-sink number, 1 digit @(0x15E,5) // INERT 0 (Condenser::condenserNumber unpublished -- see ledger). { NumericDisplay num(warehouse, 0x15e, 5, font, 1, NumericDisplay::unsignedFormat, 0, 0xff); num.Draw(&localView, 0.0f); } //---- subsystem label bitmap @(0x125,0x172): the AUTHORED aux-screen // label, cached on PoweredSubsystem at ctor time (the streamed // resource dies with the Mech ctor's buffer). This is the panel // title art -- "SENSOR CLUSTER", "MYOMERS", the weapon name and // range strip. // if (sub->IsDerivedFrom(PoweredSubsystem::ClassDerivations)) { const char *label = ((PoweredSubsystem *)sub)->GetAuxScreenLabel(); if (label != NULL && *label != '') { localView.SetColor(0xff); DrawPortBackground(&localView, warehouse, 0x125, 0x172, label); } } //---- type-specific label cells (the authentic ClassID dispatch) switch (sub->GetClassID()) { case RegisteredClass::SensorClassID: // 0xBC3 DrawStatusCells(&localView, warehouse, 1, NULL, NULL, NULL); localView.SetColor(0xff); DrawPortBackground(&localView, warehouse, 0xb8, 0x8c, statusImage[6]); // btesrnge return; case RegisteredClass::MyomersClassID: // 0xBC6 DrawStatusCells(&localView, warehouse, 0, NULL, statusImage[5], statusImage[3]); return; case RegisteredClass::EmitterClassID: // 0xBC8 case RegisteredClass::PPCClassID: // 0xBD4 DrawStatusCells(&localView, warehouse, 0, statusImage[1], statusImage[2], statusImage[3]); return; case RegisteredClass::ProjectileWeaponClassID: // 0xBCD case RegisteredClass::MissileLauncherClassID: // 0xBD0 DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[4]); return; case RegisteredClass::GaussRifleClassID: // 0xBCE -- not yet supported DEBUG_STREAM << "PrepEngr: Gauss rifle not yet supported\n"; DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[3]); return; default: return; } } } //########################################################################### //########################################################################### // SubsystemCluster family @004c8140 base //########################################################################### //########################################################################### // // @004c8140 -- SubsystemCluster ctor (vtable PTR_FUN_0051a020). GraphicGauge // base (rate/mode hard-wired every-frame / always-active; the two bit-plane // masks arrive as mfd_mode and eng_mode). Child roster (binary build order): // this[0x26] HorizTwoPartBar (temperature, MFD) @004c4170 // this[0x27] AnimatedSubsystemLamp ("CoolingLoop", btploop) @004c70a4 // this[0x29] AnimatedSourceLamp ("PowerSource", btpbus) @004c7160 (powered only) // this[0x24] BackgroundBitmap (strip art -- STAND-IN: not built) // this[0x28] AnimatedSubsystemLamp (bteloop, Eng) @004c70a4 // this[0x2D] OneOfSeveralInt (btecmode) @004c5148 // this[0x2A] AnimatedSourceLamp (btebus) @004c7160 (powered only) // this[0x25] ScalarBarGauge (evolt, slotOf) @004c721c (powered only) // this[0x2E] OneOfSeveralStates (btemode) @004c5470 (powered only) // this[0x2B]/[0x2C] VertTwoPartBar x2 (temperatures) @004c4724 // this[0x2F] LeakGauge (eleak) @004c5b7c // SubsystemCluster::SubsystemCluster( ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port, int x, int y, Subsystem *subsystem_in, ModeMask eng_mode, const char *eng_port_name, const char *tile_image, const char *label, char **image_names, const char *identification_string ): GraphicGauge((GaugeRate)Gauge::gaugeRate_A, (ModeMask)ModeManager::ModeAlwaysActive, renderer_in, owner_ID, mfd_port, identification_string) { Check_Pointer(this); L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; // // The power-bus gate: the binary gated the power children on the // "InputVoltage" attribute resolving -- the same population as the // PoweredSubsystem derivation (every subsystem with a power bus is one). // Logical powered = IsPoweredSubsystemDerived(subsystem_in); // // Temperature feeds by NAME (heat attribute wave pending -> unbound // cells; the bars draw at minimum until it lands). // Scalar *currentTemp = ScalarAttributeOf(subsystem_in, "CurrentTemperature"), *degradeTemp = ScalarAttributeOf(subsystem_in, "DegradationTemperature"), *failTemp = ScalarAttributeOf(subsystem_in, "FailureTemperature"), *coolantLeak = ScalarAttributeOf(subsystem_in, "CoolantMassLeakRate"); //------------------------------------------------------------------ MFD port temperatureBar = new HorizTwoPartBar(ChildRate(), mfd_mode, renderer_in, // @004c4170 mfd_port, x + 0x13, y + 0x10, x + 0xdc, y + 0x1f, tile_image, 0xff, 0, currentTemp, degradeTemp, failTemp, "HorizTwoPartBar"); Register_Object(temperatureBar); coolingLoopA = new AnimatedSubsystemLamp(ChildRate(), mfd_mode, renderer_in, // @004c70a4 mfd_port, x + 0xe5, y + 0xe, "btploop.pcc", 7, 1, subsystem_in, "CoolingLoop"); Register_Object(coolingLoopA); if (!powered) { powerSourceA = NULL; } else { powerSourceA = new AnimatedSourceLamp(ChildRate(), mfd_mode, renderer_in, // @004c7160 mfd_port, x + 0x10e, y + 0xe, "btpbus.pcc", 5, 1, subsystem_in, "PowerSource"); // the SUBSYSTEM -- the connection // resolves voltageSource by name Register_Object(powerSourceA); } // // Background strip art: the binary picks image_names[auxScreenPlacement] // (sub+0x1e0) -- LIVE now that PoweredSubsystem caches the authored // placement (the resource itself is freed with the Mech ctor's stream // buffer). A model that authored nothing keeps the bare frame. // background = NULL; int clusterPlacement = -1; const char *clusterBackgroundName = NULL; { int placement = -1; if (subsystem_in != NULL && subsystem_in->IsDerivedFrom(PoweredSubsystem::ClassDerivations)) { placement = ((PoweredSubsystem *)subsystem_in)->GetAuxScreenPlacement(); clusterPlacement = placement; } if (placement >= 0 && placement < 8 && image_names != NULL && image_names[placement] != NULL) { // // PLACEMENT (A/B rig, 5.3.35): the strip is the mech diagram // with THIS subsystem's segment lit, and it belongs in the // middle of the quadrant, not at its origin. Solved from the // shipped framebuffer: our extra pixels clustered at panel x // 0..70 against shipped's missing at x 140..230, and the // cross-correlation peaked at dx=+144 dy=-54 on all three // panels independently. y is bottom-up in this space, so up // the screen is +54. // background = new BackgroundBitmap( mfd_mode, renderer_in, owner_ID, mfd_port, x + 0x90, y + 0x36, image_names[placement], 1, // opaque 0xff, 0 // fg / bg ); Register_Object(background); clusterBackgroundName = image_names[placement]; } } // // The PANEL TITLE BANNER (299x26): the authored auxScreenLabel .pcc -- // "SENSOR CLUSTER" / "MYOMERS" / "ERMED LASER RANGE 500M" / the weapon // names. Cached on PoweredSubsystem at ctor time (its resource dies // with the Mech ctor's stream buffer). The banner sits at the TOP of // the quadrant; BackgroundBitmap places by (left, BOTTOM), so the row // is the panel origin plus the quadrant height less the banner. // titleBanner = NULL; if (subsystem_in != NULL && subsystem_in->IsDerivedFrom(PoweredSubsystem::ClassDerivations)) { const char *title = ((PoweredSubsystem *)subsystem_in)->GetAuxScreenLabel(); if (title != NULL && *title != '') { // Placement measured against the shipped framebuffer // (the A/B rig): the banner's bottom row sits 0xb3 above the // panel origin, inset 0x0e from its left edge. titleBanner = new BackgroundBitmap( mfd_mode, renderer_in, owner_ID, mfd_port, x + 0x09, y + 0xb4, title, 1, 0xff, 0 ); Register_Object(titleBanner); } if (getenv("BT_VIS_LOG")) DEBUG_STREAM << "[cluster] sub='" << ((subsystem_in != NULL) ? subsystem_in->GetName() : "(null)") << "' powered=" << (powered ? 1 : 0) << " placement=" << clusterPlacement << " bg='" << (clusterBackgroundName ? clusterBackgroundName : "(none)") << "' port=" << mfd_port << " engPortName='" << (eng_port_name ? eng_port_name : "(null)") << "' engPort=" << renderer_in->FindGraphicsPort(eng_port_name) << " x=" << x << " y=" << y << " title='" << (title ? title : "(null)") << "' banner=" << (titleBanner ? 1 : 0) << "\n" << flush; } //------------------------------------------------------------ // Paint the panel frame image into the MFD port. //------------------------------------------------------------ localView.SetOrigin(x, y); // vtbl+0x10 localView.SetColor(0xff); // vtbl+0x18 if (label != NULL) // (STAND-IN: label may be absent) { DrawPortBackground(&localView, warehouse, 9, 0xb4, label); // FUN_004c2ec4 } //----------------------------------------------------------- engineering port int engPort = renderer_in->FindGraphicsPort(eng_port_name); // // The second vertical bar tracked the LINKED heat sink's temperatures // (the binary resolved the "HeatSink" link attribute and read the linked // sink). Neither the link row nor a linked-sink accessor is published // yet -- INERT FEEDS (the bar draws, stays at minimum). // Scalar *linkTemp = &UnboundScalarSource, *linkDegrade = &UnboundScalarSource; coolingLoopB = new AnimatedSubsystemLamp(ChildRate(), eng_mode, renderer_in, // @004c70a4 engPort, 0x254, 0x13b, "bteloop.pcc", 7, 1, subsystem_in, "CoolingLoop"); Register_Object(coolingLoopB); // // The cooling-mode lamp: binary *(subsys+0x134) == the 1995 HeatSink // coolantAvailable word. Bound by NAME ("CoolantAvailable") -- unbound // until the heat attribute wave publishes it (frame 0 = OFF meanwhile). // modeLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148 engPort, 400, 10, "btecmode.pcc", 1, 2, IntegerAttributeOf(subsystem_in, "CoolantAvailable"), "OneOfSeveralInt"); Register_Object(modeLamp); if (!powered) { powerSourceB = NULL; generatorVoltageBar = NULL; stateLamp = NULL; } else { powerSourceB = new AnimatedSourceLamp(ChildRate(), eng_mode, renderer_in, // @004c7160 engPort, 0x1a, 0xd0, "btebus.pcc", 5, 1, subsystem_in, "PowerSource"); Register_Object(powerSourceB); generatorVoltageBar = new ScalarBarGauge(ChildRate(), eng_mode, renderer_in, // @004c721c engPort, 0x89, 0x82, 0x93, 0x13b, "evolt.pcc", 0xff, 0, WipeGaugeScalar::wipeUp, 0.0f, GeneratorMaxVoltage, (void *)subsystem_in, // the SUBSYSTEM -- BTGeneratorVoltage- "GeneratorVoltage(slotOf)"); // style resolve through voltageSource Register_Object(generatorVoltageBar); // // The connect-mode lamp (btemode, 1x3). Binary source = subsys+0x2b8 // (the PoweredSubsystem modeAlarm region: Manual/Connected/Auto); // gated IsDerivedFrom(0x50f4bc). STAND-IN: no mode accessor is // published -- the subsystem itself is passed, so the lamp tracks the // SIMULATION state word instead (frame 0 in normal play). // stateLamp = new OneOfSeveralStates(ChildRate(), eng_mode, renderer_in, // @004c5470 engPort, 0xbe, 0, "btemode.pcc", 1, 3, subsystem_in, "OneOfSeveralStates"); Register_Object(stateLamp); } vertBarA = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in, // @004c4724 engPort, 0x1d1, 0x5c, 0x1eb, 0x124, tile_image, 0xff, 0xff, 0, currentTemp, degradeTemp, failTemp, "VertTwoPartBar"); Register_Object(vertBarA); vertBarB = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in, engPort, 0x222, 0x5c, 0x23d, 0x124, tile_image, 0xff, 0xff, 0, linkTemp, linkDegrade, failTemp, "VertTwoPartBar"); Register_Object(vertBarB); leakGauge = new BitMapInverseWipe(ChildRate(), eng_mode, renderer_in, // @004c5b7c engPort, 0x255, 0xe0, "eleak.pcc", 0, 0xff, 0 /*third: binary *(subsys+0x150); stored-unused*/, 3 /*frames*/, coolantLeak, "LeakGauge"); Register_Object(leakGauge); failedState = False; // @0xC4 this[0x31] (1 = destroyed) subsystem = subsystem_in; // @0xC0 this[0x30] previousDrawState = 0; // @0xC8 this[0x32] Check_Fpu(); } // // @004c87dc -- dtor. Free the owned children (the binary frees them through // the ReleaseChildren path @004c8820). // SubsystemCluster::~SubsystemCluster() { Check(this); ReleaseChildren(); // @004c8820 } // // @004c8820 -- free each owned child gauge. // void SubsystemCluster::ReleaseChildren() { if (background) { Unregister_Object(background); delete background; background = NULL; } if (generatorVoltageBar){ Unregister_Object(generatorVoltageBar); delete generatorVoltageBar; generatorVoltageBar = NULL; } if (temperatureBar) { Unregister_Object(temperatureBar); delete temperatureBar; temperatureBar = NULL; } if (coolingLoopA) { Unregister_Object(coolingLoopA); delete coolingLoopA; coolingLoopA = NULL; } if (coolingLoopB) { Unregister_Object(coolingLoopB); delete coolingLoopB; coolingLoopB = NULL; } if (powerSourceA) { Unregister_Object(powerSourceA); delete powerSourceA; powerSourceA = NULL; } if (powerSourceB) { Unregister_Object(powerSourceB); delete powerSourceB; powerSourceB = NULL; } if (vertBarA) { Unregister_Object(vertBarA); delete vertBarA; vertBarA = NULL; } if (vertBarB) { Unregister_Object(vertBarB); delete vertBarB; vertBarB = NULL; } if (modeLamp) { Unregister_Object(modeLamp); delete modeLamp; modeLamp = NULL; } if (stateLamp) { Unregister_Object(stateLamp); delete stateLamp; stateLamp = NULL; } if (leakGauge) { Unregister_Object(leakGauge); delete leakGauge; leakGauge = NULL; } } // // @004c89c4 -- draw the panel's border frame (two thick diagonals of the box). // void SubsystemCluster::DrawPanelFrame(int color) { localView.SetColor(color); // vtbl+0x18 localView.MoveToAbsolute(9, 0x33); // vtbl+0x24 localView.DrawThickLineToAbsolute(0x132, 0xab); // vtbl+0x38 localView.MoveToAbsolute(9, 0xab); localView.DrawThickLineToAbsolute(0x132, 0x33); } // // @004c8a28 -- forward an enable/disable to the always-present MFD children. // POLARITY (PPC-dial fix, kept): the passed value is the DRAW STATE -- // 0 = alive (children run), nonzero = dead panel (children off). // void SubsystemCluster::SetChildrenEnable(int enable) { if (temperatureBar) temperatureBar->Disable(enable != 0); if (coolingLoopA) coolingLoopA->Disable(enable != 0); if (powerSourceA) powerSourceA->Disable(enable != 0); } // // @004c8990 -- the operational-state read. POLARITY (kept): state 1 = the // DEAD-panel presentation (black fill + bright frame + X lamps lit); state 0 // = ALIVE (frame 0 + background art; the WeaponCluster lamp gate runs ONLY // in state 0). The binary's secondary condition (*(subsystem+0x278) != 4 -> // 1) remains unidentified [T4] and is dropped -- a healthy subsystem reads // ALIVE here until that field is decoded. // int SubsystemCluster::GetDrawState() { return failedState ? 1 : 0; } Logical SubsystemCluster::TestInstance() const { return GraphicGauge::TestInstance(); } // // @004c88e4 -- Execute (shared by the whole family). failedState = the // subsystem's own damage saturation (>= 1.0 destroys, mechdmg.cpp -- the // PPC-dial polarity truth source; the binary read *(subsystem+0x40)==1). // On a draw-state change repaint the panel frame: alive => frame(0) + redraw // background bitmap; dead => black fill + frame(0xff); and forward the // enable to the temperature / loop children. // void SubsystemCluster::Execute() { int state; failedState = (SubsystemDamageLevelOf(subsystem) >= 1.0f) ? True : False; state = GetDrawState(); if (state != previousDrawState) { previousDrawState = state; SetChildrenEnable(state); if (state == 0) { DrawPanelFrame(0); if (background != NULL) { background->BecameActive(); // force the strip-art redraw } } else { localView.SetColor(0); // vtbl+0x18 localView.MoveToAbsolute(1, 0x32); // vtbl+0x24 localView.DrawFilledRectangleToAbsolute(0x136, 0xad); // vtbl+0x48 DrawPanelFrame(0xff); } } } // // (Re-host addition, kept from the decode: force the first-frame repaint on // every activation -- the engine-base default would self-inactivate.) // void SubsystemCluster::BecameActive() { previousDrawState = -1; } //########################################################################### // HeatSinkCluster @004c8a6c (vtable 0x519fd4) //########################################################################### // // @004c8a6c -- ctor: SubsystemCluster base + four failure TwoStates (heat / // heat-sink / power / structure) + a NumericDisplayScalar read-out, plus a // Scalar connection feeding heatLoad. // // FEED LEDGER at this site: // heatFailLamp <- own failedState LIVE // hsFailLamp <- failFlag (the HUD subsystem's failure) LIVE (Execute) // powerFailLamp <- binary subsys+0x324 INERT (unidentified // structFailLamp <- binary subsys+0x320 1995-layout words; // no recon member) // heatLoad <- "HeatLoad" by name NAME-DEFERRED // (binary subsys+0x31c == HeatableSubsystem::heatLoad; // unpublished -> numeric reads 0 until the heat wave) // HeatSinkCluster::HeatSinkCluster( ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port, int x, int y, Subsystem *subsystem_in, Subsystem *hud, ModeMask eng_mode, const char *eng_port_name, const char *tile_image, const char *label, const char *fail_lamp_image, char **image_names, const char *identification_string ): SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in, eng_mode, eng_port_name, tile_image, label, image_names, identification_string) { GaugeConnection *connection; int engPort = renderer_in->FindGraphicsPort(eng_port_name); heatLoadScaled = HeatNumericBase; // @0xD8 (100.0f seed) heatLoad = 0.0f; // @0xD4 heatFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, // @004739d8 engPort, 200, 0x110, fail_lamp_image, 0, 0xff, (int *)&failedState, "TwoState"); // bright when FAILED Register_Object(heatFailLamp); failSubsystem = hud; // @0xCC failFlag = False; // @0xD0 hsFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, engPort, 0x97, 0xe8, "btehfail.pcc", 0, 0xff, (int *)&failFlag, "TwoState"); Register_Object(hsFailLamp); powerFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, engPort, 0x97, 0xc0, "btepfail.pcc", 0, 0xff, &UnboundIntegerSource, "TwoState"); // INERT (binary subsys+0x324) Register_Object(powerFailLamp); structFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, engPort, 0x97, 0x98, "btesfail.pcc", 0xff, 0, // (colour order = binary quirk) &UnboundIntegerSource, "TwoState"); // INERT (binary subsys+0x320) Register_Object(structFailLamp); heatNumeric = new NumericDisplayScalar(ChildRate(), eng_mode, renderer_in, // @00470888 owner_ID, engPort, 0x132, 0x8c, 0x15a, 0xaa, "helv18.pcc", 3, (NumericDisplay::NumericFormat)0, 0, 0xff, &heatLoadScaled, "NumericDisplayScalar"); Register_Object(heatNumeric); connection = new GaugeConnectionDirectOf(0, &heatLoad, ScalarAttributeOf(subsystem_in, "HeatLoad")); // NAME-DEFERRED Register_Object(connection); AddConnection(connection); } HeatSinkCluster::~HeatSinkCluster() { Check(this); if (heatFailLamp) { Unregister_Object(heatFailLamp); delete heatFailLamp; heatFailLamp = NULL; } if (hsFailLamp) { Unregister_Object(hsFailLamp); delete hsFailLamp; hsFailLamp = NULL; } if (powerFailLamp) { Unregister_Object(powerFailLamp); delete powerFailLamp; powerFailLamp = NULL; } if (structFailLamp) { Unregister_Object(structFailLamp); delete structFailLamp; structFailLamp = NULL; } if (heatNumeric) { Unregister_Object(heatNumeric); delete heatNumeric; heatNumeric = NULL; } } // // @004c8db0 -- Execute: failFlag = the HUD subsystem failed (the binary read // *(failSubsystem+0x40)==1; re-hosted onto the same damage-saturation truth // source the panel itself uses); heatLoadScaled = heatLoad * 100.0; chain // SubsystemCluster::Execute. // void HeatSinkCluster::Execute() { if (failSubsystem != NULL) { failFlag = (SubsystemDamageLevelOf(failSubsystem) >= 1.0f || SubsystemDestroyed(failSubsystem)) ? True : False; } heatLoadScaled = heatLoad * HeatLoadScale; SubsystemCluster::Execute(); } //########################################################################### // MyomerCluster @004c8df4 (vtable 0x519f88) //########################################################################### // // @004c8df4 -- ctor: SubsystemCluster base + a SeekVoltageGraph ("EngrGraph") // fed by a GeneratorVoltageConnection (writes seekValue) + a seek-step // OneOfSeveralInt (bteseek.pcc). Execute = base. // // The seek-step feed: the binary's +0x800 was OOB for a 0x358 Myomers // (garbage); the decode re-pointed it at Myomers::currentSeekVoltageIndex // (@0x320, INFERRED -- @004c8df4 isn't in the assert-anchored export). The // re-host binds the same member BY NAME ("CurrentSeekVoltageIndex" -- // unpublished by the source410 Myomers table yet -> frame 0 until it lands). // MyomerCluster::MyomerCluster( ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port, int x, int y, Subsystem *subsystem_in, ModeMask eng_mode, const char *eng_port_name, const char *tile_image, const char *label, char **image_names, const char *identification_string ): SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in, eng_mode, eng_port_name, tile_image, label, image_names, identification_string) { GaugeConnection *connection; int engPort = renderer_in->FindGraphicsPort(eng_port_name); seekValue = 0.0f; // @0xCC (connection dst) seekGraph = (GraphicGauge *)new SeekVoltageGraph(ChildRate(), eng_mode, // @004c6798 renderer_in, engPort, (Entity *)subsystem_in, (AttributeAccessor *)&seekValue, "edestryd.pcc", False, "EngrGraph"); Register_Object(seekGraph); seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148 engPort, 0xf, 0, "bteseek.pcc", 1, 4, IntegerAttributeOf(subsystem_in, "CurrentSeekVoltageIndex"), "OneOfSeveralInt"); Register_Object(seekStepLamp); // // The graph's live input: the powering generator's voltage, resolved // through the named voltageSource connection (LIVE). // connection = new GeneratorVoltageConnection(&seekValue, subsystem_in); // @004c3288 Register_Object(connection); AddConnection(connection); } MyomerCluster::~MyomerCluster() { Check(this); if (seekGraph) { Unregister_Object(seekGraph); delete seekGraph; seekGraph = NULL; } if (seekStepLamp) { Unregister_Object(seekStepLamp); delete seekStepLamp; seekStepLamp = NULL; } } //########################################################################### // WeaponCluster @004c8fc4 (vtable 0x519f38) //########################################################################### // // @004c8fc4 -- ctor: SubsystemCluster base; interns the cluster image; // centres a SegmentArc270 recharge dial (reads PercentDone -- PUBLISHED, // LIVE) + a ConfigMapGauge; adds a Scalar connection feeding percentDone. // The warning-lamp centre is derived from the cluster image size, or warned // "WeaponCluster missing ". // WeaponCluster::WeaponCluster( ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port, int x, int y, Subsystem *subsystem_in, ModeMask eng_mode, const char *eng_port_name, const char *tile_image, const char *label, const char *cluster_image, char **image_names, const char *identification_string ): SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in, eng_mode, eng_port_name, tile_image, label, image_names, identification_string) { GaugeConnection *connection; Scalar *percentDoneAttr = ScalarAttributeOf(subsystem_in, "PercentDone"); // LIVE (mechweap.cpp 0x12) //------------------------------------------------------------ // Intern the cluster image + derive the warning-lamp centre. //------------------------------------------------------------ clusterImage = nameCopy(cluster_image); // @0xCC L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; BitMap *img = warehouse->bitMapBin.Get(clusterImage); // ctor AddRef (dtor releases) if (img == NULL) { DEBUG_STREAM << "WeaponCluster missing " << clusterImage << "\n"; warningCenterX = 0; // @0xDC warningCenterY = 0; // @0xE0 } else { warningCenterX = 0x41 - (img->Data.Size.x >> 1); warningCenterY = 0x6d - (img->Data.Size.y >> 1); } //------------------------------------------------------------ // rechargeArc (SegmentArc270 @004c6244): the 0..360 recharge // dial reading the weapon's PercentDone (a real 0..1 fraction). //------------------------------------------------------------ rechargeArc = (GraphicGauge *)new SegmentArc270( (GaugeRate)Gauge::gaugeRate_A, mfd_mode, renderer_in, owner_ID, mfd_port, x + 0x41, y + 0x6d, 40.0f, 60.0f, 40.0f, 60.0f, 0.0f, 360.0f, 0x14, 0, 0xff, percentDoneAttr, True, "SegmentArc270"); Register_Object(rechargeArc); configMap = new ConfigMapGauge(ChildRate(), mfd_mode, renderer_in, // @004c6d80 mfd_port, x + 0xee, y + 0x38, (Entity *)subsystem_in, "ConfigMap"); Register_Object(configMap); warningState = 0; // @0xE4 connection = new GaugeConnectionDirectOf(0, &percentDone, percentDoneAttr); Register_Object(connection); AddConnection(connection); } WeaponCluster::~WeaponCluster() { Check(this); L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; warehouse->bitMapBin.Release(clusterImage); Unregister_Pointer(clusterImage); delete[] clusterImage; clusterImage = NULL; if (rechargeArc) { Unregister_Object(rechargeArc); delete rechargeArc; rechargeArc = NULL; } if (configMap) { Unregister_Object(configMap); delete configMap; configMap = NULL; } } // // @004c932c -- DrawWarningLamp: blit the cluster warning image at its centre, // coloured on/off by the passed enable. // void WeaponCluster::DrawWarningLamp(int on) { L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; BitMap *img = warehouse->bitMapBin.Get(clusterImage); if (img != NULL) { localView.MoveToAbsolute(warningCenterX, warningCenterY); // vtbl+0x24 localView.SetColor(on ? 0xff : 0); // vtbl+0x18 localView.DrawBitMap(0, img); // vtbl+0x54 } warehouse->bitMapBin.Release(clusterImage); } // // @004c9258 -- BecameActive: reset the warning state + chain the children. // void WeaponCluster::BecameActive() { warningState = -2; if (configMap) configMap->BecameActive(); if (rechargeArc) rechargeArc->BecameActive(); SubsystemCluster::BecameActive(); } // // @004c9290 -- Execute: chain the base; when percentDone crosses the warn // threshold toggle the warning lamp (draw-state 0 = alive panels only). // // PPC-dial REPAINT FIX (kept on re-host): the base Execute BLACK-FILLS + // repaints the panel on every draw-state flip -- wiping the recharge arc's // pixels while the arc's INCREMENTAL draw memory (previousSegment) still // says "lit". The stale arc then draws nothing until the value crosses new // segments ("tick marks frozen, dot still toggles"). Detect the repaint and // reset the arc / configMap / lamp draw memory so they fully redraw. // void WeaponCluster::Execute() { int preDrawState = previousDrawState; SubsystemCluster::Execute(); if (previousDrawState != preDrawState) { if (rechargeArc) rechargeArc->BecameActive(); if (configMap) configMap->BecameActive(); warningState = -2; // lamp: force redraw too } if (getenv("BT_VIS_LOG")) { static int weaponWarnSamples = 0; if (weaponWarnSamples < 16) { ++weaponWarnSamples; DEBUG_STREAM << "[warn] " << identificationString << " drawState=" << previousDrawState << " percentDone=" << percentDone << " warnState=" << warningState << "\n" << flush; } } if (previousDrawState == 0) { int warn = (WeaponWarnThreshold < percentDone) ? 1 : 0; if (warn != warningState) { warningState = warn; DrawWarningLamp(warn); } } } //########################################################################### // EnergyWeaponCluster @004c93b0 (vtable 0x519ee8) //########################################################################### // // @004c93b0 -- ctor: WeaponCluster base + a SeekVoltageGraph ("EngrGraph", // live cursor) + a seek-step OneOfSeveralInt (bteseek.pcc). // // The live-cursor voltage: the binary bound the emitter's "OutputVoltage" // attribute (the raw charge voltage, binary currentLevel @0x414). Our // recon publishes that SAME member as "ChargeLevel" (emitter.hpp:159, the // authentic 0x1D row) -- so the authentic name is tried first and the // source410 alias second: the cursor is LIVE on every emitter/PPC today. // // The seek-step: binary subsys+0x3f0 == Emitter::seekVoltageIndex, bound BY // NAME ("CurrentSeekVoltageIndex" -- unpublished by the source410 Emitter // table yet -> frame 0 until that row lands). // EnergyWeaponCluster::EnergyWeaponCluster( ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port, int x, int y, Subsystem *subsystem_in, ModeMask eng_mode, const char *eng_port_name, const char *tile_image, const char *label, const char *cluster_image, char **image_names, const char *identification_string ): WeaponCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in, eng_mode, eng_port_name, tile_image, label, cluster_image, image_names, identification_string) { int engPort = renderer_in->FindGraphicsPort(eng_port_name); void *outputVoltage = AttributePointerOf(subsystem_in, "OutputVoltage"); if (outputVoltage == NULL) { outputVoltage = AttributePointerOf(subsystem_in, "ChargeLevel"); // source410 alias } seekGraph = new SeekVoltageGraph(ChildRate(), eng_mode, renderer_in, // @004c6798 engPort, (Entity *)subsystem_in, (AttributeAccessor *)outputVoltage, "edestryd.pcc", True, "EngrGraph"); Register_Object(seekGraph); seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148 engPort, 0xf, 0, "bteseek.pcc", 1, 4, IntegerAttributeOf(subsystem_in, "CurrentSeekVoltageIndex"), "OneOfSeveralInt"); Register_Object(seekStepLamp); } EnergyWeaponCluster::~EnergyWeaponCluster() { Check(this); if (seekGraph) { Unregister_Object(seekGraph); delete seekGraph; seekGraph = NULL; } if (seekStepLamp) { Unregister_Object(seekStepLamp); delete seekStepLamp; seekStepLamp = NULL; } } //########################################################################### // BallisticWeaponCluster @004c9558 (vtable 0x519e98) //########################################################################### // // // The linked-bin round count for the ammo readout: ProjectileWeapon // resolves its own bin (the link is protected), so the cluster asks the // weapon. -1 (no bin) reads as 0 rounds on the dial. // static int AmmoRoundsOf(Subsystem *subsystem) { if (subsystem == NULL || !subsystem->IsDerivedFrom(ProjectileWeapon::ClassDerivations)) { return 0; } int rounds = ((ProjectileWeapon *)subsystem)->GetAmmoCount(); return (rounds > 0) ? rounds : 0; } // @004c9558 -- ctor: WeaponCluster base + the ammo panel: two // NumericDisplayIntegers (ammo counts), jam/fire TwoStates, a // NumericDisplayScalarTwoState (reload seconds), a destroyed TwoState, plus // the erase tracker and eject wipe. // // FEED LEDGER at this site: // jam lamp <- jammed = GetWeaponState()==JammedState LIVE (Execute) // destroyed X <- own failedState LIVE // ammo counters <- INERT 0: the weapon's ammo store is // ProjectileWeapon::ammoBinLink (PROJWEAP.HPP:102) -- // protected, no accessor, no published attribute; the // AmmoBin accessors (GetAmmoCount/GetAmmoState) are // unreachable without the link. (The binary read the // resolved bin's count.) // fire lamp / <- INERT 0: the feed/reload state (binary bin+0x18c) has // reload numeric no recon member either. // ammoErase <- NULL: GraphicsViewRecord tracker (binary @0044ad78) -- // bring-up NULL kept; the numeric self-erases. // ejectWipe <- NULL: BitMapInverseWipeScalar (@004c61c8) is not // reconstructed in btl4gaug (notes-only); bring-up NULL. // reload timer <- left 0 (the binary's frame clock DAT_0052140c // equivalent is not wired). // BallisticWeaponCluster::BallisticWeaponCluster( ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port, int x, int y, Subsystem *subsystem_in, ModeMask eng_mode, const char *eng_port_name, const char *tile_image, const char *label, const char *cluster_image, char **image_names, const char *font_a, const char *font_b, const char *identification_string ): WeaponCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in, eng_mode, eng_port_name, tile_image, label, cluster_image, image_names, identification_string) { int engPort = renderer_in->FindGraphicsPort(eng_port_name); // // LIVE: the linked bin's rounds, refreshed by Execute. // ammoRounds = AmmoRoundsOf(subsystem_in); int *ammoValue = &ammoRounds; reloadStartTime = 0; // @0xF4 reloadSeconds = 0.0f; // @0xF8 jammed = False; // @0xE8 firing = False; // @0xEC reloading = False; // @0xF0 ammoCountA = new NumericDisplayInteger(ChildRate(), mfd_mode, renderer_in, // @00470cfc owner_ID, mfd_port, x + 0x27, y + 100, x + 0x5f, y + 0x76, font_a, 4, (NumericDisplay::NumericFormat)0, 0, 0xff, ammoValue, "NumericDisplayInteger"); Register_Object(ammoCountA); ammoCountB = new NumericDisplayInteger(ChildRate(), eng_mode, renderer_in, owner_ID, engPort, 0xdc, 0x104, 0x140, 300, font_b, 4, (NumericDisplay::NumericFormat)0, 0, 0xff, ammoValue, "NumericDisplayInteger"); Register_Object(ammoCountB); jamLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, // @004739d8 engPort, 0xa0, 0xb2, "btejam.pcc", 0, 0xff, (int *)&jammed, "TwoState"); Register_Object(jamLamp); fireLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, engPort, 0x121, 0xb6, "btefire.pcc", 0, 0xff, (int *)&firing, "TwoState"); Register_Object(fireLamp); ammoErase = NULL; // @0xFC (bring-up NULL, see ledger) ammoNumeric = new NumericDisplayScalarTwoState(ChildRate(), eng_mode, // @00473c94 renderer_in, owner_ID, engPort, 0x130, 0xbc, 0x162, 0xee, "helv42.pcc", 2, (NumericDisplay::NumericFormat)0, 0, 0xff, &reloadSeconds, (int *)&firing, "NumericDisplayScalarTwoState"); Register_Object(ammoNumeric); destroyedLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&failedState, "TwoState"); // the destroyed X: bright when FAILED Register_Object(destroyedLamp); ejectWipe = NULL; // @0x110 (bring-up NULL, see ledger) } BallisticWeaponCluster::~BallisticWeaponCluster() { Check(this); if (ammoNumeric) { Unregister_Object(ammoNumeric); delete ammoNumeric; ammoNumeric = NULL; } if (ammoCountA) { Unregister_Object(ammoCountA); delete ammoCountA; ammoCountA = NULL; } if (ammoCountB) { Unregister_Object(ammoCountB); delete ammoCountB; ammoCountB = NULL; } if (jamLamp) { Unregister_Object(jamLamp); delete jamLamp; jamLamp = NULL; } if (fireLamp) { Unregister_Object(fireLamp); delete fireLamp; fireLamp = NULL; } if (destroyedLamp) { Unregister_Object(destroyedLamp); delete destroyedLamp; destroyedLamp = NULL; } delete ammoErase; ammoErase = NULL; // (never allocated today) delete ejectWipe; ejectWipe = NULL; } // // @004c99cc -- BecameActive: chain the ammo children then WeaponCluster. // void BallisticWeaponCluster::BecameActive() { if (ammoCountA) ammoCountA->BecameActive(); if (ammoCountB) ammoCountB->BecameActive(); if (jamLamp) jamLamp->BecameActive(); if (fireLamp) fireLamp->BecameActive(); if (ammoNumeric) ammoNumeric->BecameActive(); if (destroyedLamp) destroyedLamp->BecameActive(); if (ejectWipe) ejectWipe->BecameActive(); WeaponCluster::BecameActive(); } // // @004c9a38 -- Execute: jammed = the weapon-state alarm reads Jammed (LIVE // through GetWeaponState -- the binary's raw *(subsys+0x364)==5 retired); // the ammo-bin feed/reload state is INERT (see the ctor ledger); chain // WeaponCluster::Execute. // void BallisticWeaponCluster::Execute() { jammed = WeaponJammedOf(subsystem); ammoRounds = AmmoRoundsOf(subsystem); // // INERT: reload/feed state (binary bin+0x18c) -- no recon member; the // fire lamp stays dark and the reload numeric stays gated off. // firing = False; reloading = False; WeaponCluster::Execute(); } // // @004c9b50 -- DrawWarningLamp override: draw the fire-ready "dot" (the base // blits the cluster image in on-colour 0xff / off-colour 0), THEN swap the // base-page ammo count's colours so the digits stay legible against it: // dot ABSENT (on==0) -> SetColors(bg=0, fg=0xff) == lit digits on black // dot PRESENT (on!=0) -> SetColors(bg=0xff, fg=0) == dark digits cut out // of the solid dot (the stencil look) // The SetColors call ForceUpdate()s the numeric so it repaints over the // freshly drawn dot on the next child-execute pass. (Verified vs binary // FUN_004c9b50 + the base FUN_004c932c.) // void BallisticWeaponCluster::DrawWarningLamp(int on) { WeaponCluster::DrawWarningLamp(on); // @004c932c: draw the dot if (ammoCountA) { ((NumericDisplayInteger *)ammoCountA)->SetColors( on ? 0xff : 0, on ? 0 : 0xff); } } // // @004c9adc -- TestInstance. The binary: True unless the ammo bin is // present with rounds and the weapon isn't jammed (then the base test runs). // The bin is unreachable on the re-host (see the ctor ledger), so the // jam-side of the predicate is kept and the ammo-empty gate is deferred // with the ammo feed. // Logical BallisticWeaponCluster::TestInstance() const { if (jammed != 0) { return True; } return SubsystemCluster::TestInstance(); } //########################################################################### // vehicleSubSystems Make dispatcher @004cbaf0 //########################################################################### // // The 12 auxiliary-screen geometry rows (@0x51bf34: x / y / planeMask / // subBit / mfdPort / engPort / planeIndex), keyed by aux screen - 1. The // recovered mask constants resolve exactly onto the authentic BTL4ModeManager // names (BTL4MODE.HPP): planeMask = the MFD quad plane, subBit = the // engineering sub-screen plane. // struct AuxScreenGeometry { int x, y; ModeMask planeMask; ModeMask subBit; const char *mfdPortName; const char *engPortName; int planeIndex; }; static const AuxScreenGeometry auxScreenGeometry[12] = { { 0, 0xf0, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng1, "mfd1", "eng1", 0 }, // grp 1 { 0x142, 0xf0, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng2, "mfd1", "eng1", 0 }, // grp 2 { 0, 0x1c, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng3, "mfd1", "eng1", 0 }, // grp 3 { 0x142, 0x1c, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng4, "mfd1", "eng1", 1 }, // grp 4 { 0, 0xf0, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng1, "mfd2", "eng2", 1 }, // grp 5 { 0x142, 0xf0, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng2, "mfd2", "eng2", 1 }, // grp 6 { 0, 0x1c, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng3, "mfd2", "eng2", 1 }, // grp 7 { 0x142, 0x1c, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng4, "mfd2", "eng2", 2 }, // grp 8 { 0, 0xf0, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng1, "mfd3", "eng3", 2 }, // grp 9 { 0x142, 0xf0, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng2, "mfd3", "eng3", 2 }, // grp 10 { 0, 0x1c, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng3, "mfd3", "eng3", 2 }, // grp 11 { 0x142, 0x1c, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng4, "mfd3", "eng3", 2 }, // grp 12 }; // // @004cbaf0 -- vehicleSubSystems Make. Build the 8 strip-image pointers from // the method params, then for each roster subsystem (skipping slot 0) build // the panel cluster for its classID onto the auxiliary MFD position. // // STAND-INS (see the aux-screen ledger): the screen number is the sequential // roster assignment; the label .pcc is absent (NULL -> the panel-frame label // blit is skipped). The energy branch's cluster-image pick keyed // *(sub+0x334)==0 -> qcircle / else qcircr in the binary; that word is // BEST-EFFORT identified as the rear-firing flag and read through the // PUBLISHED "RearFiring" attribute here. // Logical VehicleSubSystems::Make( int /*display_port_index*/, Vector2DOf /*position*/, Entity *entity, GaugeRenderer *gauge_renderer ) { L4GaugeRenderer *renderer = (L4GaugeRenderer *)gauge_renderer; char *imageNames[8]; int p, i, count; Check(gauge_renderer); Check(entity); //------------------------------------------------------------ // The 8 status strip bitmaps (qXXX0.pcc..qXXX7.pcc). //------------------------------------------------------------ for (p = 0; p < 8; p++) { imageNames[p] = methodDescription.parameterList[p].data.string; } count = entity->GetSubsystemCount(); for (i = 1; i < count; i++) // slot 0 = the controls mapper { Subsystem *sub = entity->GetSubsystem(i); if (sub == NULL || !IsPanelSubsystem(sub)) { continue; } int auxScreen = AuxScreenAssignmentOf(entity, sub), group = auxScreen - 1; if (group < 0) { DEBUG_STREAM << "Auxiliary screen position = zero\n"; continue; } if (group >= 12) { DEBUG_STREAM << "Illegal auxiliary screen position =" << group << "\n"; continue; } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[gau2] cluster slot=" << i << " classID=" << (int)sub->GetClassID() << " auxScreen=" << auxScreen << " name=" << sub->GetName() << endl << flush; } const AuxScreenGeometry &g = auxScreenGeometry[group]; int mfdPort = renderer->FindGraphicsPort(g.mfdPortName); char *label = NULL, // STAND-IN: no live auxScreenLabel *tile = (char *)"btwarn.pcc"; switch (sub->GetClassID()) { case RegisteredClass::SensorClassID: // 0xBC3 -> HeatSinkCluster (+ "HUD") { Subsystem *hud = entity->FindSubsystem("HUD"); HeatSinkCluster *cluster = new HeatSinkCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub, hud, g.subBit, g.engPortName, tile, label, "edestryd.pcc", imageNames, "SensorCluster"); Register_Object(cluster); break; } case RegisteredClass::MyomersClassID: // 0xBC6 -> MyomerCluster { MyomerCluster *cluster = new MyomerCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub, g.subBit, g.engPortName, tile, label, imageNames, "MyomerCluster"); Register_Object(cluster); break; } case RegisteredClass::EmitterClassID: // 0xBC8 case RegisteredClass::PPCClassID: // 0xBD4 { // // BEST-EFFORT (binary *(sub+0x334)==0): the ring-art pick, read // through the published RearFiring attribute. // const char *clusterImage = "qcircle.pcc"; Logical *rearFiring = (Logical *)AttributePointerOf(sub, "RearFiring"); if (rearFiring != NULL && *rearFiring != 0) { clusterImage = "qcircr.pcc"; } EnergyWeaponCluster *cluster = new EnergyWeaponCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub, g.subBit, g.engPortName, tile, label, clusterImage, imageNames, "EnergyWeaponCluster"); Register_Object(cluster); break; } case RegisteredClass::ProjectileWeaponClassID: // 0xBCD case RegisteredClass::MissileLauncherClassID: // 0xBD0 { BallisticWeaponCluster *cluster = new BallisticWeaponCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub, g.subBit, g.engPortName, tile, label, "qcircle.pcc", imageNames, "smlfont.pcc", "helv42.pcc", "ProjWeaponCluster"); Register_Object(cluster); break; } case RegisteredClass::GaussRifleClassID: // 0xBCE -- not yet supported DEBUG_STREAM << "Gauss rifle not yet supported\n"; break; default: break; } } return True; } // // The registered method: config primitive "vehicleSubSystems(q0..q7.pcc)". // KEYWORD + PARAMETER LIST ARE PARSE-CRITICAL. // MethodDescription VehicleSubSystems::methodDescription = { "vehicleSubSystems", VehicleSubSystems::Make, { { ParameterDescription::typeString, NULL }, // qXXX0.pcc { ParameterDescription::typeString, NULL }, // qXXX1.pcc { ParameterDescription::typeString, NULL }, // qXXX2.pcc { ParameterDescription::typeString, NULL }, // qXXX3.pcc { ParameterDescription::typeString, NULL }, // qXXX4.pcc { ParameterDescription::typeString, NULL }, // qXXX5.pcc { ParameterDescription::typeString, NULL }, // qXXX6.pcc { ParameterDescription::typeString, NULL }, // qXXX7.pcc PARAMETER_DESCRIPTION_END } }; // === btl4gau3.cpp begins at @004c9bd0 (PlayerStatusMappingGroup) -- not this TU.