Art, fonts, maps and packs

Convert art to the packed SHR format on the host. Put it in a resource pack on the game disk. Load it into banked memory, and point surfaces, fonts and maps at it. Scene sprites are different: they are near C arrays in the program.

Where the art is

Used byLocationSource
ti_gfx_blit, ti_gfx_sprite, ti_gfx_map, ti_gfx_text_fontBanked resourcesti_pack_load, or ti_res_write from near data
Sprite scenesNear memoryC arrays in the program. In the full-game layout, constants run from $0800-$1FFF.

The palette

The tool matches each converted image to one shared palette. The palette is a JSON list of a maximum of sixteen colours. Each colour is a 0RGB integer or a "#RRGGBB" string. Missing entries are 0. A #RRGGBB colour keeps the high four bits of each channel.

["#000000", "#FFFFFF", "#FF0000", "#00C000", 255, 4095]

Make the palette together with the game. Entry 0 is the transparent colour of keyed sprites and masks. You can use the entries that the art does not use for palette animation.

Images

python tools/assets.py image ship.ppm ship.ti4 --palette palette.json --palette-output palette.bin --masked --report ship-layout.json
python tools/assets.py indexed ship.json ship.ti4

The tool uses only the standard library of Python and writes no timestamps. Thus the same input always gives the same bytes. Convert PNG art to PPM with an image editor first. The STARFALL generator, tools/starfall_assets.py, shows how to read indexed PNGs directly.

Fonts

{"width": 5, "height": 7, "first": 65, "glyphs": [
  [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
  ["####.", "#...#", "#...#", "####.", "#...#", "#...#", "####."]
]}
python tools/assets.py font font.json font.tif

A glyph has a maximum size of 8 by 16 pixels. Each glyph is height strings of width dots and hashes. first is the character code of the first glyph. Its default is 32. The built-in 5x7 font needs no file.

Tile maps

{"rows": [[0, 1, 1, 0], [2, 3, 3, 2]], "tile_count": 4}
python tools/assets.py map level.json level.tim

A map has 1-256 rows of 1-256 tile numbers. All rows have the same length. If you give tile_count, the tool rejects larger numbers. The tile atlas is a usual image.

Resource packs

A pack keeps a maximum of sixteen named entries in one ProDOS file. Thus a game opens one file for all of its banked art. The pack manifest gives the name of each entry and its source file:

{"entries": [
  {"name": "SHIP", "path": "ship.ti4"},
  {"name": "FONT", "path": "font.tif"},
  {"name": "LEVEL1", "path": "level.tim"}
]}
python tools/assets.py pack pack.json RESOURCES.TPK --report resources-layout.json

Keep the converted files and the pack manifest together in one folder of the game. Let the build make the pack and put it on the disk:

"generate": [
  {"command": ["{root}/tools/assets.py", "pack", "{game}/art/pack.json", "{build}/RESOURCES.TPK"],
   "inputs": ["{root}/tools/assets.py", "art"],
   "outputs": ["{build}/RESOURCES.TPK"]}
],
"disk": {"volume": "MYGAME", "system": "MYGAME.SYSTEM",
         "files": [{"name": "RESOURCES.TPK", "path": "{build}/RESOURCES.TPK"}]}

The disk packager writes files with a maximum size of 131,072 bytes.

Load an entry and make views

ti_pack_load copies one entry into a new resource and gives you its handle. The loader does not interpret the bytes. Read the 8-byte header. Then fill in a surface, sprite, font or map that points into the resource at offset 8.

These structures are borrowed views. When you no longer need any view of an entry, free the handle of the entry with ti_res_free. Do not call ti_gfx_surface_free on a view.

static uint16_t le16(const uint8_t *p) { return p[0] | (uint16_t)p[1] << 8; }

/* A TI4 entry as a surface. */
static ti_error image_view(ti_handle entry, ti_surface *view)
{
    uint8_t h[8];
    ti_error e = ti_res_read(entry, 0, h, 8);
    if (e) return e;
    if (h[0] != 'T' || h[1] != 'I' || h[2] != '4' || h[3]) return TI_E_FORMAT;
    view->pixels = entry;
    view->offset = 8;
    view->width = le16(h + 4);
    view->height = le16(h + 6);
    view->stride = (view->width + 1) / 2;
    return TI_OK;
}

static ti_handle ship_entry, font_entry, map_entry;
static ti_sprite ship;
static ti_bitmap_font font;
static ti_tilemap map;

ti_error load_art(void)
{
    uint8_t h[8];
    uint32_t size;
    ti_error e = ti_pack_load("RESOURCES.TPK", "SHIP", &ship_entry, &size);
    if (!e) e = image_view(ship_entry, &ship.image);
    if (e) return e;
    ship.mask = ship_entry;                        /* converted with --masked */
    ship.mask_offset = 8 + (uint32_t)ship.image.stride * ship.image.height;

    e = ti_pack_load("RESOURCES.TPK", "FONT", &font_entry, &size);
    if (!e) e = ti_res_read(font_entry, 0, h, 8);   /* "TIF1", first, count, width, height */
    if (e) return e;
    font.glyphs = font_entry; font.offset = 8;
    font.first = h[4]; font.count = h[5]; font.width = h[6]; font.height = h[7];

    e = ti_pack_load("RESOURCES.TPK", "LEVEL1", &map_entry, &size);
    if (!e) e = ti_res_read(map_entry, 0, h, 8);    /* "TIM1", columns, rows */
    if (e) return e;
    map.cells = map_entry; map.offset = 8;
    map.columns = le16(h + 4); map.rows = le16(h + 6);
    return TI_OK;
}

Each ti_pack_load call opens the file and examines all of the directory again. This is satisfactory when a level changes. Each entry uses one of the eight resource slots. Thus put small images together in one atlas, and use source rectangles. Do not load many entries.

A tile set is an image view with the tile size:

ti_tileset tiles;
image_view(atlas_entry, &tiles.image);
tiles.tile_width = 16; tiles.tile_height = 16;
tiles.columns = (uint8_t)(tiles.image.width / 16);
tiles.count = 40;

Sprites for a scene

Scene images are in near memory. Each image is an array of packed bytes with two pixels in each byte. The high nibble is the left pixel, and colour 0 is transparent. You can write a small image by hand:

/* A 4x4 orb: colour 15 edge, colour 14 centre. */
static const uint8_t orb[] = {
    0x0F, 0xF0,
    0xFE, 0xEF,
    0xFE, 0xEF,
    0x0F, 0xF0
};
static const ti_image images[] = {
    {orb, 2, 4, 2},          /* row_bytes, rows, stride */
};

For larger art, make a header in a generate step of the manifest. The STARFALL tool, tools/starfall_assets.py, reads the PNG files of its archive. It packs sixteen cells of 16x16 pixels. It writes their bytes, the palette and a table of small images in one cell. The STARFALL scene table gives each cell as an image of 8 bytes by 16 rows. It gives the small images in a cell with the 8-byte stride of that cell.

Constant arrays go to the RODATA segment. The full-game layout runs this segment from low memory at $0800-$1FFF. Thus main memory stays available for code and state.