Files
BT411/scratchpad/avionics.py
T
arcattackandClaude Opus 5 33ca99eb69 Input-audit cause #1: revive the Avionics power bank + add the unhandled-message trace
THE CLASS OF BUG.  A default-constructed Receiver::MessageHandlerSet is a total
blackhole: entryCount 0 and NO parent chain, so Find() returns NullHandler for
every id and Receiver::Receive dropped the message with no log, no counter,
nothing.  docs/INPUT_PATH_AUDIT.md ranked this systemic cause #1; running its own
lint finds SEVEN such sets (Sensor, Searchlight, ThermalSight, Gyroscope,
MechTech, SubsystemMessageManager, Torso).  It is also why generators cannot be
switched off (#53) -- same shape.

FIX 1 -- Sensor / Avionics (the big one).  The streamed mappings aim 108 records
across the 18 mechs at it: the MFD2 ENG1 power bank, 6 clickable buttons plus keys
F5-F9 (route Avionics power to generators A-D, gen mode, coolant cut).  All
silently swallowed.  Sensor needed NO handlers of its own -- ids 4-8 already live
on PoweredSubsystem, which chains HeatSink for id 3, covering the whole 3-8 range
those buttons send.  It only ever needed to CHAIN, which is exactly why the
byte-identical bank on ENG2 works (Myomers chains, myomers.cpp:88-100; Sensor did
not).  Added Sensor::GetMessageHandlers() with the same function-local-static
idiom and pointed DefaultData at it instead of the empty set.

VERIFIED LIVE (scratchpad/avionics.py -- pages MFD2 and presses the bank):
  [gensel] Avionics -> GeneratorA (tapped)   ... B, C, D
  [gensel] Avionics mode -> 2
i.e. Avionics now behaves exactly like Myomers.  24 handler hits, 0 unhandled.

FIX 2 -- the standing guard.  Receiver::Receive now logs once per (class,
message id) pair when no handler is found: '[msg] UNHANDLED: <class> has no
handler for message id N -- silently dropped (is its MessageHandlerSet chained?)'.
Once-per-pair on purpose, since some ids broadcast every frame.  This one line
would have printed Avionics, Searchlight, ThermalSight, crouch and all four
generator buttons on the first boot instead of costing us months of player
reports.  Uses SharedData::derivedClasses -> Derivation::className.

DEFERRED, with reasons (not guessed at): Searchlight's ToggleLamp body exists but
is unreachable -- it needs a correctly-typed forwarder (Receiver::Handler is
void(const Message*); ToggleLamp is Logical(Message&), so a cast would be UB that
happens to work on x86) AND an id check, because its id 3 collides with HeatSink's
ToggleCooling and I have not traced whether PowerWatcher's chain reaches HeatSink.
#53 needs more than chaining: ToggleGeneratorOnOff @004b1ed0 has no body at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-25 10:05:57 -05:00

93 lines
3.1 KiB
Python

"""Verify the Sensor/Avionics handler-set fix (input-path audit systemic cause #1).
The MFD2 ENG1 page's power bank -- 6 clickable buttons, keys F5-F9 -- sends
message ids 3-8 to subsystem 14 ("Avionics", class Sensor). Sensor's
MessageHandlerSet was DEFAULT-CONSTRUCTED (entryCount 0, no parent chain), so
Find() returned NullHandler for every id and Receiver::Receive dropped them in
silence: 108 streamed records across the 18 mechs, all dead. Sensor now chains
PoweredSubsystem::GetMessageHandlers() (ids 4-8) which chains HeatSink (id 3).
PASS = a [gensel] line naming Avionics after an F-key press.
Also prints any [msg] UNHANDLED pairs the new trace catches.
Kills only the PID it spawns.
"""
import ctypes
import os
import re
import subprocess
import sys
import time
KEYUP = 0x0002
VK = {"K": 0x4B, "F5": 0x74, "F6": 0x75, "F7": 0x76, "F8": 0x77, "F9": 0x78,
"C": 0x43, "H": 0x48, "N": 0x4E}
user32 = ctypes.windll.user32
REPO = r"C:\git\bt411"
LOG = os.path.join(REPO, "scratchpad_avionics.log")
if os.path.exists(LOG):
os.remove(LOG)
env = dict(os.environ)
env.update({
"BT_START_INSIDE": "1",
"BT_KEY_NOFOCUS": "1",
"BT_DEV_GAUGES": "1",
"BT_FIRE_LOG": "1", # [gensel] lines
"BT_HEAT_LOG": "1",
"BT_MODE_LOG": "1", # [mode] preset page changes
"BT_LOG": LOG,
})
proc = subprocess.Popen(
[os.path.join(REPO, "build", "Release", "btl4.exe"), "-egg", "LAST.EGG"],
cwd=os.path.join(REPO, "content"), env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print("pid", proc.pid)
def tap(name, hold=0.10):
vk = VK[name]
user32.keybd_event(vk, 0, 0, 0)
time.sleep(hold)
user32.keybd_event(vk, 0, KEYUP, 0)
time.sleep(0.25)
try:
deadline = time.time() + 200
while time.time() < deadline:
if os.path.exists(LOG) and "first frame" in open(LOG, errors="replace").read():
break
time.sleep(2)
else:
print("FAIL: never reached the mission"); sys.exit(2)
time.sleep(6)
# page MFD2 through its preset pages, pressing the bank on each
for page in range(5):
tap("K")
time.sleep(1.0)
for k in ("F5", "F6", "F7", "F8", "F9"):
tap(k)
time.sleep(1.5)
print(" page %d: bank pressed" % page)
# and the always-active coolant controls for comparison
tap("C"); tap("H")
time.sleep(3)
finally:
proc.terminate()
time.sleep(1)
t = open(LOG, errors="replace").read() if os.path.exists(LOG) else ""
gensel = re.findall(r"^\[gensel\].*$", t, re.M)
unh = re.findall(r"^\[msg\] UNHANDLED.*$", t, re.M)
print("\n=============== RESULT ===============")
print("[gensel] lines (Sensor fix working):", len(gensel))
for l in gensel[:10]:
print(" ", l[:130])
print("\n[msg] UNHANDLED pairs caught by the new trace:", len(unh))
for l in unh[:14]:
print(" ", l[:130])
print("\nVERDICT:",
"PASS -- Avionics messages now reach a handler" if any("Avionics" in l or "Sensor" in l for l in gensel)
else ("handlers ran but not on Avionics" if gensel else "no [gensel] -- still not reaching a handler"))