248 lines
9.1 KiB
C
248 lines
9.1 KiB
C
// Native asset loaders for baked .spr and .tbk files produced by
|
|
// tools/assetbake/assetbake.py. Both formats are documented in detail
|
|
// in that tool; the short version:
|
|
//
|
|
// .spr cross-target chunky 4bpp sprite cels (the Phase 11 sprite
|
|
// walker reads chunky and c2p's inline at draw time, so a
|
|
// single bake serves every platform)
|
|
// .tbk per-target planar tile bytes (Amiga plane-major, ST row-
|
|
// major-with-planes-per-row, DOS+IIgs chunky 4bpp); the
|
|
// loader validates the file's target byte matches the build
|
|
// platform and refuses mismatches
|
|
//
|
|
// Both share a 44-byte header:
|
|
// 0 4 magic ("JSP1" or "JTB1")
|
|
// 4 1 target (0=cross-platform, 1=amiga, 2=ST, 3=dos, 4=iigs)
|
|
// 5 1 hasPalette
|
|
// 6 2 widthTiles + heightTiles (sprite) / tileCount LE16 (tile)
|
|
// 8 4 cellCount LE16 + reserved (sprite) / reserved (tile)
|
|
// 12 32 palette[16] LE16 $0RGB (zeros if hasPalette==0)
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "joey/sprite.h"
|
|
#include "joey/tile.h"
|
|
#include "spriteInternal.h"
|
|
|
|
// Push the file-IO out of the root segment on the IIgs. Every
|
|
// fopen / fread / fclose call drags a chunk of the stdio cluster
|
|
// in; concentrating them in one named segment keeps the root bank
|
|
// from overflowing past 64 KB (the failure mode is the linker's
|
|
// "Code exceeds code bank size" / "Address is not in current bank"
|
|
// errors documented in the feedback memory).
|
|
|
|
// ----- Constants -----
|
|
|
|
#define ASSET_HEADER_BYTES 44
|
|
#define ASSET_PALETTE_OFFSET 12
|
|
#define ASSET_PALETTE_ENTRIES 16
|
|
#define ASSET_TILE_DATA_BYTES 32 // 4bpp packed: 4 bytes/row * 8 rows
|
|
|
|
// Largest legitimately drawable sprite cel: the surface is 40x25 tiles
|
|
// (320/8 x 200/8), so any cel larger than that can never be fully
|
|
// on-screen. Reject crafted/truncated headers claiming more, which
|
|
// would otherwise force an unbounded per-cel malloc (DoS) and feed the
|
|
// downstream emitters/draw walkers out-of-range dimensions.
|
|
#define ASSET_MAX_TILES_W 40
|
|
#define ASSET_MAX_TILES_H 25
|
|
|
|
// Per-platform expected target byte for .tbk files. .spr files carry
|
|
// target=0 (cross-platform chunky) and pass on every build.
|
|
#if defined(JOEYLIB_PLATFORM_AMIGA)
|
|
#define EXPECTED_TILE_TARGET 1u
|
|
#elif defined(JOEYLIB_PLATFORM_ATARIST)
|
|
#define EXPECTED_TILE_TARGET 2u
|
|
#elif defined(JOEYLIB_PLATFORM_DOS)
|
|
#define EXPECTED_TILE_TARGET 3u
|
|
#elif defined(JOEYLIB_PLATFORM_IIGS)
|
|
#define EXPECTED_TILE_TARGET 4u
|
|
#elif defined(JOEYLIB_PLATFORM_BLANK)
|
|
#define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops)
|
|
#else
|
|
#error "Unknown platform for asset loader"
|
|
#endif
|
|
|
|
// ----- Prototypes -----
|
|
|
|
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp);
|
|
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette);
|
|
|
|
// ----- Internal helpers (alphabetical) -----
|
|
|
|
// Open `path`, read the shared 44-byte header, and validate the magic
|
|
// + target byte common to both .spr and .tbk. On success leaves *outFp
|
|
// open (caller must fclose) and returns true; on any failure closes
|
|
// the file (if opened) and returns false. When outPalette is non-NULL
|
|
// and the header advertises a palette (header[5] != 0), the palette is
|
|
// decoded into outPalette. Shared by jlSpriteBankLoad and
|
|
// jlTileBankLoad so the open/read/validate/parse-palette boilerplate
|
|
// (and its fclose-on-error pattern) lives in one place.
|
|
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp) {
|
|
FILE *fp;
|
|
|
|
fp = fopen(path, "rb");
|
|
if (fp == NULL) {
|
|
return false;
|
|
}
|
|
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
|
|
fclose(fp);
|
|
return false;
|
|
}
|
|
if (memcmp(header, magic, 4) != 0) {
|
|
fclose(fp);
|
|
return false;
|
|
}
|
|
if (header[4] != expectedTarget) {
|
|
fclose(fp);
|
|
return false;
|
|
}
|
|
if (outPalette != NULL && header[5] != 0u) {
|
|
readPaletteFromHeader(header, outPalette);
|
|
}
|
|
*outFp = fp;
|
|
return true;
|
|
}
|
|
|
|
|
|
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
|
|
uint8_t i;
|
|
|
|
for (i = 0; i < ASSET_PALETTE_ENTRIES; i++) {
|
|
outPalette[i] = (uint16_t)(header[ASSET_PALETTE_OFFSET + i * 2]
|
|
| (header[ASSET_PALETTE_OFFSET + i * 2 + 1] << 8));
|
|
}
|
|
}
|
|
|
|
// ----- Public API (alphabetical) -----
|
|
|
|
// Returns the number of cels successfully loaded into outCels[0..N).
|
|
// A return value strictly less than min(headerCellCount, maxCels)
|
|
// indicates the file was truncated/corrupt mid-stream relative to its
|
|
// own header count; the cels that were read are still valid.
|
|
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels,
|
|
uint16_t *outPalette) {
|
|
FILE *fp;
|
|
uint8_t header[ASSET_HEADER_BYTES];
|
|
uint8_t widthTiles;
|
|
uint8_t heightTiles;
|
|
uint16_t cellCount;
|
|
uint32_t tileBytes;
|
|
uint16_t i;
|
|
uint16_t loaded;
|
|
uint16_t toLoad;
|
|
|
|
if (path == NULL || outCels == NULL || maxCels == 0u) {
|
|
return 0u;
|
|
}
|
|
// Magic "JSP1" + target byte = 0 (cross-platform chunky). Anything
|
|
// else means the file was either baked wrong or is a stale .spr
|
|
// from the old compiled-codegen pipeline.
|
|
if (!openReadValidateHeader(path, "JSP1", 0u, header, outPalette, &fp)) {
|
|
return 0u;
|
|
}
|
|
widthTiles = header[6];
|
|
heightTiles = header[7];
|
|
cellCount = (uint16_t)(header[8] | (header[9] << 8));
|
|
|
|
// Reject degenerate (0 in either dimension -> tileBytes 0, an
|
|
// implementation-defined malloc(0)) and oversized (a crafted header
|
|
// could force a multi-MB per-cel malloc) sprites. Matches
|
|
// jlSpriteCreate's invariant and the 40x25-tile surface ceiling.
|
|
if (widthTiles == 0u || heightTiles == 0u ||
|
|
widthTiles > ASSET_MAX_TILES_W || heightTiles > ASSET_MAX_TILES_H) {
|
|
fclose(fp);
|
|
return 0u;
|
|
}
|
|
|
|
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)ASSET_TILE_DATA_BYTES;
|
|
toLoad = (cellCount < maxCels) ? cellCount : maxCels;
|
|
|
|
loaded = 0u;
|
|
for (i = 0; i < toLoad; i++) {
|
|
uint8_t *buf;
|
|
jlSpriteT *sp;
|
|
|
|
buf = (uint8_t *)malloc((size_t)tileBytes);
|
|
if (buf == NULL) {
|
|
break;
|
|
}
|
|
if (fread(buf, 1, (size_t)tileBytes, fp) != (size_t)tileBytes) {
|
|
free(buf);
|
|
break;
|
|
}
|
|
sp = (jlSpriteT *)malloc(sizeof(jlSpriteT));
|
|
if (sp == NULL) {
|
|
free(buf);
|
|
break;
|
|
}
|
|
sp->tileData = buf;
|
|
sp->widthTiles = widthTiles;
|
|
sp->heightTiles = heightTiles;
|
|
sp->ownsTileData = true;
|
|
sp->slot = NULL;
|
|
// 0xFF == SPRITE_NOT_COMPILED for every routine offset (0 is a
|
|
// valid offset, so zero-fill would alias a real entry).
|
|
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
|
|
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
|
|
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
|
|
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
|
|
outCels[i] = sp;
|
|
loaded++;
|
|
}
|
|
fclose(fp);
|
|
return loaded;
|
|
}
|
|
|
|
|
|
// Returns the number of tiles successfully loaded. outValid[0..maxTiles)
|
|
// is always fully written: true for each tile read, false for the rest
|
|
// (including slots past a truncated/short file), so the caller need not
|
|
// pre-zero it. A return value strictly less than min(headerTileCount,
|
|
// maxTiles) indicates the file was truncated/corrupt mid-stream.
|
|
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
|
|
bool *outValid, uint16_t *outPalette) {
|
|
FILE *fp;
|
|
uint8_t header[ASSET_HEADER_BYTES];
|
|
uint16_t tileCount;
|
|
uint16_t i;
|
|
uint16_t loaded;
|
|
uint16_t toLoad;
|
|
|
|
if (path == NULL || outTiles == NULL || maxTiles == 0u) {
|
|
return 0u;
|
|
}
|
|
// The target byte for .tbk must match the build platform. .spr
|
|
// files carry target=0; tiles carry the per-platform value. A
|
|
// mismatch is a build-system bug -- the per-target make rule
|
|
// should have produced a .tbk with the matching target byte.
|
|
if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
|
|
return 0u;
|
|
}
|
|
tileCount = (uint16_t)(header[6] | (header[7] << 8));
|
|
|
|
// Define the whole outValid array up front so leftover slots read
|
|
// false exactly as the tile.h contract promises, regardless of
|
|
// caller pre-zeroing.
|
|
if (outValid != NULL) {
|
|
for (i = 0; i < maxTiles; i++) {
|
|
outValid[i] = false;
|
|
}
|
|
}
|
|
|
|
toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;
|
|
|
|
loaded = 0u;
|
|
for (i = 0; i < toLoad; i++) {
|
|
if (fread(outTiles[i].pixels, 1, ASSET_TILE_DATA_BYTES, fp) != ASSET_TILE_DATA_BYTES) {
|
|
break;
|
|
}
|
|
if (outValid != NULL) {
|
|
outValid[i] = true;
|
|
}
|
|
loaded++;
|
|
}
|
|
fclose(fp);
|
|
return loaded;
|
|
}
|