// 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, gray 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 color 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= B= scale= dots= "; exit // 1 when the stage is not found or a stick cannot be read, 2 on a bad // command line. #include #include #include #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 colors sit much further apart. (All are exact in DVX's // 256-color 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 colors, in the order the nearest is chosen among equals. typedef enum JoyColorE { COLOR_BACKGROUND, COLOR_FRAME, COLOR_DOT_OFF, COLOR_DOT_ON, COLOR_BUTTON_OFF, COLOR_BUTTON_ON, COLOR_DISCONNECTED, COLOR_COUNT, COLOR_NONE = -1 } JoyColorE; // The patch's green is the dot's (COLOR_CONNECTED = COLOR_DOT_ON). #define COLOR_CONNECTED COLOR_DOT_ON // The screenshot as JOY's color 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 JoyColorE name(const ScreenT *scr, long x, long y); static bool patch(JoyColorE color); static JoyColorE stagePixel(const ScreenT *scr, long x, long y); static void stick(const ScreenT *scr, long left, StickT *out); static const uint16_t kColors[COLOR_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= B= scale= dots= \"; 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 color // 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 color name at screen (x, y), or none. static JoyColorE name(const ScreenT *scr, long x, long y) { if (x < 0 || y < 0 || x >= (long)scr->w || y >= (long)scr->h) { return COLOR_NONE; } return (JoyColorE)scr->names[(size_t)y * scr->w + (size_t)x]; } static bool patch(JoyColorE color) { return color == COLOR_CONNECTED || color == COLOR_DISCONNECTED; } // JOY's color name of stage pixel (x, y): its block's middle. static JoyColorE 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) { JoyColorE color; uint32_t dots; uint32_t colors; uint32_t b; long x; long y; long x0; long x1; long y0; long y1; long dx; long dy; long center; long bx; bool atRest; color = stagePixel(scr, left + STATUS_W / 2, STATUS_TOP + STATUS_H / 2); out->state[STATE_CONNECTED] = (color == COLOR_CONNECTED) ? '1' : ((color == COLOR_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; colors = 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++) { color = stagePixel(scr, x, y); if (color != COLOR_DOT_ON && color != COLOR_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; colors |= 1u << color; 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 && (colors & (colors - 1u)) == 0u) { out->state[STATE_X] = axisChar(dx); out->state[STATE_Y] = axisChar(dy); // Green away from the middle, gray in it. atRest = out->state[STATE_X] == '0' && out->state[STATE_Y] == '0'; if (colors != (1u << (atRest ? COLOR_DOT_OFF : COLOR_DOT_ON))) { out->state[STATE_X] = UNKNOWN; out->state[STATE_Y] = UNKNOWN; } } } center = left + FRAME_SIZE / 2; for (b = 0; b < BUTTON_COUNT; b++) { bx = (b == 0u) ? center - BUTTON_W - BUTTON_GAP / 2 : center + BUTTON_GAP / 2; color = stagePixel(scr, bx + BUTTON_W / 2, BUTTON_TOP + BUTTON_H / 2); out->state[STATE_BUTTONS + b] = (color == COLOR_BUTTON_ON) ? '1' : ((color == COLOR_BUTTON_OFF) ? '0' : UNKNOWN); } } int main(int argc, char **argv) { GateImageT img; ScreenT scr; StickT read[STICK_COUNT]; uint8_t palette[COLOR_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 < COLOR_COUNT; i++) { gateRgb12(kColors[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, COLOR_COUNT, &distance); scr.names[p] = (distance <= MAX_DIST) ? (int8_t)best : (int8_t)COLOR_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; }