From 81fbfc7eaaf49407094a89f420105af28a756936 Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 25 Jul 2026 07:48:29 -0500 Subject: [PATCH] Add scratchpad/cursoraudit.py: static audit for the #42 stale-cursor draw family Scans the gauge TUs for cursor-relative draws with no MoveToAbsolute in scope. Found 6 of 29, three of them STATE-CHANGE-ONLY draws -- notably L4GraphicLamp::NotifyOfStateChange (L4LAMP.cpp:376/380), which blits a lamp using whatever cursor the view happens to hold. That is the '#48 misplaced lamp' class: lamp-shaped, on the panels that have lamps, only on damage/jam/heat state changes, and persistent. Explains why the 5-pilot rig capture was clean (no combat = no lamp state change). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg --- scratchpad/cursoraudit.py | 75 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 scratchpad/cursoraudit.py diff --git a/scratchpad/cursoraudit.py b/scratchpad/cursoraudit.py new file mode 100644 index 0000000..4ad9acd --- /dev/null +++ b/scratchpad/cursoraudit.py @@ -0,0 +1,75 @@ +"""#48 audit: find gauge draws that are NOT preceded by a cursor set. + +The #42 family: a draw that relies on the CURRENT cursor works by accident the +first time (the cursor happens to be right) and lands in the wrong place on a +later redraw. Draws that only fire on a STATE CHANGE -- damage, heat, jam, +alarm -- are the dangerous ones: they are rare, so the bug hides until gameplay +triggers them, and nothing erases the stray pixels afterwards. + +Relative/cursor-relative draws (the risky ones) vs absolute draws (safe): + RISKY : DrawFilledRectangleToRelative, DrawBitMap*, DrawLineToRelative, + DrawPointToRelative, DrawFilledRectangleToAbsolute (uses cursor as one corner) + SAFE : anything taking explicit x,y (DrawPortBackground, NumericDisplay::Draw + which carries its own coords) + +Reports each risky draw with the distance back to the nearest MoveToAbsolute / +MoveToRelative in the same function, and flags any that has none. +""" +import os +import re + +FILES = [ + "game/reconstructed/btl4gau2.cpp", # weapon panels + eng pages (artifacts seen here) + "game/reconstructed/btl4gau3.cpp", # pilotList, PlayerStatus, MessageBoard + "game/reconstructed/btl4gau1.cpp", + "game/reconstructed/btl4grnd.cpp", + "engine/MUNGA_L4/L4GAUGE.cpp", + "engine/MUNGA_L4/L4LAMP.cpp", +] + +RISKY = re.compile(r"\.(DrawFilledRectangleToRelative|DrawBitMapOpaque|DrawBitMap|" + r"DrawLineToRelative|DrawPointToRelative|" + r"DrawFilledRectangleToAbsolute)\s*\(") +CURSOR = re.compile(r"\.(MoveToAbsolute|MoveToRelative)\s*\(") +FUNCHEAD = re.compile(r"^(?:\t|)([A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*)\s*\(") + +root = r"C:\git\bt411" +findings = [] +for rel in FILES: + path = os.path.join(root, rel.replace("/", os.sep)) + if not os.path.exists(path): + continue + lines = open(path, encoding="utf-8", errors="replace").read().splitlines() + func = "?" + func_start = 0 + last_cursor = -1 + for i, ln in enumerate(lines): + m = FUNCHEAD.match(ln) + if m: + func = m.group(1) + func_start = i + last_cursor = -1 # cursor state does not carry across functions + if CURSOR.search(ln): + last_cursor = i + mm = RISKY.search(ln) + if mm: + gap = (i - last_cursor) if last_cursor >= func_start else None + findings.append((rel, i + 1, func, mm.group(1), gap, ln.strip()[:78])) + +print("=" * 100) +print("RISKY DRAWS WITH **NO** CURSOR SET IN THE ENCLOSING FUNCTION (top suspects)") +print("=" * 100) +none = [f for f in findings if f[4] is None] +for rel, line, func, call, gap, src in none: + print(" %-32s :%-5d %-42s %s" % (rel.split("/")[-1], line, func, call)) + print(" %s" % src) +print("\n -> %d of %d risky draws have NO cursor set in scope\n" % (len(none), len(findings))) + +print("=" * 100) +print("RISKY DRAWS WHOSE CURSOR SET IS FAR AWAY (>12 lines -- may be on another branch)") +print("=" * 100) +far = [f for f in findings if f[4] is not None and f[4] > 12] +for rel, line, func, call, gap, src in far: + print(" %-32s :%-5d gap=%-4d %-38s %s" % (rel.split("/")[-1], line, gap, func, call)) +print("\n -> %d far-cursor draws" % len(far)) +print("\ntotal risky draws scanned: %d" % len(findings))