From 901cf1b459b8921b7ef5f8d8c20b5189ba572fd7 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Tue, 28 Jul 2026 13:22:21 -0500 Subject: [PATCH] four ENG-page lamps were invisible, not missing: the ctors dropped their colour parameters Playtest report: "generators don't seem to have an Auto setting" / "no auto mode is ever displayed", plus the operator's own memory that the BUS MODE button once stepped through three states. The state machine was NEVER wrong: @004b0abc has exactly two branches (<2 -> Auto(2); ==2 -> detach + Off(0)), Manual(1) is set only by the four SelectGenerator buttons, and the auto-hunt gates on ==2 -- all byte-verified, and the #62 re-attach fires live. Manual is a one-way door out of BUS MODE by design: a single cycle button cannot know WHICH generator manual should mean. Verified on-screen by the operator this session: A-D returns to manual, BUS MODE toggles auto/off thereafter. What was actually broken: the connect-mode lamp -- and three siblings -- never drew a pixel. OneOfSeveralStates (@004c5470) and OneOfSeveralInt (@004c5148) forward caller-supplied background/foreground colours in the binary; the recon dropped both parameters and hardcoded 0,0 into the base. A BitMap-strip lamp draws SetColor(bg) + DrawBitMapOpaque(fg,...), so 0/0 painted colour-0 on colour-0: invisible. Affected: btemode (connect mode, 1x3 -- THE report), btecmode (coolant on/off, 1x2), and both bteseek gear-step lamps (1x4). The cluster call sites pass 0xff/0, byte-verified (@004c866c disasm; part_014.c:1479-1481, :2146-2147). Same dropped-element family as issue #42's MoveToAbsolute. THE TRAP THAT WAS NOT LANDED, recorded so it stays unlanded: with the lamp first made visible, the frames appeared inverted against the levels (art reads AUTO/MANUAL/OFF top-down; levels run OFF/MANUAL/AUTO), and a row=(rows-1)-selected "fix" was proposed. An adversarial workflow proved it wrong: the gauge blit addresses SOURCE rows BOTTOM-UP (Video16BitBuffered::DrawBitMapOpaque, L4VB16.cpp:3846-3850 -- sTop = map_max_y - sTop, rows walked upward), the 1995 blit @0046bdfc performs the identical flip, and the vertical strips are AUTHORED bottom-up to match. Identity level->row therefore draws the pod-correct display; the inversion would have created the very bug it claimed to cure. Two of five investigators (and the first human pass) assumed top-down; the engine source overruled all three. Convention + warning now recorded in context/gauges-hud.md. btecmode doubles as the standing tripwire: a second vertical strip that must show ON when coolant is available -- if it ever reads inverted, the bottom-up verdict is falsified. Also corrected: @004c552c is OneOfSeveralStates' EXECUTE override (clamp >= 0, chain the base draw) -- the port had the body on BecameActive under a wrong label; vtable-diffed against OneOfSeveral (0x518b24 vs 0x518bf0). BecameActive is inherited. Behaviorally inert today (the state source never goes negative), byte-faithful now. README: the "KNOWN ISSUE -- automatic re-attach is not working yet" text is replaced with how power routing actually works (A-D = manual, BUS MODE = auto then off, two presses off / one back to auto). That text shipped in 600, which already contained the #62 fix -- players were being TOLD auto was broken while it worked, which is half of how a painted-out lamp became "no auto mode". Diagnostic kept: BT_GENSEL_TEST= now drives any of the five power-routing message ids (4..8, default 7) and pulses four times, so the whole mode cycle is observable headlessly under BT_FIRE_LOG ([gensel] lines). Co-Authored-By: Claude Opus 5 (1M context) --- context/gauges-hud.md | 28 ++++++++++++++++++++++ game/reconstructed/btl4gau2.cpp | 20 +++++++++++----- game/reconstructed/btl4gaug.cpp | 42 ++++++++++++++++++++++++++------- game/reconstructed/btl4gaug.hpp | 19 ++++++++++++--- game/reconstructed/mech4.cpp | 17 ++++++++++++- players/README.txt | 30 +++++++++++++++++------ 6 files changed, 130 insertions(+), 26 deletions(-) diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 95518f2..7c9370e 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -654,6 +654,34 @@ Player CALLSIGN labels (kill/damage feed, radar/target tags, score display) are BITMAPS from the egg, not text — format, renderers, and the operator-console generator: [[multiplayer]] §OPERATOR-SET CALLSIGNS. +## Lamp strips: the BOTTOM-UP source-rect convention (do NOT "fix" the flip) [T0/T1] + +**The gauge blit addresses SOURCE rows bottom-up, and the vertical strip artwork is authored +to match.** `Video16BitBuffered::DrawBitMapOpaque` converts the incoming source rect with +`sTop = map_max_y - sTop; sBottom = map_max_y - sBottom` and walks rows upward +(`engine/MUNGA_L4/L4VB16.cpp:3846-3850`, `UP_BITMAP` :2155) [T0]; the 1995 display blit +@0046bdfc performs the IDENTICAL flip [T1 disasm], and the PCC→BitMap loader stores the PCX +top scanline at memory row 0 unflipped (`GRAPH2D.cpp:27-174`). Net: in `OneOfSeveral::Execute` +`sy0 = row*frameHeight` selects frames counting from the **BOTTOM** of the image as authored. +The vertical strips are painted for exactly this: `BTEMODE.PCC` (1×3) reads AUTO/MANUAL/OFF +**top-down**, i.e. OFF is the bottom frame = value 0 — so identity level→row draws the pod- +correct display (`0=OFF, 1=MANUAL, 2=AUTO`). Horizontal strips (`btebus`/`btpbus`/`bteloop`, +rows=1) are insensitive to the convention. **A "row = (rows-1)-selected" correction here is a +trap** — it was nearly landed 2026-07-28 on the assumption the blit is top-down; it would +invert every vertical lamp. (An adversarial workflow settled this against two of five +investigators; the port and the binary agree end-to-end.) Residual [T3]: the `DrawPixelMap8` +branch's source-y convention is untraced — verify before authoring any vertical PixMap8 strip. + +**The dropped-colour-parameter family (2026-07-28):** the binary's `OneOfSeveralStates` +(@004c5470) and `OneOfSeveralInt` (@004c5148) ctors forward caller-supplied +background/foreground colours; the recon dropped both parameters and hardcoded `0,0`, so a +BitMap-strip lamp drew `SetColor(0)` + `DrawBitMapOpaque(0,…)` = invisible. Hit every one of: +`btemode` (connect mode, 1×3 — reported as "no Auto mode exists"), `btecmode` (coolant on/off, +1×2), both `bteseek` gear-step lamps (1×4). Cluster call sites pass `0xff,0` +(part_014.c:1479-1481, :2146-2147). Same bug class as issue #42's dropped `MoveToAbsolute`. +Also corrected: @004c552c is OneOfSeveralStates' **Execute** override (clamp ≥0, chain base), +not `BecameActive` — vtable-diffed. + ## Key Relationships - Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`. - Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state. diff --git a/game/reconstructed/btl4gau2.cpp b/game/reconstructed/btl4gau2.cpp index e4c3679..9571266 100644 --- a/game/reconstructed/btl4gau2.cpp +++ b/game/reconstructed/btl4gau2.cpp @@ -1466,8 +1466,9 @@ SubsystemCluster::SubsystemCluster( engPort, 0x254, 0x13b, "bteloop.pcc", 7, 1, subsystem_in, "CoolingLoop"); modeLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148 - engPort, 400, 10, "btecmode.pcc", 1, 2, - (int *)((char *)subsystem_in + 0x134), "OneOfSeveralInt"); // BEST-EFFORT raw offset + engPort, 400, 10, "btecmode.pcc", 0xff, 0, 1, 2, // colours byte-verified: + (int *)((char *)subsystem_in + 0x134), "OneOfSeveralInt"); // part_014.c:1479-1481 + // (+0x134 = coolantAvailable) if (inputVoltage == NULL) { @@ -1488,9 +1489,16 @@ SubsystemCluster::SubsystemCluster( // stateLamp gate: IsDerivedFrom 0x50f4bc = POWEREDSUBSYSTEM (#47 correction -- // the old 'Generator' label was wrong; every powered subsystem passes). if (IsGeneratorDerived(subsystem_in)) + // bg=0xff fg=0 -- byte-verified from the binary's own call site + // @004c866c (`push 3 / push 1 / push 0 / push 0xff / push + // "btemode.pcc"`). These were previously dropped and hardcoded 0/0 + // inside the ctor, which painted the lamp in colour 0: the connect + // mode cycled correctly but nothing was ever drawn, so Auto looked + // like it did not exist. stateLamp = new OneOfSeveralStates(ChildRate(), eng_mode, renderer_in, // @004c5470 - engPort, 0xbe, 0, "btemode.pcc", 1, 3, - (Entity *)((char *)subsystem_in + 0x2b8), "OneOfSeveralStates"); // BEST-EFFORT raw + engPort, 0xbe, 0, "btemode.pcc", 0xff, 0, 1, 3, + (Entity *)((char *)subsystem_in + 0x2b8), "OneOfSeveralStates"); // +0x2b8 = modeAlarm + // (offset static_asserted) else stateLamp = NULL; } @@ -1748,7 +1756,7 @@ MyomerCluster::MyomerCluster( (AttributeAccessor *)&seekValue, "edestryd.pcc", False, "EngrGraph"); seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148 - engPort, 0xf, 0, "bteseek.pcc", 1, 4, + engPort, 0xf, 0, "bteseek.pcc", 0xff, 0, 1, 4, // colours byte-verified: part_014.c:2146-2147 // gauge-complete wave: 0x800 was OOB for a 0x358 Myomers (garbage) -> the seek // gear step is Myomers::currentSeekVoltageIndex@0x320 (the analog of the // EnergyWeaponCluster's Emitter::seekVoltageIndex@0x3f0; same 1..4 semantics). @@ -1934,7 +1942,7 @@ EnergyWeaponCluster::EnergyWeaponCluster( "edestryd.pcc", True, "EngrGraph"); seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148 - engPort, 0xf, 0, "bteseek.pcc", 1, 4, + engPort, 0xf, 0, "bteseek.pcc", 0xff, 0, 1, 4, // colours byte-verified: part_014.c:2146-2147 (int *)((char *)subsystem_in + 0x3f0), "OneOfSeveralInt"); // BEST-EFFORT raw } diff --git a/game/reconstructed/btl4gaug.cpp b/game/reconstructed/btl4gaug.cpp index 66efcdb..50f11f3 100644 --- a/game/reconstructed/btl4gaug.cpp +++ b/game/reconstructed/btl4gaug.cpp @@ -1556,8 +1556,8 @@ void // // @004c5390 Make / @004c5470 ctor -- OneOfSeveralStates (vtable PTR_FUN_00518b24): // OneOfSeveral(fromImageStrip=1) + a StateConnection (FUN_004c3324) reading the -// subsystem state word @0x14. BecameActive @004c552c clamps the state >= 0 -// before chaining to OneOfSeveral::Execute @004c4f28. dtor @004c5500. +// subsystem state word @0x14. @004c552c is the EXECUTE override (clamp >= 0, +// chain OneOfSeveral::Execute @004c4f28); BecameActive is inherited. dtor @004c5500. // // @@ -1917,13 +1917,20 @@ void HorizTwoPartBar::Execute() // @004c5148 -- OneOfSeveralInt ctor (vtable 0x518bac): OneOfSeveral(fromStrip=1) + // a GaugeConnectionDirectOf feeding the selected frame. dtor @004c51d8. // +// Colours are CALLER-SUPPLIED (fixed 2026-07-28, same dropped-parameter bug as +// OneOfSeveralStates): hardcoding 0/0 into the base drew every OneOfSeveralInt +// lamp in colour 0 -- btecmode (coolant on/off) and both bteseek gear-step +// lamps were invisible. Binary: FUN_004c5148 forwards its args; call sites +// pass 0xff/0 (part_014.c:1479-1481 and :2146-2147). OneOfSeveralInt::OneOfSeveralInt( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, const char *image, + int background_color, int foreground_color, int columns, int rows, int *value_pointer, const char *identification_string ): OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number, x, y, - True, image, 0, 0, columns, rows, identification_string) + True, image, background_color, foreground_color, columns, rows, + identification_string) { selected = 0; AddConnection(new GaugeConnectionDirectOf(0, &selected, value_pointer)); // FUN_004749de @@ -1933,16 +1940,28 @@ OneOfSeveralInt::~OneOfSeveralInt() {} // @004c51d8 // // @004c5470 -- OneOfSeveralStates ctor (vtable 0x518b24): OneOfSeveral + a -// StateConnection reading the subsystem state word @0x14. BecameActive @004c552c -// clamps the state >= 0 then chains OneOfSeveral::Execute. dtor @004c5500. +// StateConnection reading the subsystem state word @0x14. @004c552c is the +// EXECUTE override (clamp >= 0, chain the base); BecameActive is inherited. +// dtor @004c5500. // +// THE COLOURS ARE CALLER-SUPPLIED (fixed 2026-07-28). This ctor used to drop +// them and hardcode 0/0 into the base. For a BitMap strip Execute does +// `SetColor(backgroundColor)` then `DrawBitMapOpaque(foregroundColor, ...)`, so +// 0/0 draws the lamp in colour 0 and it is invisible -- which is why the ENG +// page's connect-mode lamp (btemode.pcc) never appeared and pilots reported +// "there is no Auto setting": the mode was cycling correctly underneath an +// unpainted lamp. The binary's ctor @004c5470 forwards its own arguments to +// the base (only fromImageStrip is a literal 1), and its eng-page caller +// @004c866c pushes `0xff` then `0` -- background 0xFF, foreground 0. OneOfSeveralStates::OneOfSeveralStates( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, const char *image, - int columns, int rows, Entity *subsystem_source, const char *identification_string + int background_color, int foreground_color, int columns, int rows, + Entity *subsystem_source, const char *identification_string ): OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number, x, y, - True, image, 0, 0, columns, rows, identification_string) + True, image, background_color, foreground_color, columns, rows, + identification_string) { selected = 0; AddConnection(new StateConnection(&selected, subsystem_source)); // FUN_004c3324 @@ -1950,11 +1969,16 @@ OneOfSeveralStates::OneOfSeveralStates( OneOfSeveralStates::~OneOfSeveralStates() {} // @004c5500 -void OneOfSeveralStates::BecameActive() // @004c552c +// @004c552c -- the EXECUTE override (corrected 2026-07-28; the port had this +// body on BecameActive). The binary's OneOfSeveralStates vtable was dumped and +// diffed against OneOfSeveral's: the one differing slot is Execute, whose body +// clamps a negative state to 0 and chains the base draw. On the base class +// BecameActive just forces a redraw (previousSelected = -1) -- inherited. +void OneOfSeveralStates::Execute() // @004c552c { if (selected < 0) selected = 0; - OneOfSeveral::BecameActive(); + OneOfSeveral::Execute(); } // diff --git a/game/reconstructed/btl4gaug.hpp b/game/reconstructed/btl4gaug.hpp index 864bcec..a615721 100644 --- a/game/reconstructed/btl4gaug.hpp +++ b/game/reconstructed/btl4gaug.hpp @@ -390,8 +390,14 @@ { public: static Logical Make(int, Vector2DOf, Entity *, GaugeRenderer *); + // background/foreground are CALLER-SUPPLIED -- the binary's cluster call + // sites pass 0xff/0 (btecmode: part_014.c:1479-1481, bteseek: :2146-2147). + // They were dropped here and hardcoded 0/0, which drew these lamps in + // colour 0 = invisible (the same dropped-parameter bug as + // OneOfSeveralStates / the btemode connect-mode lamp). OneOfSeveralInt(GaugeRate, ModeMask, L4GaugeRenderer *, int, - int x, int y, const char *image, int columns, int rows, + int x, int y, const char *image, + int background_color, int foreground_color, int columns, int rows, int *value_pointer, const char *); ~OneOfSeveralInt(); // @004c51d8 }; @@ -425,11 +431,18 @@ { public: static Logical Make(int, Vector2DOf, Entity *, GaugeRenderer *); + // background/foreground are CALLER-SUPPLIED (@004c5470 forwards its own + // args to the base; only fromImageStrip is a literal). They used to be + // dropped here and hardcoded 0/0 in the ctor -- see the note there. OneOfSeveralStates(GaugeRate, ModeMask, L4GaugeRenderer *, int, - int x, int y, const char *image, int columns, int rows, + int x, int y, const char *image, + int background_color, int foreground_color, int columns, int rows, Entity *subsystem_source, const char *); ~OneOfSeveralStates(); // @004c5500 - void BecameActive(); // @004c552c + // @004c552c is the EXECUTE override (vtable-diffed 2026-07-28): clamp a + // negative state to 0, then chain OneOfSeveral::Execute. It was + // mislabeled BecameActive here; BecameActive is inherited from the base. + void Execute(); // @004c552c }; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 97b3560..21e2d2e 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -5518,12 +5518,27 @@ void // task #12 scripted verify (BT_GENSEL_TEST=1): one SelectGeneratorD // press at frame ~2500 -- the PPC re-taps from its authored GeneratorA // onto GeneratorD; the charging I^2R then lands on GenD ([heat-t]). + // BT_GENSEL_TEST= -- the id defaults to 7 (SelectGeneratorD, the + // original task-#12 verify). Set it to 8 to drive ToggleGeneratorMode + // and watch the three-level cycle: SelectGenerator leaves the subsystem + // at Connected(1), then presses run 1 -> Auto(2) -> Manual/off(0) -> + // Auto(2) -> ... Pulsed FOUR times so the whole cycle is observable in + // one headless run ([gensel] lines under BT_FIRE_LOG). if ((Entity *)this == application->GetViewpointEntity() && getenv("BT_GENSEL_TEST")) { static int s_gsFrame = 0; + static const int s_gsID = + (atoi(getenv("BT_GENSEL_TEST")) > 0) ? atoi(getenv("BT_GENSEL_TEST")) : 7; ++s_gsFrame; - gBTGenSelKey = (s_gsFrame >= 600 && s_gsFrame < 610) ? 7 : 0; + int pulse = 0; + for (int p = 0; p < 4; ++p) + { + const int at = 600 + p * 150; + if (s_gsFrame >= at && s_gsFrame < at + 10) + pulse = 1; + } + gBTGenSelKey = pulse ? s_gsID : 0; } // task #13 scripted verify (BT_VALVE_TEST=1): one MoveValve press at diff --git a/players/README.txt b/players/README.txt index b9b419f..e4caee4 100644 --- a/players/README.txt +++ b/players/README.txt @@ -122,13 +122,29 @@ CONTROLS (keyboard) -- NEW DEFAULT LAYOUT THIS BUILD: weapon panel's red PROGRAM button with the mouse and tap a fire key; the joystick diagram on the panel shows the binding live. -KNOWN ISSUE THIS BUILD -- a weapon that goes dark after you press ENG-page -buttons: the BUS MODE button detaches that weapon from its generator on -its second press, and the automatic re-attach is not working yet. The -symptom is a weapon whose recharge arc and ready dot are BOTH blank and -which will not fire, while its panel still shows normal data. THE FIX: -press one of the GENERATOR SELECT buttons (A/B/C/D) on that weapon's -panel and it comes straight back. Not a lost mech -- just that button. +FIXED THIS BUILD -- the automatic re-attach now works. Previously a weapon +detached by the BUS MODE button stayed dark for the rest of the mission and +you had to rescue it with a GENERATOR SELECT button. It now recovers by +itself, as the arcade did. + +HOW POWER ROUTING WORKS (the ENG page). Each weapon draws from one +generator, and it has three routing modes: + GENERATOR SELECT A/B/C/D -- you pick that generator by hand. + BUS MODE, first press ---- AUTO: the mech hunts for any live generator + and attaches to it by itself. This is what + recovers a weapon whose generator died. + BUS MODE, second press --- detached. The weapon goes dark on purpose -- + blank recharge arc, no ready dot, will not + fire. Press BUS MODE again for AUTO, or a + GENERATOR SELECT button to choose one. +So it is TWO presses of BUS MODE to take a weapon off line, and one more to +put it back on AUTO. There is no on-screen label for the current mode -- +that is authentic, the pod had none either. You tell by behaviour: on AUTO +a weapon with a working generator comes back on its own. + +NOTE these are two different buttons: BUS MODE routes a WEAPON to a +generator, while the GENERATOR A-D buttons on the radar rail (F9-F12) turn +the GENERATORS THEMSELVES on and off. An Xbox controller works out of the box. FLIGHT STICKS / HOTAS / pedals: run joyconfig.bat ONCE -- a wizard asks you to move each