Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11. Both string accessors in json_helpers.c ended their ATTEMPT block with FINISH(errctx, false), which swallows the failure, and then strncpy'd through the pointer akgl_heap_next_string never set. So the one condition the pool exists to report -- it is full, which in practice means something is not releasing -- arrived as a segfault somewhere else entirely. It is FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the old code, which is also how the tilemap leak test in the previous commit confirmed this one. akgl_tilemap_release tested layers[i].texture and destroyed tilesets[i].texture, so every tileset texture was freed twice on one release and no image layer's texture was freed at all. Pointers are cleared as they go, so a second release is safe instead of a use-after-free. akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on frame one because fps is 0 for the first second. Only akgl_game_init installs it, and renderer.h documents the other path deliberately -- a host that owns its window binds a backend instead. It installs the default when it finds NULL. akgl_controller_pushmap and akgl_controller_default checked only the upper bound, so a negative id indexed before akgl_controlmaps. The two test-harness helpers were quietly worthless. akgl_render_and_compare drew t1 on both passes, so it always reported a match and every image assertion built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd s1->pitch * s1->h bytes out of both surfaces without checking that the second was the same size, so a smaller one was read past its end. Both fixed, both tested. tests/util.c also now calls the collide-point test it has defined and never run. 25/25 pass, memcheck clean, reindent --check and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
355 lines
18 KiB
C
355 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 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. akgl_game_init installs akgl_game_lowfps, and akgl_game_update_fps installs it too if it finds this NULL -- an embedder that binds its own renderer rather than calling akgl_game_init used to crash here on frame one. 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.
|
|
*
|
|
* Fully parenthesized, so it composes: `!AKGL_BITMASK_HAS(a, b)` and
|
|
* `AKGL_BITMASK_HAS(a, b) && cond` both mean what they read as. Until 0.5.0 it
|
|
* expanded to a bare `(x & y) == y`, so a negation bound to the `&` and parsed
|
|
* as `!(a & b) == b`. Nothing in the tree negated it -- #AKGL_BITMASK_HASNOT
|
|
* exists for that -- which is the only reason it was latent rather than live.
|
|
*/
|
|
#define AKGL_BITMASK_HAS(x, y) ((((x) & (y)) == (y)))
|
|
/** @brief True when at least one bit of `y` is missing from `x`. */
|
|
#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`. Modifies `x`.
|
|
*
|
|
* Carries no trailing semicolon: write `AKGL_BITMASK_CLEAR(flags);` like any
|
|
* other statement. It used to include one, so every ordinary use produced an
|
|
* empty statement after it and a use as the whole body of an unbraced `if`
|
|
* would have taken the following statement with it.
|
|
*/
|
|
#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_
|