Closes internal-consistency items 1 through 6, 12 and 13. Every include guard is _AKGL_<FILE>_H_, every in-project header include is angled, and every exported function, type and global carries the akgl_ prefix. This is an ABI break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename table. The renames were driven by renaming each declaration and letting the compiler find the uses, not by pattern substitution: renderer, physics and camera are also parameter and struct-member names, and a sed would have rewritten map->physics and every akgl_RenderBackend *renderer parameter without a word. Item 4 turned out not to be cosmetic. The library exported a global called renderer and tests/character.c defined an SDL_Renderer *renderer of its own; the executable's definition preempted the library's, akgl_sprite_load_json read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load in that suite failed. The suite reported success anyway, because libakerror's unhandled-error handler ends in exit(errctx->status), exit keeps only the low byte, and AKGL_ERR_SDL is exactly 256. So character had been green while running one of its four tests, and every suite in the tree was unable to fail on the most common status in a library built on SDL. Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which collapses any status a byte cannot carry onto 1, and every suite installs it. character binds a real backend with akgl_render_2d_bind. Its fourth test then runs for the first time and fails on a defect it has asserted all along, so akgl_heap_release_character now walks state_sprites with AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot -- TODO.md Defects item 21 and half of Carried over item 1. AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so akgl_game_state_lock waited roughly sixteen minutes rather than one second. It is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and tests/game.c holds the mutex from a second thread to assert the wait -- the contended path had no coverage at all. Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both install() and a generated translation unit per header, so a header that ships is a header that is checked. Writing that found registry.h, which used SDL_PropertiesID in eight declarations and included no SDL header. 23/23 suites pass, memcheck is clean, reindent --check is clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
315 lines
16 KiB
C
315 lines
16 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();
|
|
/**
|
|
* @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.
|
|
*
|
|
* 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();
|
|
/**
|
|
* @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);
|
|
|
|
#endif //_AKGL_GAME_H_
|