Files
BT411/scratchpad/cursoraudit.py
T
arcattackandClaude Opus 5 81fbfc7eaa 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-25 07:48:29 -05:00

76 lines
3.2 KiB
Python

"""#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))