# 13. Tilemaps

libakgl does not have a map format. It has a *loader* for somebody else's, and the
somebody else is the [Tiled map editor](https://www.mapeditor.org/). Levels are authored in
Tiled, saved as JSON (`.tmj`), and read by `akgl_tilemap_load`.

That division is deliberate, and it means this chapter owes you almost nothing about the
format itself. What a layer is, what a tileset is, how `firstgid` works, how custom
properties are stored — all of that is
[the Tiled JSON map format reference](https://doc.mapeditor.org/en/stable/reference/json-map-format/),
maintained by the people who write the editor. Read it there.

What this chapter owes you is the part Tiled cannot tell you: **which subset of the format
libakgl actually reads, what it refuses, the three things it adds on top, and how big the
resulting object is.** All four have bitten somebody.

## The size of an `akgl_Tilemap`

Start here, because it is the one that produces a crash rather than an error message.

```text
                sizeof(akgl_Tilemap) = 26,388,008 bytes = 25.2 MiB

  layers[16]      16 x 1,120,296  = 17.1 MiB   512x512 ints of tile data, plus
                                               128 x 560-byte objects, per layer
  tilesets[16]    16 x   528,944  =  8.1 MiB   65,536 x 2 ints of tile offsets,
                                               plus a PATH_MAX image path, each
  everything else                 = ~0.0 MiB   geometry, perspective, physics
```

**An `akgl_Tilemap` belongs in static storage and nowhere else.** A default thread stack is
8 MiB on Linux; this object is three times that, so a local one blows the stack before the
loader writes its first byte, and it does so as a segfault with no error context. That is
why the library's own map, `akgl_default_gamemap`, is a file-scope object in
`src/game.c`, and why `akgl_gamemap` is a pointer to it rather than a copy.

The bounds are fixed at compile time, so the size is fixed too. Every field is present
whether the map uses it or not: a 20x15 map with one tileset and two layers costs the same
25.2 MiB as a 511x511 one. Raising any of the constants below raises this number and
changes the ABI — see [Chapter 23](23-appendix-limits.md).

## Limits

```c excerpt=include/akgl/tilemap.h
/** @brief Widest map, in tiles. Width times height is what is actually bounded. */
#define AKGL_TILEMAP_MAX_WIDTH                        512
/** @brief Tallest map, in tiles. */
#define AKGL_TILEMAP_MAX_HEIGHT                       512
/** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */
#define AKGL_TILEMAP_MAX_LAYERS                       16
/** @brief Tilesets per map. */
#define AKGL_TILEMAP_MAX_TILESETS                     16
/** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */
#define AKGL_TILEMAP_MAX_TILES_PER_IMAGE              65536
/** @brief Longest tileset name. */
#define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE            512
/** @brief Longest resolved tileset image path. */
#define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE        PATH_MAX
/** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */
#define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE             512
/** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */
#define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER            128
```

Two of these carry a wrinkle worth knowing before you lay out a level.

**The width and height bound is on the product, and it is exclusive.** `akgl_tilemap_load`
refuses when `width * height >= AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT`, so
262,144 cells is one too many: **a literal 512x512 map is rejected with
`AKERR_OUTOFBOUNDS`**, and 512x511 is the largest rectangle that loads. The same
off-by-one applies per tile layer in `akgl_tilemap_load_layer_tile`. Nothing is wrong with
either check other than the `>=`; it is recorded here because "the maximum is 512x512" is
what the constant names say and is not what the code does.

**The doc comment on `AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER` is stale.** It says the loader
does not enforce it. The loader does — `akgl_tilemap_load_layer_objects` checks `j` at the
top of the loop body and raises `AKERR_OUTOFBOUNDS` naming the layer, and
`akgl_tilemap_load_tilesets` does the same against `AKGL_TILEMAP_MAX_TILESETS`. Both were
genuinely unbounded and both were fixed in 0.5.0 (`TODO.md`, "Found while rewriting the
Doxygen comments" item 17), with fixtures in `tests/tilemap.c` at exactly 128, 129 and 17.
Trust the code.

## What the loader accepts

The loader reads a strict subset of the TMJ format. Everything in this section is a
requirement, not a suggestion: a key that is absent raises `AKERR_KEY` and a key with the
wrong JSON type raises `AKERR_TYPE`, both naming what was wanted. See
[Chapter 4](04-errors.md) for what those statuses mean in libakgl.

| Level | Members `akgl_tilemap_load` requires | Notes |
|---|---|---|
| Map root | `tilewidth`, `tileheight`, `width`, `height`, `layers`, `tilesets` | `orientation` is read from nowhere and forced to 0; isometric is not implemented |
| Every layer | `id`, `opacity`, `visible`, `x`, `y`, `type` | A `type` other than the three below keeps these fields and is otherwise skipped |
| `tilelayer` | `width`, `height`, `data` | `data` must be a plain array of integers |
| `imagelayer` | `image` | Layer `width`/`height` are taken from the loaded texture, not the JSON |
| `objectgroup` | `objects` | See below — every object has requirements of its own |
| Every object | `name`, `x`, `y`, `visible`, `type` | **Including plain rectangles.** An object with no `type` member fails the whole load |
| Every tileset | `columns`, `firstgid`, `imagewidth`, `imageheight`, `margin`, `spacing`, `tilecount`, `tilewidth`, `tileheight`, `name`, `image` | The **embedded** form; see below |

Three of these deserve their own paragraph.

**Tilesets must be embedded, not external.** Tiled writes a tileset into the map's
`tilesets` array in one of two shapes: the whole tileset inline, or a two-member stub
`{"firstgid": N, "source": "tiles.tsj"}` pointing at a separate file.
`akgl_tilemap_load_tilesets_each` reads `columns`, `image` and the rest directly out of the
array element and there is no code anywhere in `src/` that opens a `source` file, so **an
external tileset reference fails with `AKERR_KEY` on the missing `columns`.** Turn
*Embed tileset* on when you save, or use Tiled's *Map → Embed Tilesets* command. The
shipped fixture `tests/assets/testmap.tmj` is embedded, which is what makes it load.

**Every object needs a `type` string, and `type` is where the extensions live.** The loader
reads it on every object in an object group and compares it against `"actor"` and
`"perspective"`; anything else is recorded in the layer's object array and otherwise
ignored. An object with the member missing entirely does not reach that comparison — it
fails the load. If your editor writes the object's class under a different member name,
that is the drift to check first; Tiled's own field naming has changed across releases, and
the fixture in this repository is a Tiled 1.8.2 export.

**The map file's directory is the root every path resolves against**, so a map and its art
move together. The two resolvers are not the same, though, and the difference matters:

| Path | Resolved by | Consequence |
|---|---|---|
| A tileset's `image` | `akgl_path_relative` | Canonicalized against the map's directory; an absolute path works |
| An image layer's `image` | `aksl_snprintf("%s/%s", ...)` | A plain join. **Not** canonicalized, so an absolute path becomes `/maps//art/sky.png` and does not open |

Both are bounded at `AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE` (`PATH_MAX`). The image-layer
join raises `AKERR_OUTOFBOUNDS` naming both lengths when it does not fit; it used to
truncate silently, and an over-long path reported itself as a missing file with a name the
caller never wrote.

## libakgl's three extensions to Tiled

None of these are part of the TMJ format. They are conventions the loader imposes on
custom properties and object types, and Tiled will happily author all three without
knowing what they mean.

### 1. Object type `actor` spawns a real actor

An object whose `type` is the string `"actor"` does not stay a rectangle. The loader
claims an actor from the pool, initializes it, binds a character, publishes it in
`AKGL_REGISTRY_ACTOR`, and copies the object's position, visibility and layer index onto
it. The object's `name` is the registry key. See [Chapter 12](12-actors.md) for what an
actor is and [Chapter 11](11-characters.md) for what a character supplies.

Two custom properties are required on the object:

| Property | Tiled type | Meaning |
|---|---|---|
| `character` | `string` | Name of a character already in `AKGL_REGISTRY_CHARACTER` |
| `state` | `int` | The actor's initial `AKGL_ACTOR_STATE_*` bitmask, **as a number** |

**`state` is an integer here, and only here.** Character JSON accepts the string-array form
— `["AKGL_ACTOR_STATE_ALIVE", "AKGL_ACTOR_STATE_FACE_DOWN"]` — because
`AKGL_ACTOR_STATE_STRING_NAMES` exists to decode it. `akgl_tilemap_load_layer_object_actor`
calls `akgl_get_json_properties_integer`, which demands a property Tiled declared as
`"int"`, so a map that spells the state as names raises `AKERR_TYPE`. Add up the bit values
from [Chapter 12](12-actors.md) and write the sum.

Two consequences of the registry being the identity:

- **The characters have to be loaded before the map.** An unregistered `character` name
  comes back as `AKERR_KEY` from `akgl_actor_set_character`, in the middle of a partly
  loaded map.
- **Two objects naming one actor do not make two actors.** The second placement takes
  another reference on the first and overwrites its position, so the actor ends up wherever
  the last object put it. That is the mechanism for placing one actor from two layers; it
  is not a way to spawn a crowd.

An `actor` object with an empty `name` is refused with `AKERR_KEY` — an actor has to be
addressable.

### 2. A `physics.model` property gives the map its own physics

A custom property named `physics.model` on the **map root** hands the map's name to
`akgl_physics_factory` and stores the resulting backend in `map->physics`, then sets
`map->use_own_physics`. Six more properties configure it, each defaulting to `0.0` when
absent:

| Property | Tiled type | Sets |
|---|---|---|
| `physics.model` | `string` | Which backend — `null` or `arcade`. See [Chapter 14](14-physics.md) |
| `physics.gravity.x` / `.y` / `.z` | `float` | The backend's gravity constants |
| `physics.drag.x` / `.y` / `.z` | `float` | The backend's drag coefficients |

A swimming level and a walking level then differ by data rather than by code.

**Nothing in the library acts on `use_own_physics`.** `akgl_game_update` steps the global
`akgl_physics` and never looks at the map's. The flag is a signal to *you*: read it after
loading and point `akgl_physics` at `map->physics` if you want it honoured. The example
below does exactly that.

A map with no `properties` array at all, or with properties but no `physics.model`, is the
normal case: `akgl_tilemap_load_physics` returns success and the map uses the game's global
backend. The `AKERR_KEY` handlers in that function are the "map did not ask" path, not
error paths. A `physics.model` naming a backend that does not exist *is* an error.

### 3. `p_foreground` and `p_vanishing` define the pseudo-3D band

Two objects in an object layer, each with `"type": "perspective"` and a `scale` custom
property declared `float`, define a band down the screen over which actors are scaled. That
is what makes a character look further away as they walk up a top-down map.

```text
  y = 0  -----------------------------------------------------  top of the map
                                  .
                                  .   actors above p_vanishing_y stay at
                                  .   p_vanishing_scale
  p_vanishing_y  -----------  [tiny actor]      scale = p_vanishing_scale
                                 /  \
                                /    \      linear interpolation between the
                               /      \     two rows, at p_rate per pixel of y
                              /        \
  p_foreground_y ---------  [ big actor ]      scale = p_foreground_scale
                                              actors below here stay at
                                              p_foreground_scale
```

The names are matched literally: `p_foreground` sets the row where actors are at full size,
`p_vanishing` the row where they are smallest. **Both the type and the name have to be
right.** The loader checks `type == "perspective"` first and only then compares the name,
so an object named `p_vanishing` with type `"object"` is silently just an object — no error,
no perspective, and a very confusing afternoon.

Each marker contributes its `y` and its `scale`; `p_rate` is derived at load as the scale
change per pixel between the two. **A marker at `y = 0` disables perspective**, because the
rate is only computed when both `p_foreground_y` and `p_vanishing_y` are non-zero — and a
map with no markers has both scales at 1.0 and a rate of 0, so the whole feature is a no-op
rather than a special case. Perspective markers are forced invisible: they are geometry,
not scenery.

`akgl_tilemap_scale_actor` applies the band, writing only the actor's `scale`. It is called
per actor by `akgl_game_update` against the **global** `akgl_gamemap` when the frame's
iterator carries `AKGL_ITERATOR_OP_TILEMAPSCALE`; see [Chapter 7](07-the-game-and-the-frame.md).
The marker heights (`p_foreground_h`, `p_vanishing_h`) are recorded and not used by the
current rate calculation.

## A minimal map

Two 16-pixel tiles square, one tile layer, one embedded tileset, and one actor object. The
state value `17` is `AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN` — 16 plus 1, from
the bit table in [Chapter 12](12-actors.md). Add the values you want; there is no symbolic
form on this side.

```json kind=tilemap setup=tilemap
{
  "compressionlevel": -1,
  "height": 2,
  "infinite": false,
  "nextlayerid": 3,
  "nextobjectid": 2,
  "orientation": "orthogonal",
  "renderorder": "right-down",
  "tiledversion": "1.8.2",
  "tileheight": 16,
  "tilewidth": 16,
  "type": "map",
  "version": "1.8",
  "width": 2,
  "layers": [
    {
      "data": [1, 2, 3, 4],
      "height": 2,
      "id": 1,
      "name": "Tile Layer 1",
      "opacity": 1,
      "type": "tilelayer",
      "visible": true,
      "width": 2,
      "x": 0,
      "y": 0
    },
    {
      "draworder": "topdown",
      "id": 2,
      "name": "Object Layer 1",
      "opacity": 1,
      "type": "objectgroup",
      "visible": true,
      "x": 0,
      "y": 0,
      "objects": [
        {
          "gid": 195,
          "height": 16,
          "id": 1,
          "name": "testactor",
          "rotation": 0,
          "type": "actor",
          "visible": true,
          "width": 16,
          "x": 16,
          "y": 16,
          "properties": [
            { "name": "character", "type": "string", "value": "testcharacter" },
            { "name": "state", "type": "int", "value": 17 }
          ]
        }
      ]
    }
  ],
  "tilesets": [
    {
      "columns": 48,
      "firstgid": 1,
      "image": "assets/tileset.png",
      "imageheight": 576,
      "imagewidth": 768,
      "margin": 0,
      "name": "World_A1",
      "spacing": 0,
      "tilecount": 1728,
      "tileheight": 16,
      "tilewidth": 16
    }
  ]
}
```

## Loading one

```c
#include <akgl/tilemap.h>
#include <akgl/error.h>
#include <akgl/game.h>

/*
 * 25 MiB of tilemap. Static storage, never a local: a stack frame this size
 * overruns the default 8 MiB thread stack before the loader writes a byte.
 */
static akgl_Tilemap townmap;

akerr_ErrorContext AKERR_NOIGNORE *load_town(void)
{
    PREPARE_ERROR(errctx);

    /* Reusing a map struct leaks the previous map's textures unless this runs
     * first -- akgl_tilemap_load zeroes rather than releases. */
    PASS(errctx, akgl_tilemap_release(&townmap));
    PASS(errctx, akgl_tilemap_load("assets/town.tmj", &townmap));

    /* The map may have brought its own physics with it. */
    if ( townmap.use_own_physics == true ) {
	akgl_physics = &townmap.physics;
    }
    akgl_gamemap = &townmap;
    SUCCEED_RETURN(errctx);
}
```

Loading a map has side effects well beyond the destination struct: textures are uploaded,
actors are created in the pool, and names are published in the registry. So **the renderer,
the pools, the registries and every character the map names have to be in place first.**
The startup order that produces that is in [Chapter 7](07-the-game-and-the-frame.md).

Drawing is per layer. `akgl_tilemap_draw(map, viewport, layeridx)` draws one layer clipped
to a viewport in map pixels — usually `akgl_camera` — and draws the tiles at the viewport's
edges partially, so scrolling is smooth rather than snapping to the grid. An image layer
ignores the viewport and is drawn once at the origin. `akgl_render_2d_draw_world` makes one
pass per layer; see [Chapter 8](08-rendering.md).

## Known defects

Documented here because this is where a reader hits them. Each is cross-referenced to
`TODO.md`.

**A failed load releases nothing.** `akgl_tilemap_load` zeroes the destination up front and
then loads physics, geometry, layers and tilesets in order. If any of that fails, the
textures already uploaded and the actors already created stay exactly where they are, and
the destination holds a half-built map. `CLEANUP` gives back the JSON document and one pool
string and nothing else. **Treat a failed load as needing `akgl_tilemap_release` and a
fresh actor registry** before you try again — otherwise the retry inherits the corpse, and
`akgl_tilemap_load` will zero the struct over the previous attempt's texture pointers and
leak them.

**`akgl_tilemap_release` does not release the actors an object layer created.** It destroys
tileset and image-layer textures and clears each pointer as it goes — that half is correct
as of 0.5.0, and a second release is safe rather than a use-after-free (`TODO.md`, "Known
and still open" item 2). The actors, and their references on characters and sprites, are
yours to release. The header's `@warning` describing a tileset double-free is stale; the
loop it describes was fixed and `tests/tilemap.c` releases the fixture three times and
asserts every texture pointer is `NULL`.

**`akgl_tilemap_draw` does not bounds-check `layeridx`.** An index at or past `numlayers`
reads a neighbouring layer, and one past 16 reads past the array. `akgl_tilemap_draw_tileset`
checks its upper bound but not for a negative index.

**A tileset's `margin` is recorded and never applied.** `akgl_tilemap_compute_tileset_offsets`
steps by tile size plus `spacing` and ignores `margin` entirely, so a tileset image with a
border produces offsets shifted by it — every tile drawn from slightly the wrong place. The
FIXME is at `src/tilemap.c:187`. Author tilesets with `margin: 0` until it is fixed.

**A second tileset wastes its whole offset table.** The offset table is indexed by local
tile id from 0, so tileset B's entries sit at the front of B's own 65,536-entry table while
`akgl_tilemap_draw` indexes it by `tilenum - firstgid` — correct, but it means every
tileset pays 512 KiB regardless of how many tiles it has. The FIXME on
`akgl_Tileset::tile_offsets` explains it. Consequence for you: prefer one large tileset per
map to five small ones.

**Tileset images do not go through the spritesheet registry**, so an image shared between
two maps is loaded twice, into two textures.

**`numlayers` is set before the layer bound is checked.** `akgl_tilemap_load_layers` assigns
the JSON array's length and *then* refuses a seventeenth layer, so on that failure path the
struct briefly claims more layers than it has. It is only reachable on a path that has
already failed, but do not read `numlayers` off a map whose load raised.
