Drop the savegame name-width aliases

They were four #defines, one per table, each expanding to exactly one existing
constant and used exactly once per side. That is indirection with no second
consumer, and this repository's own rule is to abstract when the second
consumer is real and not before.

The reasoning behind them does not survive contact either. They were meant to
let the on-disk format's field widths diverge from the object model's -- but
raising AKGL_ACTOR_MAX_NAME_LENGTH is an ABI change, which bumps the version,
and akgl_game_load refuses a save whose libversion does not match before it
reads a single name table. The divergence they anticipated cannot happen
quietly.

The reader now names the same constant the writer does, per table, which is all
the fix ever needed to be. What actually catches a disagreement is the EOF
check at the end of akgl_game_load, and that works however the widths are
spelled: reverting the spritesheet reader to the wrong constant still fails the
roundtrip test with AKERR_IO.

25/25 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 07:23:57 -04:00
parent 28fa929f6a
commit 58b99e0477
2 changed files with 46 additions and 44 deletions

View File

@@ -51,34 +51,27 @@ 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 four savegame name tables are written and read at each object's own
* maximum name length -- AKGL_ACTOR_MAX_NAME_LENGTH for actors,
* AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH for spritesheets, and so on.
*
* 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.
* **The writer and the reader have to name the same constant per table.** The
* tables carry no length prefix, so the reader finds each next entry by
* stepping that many bytes. They disagreed until 0.5.0: the writer used each
* object's own length and the reader used AKGL_ACTOR_MAX_NAME_LENGTH for all
* four. The other three are 128 as well, so only the spritesheet table was
* wrong -- and that was enough. See the EOF check at the end of akgl_game_load,
* which is what turns a disagreement into an error instead of a corruption.
*/
#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; the other three are half that or less.
* The spritesheet table is the widest at #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH;
* the other three are name lengths half that or less.
*/
#define AKGL_GAME_SAVE_MAX_NAME_FIELD AKGL_GAME_SAVE_SPRITESHEET_NAME_WIDTH
#define AKGL_GAME_SAVE_MAX_NAME_FIELD AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH
/**
* @brief Fail the build if a table ever declares a field wider than the staging buffer.
@@ -88,10 +81,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_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];
((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];
/**
* @brief Write a registry key as a fixed-width, zero-padded field.
@@ -321,7 +314,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_GAME_SAVE_ACTOR_NAME_WIDTH, fp));
CATCH(errctx, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
actor = SDL_GetPointerProperty(props, name, NULL);
CATCH(errctx, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
} CLEANUP {
@@ -350,7 +343,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_GAME_SAVE_SPRITE_NAME_WIDTH, fp));
CATCH(errctx, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
CATCH(errctx, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
} CLEANUP {
} PROCESS(errctx) {
@@ -382,7 +375,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_GAME_SAVE_SPRITESHEET_NAME_WIDTH, fp));
CATCH(errctx, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
CATCH(errctx, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
} CLEANUP {
} PROCESS(errctx) {
@@ -410,7 +403,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_GAME_SAVE_CHARACTER_NAME_WIDTH, fp));
CATCH(errctx, write_name_field(name, AKGL_CHARACTER_MAX_NAME_LENGTH, fp));
CATCH(errctx, write_exact(&character, 1, sizeof(akgl_Character *), fp));
} CLEANUP {
} PROCESS(errctx) {
@@ -641,16 +634,16 @@ 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_GAME_SAVE_ACTOR_NAME_WIDTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
CATCH(errctx, load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
// Load sprite name map
spritemap = SDL_CreateProperties();
CATCH(errctx, load_objectnamemap(fp, spritemap, AKGL_GAME_SAVE_SPRITE_NAME_WIDTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
CATCH(errctx, load_objectnamemap(fp, spritemap, AKGL_SPRITE_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
// Load spritesheet name map
spritesheetmap = SDL_CreateProperties();
CATCH(errctx, load_objectnamemap(fp, spritesheetmap, AKGL_GAME_SAVE_SPRITESHEET_NAME_WIDTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
CATCH(errctx, load_objectnamemap(fp, spritesheetmap, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
// Load character name map
charactermap = SDL_CreateProperties();
CATCH(errctx, load_objectnamemap(fp, charactermap, AKGL_GAME_SAVE_CHARACTER_NAME_WIDTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
CATCH(errctx, load_objectnamemap(fp, charactermap, AKGL_CHARACTER_MAX_NAME_LENGTH, 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