joeylib2/scripts/lib/keysRow.py
2026-09-29 21:25:37 -05:00

98 lines
3.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""keysRow.py - read KEYS's verification row off a screenshot.
KEYS (examples/keys) draws every character it received on its row 198 as
pixel nibbles: the sentinel A 5 A 5, the count byte, a zero byte, then each
character, two pixels a byte, high nibble first. This finds that row in a
screenshot (on a desktop the window is wherever DVX put it) and checks it
holds exactly TARGET.
keysRow.py PNG TARGET [--size WxH] [--at X,Y]
--size: the screenshot must be that size (the full-screen mode, not the
desktop). --at: the row must start exactly there (the stage's own row 198
at 1x on a full screen). Prints the row it found; exit 0 when it is right.
"""
import sys
from PIL import Image
# KEYS palette 0 (buildPalette): a gray ramp with four demo colours and red
# at 15, 12-bit $0RGB, each channel widened x17 as the DVX port's tables do.
PAL12 = [(i << 8) | (i << 4) | i for i in range(16)]
PAL12[0], PAL12[1], PAL12[2], PAL12[3] = 0x000, 0x333, 0x0F0, 0xFFF
PAL12[15] = 0xF00
PAL_RGB = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in PAL12]
# A 16-bit desktop (5-6-5) is off by at most 8 a channel after the round
# trip, the 6-bit VGA DAC by at most 3, while the palette entries sit 17
# apart: nearest is exact.
MAX_DIST = 3 * 9 ** 2
SENTINEL = (0xA, 0x5, 0xA, 0x5)
def main(argv):
if len(argv) < 3:
sys.stderr.write(__doc__)
return 2
png, target = argv[1], argv[2]
size = None
at = None
args = argv[3:]
while args:
if args[0] == "--size" and len(args) > 1:
size = tuple(int(n) for n in args[1].split("x"))
elif args[0] == "--at" and len(args) > 1:
at = tuple(int(n) for n in args[1].split(","))
else:
sys.stderr.write(__doc__)
return 2
args = args[2:]
img = Image.open(png).convert("RGB")
w, h = img.size
px = img.load()
if size is not None and img.size != size:
print("keysRow: FAIL - %s is %dx%d, want %dx%d" % (png, w, h, size[0], size[1]))
return 1
nearest = {}
def nib(x, y):
rgb = px[x, y]
if rgb not in nearest:
best, bestDist = 0, 1 << 30
for n, pal in enumerate(PAL_RGB):
d = sum((c - p) ** 2 for c, p in zip(rgb, pal))
if d < bestDist:
best, bestDist = n, d
nearest[rgb] = best if bestDist <= MAX_DIST else -1
return nearest[rgb]
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
found = None
for y in range(h):
for x in range(w - 2 * len(expect)):
if tuple(nib(x + i, y) for i in range(len(SENTINEL))) == SENTINEL:
row = bytes(((nib(x + 2 * i, y) & 0xF) << 4) | (nib(x + 2 * i + 1, y) & 0xF) for i in range(len(expect)))
if row[:4] == expect[:4]:
found = (x, y, row)
break
if found:
break
if not found:
print("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen" % len(target))
return 1
x, y, row = found
print("keysRow: row at (%d,%d)=%s" % (x, y, row.hex().upper()))
if row != expect:
print("keysRow: FAIL - screen row %s, want %s" % (row.hex().upper(), expect.hex().upper()))
return 1
if at is not None and (x, y) != at:
print("keysRow: FAIL - the row is at (%d,%d), want (%d,%d)" % (x, y, at[0], at[1]))
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))