A tile layer with a `collidable` boolean custom property is collision geometry. Before this a game had to hard-code a layer index -- both examples do, because akgl_TilemapLayer does not retain the name Tiled wrote -- and that index changes the moment somebody reorders layers in the editor. Solid tiles are **not** given proxies. At the maximum map size that is a quarter of a million per layer, tens of megabytes of index to describe data that is already a dense grid sitting in the tilemap. The world keeps a borrowed pointer and reads layers[i].data[] over whatever cell range a query covers: nine array reads for a 32-pixel actor on 16-pixel tiles, nothing to build at level load, and nothing to maintain per frame. Static geometry that is not tile-aligned is still an ordinary proxy with AKGL_COLLISION_FLAG_STATIC; both mechanisms exist and tiles use the free one because there are a hundred thousand of them. Four public queries come with it, all of which answer without resolving: solid_at, box_blocked, query_box and settle. They are what a game reaches for when it wants to know rather than to be pushed -- a ledge probe ahead of a walking enemy, a check that a doorway is clear -- and the sidescroller cannot drop its hand-rolled collision without them. akgl_collision_settle is the one worth naming. Resolution stops a shape entering geometry and has nothing to say about one that began inside it: what it does instead is refuse every move, so an actor spawned in a wall is simply stuck. Level authors produce that constantly, so settling walks a shape up a tile at a time and refuses loudly rather than searching forever. Two defects found by writing the tests: - The fixture put an akgl_Tilemap on the stack and segfaulted before the first assertion. It is about 26 MB -- the layer and tileset arrays are sized for the worst case the format allows -- and tilemap.h says so. It is static now, with a comment saying why, because the next person to write a map fixture will reach for a local first as well. - The far-edge nudge used AKGL_COLLISION_EPSILON, which is 1e-6. That is a sensible tolerance on a unit vector and a meaningless one on a map coordinate: `float` carries about seven significant digits, so at a coordinate of 144 the smallest representable step is around 1.5e-5 and `144.0f - 1e-6f` is exactly 144.0f. The nudge did nothing, a box resting flush on the floor read as inside it, and every move it tried looked blocked -- an actor standing on the ground unable to walk. Two different quantities were sharing one constant; the tile one is now its own, at a thousandth of a pixel, which is what the sidescroller example independently arrived at. Both breaks verified: removing the nudge and ignoring the `collidable` bit each turn the suite red with the symptom named. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
660 lines
40 KiB
C
660 lines
40 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_default_gamemap` puts it. Do not put
|
|
* one on the stack.
|
|
*/
|
|
|
|
#ifndef _AKGL_TILEMAP_H_
|
|
#define _AKGL_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 akgl_TilemapObject {
|
|
float32_t x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
|
|
float32_t 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 akgl_TilemapLayer {
|
|
short type; /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */
|
|
float32_t 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 akgl_Tileset {
|
|
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 akgl_Tilemap {
|
|
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. */
|
|
float32_t p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
|
|
float32_t p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
|
|
float32_t p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */
|
|
float32_t 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.
|
|
uint32_t collidablelayers; /**< Bit `i` set means `layers[i]` is solid geometry for collision, from that layer's `collidable` custom property in Tiled. 0 means nothing on this map collides, which is what a map authored before collision existed gets. */
|
|
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `akgl_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 `akgl_renderer`, not through a backend passed in.
|
|
*
|
|
* @param map The map to draw from. Required.
|
|
* @param viewport The rectangle of the map, in map pixels, that is on screen.
|
|
* Required. Usually the global `akgl_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 map 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 *map, 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 map The map holding the tileset. Required.
|
|
* @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 map 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 *map, 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 Read a boolean Tiled custom property.
|
|
*
|
|
* Tiled writes these with a declared type of `"bool"`, and the accessor matches
|
|
* on that type -- a property written as an `int` named the same thing raises
|
|
* `AKERR_TYPE` rather than being coerced, because a map that says `1` where it
|
|
* means `true` is a map that will say something else next time.
|
|
*
|
|
* @param obj The object carrying a `properties` array. Required.
|
|
* @param key Property name. Required.
|
|
* @param dest Receives the value. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_KEY If no property of that name exists. **Absent is not an
|
|
* error to the loader** -- a layer with no `collidable` property is not
|
|
* collidable, which is the common case.
|
|
* @throws AKERR_TYPE If the property exists but was not written as a bool.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_boolean(json_t *obj, char *key, bool *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);
|
|
|
|
/**
|
|
* @brief Read a Tiled custom property declared as `number`.
|
|
*
|
|
* Tiled writes `float` for a floating-point property, so this matches nothing
|
|
* Tiled produces today -- akgl_get_json_properties_float is the one the loader
|
|
* uses. Kept because a hand-written or older map may still say `number`.
|
|
*
|
|
* @param obj The Tiled object whose `properties` array to search. Required.
|
|
* @param key The property name. Required.
|
|
* @param dest Receives the value as a `float`. 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 `number`, or its `value` is
|
|
* not numeric.
|
|
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_number(json_t *obj, char *key, float32_t *dest);
|
|
/**
|
|
* @brief Read a Tiled custom property declared as `float`.
|
|
*
|
|
* The spelling Tiled actually writes for a floating-point property. This is how
|
|
* the `scale` on a perspective marker is read.
|
|
*
|
|
* @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 `float`, or its `value` is
|
|
* not numeric.
|
|
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_float(json_t *obj, char *key, float32_t *dest);
|
|
/**
|
|
* @brief Read a Tiled custom property declared as `float`, at full precision.
|
|
*
|
|
* Same declared type as akgl_get_json_properties_float, but writing a `double`.
|
|
* The map's physics constants are `double`, which is the reason both exist.
|
|
*
|
|
* @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.
|
|
* akgl_tilemap_load_physics passes this status to
|
|
* akgl_get_json_with_default, which turns it into a default of 0.0.
|
|
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
|
|
* not numeric.
|
|
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_double(json_t *obj, char *key, float64_t *dest);
|
|
/**
|
|
* @brief Turn one Tiled object into a live actor, creating it if it is not already registered.
|
|
*
|
|
* The object's name is the actor's registry key, which is what lets the same
|
|
* actor be placed by more than one object: the first placement creates it and
|
|
* binds the character named in the object's `character` property, and every
|
|
* later one just takes another reference. Either way the object's position,
|
|
* visibility, and layer are copied onto the actor and the object keeps a
|
|
* pointer to it.
|
|
*
|
|
* Not declared in tilemap.h -- reachable only through
|
|
* akgl_tilemap_load_layer_objects.
|
|
*
|
|
* @param curobj The tilemap object, with its `name`, `x`, `y`, and
|
|
* `visible` already read from JSON. Required, unchecked,
|
|
* dereferenced at once.
|
|
* @param layerdatavalue The object's JSON, for the custom properties --
|
|
* `character` and `state` -- that are not part of Tiled's
|
|
* own object fields. Required.
|
|
* @param layerid The layer this object sits on, copied onto the actor.
|
|
* @param dirname Directory to resolve paths against. Accepted for
|
|
* signature consistency; nothing here uses it.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_KEY If the object's name is empty -- an actor has to be
|
|
* addressable -- if the `character` or `state` property is absent, or if
|
|
* the named character is not in #AKGL_REGISTRY_CHARACTER.
|
|
* @throws AKERR_TYPE If `character` is not declared `string` or `state` not
|
|
* declared `int`.
|
|
* @throws AKERR_NULLPOINTER If akgl_actor_initialize refuses the name.
|
|
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
|
|
*
|
|
* @note An existing actor's `character` and position are overwritten by each
|
|
* later placement, so two objects naming one actor leave it wherever the
|
|
* last one put it rather than producing two.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname);
|
|
/**
|
|
* @brief Load one image layer: upload its image as a single texture.
|
|
*
|
|
* An image layer is one picture rather than a grid, which is what a parallax
|
|
* backdrop wants. The layer's `width` and `height` are taken from the loaded
|
|
* texture rather than from the JSON, so they are always the true pixel size.
|
|
*
|
|
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load_layers.
|
|
*
|
|
* @param dest The map to load into. Required.
|
|
* @param root The layer's JSON object. Required.
|
|
* @param layerid Which layer slot to fill. Not bounds-checked.
|
|
* @param dirname Directory to resolve the layer's `image` path against.
|
|
* Required. Joined with a `/` rather than run through
|
|
* akgl_path_relative, so unlike a tileset image this path is not
|
|
* canonicalized and an absolute one will not work.
|
|
* @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_KEY If the layer has no `image`.
|
|
* @throws AKERR_TYPE If `image` is not a string.
|
|
* @throws AKGL_ERR_SDL If the image cannot be loaded. The message carries
|
|
* `SDL_GetError()`.
|
|
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
|
* @throws AKERR_OUTOFBOUNDS If `dirname` and the image name do not fit in
|
|
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE when joined, naming both
|
|
* lengths. This used to truncate silently, so an over-long path failed
|
|
* as a missing file rather than as the length error it is.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
|
/**
|
|
* @brief Give the map its own physics backend, if it asked for one.
|
|
*
|
|
* A map with a `physics.model` custom property gets its own backend, configured
|
|
* from `physics.gravity.x`/`.y`/`.z` and `physics.drag.x`/`.y`/`.z`, and
|
|
* `use_own_physics` is set so a caller knows to simulate through it. Each
|
|
* constant defaults to 0.0 when absent, so a map can name a model and override
|
|
* only what it cares about.
|
|
*
|
|
* Absence is the normal case at every level: a map with no properties at all, or
|
|
* with properties but no `physics.model`, uses the game's global backend and
|
|
* this returns success. That is why the AKERR_KEY handlers here are not error
|
|
* paths -- they are the "map did not ask" path.
|
|
*
|
|
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load.
|
|
*
|
|
* @param dest The map to configure. Required in practice; not checked here,
|
|
* since akgl_tilemap_load has already done so.
|
|
* @param root The map's root JSON object. Required, likewise.
|
|
* @return `NULL` on success -- including when the map declared no physics at
|
|
* all -- otherwise an error context owned by the caller.
|
|
* @throws AKERR_KEY If `physics.model` names a backend that does not exist.
|
|
* @throws AKERR_TYPE If `physics.model` is not declared `string`, or one of the
|
|
* six constants is not declared `float`.
|
|
* @throws AKERR_NULLPOINTER If akgl_physics_factory refuses its arguments.
|
|
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root);
|
|
|
|
#endif //_AKGL_TILEMAP_H_
|