Updates deps/libakstdlib to 669b2b3. That commit is a TODO entry rather than new code, so the interesting part is what libakgl was not yet calling: it links libakstdlib and uses ten of its wrappers while calling the raw libc function at a good many more sites. Four of those were carrying a real defect. akgl_game_save checked every write and threw away the close. Every write goes through aksl_fwrite, and for a record this size all of them land in stdio's buffer -- nothing reaches the device until the close flushes it, so a full disk, an exceeded quota or an NFS server going away is reported only there. The function returned success over a savegame that was never written. aksl_fclose surfaces it. tests/game.c reaches the path through /dev/full, which accepts writes and fails at the flush; against the unfixed code the test reports "expected a failure, got success" with ENOSPC. The close has to drop the FILE * before the status is examined, because fclose(3) disassociates the stream either way and CLEANUP would close it again. Written the other way round it aborted in glibc with "double free detected in tcache" the first time a close actually failed, which is how that ordering was found. akgl_game_load's end-of-file check used fgetc(3), which spells "end of file" and "the read failed" with the same EOF -- a disk error there read as a clean end and passed the check it was meant to fail. aksl_fgetc tells them apart. Extracted to require_at_eof, because inverting the sense of a call inline needs a nested ATTEMPT whose break binds to the wrong switch. Two snprintf sites were truncating under a silenced -Wformat-truncation. The compiler was right about both. The tileset one handed the shortened path to IMG_LoadTexture, so a length error was reported as a missing file, with SDL naming a path the caller never wrote. Both use aksl_snprintf now and the suppression is gone; nothing in the tree uses those macros any more. tests/tilemap.c covers the join, and against the unfixed code it gets AKGL_ERR_SDL where it wants AKERR_OUTOFBOUNDS. The two src/game.c strncpy sites the fixed-width copy sweep missed -- the savegame name field and the libversion stamp -- use aksl_strncpy and sizeof rather than a repeated 32. Also makes tests/physics_sim.c deterministic. It placed gravity_time at "now - dt" and let the real clock supply the step, so a machine busy enough to deschedule the process between that store and the SDL_GetTicksNS() inside simulate got a longer step. It went red once, under a parallel ctest. It now sets max_timestep to the step it wants and gravity_time to zero, so the bound supplies the step and the scheduler cannot reach it. TODO.md records what is deliberately not adopted: the pure-arithmetic wrappers, which can only fail on NULL and which the tree already spells two ways, and the collections, which the registries do not want because SDL owns the property-set lifetime. AGENTS.md has the rule and the fclose ordering trap. 26/26 ctest, memcheck clean, warning-clean at -Wall -Werror. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
373 lines
20 KiB
C
373 lines
20 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.
|
|
* @throws ENOSPC, EDQUOT Or whatever else the close reports. A record this size
|
|
* fits entirely in stdio's buffer, so nothing reaches the device until
|
|
* the close flushes it and that is the only place a full disk, an
|
|
* exceeded quota or a server going away can be seen. The close is
|
|
* checked; success here means the bytes are on the device.
|
|
* @throws AKERR_OUTOFBOUNDS If a registered name does not fit its field.
|
|
*
|
|
* @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.
|
|
* @throws AKERR_IO If anything remains in the file after the four name tables.
|
|
* The tables carry no length prefix and end at a zeroed sentinel, so a
|
|
* reader whose field widths disagree with the writer's stops early
|
|
* inside an entry and would otherwise report success with silently
|
|
* wrong maps. Trailing data is the symptom of that disagreement.
|
|
*
|
|
* @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 calls each live actor's
|
|
* `updatefunc` **exactly once**, optionally rescaling it to the tilemap first.
|
|
* Then it steps `akgl_physics` and draws through `akgl_renderer`, and releases
|
|
* the lock.
|
|
*
|
|
* Until 0.5.0 the sweep sat inside a walk over #AKGL_TILEMAP_MAX_LAYERS that
|
|
* never compared an actor's `layer` to the layer it was on, so every actor
|
|
* updated sixteen times a frame. Updating an actor is not a per-layer
|
|
* operation; drawing is, and akgl_render_2d_draw_world walks the layers itself.
|
|
*
|
|
* @param opflags Iterator flags. Optional -- `NULL` selects
|
|
* #AKGL_ITERATOR_OP_UPDATE alone, which is every live actor
|
|
* once. Two bits are read here:
|
|
* #AKGL_ITERATOR_OP_TILEMAPSCALE, without which every actor's
|
|
* `scale` is forced to 1.0; and #AKGL_ITERATOR_OP_LAYERMASK,
|
|
* which restricts the sweep to the actors whose `layer` matches
|
|
* `layerid`. 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_
|