Files
libakgl/include/akgl/game.h
Andrew Kesterson 3a262bee54 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>
2026-08-01 07:33:35 -04:00

352 lines
18 KiB
C

/**
* @file game.h
* @brief Process-wide game state, the startup sequence, and the per-frame tick.
*
* This is the top of the library. `akgl_game_init` brings up SDL, the pools, the
* registries and the audio and font engines; `akgl_game_update` is one frame --
* update every actor, step the physics, draw the world.
*
* There is exactly one of everything. `akgl_renderer`, `akgl_physics`, `akgl_camera`, and
* `akgl_gamemap` are globals pointing at the `akgl_default_*` storage below them, so a
* program can swap in its own instance by reassigning the pointer without the
* rest of the library knowing. That is the whole extent of the indirection:
* there is no notion of two worlds at once.
*
* The startup order that actually works:
*
* 1. fill in `akgl_game.name`, `akgl_game.version`, and `akgl_game.uri` -- akgl_game_init
* refuses to run without them;
* 2. akgl_game_init();
* 3. akgl_registry_load_properties() or akgl_set_property(), to configure
* screen size, physics constants and so on;
* 4. akgl_render_2d_init(renderer) and akgl_physics_factory(physics, ...), both
* of which read that configuration;
* 5. load assets, then loop on akgl_game_update().
*
* @warning None of this is thread-safe beyond the `akgl_game.state` mutex, and that
* mutex protects the state flags, not the pools or the registries.
*/
#ifndef _AKGL_GAME_H_
#define _AKGL_GAME_H_
#include <stdint.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <akgl/types.h>
#include <akgl/tilemap.h>
#include <akgl/renderer.h>
#include <akgl/physics.h>
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
// ship an empty Version field: nothing tied the two together.
#include <akgl/version.h>
/** @brief Slot in ::akgl_tracks reserved for background music. Note that slot 0 is unused. */
#define AKGL_GAME_AUDIO_TRACK_BGM 1
/** @brief Size of the ::akgl_tracks table. Every simultaneous sound needs its own slot. */
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
/** @brief Nanoseconds in one second. The unit `SDL_GetTicksNS` reports in. */
#define AKGL_TIME_ONESEC_NS 1000000000
/**
* @brief Nanoseconds in one millisecond. The scale factor from JSON durations to internal ones.
*
* Sprite and character definitions state frame durations in milliseconds
* because that is what a human writing one wants to type; everything that
* compares them against `SDL_GetTicksNS` needs nanoseconds.
*
* This was called `AKGL_TIME_ONESEC_MS` until 0.5.0, which said "one second in
* milliseconds" and held 1000000 -- a name and a value that described two
* different quantities. Both callers that meant the scale factor were right;
* the one that read it as a one-second budget was wrong by a factor of a
* thousand. See akgl_game_state_lock.
*/
#define AKGL_TIME_ONEMS_NS 1000000
/**
* @brief How long akgl_game_state_lock keeps trying for the state mutex, in milliseconds.
*
* A deliberate ceiling rather than a blocking wait: a deadlock here reports an
* error the caller can act on instead of hanging the process.
*/
#define AKGL_GAME_STATE_LOCK_BUDGET_MS 1000
/** @brief How long akgl_game_state_lock sleeps between attempts, in milliseconds. */
#define AKGL_GAME_STATE_LOCK_RETRY_MS 100
/* ==================== GAME STATE VARIABLES =================== */
/** @brief Describes a renderable frame. Declared but not used by anything in the library. */
typedef struct {
float32_t w; /**< Width in pixels. */
float32_t h; /**< Height in pixels. */
SDL_Texture *texture; /**< The frame's texture. */
} akgl_Frame;
/** @brief Stores application-defined game-state flags. */
typedef struct {
int32_t flags; /**< Meaning is entirely the application's; the library never reads it. Guard changes with akgl_game_state_lock. */
} akgl_GameState;
/** @brief Stores game metadata, timing, synchronization, and FPS accounting. */
typedef struct {
char libversion[32]; /**< libakgl's version, stamped by akgl_game_init. Compared on load to refuse a save from another build. */
char version[32]; /**< The *application's* version. Caller-supplied, required, and must be a semver string. */
char name[256]; /**< Application name. Caller-supplied and required; also becomes SDL's app metadata. */
char uri[256]; /**< Application URI, e.g. a reverse-DNS identifier. Caller-supplied, required, and used as the window title. */
akgl_GameState state; /**< The application's own state flags. */
SDL_Mutex *statelock; /**< Guards `state`. Created by akgl_game_init. */
int16_t fps; /**< Frames drawn during the last completed second. Recomputed once per second, not per frame. */
SDL_Time gameStartTime; /**< `SDL_GetTicksNS()` at akgl_game_init. */
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_update_fps call. */
SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */
int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */
} akgl_Game;
/** @brief The SDL window, created by akgl_render_2d_init. `NULL` until then. */
extern SDL_Window *akgl_window;
/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */
extern MIX_Audio *akgl_bgm;
/** @brief The mixer device, created by akgl_game_init. Everything audio goes through it. */
extern MIX_Mixer *akgl_mixer;
/** @brief Playback tracks by slot. #AKGL_GAME_AUDIO_TRACK_BGM is the music track; the rest are the application's to assign. */
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
/** @brief Storage behind the default `akgl_camera`. Point `akgl_camera` elsewhere rather than reaching for this. */
extern SDL_FRect akgl_default_camera;
/** @brief The one game object: metadata, timing, and FPS accounting. */
extern akgl_Game akgl_game;
/** @brief Storage behind the default `akgl_renderer`. */
extern akgl_RenderBackend akgl_default_renderer;
/** @brief Storage behind the default `akgl_physics`. */
extern akgl_PhysicsBackend akgl_default_physics;
/** @brief Storage behind the default `akgl_gamemap`. */
extern akgl_Tilemap akgl_default_gamemap;
/** @brief Currently active tilemap. */
extern akgl_Tilemap *akgl_gamemap;
/** @brief Currently active renderer. */
extern akgl_RenderBackend *akgl_renderer;
/** @brief Currently active physics backend. */
extern akgl_PhysicsBackend *akgl_physics;
/** @brief Currently active camera. */
extern SDL_FRect *akgl_camera;
/**
* @brief True when every bit of `y` is set in `x`. Not "any of them" -- all of them.
* @warning Unparenthesized: it expands to `(x & y) == y`, so `!AKGL_BITMASK_HAS(a, b)`
* parses as `!(a & b) == b`. Use #AKGL_BITMASK_HASNOT rather than
* negating this. TODO.md item 21.
*/
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
/** @brief True when at least one bit of `y` is missing from `x`. Same parenthesization caveat as #AKGL_BITMASK_HAS. */
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
/** @brief Set every bit of `y` in `x`. Modifies `x`. */
#define AKGL_BITMASK_ADD(x, y) x |= y
/** @brief Clear every bit of `y` in `x`. Modifies `x`. */
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
/** @brief Clear every bit of `x`. Carries its own trailing semicolon, so do not add another. */
#define AKGL_BITMASK_CLEAR(x) x = 0;
/**
* @brief Bring the whole library up: error codes, pools, registries, SDL, audio, fonts, gamepads.
*
* In order: claim the libakgl status band (so every later error has a name),
* stamp the library version, start the frame clock, create the state mutex,
* check that the caller filled in the three required `game` fields, zero the
* pools, create the registries, hand SDL the app metadata, clear the control
* maps, `SDL_Init` video/gamepad/audio, load the bundled controller database,
* open any attached gamepads, start SDL_mixer and SDL_ttf, and finally point
* `akgl_renderer`, `akgl_physics`, `akgl_camera`, and `akgl_gamemap` at their default storage.
*
* What it does *not* do: create the window, choose a physics backend, or load
* any configuration. Those read properties, so they come after the caller has
* set them. See the sequence at the top of this file.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If `akgl_game.name`, `akgl_game.version`, or `akgl_game.uri` is
* empty. All three are required and there are no defaults -- the
* window title, SDL's app metadata, and the savegame compatibility
* check are all built from them.
* @throws AKGL_ERR_SDL If the state mutex cannot be created, if `SDL_Init`
* fails, if a controller-database entry is rejected, if SDL_mixer
* cannot start or open the default playback device, or if SDL_ttf
* cannot start. Each message carries `SDL_GetError()`.
* @throws AKERR_STATUS_RANGE_OVERLAP If another component already owns part of
* the libakgl status band. See akgl_error_init.
* @throws AKERR_* Whatever the heap, registry, and controller initializers raise.
*
* @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.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init(void);
/**
* @brief Count this frame, and recompute the frame rate once a second has passed.
*
* Called at the top of akgl_game_update, so a caller running its own loop needs
* to call it itself. `akgl_game.fps` is only refreshed when a full second has
* elapsed, so it is a completed-second average rather than an instantaneous
* figure -- and it reads 0 for the first second of the process, which is under
* the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first
* second is up.
*/
void akgl_game_update_fps(void);
/**
* @brief Write the game state and the name-to-pointer tables to a save file.
*
* Writes the `akgl_Game` struct verbatim, then four name tables -- actors,
* sprites, spritesheets, characters -- each mapping a registered name to the
* address the object had at save time, and each terminated by a zeroed name and
* a zeroed pointer. The tables are what let akgl_game_load reconnect pointers
* between objects that will be at different addresses next run.
*
* @param fpath Path to write. Required. Opened with `"wb"`; an existing file is
* truncated.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports, with the path in
* the message.
* @throws AKERR_IO On a stream error or a short write.
*
* @note This is a partial implementation: the name tables are written but the
* objects themselves are not, so the file is not yet enough to restore a
* session. See also TODO.md, "Known and still open" item 7 -- the writer
* and the reader disagree on the name-field widths, so a save with any
* registered spritesheet cannot be read back.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
/**
* @brief Read a save file back, refusing one that does not match this build.
*
* Reads the saved `akgl_Game`, then rejects the file unless the library version,
* the game version, the game name, and the game URI all match the running
* program -- versions by exact semver equality, name and URI by string compare.
* Only then does it copy the saved state over `game` and rebuild the four
* old-address-to-current-object maps.
*
* @param fpath Path to read. Required. Opened with `"rb"`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports.
* @throws AKERR_EOF If a read runs off the end of the file -- which is what a
* truncated or corrupt name table looks like.
* @throws AKERR_IO On a stream error.
* @throws AKERR_VALUE If either version string in the save file, or either in
* the running game, is not valid semver.
* @throws AKERR_API If the save file is from a different library version, game
* version, game name, or game URI.
*
* @note Like akgl_game_save, this is partial: it rebuilds the pointer maps but
* does not yet read back any objects. The four `SDL_CreateProperties`
* sets it builds are never destroyed, so each call leaks them.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
/**
* @brief The default `akgl_game.lowfpsfunc`: log the current frame rate.
*
* Called from akgl_game_update_fps on every frame where `akgl_game.fps` is under 30.
* It is a placeholder -- the point of the hook is that a game can replace it
* with something that actually sheds work.
*/
void akgl_game_lowfps(void);
/**
* @brief Take the game-state mutex, retrying rather than blocking.
*
* Polls with `SDL_TryLockMutex` on a 100 ms cadence instead of blocking
* outright, so a deadlock reports an error rather than hanging the process.
*
* @return `NULL` once the lock is held, otherwise an error context owned by the
* caller.
* @throws AKGL_ERR_SDL If #AKGL_GAME_STATE_LOCK_BUDGET_MS elapses with the lock
* still held elsewhere. `SDL_GetError()` is appended for whatever it is
* worth, but after a failed `SDL_TryLockMutex` it is usually stale or
* empty -- contention is reported by the return value, not by an error
* string. The status is the signal, not the text.
*
* @note Before 0.5.0 the loop counted against a constant named
* `AKGL_TIME_ONESEC_MS` that held 1000000, so it retried 10,000 times at
* 100 ms and blocked for roughly sixteen minutes rather than the one
* second documented here.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
/**
* @brief Release the game-state mutex.
*
* @return `NULL`. `SDL_UnlockMutex` reports nothing, so there is no failure path
* -- including for the case that matters, unlocking a mutex this thread
* does not hold, which is undefined behaviour in SDL rather than an
* error here.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
/**
* @brief One frame: update every actor layer by layer, step the physics, draw the world.
*
* Takes the state lock, counts the frame, then walks layers 0 through
* #AKGL_TILEMAP_MAX_LAYERS calling each live actor's `updatefunc`, optionally
* rescaling it to the tilemap first. Then it steps `akgl_physics` and draws through
* `akgl_renderer`, and releases the lock.
*
* @param opflags Iterator flags. Optional -- `NULL` selects a default set that
* sweeps one layer at a time, in which case `layerid` is advanced
* by the loop. Only #AKGL_ITERATOR_OP_TILEMAPSCALE is read here;
* with it clear every actor's `scale` is forced to 1.0. Note that
* the flags are *not* forwarded to the physics or render calls,
* both of which are passed `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the state lock cannot be taken.
* @throws AKERR_* Whatever an actor's `updatefunc`, akgl_tilemap_scale_actor,
* the physics backend, or the renderer raises.
*
* @warning Every failure path returns with the state lock still held, and a live
* actor's `updatefunc` is called without a `NULL` check -- a hand-built
* actor that never went through akgl_actor_initialize crashes here.
*/
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_