266 lines
9.3 KiB
C
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 colour.
|
|
//
|
|
// --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 colour is also one of DVX's
|
|
// 256-colour 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_COLOURS];
|
|
uint8_t palette[GATE_KEYS_COLOURS][GATE_RGB_BYTES];
|
|
uint32_t i;
|
|
uint32_t best;
|
|
uint32_t distance;
|
|
size_t p;
|
|
|
|
gateKeysPalette(words);
|
|
for (i = 0; i < GATE_KEYS_COLOURS; 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_COLOURS, &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;
|
|
}
|