joeylib2/tests/gate/joyScreen.c

374 lines
13 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// joyScreen.c - read both joysticks' state off a screenshot of JOY.
//
// joyScreen PNG [--scale S] [--st] [--at X,Y]
//
// JOY (examples/joy) shows each stick in a frame: a dot that sits in the
// middle at rest and moves DOT_TRAVEL stage pixels per full axis
// (jlJoystickX/Y * DOT_TRAVEL / 127, grey at rest, green away from it), two
// button squares (yellow while down) and a patch under them, green when the
// port reports a joystick and red when it does not. This finds the stage in
// a screenshot (on a desktop the window is wherever DVX put it, at the
// window's scale) by the two patches, and prints each stick's state as five
// characters: connected (1/0), X and Y (- 0 +: the dot at least half its
// travel left or up, in the middle, at least half its travel right or
// down), buttons 0 and 1 (1/0); '?' for anything else (a dot part way from
// the middle, a colour that is not JOY's). Half way, as DVX's own joystick
// tests judge an axis: an uncalibrated DVX axis ranges itself as it moves,
// so an emulated stick's full push reads a little short of the end.
//
// --st: the screen shows the plain Atari ST's 3-bit palette, which drops
// each channel's low bit (Hatari shows 3-bit n as the STe's 4-bit 2n).
// --at: the stage starts there on the screen (at the scale --scale gives,
// else 1), so the patches need not be seen: where something covers one (the
// PSP's keyboard), its stick's connected character is '?'.
//
// Prints "A=<state> B=<state> scale=<S> dots=<A's x,y offset> <B's>"; exit
// 1 when the stage is not found or a stick cannot be read, 2 on a bad
// command line.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gateArg.h"
#include "gateImage.h"
#include "toolFile.h"
#define TOOL_NAME "joyScreen"
#define FAIL_EXIT 1
// examples/joy/joy.c: its layout (stage pixels).
#define STICK_COUNT 2u
#define STICK_TOP 16
#define FRAME_SIZE 80
#define FRAME_INSET 2
#define DOT_SIZE 8
#define DOT_TRAVEL ((FRAME_SIZE - DOT_SIZE) / 2 - FRAME_INSET)
#define BUTTON_W 24
#define BUTTON_H 16
#define BUTTON_GAP 4
#define BUTTON_COUNT 2u
#define ROW_GAP 6
#define BUTTON_TOP (STICK_TOP + FRAME_SIZE + ROW_GAP)
#define STATUS_W 80
#define STATUS_H 4
#define STATUS_TOP (BUTTON_TOP + BUTTON_H + ROW_GAP)
// A 15-bit desktop is off by at most 8 a channel, the 6-bit VGA DAC by at
// most 3; JOY's colours sit much further apart. (All are exact in DVX's
// 256-colour desktop palette.)
#define MAX_DIST (3u * 9u * 9u)
#define SCALE_COUNT 3u
#define UNKNOWN '?'
#define FULL_MASK 0xFFFu
// A plain ST's 3-bit palette keeps a channel's top three bits.
#define ST_CHANNEL_MASK 0xEEEu
// A state: connected, X, Y, and the two buttons; an offset "+dd,+dd".
#define STATE_CONNECTED 0u
#define STATE_X 1u
#define STATE_Y 2u
#define STATE_BUTTONS 3u
#define STATE_CHARS 5u
#define OFFSET_CHARS 24u
// JOY's colours, in the order the nearest is chosen among equals.
typedef enum JoyColourE {
COLOUR_BACKGROUND,
COLOUR_FRAME,
COLOUR_DOT_OFF,
COLOUR_DOT_ON,
COLOUR_BUTTON_OFF,
COLOUR_BUTTON_ON,
COLOUR_DISCONNECTED,
COLOUR_COUNT,
COLOUR_NONE = -1
} JoyColourE;
// The patch's green is the dot's (COLOR_CONNECTED = COLOR_DOT_ON).
#define COLOUR_CONNECTED COLOUR_DOT_ON
// The screenshot as JOY's colour names, and the stage's place and scale.
typedef struct ScreenT {
uint32_t w;
uint32_t h;
int8_t *names;
long originX;
long originY;
long scale;
} ScreenT;
// One stick read: its five characters and its dot's offset.
typedef struct StickT {
char state[STATE_CHARS + 1u];
char offset[OFFSET_CHARS];
} StickT;
static char axisChar(long offset);
static bool findStage(ScreenT *scr);
static JoyColourE name(const ScreenT *scr, long x, long y);
static bool patch(JoyColourE colour);
static JoyColourE stagePixel(const ScreenT *scr, long x, long y);
static void stick(const ScreenT *scr, long left, StickT *out);
static const uint16_t kColours[COLOUR_COUNT] = { 0x000, 0x444, 0x666, 0x0F0, 0x333, 0xFF0, 0xF00 };
static const long kStickLeft[STICK_COUNT] = { 24, 216 };
static const long kScales[SCALE_COUNT] = { 1, 2, 3 };
static const char *const kUsage[] = {
"joyScreen - read both joysticks' state off a screenshot of JOY.",
"",
" joyScreen PNG [--scale S] [--st] [--at X,Y]",
"",
"--st: the screen shows the plain Atari ST's 3-bit palette, which drops each",
"channel's low bit (Hatari shows 3-bit n as the STe's 4-bit 2n). --at: the",
"stage starts there on the screen (at the scale --scale gives, else 1), so",
"the patches need not be seen: where something covers one (the PSP's",
"keyboard), its stick's connected character is '?'.",
"",
"Prints \"A=<state> B=<state> scale=<S> dots=<A's x,y offset> <B's>\"; exit 1",
"when the stage is not found or a stick cannot be read."
};
static char axisChar(long offset) {
if (offset == 0) {
return '0';
}
if (offset <= -(DOT_TRAVEL / 2)) {
return '-';
}
if (offset >= DOT_TRAVEL / 2) {
return '+';
}
return UNKNOWN;
}
// The screen position of stage (0, 0) at the screen's scale, found by the
// two status patches: runs of STATUS_W stage pixels in either patch colour
// on one row, as far apart as the two frames.
static bool findStage(ScreenT *scr) {
long run;
long gap;
long x;
long y;
long i;
long start;
long other;
bool same;
run = STATUS_W * scr->scale;
gap = (kStickLeft[1] - kStickLeft[0]) * scr->scale;
for (y = 0; y < (long)scr->h - STATUS_H * scr->scale + 1; y++) {
x = 0;
while (x < (long)scr->w) {
if (!patch(name(scr, x, y))) {
x++;
continue;
}
start = x;
while (x < (long)scr->w && patch(name(scr, x, y))) {
x++;
}
if (x - start != run) {
continue;
}
other = start + gap;
same = true;
for (i = 0; i < run && same; i++) {
same = patch(name(scr, other + i, y));
}
if (same && !patch(name(scr, other + run, y))) {
scr->originX = start - kStickLeft[0] * scr->scale;
scr->originY = y - STATUS_TOP * scr->scale;
if (scr->originX >= 0 && scr->originY >= 0) {
return true;
}
}
}
}
return false;
}
// JOY's colour name at screen (x, y), or none.
static JoyColourE name(const ScreenT *scr, long x, long y) {
if (x < 0 || y < 0 || x >= (long)scr->w || y >= (long)scr->h) {
return COLOUR_NONE;
}
return (JoyColourE)scr->names[(size_t)y * scr->w + (size_t)x];
}
static bool patch(JoyColourE colour) {
return colour == COLOUR_CONNECTED || colour == COLOUR_DISCONNECTED;
}
// JOY's colour name of stage pixel (x, y): its block's middle.
static JoyColourE stagePixel(const ScreenT *scr, long x, long y) {
return name(scr, scr->originX + x * scr->scale + scr->scale / 2, scr->originY + y * scr->scale + scr->scale / 2);
}
// The five-character state of the stick whose frame starts at left, and
// its dot's offset from the middle ("x,y", "?" when there is no dot).
static void stick(const ScreenT *scr, long left, StickT *out) {
JoyColourE colour;
uint32_t dots;
uint32_t colours;
uint32_t b;
long x;
long y;
long x0;
long x1;
long y0;
long y1;
long dx;
long dy;
long centre;
long bx;
bool atRest;
colour = stagePixel(scr, left + STATUS_W / 2, STATUS_TOP + STATUS_H / 2);
out->state[STATE_CONNECTED] = (colour == COLOUR_CONNECTED) ? '1' : ((colour == COLOUR_DISCONNECTED) ? '0' : UNKNOWN);
out->state[STATE_X] = UNKNOWN;
out->state[STATE_Y] = UNKNOWN;
out->state[STATE_CHARS] = '\0';
snprintf(out->offset, sizeof(out->offset), "%c", UNKNOWN);
dots = 0u;
colours = 0u;
x0 = 0;
x1 = 0;
y0 = 0;
y1 = 0;
for (y = STICK_TOP; y < STICK_TOP + FRAME_SIZE; y++) {
for (x = left; x < left + FRAME_SIZE; x++) {
colour = stagePixel(scr, x, y);
if (colour != COLOUR_DOT_ON && colour != COLOUR_DOT_OFF) {
continue;
}
x0 = (dots == 0u || x < x0) ? x : x0;
x1 = (dots == 0u || x > x1) ? x : x1;
y0 = (dots == 0u || y < y0) ? y : y0;
y1 = (dots == 0u || y > y1) ? y : y1;
colours |= 1u << colour;
dots++;
}
}
if (dots > 0u) {
dx = x0 - (left + FRAME_SIZE / 2 - DOT_SIZE / 2);
dy = y0 - (STICK_TOP + FRAME_SIZE / 2 - DOT_SIZE / 2);
snprintf(out->offset, sizeof(out->offset), "%+ld,%+ld", dx, dy);
if (dots == DOT_SIZE * DOT_SIZE && x1 == x0 + DOT_SIZE - 1 && y1 == y0 + DOT_SIZE - 1 && (colours & (colours - 1u)) == 0u) {
out->state[STATE_X] = axisChar(dx);
out->state[STATE_Y] = axisChar(dy);
// Green away from the middle, grey in it.
atRest = out->state[STATE_X] == '0' && out->state[STATE_Y] == '0';
if (colours != (1u << (atRest ? COLOUR_DOT_OFF : COLOUR_DOT_ON))) {
out->state[STATE_X] = UNKNOWN;
out->state[STATE_Y] = UNKNOWN;
}
}
}
centre = left + FRAME_SIZE / 2;
for (b = 0; b < BUTTON_COUNT; b++) {
bx = (b == 0u) ? centre - BUTTON_W - BUTTON_GAP / 2 : centre + BUTTON_GAP / 2;
colour = stagePixel(scr, bx + BUTTON_W / 2, BUTTON_TOP + BUTTON_H / 2);
out->state[STATE_BUTTONS + b] = (colour == COLOUR_BUTTON_ON) ? '1' : ((colour == COLOUR_BUTTON_OFF) ? '0' : UNKNOWN);
}
}
int main(int argc, char **argv) {
GateImageT img;
ScreenT scr;
StickT read[STICK_COUNT];
uint8_t palette[COLOUR_COUNT][GATE_RGB_BYTES];
const long *scales;
uint32_t scaleCount;
uint32_t mask;
uint32_t distance;
uint32_t best;
uint32_t i;
size_t p;
long oneScale;
long atX;
long atY;
bool haveAt;
bool found;
int arg;
toolFileName(TOOL_NAME);
if (argc < 2) {
return gateUsage(kUsage, GATE_COUNT_OF(kUsage));
}
scales = kScales;
scaleCount = SCALE_COUNT;
mask = FULL_MASK;
oneScale = 0;
atX = 0;
atY = 0;
haveAt = false;
for (arg = 2; arg < argc; arg++) {
if (strcmp(argv[arg], "--scale") == 0 && arg + 1 < argc) {
oneScale = gateArgLong("scale", argv[++arg]);
if (oneScale < 1) {
gateArgBad("scale", argv[arg]);
}
scales = &oneScale;
scaleCount = 1u;
} else if (strcmp(argv[arg], "--st") == 0) {
mask = ST_CHANNEL_MASK;
} else if (strcmp(argv[arg], "--at") == 0 && arg + 1 < argc) {
gateArgPair("at", argv[++arg], ',', &atX, &atY);
haveAt = true;
} else {
return gateUsage(kUsage, GATE_COUNT_OF(kUsage));
}
}
gateImageLoad(argv[1], &img);
for (i = 0; i < COLOUR_COUNT; i++) {
gateRgb12(kColours[i] & mask, palette[i]);
}
scr.w = img.width;
scr.h = img.height;
scr.names = (int8_t *)malloc((size_t)img.width * img.height + 1u);
if (scr.names == NULL) {
toolFail("out of memory");
}
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, COLOUR_COUNT, &distance);
scr.names[p] = (distance <= MAX_DIST) ? (int8_t)best : (int8_t)COLOUR_NONE;
}
found = false;
for (i = 0; i < scaleCount && !found; i++) {
scr.scale = scales[i];
scr.originX = atX;
scr.originY = atY;
found = haveAt || findStage(&scr);
}
if (!found) {
printf("joyScreen: FAIL - JOY's two status patches are not on the screen\n");
return FAIL_EXIT;
}
for (i = 0; i < STICK_COUNT; i++) {
stick(&scr, kStickLeft[i], &read[i]);
}
printf("A=%s B=%s scale=%ld dots=%s %s\n", read[0].state, read[1].state, scr.scale, read[0].offset, read[1].offset);
free(scr.names);
gateImageFree(&img);
for (i = 0; i < STICK_COUNT; i++) {
if (strchr(read[i].state, UNKNOWN) != NULL) {
return FAIL_EXIT;
}
}
return 0;
}