706 lines
23 KiB
C
706 lines
23 KiB
C
// joeysprite: host-side compiler that turns sprite art into a `.spr`
|
|
// file ready to be loaded at runtime by spriteLoadFile.
|
|
//
|
|
// Usage:
|
|
// joeysprite --target {iigs,amiga,atarist,dos}
|
|
// [--width-tiles N --height-tiles M]
|
|
// INPUT OUTPUT.spr
|
|
//
|
|
// Two input formats are accepted; the first 2 bytes select the path:
|
|
//
|
|
// PPM (P6) -- 8-bit-per-channel raster from any pixel-art tool that
|
|
// exports PPM (GIMP, ImageMagick `convert`, paint.net, etc.). Image
|
|
// dimensions must be multiples of 8 in both axes; widthTiles /
|
|
// heightTiles are auto-derived as W/8 and H/8 (CLI overrides are
|
|
// optional and must match). Each input RGB is reduced to a 12-bit
|
|
// $0RGB color (high nibble of each channel); the input must use
|
|
// no more than 16 distinct $0RGB colors after that reduction. The
|
|
// FIRST color encountered (typically the top-left pixel) is bound
|
|
// to palette index 0, which the runtime treats as transparent --
|
|
// so paint your sprite background with that pixel's color.
|
|
//
|
|
// Raw `.tiles` -- widthTiles * heightTiles * 32 bytes, laid out
|
|
// tile-major as the runtime SpriteT.tileData expects: tile (0,0)
|
|
// first 32 bytes, tile (1,0) next 32, ... tile (widthTiles-1, 0),
|
|
// then tile (0,1), and so on. Inside each tile, rows are stored
|
|
// top-to-bottom and each row is 4 bytes (8 pixels at 4bpp packed,
|
|
// high nibble = left pixel). --width-tiles / --height-tiles are
|
|
// required for this path since the file carries no header.
|
|
//
|
|
// The .spr output carries indices only -- the palette mapping is the
|
|
// application's responsibility (typical pattern: ship a separate
|
|
// .jas built from the same PPM via joeyasset, or hand-author the
|
|
// palette in code).
|
|
//
|
|
// Output `.spr` format (target-native byte order for code; see
|
|
// DESIGN.md §12). Mirrors src/core/sprite.c's reader:
|
|
// byte 0 widthTiles
|
|
// byte 1 heightTiles
|
|
// bytes 2-3 codeSize (LE16)
|
|
// bytes 4-5 tileBytes (LE16) = widthTiles*heightTiles*32
|
|
// ... offsets (JOEY_SPRITE_SHIFT_COUNT * SPRITE_OP_COUNT *
|
|
// uint16_t LE): [draw_s0, save_s0, restore_s0,
|
|
// draw_s1, save_s1, restore_s1]. Each entry is the
|
|
// byte offset of that routine within the compiled-code
|
|
// region, or 0xFFFF (SPRITE_NOT_COMPILED) if the per-CPU
|
|
// emitter returned 0 bytes for that op -- the runtime
|
|
// then falls back to the interpreted memcpy/RMW path.
|
|
// ... compiled code (codeSize bytes)
|
|
// ... raw tile data (tileBytes bytes; same layout as the
|
|
// input file, lets the runtime interpreter handle
|
|
// clipped draws without decoding the compiled bytes).
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "joey/sprite.h"
|
|
#include "spriteEmitter.h"
|
|
#include "spriteInternal.h"
|
|
|
|
|
|
typedef enum {
|
|
TARGET_IIGS,
|
|
TARGET_AMIGA,
|
|
TARGET_ATARIST,
|
|
TARGET_DOS,
|
|
TARGET_INVALID
|
|
} TargetE;
|
|
|
|
|
|
// ----- Constants -----
|
|
|
|
#define MAX_SCRATCH_BYTES (16u * 1024u)
|
|
// Pixel art conventions for sprite work.
|
|
#define TILE_PIXELS 8
|
|
#define TILE_BYTES 32
|
|
#define TILE_BYTES_PER_ROW 4
|
|
#define MAX_PALETTE_ENTRIES 16
|
|
|
|
#define PPM_TOKEN_MAX 64
|
|
|
|
|
|
// ----- Prototypes -----
|
|
|
|
static int buildPalette(const uint8_t *rgb, int width, int height, uint8_t *outIndices);
|
|
static int compileToSpr(const SpriteT *sp, TargetE target, const char *outPath);
|
|
static uint16_t emitForTarget(uint8_t *out, const SpriteT *sp, uint8_t shift, uint8_t op, TargetE target);
|
|
static bool fileIsPpm(const char *path);
|
|
static int loadPpm(const char *path, int *outWidth, int *outHeight, uint8_t **outPixels);
|
|
static int loadPpmAsTiles(const char *path, long *widthTiles, long *heightTiles, uint8_t **outTiles, uint32_t *outSize);
|
|
static int loadTileData(const char *path, uint8_t **outBytes, uint32_t *outSize);
|
|
static void packIndicesToTiles(const uint8_t *indices, int width, int height, uint8_t *outTiles);
|
|
static int parsePpmToken(FILE *fp, char *out, int outLen);
|
|
static TargetE parseTarget(const char *name);
|
|
static int usage(const char *prog);
|
|
static int writeLE16(FILE *fp, uint16_t v);
|
|
|
|
|
|
// ----- Internal helpers (alphabetical) -----
|
|
|
|
// Reduce every input RGB triple to a 12-bit $0RGB color and assign
|
|
// palette indices in encounter order: top-left pixel = index 0,
|
|
// next-encountered = index 1, etc. The runtime treats index 0 as
|
|
// transparent, so the top-left pixel must be the sprite's background
|
|
// color. Returns the number of distinct colors found, or -1 if the
|
|
// image needs more than 16 entries after $0RGB quantization.
|
|
//
|
|
// Mirrors joeyasset's buildPalette but only emits the index array;
|
|
// joeysprite drops the $0RGB palette since the .spr format carries
|
|
// indices alone.
|
|
static int buildPalette(const uint8_t *rgb, int width, int height, uint8_t *outIndices) {
|
|
uint16_t palette[MAX_PALETTE_ENTRIES];
|
|
int paletteCount;
|
|
int total;
|
|
int i;
|
|
int j;
|
|
uint8_t r;
|
|
uint8_t g;
|
|
uint8_t b;
|
|
uint16_t color;
|
|
|
|
total = width * height;
|
|
paletteCount = 0;
|
|
for (i = 0; i < total; i++) {
|
|
r = (uint8_t)(rgb[i * 3 + 0] >> 4);
|
|
g = (uint8_t)(rgb[i * 3 + 1] >> 4);
|
|
b = (uint8_t)(rgb[i * 3 + 2] >> 4);
|
|
color = (uint16_t)((r << 8) | (g << 4) | b);
|
|
for (j = 0; j < paletteCount; j++) {
|
|
if (palette[j] == color) {
|
|
break;
|
|
}
|
|
}
|
|
if (j == paletteCount) {
|
|
if (paletteCount >= MAX_PALETTE_ENTRIES) {
|
|
return -1;
|
|
}
|
|
palette[paletteCount] = color;
|
|
paletteCount++;
|
|
}
|
|
outIndices[i] = (uint8_t)j;
|
|
}
|
|
return paletteCount;
|
|
}
|
|
|
|
|
|
// Two-pass: pass 1 sizes every (shift, op) routine into shiftOpSizes;
|
|
// pass 2 stamps them into the code buffer at their cumulative offsets.
|
|
// Routines that return 0 bytes (the per-CPU emitter doesn't implement
|
|
// that op) get SPRITE_NOT_COMPILED in their offset slot so the runtime
|
|
// dispatch falls back to the interpreted path.
|
|
static int compileToSpr(const SpriteT *sp, TargetE target, const char *outPath) {
|
|
uint8_t *scratch;
|
|
uint8_t *codeBuf;
|
|
uint16_t routineSizes[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
|
|
uint16_t routineOffsets[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
|
|
uint32_t totalCodeSize;
|
|
uint8_t shift;
|
|
uint8_t op;
|
|
uint16_t written;
|
|
uint16_t cursor;
|
|
uint16_t value;
|
|
FILE *fp;
|
|
int rc;
|
|
|
|
scratch = (uint8_t *)malloc(MAX_SCRATCH_BYTES);
|
|
if (scratch == NULL) {
|
|
fprintf(stderr, "joeysprite: out of memory\n");
|
|
return 2;
|
|
}
|
|
|
|
totalCodeSize = 0;
|
|
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
|
|
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
|
written = emitForTarget(scratch, sp, shift, op, target);
|
|
routineSizes[shift][op] = written;
|
|
if (written == 0) {
|
|
routineOffsets[shift][op] = SPRITE_NOT_COMPILED;
|
|
} else {
|
|
routineOffsets[shift][op] = (uint16_t)totalCodeSize;
|
|
totalCodeSize += written;
|
|
}
|
|
}
|
|
}
|
|
if (totalCodeSize > 0xFFFFu) {
|
|
fprintf(stderr, "joeysprite: emitted %u code bytes; max is 65535\n",
|
|
(unsigned)totalCodeSize);
|
|
free(scratch);
|
|
return 2;
|
|
}
|
|
|
|
codeBuf = (uint8_t *)malloc(totalCodeSize > 0 ? totalCodeSize : 1);
|
|
if (codeBuf == NULL) {
|
|
fprintf(stderr, "joeysprite: out of memory for code buffer\n");
|
|
free(scratch);
|
|
return 2;
|
|
}
|
|
|
|
cursor = 0;
|
|
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
|
|
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
|
if (routineSizes[shift][op] == 0) {
|
|
continue;
|
|
}
|
|
written = emitForTarget(codeBuf + cursor, sp, shift, op, target);
|
|
cursor = (uint16_t)(cursor + written);
|
|
}
|
|
}
|
|
|
|
fp = fopen(outPath, "wb");
|
|
if (fp == NULL) {
|
|
fprintf(stderr, "joeysprite: cannot open %s for writing\n", outPath);
|
|
free(codeBuf);
|
|
free(scratch);
|
|
return 2;
|
|
}
|
|
|
|
rc = 0;
|
|
if (fputc(sp->widthTiles, fp) == EOF) rc = 2;
|
|
if (rc == 0 && fputc(sp->heightTiles, fp) == EOF) rc = 2;
|
|
if (rc == 0 && writeLE16(fp, (uint16_t)totalCodeSize) != 0) rc = 2;
|
|
if (rc == 0 && writeLE16(fp, (uint16_t)(sp->widthTiles * sp->heightTiles * 32u)) != 0) rc = 2;
|
|
|
|
for (shift = 0; rc == 0 && shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
|
|
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
|
value = routineOffsets[shift][op];
|
|
if (writeLE16(fp, value) != 0) {
|
|
rc = 2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rc == 0 && totalCodeSize > 0) {
|
|
if (fwrite(codeBuf, 1, totalCodeSize, fp) != totalCodeSize) {
|
|
rc = 2;
|
|
}
|
|
}
|
|
if (rc == 0) {
|
|
// Append the raw tile data so the runtime interpreter has it
|
|
// available for clipped draws.
|
|
uint32_t tileBytes = (uint32_t)sp->widthTiles * (uint32_t)sp->heightTiles * 32u;
|
|
if (sp->tileData == NULL) {
|
|
fprintf(stderr, "joeysprite: sprite missing tile data, cannot save\n");
|
|
rc = 2;
|
|
} else if (fwrite(sp->tileData, 1, tileBytes, fp) != tileBytes) {
|
|
rc = 2;
|
|
}
|
|
}
|
|
fclose(fp);
|
|
free(codeBuf);
|
|
free(scratch);
|
|
|
|
if (rc == 0) {
|
|
printf("joeysprite: %u code bytes -> %s (target=%s, %ux%u tiles)\n",
|
|
(unsigned)totalCodeSize, outPath,
|
|
target == TARGET_IIGS ? "iigs" :
|
|
target == TARGET_AMIGA ? "amiga" :
|
|
target == TARGET_ATARIST ? "atarist" : "dos",
|
|
sp->widthTiles, sp->heightTiles);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
|
|
static uint16_t emitForTarget(uint8_t *out, const SpriteT *sp, uint8_t shift, uint8_t op, TargetE target) {
|
|
switch (target) {
|
|
case TARGET_DOS:
|
|
switch (op) {
|
|
case SPRITE_OP_DRAW: return spriteEmitDrawX86 (out, sp, shift);
|
|
case SPRITE_OP_SAVE: return spriteEmitSaveX86 (out, sp, shift);
|
|
case SPRITE_OP_RESTORE: return spriteEmitRestoreX86(out, sp, shift);
|
|
default: return 0;
|
|
}
|
|
case TARGET_AMIGA:
|
|
case TARGET_ATARIST:
|
|
switch (op) {
|
|
case SPRITE_OP_DRAW: return spriteEmitDraw68k (out, sp, shift);
|
|
case SPRITE_OP_SAVE: return spriteEmitSave68k (out, sp, shift);
|
|
case SPRITE_OP_RESTORE: return spriteEmitRestore68k(out, sp, shift);
|
|
default: return 0;
|
|
}
|
|
case TARGET_IIGS:
|
|
switch (op) {
|
|
case SPRITE_OP_DRAW: return spriteEmitDrawIigs (out, sp, shift);
|
|
case SPRITE_OP_SAVE: return spriteEmitSaveIigs (out, sp, shift);
|
|
case SPRITE_OP_RESTORE: return spriteEmitRestoreIigs(out, sp, shift);
|
|
default: return 0;
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
// Sniff the first 2 bytes for the PPM magic. Errors return false (the
|
|
// caller will fall through to the .tiles loader, which surfaces a
|
|
// clear error if the bytes aren't valid tile data either).
|
|
static bool fileIsPpm(const char *path) {
|
|
FILE *fp;
|
|
int c0;
|
|
int c1;
|
|
|
|
fp = fopen(path, "rb");
|
|
if (fp == NULL) {
|
|
return false;
|
|
}
|
|
c0 = fgetc(fp);
|
|
c1 = fgetc(fp);
|
|
fclose(fp);
|
|
return (c0 == 'P' && c1 == '6');
|
|
}
|
|
|
|
|
|
// Read a PPM (P6) raster into a freshly allocated 8-bit RGB buffer.
|
|
// Mirrors joeyasset's loadPpm. Caller frees *outPixels.
|
|
static int loadPpm(const char *path, int *outWidth, int *outHeight, uint8_t **outPixels) {
|
|
FILE *fp;
|
|
char tok[PPM_TOKEN_MAX];
|
|
int width;
|
|
int height;
|
|
int maxval;
|
|
size_t pixelBytes;
|
|
uint8_t *buf;
|
|
size_t read;
|
|
|
|
fp = fopen(path, "rb");
|
|
if (fp == NULL) {
|
|
fprintf(stderr, "joeysprite: cannot open %s: %s\n", path, strerror(errno));
|
|
return 2;
|
|
}
|
|
if (parsePpmToken(fp, tok, sizeof(tok)) != 0 || strcmp(tok, "P6") != 0) {
|
|
fprintf(stderr, "joeysprite: %s is not a PPM (P6) file\n", path);
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
if (parsePpmToken(fp, tok, sizeof(tok)) != 0) {
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
width = atoi(tok);
|
|
if (parsePpmToken(fp, tok, sizeof(tok)) != 0) {
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
height = atoi(tok);
|
|
if (parsePpmToken(fp, tok, sizeof(tok)) != 0) {
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
maxval = atoi(tok);
|
|
if (width <= 0 || height <= 0) {
|
|
fprintf(stderr, "joeysprite: %s has non-positive dimensions\n", path);
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
if (maxval != 255) {
|
|
fprintf(stderr, "joeysprite: %s maxval %d unsupported (must be 255)\n", path, maxval);
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
pixelBytes = (size_t)width * (size_t)height * 3u;
|
|
buf = (uint8_t *)malloc(pixelBytes);
|
|
if (buf == NULL) {
|
|
fprintf(stderr, "joeysprite: out of memory (%zu bytes)\n", pixelBytes);
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
read = fread(buf, 1, pixelBytes, fp);
|
|
fclose(fp);
|
|
if (read != pixelBytes) {
|
|
fprintf(stderr, "joeysprite: short raster in %s (got %zu, need %zu)\n",
|
|
path, read, pixelBytes);
|
|
free(buf);
|
|
return 2;
|
|
}
|
|
*outWidth = width;
|
|
*outHeight = height;
|
|
*outPixels = buf;
|
|
return 0;
|
|
}
|
|
|
|
|
|
// End-to-end PPM -> tile-major 4bpp packed. On entry, *widthTiles /
|
|
// *heightTiles are 0 if the user didn't pass --width-tiles /
|
|
// --height-tiles, or the user-provided values otherwise; we fill in
|
|
// the auto-derived values when the user left them at 0, and validate
|
|
// against the image when they didn't.
|
|
static int loadPpmAsTiles(const char *path, long *widthTiles, long *heightTiles, uint8_t **outTiles, uint32_t *outSize) {
|
|
uint8_t *rgb;
|
|
uint8_t *indices;
|
|
uint8_t *tiles;
|
|
int width;
|
|
int height;
|
|
long wTiles;
|
|
long hTiles;
|
|
uint32_t tileBytes;
|
|
int paletteCount;
|
|
int rc;
|
|
|
|
rc = loadPpm(path, &width, &height, &rgb);
|
|
if (rc != 0) {
|
|
return rc;
|
|
}
|
|
if ((width % TILE_PIXELS) != 0 || (height % TILE_PIXELS) != 0) {
|
|
fprintf(stderr,
|
|
"joeysprite: %s is %dx%d -- both dimensions must be multiples of %d\n",
|
|
path, width, height, TILE_PIXELS);
|
|
free(rgb);
|
|
return 2;
|
|
}
|
|
wTiles = width / TILE_PIXELS;
|
|
hTiles = height / TILE_PIXELS;
|
|
if (*widthTiles == 0) {
|
|
*widthTiles = wTiles;
|
|
} else if (*widthTiles != wTiles) {
|
|
fprintf(stderr,
|
|
"joeysprite: --width-tiles %ld disagrees with image width %d (%ld tiles)\n",
|
|
*widthTiles, width, wTiles);
|
|
free(rgb);
|
|
return 2;
|
|
}
|
|
if (*heightTiles == 0) {
|
|
*heightTiles = hTiles;
|
|
} else if (*heightTiles != hTiles) {
|
|
fprintf(stderr,
|
|
"joeysprite: --height-tiles %ld disagrees with image height %d (%ld tiles)\n",
|
|
*heightTiles, height, hTiles);
|
|
free(rgb);
|
|
return 2;
|
|
}
|
|
|
|
indices = (uint8_t *)malloc((size_t)width * (size_t)height);
|
|
if (indices == NULL) {
|
|
fprintf(stderr, "joeysprite: out of memory for index buffer\n");
|
|
free(rgb);
|
|
return 2;
|
|
}
|
|
paletteCount = buildPalette(rgb, width, height, indices);
|
|
free(rgb);
|
|
if (paletteCount < 0) {
|
|
fprintf(stderr,
|
|
"joeysprite: %s has more than 16 distinct $0RGB colors after\n"
|
|
" 4-bit-per-channel quantization. Reduce the input palette and\n"
|
|
" retry (e.g. pngquant --nofs 16, or GIMP -> Image -> Mode ->\n"
|
|
" Indexed... with 16 colors and no dithering).\n", path);
|
|
free(indices);
|
|
return 2;
|
|
}
|
|
|
|
tileBytes = (uint32_t)wTiles * (uint32_t)hTiles * TILE_BYTES;
|
|
tiles = (uint8_t *)malloc(tileBytes);
|
|
if (tiles == NULL) {
|
|
fprintf(stderr, "joeysprite: out of memory for tile buffer\n");
|
|
free(indices);
|
|
return 2;
|
|
}
|
|
packIndicesToTiles(indices, width, height, tiles);
|
|
free(indices);
|
|
|
|
*outTiles = tiles;
|
|
*outSize = tileBytes;
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int loadTileData(const char *path, uint8_t **outBytes, uint32_t *outSize) {
|
|
FILE *fp;
|
|
long fileSize;
|
|
uint8_t *buf;
|
|
size_t read;
|
|
|
|
fp = fopen(path, "rb");
|
|
if (fp == NULL) {
|
|
fprintf(stderr, "joeysprite: cannot open %s\n", path);
|
|
return 2;
|
|
}
|
|
if (fseek(fp, 0L, SEEK_END) != 0) {
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
fileSize = ftell(fp);
|
|
if (fileSize <= 0) {
|
|
fprintf(stderr, "joeysprite: %s is empty\n", path);
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
if (fseek(fp, 0L, SEEK_SET) != 0) {
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
buf = (uint8_t *)malloc((size_t)fileSize);
|
|
if (buf == NULL) {
|
|
fclose(fp);
|
|
return 2;
|
|
}
|
|
read = fread(buf, 1, (size_t)fileSize, fp);
|
|
fclose(fp);
|
|
if (read != (size_t)fileSize) {
|
|
free(buf);
|
|
return 2;
|
|
}
|
|
*outBytes = buf;
|
|
*outSize = (uint32_t)fileSize;
|
|
return 0;
|
|
}
|
|
|
|
|
|
// Reshuffle row-major palette indices into the tile-major 4bpp packed
|
|
// layout the runtime SpriteT.tileData expects: tile (tx,ty)'s 32 bytes
|
|
// land contiguously at outTiles[(ty*widthTiles + tx) * 32], with each
|
|
// row inside the tile as 4 packed bytes (high nibble = left pixel).
|
|
static void packIndicesToTiles(const uint8_t *indices, int width, int height, uint8_t *outTiles) {
|
|
int widthTiles;
|
|
int heightTiles;
|
|
int tx;
|
|
int ty;
|
|
int row;
|
|
int col;
|
|
int pxX;
|
|
int pxY;
|
|
uint8_t hi;
|
|
uint8_t lo;
|
|
uint8_t *tile;
|
|
|
|
widthTiles = width / TILE_PIXELS;
|
|
heightTiles = height / TILE_PIXELS;
|
|
for (ty = 0; ty < heightTiles; ty++) {
|
|
for (tx = 0; tx < widthTiles; tx++) {
|
|
tile = &outTiles[(ty * widthTiles + tx) * TILE_BYTES];
|
|
for (row = 0; row < TILE_PIXELS; row++) {
|
|
pxY = ty * TILE_PIXELS + row;
|
|
for (col = 0; col < TILE_BYTES_PER_ROW; col++) {
|
|
pxX = tx * TILE_PIXELS + col * 2;
|
|
hi = (uint8_t)(indices[pxY * width + pxX] & 0x0Fu);
|
|
lo = (uint8_t)(indices[pxY * width + pxX + 1] & 0x0Fu);
|
|
tile[row * TILE_BYTES_PER_ROW + col] = (uint8_t)((hi << 4) | lo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Reads a single whitespace-separated token from a PPM header,
|
|
// skipping `#` comments to end-of-line. Mirrors joeyasset.
|
|
static int parsePpmToken(FILE *fp, char *out, int outLen) {
|
|
int c;
|
|
int pos;
|
|
|
|
pos = 0;
|
|
for (;;) {
|
|
c = fgetc(fp);
|
|
if (c == EOF) {
|
|
return -1;
|
|
}
|
|
if (isspace(c)) {
|
|
continue;
|
|
}
|
|
if (c == '#') {
|
|
while ((c = fgetc(fp)) != EOF && c != '\n') {
|
|
/* skip */;
|
|
}
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
while (c != EOF && !isspace(c) && c != '#') {
|
|
if (pos < outLen - 1) {
|
|
out[pos++] = (char)c;
|
|
}
|
|
c = fgetc(fp);
|
|
}
|
|
out[pos] = 0;
|
|
return 0;
|
|
}
|
|
|
|
|
|
static TargetE parseTarget(const char *name) {
|
|
if (strcmp(name, "iigs") == 0) return TARGET_IIGS;
|
|
if (strcmp(name, "amiga") == 0) return TARGET_AMIGA;
|
|
if (strcmp(name, "atarist") == 0) return TARGET_ATARIST;
|
|
if (strcmp(name, "dos") == 0) return TARGET_DOS;
|
|
return TARGET_INVALID;
|
|
}
|
|
|
|
|
|
static int usage(const char *prog) {
|
|
fprintf(stderr,
|
|
"usage: %s --target {iigs,amiga,atarist,dos} \\\n"
|
|
" [--width-tiles N --height-tiles M] \\\n"
|
|
" INPUT OUTPUT.spr\n"
|
|
" INPUT is a PPM (P6) file (auto-derives tile dims from W/8, H/8)\n"
|
|
" or a raw .tiles byte stream (requires --width-tiles/--height-tiles).\n",
|
|
prog);
|
|
return 2;
|
|
}
|
|
|
|
|
|
// 65816 / x86 / 68k all expect target-native byte order in the .spr
|
|
// header offsets, but the file format is little-endian (matches the
|
|
// runtime spriteFromCompiledMem parser, which reads byte-by-byte).
|
|
static int writeLE16(FILE *fp, uint16_t v) {
|
|
if (fputc((int)(v & 0xFFu), fp) == EOF) return -1;
|
|
if (fputc((int)((v >> 8) & 0xFFu), fp) == EOF) return -1;
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ----- main -----
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *targetName;
|
|
const char *inPath;
|
|
const char *outPath;
|
|
long widthTiles;
|
|
long heightTiles;
|
|
int i;
|
|
TargetE target;
|
|
uint8_t *tileBytes;
|
|
uint32_t tileSize;
|
|
uint32_t expectedTileSize;
|
|
SpriteT sp;
|
|
int rc;
|
|
|
|
targetName = NULL;
|
|
widthTiles = 0;
|
|
heightTiles = 0;
|
|
inPath = NULL;
|
|
outPath = NULL;
|
|
|
|
for (i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--target") == 0 && i + 1 < argc) {
|
|
targetName = argv[++i];
|
|
} else if (strcmp(argv[i], "--width-tiles") == 0 && i + 1 < argc) {
|
|
widthTiles = strtol(argv[++i], NULL, 10);
|
|
} else if (strcmp(argv[i], "--height-tiles") == 0 && i + 1 < argc) {
|
|
heightTiles = strtol(argv[++i], NULL, 10);
|
|
} else if (inPath == NULL) {
|
|
inPath = argv[i];
|
|
} else if (outPath == NULL) {
|
|
outPath = argv[i];
|
|
} else {
|
|
return usage(argv[0]);
|
|
}
|
|
}
|
|
if (targetName == NULL || inPath == NULL || outPath == NULL) {
|
|
return usage(argv[0]);
|
|
}
|
|
if (widthTiles < 0 || widthTiles > 255 ||
|
|
heightTiles < 0 || heightTiles > 255) {
|
|
return usage(argv[0]);
|
|
}
|
|
|
|
target = parseTarget(targetName);
|
|
if (target == TARGET_INVALID) {
|
|
fprintf(stderr, "joeysprite: unknown --target %s\n", targetName);
|
|
return usage(argv[0]);
|
|
}
|
|
|
|
if (fileIsPpm(inPath)) {
|
|
// PPM path: tile dims auto-derive (or validate against CLI).
|
|
rc = loadPpmAsTiles(inPath, &widthTiles, &heightTiles, &tileBytes, &tileSize);
|
|
if (rc != 0) {
|
|
return rc;
|
|
}
|
|
} else {
|
|
// Raw .tiles path: tile dims required.
|
|
if (widthTiles <= 0 || heightTiles <= 0) {
|
|
fprintf(stderr,
|
|
"joeysprite: %s is not a PPM; --width-tiles and --height-tiles are required\n",
|
|
inPath);
|
|
return usage(argv[0]);
|
|
}
|
|
rc = loadTileData(inPath, &tileBytes, &tileSize);
|
|
if (rc != 0) {
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
expectedTileSize = (uint32_t)(widthTiles * heightTiles * 32);
|
|
if (tileSize != expectedTileSize) {
|
|
fprintf(stderr,
|
|
"joeysprite: %s is %u bytes; expected %u (%ld * %ld tiles * 32 bytes)\n",
|
|
inPath, (unsigned)tileSize, (unsigned)expectedTileSize,
|
|
widthTiles, heightTiles);
|
|
free(tileBytes);
|
|
return 2;
|
|
}
|
|
|
|
sp.tileData = tileBytes;
|
|
sp.widthTiles = (uint8_t)widthTiles;
|
|
sp.heightTiles = (uint8_t)heightTiles;
|
|
sp.ownsTileData = false;
|
|
sp.slot = NULL;
|
|
memset(sp.routineOffsets, 0, sizeof(sp.routineOffsets));
|
|
sp.flags = SPRITE_FLAGS_NONE;
|
|
|
|
rc = compileToSpr(&sp, target, outPath);
|
|
free(tileBytes);
|
|
return rc;
|
|
}
|