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 by | Location | Source |
|---|---|---|
ti_gfx_blit, ti_gfx_sprite, ti_gfx_map, ti_gfx_text_font | Banked resources | ti_pack_load, or ti_res_write from near data |
| Sprite scenes | Near memory | C 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
imagereads a P3 or P6 PPM with 8-bit channels and 1-320 by 1-200 pixels. It gives each pixel the nearest palette entry.--palette-outputalso writes the sixteen palette words forti_gfx_begin.indexedreads JSON withwidth,height,pixelswith values 0-15 in row order, and an optional"masked": true.--maskedadds an opacity mask of one bit for each pixel, forti_gfx_sprite. In the mask, index 0 is transparent.- The output is a TI4 file: an 8-byte header with the width and height, then the packed rows, then the mask. Data formats gives the bytes.
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
- Entry names are unique and 1-15 bytes long. They start with a capital letter. The other characters are capitals, digits, periods or underscores.
- Each entry is 1 byte to 4 MiB. All of the pack must fit the 24-bit file size of ProDOS.
- Entry paths are relative to the manifest. They must be usual files in the folder of the manifest. The tool rejects links and paths outside that folder.
- The report gives the size of the pack and the minimum number of 48,640-byte banks that it needs. The report does not include the screen backup or other allocations.
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.