Give every exported function a declaration, and check that it stays that way

Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.

The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.

The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.

scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.

The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.

Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.

24/24 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 23:44:38 -04:00
parent 9924d74dcc
commit 3a262bee54
29 changed files with 676 additions and 620 deletions

View File

@@ -302,6 +302,20 @@ endforeach()
add_test(NAME semver_unit COMMAND akgl_test_semver_unit) add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
# Not a C program, so it is not in AKGL_TEST_SUITES: it reads the built library's
# dynamic symbol table and every public header, and fails when a function with
# external linkage is declared nowhere. That is an ABI question rather than a
# behavioural one, and nineteen symbols had drifted out of the headers before
# anything was checking.
add_test(
NAME api_surface
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/check_api_surface.sh
$<TARGET_FILE:akgl>
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}/include
)
set_tests_properties(api_surface PROPERTIES SKIP_RETURN_CODE 2)
# One translation unit per public header, each including exactly that header and # One translation unit per public header, each including exactly that header and
# nothing before it. This is the only shape that proves a header is # nothing before it. This is the only shape that proves a header is
# self-contained: a second #include in the same file proves nothing about the # self-contained: a second #include in the same file proves nothing about the
@@ -325,6 +339,9 @@ int akgl_header_selftest_${header}(void)\n\
list(APPEND AKGL_HEADER_SELFTEST_SOURCES "${generated}") list(APPEND AKGL_HEADER_SELFTEST_SOURCES "${generated}")
endforeach() endforeach()
target_sources(akgl_test_headers PRIVATE ${AKGL_HEADER_SELFTEST_SOURCES}) target_sources(akgl_test_headers PRIVATE ${AKGL_HEADER_SELFTEST_SOURCES})
# Hand-written rather than generated: it checks a header's behaviour under a
# caller-supplied override, which is not something the generated shape covers.
target_sources(akgl_test_headers PRIVATE tests/header_pool_override.c)
# TIMEOUT is generous because CTest applies the same property to `ctest -T # TIMEOUT is generous because CTest applies the same property to `ctest -T
# memcheck`, where every suite runs under valgrind at something like twenty # memcheck`, where every suite runs under valgrind at something like twenty

197
TODO.md
View File

@@ -68,100 +68,110 @@ with no test to notice.
### 2. Header/implementation surface drift ### 2. Header/implementation surface drift
7. **Nineteen non-static functions are defined in `src/` but declared in no **Items 7, 8, 9, 11, 12, 13, 14 and 15 are resolved in 0.5.0.** Item 10 is
header.** They have external linkage and public-looking names, so they are resolved for every pair it listed.
part of the ABI whether intended or not, and no consumer can call them:
`akgl_game_load_objectnamemap`, `akgl_game_load_versioncmp`, 7. **Nineteen non-static functions were defined in `src/` but declared in no
`akgl_game_save_actors`, `akgl_game_save_actorname_iterator`, header.** Each is now declared or `static`:
`akgl_game_save_charactername_iterator`, `akgl_game_save_spritename_iterator`,
`akgl_game_save_spritesheetname_iterator` (`src/game.c`);
`akgl_get_json_properties_double`, `akgl_get_json_properties_float`,
`akgl_get_json_properties_number`, `akgl_tilemap_load_layer_image`,
`akgl_tilemap_load_layer_object_actor`, `akgl_tilemap_load_physics`
(`src/tilemap.c`); `akgl_path_relative_from`, `akgl_path_relative_root`
(`src/util.c`); `gamepad_handle_added`, `gamepad_handle_button_down`,
`gamepad_handle_button_up`, `gamepad_handle_removed` (`src/controller.c`).
Each should be either declared in its header or made `static`. Note that - `akgl_controller_handle_button_down`, `_button_up`, `_added`, `_removed`
`tilemap.h` already has a "part of the internal API, exposed here for unit were defined as `gamepad_handle_*` while `controller.h` declared the
testing" block — the tilemap entries belong there. `akgl_controller_handle_*` names it never defined. The definitions now
carry the declared names, which closes this and **Defects → Known and
still open item 10** in one change, and their documentation moved from the
definitions to the header where the convention puts it. `tests/controller.c`
no longer needs its local re-declarations.
- `akgl_game_save_actors` and `akgl_game_load_versioncmp` are declared in
`game.h` under a new "part of the internal API" block; `tests/game.c`
reaches them through the header now instead of declaring them itself.
- The four save-table iterators and `akgl_game_load_objectnamemap` are
`static` and have dropped the prefix -- `save_actorname_iterator`,
`load_objectnamemap` and so on. They are SDL enumeration callbacks and a
file-local reader; nothing outside `game.c` has any business calling them.
- `akgl_get_json_properties_number`, `_float`, `_double`,
`akgl_tilemap_load_layer_image`, `akgl_tilemap_load_layer_object_actor` and
`akgl_tilemap_load_physics` are declared in `tilemap.h`'s internal-API
block, again with their documentation moved to the header. That is where
the "does not need a renderer" work under **Remaining work** wanted them.
- `akgl_path_relative_root` is `static path_relative_root`.
`akgl_path_relative_from` is **deleted**; see below.
8. **`akgl_game_init_screen` is declared but never defined** **This is enforced now.** `scripts/check_api_surface.sh` reads the built
(`include/akgl/game.h:100`). Same failure mode as **Defects → Known and still library's dynamic symbol table, strips comments out of every public header,
open #10**, which covers the four `akgl_controller_handle_*` declarations; and fails when an exported `akgl_*` symbol is declared nowhere. It runs as
fold this one into that item. the `api_surface` CTest test. Stripping comments is the point: four of these
symbols were *mentioned* in `controller.h` prose, which is not the same as
being declared there and is exactly how they went unnoticed.
9. **Static helpers use three different naming styles.** `actor_visible` 8. **`akgl_game_init_screen` was declared and never defined.** The declaration
(`src/actor.c:185`) is bare; `akgl_character_load_json_inner` and is gone. Screen setup is `akgl_render_2d_init`.
`akgl_character_load_json_state_int_from_strings` (`src/character.c:101,132`)
and `akgl_sprite_load_json_spritesheet` (`src/sprite.c:51`) carry the full
public prefix; `gamepad_handle_*` (`src/controller.c:121+`) uses a third
subsystem word that appears nowhere else. Adopt one rule — the clearest is
that `static` helpers drop the `akgl_` prefix, since it exists to avoid
external collisions.
10. **Parameter names disagree between declaration and definition.** Doxygen 9. **Static helpers used three naming styles.** They drop the `akgl_` prefix
documents the header spelling, so the generated docs describe names the now, which is what it is for: `character_load_json_inner`,
implementation does not use: `character_load_json_state_int_from_strings`, `sprite_load_json_spritesheet`,
alongside the `actor_visible`, `write_exact` and `write_name_field` that
already did.
| Header | Implementation | 10. **Parameter names disagreed between declaration and definition.** All six
|---|---| pairs agree now. `akgl_character_initialize` takes `obj`,
| `character.h:41` `basechar` | `character.c:21` `obj` | `akgl_character_state_sprites_iterate` takes `props`, `akgl_heap_release_character`
| `character.h:69` `props` | `character.c:75` `registry` | takes `ptr`, `akgl_set_property` takes `value`, and the two `akgl_tilemap_draw*`
| ~~`heap.h:121` `ptr`~~ | ~~`heap.c:143` `basechar`~~ — fixed in 0.5.0 | functions take `map` -- the header called them `dest` and documented them as
| `registry.h:97` `value` | `registry.c:163` `src` | "Output destination populated by the function", which they are not.
| `json_helpers.h:134` `e` | `json_helpers.c:149` `err` |
| `tilemap.h:134,143` `dest` | `tilemap.c:646,767` `map` |
The `tilemap.h` pair is the most misleading: the parameter is the map being `akgl_get_json_with_default` was the interesting one: it took the incoming
*read* and drawn, but it is named `dest` and documented as "Output context as `err` and named its *own* context `e`, which is the name the
destination populated by the function". convention reserves for an incoming one. It is `e` and `errctx` now, and
renaming it was what surfaced that the two had been swapped rather than
merely misspelled.
11. **Object-pool size macros are defined twice, and the override hook is 11. **Object-pool size macros were defined twice and the override hook was
dead.** `heap.h:15-29` wraps `AKGL_MAX_HEAP_ACTOR`, `_SPRITE`, `_SPRITESHEET`, dead.** `AKGL_MAX_HEAP_ACTOR`, `_SPRITE`, `_SPRITESHEET` and `_CHARACTER` are
`_CHARACTER`, and `_STRING` in `#ifndef` guards so a consumer can override defined once, in `heap.h`, inside the `#ifndef` guards that were always
them, but `actor.h:65`, `sprite.h:19-20`, and `character.h:14` define the same supposed to make them overridable. `actor.h`, `sprite.h` and `character.h`
four unconditionally and are included from `heap.h:9-11`. Whichever header no longer define them.
lands first wins and the `#ifndef` never fires, so the override mechanism
cannot work. Define each pool size once — `heap.h` is the natural home.
12. **Headers rely on their includers for types.** `iterator.h` used `uint32_t` `tests/header_pool_override.c` is the regression test: it defines its own
without `<stdint.h>`; `json_helpers.h` used `json_t` without `<jansson.h>`; ceilings, includes `heap.h`, and `#error`s if the guard did not fire or if
`util.h` used `SDL_FRect` and `bool` without any SDL include. Each compiled `AKGL_MAX_HEAP_SPRITE` stopped deriving from `AKGL_MAX_HEAP_ACTOR`. The
only because of `.c`-file include ordering. assertion is the compile -- it deliberately disagrees with the built
library's ceilings and never touches the pools, which is fine because
overriding a ceiling means the library and everything linking it have to be
rebuilt together anyway.
12. **Headers relied on their includers for types.** `iterator.h` used
`uint32_t` without `<stdint.h>`; `json_helpers.h` used `json_t` without
`<jansson.h>`; `util.h` used `SDL_FRect` and `bool` without any SDL include.
Each compiled only because of `.c`-file include ordering.
**Resolved, and enforced rather than merely fixed.** `AKGL_PUBLIC_HEADERS` **Resolved, and enforced rather than merely fixed.** `AKGL_PUBLIC_HEADERS`
in `CMakeLists.txt` is now the single list behind both `install()` and a in `CMakeLists.txt` is now the single list behind both `install()` and a
generated translation unit per header each including exactly that header generated translation unit per header -- each including exactly that header
and nothing before it linked into the `headers` suite. A header that ships and nothing before it -- linked into the `headers` suite. A header that
is a header that is checked, and adding one to that list is all a new header ships is a header that is checked.
needs.
Writing the check found a case this item had missed: `registry.h` uses Writing that check found a case this item had missed: `registry.h` uses
`SDL_PropertiesID` in eight declarations and included no SDL header at all. `SDL_PropertiesID` in eight declarations and included no SDL header at all.
That is the argument for generating the check off the install list instead That is the argument for generating the check off the install list rather
of hand-listing the headers thought to be at risk. than hand-listing the headers somebody thought were at risk.
13. **Include spelling is split between quoted and angled forms for the same 13. **Include spelling was split between quoted and angled forms.**
directory.** **Resolved.** Every in-project include in a public header uses **Resolved.** Every in-project include in a public header uses
`#include <akgl/sibling.h>`. `tests/*.c` still use `#include "testutil.h"`, `#include <akgl/sibling.h>`, and `staticstring.h`'s `#include "string.h"` --
correctly: that one is a test-local header rather than an installed one. a relative-first lookup that reached the system header by accident -- is
`#include <string.h>`. `tests/*.c` still use `#include "testutil.h"`,
correctly: that one is test-local rather than installed.
14. **Empty parameter lists.** `akgl_heap_init()`, `akgl_heap_init_actor()`, 14. **Empty parameter lists.** **Resolved.** `akgl_game_init`,
`akgl_registry_init*()`, `akgl_game_init()`, `akgl_game_init_screen()`, and `akgl_game_update_fps`, `akgl_heap_init`, `akgl_heap_init_actor` and the
`akgl_game_updateFPS()` declare `()` rather than `(void)`, while eight `akgl_registry_init*` functions declare and define `(void)`. Before
`akgl_controller_list_keyboards(void)`, `akgl_controller_open_gamepads(void)`, C23 `()` means "unspecified arguments" and suppresses argument checking, so
`akgl_game_lowfps(void)`, and `akgl_game_state_lock(void)` use `(void)`. these were the entry points a caller could pass anything to.
Before C23 the two are not equivalent — `()` suppresses argument checking.
`akgl_heap_init_actor` is even declared `()` in `heap.h:57` and defined
`(void)` in `heap.c:48`.
15. **`AKERR_NOIGNORE` is applied inconsistently at definition sites.** Headers 15. **`AKERR_NOIGNORE` was applied inconsistently at definition sites.**
use it uniformly (except `akgl_sprite_sheet_coords_for_frame`, `sprite.h:86`, **Resolved.** It is on the declarations, where it does its work, and on no
which omits it). Definitions are split even within one file: `registry.c:55,120,163,172` definition in `src/`.
repeat it, `registry.c:27,44,63,71,79,96,104,112` do not. Since the attribute
is already on the declaration, drop it from all definitions.
### 3. Error-handling pattern ### 3. Error-handling pattern
@@ -359,13 +369,14 @@ with no test to notice.
NULL filepath reports `AKERR_NULLPOINTER`. NULL filepath reports `AKERR_NULLPOINTER`.
40. **`akgl_path_relative` and `akgl_path_relative_from` disagree on the output 40. **`akgl_path_relative` and `akgl_path_relative_from` disagree on the output
parameter.** `akgl_path_relative` and `akgl_path_relative_root` take parameter.** **Moot in 0.5.0**: `akgl_path_relative_from` is deleted (see
`akgl_String *dst`; `akgl_path_relative_from` takes `akgl_String **dst` **Defects → Known and still open** item 4) and `akgl_path_relative_root` is
(`src/util.c:105`). The `**` form matches the rest of the library `static`, so `akgl_path_relative` is the only member of the family left and
(`akgl_get_json_string_value`, `akgl_get_property`, `akgl_heap_next_string`), there is nothing left to disagree with. It still takes `akgl_String *dst`
which allocate when `*dest` is NULL. Related: **Defects → Known and still rather than the `**` form the rest of the library uses, which stays as
open #4**, which covers the fact that `akgl_path_relative_from` never writes **item 41**'s question of naming and this one's question of shape -- but
`*dst` at all. with one function it is a decision to make when something needs it, not a
drift between siblings.
41. **`dst` vs `dest` for output parameters.** `akgl_string_copy` and the 41. **`dst` vs `dest` for output parameters.** `akgl_string_copy` and the
`akgl_path_relative*` family use `dst`; everything else uses `dest`. `akgl_path_relative*` family use `dst`; everything else uses `dest`.
@@ -659,9 +670,13 @@ Each was found by a test written to assert correct behavior.
`akgl_physics_init_arcade` and `akgl_render_init2d` silently ignore `akgl_physics_init_arcade` and `akgl_render_init2d` silently ignore
configuration. `akgl_game_init` does call it; a caller that does not use configuration. `akgl_game_init` does call it; a caller that does not use
`akgl_game_init` does not get it. `akgl_game_init` does not get it.
4. **`akgl_path_relative_from` is a stub that leaks.** `src/util.c:105` claims a 4. **`akgl_path_relative_from` is a stub that leaks.** **Deleted in 0.5.0.**
heap string, never writes `*dst`, and never releases it. 256 calls exhaust It claimed a heap string, never wrote `*dst`, and never released it, so 256
the string pool. calls exhausted the string pool. It was declared in no header, called from
nowhere, and duplicated what `akgl_path_relative` already does. Fixing an
unfinished function nobody can reach is worse than deleting it; this also
closes item 40, which was about the output-parameter shape it disagreed with
the rest of the family on.
5. **`akgl_compare_sdl_surfaces` memcmps without checking geometry.** 5. **`akgl_compare_sdl_surfaces` memcmps without checking geometry.**
`src/util.c:208` compares `s1->pitch * s1->h` bytes of `s2` without verifying `src/util.c:208` compares `s1->pitch * s1->h` bytes of `s2` without verifying
the surfaces share dimensions, pitch, or format. the surfaces share dimensions, pitch, or format.
@@ -682,10 +697,10 @@ Each was found by a test written to assert correct behavior.
decide whether to make them symmetric or document the split. decide whether to make them symmetric or document the split.
9. **`tests/util.c` defines `test_akgl_collide_point_rectangle_logic` but 9. **`tests/util.c` defines `test_akgl_collide_point_rectangle_logic` but
`main()` never calls it.** `main()` never calls it.**
10. **`controller.h` declares functions that do not exist.** It declares 10. **`controller.h` declares functions that do not exist.** **Fixed in 0.5.0**;
`akgl_controller_handle_button_down`, `_button_up`, `_added`, and `_removed`, the definitions carry the declared `akgl_controller_handle_*` names now. See
but `src/controller.c` defines them as `gamepad_handle_*`. Anything compiled internal-consistency item 7, and `scripts/check_api_surface.sh`, which is
against the header alone fails to link. what stops this class of drift coming back.
11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map 11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map
ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not
`controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`. `controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`.

View File

@@ -109,8 +109,6 @@ extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
/** @brief Children one actor can carry. A child moves with its parent rather than simulating. */ /** @brief Children one actor can carry. A child moves with its parent rather than simulating. */
#define AKGL_ACTOR_MAX_CHILDREN 8 #define AKGL_ACTOR_MAX_CHILDREN 8
/** @brief Actors in the pool. Override before including heap.h to change it; see heap.h. */
#define AKGL_MAX_HEAP_ACTOR 64
/** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */ /** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */
typedef struct akgl_Actor { typedef struct akgl_Actor {

View File

@@ -20,7 +20,6 @@
#include <akgl/sprite.h> #include <akgl/sprite.h>
#define AKGL_CHARACTER_MAX_NAME_LENGTH 128 #define AKGL_CHARACTER_MAX_NAME_LENGTH 128
#define AKGL_MAX_HEAP_CHARACTER 256
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */ /** @brief Defines reusable movement parameters and actor-state sprite bindings. */
typedef struct akgl_Character { typedef struct akgl_Character {
@@ -62,7 +61,7 @@ typedef struct akgl_Character {
* #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init * #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init
* has not run and the registry id is still 0. * has not run and the registry id is still 0.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name); akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *obj, char *name);
/** /**
* @brief Bind a sprite to one exact combination of actor-state bits. * @brief Bind a sprite to one exact combination of actor-state bits.
* *

View File

@@ -138,49 +138,77 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event); akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
/** /**
* @brief Declared but not defined under this name. Do not call. * @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
* *
* The implementation exists as `gamepad_handle_button_down` in `src/controller.c` * A whole-application shortcut rather than a control-map binding: it looks the
* and is `static`-in-spirit -- it is not declared anywhere -- so a caller that * actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
* uses this declaration compiles and then fails to link. TODO.md, "Known and * being told which actor to act on, so it only ever drives one. The
* still open" item 10. * akgl_actor_cmhf_* handlers are the general form.
* *
* @param appstate Application state supplied by SDL. * Facing follows movement unless the actor sets `movement_controls_face`, which
* @param event SDL input event to process. * is how an actor that aims independently of the direction it walks opts out.
* @return Nothing; it cannot be called. *
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
* never read.
* @param event The button or key event. Required. Both the gamepad and
* keyboard unions are read on every call, so the arm that does
* not correspond to the event type is read as garbage -- which
* works only because no real button and keycode pair collides.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event); akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
/** /**
* @brief Declared but not defined under this name. Do not call. * @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
* *
* See akgl_controller_handle_button_down. The implementation is * The counterpart to akgl_controller_handle_button_down. It clears only the movement
* `gamepad_handle_button_up`. * bit, leaving the facing bits alone so the actor keeps looking the way it was
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
* first frame of the walk cycle rather than wherever it stopped.
* *
* @param appstate Application state supplied by SDL. * @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event SDL input event to process. * @param event The button or key release event. Required.
* @return Nothing; it cannot be called. * @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event); akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
/** /**
* @brief Declared but not defined under this name. Do not call. * @brief Open a gamepad that has just been plugged in, and log its mapping.
* *
* See akgl_controller_handle_button_down. The implementation is * SDL delivers no button events from an unopened gamepad, so a device that
* `gamepad_handle_added`, which opens a newly plugged-in gamepad. * appears mid-session has to be opened here the way akgl_controller_open_gamepads
* opens the ones present at startup. A gamepad SDL has already opened is logged
* and left alone.
* *
* @param appstate Application state supplied by SDL. * The mapping is logged because a controller with no entry in the database
* @param event SDL input event to process. * produces no button events at all, and that is otherwise indistinguishable
* @return Nothing; it cannot be called. * from a broken binding.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
* is read out of the `gbutton` arm rather than `gdevice`, which
* works because the two share their `which` field.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
* success and the device stays silent.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event); akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
/** /**
* @brief Declared but not defined under this name. Do not call. * @brief Close a gamepad that has been unplugged.
* *
* See akgl_controller_handle_button_down. The implementation is * An unplugged device SDL still has open is a leaked handle, so this closes it.
* `gamepad_handle_removed`, which closes an unplugged gamepad. * A removal for a device that was never opened is logged and otherwise ignored.
* Any control map still holding that `jsid` simply stops matching -- the map is
* not torn down.
* *
* @param appstate Application state supplied by SDL. * @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event SDL input event to process. * @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
* @return Nothing; it cannot be called. * @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event); akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);

View File

@@ -177,17 +177,7 @@ extern SDL_FRect *akgl_camera;
* @note It takes the state lock on the way in and releases it on the way out, so * @note It takes the state lock on the way in and releases it on the way out, so
* a failure part-way through leaves the mutex held. * a failure part-way through leaves the mutex held.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init(); akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init(void);
/**
* @brief Declared but never defined. Do not call.
*
* There is no definition anywhere in the library, so a translation unit that
* calls this compiles and then fails to link. Screen setup is akgl_render_2d_init.
* Tracked in TODO.md under header/implementation surface drift.
*
* @return Nothing; it cannot be called.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
/** /**
* @brief Count this frame, and recompute the frame rate once a second has passed. * @brief Count this frame, and recompute the frame rate once a second has passed.
* *
@@ -198,7 +188,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
* the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first * the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first
* second is up. * second is up.
*/ */
void akgl_game_update_fps(); void akgl_game_update_fps(void);
/** /**
* @brief Write the game state and the name-to-pointer tables to a save file. * @brief Write the game state and the name-to-pointer tables to a save file.
* *
@@ -311,4 +301,51 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags); akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
/*
* 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 Write all four name-to-address tables, each with its terminator.
*
* Actors, sprites, spritesheets, characters, in that order -- which is the order
* akgl_game_load reads them back in. Each table is the registry enumerated one
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
* reader stops on.
*
* @param fp The open save-game stream. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
* @throws AKERR_IO On a stream error or a short write while emitting a
* terminator.
*
* @note Only the terminators are written through the error-reporting path. The
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
* cannot report failure upward and instead terminate the process -- so a
* write error mid-table never reaches this function's return value.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp);
/**
* @brief Refuse a save file unless its version matches the running one exactly.
*
* Both strings are parsed as semver and compared with `"="` -- exact equality,
* not compatibility. That is deliberate and strict: a save file is a raw memory
* image of `akgl_Game` and the object tables, so any change to a struct layout
* invalidates it, and semver has no way to say "the layout did not move".
*
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
* only to build the message, but required, since a bare
* "incompatible version" error would not say which.
* @param newversion The version read out of the save file. Required.
* @param curversion The version of the running program or library. Required.
* @return `NULL` when the two match, otherwise an error context owned by the
* caller.
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
* @throws AKERR_VALUE If either string is not valid semver. The message says
* which side it came from.
* @throws AKERR_API If both parse but are not equal. The message quotes both.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion);
#endif //_AKGL_GAME_H_ #endif //_AKGL_GAME_H_

View File

@@ -74,7 +74,7 @@ extern akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING];
* -- but check it anyway; the signature exists so a layer that needs * -- but check it anyway; the signature exists so a layer that needs
* real setup has somewhere to report from. * real setup has somewhere to report from.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init(); akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init(void);
/** /**
* @brief Zero the actor pool only. * @brief Zero the actor pool only.
* *
@@ -85,7 +85,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
* *
* @return `NULL`. No failure path today; see akgl_heap_init. * @return `NULL`. No failure path today; see akgl_heap_init.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor(); akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor(void);
/** /**
* @brief Claim a free actor slot. * @brief Claim a free actor slot.
* @param dest Receives a pointer to the free slot. Required, and **not** * @param dest Receives a pointer to the free slot. Required, and **not**

View File

@@ -64,7 +64,7 @@ extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
* @warning This does not create #AKGL_REGISTRY_PROPERTIES -- see the warning on * @warning This does not create #AKGL_REGISTRY_PROPERTIES -- see the warning on
* this file. * this file.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init(void);
/** /**
* @brief Create the music registry. * @brief Create the music registry.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
@@ -72,7 +72,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
* @note Overwrites the existing id without destroying the old set, so calling it * @note Overwrites the existing id without destroying the old set, so calling it
* twice leaks the first one along with everything registered in it. * twice leaks the first one along with everything registered in it.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music(void);
/** /**
* @brief Create the font registry. * @brief Create the font registry.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
@@ -80,7 +80,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
* @note Overwrites the existing id without destroying the old set. See * @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music. * akgl_registry_init_music.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font(void);
/** /**
* @brief Create the actor registry, destroying any previous one first. * @brief Create the actor registry, destroying any previous one first.
* *
@@ -94,7 +94,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The old one * @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The old one
* has already been destroyed at that point, so the registry is left at 0. * has already been destroyed at that point, so the registry is left at 0.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor(void);
/** /**
* @brief Create the sprite registry. * @brief Create the sprite registry.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
@@ -102,7 +102,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
* @note Overwrites the existing id without destroying the old set. See * @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music. * akgl_registry_init_music.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite(void);
/** /**
* @brief Create the spritesheet registry. * @brief Create the spritesheet registry.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
@@ -110,7 +110,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
* @note Overwrites the existing id without destroying the old set. See * @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music. * akgl_registry_init_music.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet(void);
/** /**
* @brief Create the character registry. * @brief Create the character registry.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
@@ -118,7 +118,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
* @note Overwrites the existing id without destroying the old set. See * @note Overwrites the existing id without destroying the old set. See
* akgl_registry_init_music. * akgl_registry_init_music.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character(void);
/** /**
* @brief Create the configuration-property registry. * @brief Create the configuration-property registry.
* *
@@ -133,7 +133,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
* @note Overwrites the existing id without destroying the old set, so every * @note Overwrites the existing id without destroying the old set, so every
* property set before a second call is lost. * property set before a second call is lost.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties(void);
/** /**
* @brief Load a JSON configuration file into the property registry. * @brief Load a JSON configuration file into the property registry.
* *
@@ -172,7 +172,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
* and `MOVING_OUT`, so those two states cannot be named from JSON at all. * and `MOVING_OUT`, so those two states cannot be named from JSON at all.
* TODO.md items 24-26. * TODO.md items 24-26.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings(); akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings(void);
/** /**
* @brief Set one configuration property. * @brief Set one configuration property.
* *

View File

@@ -28,8 +28,6 @@
#define AKGL_SPRITE_MAX_REGISTRY_SIZE 1024 #define AKGL_SPRITE_MAX_REGISTRY_SIZE 1024
#define AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH 512 #define AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH 512
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
/** @brief Stores a loaded spritesheet texture and frame geometry. */ /** @brief Stores a loaded spritesheet texture and frame geometry. */
typedef struct { typedef struct {

View File

@@ -206,19 +206,18 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *
* *
* Drawing goes through the global `akgl_renderer`, not through a backend passed in. * Drawing goes through the global `akgl_renderer`, not through a backend passed in.
* *
* @param dest The map to draw from. Required. Not an output parameter * @param map The map to draw from. Required.
* despite the name.
* @param viewport The rectangle of the map, in map pixels, that is on screen. * @param viewport The rectangle of the map, in map pixels, that is on screen.
* Required. Usually the global `akgl_camera`. * Required. Usually the global `akgl_camera`.
* @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not** * @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not**
* bounds-checked: an index past the end reads a neighbouring * bounds-checked: an index past the end reads a neighbouring
* layer, or past the array. * layer, or past the array.
* @return `NULL` on success, otherwise an error context owned by the caller. * @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_NULLPOINTER If @p map or @p viewport is `NULL`.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first * @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first
* failure abandons the rest of the layer. * failure abandons the rest of the layer.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx); 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. * @brief Draw a whole tileset to the screen, tile by tile, from its offset table.
* *
@@ -227,11 +226,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRe
* matches the source image the offset table is right -- and if tiles are shifted * matches the source image the offset table is right -- and if tiles are shifted
* or duplicated, it is not. The `charviewer` utility uses it. * or duplicated, it is not. The `charviewer` utility uses it.
* *
* @param dest The map holding the tileset. Required. Not an output * @param map The map holding the tileset. Required.
* parameter despite the name.
* @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`. * @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`. * @throws AKERR_NULLPOINTER If @p map is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A * @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A
* negative index is not rejected. * negative index is not rejected.
* @throws AKERR_* Whatever the renderer's `draw_texture` raises. * @throws AKERR_* Whatever the renderer's `draw_texture` raises.
@@ -240,7 +238,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRe
* tileset's, so a tileset whose tiles are a different size from the map's * tileset's, so a tileset whose tiles are a different size from the map's
* is reconstructed at the wrong pitch. * is reconstructed at the wrong pitch.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx); 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. * These functions are part of the internal API and should not be called by the user.
@@ -490,4 +488,152 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor); 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, float *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, float *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, double *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.
*
* @note The joined path is truncated at
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE without complaint, so an
* over-long path fails as a missing file rather than as a length error.
*/
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_ #endif //_AKGL_TILEMAP_H_

86
scripts/check_api_surface.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
#
# Every function with external linkage must be declared in a header.
#
# A non-static function that appears in no header is still in the ABI but is
# unreachable by callers, and it collides with any consumer that happens to pick
# the same name. libakgl shipped nineteen of those. The rule is in AGENTS.md
# under "API Surface"; this is what enforces it.
#
# Usage: check_api_surface.sh <libakgl.so> <header-dir> [<header-dir> ...]
#
# Exits 0 when every exported akgl_* symbol is declared, 1 otherwise, and 2 when
# it cannot do its job -- a missing library or no nm.
set -u
if [ "$#" -lt 2 ]; then
echo "usage: $0 <libakgl.so> <header-dir> [<header-dir> ...]" >&2
exit 2
fi
LIBRARY="$1"
shift
if ! command -v nm >/dev/null 2>&1; then
echo "check_api_surface: nm is not available; skipping" >&2
exit 2
fi
if [ ! -f "${LIBRARY}" ]; then
echo "check_api_surface: no such library: ${LIBRARY}" >&2
exit 2
fi
DECLARATIONS="$(mktemp)"
trap 'rm -f "${DECLARATIONS}"' EXIT
# Comments are stripped first. Several of these names appear in prose -- the
# whole point of the check is that being *mentioned* in a header is not the same
# as being declared in one, and that distinction is exactly what went wrong with
# the gamepad_handle_* handlers.
for dir in "$@"; do
[ -d "${dir}" ] || continue
find "${dir}" -name '*.h' -print0 |
xargs -0 --no-run-if-empty cat |
sed -e 's://.*::' |
awk 'BEGIN { incomment = 0 }
{
line = $0
while ( 1 ) {
if ( incomment ) {
idx = index(line, "*/")
if ( idx == 0 ) { line = ""; break }
line = substr(line, idx + 2); incomment = 0
} else {
idx = index(line, "/*")
if ( idx == 0 ) { break }
printf "%s", substr(line, 1, idx - 1)
line = substr(line, idx + 2); incomment = 1
}
}
print line
}' >> "${DECLARATIONS}"
done
UNDECLARED=""
for symbol in $(nm -D --defined-only "${LIBRARY}" | awk '$2 == "T" { print $3 }' | grep '^akgl_' | sort); do
if ! grep -qE "(^|[^A-Za-z0-9_])${symbol}[^A-Za-z0-9_]" "${DECLARATIONS}"; then
UNDECLARED="${UNDECLARED} ${symbol}"
fi
done
if [ -n "${UNDECLARED}" ]; then
echo "check_api_surface: exported but declared in no header:" >&2
for symbol in ${UNDECLARED}; do
echo " ${symbol}" >&2
done
echo "" >&2
echo "Declare each one in the header for its subsystem, or make it static." >&2
echo "A function exposed only so tests can reach it goes under the existing" >&2
echo "\"part of the internal API\" comment block." >&2
exit 1
fi
echo "check_api_surface: every exported akgl_* symbol is declared"
exit 0

View File

@@ -328,7 +328,7 @@ void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, cons
} FINISH_NORETURN(errctx); } FINISH_NORETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
@@ -342,7 +342,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_left_on(akgl_Actor *obj, SDL_
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -357,7 +357,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_left_off(akgl_Actor *obj, SDL
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -371,7 +371,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_right_on(akgl_Actor *obj, SDL
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -386,7 +386,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_right_off(akgl_Actor *obj, SD
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -400,7 +400,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_up_on(akgl_Actor *obj, SDL_Ev
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -415,7 +415,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_up_off(akgl_Actor *obj, SDL_E
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
@@ -429,7 +429,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_down_on(akgl_Actor *obj, SDL_
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event) akerr_ErrorContext *akgl_actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");

View File

@@ -283,7 +283,7 @@ static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *strea
} }
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void) akerr_ErrorContext *akgl_audio_init(void)
{ {
SDL_AudioSpec spec; SDL_AudioSpec spec;
@@ -322,7 +322,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void) akerr_ErrorContext *akgl_audio_shutdown(void)
{ {
SDL_AudioStream *closing = audiostream; SDL_AudioStream *closing = audiostream;
@@ -337,7 +337,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms) akerr_ErrorContext *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice)); PASS(errctx, check_voice(voice));
@@ -350,7 +350,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms) akerr_ErrorContext *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice)); PASS(errctx, check_voice(voice));
@@ -368,7 +368,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice) akerr_ErrorContext *akgl_audio_stop(int voice)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice)); PASS(errctx, check_voice(voice));
@@ -382,7 +382,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform) akerr_ErrorContext *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice)); PASS(errctx, check_voice(voice));
@@ -401,7 +401,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWave
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release) akerr_ErrorContext *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice)); PASS(errctx, check_voice(voice));
@@ -422,7 +422,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attac
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level) akerr_ErrorContext *akgl_audio_volume(float32_t level)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN( FAIL_NONZERO_RETURN(
@@ -439,7 +439,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active) akerr_ErrorContext *akgl_audio_voice_active(int voice, bool *active)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
PASS(errctx, check_voice(voice)); PASS(errctx, check_voice(voice));
@@ -452,7 +452,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *acti
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames) akerr_ErrorContext *akgl_audio_mix(float32_t *dest, int frames)
{ {
akgl_AudioVoice *voice = NULL; akgl_AudioVoice *voice = NULL;
float32_t sum = 0.0f; float32_t sum = 0.0f;

View File

@@ -72,7 +72,7 @@ akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int stat
// SDL iterator so we can't return error information here, void only // SDL iterator so we can't return error information here, void only
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here // this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID registry, const char *name) void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akgl_Sprite *spriteptr; akgl_Sprite *spriteptr;
@@ -80,7 +80,7 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
ATTEMPT { ATTEMPT {
FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer"); FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer");
FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name"); FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name");
spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(registry, name, NULL); spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(props, name, NULL);
FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name); FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name);
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) { if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) {
CATCH(errctx, akgl_heap_release_sprite(spriteptr)); CATCH(errctx, akgl_heap_release_sprite(spriteptr));
@@ -114,7 +114,7 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end. * @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted. * @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/ */
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest) static akerr_ErrorContext *character_load_json_state_int_from_strings(json_t *states, int *dest)
{ {
int i = 0; int i = 0;
long newstate = 0; long newstate = 0;
@@ -161,7 +161,7 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
* @throws AKERR_OUTOFBOUNDS If an array is indexed past its end. * @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted. * @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/ */
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj) static akerr_ErrorContext *character_load_json_inner(json_t *json, akgl_Character *obj)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *mappings = NULL; json_t *mappings = NULL;
@@ -189,7 +189,7 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));
CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray)); CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
CATCH(errctx, akgl_character_load_json_state_int_from_strings(statearray, &stateval)); CATCH(errctx, character_load_json_state_int_from_strings(statearray, &stateval));
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr)); CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
@@ -236,7 +236,7 @@ akerr_ErrorContext *akgl_character_load_json(char *filename)
AKERR_NULLPOINTER, AKERR_NULLPOINTER,
"Error while loading character from %s on line %d: %s", filename, error.line, error.text "Error while loading character from %s on line %d: %s", filename, error.line, error.text
); );
CATCH(errctx, akgl_character_load_json_inner(json, obj)); CATCH(errctx, character_load_json_inner(json, obj));
CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime)); CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime));
obj->speedtime = obj->speedtime * AKGL_TIME_ONEMS_NS; obj->speedtime = obj->speedtime * AKGL_TIME_ONEMS_NS;
CATCH(errctx, akgl_get_json_number_value(json, "speed_x", &obj->sx)); CATCH(errctx, akgl_get_json_number_value(json, "speed_x", &obj->sx));

View File

@@ -182,7 +182,7 @@ static bool keybuffer_take(akgl_Keystroke *dest)
return true; return true;
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void) akerr_ErrorContext *akgl_controller_list_keyboards(void)
{ {
int count; int count;
SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count); SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count);
@@ -206,7 +206,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void) akerr_ErrorContext *akgl_controller_open_gamepads(void)
{ {
int count = 0; int count = 0;
int i = 0; int i = 0;
@@ -302,32 +302,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/** akerr_ErrorContext *akgl_controller_handle_button_down(void *appstate, SDL_Event *event)
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
*
* A whole-application shortcut rather than a control-map binding: it looks the
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
* being told which actor to act on, so it only ever drives one. The
* akgl_actor_cmhf_* handlers are the general form.
*
* Facing follows movement unless the actor sets `movement_controls_face`, which
* is how an actor that aims independently of the direction it walks opts out.
*
* Declared nowhere -- `controller.h` advertises this as
* `akgl_controller_handle_button_down`, which does not exist. TODO.md, "Known
* and still open" item 10.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
* never read.
* @param event The button or key event. Required. Both the gamepad and
* keyboard unions are read on every call, so the arm that does
* not correspond to the event type is read as garbage -- which
* works only because no real button and keycode pair collides.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
{ {
akgl_Actor *player = NULL; akgl_Actor *player = NULL;
@@ -378,23 +353,7 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/** akerr_ErrorContext *akgl_controller_handle_button_up(void *appstate, SDL_Event *event)
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
*
* The counterpart to gamepad_handle_button_down. It clears only the movement
* bit, leaving the facing bits alone so the actor keeps looking the way it was
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
* first frame of the walk cycle rather than wherever it stopped.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The button or key release event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
{ {
akgl_Actor *player = NULL; akgl_Actor *player = NULL;
@@ -433,31 +392,7 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/** akerr_ErrorContext *akgl_controller_handle_added(void *appstate, SDL_Event *event)
* @brief Open a gamepad that has just been plugged in, and log its mapping.
*
* SDL delivers no button events from an unopened gamepad, so a device that
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
* opens the ones present at startup. A gamepad SDL has already opened is logged
* and left alone.
*
* The mapping is logged because a controller with no entry in the database
* produces no button events at all, and that is otherwise indistinguishable
* from a broken binding.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
* is read out of the `gbutton` arm rather than `gdevice`, which
* works because the two share their `which` field.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
* success and the device stays silent.
*/
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
{ {
SDL_JoystickID which; SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL; SDL_Gamepad *gamepad = NULL;
@@ -487,22 +422,7 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/** akerr_ErrorContext *akgl_controller_handle_removed(void *appstate, SDL_Event *event)
* @brief Close a gamepad that has been unplugged.
*
* An unplugged device SDL still has open is a leaked handle, so this closes it.
* A removal for a device that was never opened is logged and otherwise ignored.
* Any control map still holding that `jsid` simply stops matching -- the map is
* not torn down.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*/
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
{ {
SDL_JoystickID which; SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL; SDL_Gamepad *gamepad = NULL;
@@ -521,7 +441,7 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control) akerr_ErrorContext *akgl_controller_pushmap(int controlmapid, akgl_Control *control)
{ {
int newmapid = 0; int newmapid = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -538,7 +458,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akg
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid) akerr_ErrorContext *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
{ {
akgl_ControlMap *controlmap; akgl_ControlMap *controlmap;
akgl_Control control; akgl_Control control;
@@ -630,7 +550,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
} FINISH(errctx, true); } FINISH(errctx, true);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available) akerr_ErrorContext *akgl_controller_poll_key(int *keycode, bool *available)
{ {
akgl_Keystroke keystroke; akgl_Keystroke keystroke;
@@ -655,7 +575,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available) akerr_ErrorContext *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination");
@@ -666,7 +586,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void) akerr_ErrorContext *akgl_controller_flush_keys(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
keybuffer_head = 0; keybuffer_head = 0;

View File

@@ -236,7 +236,7 @@ static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color) akerr_ErrorContext *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
{ {
SDL_Color previous; SDL_Color previous;
@@ -258,7 +258,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, flo
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color) akerr_ErrorContext *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
{ {
SDL_Color previous; SDL_Color previous;
@@ -280,7 +280,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, floa
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color) akerr_ErrorContext *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{ {
SDL_Color previous; SDL_Color previous;
@@ -303,7 +303,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color) akerr_ErrorContext *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
{ {
SDL_Color previous; SDL_Color previous;
@@ -326,7 +326,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *sel
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color) akerr_ErrorContext *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
{ {
SDL_Color previous; SDL_Color previous;
SDL_FPoint octants[8]; SDL_FPoint octants[8];
@@ -393,7 +393,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, fl
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color) akerr_ErrorContext *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
{ {
SDL_Surface *target = NULL; SDL_Surface *target = NULL;
SDL_Surface *rgba = NULL; SDL_Surface *rgba = NULL;
@@ -481,7 +481,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest) akerr_ErrorContext *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
{ {
SDL_Surface *saved = NULL; SDL_Surface *saved = NULL;
int width = 0; int width = 0;
@@ -551,7 +551,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *sel
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y) akerr_ErrorContext *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
{ {
SDL_Texture *patch = NULL; SDL_Texture *patch = NULL;
SDL_FRect dest; SDL_FRect dest;

View File

@@ -129,7 +129,7 @@ void akgl_game_lowfps(void)
return; return;
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init() akerr_ErrorContext *akgl_game_init(void)
{ {
int screenwidth = 0; int screenwidth = 0;
int screenheight = 0; int screenheight = 0;
@@ -213,7 +213,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void) akerr_ErrorContext *akgl_game_state_lock(void)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
// Milliseconds, which is what SDL_Delay takes. This used to count against // Milliseconds, which is what SDL_Delay takes. This used to count against
@@ -240,14 +240,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void)
SDL_GetError()); SDL_GetError());
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void) akerr_ErrorContext *akgl_game_state_unlock(void)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
SDL_UnlockMutex(akgl_game.statelock); SDL_UnlockMutex(akgl_game.statelock);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
void akgl_game_update_fps() void akgl_game_update_fps(void)
{ {
SDL_Time curTime; SDL_Time curTime;
curTime = SDL_GetTicksNS(); curTime = SDL_GetTicksNS();
@@ -287,7 +287,7 @@ void akgl_game_update_fps()
* **exits the process**. A failed write during a save terminates the * **exits the process**. A failed write during a save terminates the
* game rather than returning an error. * game rather than returning an error.
*/ */
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name) static void save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{ {
FILE *fp = (FILE *)userdata; FILE *fp = (FILE *)userdata;
akgl_Actor *actor = NULL; akgl_Actor *actor = NULL;
@@ -305,7 +305,7 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
/** /**
* @brief Write one sprite's name and save-time address to the name table. * @brief Write one sprite's name and save-time address to the name table.
* *
* As akgl_game_save_actorname_iterator, but the name field is * As save_actorname_iterator, but the name field is
* #AKGL_SPRITE_MAX_NAME_LENGTH wide. * #AKGL_SPRITE_MAX_NAME_LENGTH wide.
* *
* @param userdata The open output stream, as a `FILE *`. Required. * @param userdata The open output stream, as a `FILE *`. Required.
@@ -313,9 +313,9 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
* @param name The sprite's registry key, written at fixed width. * @param name The sprite's registry key, written at fixed width.
* *
* @warning Terminates the process on an unhandled error. See * @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator. * save_actorname_iterator.
*/ */
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name) static void save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{ {
FILE *fp = (FILE *)userdata; FILE *fp = (FILE *)userdata;
akgl_Sprite *sprite = NULL; akgl_Sprite *sprite = NULL;
@@ -333,7 +333,7 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
/** /**
* @brief Write one spritesheet's name and save-time address to the name table. * @brief Write one spritesheet's name and save-time address to the name table.
* *
* As akgl_game_save_actorname_iterator, but the name field is * As save_actorname_iterator, but the name field is
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since * #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since
* a spritesheet is keyed by its resolved path. * a spritesheet is keyed by its resolved path.
* *
@@ -342,12 +342,12 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
* @param name The spritesheet's registry key, written at fixed width. * @param name The spritesheet's registry key, written at fixed width.
* *
* @warning Terminates the process on an unhandled error. See * @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator. * save_actorname_iterator.
* @note This is the table the loader disagrees with: it reads every table at * @note This is the table the loader disagrees with: it reads every table at
* #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot * #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot
* be read back. TODO.md, "Known and still open" item 7. * be read back. TODO.md, "Known and still open" item 7.
*/ */
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name) static void save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{ {
FILE *fp = (FILE *)userdata; FILE *fp = (FILE *)userdata;
akgl_SpriteSheet *spritesheet = NULL; akgl_SpriteSheet *spritesheet = NULL;
@@ -365,7 +365,7 @@ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID pr
/** /**
* @brief Write one character's name and save-time address to the name table. * @brief Write one character's name and save-time address to the name table.
* *
* As akgl_game_save_actorname_iterator, but the name field is * As save_actorname_iterator, but the name field is
* #AKGL_CHARACTER_MAX_NAME_LENGTH wide. * #AKGL_CHARACTER_MAX_NAME_LENGTH wide.
* *
* @param userdata The open output stream, as a `FILE *`. Required. * @param userdata The open output stream, as a `FILE *`. Required.
@@ -373,9 +373,9 @@ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID pr
* @param name The character's registry key, written at fixed width. * @param name The character's registry key, written at fixed width.
* *
* @warning Terminates the process on an unhandled error. See * @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator. * save_actorname_iterator.
*/ */
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name) static void save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{ {
FILE *fp = (FILE *)userdata; FILE *fp = (FILE *)userdata;
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -390,30 +390,11 @@ void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID prop
} FINISH_NORETURN(e); } FINISH_NORETURN(e);
} }
/** akerr_ErrorContext *akgl_game_save_actors(FILE *fp)
* @brief Write all four name-to-address tables, each with its terminator.
*
* Actors, sprites, spritesheets, characters, in that order -- which is the order
* akgl_game_load reads them back in. Each table is the registry enumerated one
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
* reader stops on.
*
* @param fp The open save-game stream. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
* @throws AKERR_IO On a stream error or a short write while emitting a
* terminator.
*
* @note Only the terminators are written through the error-reporting path. The
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
* cannot report failure upward and instead terminate the process -- so a
* write error mid-table never reaches this function's return value.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
// Each name table ends with a zeroed name field and a zeroed pointer, which // Each name table ends with a zeroed name field and a zeroed pointer, which
// is what akgl_game_load_objectnamemap() looks for to stop reading. The // is what load_objectnamemap() looks for to stop reading. The
// terminator has to come from a buffer at least as long as the longest name // terminator has to come from a buffer at least as long as the longest name
// field: writing N bytes from the address of a single char would emit N-1 // field: writing N bytes from the address of a single char would emit N-1
// bytes of whatever happened to follow it on the stack, which both leaks // bytes of whatever happened to follow it on the stack, which both leaks
@@ -428,28 +409,28 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
// write the actor name pointer table // write the actor name pointer table
SDL_EnumerateProperties( SDL_EnumerateProperties(
AKGL_REGISTRY_ACTOR, AKGL_REGISTRY_ACTOR,
&akgl_game_save_actorname_iterator, &save_actorname_iterator,
(void *)fp); (void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp)); CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp)); CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
// write the sprite name pointer table // write the sprite name pointer table
SDL_EnumerateProperties( SDL_EnumerateProperties(
AKGL_REGISTRY_SPRITE, AKGL_REGISTRY_SPRITE,
&akgl_game_save_spritename_iterator, &save_spritename_iterator,
(void *)fp); (void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp)); CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp)); CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
// write the spritesheet name pointer table // write the spritesheet name pointer table
SDL_EnumerateProperties( SDL_EnumerateProperties(
AKGL_REGISTRY_SPRITESHEET, AKGL_REGISTRY_SPRITESHEET,
&akgl_game_save_spritesheetname_iterator, &save_spritesheetname_iterator,
(void *)fp); (void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp)); CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp)); CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
// write the character name pointer table // write the character name pointer table
SDL_EnumerateProperties( SDL_EnumerateProperties(
AKGL_REGISTRY_CHARACTER, AKGL_REGISTRY_CHARACTER,
&akgl_game_save_charactername_iterator, &save_charactername_iterator,
(void *)fp); (void *)fp);
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_CHARACTER_MAX_NAME_LENGTH, fp)); CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_CHARACTER_MAX_NAME_LENGTH, fp));
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp)); CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
@@ -459,7 +440,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath) akerr_ErrorContext *akgl_game_save(char *fpath)
{ {
FILE *fp = NULL; FILE *fp = NULL;
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -517,7 +498,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
* rather than being reported: the entry is written with whatever * rather than being reported: the entry is written with whatever
* `SDL_GetPointerProperty` returned. * `SDL_GetPointerProperty` returned.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry) static akerr_ErrorContext *load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
{ {
void *ptr = NULL; void *ptr = NULL;
char ptrstring[32]; char ptrstring[32];
@@ -561,27 +542,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
/** akerr_ErrorContext *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
* @brief Refuse a save file unless its version matches the running one exactly.
*
* Both strings are parsed as semver and compared with `"="` -- exact equality,
* not compatibility. That is deliberate and strict: a save file is a raw memory
* image of `akgl_Game` and the object tables, so any change to a struct layout
* invalidates it, and semver has no way to say "the layout did not move".
*
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
* only to build the message, but required, since a bare
* "incompatible version" error would not say which.
* @param newversion The version read out of the save file. Required.
* @param curversion The version of the running program or library. Required.
* @return `NULL` when the two match, otherwise an error context owned by the
* caller.
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
* @throws AKERR_VALUE If either string is not valid semver. The message says
* which side it came from.
* @throws AKERR_API If both parse but are not equal. The message quotes both.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
{ {
semver_t current_version = {}; semver_t current_version = {};
semver_t compare_version = {}; semver_t compare_version = {};
@@ -622,7 +583,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype,
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath) akerr_ErrorContext *akgl_game_load(char *fpath)
{ {
akgl_Game savegame; akgl_Game savegame;
SDL_PropertiesID actormap; SDL_PropertiesID actormap;
@@ -653,16 +614,16 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
memcpy((void *)&akgl_game, (void *)&savegame, sizeof(akgl_Game)); memcpy((void *)&akgl_game, (void *)&savegame, sizeof(akgl_Game));
// Load actor name map // Load actor name map
actormap = SDL_CreateProperties(); actormap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR)); CATCH(e, load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
// Load sprite name map // Load sprite name map
spritemap = SDL_CreateProperties(); spritemap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE)); CATCH(e, load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
// Load spritesheet name map // Load spritesheet name map
spritesheetmap = SDL_CreateProperties(); spritesheetmap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET)); CATCH(e, load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
// Load character name map // Load character name map
charactermap = SDL_CreateProperties(); charactermap = SDL_CreateProperties();
CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER)); CATCH(e, load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
// Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers // Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers
} CLEANUP { } CLEANUP {
if ( fp != NULL ) { if ( fp != NULL ) {
@@ -674,7 +635,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags) akerr_ErrorContext *akgl_game_update(akgl_Iterator *opflags)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
akgl_Iterator defflags = { akgl_Iterator defflags = {

View File

@@ -20,7 +20,7 @@ akgl_SpriteSheet akgl_heap_spritesheets[AKGL_MAX_HEAP_SPRITESHEET];
akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER]; akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER];
akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING]; akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING];
akerr_ErrorContext *akgl_heap_init() akerr_ErrorContext *akgl_heap_init(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int i = 0; int i = 0;

View File

@@ -57,7 +57,7 @@ akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *de
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest) akerr_ErrorContext *akgl_get_json_double_value(json_t *obj, char *key, double *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
@@ -146,25 +146,28 @@ akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, a
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize) akerr_ErrorContext *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize)
{ {
PREPARE_ERROR(e); // Two contexts, and the names now say which is which: `e` is the incoming
if ( err == NULL ) { // one being inspected, `errctx` is this function's own. They were `err` and
SUCCEED_RETURN(e); // `e`, which put the incoming context under the name the convention
// reserves for exactly that and the local one under a name that reads like
// it.
PREPARE_ERROR(errctx);
if ( e == NULL ) {
SUCCEED_RETURN(errctx);
} }
int docopy = 0;
FAIL_ZERO_RETURN(e, err, AKERR_NULLPOINTER, "err"); FAIL_ZERO_RETURN(errctx, defval, AKERR_NULLPOINTER, "defval");
FAIL_ZERO_RETURN(e, defval, AKERR_NULLPOINTER, "defval"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT { ATTEMPT {
} CLEANUP { } CLEANUP {
} PROCESS(err) { } PROCESS(e) {
} HANDLE_GROUP(err, AKERR_KEY) { } HANDLE_GROUP(e, AKERR_KEY) {
} HANDLE_GROUP(err, AKERR_INDEX) { } HANDLE_GROUP(e, AKERR_INDEX) {
memcpy(dest, defval, defsize); memcpy(dest, defval, defsize);
} FINISH(err, true); } FINISH(e, true);
SUCCEED_RETURN(e); SUCCEED_RETURN(errctx);
} }

View File

@@ -12,28 +12,28 @@
#include <akgl/heap.h> #include <akgl/heap.h>
#include <akgl/registry.h> #include <akgl/registry.h>
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) akerr_ErrorContext *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2) akerr_ErrorContext *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) akerr_ErrorContext *akgl_physics_null_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self) akerr_ErrorContext *akgl_physics_init_null(akgl_PhysicsBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
@@ -47,7 +47,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *s
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) akerr_ErrorContext *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
@@ -69,7 +69,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBacke
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2) akerr_ErrorContext *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
@@ -77,7 +77,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBacke
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) akerr_ErrorContext *akgl_physics_arcade_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
@@ -88,7 +88,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsB
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self) akerr_ErrorContext *akgl_physics_init_arcade(akgl_PhysicsBackend *self)
{ {
akgl_String *tmp; akgl_String *tmp;
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -121,7 +121,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags) akerr_ErrorContext *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
akgl_Iterator defflags = { akgl_Iterator defflags = {
@@ -227,7 +227,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type) akerr_ErrorContext *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type)
{ {
uint32_t hashval; uint32_t hashval;
PREPARE_ERROR(e); PREPARE_ERROR(e);

View File

@@ -25,7 +25,7 @@ SDL_PropertiesID AKGL_REGISTRY_MUSIC = 0;
SDL_PropertiesID AKGL_REGISTRY_FONT = 0; SDL_PropertiesID AKGL_REGISTRY_FONT = 0;
SDL_PropertiesID AKGL_REGISTRY_PROPERTIES = 0; SDL_PropertiesID AKGL_REGISTRY_PROPERTIES = 0;
akerr_ErrorContext *akgl_registry_init() akerr_ErrorContext *akgl_registry_init(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
@@ -42,7 +42,7 @@ akerr_ErrorContext *akgl_registry_init()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_registry_init_actor() akerr_ErrorContext *akgl_registry_init_actor(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
if ( AKGL_REGISTRY_ACTOR != 0 ) { if ( AKGL_REGISTRY_ACTOR != 0 ) {
@@ -53,7 +53,7 @@ akerr_ErrorContext *akgl_registry_init_actor()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font() akerr_ErrorContext *akgl_registry_init_font(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_REGISTRY_FONT = SDL_CreateProperties(); AKGL_REGISTRY_FONT = SDL_CreateProperties();
@@ -61,7 +61,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_registry_init_music() akerr_ErrorContext *akgl_registry_init_music(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_REGISTRY_MUSIC = SDL_CreateProperties(); AKGL_REGISTRY_MUSIC = SDL_CreateProperties();
@@ -69,7 +69,7 @@ akerr_ErrorContext *akgl_registry_init_music()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_registry_init_properties() akerr_ErrorContext *akgl_registry_init_properties(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_REGISTRY_PROPERTIES = SDL_CreateProperties(); AKGL_REGISTRY_PROPERTIES = SDL_CreateProperties();
@@ -77,7 +77,7 @@ akerr_ErrorContext *akgl_registry_init_properties()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_registry_init_actor_state_strings() akerr_ErrorContext *akgl_registry_init_actor_state_strings(void)
{ {
int i = 0; int i = 0;
int flag = 0; int flag = 0;
@@ -94,7 +94,7 @@ akerr_ErrorContext *akgl_registry_init_actor_state_strings()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_registry_init_sprite() akerr_ErrorContext *akgl_registry_init_sprite(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_REGISTRY_SPRITE = SDL_CreateProperties(); AKGL_REGISTRY_SPRITE = SDL_CreateProperties();
@@ -102,7 +102,7 @@ akerr_ErrorContext *akgl_registry_init_sprite()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_registry_init_spritesheet() akerr_ErrorContext *akgl_registry_init_spritesheet(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_REGISTRY_SPRITESHEET = SDL_CreateProperties(); AKGL_REGISTRY_SPRITESHEET = SDL_CreateProperties();
@@ -110,7 +110,7 @@ akerr_ErrorContext *akgl_registry_init_spritesheet()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_registry_init_character() akerr_ErrorContext *akgl_registry_init_character(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
AKGL_REGISTRY_CHARACTER = SDL_CreateProperties(); AKGL_REGISTRY_CHARACTER = SDL_CreateProperties();
@@ -118,7 +118,7 @@ akerr_ErrorContext *akgl_registry_init_character()
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname) akerr_ErrorContext *akgl_registry_load_properties(char *fname)
{ {
json_t *json = NULL; json_t *json = NULL;
json_t *props = NULL; json_t *props = NULL;
@@ -175,16 +175,16 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *src) akerr_ErrorContext *akgl_set_property(char *name, char *value)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *"); FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "NULL char *"); FAIL_ZERO_RETURN(e, value, AKERR_NULLPOINTER, "NULL char *");
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, name, src); SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, name, value);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def) akerr_ErrorContext *akgl_get_property(char *name, akgl_String **dest, char *def)
{ {
const char *value = NULL; const char *value = NULL;
size_t valuelen = 0; size_t valuelen = 0;

View File

@@ -14,7 +14,7 @@
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h> #include <akstdlib.h>
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_init(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_init(akgl_RenderBackend *self)
{ {
akgl_String *width = NULL; akgl_String *width = NULL;
akgl_String *height = NULL; akgl_String *height = NULL;
@@ -47,7 +47,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_init(akgl_RenderBackend *self)
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_bind(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_bind(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
@@ -65,13 +65,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_bind(akgl_RenderBackend *self)
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_shutdown(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_frame_start(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
@@ -80,7 +80,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_frame_end(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
@@ -88,7 +88,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip) akerr_ErrorContext *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
@@ -113,13 +113,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBacken
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_draw_mesh(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_RETURN(e, AKERR_API, "Not implemented"); FAIL_RETURN(e, AKERR_API, "Not implemented");
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags) akerr_ErrorContext *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
akgl_Iterator defflags; akgl_Iterator defflags;

View File

@@ -67,7 +67,7 @@ akerr_ErrorContext *akgl_spritesheet_coords_for_frame(akgl_Sprite *self, SDL_FRe
* @throws AKGL_ERR_SDL If the image cannot be decoded. * @throws AKGL_ERR_SDL If the image cannot be decoded.
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted. * @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
*/ */
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path) static akerr_ErrorContext *sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
json_t *spritesheet_json = NULL; json_t *spritesheet_json = NULL;
@@ -145,7 +145,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text "Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
); );
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data))); CATCH(errctx, sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename)); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
CATCH(errctx, CATCH(errctx,
akgl_sprite_initialize( akgl_sprite_initialize(

View File

@@ -10,7 +10,7 @@
#include <akgl/registry.h> #include <akgl/registry.h>
#include <akgl/game.h> #include <akgl/game.h>
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size) akerr_ErrorContext *akgl_text_loadfont(char *name, char *filepath, int size)
{ {
TTF_Font *font = NULL; TTF_Font *font = NULL;
@@ -37,7 +37,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name) akerr_ErrorContext *akgl_text_unloadfont(char *name)
{ {
TTF_Font *font = NULL; TTF_Font *font = NULL;
@@ -54,7 +54,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y) akerr_ErrorContext *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
{ {
SDL_Surface *textsurf = NULL; SDL_Surface *textsurf = NULL;
SDL_Texture *texture = NULL; SDL_Texture *texture = NULL;
@@ -96,7 +96,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h) akerr_ErrorContext *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font"); FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
@@ -114,7 +114,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text,
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h) akerr_ErrorContext *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font"); FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");

View File

@@ -87,23 +87,6 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @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 *akgl_get_json_properties_number(json_t *obj, char *key, float *dest) akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -114,22 +97,6 @@ akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, floa
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @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 *akgl_get_json_properties_float(json_t *obj, char *key, float *dest) akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -140,24 +107,6 @@ akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @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 *akgl_get_json_properties_double(json_t *obj, char *key, double *dest) akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, double *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -288,41 +237,6 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @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 *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname) akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -444,34 +358,6 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @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.
*
* @note The joined path is truncated at
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE without complaint, so an
* over-long path fails as a missing file rather than as a length error.
*/
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname) akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -558,33 +444,6 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @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 *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root) akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -913,7 +772,7 @@ akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor) akerr_ErrorContext *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -930,7 +789,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, a
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest) akerr_ErrorContext *akgl_tilemap_release(akgl_Tilemap *dest)
{ {
// Release all tileset textures // Release all tileset textures
// Release all image layer textures // Release all image layer textures

View File

@@ -27,7 +27,8 @@
* the join, so symlinks and `..` are folded out and the result is absolute. It * the join, so symlinks and `..` are folded out and the result is absolute. It
* is the fallback, reached only once resolving @p path on its own has failed. * is the fallback, reached only once resolving @p path on its own has failed.
* *
* Not declared in util.h -- it is reachable only through akgl_path_relative. * `static`: it is reachable only through akgl_path_relative, which is the only
* caller and the only sensible one.
* *
* @param root Directory to resolve against, normally `dirname` of the file that * @param root Directory to resolve against, normally `dirname` of the file that
* named @p path. Required. A trailing `/` is harmless; the join adds * named @p path. Required. A trailing `/` is harmless; the join adds
@@ -50,7 +51,7 @@
* scratch strings are never released. This is exactly the hazard AGENTS.md * scratch strings are never released. This is exactly the hazard AGENTS.md
* warns about; it wants a `FAIL_BREAK`. * warns about; it wants a `FAIL_BREAK`.
*/ */
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst) static akerr_ErrorContext *path_relative_root(char *root, char *path, akgl_String *dst)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
akgl_String *pathbuf; akgl_String *pathbuf;
@@ -128,47 +129,11 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
} FINISH(e, true); } FINISH(e, true);
if ( relative_to_root == true ) { if ( relative_to_root == true ) {
PASS(e, akgl_path_relative_root(root, path, dst)); PASS(e, path_relative_root(root, path, dst));
} }
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
/**
* @brief Intended to resolve @p path against the directory holding @p from. Unfinished.
*
* The shape is there -- resolve @p from, take its `dirname` -- but the body
* stops before it does anything with @p path, and `*dst` is never written. Not
* declared in util.h, and nothing in the tree calls it.
*
* @param path The path to resolve. Required, and currently ignored past the
* `NULL` check.
* @param from A file whose directory @p path should be taken as relative to.
* Required. Must exist -- it is resolved with `realpath(3)`.
* @param dst Intended to receive the resolved path. Never written. Note the
* double indirection, which disagrees with the rest of the
* `akgl_path_relative*` family; see TODO.md item 40.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p path or @p from is `NULL`.
* @throws ENOENT If @p from does not exist, along with any other `errno`
* `realpath(3)` raises.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @warning Known defect: the scratch string it claims is never released, so 256
* calls exhaust the string pool. TODO.md, "Known and still open" item 4.
*/
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
{
akgl_String *dirnamestr;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path");
FAIL_ZERO_RETURN(e, from, AKERR_NULLPOINTER, "from");
PASS(e, akgl_heap_next_string(&dirnamestr));
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
dirname((char *)&dirnamestr->data);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect) akerr_ErrorContext *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);

View File

@@ -24,16 +24,6 @@
#include "testutil.h" #include "testutil.h"
/*
* akgl/controller.h declares these as akgl_controller_handle_*, but src has
* always defined them as gamepad_handle_*. Declared here under the names that
* actually link, so the tests can reach them without renaming shipped symbols.
*/
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event);
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event);
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event);
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event);
/** @brief Keyboard id the tests bind their control maps to. */ /** @brief Keyboard id the tests bind their control maps to. */
#define TEST_KBID 11 #define TEST_KBID 11
/** @brief Gamepad id the tests bind their control maps to. */ /** @brief Gamepad id the tests bind their control maps to. */
@@ -424,7 +414,7 @@ akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
player->state = 0; player->state = 0;
player->movement_controls_face = false; player->movement_controls_face = false;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event),
"dpad down press handler"); "dpad down press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
"the dpad down handler did not set MOVING_DOWN (state %d)", player->state); "the dpad down handler did not set MOVING_DOWN (state %d)", player->state);
@@ -432,7 +422,7 @@ akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN), TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
"the dpad down handler did not set FACE_DOWN (state %d)", player->state); "the dpad down handler did not set FACE_DOWN (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event),
"dpad down release handler"); "dpad down release handler");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
"the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state); "the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state);
@@ -441,55 +431,55 @@ akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
player->state = 0; player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad up press handler"); TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event), "dpad up press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP), TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
"the dpad up handler did not set MOVING_UP (state %d)", player->state); "the dpad up handler did not set MOVING_UP (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad up release handler"); TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event), "dpad up release handler");
player->state = 0; player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad left press handler"); TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event), "dpad left press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
"the dpad left handler did not set MOVING_LEFT (state %d)", player->state); "the dpad left handler did not set MOVING_LEFT (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad left release handler"); TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event), "dpad left release handler");
player->state = 0; player->state = 0;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad right press handler"); TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event), "dpad right press handler");
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT), TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"the dpad right handler did not set MOVING_RIGHT (state %d)", player->state); "the dpad right handler did not set MOVING_RIGHT (state %d)", player->state);
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad right release handler"); TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event), "dpad right release handler");
// With automatic facing on, the handler leaves facing to the actor's own // With automatic facing on, the handler leaves facing to the actor's own
// face function. // face function.
player->state = 0; player->state = 0;
player->movement_controls_face = true; player->movement_controls_face = true;
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event),
"dpad down press with automatic facing on"); "dpad down press with automatic facing on");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN), TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
"the handler set facing even though the actor faces automatically (state %d)", "the handler set facing even though the actor faces automatically (state %d)",
player->state); player->state);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(NULL, &event), TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_button_down(NULL, &event),
"dpad press handler with a NULL appstate"); "dpad press handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_down(&appstate_placeholder, NULL), akgl_controller_handle_button_down(&appstate_placeholder, NULL),
"dpad press handler with a NULL event"); "dpad press handler with a NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(NULL, &event), TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_button_up(NULL, &event),
"dpad release handler with a NULL appstate"); "dpad release handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_up(&appstate_placeholder, NULL), akgl_controller_handle_button_up(&appstate_placeholder, NULL),
"dpad release handler with a NULL event"); "dpad release handler with a NULL event");
// With no actor named "player" registered there is nothing to drive. // With no actor named "player" registered there is nothing to drive.
CATCH(e, akgl_registry_init_actor()); CATCH(e, akgl_registry_init_actor());
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_down(&appstate_placeholder, &event), akgl_controller_handle_button_down(&appstate_placeholder, &event),
"dpad press handler with no player actor registered"); "dpad press handler with no player actor registered");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_button_up(&appstate_placeholder, &event), akgl_controller_handle_button_up(&appstate_placeholder, &event),
"dpad release handler with no player actor registered"); "dpad release handler with no player actor registered");
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
@@ -506,22 +496,22 @@ akerr_ErrorContext *test_controller_device_events(void)
// The dummy drivers present no gamepads, so the add and remove handlers // The dummy drivers present no gamepads, so the add and remove handlers
// take their "unknown device" paths. Neither may crash. // take their "unknown device" paths. Neither may crash.
make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0); make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0);
TEST_EXPECT_OK(e, gamepad_handle_added(&appstate_placeholder, &event), TEST_EXPECT_OK(e, akgl_controller_handle_added(&appstate_placeholder, &event),
"handling an add for a device that cannot be opened"); "handling an add for a device that cannot be opened");
make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0); make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0);
TEST_EXPECT_OK(e, gamepad_handle_removed(&appstate_placeholder, &event), TEST_EXPECT_OK(e, akgl_controller_handle_removed(&appstate_placeholder, &event),
"handling a remove for a device that was never open"); "handling a remove for a device that was never open");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(NULL, &event), TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_added(NULL, &event),
"add handler with a NULL appstate"); "add handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_added(&appstate_placeholder, NULL), akgl_controller_handle_added(&appstate_placeholder, NULL),
"add handler with a NULL event"); "add handler with a NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event), TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_removed(NULL, &event),
"remove handler with a NULL appstate"); "remove handler with a NULL appstate");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
gamepad_handle_removed(&appstate_placeholder, NULL), akgl_controller_handle_removed(&appstate_placeholder, NULL),
"remove handler with a NULL event"); "remove handler with a NULL event");
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {

View File

@@ -24,14 +24,6 @@
#include "testutil.h" #include "testutil.h"
/*
* Exported by src/game.c but not declared in akgl/game.h, because they are
* savegame internals rather than part of the supported surface. Declared here
* so the tests can reach them without widening the public API.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp);
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion);
/** @brief Scratch savegame path, created and removed by the tests that use it. */ /** @brief Scratch savegame path, created and removed by the tests that use it. */
static char savepath[] = "akgl_test_savegame.bin"; static char savepath[] = "akgl_test_savegame.bin";
/** @brief Scratch path for deliberately malformed savegames. */ /** @brief Scratch path for deliberately malformed savegames. */

View File

@@ -0,0 +1,42 @@
/**
* @file header_pool_override.c
* @brief Checks that the compile-time pool-size overrides in heap.h actually fire.
*
* `heap.h` wraps every `AKGL_MAX_HEAP_*` ceiling in an `#ifndef` so a game that
* needs more actors can define its own before including it. That mechanism was
* dead until 0.5.0: `actor.h`, `sprite.h` and `character.h` defined the same
* four unconditionally, and `heap.h` includes all three *above* its own
* `#ifndef` block, so whichever landed first won and the guard never fired. The
* override was documented, and did nothing.
*
* The assertion is the compile. Nothing here links against the pools -- it
* cannot, because the library was built with the default ceilings and this
* translation unit deliberately disagrees with it. That disagreement is the
* documented consequence of overriding a ceiling, not something this file is
* working around: the arrays are sized at compile time, so the library and
* everything linking it have to agree.
*/
#define AKGL_MAX_HEAP_ACTOR 128
#define AKGL_MAX_HEAP_STRING 512
#include <akgl/heap.h>
#if AKGL_MAX_HEAP_ACTOR != 128
#error "heap.h overrode a caller's AKGL_MAX_HEAP_ACTOR; the #ifndef guard is dead again"
#endif
#if AKGL_MAX_HEAP_STRING != 512
#error "heap.h overrode a caller's AKGL_MAX_HEAP_STRING; the #ifndef guard is dead again"
#endif
/* The derived ceilings have to follow the override rather than a stale literal. */
#if AKGL_MAX_HEAP_SPRITE != (128 * 16)
#error "AKGL_MAX_HEAP_SPRITE no longer derives from AKGL_MAX_HEAP_ACTOR"
#endif
int akgl_header_selftest_pool_override(void);
int akgl_header_selftest_pool_override(void)
{
return 0;
}