374 lines
13 KiB
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;
|
|
}
|