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