385 lines
13 KiB
C
385 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
|
|
//
|
|
// assetbake.c -- bake indexed PNG assets into JoeyLib's native binary
|
|
// formats:
|
|
// * Tile banks (.tbk) are per target: plane-major on the Amiga and the
|
|
// X68000, a row's four plane bytes together on the ST, chunky 4bpp on
|
|
// DOS and the IIgs (the layout each port's tile paste reads).
|
|
// * Sprite cels (.spr) are chunky 4bpp for every target: the sprite
|
|
// walker converts at draw time.
|
|
// Both embed the PNG palette's first 16 entries as $0RGB.
|
|
//
|
|
// Source PNGs must be indexed with pixel dimensions that are multiples of
|
|
// 8. Tile banks take one 8x8 tile a grid cell, row by row; sprite sheets
|
|
// one cel a grid cell, the cel size given in tiles with --cell WxH.
|
|
//
|
|
// assetbake --type tile --target amiga|atarist|dos|iigs|x68000 in.png out.tbk
|
|
// assetbake --type sprite --cell WxH in.png out.spr
|
|
//
|
|
// .tbk: "JTB1", target (1 amiga, 2 atarist, 3 dos, 4 iigs, 5 x68000),
|
|
// hasPalette, tileCount (LE16), 4 reserved, 16 LE16 $0RGB palette words,
|
|
// then 32 bytes a tile.
|
|
// .spr: "JSP1", target 0, hasPalette, widthTiles, heightTiles, cellCount
|
|
// (LE16), 2 reserved, the palette, then each cel's tiles left to right, top
|
|
// to bottom, 8 rows of 4 chunky bytes each (high nibble the left pixel).
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "toolFile.h"
|
|
#include "toolPng.h"
|
|
|
|
#define TOOL_NAME "assetbake"
|
|
#define MAGIC_BYTES 4u
|
|
#define TARGET_CROSS_CHUNKY 0u
|
|
#define TARGET_AMIGA 1u
|
|
#define TARGET_ATARIST 2u
|
|
#define TARGET_DOS 3u
|
|
#define TARGET_IIGS 4u
|
|
#define TARGET_X68000 5u
|
|
#define TILE_PX 8u
|
|
#define TILE_BYTES 32u
|
|
#define PLANES 4u
|
|
#define PALETTE_ENTRIES 16u
|
|
#define HEADER_BYTES 44u
|
|
#define HEADER_PALETTE_AT 12u
|
|
#define MAX_COUNT 0xFFFFu
|
|
#define NIBBLE_MASK 0x0Fu
|
|
#define NIBBLE_SHIFT 4u
|
|
#define USAGE_EXIT 2
|
|
|
|
typedef void (*TileEncoderT)(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
|
|
|
|
typedef struct TargetT {
|
|
const char *name;
|
|
uint8_t id;
|
|
TileEncoderT encoder;
|
|
} TargetT;
|
|
|
|
static void encodeTileChunky(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
|
|
static void encodeTilePlaneMajor(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
|
|
static void encodeTileRowPlanes(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out);
|
|
static void headerWrite(uint8_t *header, const char *magic, const ToolImageT *img);
|
|
static void loadIndexedPng(const char *path, ToolImageT *img);
|
|
static const char *optionValue(int argc, char **argv, int *a, const char *name);
|
|
static void parseCell(const char *spec, uint32_t *w, uint32_t *h);
|
|
static uint8_t planeByte(const ToolImageT *img, uint32_t ox, uint32_t y, uint32_t plane);
|
|
static void usage(const char *message);
|
|
static void writeOutput(const char *path, const uint8_t *header, const uint8_t *body, size_t bodyLen);
|
|
|
|
static const TargetT kTargets[] = {
|
|
{ "amiga", TARGET_AMIGA, encodeTilePlaneMajor },
|
|
{ "atarist", TARGET_ATARIST, encodeTileRowPlanes },
|
|
{ "dos", TARGET_DOS, encodeTileChunky },
|
|
{ "iigs", TARGET_IIGS, encodeTileChunky },
|
|
{ "x68000", TARGET_X68000, encodeTilePlaneMajor }
|
|
};
|
|
|
|
#define TARGET_COUNT (sizeof(kTargets) / sizeof(kTargets[0]))
|
|
|
|
|
|
// 8 rows of 4 bytes, the high nibble the left pixel.
|
|
static void encodeTileChunky(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out) {
|
|
const uint8_t *row;
|
|
uint32_t r;
|
|
uint32_t c;
|
|
|
|
for (r = 0; r < TILE_PX; r++) {
|
|
row = img->pixels + (size_t)(oy + r) * img->width + ox;
|
|
for (c = 0; c < TILE_PX; c += 2u) {
|
|
*out++ = (uint8_t)(((row[c] & NIBBLE_MASK) << NIBBLE_SHIFT) | (row[c + 1u] & NIBBLE_MASK));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The Amiga and the X68000: plane 0's 8 row bytes, then plane 1's, ...
|
|
static void encodeTilePlaneMajor(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out) {
|
|
uint32_t k;
|
|
uint32_t r;
|
|
|
|
for (k = 0; k < PLANES; k++) {
|
|
for (r = 0; r < TILE_PX; r++) {
|
|
*out++ = planeByte(img, ox, oy + r, k);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The ST: each row's plane bytes 0..3 together.
|
|
static void encodeTileRowPlanes(const ToolImageT *img, uint32_t ox, uint32_t oy, uint8_t *out) {
|
|
uint32_t r;
|
|
uint32_t k;
|
|
|
|
for (r = 0; r < TILE_PX; r++) {
|
|
for (k = 0; k < PLANES; k++) {
|
|
*out++ = planeByte(img, ox, oy + r, k);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The header both formats share: the magic, the PNG palette's first 16
|
|
// entries as LE16 $0RGB (an entry the PNG lacks is 0), and the caller's
|
|
// fields at 4..11. An indexed PNG always has a palette.
|
|
static void headerWrite(uint8_t *header, const char *magic, const ToolImageT *img) {
|
|
uint16_t word;
|
|
uint32_t i;
|
|
|
|
memset(header, 0, HEADER_BYTES);
|
|
memcpy(header, magic, MAGIC_BYTES);
|
|
for (i = 0; i < PALETTE_ENTRIES && i < img->paletteCount; i++) {
|
|
word = (uint16_t)((img->palette[i][0] >> NIBBLE_SHIFT) << 8 | (img->palette[i][1] >> NIBBLE_SHIFT) << 4 | (img->palette[i][2] >> NIBBLE_SHIFT));
|
|
header[HEADER_PALETTE_AT + i * 2u] = (uint8_t)word;
|
|
header[HEADER_PALETTE_AT + i * 2u + 1u] = (uint8_t)(word >> 8);
|
|
}
|
|
}
|
|
|
|
|
|
static void loadIndexedPng(const char *path, ToolImageT *img) {
|
|
char err[TOOL_PNG_ERROR_BYTES];
|
|
|
|
if (!toolPngLoad(path, img, err)) {
|
|
toolFail("%s: %s", path, err);
|
|
}
|
|
if (!img->indexed) {
|
|
toolFail("%s is not an indexed PNG, need 'P' (indexed)", path);
|
|
}
|
|
if (img->width % TILE_PX != 0u || img->height % TILE_PX != 0u) {
|
|
toolFail("%s dims %ux%u not 8-px-tile multiples", path, img->width, img->height);
|
|
}
|
|
}
|
|
|
|
|
|
// The value of option `name` at argv[*a] (`--name value` or `--name=value`),
|
|
// *a stepped past it; NULL when argv[*a] is not that option.
|
|
static const char *optionValue(int argc, char **argv, int *a, const char *name) {
|
|
size_t len;
|
|
|
|
len = strlen(name);
|
|
if (strncmp(argv[*a], name, len) != 0) {
|
|
return NULL;
|
|
}
|
|
if (argv[*a][len] == '=') {
|
|
return argv[*a] + len + 1;
|
|
}
|
|
if (argv[*a][len] != '\0') {
|
|
return NULL;
|
|
}
|
|
if (*a + 1 >= argc) {
|
|
usage("an option needs a value");
|
|
}
|
|
*a += 1;
|
|
return argv[*a];
|
|
}
|
|
|
|
|
|
// "WxH" in tiles, as the Python read it: exactly one x (either case), and
|
|
// two integers, both positive.
|
|
static void parseCell(const char *spec, uint32_t *w, uint32_t *h) {
|
|
const char *x;
|
|
long width;
|
|
long height;
|
|
|
|
x = strpbrk(spec, "xX");
|
|
if (x == NULL || strpbrk(x + 1, "xX") != NULL) {
|
|
toolFail("bad --cell '%s', need 'WxH'", spec);
|
|
}
|
|
if (!toolParseInt(spec, (size_t)(x - spec), &width) || !toolParseInt(x + 1, strlen(x + 1), &height)) {
|
|
toolFail("bad --cell '%s', non-integer", spec);
|
|
}
|
|
if (width <= 0 || height <= 0) {
|
|
toolFail("bad --cell '%s', zero/negative", spec);
|
|
}
|
|
*w = (uint32_t)width;
|
|
*h = (uint32_t)height;
|
|
}
|
|
|
|
|
|
// One plane's bits of the 8 pixels from (ox, y), the leftmost in bit 7.
|
|
static uint8_t planeByte(const ToolImageT *img, uint32_t ox, uint32_t y, uint32_t plane) {
|
|
const uint8_t *row;
|
|
uint32_t c;
|
|
uint8_t b;
|
|
|
|
row = img->pixels + (size_t)y * img->width + ox;
|
|
b = 0u;
|
|
for (c = 0; c < TILE_PX; c++) {
|
|
if (((row[c] >> plane) & 1u) != 0u) {
|
|
b = (uint8_t)(b | (0x80u >> c));
|
|
}
|
|
}
|
|
return b;
|
|
}
|
|
|
|
|
|
// The command line, the targets read from kTargets (scripts read them from
|
|
// here: tests/host/tileBankStage.sh).
|
|
static void usage(const char *message) {
|
|
size_t i;
|
|
|
|
fprintf(stderr, "usage: %s --type {sprite,tile} [--target {", TOOL_NAME);
|
|
for (i = 0; i < TARGET_COUNT; i++) {
|
|
fprintf(stderr, "%s%s", i == 0u ? "" : ",", kTargets[i].name);
|
|
}
|
|
fprintf(stderr, "}] [--cell WxH] input output\n");
|
|
fprintf(stderr, "%s: error: %s\n", TOOL_NAME, message);
|
|
exit(USAGE_EXIT);
|
|
}
|
|
|
|
|
|
static void writeOutput(const char *path, const uint8_t *header, const uint8_t *body, size_t bodyLen) {
|
|
FILE *f;
|
|
bool ok;
|
|
|
|
f = fopen(path, "wb");
|
|
if (f == NULL) {
|
|
toolFail("cannot write %s", path);
|
|
}
|
|
ok = fwrite(header, 1, HEADER_BYTES, f) == HEADER_BYTES && fwrite(body, 1, bodyLen, f) == bodyLen;
|
|
if (fclose(f) != 0 || !ok) {
|
|
toolFail("cannot write %s", path);
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
ToolImageT img;
|
|
const TargetT *target;
|
|
const char *type;
|
|
const char *targetName;
|
|
const char *cell;
|
|
const char *paths[2];
|
|
const char *value;
|
|
uint8_t header[HEADER_BYTES];
|
|
uint8_t *body;
|
|
uint8_t *out;
|
|
size_t bodyLen;
|
|
uint32_t pathCount;
|
|
uint32_t count;
|
|
uint32_t cellW;
|
|
uint32_t cellH;
|
|
uint32_t cx;
|
|
uint32_t cy;
|
|
uint32_t tx;
|
|
uint32_t ty;
|
|
uint32_t i;
|
|
int a;
|
|
|
|
toolFileName(TOOL_NAME);
|
|
type = NULL;
|
|
targetName = NULL;
|
|
cell = NULL;
|
|
pathCount = 0u;
|
|
for (a = 1; a < argc; a++) {
|
|
value = optionValue(argc, argv, &a, "--type");
|
|
if (value != NULL) {
|
|
type = value;
|
|
continue;
|
|
}
|
|
value = optionValue(argc, argv, &a, "--target");
|
|
if (value != NULL) {
|
|
targetName = value;
|
|
continue;
|
|
}
|
|
value = optionValue(argc, argv, &a, "--cell");
|
|
if (value != NULL) {
|
|
cell = value;
|
|
continue;
|
|
}
|
|
if (argv[a][0] == '-' && argv[a][1] != '\0') {
|
|
usage("unrecognized arguments");
|
|
}
|
|
if (pathCount == 2u) {
|
|
usage("unrecognized arguments");
|
|
}
|
|
paths[pathCount++] = argv[a];
|
|
}
|
|
if (type == NULL || pathCount < 2u) {
|
|
usage("the following arguments are required: --type, input, output");
|
|
}
|
|
if (strcmp(type, "tile") != 0 && strcmp(type, "sprite") != 0) {
|
|
usage("argument --type: invalid choice");
|
|
}
|
|
target = NULL;
|
|
if (targetName != NULL) {
|
|
for (i = 0; i < TARGET_COUNT; i++) {
|
|
if (strcmp(targetName, kTargets[i].name) == 0) {
|
|
target = &kTargets[i];
|
|
}
|
|
}
|
|
if (target == NULL) {
|
|
usage("argument --target: invalid choice");
|
|
}
|
|
}
|
|
if (strcmp(type, "tile") == 0) {
|
|
if (target == NULL) {
|
|
toolFail("--type tile requires --target");
|
|
}
|
|
loadIndexedPng(paths[0], &img);
|
|
count = (img.width / TILE_PX) * (img.height / TILE_PX);
|
|
if (count > MAX_COUNT) {
|
|
toolFail("%s %u tiles exceeds 65535", paths[0], count);
|
|
}
|
|
headerWrite(header, "JTB1", &img);
|
|
header[4] = target->id;
|
|
header[5] = 1u;
|
|
header[6] = (uint8_t)count;
|
|
header[7] = (uint8_t)(count >> 8);
|
|
bodyLen = (size_t)count * TILE_BYTES;
|
|
body = (uint8_t *)malloc(bodyLen > 0u ? bodyLen : 1u);
|
|
if (body == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
out = body;
|
|
for (ty = 0; ty < img.height / TILE_PX; ty++) {
|
|
for (tx = 0; tx < img.width / TILE_PX; tx++) {
|
|
target->encoder(&img, tx * TILE_PX, ty * TILE_PX, out);
|
|
out += TILE_BYTES;
|
|
}
|
|
}
|
|
} else {
|
|
if (cell == NULL) {
|
|
toolFail("--type sprite requires --cell WxH");
|
|
}
|
|
parseCell(cell, &cellW, &cellH);
|
|
loadIndexedPng(paths[0], &img);
|
|
if (img.width % (cellW * TILE_PX) != 0u || img.height % (cellH * TILE_PX) != 0u) {
|
|
toolFail("%s %ux%u not multiple of cell %ux%u", paths[0], img.width, img.height, cellW * TILE_PX, cellH * TILE_PX);
|
|
}
|
|
count = (img.width / (cellW * TILE_PX)) * (img.height / (cellH * TILE_PX));
|
|
if (count > MAX_COUNT) {
|
|
toolFail("%s %u cels exceeds 65535", paths[0], count);
|
|
}
|
|
headerWrite(header, "JSP1", &img);
|
|
header[4] = TARGET_CROSS_CHUNKY;
|
|
header[5] = 1u;
|
|
header[6] = (uint8_t)cellW;
|
|
header[7] = (uint8_t)cellH;
|
|
header[8] = (uint8_t)count;
|
|
header[9] = (uint8_t)(count >> 8);
|
|
bodyLen = (size_t)count * cellW * cellH * TILE_BYTES;
|
|
body = (uint8_t *)malloc(bodyLen > 0u ? bodyLen : 1u);
|
|
if (body == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
out = body;
|
|
for (cy = 0; cy < img.height / (cellH * TILE_PX); cy++) {
|
|
for (cx = 0; cx < img.width / (cellW * TILE_PX); cx++) {
|
|
for (ty = 0; ty < cellH; ty++) {
|
|
for (tx = 0; tx < cellW; tx++) {
|
|
encodeTileChunky(&img, (cx * cellW + tx) * TILE_PX, (cy * cellH + ty) * TILE_PX, out);
|
|
out += TILE_BYTES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
writeOutput(paths[1], header, body, bodyLen);
|
|
free(body);
|
|
toolImageFree(&img);
|
|
return 0;
|
|
}
|