diff --git a/engine/MUNGA/RECEIVER.cpp b/engine/MUNGA/RECEIVER.cpp index 009a3d6..d2d8aba 100644 --- a/engine/MUNGA/RECEIVER.cpp +++ b/engine/MUNGA/RECEIVER.cpp @@ -102,6 +102,54 @@ void { (this->*handler)(what); } + else + { + // + // UNHANDLED-MESSAGE TRACE (input-path audit, systemic cause #1). + // + // A message with no handler was dropped here in complete silence -- no log, + // no counter -- and a DEFAULT-CONSTRUCTED `MessageHandlerSet` has + // entryCount 0 and no parent chain, so it swallows EVERY id. That is how + // the Avionics power bank (108 streamed records: 6 cockpit buttons + keys + // F5-F9), the searchlight toggle, the thermal sight, the crouch button and + // all four generator ON/OFF buttons were dead for months while looking + // perfectly wired. This trace is the standing guard for the whole class: + // one line per (class, message id) pair, first occurrence only, so a newly + // dead control announces itself on the first boot instead of waiting for a + // player report. + // + // Deliberately once-per-pair: some ids are broadcast every frame, and this + // must never become a per-frame log. + // + static const int kMaxSeen = 64; + static struct { const char *name; int id; } seen[kMaxSeen]; + static int seenCount = 0; + + Derivation *derivation = sharedData->derivedClasses; + const char *class_name = (derivation != 0 && derivation->className != 0) + ? derivation->className : ""; + const int id = (int)what->messageID; + + int known = 0; + for (int i = 0; i < seenCount; ++i) + { + if (seen[i].id == id && seen[i].name == class_name) + { + known = 1; + break; + } + } + if (!known && seenCount < kMaxSeen) + { + seen[seenCount].name = class_name; + seen[seenCount].id = id; + ++seenCount; + DEBUG_STREAM << "[msg] UNHANDLED: " << class_name + << " has no handler for message id " << id + << " -- silently dropped (is its MessageHandlerSet chained?)" + << std::endl; + } + } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/game/reconstructed/sensor.cpp b/game/reconstructed/sensor.cpp index 7910fc8..e720756 100644 --- a/game/reconstructed/sensor.cpp +++ b/game/reconstructed/sensor.cpp @@ -100,6 +100,33 @@ Derivation "Sensor" ); +// +// MESSAGE HANDLERS (Gitea #53 family / input-path audit systemic cause #1). +// +// This was a DEFAULT-CONSTRUCTED `MessageHandlerSet`, which is a total blackhole: +// it has `entryCount = 0` and **no parent chain**, so `Find()` returns +// `NullHandler` for every id (`engine/MUNGA/RECEIVER.h`), and `Receiver::Receive` +// drops the message with no log, no warning and no counter. The Sensor is the +// cockpit's **"Avionics"** subsystem, and the streamed control mappings aim +// **108 records across the 18 mechs** at it -- the MFD2 ENG1 page's power bank +// (6 clickable buttons, keys F5-F9): route Avionics power to generator A-D, gen +// mode, coolant cut. Every one of them was silently swallowed. +// +// Sensor needs NO handlers of its own -- those ids (4-8) already exist on +// `PoweredSubsystem`. It only ever needed to CHAIN, which is exactly why the +// byte-identical bank on ENG2 works: Myomers chains (myomers.cpp:88-100) and +// Sensor did not. Same idiom, function-local static so construction order is +// safe. +// +Receiver::MessageHandlerSet& + Sensor::GetMessageHandlers() +{ + static Receiver::MessageHandlerSet messageHandlers( + 0, 0, // no ids of its own + PoweredSubsystem::GetMessageHandlers()); // copy-inherit ids 4-8 + return messageHandlers; +} + Receiver::MessageHandlerSet Sensor::MessageHandlers; @@ -130,7 +157,7 @@ Sensor::AttributeIndexSet Sensor::SharedData Sensor::DefaultData( &Sensor::ClassDerivations, - Sensor::MessageHandlers, + Sensor::GetMessageHandlers(), // was the EMPTY MessageHandlers (blackhole) Sensor::AttributeIndex, Sensor::StateCount ); diff --git a/game/reconstructed/sensor.hpp b/game/reconstructed/sensor.hpp index 8d69969..36c3a99 100644 --- a/game/reconstructed/sensor.hpp +++ b/game/reconstructed/sensor.hpp @@ -90,6 +90,10 @@ static Derivation ClassDerivations; static SharedData DefaultData; static Receiver::MessageHandlerSet MessageHandlers; + // The set the DefaultData actually hands out. Chains + // PoweredSubsystem's ids 4-8 -- the bare `MessageHandlers` above is + // default-constructed and swallows every message (see sensor.cpp). + static Receiver::MessageHandlerSet& GetMessageHandlers(); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BT segment / base-state compatibility shims. diff --git a/scratchpad/avionics.py b/scratchpad/avionics.py new file mode 100644 index 0000000..5262329 --- /dev/null +++ b/scratchpad/avionics.py @@ -0,0 +1,92 @@ +"""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"))