/** * @file game.c * @brief Implements the game subsystem. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include SDL_Window *window = NULL; // Currently active objects akgl_RenderBackend *renderer; akgl_PhysicsBackend *physics; SDL_FRect *camera; akgl_Tilemap *gamemap; // Default objects akgl_RenderBackend _akgl_renderer; akgl_PhysicsBackend _akgl_physics; SDL_FRect _akgl_camera; akgl_Tilemap _akgl_gamemap; MIX_Audio *bgm = NULL; MIX_Mixer *akgl_mixer = NULL; MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS]; akgl_Game game; void akgl_game_lowfps(void) { SDL_Log("Low FPS! %d", game.fps); return; } akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init() { int screenwidth = 0; int screenheight = 0; int i = 0; PREPARE_ERROR(e); // First, before anything that can raise: everything below reports through // AKGL_ERR_* codes, and a code raised before its name is registered prints // as "Unknown Error" in the stack trace the caller is left holding. PASS(e, akgl_error_init()); strncpy((char *)&game.libversion, AKGL_VERSION, 32); game.gameStartTime = SDL_GetTicksNS(); game.lastIterTime = game.gameStartTime; game.lastFPSTime = game.gameStartTime; game.lowfpsfunc = &akgl_game_lowfps; game.statelock = SDL_CreateMutex(); FAIL_ZERO_RETURN(e, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError()); PASS(e, akgl_game_state_lock()); FAIL_ZERO_RETURN(e, strlen((char *)&game.name), AKERR_NULLPOINTER, "Must provide game name"); FAIL_ZERO_RETURN(e, strlen((char *)&game.version), AKERR_NULLPOINTER, "Must provide game version"); FAIL_ZERO_RETURN(e, strlen((char *)&game.uri), AKERR_NULLPOINTER, "Must provide game uri"); PASS(e, akgl_heap_init()); PASS(e, akgl_registry_init_actor()); PASS(e, akgl_registry_init_sprite()); PASS(e, akgl_registry_init_spritesheet()); PASS(e, akgl_registry_init_character()); PASS(e, akgl_registry_init_font()); PASS(e, akgl_registry_init_music()); PASS(e, akgl_registry_init_properties()); PASS(e, akgl_registry_init_actor_state_strings()); SDL_SetAppMetadata(game.name, game.version, game.uri); for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) { memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap)); } FAIL_ZERO_RETURN( e, SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO), AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); // Load the Game Controller DB for ( i = 0; i < AKGL_SDL_GAMECONTROLLER_DB_LEN ; i++ ) { if ( SDL_AddGamepadMapping(SDL_GAMECONTROLLER_DB[i]) == -1 ) { FAIL_ZERO_RETURN(e, 0, AKGL_ERR_SDL, "%s", SDL_GetError()); } } PASS(e, akgl_controller_open_gamepads()); FAIL_ZERO_RETURN( e, MIX_Init(), AKGL_ERR_SDL, "Couldn't initialize audio: %s", SDL_GetError()); akgl_mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 0); FAIL_ZERO_RETURN( e, akgl_mixer, AKGL_ERR_SDL, "Unable to create mixer device: %s", SDL_GetError()); FAIL_ZERO_RETURN( e, TTF_Init(), AKGL_ERR_SDL, "Couldn't initialize front engine: %s", SDL_GetError()); camera = &_akgl_camera; renderer = &_akgl_renderer; physics = &_akgl_physics; gamemap = &_akgl_gamemap; PASS(e, akgl_game_state_unlock()); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void) { PREPARE_ERROR(e); SDL_Time totaltime = 0; while ( totaltime < AKGL_TIME_ONESEC_MS ) { if ( SDL_TryLockMutex(game.statelock) == true ) { SUCCEED_RETURN(e); } totaltime += 100; SDL_Delay(100); } FAIL_RETURN(e, AKGL_ERR_SDL, "%s", SDL_GetError()); } akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void) { PREPARE_ERROR(e); SDL_UnlockMutex(game.statelock); SUCCEED_RETURN(e); } void akgl_game_updateFPS() { SDL_Time curTime; curTime = SDL_GetTicksNS(); if ( (curTime - game.lastFPSTime) > AKGL_TIME_ONESEC_NS ) { game.fps = game.framesSinceUpdate; game.framesSinceUpdate = 0; game.lastFPSTime = curTime; } if ( game.fps < 30 ) { game.lowfpsfunc(); } game.framesSinceUpdate += 1; game.lastIterTime = curTime; } /* * entity name -> pointer map tables */ /** * @brief Serializes an actor name-to-pointer entry. * @param userdata Open output stream supplied by the caller. * @param props Actor registry being iterated. * @param name Actor registry key to serialize. */ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name) { FILE *fp = (FILE *)userdata; akgl_Actor *actor = NULL; PREPARE_ERROR(e); ATTEMPT { FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer"); CATCH(e, aksl_fwrite((char *)name, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp)); actor = SDL_GetPointerProperty(props, name, NULL); CATCH(e, aksl_fwrite(&actor, 1, sizeof(akgl_Actor *), fp)); } CLEANUP { } PROCESS(e) { } FINISH_NORETURN(e); } /** * @brief Game save spritename iterator. * @param userdata Caller data supplied to the SDL property iterator. * @param props SDL property collection being iterated. * @param name Registry key or human-readable object name. */ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name) { FILE *fp = (FILE *)userdata; akgl_Sprite *sprite = NULL; PREPARE_ERROR(e); ATTEMPT { FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer"); sprite = SDL_GetPointerProperty(props, name, NULL); CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp)); CATCH(e, aksl_fwrite(&sprite, 1, sizeof(akgl_Sprite *), fp)); } CLEANUP { } PROCESS(e) { } FINISH_NORETURN(e); } /** * @brief Game save spritesheetname iterator. * @param userdata Caller data supplied to the SDL property iterator. * @param props SDL property collection being iterated. * @param name Registry key or human-readable object name. */ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name) { FILE *fp = (FILE *)userdata; akgl_SpriteSheet *spritesheet = NULL; PREPARE_ERROR(e); ATTEMPT { FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer"); spritesheet = SDL_GetPointerProperty(props, name, NULL); CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp)); CATCH(e, aksl_fwrite(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp)); } CLEANUP { } PROCESS(e) { } FINISH_NORETURN(e); } /** * @brief Game save charactername iterator. * @param userdata Caller data supplied to the SDL property iterator. * @param props SDL property collection being iterated. * @param name Registry key or human-readable object name. */ void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name) { FILE *fp = (FILE *)userdata; PREPARE_ERROR(e); akgl_Character *character = NULL; ATTEMPT { FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer"); character = SDL_GetPointerProperty(props, name, NULL); CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp)); CATCH(e, aksl_fwrite(&character, 1, sizeof(akgl_Character *), fp)); } CLEANUP { } PROCESS(e) { } FINISH_NORETURN(e); } /** * @brief Game save actors. * @param fp Open save-game stream. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp) { PREPARE_ERROR(e); // Each name table ends with a zeroed name field and a zeroed pointer, which // is what akgl_game_load_objectnamemap() looks for to stop reading. The // terminator has to come from a buffer at least as long as the longest name // field: writing N bytes from the address of a single char would emit N-1 // bytes of whatever happened to follow it on the stack, which both leaks // stack contents into the save file and produces a sentinel the loader // cannot recognize. char nullbuf[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; memset((void *)&nullbuf, 0x00, sizeof(nullbuf)); ATTEMPT { FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer"); // write the actor name pointer table SDL_EnumerateProperties( AKGL_REGISTRY_ACTOR, &akgl_game_save_actorname_iterator, (void *)fp); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp)); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp)); // write the sprite name pointer table SDL_EnumerateProperties( AKGL_REGISTRY_SPRITE, &akgl_game_save_spritename_iterator, (void *)fp); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp)); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp)); // write the spritesheet name pointer table SDL_EnumerateProperties( AKGL_REGISTRY_SPRITESHEET, &akgl_game_save_spritesheetname_iterator, (void *)fp); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp)); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp)); // write the character name pointer table SDL_EnumerateProperties( AKGL_REGISTRY_CHARACTER, &akgl_game_save_charactername_iterator, (void *)fp); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp)); CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Character *), fp)); } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath) { FILE *fp = NULL; PREPARE_ERROR(e); ATTEMPT { FAIL_ZERO_BREAK(e, fpath, AKERR_NULLPOINTER, "NULL file path"); CATCH(e, aksl_fopen(fpath, "wb", &fp)); CATCH(e, aksl_fwrite(&game, 1, sizeof(akgl_Game), fp)); CATCH(e, akgl_game_save_actors(fp)); } CLEANUP { // CLEANUP must precede PROCESS: with the two transposed, the fclose // lands inside the PROCESS switch and only runs when an error context // exists and reports success, so an ordinary save never flushed or // closed its stream. if ( fp != NULL ) fclose(fp); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); // SUCCEED_NORETURN if in main(). } /** * @brief Game load objectnamemap. * @param fp Open save-game stream. * @param map Tilemap to inspect, draw, or modify. * @param namelength Fixed serialized name-field length. * @param ptrlength Serialized pointer-field length. * @param registry SDL property registry being queried. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_* Propagates an error reported by a delegated operation. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry) { void *ptr = NULL; char ptrstring[32]; char objname[namelength]; int retval = 0; bool done = false; PREPARE_ERROR(e); // The ATTEMPT block sits inside the loop on purpose. CATCH reports a // failure by breaking, and a break binds to the innermost enclosing switch // or loop, so a CATCH written directly inside `while` would leave the loop // and then fall through to SUCCEED_RETURN -- reporting a truncated or // corrupt name table as a successful load. while ( done == false ) { ATTEMPT { CATCH(e, aksl_fread((void *)&objname, 1, namelength, fp)); CATCH(e, aksl_fread((void *)&ptr, 1, ptrlength, fp)); // End of the map if ( ptr == 0x00 && objname[0] == 0x00 ) { done = true; break; } // The map allows us to say "Object X has a reference to object Y at // address Z. The object they had at address Z was named A. Our current // instance of object named A is at address B. So we map address Z to // address B, so that we can reconnect function pointers on objects loaded // from the save game state." // SDL_Properties objects can only use string keys, so we can't use the // old pointer as a key without first converting it to a string. CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32)); snprintf((char *)&ptrstring, 32, "%p", ptr); SDL_SetPointerProperty( map, ptrstring, SDL_GetPointerProperty(registry, objname, NULL)); } CLEANUP { } PROCESS(e) { } FINISH(e, true); }; SUCCEED_RETURN(e); } /** * @brief Game load versioncmp. * @param versiontype Description of the version being compared. * @param newversion Version read from the save data. * @param curversion Version supported by the running library. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_API When the corresponding validation or operation fails. * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. * @throws AKERR_VALUE When the corresponding validation or operation fails. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion) { semver_t current_version = {}; semver_t compare_version = {}; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, versiontype, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, curversion, AKERR_NULLPOINTER, "NULL argument"); FAIL_ZERO_RETURN(e, newversion, AKERR_NULLPOINTER, "NULL argument"); ATTEMPT { // Check save game library version FAIL_NONZERO_BREAK( e, semver_parse((const char *)curversion, ¤t_version), AKERR_VALUE, "Invalid semantic %s version in current game: %s", versiontype, (char *)curversion); FAIL_NONZERO_BREAK( e, semver_parse((const char *)newversion, &compare_version), AKERR_VALUE, "Invalid semantic %s version in save game: %s", versiontype, (char *)&newversion); FAIL_ZERO_BREAK( e, semver_satisfies(compare_version, current_version, "="), AKERR_API, "Incompatible save game %s version (%s != %s)", versiontype, curversion, (char *)&newversion); } CLEANUP { semver_free(¤t_version); semver_free(&compare_version); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath) { akgl_Game savegame; SDL_PropertiesID actormap; SDL_PropertiesID spritemap; SDL_PropertiesID spritesheetmap; SDL_PropertiesID charactermap; FILE *fp = NULL; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path"); ATTEMPT { CATCH(e, aksl_fopen(fpath, "rb", &fp)); CATCH(e, aksl_fread((void *)&savegame, 1, sizeof(akgl_Game), fp)); CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION)); CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&game.version)); FAIL_NONZERO_RETURN( e, strncmp((char *)&savegame.name, (char *)&game.name, 256), AKERR_API, "Savegame is not compatible with this game"); FAIL_NONZERO_RETURN( e, strncmp((char *)&savegame.uri, (char *)&game.uri, 256), AKERR_API, "Savegame is not compatible with this game"); memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game)); // Load actor name map actormap = SDL_CreateProperties(); CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR)); // Load sprite name map spritemap = SDL_CreateProperties(); CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE)); // Load spritesheet name map spritesheetmap = SDL_CreateProperties(); CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET)); // Load character name map charactermap = SDL_CreateProperties(); CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER)); // Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers } CLEANUP { if ( fp != NULL ) { fclose(fp); } } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags) { PREPARE_ERROR(e); akgl_Iterator defflags = { .flags = (AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK), .layerid = 0 }; SDL_Time curTime = SDL_GetTicksNS(); akgl_Actor *actor = NULL; if ( opflags == NULL ) { opflags = &defflags; } PASS(e, akgl_game_state_lock()); akgl_game_updateFPS(); for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) { if ( opflags == &defflags ) { opflags->layerid = i; } for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) { actor = &HEAP_ACTOR[j]; if ( actor->refcount == 0 ) { continue; } if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) { PASS(e, akgl_tilemap_scale_actor(gamemap, actor)); } else { actor->scale = 1.0; } PASS(e, actor->updatefunc(actor)); } } PASS(e, physics->simulate(physics, NULL)); PASS(e, renderer->draw_world(renderer, NULL)); PASS(e, akgl_game_state_unlock()); SUCCEED_RETURN(e); }