# JoeyLib A unified C game-development library targeting five early 16-bit platforms, and one DOS desktop, from a single codebase: - Apple IIgs (reference platform) - Commodore Amiga (A500 / 68000 baseline) - Atari ST (STF / 68000 baseline) - MS-DOS (386 / VGA, DJGPP) - Sharp X68000 (68000 @ 10 MHz, Human68k, elf2x68k) - DVX (the DOS desktop GUI in `../dvxgui`: each program a windowed `.app` that can also take the whole screen, built with the DOS compiler against DVX's SDK) The Apple IIgs defines the capability ceiling. Stronger platforms coast. Hot paths are hand-written assembly per port; the public API is C. See `claudeDocs/DESIGN.md` for the full 1.0 design. ## Quick start JoeyLib builds on Linux x86-64, and on Windows inside WSL2. One script installs every cross toolchain, pinned and checksum-verified, into `toolchains/`; outside that directory it only installs ordinary packages from your distribution (apt or dnf), and only when asked. ``` git clone https://forge.duensing.digital/AI_Slop/joeylib2.git joeylib cd joeylib ./install.sh deps toolchains --install # distribution packages (sudo) ./install.sh install toolchains # every cross toolchain make ``` The first `install` takes about an hour, almost all of it compiling the IIgs and Amiga compilers. `./install.sh install toolchain:dos` (or any other port) installs just one. `./install.sh list` shows every component and whether it is current, and `./install.sh doctor` smoke-tests what is installed. The manual's Getting Started chapter (`docs/GettingStarted.adoc`) has the full installer reference. No environment setup is needed: `make` reads the tool paths the installer records in `toolchains/config.mk`. For running a cross compiler by hand, `source toolchains/env.sh` sets the same variables. `make` builds `libjoey.a` for every port whose toolchain is installed, plus the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`, `sertest`, `save`, `tilebank`, `joy`, `sprite`, `audio`, `uber`, `adventure`, `adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception: `make iigs` builds the library, and `make iigs-disk` builds its examples and packages them onto a disk image. DVX needs the `dvx` installer component, which unpacks DVX and its SDK from a DVX tree's distribution, `dist/dvx.zip` and `dist/sdk.zip` (`./install.sh --dvx-dir PATH install dvx`; the default is `../dvxgui`); its examples land in `build/dvx/bin/apps/joeylib//`, laid out as they install in DVX. Space Taxi's levels come from the original Commodore 64 game, which is not in the repository. Without `stuff/spacetaxi/raw.bin` the build compiles the STAXI program, skips only its data, and prints a note saying so. ## Running programs `joey-run` starts a built program in the right emulator; the installer sets up the emulators and their firmware: ``` ./install.sh install emulators # or emulator:dos, emulator:iigs, ... ./joey-run build/dos/bin/DRAW.EXE ./joey-run --port amiga draw ./joey-run --headless --seconds 30 --snapshot draw.png --port atarist draw ./joey-run --port dvx staxi # DVX in DOSBox, booting into the program ``` Four files are never downloaded -- the Amiga Kickstart, the Amiga `serial.device` from Workbench 3.1, the X68000 font ROM and Space Taxi's C64 memory image -- so supply your copies once with `./install.sh import PATH` (files, folders, `.zip` archives or Amiga `.adf` disk images). The manual's Getting Started chapter covers `joey-run`'s options and the private-file bundle. ## Building for a single target ``` make iigs make amiga make atarist make dos make x68000 make dvx ``` Every fragment also answers to a bare `clean`, and every fragment that builds through `make/common.mk` -- all of them except `make/iigs.mk`, which assembles its own clang flags -- honors `EXTRA_CFLAGS`: ``` make -f make/atarist.mk clean make -f make/x68000.mk EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE ``` ## Conformance gate `examples/uber` is the conformance vehicle: it exercises every public op and logs a surface hash per op. `tools/diff-uber-hashes ` compares two `joeylog.txt` captures and exits non-zero on any mismatch. The frozen references live in `tests/goldens/uber/`. All five ports currently agree on all 51 hashes; the X68000 has no golden of its own and is diffed against `tests/goldens/uber/iigs.txt`, which it matches exactly, and DVX, which shows the DOS stage, is diffed against `tests/goldens/uber/dos.txt` (`scripts/bench-dvx.sh`, then `tools/diff-uber-hashes`): ``` make -f make/x68000.mk x68000-verify-golden # ~1 min under the patched MAME make -f make/x68000.mk x68000-verify-serial # both-directions RS-232C, ~1 min make -f make/x68000.mk x68000-verify-joy # both joysticks, every line, ~10 s ``` These X68000 gates run under the patched MAME and the X68000 firmware that `./install.sh install emulator:x68000` sets up. ## Repository layout ``` docs/ design and reference documentation include/joey/ public headers src/core/ portable library code src/generic/ portable-C default for every overridable op src/codegen/ shared sprite-codegen staging / compile machinery src// per-machine HAL + per-CPU sprite emitter (iigs, amiga, atarist, dos, x68000, dvx) src/m68k/ 68k code shared across machines (planar and word- interleaved sprite emitters; surface68k.s, which is linked by Amiga and Atari ST only) src/x86/ x86 code DOS and DVX share (the sprite emitter, the whole-map tile walker, the set-1 scan code table) src/blank/ copy-to-start template for a new port tools/assetbake/ PNG -> native .tbk / .spr baker (Python) tools/joeymod/ Protracker .MOD converter (passthrough or .NTP) tools/spritebake/ offline .spr -> .spc pre-compiled sprite baker tools/songbake/ chip-tracker text notation -> JYM1 stream tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer tools/diff-uber-hashes UBER golden-hash comparator examples/ example programs joey-run run a built program in its emulator scripts/ bench / verification scripts (scripts/lib: shared helpers) tests/goldens/uber/ frozen UBER conformance hashes install.sh installs toolchains, emulators and firmware installer/ the installer's component recipes, patches, lists toolchains/ everything install.sh downloads and builds (not in git; delete it any time and re-run install.sh) make/ per-target Makefile fragments (make/support: build helpers) build// per-target build outputs ``` ## Public API Game code includes a single umbrella header: ```c #include ``` That pulls in every public surface listed below. Full documentation lives in the per-feature headers under `include/joey/`; what follows is a quick reference. Every entry point is plain C, no C++ extensions. ### Lifecycle (`joey/core.h`) ```c typedef struct { uint32_t codegenBytes; // runtime compiled-sprite cache size } jlConfigT; bool jlInit (const jlConfigT *config); void jlShutdown (void); const char *jlLastError (void); const char *jlPlatformName (void); const char *jlVersionString(void); void *jlAlloc (uint32_t bytes); // native allocator, not malloc void jlFree (void *p); void jlWaitVBL (void); // block until next VBL uint16_t jlFrameCount (void); // monotonic 16-bit frame counter uint16_t jlFrameHz (void); // 50 / 55 / 60 / 70 depending on port uint32_t jlMillisElapsed(void); // monotonic ms since jlInit uint32_t jlRandom (void); // portable, bit-identical per seed uint16_t jlRandomRange (uint16_t bound); void jlRandomSeed (uint32_t seed); ``` ### Surfaces (`joey/surface.h`) All surfaces are 320x200 16-color images with a 200-entry SCB table and 16 palettes of 16 `$0RGB` colors. In-memory storage is target-native: chunky 4bpp packed on IIgs, DOS and DVX, native planar (separate bitplanes on Amiga and Sharp X68000, word-interleaved planes on Atari ST) on the 68k ports. The public API speaks in color indices (0..15) and hides the storage format. ```c #define SURFACE_WIDTH 320 #define SURFACE_HEIGHT 200 #define SURFACE_BYTES_PER_ROW 160 #define SURFACE_PIXELS_SIZE (SURFACE_BYTES_PER_ROW * SURFACE_HEIGHT) #define SURFACE_PALETTE_COUNT 16 #define SURFACE_COLORS_PER_PALETTE 16 typedef struct jlSurfaceT jlSurfaceT; // opaque jlSurfaceT *jlSurfaceCreate (void); void jlSurfaceDestroy(jlSurfaceT *s); jlSurfaceT *jlStageGet (void); // library back-buffer void jlSurfaceCopy (jlSurfaceT *dst, const jlSurfaceT *src); bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path); bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path); uint32_t jlSurfaceHash (const jlSurfaceT *s); // FNV-1a of logical pixels ``` `jlSurfaceSaveFile` writes the surface in **target-native** form. Files are NOT cross-port portable; the asset pipeline handles conversion. ### Drawing (`joey/draw.h`) All primitives clip to the surface; off-surface coords are silent no-ops. Color 0 is plotted normally (use the masked variants if you need transparency). ```c void jlSurfaceClear (jlSurfaceT *s, uint8_t color); void jlDrawPixel (jlSurfaceT *s, int16_t x, int16_t y, uint8_t color); uint8_t jlSamplePixel (const jlSurfaceT *s, int16_t x, int16_t y); void jlDrawLine (jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color); void jlDrawRect (jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t color); void jlFillRect (jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t color); void jlDrawCircle (jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t color); void jlFillCircle (jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t color); void jlFloodFill (jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor); void jlFloodFillBounded (jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor, uint8_t boundaryColor); ``` ### Palette and SCB (`joey/palette.h`) Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to black on `jlPaletteSet`. Each scanline picks one of the 16 palettes via the SCB. Every port honours it. The X68000 presents to its text plane, which is planar like the stage (so a present is a copy) but has one 16-colour palette: a raster interrupt reloads that palette wherever the SCB changes, which for a single-palette screen is never. A build switch (`X68K_PRESENT_TEXT=0`) uses the 256-colour graphics plane the way the DOS port uses its DAC instead -- each pixel byte is `(scb[y] << 4) | nibble`, exact with no interrupt, but slower. ```c void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16); void jlPaletteGet (const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16); void jlScbSet (jlSurfaceT *s, uint16_t line, uint8_t paletteIndex); void jlScbSetRange (jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t paletteIndex); uint8_t jlScbGet (const jlSurfaceT *s, uint16_t line); ``` ### Tiles (`joey/tile.h`) A "tile" is just an 8x8-aligned region of any surface. The API moves 32-byte chunks between surfaces and provides a small `jlTileT` value type so callers can stash a copy without allocating a scratch surface. ```c #define TILE_PIXELS_PER_SIDE 8 #define TILE_BYTES_PER_ROW 4 #define TILE_BYTES (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE) #define TILE_BLOCKS_PER_ROW (SURFACE_WIDTH / TILE_PIXELS_PER_SIDE) // 40 #define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) // 25 #define TILE_NO_GLYPH ((uint16_t)0xFFFFu) typedef struct jlTileT { uint8_t pixels[TILE_BYTES]; } jlTileT; void jlTileCopy (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy); void jlTileCopyMasked (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex); void jlTileFill (jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t color); void jlTileSnap (const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out); void jlTilePaste (jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in); void jlTilePasteMono (jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor); // Load up to maxTiles tiles from a baked .tbk file (per-target planar // bytes from tools/assetbake/assetbake.py --type tile --target ...). // Refuses files baked for the wrong target. outValid (optional) marks // each loaded slot; outPalette (optional) receives the embedded // 16-entry $0RGB palette if present. uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, bool *outValid, uint16_t *outPalette); void jlDrawText (jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontSurface, const uint16_t *asciiMap, const char *str); ``` ### Sprites (`joey/sprite.h`) Rectangles of 8x8 tiles drawn at arbitrary pixel positions with color-0 transparency. Tile data is `widthTiles * heightTiles * 32` bytes, tile-major 4bpp packed. Sprites can be runtime-compiled into per-shift code variants for fast draws. ```c typedef struct jlSpriteT jlSpriteT; // opaque typedef struct { jlSpriteT *sprite; int16_t x, y; uint16_t width, height; // pixels uint8_t *bytes; // caller-owned save-under buffer uint16_t sizeBytes; } jlSpriteBackupT; jlSpriteT *jlSpriteCreate (const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles); jlSpriteT *jlSpriteCreateFromSurface (const jlSurfaceT *src, int16_t x, int16_t y, uint8_t widthTiles, uint8_t heightTiles); void jlSpriteDestroy (jlSpriteT *sp); // Load up to maxCels cels from a baked .spr file (cross-target chunky // 4bpp blob from tools/assetbake/assetbake.py --type sprite). Each // cel becomes a freshly-allocated jlSpriteT (release with // jlSpriteDestroy). outPalette (optional) receives the embedded // 16-entry $0RGB palette if present. uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette); // Same contract, but reading a `.spc` bank pre-compiled for THIS target by // the offline baker `tools/spritebake`: the native blit routines are copied // straight into the codegen arena, so there is no startup JIT. The runtime // twin writes one back out after a jlSpriteCompile pass. uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette); bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t count, const uint16_t *palette); bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already uint32_t jlSpriteCompiledSize (const jlSpriteT *sp); void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); void jlSpriteSaveUnder (const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); void jlSpriteRestoreUnder (jlSurfaceT *s, const jlSpriteBackupT *backup); void jlSpriteSaveAndDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); // TRUSTED variants: skip the per-call geometry validation for a caller that // pre-compiles its sprites and guarantees the sprite is FULLY on-surface. // UB if that is violated -- no clipping happens. They fall back to the // validated entry for uncompiled sprites and on ports with no compiled path. void jlSpriteDrawTrusted (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); void jlSpriteSaveUnderTrusted (const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup); void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); void jlSpriteCompact (void); // defrag the codegen arena uint32_t jlSpriteCodegenBytesUsed (void); uint32_t jlSpriteCodegenBytesTotal (void); ``` ### Assets (baked at build time) Asset PNGs live in each example's `assets/` directory and are baked to native binary blobs at build time by `tools/assetbake/assetbake.py` (Python+PIL). The runtime loads those blobs directly with no conversion -- there is no in-memory chunky-to-planar or palette re-indexing on the device. Two blob formats: * **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target planar layout (Amiga and X68000 plane-major; Atari ST row-major-with- planes-per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad` rejects files baked for the wrong target. * **`.spr` (sprite cel set)** -- one or more uniform-sized sprite cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky and converts to planar at draw time, so the same blob serves every platform. Both formats embed an optional 16-entry `$0RGB` palette. ``` tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs|x68000} in.png out.tbk tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr ``` The Makefiles (`make/{amiga,atarist,dos}.mk`) wire bake rules per target so `make ` produces baked blobs under `examples//generated//` and stages them into the runtime tree at `build//.../DATA/`. `make/iigs.mk` runs no `assetbake.py`: its `generated/iigs` tree is produced out-of-band and the fragment only bakes `.spc` from the committed `.spr`. `make/x68000.mk` builds only what Space Taxi loads there: its levels and pre-compiled cel banks. For runtime-extracted content (sprites peeled out of a procedural or captured surface), use `jlSpriteCreateFromSurface` -- the on-the-fly path stays alongside the baked load API. ### Present (`joey/present.h`) ```c void jlStagePresent(void); ``` Flips the dirty rows of the stage to the display, then clears dirty state. Drawing primitives mark dirty as a side effect, so calling `jlStagePresent` once at end-of-frame is enough. ### Input (`joey/input.h`) Call `jlInputPoll` once per frame, then query the state predicates. Edge predicates (`*Pressed`, `*Released`) fire only in the frame the transition happened. Text entry uses the separate typed-character queue: `jlInputGetChar` pops the next typed character (printable ASCII 0x20..0x7E plus `JL_CHAR_BACKSPACE`/`TAB`/`RETURN`/`ESCAPE`) or -1 when empty, with shift, caps lock, and the machine's keyboard layout already applied by the backend -- so punctuation like `.` and `:` arrives correctly on every port. Refilled by the same `jlInputPoll`; see `claudeDocs/input.md` for per-port sources and limitations. ```c typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE, KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT, KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE; typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE; typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE; typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE; #define JOYSTICK_AXIS_MAX 127 #define JOYSTICK_AXIS_MIN (-127) #define JL_CHAR_BACKSPACE 0x08 #define JL_CHAR_TAB 0x09 #define JL_CHAR_RETURN 0x0D #define JL_CHAR_ESCAPE 0x1B void jlInputPoll (void); void jlWaitForAnyKey (void); int jlInputGetChar (void); bool jlKeyDown (jlKeyE key); bool jlKeyPressed (jlKeyE key); bool jlKeyReleased (jlKeyE key); int16_t jlMouseX (void); int16_t jlMouseY (void); bool jlMouseDown (jlMouseButtonE b); bool jlMousePressed (jlMouseButtonE b); bool jlMouseReleased (jlMouseButtonE b); bool jlJoystickConnected(jlJoystickE js); int8_t jlJoystickX (jlJoystickE js); int8_t jlJoystickY (jlJoystickE js); bool jlJoyDown (jlJoystickE js, jlJoyButtonE b); bool jlJoyPressed (jlJoystickE js, jlJoyButtonE b); bool jlJoyReleased (jlJoystickE js, jlJoyButtonE b); void jlJoystickReset (jlJoystickE js, uint8_t deadZone); ``` ### Audio (`joey/audio.h`) 4-channel Protracker-style music plus five one-shot SFX slots, three PSG-style tone voices and a noise channel. Module data must be the platform-native form produced by `tools/joeymod` (`.mod` for Amiga/DOS/ST/X68000; `.ntp` for IIgs; `.amod` if you want loop=false on Amiga). Amiga plays `.mod` through PTPlayer and the IIgs through the NinjaTrackerPlus replayer; DOS, Atari ST and X68000 decode with libxmp-lite. A failed `jlAudioInit` is non-fatal; the rest of the API stays callable as no-ops. ```c #define JOEY_AUDIO_SFX_SLOTS 5 #define JOEY_AUDIO_VOICES 3 bool jlAudioInit (void); void jlAudioShutdown (void); void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop); void jlAudioStopMod (void); bool jlAudioIsPlayingMod (void); void jlAudioPlaySfx (uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz); void jlAudioPlaySfxStream (uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz); void jlAudioStopSfx (uint8_t slot); // Portable chip-tracker music: a JYM1 event stream baked by tools/songbake, // played through the tone/noise layer at register-write cost on every port. // No CPU mixing, so it is viable where jlAudioPlayMod is not. bool jlMusicPlay (const uint8_t *data, uint32_t length, bool loop); void jlMusicStop (void); bool jlMusicIsPlaying (void); void jlMusicPause (void); void jlMusicResume (void); void jlMusicSetAtten (uint8_t atten); void jlAudioTone (uint16_t freqHz); void jlAudioVoice (uint8_t voice, uint16_t freqHz, uint8_t atten); void jlAudioNoise (uint8_t pitch, uint8_t atten); void jlAudioFrameTick (void); ``` ### Serial (`joey/serial.h`) -- opt-in add-on RS-232 serial communications. This is an **opt-in add-on**: it is the one public header the umbrella `` does **not** include, so pull in `` yourself. Ports that have no serial hardware, and the BLANK template, link a no-op stub, so the API stays callable everywhere -- a failed `jlSerialOpen` leaves every other call an inert no-op. The API is non-blocking and buffered: received bytes accumulate in a background ring that you drain once per frame with `jlSerialPoll`, and writes never stall the frame (they report a short count under flow control -- resend the remainder next frame). One port is open at a time. ```c typedef enum jlSerialDeviceE { JL_SERIAL_DEFAULT = 0, // the platform's natural port JL_SERIAL_MODEM, // IIgs modem port (SCC channel A) JL_SERIAL_PRINTER, // IIgs printer port (SCC channel B) JL_SERIAL_SLOT // a card in slot/unit `config.unit` } jlSerialDeviceE; typedef struct jlSerialConfigT { uint32_t baud; // 0 -> 9600 uint8_t dataBits; // 5..8, 0 -> 8 uint8_t stopBits; // 1..2, 0 -> 1 jlSerialParityE parity; // NONE / ODD / EVEN jlSerialFlowE flow; // NONE / RTSCTS / XONXOFF uint8_t unit; // slot (IIgs SSC) or COM/device index } jlSerialConfigT; bool jlSerialOpen (jlSerialDeviceE device, const jlSerialConfigT *config); void jlSerialClose (void); void jlSerialPoll (void); // call once per frame to drain RX uint16_t jlSerialAvailable(void); uint16_t jlSerialRead (uint8_t *buf, uint16_t max); int16_t jlSerialReadByte (void); // -1 if none uint16_t jlSerialWrite (const uint8_t *buf, uint16_t len); bool jlSerialWriteByte(uint8_t b); void jlSerialFlush (void); // discard buffered input ``` Per-port device mapping: | Device | IIgs | DOS | Amiga | Atari ST | X68000 | |---------------------|-------------------------|-----------|-------------------|--------------|-----------------| | `JL_SERIAL_DEFAULT` | modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) | | `JL_SERIAL_MODEM` | modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) | | `JL_SERIAL_PRINTER` | printer port (SCC B) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) | | `JL_SERIAL_SLOT` | 6551 SSC in slot `unit` | COM`unit` | serial.device `unit` | Modem/RS-232 | RS-232C (SCC A) | Only the IIgs has multiple built-in ports; elsewhere `MODEM`/`PRINTER` fall back to the natural port. The `serial` example is an echo terminal and loopback self-test. `scripts/check-serial.sh` unit-tests the portable gate layer against a loopback mock with no hardware. Full reference, per-port behavior, and porting notes: [`claudeDocs/serial.md`](claudeDocs/serial.md). ### Files, saves & disk space (`joey/file.h`) -- opt-in File access, not pulled in by `` -- include ``. Two directories sit beside the executable: read-only `DATA/` (game content) and writable `SAVES/` (save / preference files, created on demand). Names are relative to their directory (`"hero.sav"`, not `"SAVES/hero.sav"`). Keep save names to 8 characters plus a short extension, letters/digits only, so they fit ProDOS and 8.3. Every call is non-fatal: a failed save reports `false`/`0`. ```c FILE * jlDataOpen (const char *name, const char *mode); // DATA/ (read-only) FILE * jlSaveOpen (const char *name, const char *mode); // SAVES/ stream bool jlSaveWrite (const char *name, const void *buf, uint32_t len); uint32_t jlSaveRead (const char *name, void *buf, uint32_t max); bool jlSaveExists(const char *name); bool jlSaveDelete(const char *name); uint32_t jlDiskFree (void); // free bytes on the save volume (0 if unknown) ``` Directory create / delete / free-space use a per-port HAL (POSIX `mkdir` + `remove` + INT 21h on DOS; `mkdir` + GEMDOS `Dfree` on ST; dos.library `CreateDir`/`DeleteFile`/`Info` on Amiga; GS/OS `Create` + a `GetDevNumber`/`DInfo`/`Volume` chain on the IIgs; Human68k `_dos_mkdir` + `remove` + `_dos_dskfre` on the X68000). `scripts/check-save.sh` round-trips the core against real files; `make iigs-verify-save` boots the IIgs save path under MAME. Full reference, per-port behavior, and porting notes: [`claudeDocs/save.md`](claudeDocs/save.md). ### Debug logging (`joey/debug.h`) Crash-tracing logger. Writes are buffered and durable across normal exit; call `jlLogFlush` ahead of suspected hang points if you want a guaranteed last-line-on-disk. ```c void jlLog (const char *msg); void jlLogF (const char *fmt, ...); void jlLogFlush(void); void jlLogReset(void); ``` Output goes to `joeylog.txt` in the program's working directory. Building with `EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE` additionally mirrors every `jlLog` / `jlLogF` line out the port's serial line as it is written -- unbounded, needs no disk, and arrives in real time, which is what makes a wedged run diagnosable. It is OFF by default and is currently implemented only for the X68000 (`jlpLogTee` in `src/x68000/hal.c`); the host end is `scripts/x68kSerialPeer.py`, which must be run under `python3 -u` -- it never passes `flush=True`, so a killed peer loses its whole capture to stdio buffering. ### Platform macros (`joey/platform.h`) The build system normally sets the platform via `-D`; auto-detection from compiler-predefined macros is a fallback. Game code can conditionally compile on these: ``` JOEYLIB_PLATFORM_IIGS / _AMIGA / _ATARIST / _DOS / _X68000 / _DVX // exactly one defined JOEYLIB_CPU_65816 / _68000 / _X86 JOEYLIB_ENDIAN_LITTLE / _BIG JOEYLIB_NATIVE_CHUNKY / _NATIVE_PLANAR JOEYLIB_HAS_BLITTER / _HAS_COPPER // Amiga only JOEYLIB_PLATFORM_NAME // human-readable string JOEYLIB_VERSION_MAJOR / _MINOR / _PATCH / _STRING ``` ## License TBD.