joeylib2/tests/gate/keysRow.c
2026-10-19 15:46:47 -05:00

266 lines
9.3 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// keysRow.c - read KEYS's verification row off a screenshot.
//
// keysRow PNG TARGET [--size WxH] [--at X,Y] [--scale S]
//
// 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, at the
// window's scale: every stage pixel a SCALE x SCALE block) and checks it
// holds exactly TARGET, and that every block of it is whole: each of its
// screen pixels the same color.
//
// --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). --scale: the row must be at that scale (else
// the first of 1, 2, 3 it is found at). Prints the row it found; exits 0
// when it is right, 1 when not, 2 on a bad command line.
//
// A pixel is the KEYS palette entry nearest it (each 12-bit entry's guns
// widened x17, as the DVX port's tables do), or no entry when none is
// within MAX_DIST: a 15- or 16-bit desktop is off by at most 8 a gun after
// the round trip, the 6-bit VGA DAC by at most 3, while the entries sit 17
// apart, so nearest is exact. (Every KEYS color is also one of DVX's
// 256-color desktop palette, so 8 bits is exact too.)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gateArg.h"
#include "gateImage.h"
#include "toolBuf.h"
#include "toolFile.h"
#define TOOL_NAME "keysRow"
#define FAIL_EXIT 1
#define MAX_DIST (3u * 9u * 9u)
#define NO_NIBBLE (-1)
#define NIBBLE_BITS 4
#define NIBBLE_MASK 0xF
#define SENTINEL_COUNT 4u
#define HEADER_BYTES 4u
#define SCALE_COUNT 3u
#define NIBBLES_A_BYTE 2
static bool findRow(const int8_t *nibs, uint32_t w, uint32_t h, long scale, const ToolBufT *expect, long *foundX, long *foundY, uint8_t *row);
static void nibbleMap(const GateImageT *img, int8_t *nibs);
static uint32_t splitBlocks(const GateImageT *img, long x, long y, long scale, uint32_t count);
static const int8_t kSentinel[SENTINEL_COUNT] = { 0xA, 0x5, 0xA, 0x5 };
static const long kScales[SCALE_COUNT] = { 1, 2, 3 };
static const char *const kUsage[] = {
"keysRow - read KEYS's verification row off a screenshot.",
"",
" keysRow PNG TARGET [--size WxH] [--at X,Y] [--scale S]",
"",
"--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). --scale: the row must be at that scale (else the",
"first of 1, 2, 3 it is found at). Prints the row it found; exit 0 when it",
"is right."
};
// The (x, y, bytes) of the first row at scale whose header matches.
static bool findRow(const int8_t *nibs, uint32_t w, uint32_t h, long scale, const ToolBufT *expect, long *foundX, long *foundY, uint8_t *row) {
long span;
long x;
uint32_t y;
uint32_t i;
const int8_t *line;
bool sentinel;
span = NIBBLES_A_BYTE * (long)expect->len * scale;
for (y = 0; y < h; y++) {
line = nibs + (size_t)y * w;
for (x = 0; x < (long)w - span + 1; x++) {
sentinel = true;
for (i = 0; i < SENTINEL_COUNT && sentinel; i++) {
sentinel = line[x + (long)i * scale] == kSentinel[i];
}
if (!sentinel) {
continue;
}
for (i = 0; i < expect->len; i++) {
row[i] = (uint8_t)(((line[x + NIBBLES_A_BYTE * (long)i * scale] & NIBBLE_MASK) << NIBBLE_BITS) | (line[x + (NIBBLES_A_BYTE * (long)i + 1) * scale] & NIBBLE_MASK));
}
if (memcmp(row, expect->data, HEADER_BYTES) == 0) {
*foundX = x;
*foundY = (long)y;
return true;
}
}
}
return false;
}
// Every pixel's KEYS palette index, NO_NIBBLE where no entry is near.
static void nibbleMap(const GateImageT *img, int8_t *nibs) {
uint16_t words[GATE_KEYS_COLORS];
uint8_t palette[GATE_KEYS_COLORS][GATE_RGB_BYTES];
uint32_t i;
uint32_t best;
uint32_t distance;
size_t p;
gateKeysPalette(words);
for (i = 0; i < GATE_KEYS_COLORS; i++) {
gateRgb12(words[i], palette[i]);
}
for (p = 0; p < (size_t)img->width * img->height; p++) {
best = gateNearest(img->rgba + p * GATE_RGBA_BYTES, (const uint8_t (*)[GATE_RGB_BYTES])palette, GATE_KEYS_COLORS, &distance);
nibs[p] = (distance <= MAX_DIST) ? (int8_t)best : (int8_t)NO_NIBBLE;
}
}
// How many of the count stage pixels from (x, y) are not whole blocks.
static uint32_t splitBlocks(const GateImageT *img, long x, long y, long scale, uint32_t count) {
uint32_t split;
uint32_t i;
long left;
long dx;
long dy;
const uint8_t *want;
bool whole;
split = 0u;
for (i = 0; i < count; i++) {
left = x + (long)i * scale;
want = gateImagePixel(img, (uint32_t)left, (uint32_t)y);
whole = y + scale <= (long)img->height;
for (dy = 0; dy < scale && whole; dy++) {
for (dx = 0; dx < scale && whole; dx++) {
whole = memcmp(gateImagePixel(img, (uint32_t)(left + dx), (uint32_t)(y + dy)), want, GATE_RGB_BYTES) == 0;
}
}
if (!whole) {
split++;
}
}
return split;
}
int main(int argc, char **argv) {
GateImageT img;
ToolBufT expect;
int8_t *nibs;
uint8_t *row;
const long *scales;
uint32_t scaleCount;
long oneScale;
long sizeW;
long sizeH;
long atX;
long atY;
long x;
long y;
long scale;
bool haveSize;
bool haveAt;
bool found;
uint32_t split;
uint32_t i;
int arg;
toolFileName(TOOL_NAME);
if (argc < 3) {
return gateUsage(kUsage, GATE_COUNT_OF(kUsage));
}
haveSize = false;
haveAt = false;
scales = kScales;
scaleCount = SCALE_COUNT;
sizeW = 0;
sizeH = 0;
atX = 0;
atY = 0;
oneScale = 0;
for (arg = 3; arg < argc; arg += 2) {
if (arg + 1 >= argc) {
return gateUsage(kUsage, GATE_COUNT_OF(kUsage));
}
if (strcmp(argv[arg], "--size") == 0) {
gateArgPair("size", argv[arg + 1], 'x', &sizeW, &sizeH);
haveSize = true;
} else if (strcmp(argv[arg], "--at") == 0) {
gateArgPair("at", argv[arg + 1], ',', &atX, &atY);
haveAt = true;
} else if (strcmp(argv[arg], "--scale") == 0) {
oneScale = gateArgLong("scale", argv[arg + 1]);
if (oneScale < 1) {
gateArgBad("scale", argv[arg + 1]);
}
scales = &oneScale;
scaleCount = 1u;
} else {
return gateUsage(kUsage, GATE_COUNT_OF(kUsage));
}
}
gateImageLoad(argv[1], &img);
if (haveSize && ((long)img.width != sizeW || (long)img.height != sizeH)) {
printf("keysRow: FAIL - %s is %ux%u, want %ldx%ld\n", argv[1], (unsigned)img.width, (unsigned)img.height, sizeW, sizeH);
return FAIL_EXIT;
}
nibs = (int8_t *)malloc((size_t)img.width * img.height + 1u);
if (nibs == NULL) {
toolFail("out of memory");
}
nibbleMap(&img, nibs);
memset(&expect, 0, sizeof(expect));
gateKeysExpect(argv[2], &expect);
row = (uint8_t *)malloc(expect.len);
if (row == NULL) {
toolFail("out of memory");
}
found = false;
scale = scales[0];
x = 0;
y = 0;
for (i = 0; i < scaleCount && !found; i++) {
scale = scales[i];
found = findRow(nibs, img.width, img.height, scale, &expect, &x, &y, row);
}
if (!found) {
printf("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen at ", (unsigned)strlen(argv[2]));
for (i = 0; i < scaleCount; i++) {
printf("%s%ldx", (i == 0u) ? "" : " or ", scales[i]);
}
printf("\n");
return FAIL_EXIT;
}
printf("keysRow: row at (%ld,%ld) at %ldx=", x, y, scale);
gateHexPrint(row, expect.len);
printf("\n");
if (memcmp(row, expect.data, expect.len) != 0) {
printf("keysRow: FAIL - screen row ");
gateHexPrint(row, expect.len);
printf(", want ");
gateHexPrint(expect.data, expect.len);
printf("\n");
return FAIL_EXIT;
}
split = splitBlocks(&img, x, y, scale, NIBBLES_A_BYTE * (uint32_t)expect.len);
if (split != 0u) {
printf("keysRow: FAIL - %u of the row's %u stage pixels are not whole %ldx%ld blocks\n", (unsigned)split, (unsigned)(NIBBLES_A_BYTE * expect.len), scale, scale);
return FAIL_EXIT;
}
if (haveAt && (x != atX || y != atY)) {
printf("keysRow: FAIL - the row is at (%ld,%ld), want (%ld,%ld)\n", x, y, atX, atY);
return FAIL_EXIT;
}
free(row);
free(nibs);
toolBufFree(&expect);
gateImageFree(&img);
return 0;
}