/** * @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 #include #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 /** @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_