Files
libakgl/include/akgl/tilemap.h
Andrew Kesterson f0858b0d38 Document what the functions actually do instead of that they can fail
The Doxygen comments were generated from the declarations, so 217 @throws
lines across 21 headers read "When the corresponding validation or operation
fails" and told a caller nothing beyond the status name. The @param lines were
the same shape: every output was "Output destination populated by the
function", every instance "Object to initialize, inspect, or modify".

Rewritten against the implementations, following the pattern libakstdlib
already uses:

- @throws names the condition. akgl_sprite_load_json separated AKERR_KEY
  (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS
  (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL
  and AKGL_ERR_HEAP, which it raises and never declared.
- Parameters say whether they are required, what a NULL means, and what is
  written on a failure path. Where an argument is not checked, the doc says so:
  akgl_heap_next_actor's dest is a crash on NULL, not an error, and
  akgl_render_2d_frame_start dereferences self before testing it.
- The conventions move up to the file blocks so the per-function docs stay
  short. json_helpers.h states once that absence is an error here and that
  json_t * results are borrowed; heap.h explains the pool model and the
  acquire asymmetry; physics.h carries the thrust/environmental/velocity table.
- Struct fields, enum values, macros and exported globals are documented,
  including the dead ones - sprite_w/sprite_h, movetimer, p_scale and
  timer_gravity are read by nothing, and say so.

Also fixes ten comments in error.h and audio.h that opened with /** rather
than /**<, so Doxygen attached them to the following entity and rendered the
text as part of the macro's value. Verified against the generated HTML.

Comments only - no declaration changed. Doxygen builds clean under
WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:23:15 -04:00

494 lines
30 KiB
C

/**
* @file tilemap.h
* @brief Loading and drawing Tiled maps: tilesets, layers, embedded actors, per-map physics.
*
* The map format is the JSON export from the Tiled editor, so the level design
* tool is somebody else's and the loader is the only thing that has to know
* about it. A map is a stack of layers -- tile grids, images, and object groups
* -- drawn over a set of tilesets, each tileset being one image cut into a grid.
*
* Three things beyond ordinary Tiled semantics are worth knowing:
*
* - **Object layers can spawn actors.** An object of type `actor` names a
* character in its custom properties, and loading the map creates and
* registers the actor rather than just recording a rectangle.
* - **A map can carry its own physics.** A `physics.model` custom property on
* the map selects a backend, and `physics.gravity.*` / `physics.drag.*` set
* its constants, so a swimming level and a walking level differ by data
* rather than by code.
* - **A map can carry a pseudo-3D perspective.** Two objects named
* `p_foreground` and `p_vanishing` in an object layer, each with a `scale`
* property, define a band down the screen over which actors are scaled --
* which is what makes a character look further away as they walk up the
* screen. akgl_tilemap_scale_actor applies it.
*
* All paths inside a map -- tileset images, layer images -- are resolved
* relative to the map file, so a map and its art move together.
*
* Note the size of akgl_Tilemap: a layer's tile grid alone is 512x512 ints, and
* a tileset's offset table is 65536 pairs. It is a megabytes-large object and
* belongs in static storage, which is where `_akgl_gamemap` puts it. Do not put
* one on the stack.
*/
#ifndef _TILEMAP_H_
#define _TILEMAP_H_
#include <limits.h>
#include <akgl/actor.h>
#include <akgl/staticstring.h>
#include <akgl/physics.h>
#include <jansson.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
/** @brief Object `type` for a Tiled object whose `type` field is the string "actor". */
#define AKGL_TILEMAP_OBJECT_TYPE_ACTOR 1
/** @brief Layer `type` for a Tiled `tilelayer`: a grid of tile ids. */
#define AKGL_TILEMAP_LAYER_TYPE_TILES 1
/** @brief Layer `type` for a Tiled `objectgroup`: actors and perspective markers. */
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2
/** @brief Layer `type` for a Tiled `imagelayer`: one image drawn at the origin. */
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3
/** @brief One object placed in a Tiled object layer. */
typedef struct {
float x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
float y; /**< Position in map pixels. */
int gid; /**< Global tile id, for a tile object. Not read by the loader. */
int id; /**< Tiled's object id. Not read by the loader. */
int height; /**< Height in map pixels. Read into the map's perspective bands, not into this field. */
int width; /**< Width in map pixels. Not read by the loader. */
int rotation; /**< Rotation in degrees. Not read by the loader. */
int type; /**< #AKGL_TILEMAP_OBJECT_TYPE_ACTOR, or 0 for anything else. */
bool visible; /**< Copied onto the actor. Forced to `false` for perspective markers, which are geometry rather than scenery. */
akgl_Actor *actorptr; /**< The actor this object spawned or attached to, for an actor object. `NULL` otherwise. Borrowed. */
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE]; /**< Object name. For an actor object this is the registry key; for a perspective marker it is `p_foreground` or `p_vanishing`. */
} akgl_TilemapObject;
/** @brief One layer of a tilemap: a tile grid, an image, or a group of objects. */
typedef struct {
short type; /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */
float opacity; /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */
bool visible; /**< From Tiled. Recorded but not yet consulted at draw time. */
int height; /**< Tile layer: height in tiles. Image layer: the texture's height in pixels. */
int width; /**< Tile layer: width in tiles. Image layer: the texture's width in pixels. */
int x; /**< Layer offset from Tiled. Recorded but not applied at draw time. */
int y; /**< Layer offset from Tiled. */
int id; /**< Tiled's layer id. Not the same as the index into akgl_Tilemap::layers. */
SDL_Texture *texture; /**< Image layers only: the whole layer as one texture. `NULL` otherwise. Owned by this struct. */
int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT]; /**< Tile layers only: global tile ids in row-major order. 0 means an empty cell. */
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER]; /**< Object layers only. */
} akgl_TilemapLayer;
/** @brief One tileset: an image cut into a grid, plus the lookup table that finds a tile in it. */
typedef struct {
int columns; /**< Tiles per row in the image. What turns a linear tile id into a row and column. */
int firstgid; /**< Global id of this tileset's first tile. Subtracting it from a map cell gives the local tile id. */
char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE]; /**< Resolved absolute path to the image, from the map file's directory. */
int imageheight; /**< Image height in pixels, as declared by Tiled. */
int imagewidth; /**< Image width in pixels, as declared. */
char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE]; /**< Tileset name from Tiled. Diagnostic only; lookups go by `firstgid`. */
SDL_Texture *texture; /**< The image as one texture. Owned by this struct. */
// Use this as a lookup table instead of storing tiles
// in individual textures to blit them from a single
// texture at runtime
// FIXME: This is probably not very efficient. For a map
// with a single tileset it makes sense. For a map with
// multiple tilesets you may have set A start at firstgid 1
// and have 1728 tiles. Set B may start at firstgid 1729 and
// have 1728 more tiles. This means Set B has 1728 empty
// tile_offsets[] entries before firstgid 1729 because of the
// way akgl_tilemap_load_tilesets() works. This is really inefficient
// and should be improved in the future, and will eventually
// lead to premature exhaustion of AKGL_TILEMAP_MAX_TILES_PER_IMAGE
// because set D or E may only have 64 tiles but they may be
// at the upper end of the array bound already because of this.
int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2]; /**< Local tile id -> {x, y} pixel offset into the image. Computed once at load by akgl_tilemap_compute_tileset_offsets. */
int tilecount; /**< How many tiles the image holds. */
int tileheight; /**< Height of one tile in pixels. */
int tilewidth; /**< Width of one tile in pixels. */
int spacing; /**< Pixels between adjacent tiles in the image. */
int margin; /**< Pixels of border around the whole grid. Recorded but **not** accounted for by the offset computation. */
} akgl_Tileset;
/** @brief Represents a complete tilemap and its optional physics backend. */
typedef struct {
int tilewidth; /**< Width of one map cell in pixels. Tiles from a tileset with a different tile size are not rescaled. */
int tileheight; /**< Height of one map cell in pixels. */
int width; /**< Map width in tiles. */
int height; /**< Map height in tiles. */
int numlayers; /**< Layers actually loaded, at most #AKGL_TILEMAP_MAX_LAYERS. */
int orientation; /**< 0 = orthogonal, 1 = isometric. Always set to 0 by the loader; isometric is not implemented. */
int numtilesets; /**< Tilesets actually loaded. */
int p_foreground_y; /**< Y of the `p_foreground` marker: the row at which actors are at full size. 0 disables perspective. */
int p_vanishing_y; /**< Y of the `p_vanishing` marker: the row at which actors are smallest. 0 disables perspective. */
int p_foreground_h; /**< Height of the `p_foreground` marker object. Recorded; not used in the current rate calculation. */
int p_vanishing_h; /**< Height of the `p_vanishing` marker object. Recorded, likewise. */
float p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
float p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
float p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */
float p_rate; /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS]; /**< The tilesets, in the order Tiled listed them. */
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; /**< The layers, in draw order: index 0 is furthest back. */
// Different levels may have different physics.
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `physics` instead of the global backend. */
akgl_PhysicsBackend physics; /**< This map's own backend, valid only when `use_own_physics` is set. */
} akgl_Tilemap;
/**
* @brief Load a Tiled JSON map: physics, geometry, layers, tilesets, and any actors it spawns.
*
* Zeroes @p dest, then reads the map's own physics properties, its tile
* dimensions, its size in tiles, its layers, and its tilesets -- in that order,
* because layers reference tilesets by global id and objects reference
* characters by name. Finally, if the map carried both perspective markers, it
* works out the per-pixel scaling rate between them.
*
* Loading a map has side effects beyond @p dest: tileset and layer images are
* uploaded as textures, and every `actor` object in an object layer is created
* in the actor pool and published in #AKGL_REGISTRY_ACTOR. So the renderer, the
* pools, the registries, and the characters the map names all have to be in
* place first.
*
* @param fname Path to the map JSON. Required. Its directory becomes the root
* that every tileset and layer image inside is resolved against.
* @param dest The tilemap to fill in. Required. Zeroed first, so a map already
* loaded into it has its textures leaked rather than destroyed --
* call akgl_tilemap_release first if you are reusing one. Do not
* put one of these on the stack; see the note on this file.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fname or @p dest is `NULL`; if the map file
* cannot be opened or does not parse, with jansson's line and text; or
* if a tileset image cannot be loaded.
* @throws AKERR_OUTOFBOUNDS If the map is larger than
* #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT tiles, if it has
* more than #AKGL_TILEMAP_MAX_LAYERS layers, or if a layer's own
* declared size exceeds the same bound.
* @throws AKERR_KEY If a required key is absent, if an `actor` object has an
* empty name, or if the map names a physics model that does not exist.
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
* @throws ENOENT If @p fname or a path referenced inside it does not exist.
* @throws AKGL_ERR_SDL If a layer image cannot be loaded.
* @throws AKGL_ERR_HEAP If the string or actor pool is exhausted.
*
* @note Nothing is released on a failure part-way through: textures already
* uploaded and actors already created stay. Treat a failed load as
* needing akgl_tilemap_release and a fresh actor registry.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
/**
* @brief Draw one layer of a map, clipped to a viewport.
*
* Only the tiles that intersect @p viewport are drawn, and the tiles at its
* edges are drawn partially, so scrolling is smooth rather than snapping to the
* tile grid. An image layer ignores the viewport entirely and is drawn once at
* the origin.
*
* Drawing goes through the global `renderer`, not through a backend passed in.
*
* @param dest The map to draw from. Required. Not an output parameter
* despite the name.
* @param viewport The rectangle of the map, in map pixels, that is on screen.
* Required. Usually the global `camera`.
* @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not**
* bounds-checked: an index past the end reads a neighbouring
* layer, or past the array.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p viewport is `NULL`.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first
* failure abandons the rest of the layer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
/**
* @brief Draw a whole tileset to the screen, tile by tile, from its offset table.
*
* A debugging tool, not a rendering path. It reconstructs the original tileset
* image out of the offsets computed at load time, so if the picture that appears
* matches the source image the offset table is right -- and if tiles are shifted
* or duplicated, it is not. The `charviewer` utility uses it.
*
* @param dest The map holding the tileset. Required. Not an output
* parameter despite the name.
* @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A
* negative index is not rejected.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises.
*
* @note Tiles are laid out using the *map's* tile size rather than the
* tileset's, so a tileset whose tiles are a different size from the map's
* is reconstructed at the wrong pitch.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
/*
* These functions are part of the internal API and should not be called by the user.
* They are only exposed here for unit testing.
*/
/**
* @brief Find one entry in a Tiled `properties` array by name, checking its declared type.
*
* Tiled does not store custom properties as JSON object members. It stores them
* as an array of `{"name": ..., "type": ..., "value": ...}` objects, so getting
* one means a linear scan and a type-string comparison rather than a lookup.
* That is what this does; the `akgl_get_json_properties_*` wrappers put a typed
* face on it.
*
* @param obj The Tiled object -- map, layer, or object -- whose `properties`
* array to search. Required.
* @param key The property name to find. Required.
* @param type The type string Tiled wrote, e.g. `"string"`, `"int"`, `"float"`.
* A property found under the right name but the wrong type is an
* error, not a miss. Not `NULL`-checked.
* @param dest Receives a borrowed pointer to the whole property object -- the
* one with `name`, `type`, and `value` -- not to its value. Not
* `NULL`-checked.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If @p obj has no `properties` array, or if no entry in it
* has that name.
* @throws AKERR_TYPE If `properties` is not an array, an entry is malformed, or
* the named property's declared type is not @p type. That last message
* reports both the expected and the actual type.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
/**
* @brief Read a Tiled custom property declared as `string`.
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value in a *newly claimed* pool string. Required. Note
* that this always claims -- unlike akgl_get_json_string_value it
* does not reuse a string already in `*dest`, so passing one leaks
* it. The caller releases the result with akgl_heap_release_string.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `string`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
/**
* @brief Read a Tiled custom property declared as `int`.
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `int`, or its `value` is
* not an integer.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
/**
* @brief Build a tileset's local-id-to-pixel-offset table.
*
* Run once per tileset at load time so drawing a tile is an array lookup rather
* than a division. Walks the grid left to right and top to bottom, stepping by
* tile size plus `spacing`.
*
* @param dest The map holding the tileset. Required in practice, though
* not checked -- it is dereferenced immediately.
* @param tilesetidx Which tileset, 0 to `numtilesets - 1`. Not bounds-checked.
* @return `NULL`. There is no failure path: it is arithmetic into an already
* allocated table.
*
* @note Two known limits. `margin` is not accounted for, so a tileset image with
* a border produces offsets shifted by it. And the table is indexed by
* local id from 0, while akgl_tilemap_draw indexes it by
* `tilenum - firstgid`, so the entries a second tileset needs sit at the
* front of its own table -- see the FIXME on akgl_Tileset::tile_offsets
* for why that wastes space.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
/**
* @brief Load one object layer: spawn its actors and record its perspective markers.
*
* Two kinds of object are understood. `actor` objects name a registered actor,
* or create one -- claiming it from the pool, initializing it, and binding the
* character named in its `character` property -- and place it at the object's
* coordinates on this layer. `perspective` objects named `p_foreground` or
* `p_vanishing` set the map's scaling band and are themselves invisible.
* Anything else is recorded and otherwise ignored.
*
* An actor named by two objects is not duplicated: the second takes another
* reference on the first.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object -- not the map root. Required.
* @param layerid Index of the layer being loaded, which becomes each spawned
* actor's `layer`. Not bounds-checked.
* @param dirname Directory to resolve paths against. Accepted for signature
* consistency with the other layer loaders; nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
* @throws AKERR_KEY If the layer has no `objects` array, if a required key is
* absent, or if an `actor` object has an empty name.
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
* @throws AKERR_* Whatever akgl_actor_initialize or akgl_actor_set_character
* raises -- notably AKERR_KEY if the named character is not registered.
*
* @warning The object count is not checked against
* #AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, so a layer with more than 128
* objects writes past the array.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
* @brief Load one tile layer's grid of global tile ids.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object -- not the map root. Required.
* @param layerid Index of the layer being loaded. Not bounds-checked.
* @param dirname Directory to resolve paths against. Required, though a tile
* layer references no files and nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_OUTOFBOUNDS If the layer's declared `width` times `height`
* reaches #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT, or if the
* `data` array is shorter than that product.
* @throws AKERR_KEY If `height`, `width`, or `data` is absent.
* @throws AKERR_TYPE If one of them has the wrong JSON type, or a cell is not an
* integer.
*
* @note The declared `width` and `height` are trusted over the actual length of
* `data`: a layer declaring more cells than it lists fails on the read
* past the end rather than on a length check.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
/**
* @brief Load every layer of a map, dispatching on each layer's declared type.
*
* Reads the common fields -- id, opacity, visibility, offset -- then hands the
* layer to the loader for its kind: `objectgroup`, `tilelayer`, or `imagelayer`.
* A layer of any other type keeps its common fields and is otherwise skipped,
* with `type` left at 0.
*
* @param dest The map to load into. Required. `numlayers` is set from the
* array's length before any bound is checked, so it may briefly
* exceed #AKGL_TILEMAP_MAX_LAYERS on the failure path.
* @param root The map's root JSON object. Required.
* @param dirname Directory to resolve layer image paths against. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_OUTOFBOUNDS If the map has more than #AKGL_TILEMAP_MAX_LAYERS
* layers.
* @throws AKERR_KEY If the map has no `layers` array, or a layer is missing a
* common field.
* @throws AKERR_TYPE If a field has the wrong JSON type.
* @throws AKERR_* Whatever the per-kind loader raises.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
/**
* @brief Load one tileset's metadata and upload its image.
*
* @param tileset The tileset's JSON object. Required in practice, though not
* checked -- it is passed straight to the accessors, which
* report it.
* @param dest The map to load into. Required, unchecked, dereferenced at once.
* @param tsidx Which slot to fill, 0 to #AKGL_TILEMAP_MAX_TILESETS - 1. Not
* bounds-checked.
* @param dirname Directory to resolve the tileset's `image` path against --
* the map file's own directory. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If a required key is missing its object, or if the
* tileset image fails to load. The image message carries
* `SDL_GetError()`.
* @throws AKERR_KEY If one of `columns`, `firstgid`, `imageheight`,
* `imagewidth`, `margin`, `spacing`, `tilecount`, `tileheight`,
* `tilewidth`, `name`, or `image` is absent.
* @throws AKERR_TYPE If one of them has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If the resolved image path is too long for a pooled
* string.
* @throws ENOENT If the image path does not exist.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note This does not go through the spritesheet registry, so a tileset image
* shared between two maps is loaded twice.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
/**
* @brief Load every tileset of a map and compute each one's offset table.
*
* @param dest The map to load into. Required. `numtilesets` is reset to 0 and
* incremented as each tileset succeeds, so it always reflects
* what actually loaded.
* @param root The map's root JSON object. Required.
* @param dirname Directory to resolve tileset image paths against. Required in
* practice; not checked here.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
* @throws AKERR_KEY If the map has no `tilesets` array.
* @throws AKERR_TYPE If `tilesets` is not an array of objects.
* @throws AKERR_* Whatever akgl_tilemap_load_tilesets_each raises.
*
* @warning The tileset count is not checked against
* #AKGL_TILEMAP_MAX_TILESETS, so a map with more than 16 tilesets
* writes past the array.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
/**
* @brief Destroy the textures a map owns.
*
* Call before reusing a tilemap struct for a different map; akgl_tilemap_load
* zeroes rather than releases, so without this the previous map's textures are
* leaked.
*
* @param dest The map to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
*
* @warning Known defect: the layer loop destroys `tilesets[i].texture` rather
* than `layers[i].texture`, so tileset textures are destroyed twice and
* image-layer textures never. Nothing is set to `NULL` either, so a
* second call is a use-after-free. It also does not release the actors
* the map's object layers created. TODO.md, "Known and still open"
* item 2.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
/**
* @brief Scale an actor for its distance up the screen, using the map's perspective band.
*
* The pseudo-3D trick: an actor at or below `p_foreground_y` is drawn at
* `p_foreground_scale`, one at or above `p_vanishing_y` at `p_vanishing_scale`,
* and one in between is interpolated linearly. A map with no perspective markers
* has both scales at 1.0 and a rate of 0, so this is a no-op rather than a
* special case.
*
* Only the actor's `scale` is written; nothing is drawn. akgl_game_update calls
* it before each actor's update when #AKGL_ITERATOR_OP_TILEMAPSCALE is set.
*
* @param map The map supplying the perspective band. Required.
* @param actor The actor to scale. Required. Its `y` is read and its `scale`
* written.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p map or @p actor is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
#endif //_TILEMAP_H_