// 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 #include #include #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; }