The Doxygen comments were generated from the declarations, so 217 @throws lines across 21 headers read "When the corresponding validation or operation fails" and told a caller nothing beyond the status name. The @param lines were the same shape: every output was "Output destination populated by the function", every instance "Object to initialize, inspect, or modify". Rewritten against the implementations, following the pattern libakstdlib already uses: - @throws names the condition. akgl_sprite_load_json separated AKERR_KEY (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL and AKGL_ERR_HEAP, which it raises and never declared. - Parameters say whether they are required, what a NULL means, and what is written on a failure path. Where an argument is not checked, the doc says so: akgl_heap_next_actor's dest is a crash on NULL, not an error, and akgl_render_2d_frame_start dereferences self before testing it. - The conventions move up to the file blocks so the per-function docs stay short. json_helpers.h states once that absence is an error here and that json_t * results are borrowed; heap.h explains the pool model and the acquire asymmetry; physics.h carries the thrust/environmental/velocity table. - Struct fields, enum values, macros and exported globals are documented, including the dead ones - sprite_w/sprite_h, movetimer, p_scale and timer_gravity are read by nothing, and say so. Also fixes ten comments in error.h and audio.h that opened with /** rather than /**<, so Doxygen attached them to the following entity and rendered the text as part of the macro's value. Verified against the generated HTML. Comments only - no declaration changed. Doxygen builds clean under WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
300 lines
15 KiB
C
300 lines
15 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. `renderer`, `physics`, `camera`, and
|
|
* `gamemap` are globals pointing at the `_akgl_*` 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 `game.name`, `game.version`, and `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_init2d(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 `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 "types.h"
|
|
#include "tilemap.h"
|
|
#include "renderer.h"
|
|
#include "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 Misnamed: this is nanoseconds per **millisecond**, not milliseconds per second.
|
|
*
|
|
* One second in milliseconds is 1000. 1000000 is one millisecond in
|
|
* nanoseconds, which is how sprite and character load actually use it -- as a
|
|
* milliseconds-to-nanoseconds scale factor -- and is not how
|
|
* akgl_game_state_lock uses it. TODO.md item 6 proposes renaming it
|
|
* `AKGL_TIME_ONEMS_NS`.
|
|
*/
|
|
#define AKGL_TIME_ONESEC_MS 1000000
|
|
|
|
/* ==================== 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_updateFPS 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_init2d. `NULL` until then. */
|
|
extern SDL_Window *window;
|
|
/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */
|
|
extern MIX_Audio *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 `camera`. Point `camera` elsewhere rather than reaching for this. */
|
|
extern SDL_FRect _akgl_camera;
|
|
/** @brief The one game object: metadata, timing, and FPS accounting. */
|
|
extern akgl_Game game;
|
|
/** @brief Storage behind the default `renderer`. */
|
|
extern akgl_RenderBackend _akgl_renderer;
|
|
/** @brief Storage behind the default `physics`. */
|
|
extern akgl_PhysicsBackend _akgl_physics;
|
|
/** @brief Storage behind the default `gamemap`. */
|
|
extern akgl_Tilemap _akgl_gamemap;
|
|
|
|
/** @brief Currently active tilemap. */
|
|
extern akgl_Tilemap *gamemap;
|
|
/** @brief Currently active renderer. */
|
|
extern akgl_RenderBackend *renderer;
|
|
/** @brief Currently active physics backend. */
|
|
extern akgl_PhysicsBackend *physics;
|
|
/** @brief Currently active camera. */
|
|
extern SDL_FRect *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
|
|
* `renderer`, `physics`, `camera`, and `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 `game.name`, `game.version`, or `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_init2d.
|
|
* 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. `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_updateFPS();
|
|
/**
|
|
* @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 `game.lowfpsfunc`: log the current frame rate.
|
|
*
|
|
* Called from akgl_game_updateFPS on every frame where `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 the budget runs out with the lock still held
|
|
* elsewhere. The message carries `SDL_GetError()`, which after a failed
|
|
* `SDL_TryLockMutex` is usually stale or empty -- the status is the
|
|
* signal, not the text.
|
|
*
|
|
* @note The budget is meant to be one second but is not: the loop counts against
|
|
* #AKGL_TIME_ONESEC_MS, which is nanoseconds-per-millisecond (1000000)
|
|
* rather than milliseconds-per-second, so it retries 10,000 times at 100 ms
|
|
* and gives up after roughly 16 minutes. TODO.md item 6.
|
|
*/
|
|
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 `physics` and draws through
|
|
* `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_
|