Make savegames, optional array elements, empty text and coverage work
Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27. The savegame name tables carry no length prefix, so writer and reader have to agree on a field width exactly. The writer used each object's own maximum name length -- 512 for a spritesheet, a filename -- and the reader used AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH constants drive both sides now. The failure turned out to be worse than "cannot be read back": a reader stepping the wrong width does not run off anything, it finds a run of zeros inside an entry, stops early, and reports success with silently wrong maps. A test asserting only that the load succeeded passed against the broken reader. So akgl_game_load checks it is at EOF once the tables are read, which turns a width disagreement into AKERR_IO instead of a corruption. That is the assertion the new roundtrip test -- the first with all four registries populated -- hangs on. akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS, which is what the array index accessors report, so "this element is optional" works for an array element and not only for an object member. The arm goes above the one holding the memcpy: HANDLE_GROUP emits no break and every arm falls into that body. That test needed a second attempt. with_default returns *the context it was given* when it does not handle the status, so TEST_EXPECT_OK -- which releases whatever the statement returns -- double-released it against a CLEANUP block that released it too, and a double-released context corrupts the failure rather than reporting it. The first draft passed against the unfixed library. akgl_text_rendertextat returns success without rasterizing for the empty string, matching akgl_text_measure, which has always accepted it. The check sits after the font and backend guards, so drawing nothing still refuses what drawing something refuses. tests/text.c had this case written and unasserted waiting for the two halves of the header to agree. character_load_json_state_int_from_strings guards dest rather than testing states twice. Not asserted: the function is static with one call site that passes a real pointer, so the guard cannot fire, and reaching it from a test would mean giving it external linkage purely for that. Both gcovr invocations take the build tree as an explicit positional search path. gcovr searches --root when given none, which is the source directory, where build trees live; --object-directory does not narrow it. Verified by building two instrumented trees with a source edit between them: the old invocation fails with "Got function write_exact on multiple lines: 46, 48" and exits 64, the new one exits 0 and the full coverage run passes with the stale tree still present. 25/25 pass, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -142,7 +142,7 @@ static akerr_ErrorContext *character_load_json_state_int_from_strings(json_t *st
|
||||
akgl_String *tmpstring = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
|
||||
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination integer");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstring));
|
||||
|
||||
70
src/game.c
70
src/game.c
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <akstdlib.h>
|
||||
#include <akgl/game.h>
|
||||
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/tilemap.h>
|
||||
#include <akgl/sprite.h>
|
||||
@@ -50,13 +51,34 @@ static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmem
|
||||
return aksl_fwrite(ptr, size, nmemb, fp, &transferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Field widths of the four savegame name tables.
|
||||
*
|
||||
* The writer and the reader have to agree exactly. The tables carry no length
|
||||
* prefix, so the reader finds each next entry by stepping this many bytes; get
|
||||
* it wrong and every entry after the first is read out of the middle of its
|
||||
* neighbour.
|
||||
*
|
||||
* They disagreed until 0.5.0. The writer used each object's own maximum name
|
||||
* length -- 512 for a spritesheet, which is a filename -- and the reader used
|
||||
* `AKGL_ACTOR_MAX_NAME_LENGTH` for all four. The other three happen to be 128
|
||||
* as well, so only the spritesheet table was wrong, and that was enough: a save
|
||||
* containing any registered spritesheet could not be read back. The roundtrip
|
||||
* test passed because empty registries write nothing but the zeroed sentinel.
|
||||
*
|
||||
* One constant per table, used on both sides, so the two cannot drift again.
|
||||
*/
|
||||
#define AKGL_GAME_SAVE_ACTOR_NAME_WIDTH AKGL_ACTOR_MAX_NAME_LENGTH
|
||||
#define AKGL_GAME_SAVE_SPRITE_NAME_WIDTH AKGL_SPRITE_MAX_NAME_LENGTH
|
||||
#define AKGL_GAME_SAVE_SPRITESHEET_NAME_WIDTH AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH
|
||||
#define AKGL_GAME_SAVE_CHARACTER_NAME_WIDTH AKGL_CHARACTER_MAX_NAME_LENGTH
|
||||
|
||||
/**
|
||||
* @brief Widest name field any of the save tables writes.
|
||||
*
|
||||
* The spritesheet table is the widest at #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH;
|
||||
* the other three are name lengths half that or less.
|
||||
* The spritesheet table is the widest; the other three are half that or less.
|
||||
*/
|
||||
#define AKGL_GAME_SAVE_MAX_NAME_FIELD AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH
|
||||
#define AKGL_GAME_SAVE_MAX_NAME_FIELD AKGL_GAME_SAVE_SPRITESHEET_NAME_WIDTH
|
||||
|
||||
/**
|
||||
* @brief Fail the build if a table ever declares a field wider than the staging buffer.
|
||||
@@ -66,10 +88,10 @@ static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmem
|
||||
* and turn back into the overread this buffer exists to prevent.
|
||||
*/
|
||||
typedef char akgl_game_save_name_field_fits[
|
||||
((AKGL_ACTOR_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_CHARACTER_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];
|
||||
((AKGL_GAME_SAVE_ACTOR_NAME_WIDTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_GAME_SAVE_SPRITE_NAME_WIDTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_GAME_SAVE_SPRITESHEET_NAME_WIDTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_GAME_SAVE_CHARACTER_NAME_WIDTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];
|
||||
|
||||
/**
|
||||
* @brief Write a registry key as a fixed-width, zero-padded field.
|
||||
@@ -299,7 +321,7 @@ static void save_actorname_iterator(void *userdata, SDL_PropertiesID props, cons
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
CATCH(errctx, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
CATCH(errctx, write_name_field(name, AKGL_GAME_SAVE_ACTOR_NAME_WIDTH, fp));
|
||||
actor = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(errctx, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
|
||||
} CLEANUP {
|
||||
@@ -328,7 +350,7 @@ static void save_spritename_iterator(void *userdata, SDL_PropertiesID props, con
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
sprite = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(errctx, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(errctx, write_name_field(name, AKGL_GAME_SAVE_SPRITE_NAME_WIDTH, fp));
|
||||
CATCH(errctx, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -360,7 +382,7 @@ static void save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
spritesheet = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(errctx, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(errctx, write_name_field(name, AKGL_GAME_SAVE_SPRITESHEET_NAME_WIDTH, fp));
|
||||
CATCH(errctx, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -388,7 +410,7 @@ static void save_charactername_iterator(void *userdata, SDL_PropertiesID props,
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
character = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(errctx, write_name_field(name, AKGL_CHARACTER_MAX_NAME_LENGTH, fp));
|
||||
CATCH(errctx, write_name_field(name, AKGL_GAME_SAVE_CHARACTER_NAME_WIDTH, fp));
|
||||
CATCH(errctx, write_exact(&character, 1, sizeof(akgl_Character *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -619,16 +641,34 @@ akerr_ErrorContext *akgl_game_load(char *fpath)
|
||||
memcpy((void *)&akgl_game, (void *)&savegame, sizeof(akgl_Game));
|
||||
// Load actor name map
|
||||
actormap = SDL_CreateProperties();
|
||||
CATCH(errctx, load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
|
||||
CATCH(errctx, load_objectnamemap(fp, actormap, AKGL_GAME_SAVE_ACTOR_NAME_WIDTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
|
||||
// Load sprite name map
|
||||
spritemap = SDL_CreateProperties();
|
||||
CATCH(errctx, load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
|
||||
CATCH(errctx, load_objectnamemap(fp, spritemap, AKGL_GAME_SAVE_SPRITE_NAME_WIDTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
|
||||
// Load spritesheet name map
|
||||
spritesheetmap = SDL_CreateProperties();
|
||||
CATCH(errctx, load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
|
||||
CATCH(errctx, load_objectnamemap(fp, spritesheetmap, AKGL_GAME_SAVE_SPRITESHEET_NAME_WIDTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
|
||||
// Load character name map
|
||||
charactermap = SDL_CreateProperties();
|
||||
CATCH(errctx, load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
|
||||
CATCH(errctx, load_objectnamemap(fp, charactermap, AKGL_GAME_SAVE_CHARACTER_NAME_WIDTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
|
||||
|
||||
// The four tables are the whole file, so the stream has to be at its end
|
||||
// now. Nothing else checks: the tables carry no length prefix and end at
|
||||
// a zeroed sentinel, so a reader whose field width disagrees with the
|
||||
// writer's does not run off anything -- it finds a run of zeros
|
||||
// somewhere inside an entry, stops early, and reports success with
|
||||
// silently wrong maps. This is what makes that disagreement an error
|
||||
// rather than a corruption, and it is the check that would have caught
|
||||
// the spritesheet width being wrong for however long it was.
|
||||
//
|
||||
// It also has to move once the objects themselves are written; the
|
||||
// comment below is the marker for that.
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(fgetc(fp) != EOF),
|
||||
AKERR_IO,
|
||||
"Savegame has data left after its name tables; a name field width does not match the writer's");
|
||||
|
||||
// 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 ) {
|
||||
|
||||
@@ -186,7 +186,17 @@ akerr_ErrorContext *akgl_get_json_with_default(akerr_ErrorContext *e, void *defv
|
||||
ATTEMPT {
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
// All three arms fall into the memcpy: HANDLE_GROUP emits no `break`,
|
||||
// so a new status has to be added *above* the arm holding the body, not
|
||||
// below it.
|
||||
//
|
||||
// AKERR_OUTOFBOUNDS is what akgl_get_json_array_index_object, _integer
|
||||
// and _string report for a short array. Without it here, "this element
|
||||
// is optional" worked for an object member and silently did not work
|
||||
// for an array element -- the error propagated instead of being
|
||||
// replaced by the default.
|
||||
} HANDLE_GROUP(e, AKERR_KEY) {
|
||||
} HANDLE_GROUP(e, AKERR_OUTOFBOUNDS) {
|
||||
} HANDLE_GROUP(e, AKERR_INDEX) {
|
||||
memcpy(dest, defval, defsize);
|
||||
} FINISH(e, true);
|
||||
|
||||
@@ -102,6 +102,15 @@ akerr_ErrorContext *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color
|
||||
FAIL_ZERO_RETURN(errctx, akgl_renderer, AKERR_NULLPOINTER, "No renderer backend");
|
||||
FAIL_ZERO_RETURN(errctx, akgl_renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, akgl_renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
|
||||
// Drawing nothing is not a failure. SDL_ttf returns NULL with "Text has
|
||||
// zero width" from both rasterizers for "", so this used to report
|
||||
// AKERR_NULLPOINTER for what a caller means as "draw an empty line" --
|
||||
// while akgl_text_measure("") is documented as legal and returns 0 wide by
|
||||
// one line high. Two halves of one header disagreeing about one string.
|
||||
if ( text[0] == '\0' ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
if ( wraplength > 0 ) {
|
||||
textsurf = TTF_RenderText_Blended_Wrapped(
|
||||
font,
|
||||
|
||||
Reference in New Issue
Block a user