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=<id> 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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c32d02b3cc
commit
901cf1b459
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int> 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<int>(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();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -390,8 +390,14 @@
|
||||
{
|
||||
public:
|
||||
static Logical Make(int, Vector2DOf<int>, 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<int>, 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
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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=<id> -- 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
|
||||
|
||||
+23
-7
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user