98 lines
3.5 KiB
Python
Executable file
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))
|