// 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 #include #include #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; }