Fix the build after archiving rpg/ui/scene: drop their last live dependents

rpg/ui/scene moved to archive/ earlier left a handful of otherwise-generic
systems with dead references into them:
- asset/loader/chunk existed solely to load rpg overworld map chunks
  (tile_t, worldpos_t) - deleted outright, along with its
  ASSET_LOADER_TYPE_CHUNK entry in assetloader.c/h.
- save/slot wrapped party_t/storyflagval_t/map-name/cutscene-path, which is
  100% rpg save data - deleted outright (save.c/h, savedevice.c/h and the
  Linux save backend keep their generic settings/device-discovery logic,
  just lose the slot read/write pieces that only existed for it).
- locale/localemanager.c and test/CMakeLists.txt/test/save/* had a few
  stray includes and test cases that only existed for the now-gone code.

Also restores assets/locale/en_US.po (copied back from archive/assets/) -
locale init still needs a real locale file to boot into, even with the
asset-moving/archiving done separately.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
YourWishesandClaude Sonnet 5 committed 2026-10-15 09:08:02 -05:00
1 parent f6b884bf7f
commit fdfdfa3a66
31 files changed
+292 -1994

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: en\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
# Initial Scene
msgid "initial.checking_save.title"
msgstr "Checking for save data"
msgid "initial.checking_save.message"
msgstr "Please wait..."
msgid "initial.no_device.title"
msgstr "No Save Device Found"
msgid "initial.no_device.message"
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
msgid "initial.no_device.retry"
msgstr "Try again"
msgid "initial.no_device.continue"
msgstr "Continue without saving"
# Main Menu Scene
msgid "main_menu.start_game"
msgstr "Start Game"
msgid "main_menu.options"
msgstr "Options"
msgid "main_menu.quit"
msgstr "Quit Game"
msgid "main_menu.quit_confirm"
msgstr "Are you sure you want to quit?"
msgid "main_menu.checking_save.title"
msgstr "Checking for save data"
msgid "main_menu.checking_save.message"
msgstr "Please wait..."
msgid "main_menu.no_device.title"
msgstr "No Save Device Found"
msgid "main_menu.no_device.message"
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
msgid "main_menu.no_device.retry"
msgstr "Try again"
msgid "main_menu.no_device.continue"
msgstr "Continue without saving"
msgid "main_menu.save_load_error.title"
msgstr "Error"
msgid "main_menu.save_load_error.message"
msgstr "Failed to load save data. Please try again."
msgid "main_menu.save_load_error.retry"
msgstr "Try Again"
# Select Save Screen
msgid "ui.select_save.title"
msgstr "Select Save"
msgid "ui.select_save.empty"
msgstr "Empty Slot"
msgid "ui.select_save.corrupt"
msgstr "Corrupted Save"
msgid "ui.select_save.corrupt_warning"
msgstr "This save file is corrupted and could not be loaded. Please contact support if this issue persists."
msgid "ui.select_save.slot_format"
msgstr "%s Lv.%d %s"
msgid "ui.select_save.delete_mode"
msgstr "Delete a Save"
msgid "ui.select_save.delete_confirm"
msgstr "Are you sure you want to delete this save?"
msgid "ui.select_save.name_title"
msgstr "Enter game save file name"
msgid "ui.save_slot.number_format"
msgstr "Slot %d"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"Welcome"
msgid "save.linux.mkdirp_failed"
msgstr "Failed to create save directory, check the disk is not full or write-protected."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Input"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Display"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Language"
msgid "ui.settings.general.language_detail"
msgstr "Takes effect after restarting the application."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Apply"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "Discard unsaved changes?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Characters"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Items"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Settings"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Save"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "Game saved."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "Save cancelled."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "Can't save - no save device found."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "This session is temporary - no save device was found, so saving is disabled."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "No save data found. Create a new save?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "Save failed: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "Can't save: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "No save device found. You can continue, but\nprogress will not be saved."
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "Retry"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "Continue Anyway"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "No save data found. Create a new save?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "Yes"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "No"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "Confirm"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "Cancel"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "Attack"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "Flee"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "Enemy %u (%u/%u HP)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "HP %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "MP %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "No audio settings yet"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "No display settings yet"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "No input settings yet"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "Category %u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "loading"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "SAVING"
#
# Items
#
msgid "item.potion.name"
msgstr "Potion"
msgid "item.potato.name"
msgstr "Potato"
msgid "item.apple.name"
msgstr "Apple"
#
# Moves
#
msgid "move.fire.name"
msgstr "Fire"
msgid "move.slash.name"
msgstr "Slash"
-1
View File
@@ -15,7 +15,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(chunk)
add_subdirectory(dmf)
add_subdirectory(wav)
add_subdirectory(mp3)
-6
View File
@@ -46,12 +46,6 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.dispose = assetJsonDispose
},
[ASSET_LOADER_TYPE_CHUNK] = {
.loadSync = assetChunkLoaderSync,
.loadAsync = assetChunkLoaderAsync,
.dispose = assetChunkDispose
},
[ASSET_LOADER_TYPE_WAV] = {
.loadSync = assetWavLoaderSync,
.loadAsync = assetWavLoaderAsync,
-5
View File
@@ -12,7 +12,6 @@
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/chunk/assetchunkloader.h"
#include "asset/loader/wav/assetwavloader.h"
#include "asset/loader/mp3/assetmp3loader.h"
@@ -25,7 +24,6 @@ typedef enum {
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_CHUNK,
ASSET_LOADER_TYPE_WAV,
ASSET_LOADER_TYPE_MP3,
@@ -39,7 +37,6 @@ typedef union {
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
assetchunkloaderloading_t chunk;
assetwavloaderloading_t wav;
assetmp3loaderloading_t mp3;
} assetloaderloading_t;
@@ -51,7 +48,6 @@ typedef union {
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
assetchunkoutput_t chunk;
assetwavoutput_t wav;
assetmp3output_t mp3;
} assetloaderoutput_t;
@@ -61,7 +57,6 @@ typedef union {
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
assetchunkloaderinput_t chunk;
assetwavloaderinput_t wav;
assetmp3loaderinput_t mp3;
} assetloaderinput_t;
@@ -1,10 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetchunkloader.c
)
@@ -1,484 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetchunkloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "asset/asset.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "display/color.h"
#include "display/texture/texture.h"
#include "rpg/overworld/worldpos.h"
#include "yyjson.h"
#define ASSET_CHUNK_TERRAIN_TEXTURE "tiles.png"
// Per-shape corner Z offsets (sw, se, ne, nw) as offsets from a tile's own
// base Z, ported 1:1 from tools/asset/chunk/__main__.py's _RAMP_CORNERS.
// TILE_SHAPE_NULL's row is never read - assetChunkTilesHaveTerrain/
// assetChunkBuildTerrain skip those tiles outright.
static const float_t CHUNK_RAMP_CORNERS[TILE_SHAPE_COUNT][4] = {
[TILE_SHAPE_GROUND] = {0.0f, 0.0f, 0.0f, 0.0f},
[TILE_SHAPE_RAMP_NORTH] = {0.0f, 0.0f, 1.0f, 1.0f},
[TILE_SHAPE_RAMP_SOUTH] = {1.0f, 1.0f, 0.0f, 0.0f},
[TILE_SHAPE_RAMP_EAST] = {0.0f, 1.0f, 1.0f, 0.0f},
[TILE_SHAPE_RAMP_WEST] = {1.0f, 0.0f, 0.0f, 1.0f},
[TILE_SHAPE_RAMP_NORTHEAST] = {0.0f, 0.0f, 1.0f, 0.0f},
[TILE_SHAPE_RAMP_NORTHWEST] = {0.0f, 0.0f, 0.0f, 1.0f},
[TILE_SHAPE_RAMP_SOUTHEAST] = {0.0f, 1.0f, 0.0f, 0.0f},
[TILE_SHAPE_RAMP_SOUTHWEST] = {1.0f, 0.0f, 0.0f, 0.0f},
[TILE_SHAPE_RAMP_NORTHEAST_INNER] = {0.0f, 1.0f, 1.0f, 1.0f},
[TILE_SHAPE_RAMP_NORTHWEST_INNER] = {1.0f, 0.0f, 1.0f, 1.0f},
[TILE_SHAPE_RAMP_SOUTHEAST_INNER] = {1.0f, 1.0f, 1.0f, 0.0f},
[TILE_SHAPE_RAMP_SOUTHWEST_INNER] = {1.0f, 1.0f, 0.0f, 1.0f},
};
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
errorOk();
}
static bool_t assetChunkTilesHaveTerrain(const tile_t *tiles) {
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
if(tiles[i].shape != TILE_SHAPE_NULL) return true;
}
return false;
}
// Reads the "tiles" array - a dense, fixed-size CHUNK_TILE_COUNT grid,
// one slot per (x, y) column at index x + y * CHUNK_WIDTH (position is
// implicit in the index, not stored). Each slot is either `null` (empty
// column, already zeroed = TILE_SHAPE_NULL) or a 2-element [type, z]
// array.
static void assetChunkParseTiles(yyjson_val *root, tile_t *tiles) {
memoryZero(tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
yyjson_val *tilesArr = yyjson_obj_get(root, "tiles");
if(!yyjson_is_arr(tilesArr)) return;
size_t idx, len;
yyjson_val *tileVal;
yyjson_arr_foreach(tilesArr, idx, len, tileVal) {
if(idx >= CHUNK_TILE_COUNT) break;
if(!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) < 2) continue;
int32_t type = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 0));
int32_t z = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 1));
tiles[idx] = (tile_t){ .shape = (tileshape_t)type, .z = (uint8_t)z };
}
}
// Resolves "meshes" (external building references, by bare filename) into
// out->modelNames/meshOffsets starting at *ioIndex, advancing it as
// entries are written. Filenames are resolved as
// "models/buildings/<basename>.json" - a hardcoded stand-in for
// tools/asset/chunk/__main__.py's find_model() recursive search, valid
// as long as every building model lives flat under that one directory
// (true today - see the raw-json-maps plan notes for the tradeoff).
static void assetChunkParseMeshes(
yyjson_val *root, assetchunkoutput_t *out, uint8_t *ioIndex
) {
yyjson_val *meshesArr = yyjson_obj_get(root, "meshes");
if(!yyjson_is_arr(meshesArr)) return;
size_t idx, len;
yyjson_val *meshVal;
yyjson_arr_foreach(meshesArr, idx, len, meshVal) {
if(*ioIndex >= CHUNK_MESH_COUNT_MAX) break;
yyjson_val *fileVal = yyjson_obj_get(meshVal, "file");
if(!fileVal || !yyjson_is_str(fileVal)) continue;
char_t baseName[CHUNK_MESH_NAME_MAX];
stringCopy(baseName, yyjson_get_str(fileVal), sizeof(baseName));
char_t *dot = stringFindLastChar(baseName, '.');
if(dot != NULL) *dot = '\0';
uint8_t m = *ioIndex;
stringFormat(
out->modelNames[m], CHUNK_MESH_NAME_MAX,
"models/buildings/%s.json", baseName
);
vec3 offset = {0.0f, 0.0f, 0.0f};
yyjson_val *posArr = yyjson_obj_get(meshVal, "pos");
if(yyjson_is_arr(posArr) && yyjson_arr_size(posArr) >= 3) {
offset[0] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 0));
offset[1] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 1));
offset[2] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 2));
}
memoryCopy(out->meshOffsets[m], offset, sizeof(vec3));
(*ioIndex)++;
}
}
static void assetChunkParseEntities(yyjson_val *root, assetchunkoutput_t *out) {
out->entitySpawnCount = 0;
yyjson_val *arr = yyjson_obj_get(root, "entities");
if(!yyjson_is_arr(arr)) return;
size_t idx, len;
yyjson_val *val;
yyjson_arr_foreach(arr, idx, len, val) {
if(out->entitySpawnCount >= CHUNK_ENTITY_SPAWN_COUNT_MAX) break;
const char_t *type = yyjson_get_str(yyjson_obj_get(val, "type"));
yyjson_val *posArr = yyjson_obj_get(val, "pos");
if(type == NULL || !yyjson_is_arr(posArr) || yyjson_arr_size(posArr) < 3) {
continue;
}
worldpos_t pos = {
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 0)),
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 1)),
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 2)),
};
chunkentityspawn_t *spawn = &out->entitySpawns[out->entitySpawnCount];
if(stringEquals(type, "global")) {
spawn->kind = CHUNK_ENTITY_SPAWN_KIND_GLOBAL;
spawn->globalId =
(uint16_t)yyjson_get_int(yyjson_obj_get(val, "globalId"));
spawn->itemId = 0;
spawn->itemQuantity = 0;
} else if(stringEquals(type, "item")) {
spawn->kind = CHUNK_ENTITY_SPAWN_KIND_ITEM;
spawn->globalId = 0;
spawn->itemId = (uint16_t)yyjson_get_int(yyjson_obj_get(val, "itemId"));
spawn->itemQuantity =
(uint8_t)yyjson_get_int(yyjson_obj_get(val, "quantity"));
} else {
continue;
}
spawn->position = pos;
out->entitySpawnCount++;
}
}
static void assetChunkParseAreas(yyjson_val *root, assetchunkoutput_t *out) {
out->areaSpawnCount = 0;
yyjson_val *arr = yyjson_obj_get(root, "areas");
if(!yyjson_is_arr(arr)) return;
size_t idx, len;
yyjson_val *val;
yyjson_arr_foreach(arr, idx, len, val) {
if(out->areaSpawnCount >= CHUNK_AREA_COUNT_MAX) break;
yyjson_val *minArr = yyjson_obj_get(val, "min");
yyjson_val *maxArr = yyjson_obj_get(val, "max");
if(
!yyjson_is_arr(minArr) || yyjson_arr_size(minArr) < 3 ||
!yyjson_is_arr(maxArr) || yyjson_arr_size(maxArr) < 3
) continue;
chunkareaspawn_t *area = &out->areaSpawns[out->areaSpawnCount];
area->min = (worldpos_t){
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 0)),
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 1)),
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 2)),
};
area->max = (worldpos_t){
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 0)),
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 1)),
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 2)),
};
area->callbackId =
(uint16_t)yyjson_get_int(yyjson_obj_get(val, "callbackId"));
area->notify = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "notify"));
area->trigger = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "trigger"));
out->areaSpawnCount++;
}
}
// Six vertices (2 CCW triangles) for one tile's quad, matching
// tools/asset/chunk/__main__.py's _tile_quad. Corner order: SW=(x,y),
// SE=(x+1,y), NE=(x+1,y+1), NW=(x,y+1). NORTH=+Y, EAST=+X.
static void assetChunkAppendTileQuad(
meshvertex_t *verts, uint32_t *vertCount,
const uint32_t x, const uint32_t y,
const float_t sw, const float_t se, const float_t ne, const float_t nw
) {
const float_t u0 = (float_t)x / (float_t)CHUNK_WIDTH;
const float_t u1 = (float_t)(x + 1) / (float_t)CHUNK_WIDTH;
const float_t v0 = (float_t)y / (float_t)CHUNK_HEIGHT;
const float_t v1 = (float_t)(y + 1) / (float_t)CHUNK_HEIGHT;
const float_t fx = (float_t)x;
const float_t fy = (float_t)y;
meshvertex_t *v = verts + *vertCount;
v[0] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
v[1] = (meshvertex_t){ .uv = {u1, v0}, .pos = {fx + 1, fy, se} };
v[2] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
v[3] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
v[4] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
v[5] = (meshvertex_t){ .uv = {u0, v1}, .pos = {fx, fy + 1, nw} };
*vertCount += 6;
}
// Builds a mesh_t straight from the tile grid - one quad per non-null
// tile column, shaped by CHUNK_RAMP_CORNERS - the runtime equivalent of
// tools/asset/chunk/__main__.py's build_terrain_verts. Only called once
// assetChunkTilesHaveTerrain has confirmed at least one real tile exists.
// Mirrors assetMeshLoaderSync's meshInit/meshFlush/free-after-upload
// sequence (assetmeshloader.c) since this produces the same
// assetmeshoutput_t shape, just from generated verts instead of a file.
static errorret_t assetChunkBuildTerrain(
const tile_t *tiles, assetmeshoutput_t *out
) {
meshvertex_t *verts = memoryAllocate(
CHUNK_TILE_COUNT * 6 * sizeof(meshvertex_t)
);
uint32_t vertCount = 0;
for(uint32_t y = 0; y < CHUNK_HEIGHT; y++) {
for(uint32_t x = 0; x < CHUNK_WIDTH; x++) {
const tile_t *tile = &tiles[x + y * CHUNK_WIDTH];
if(tile->shape == TILE_SHAPE_NULL) continue;
const float_t *corners = CHUNK_RAMP_CORNERS[tile->shape];
const float_t fz = (float_t)tile->z;
assetChunkAppendTileQuad(
verts, &vertCount, x, y,
(fz + corners[0]) * WORLD_LAYER_HEIGHT,
(fz + corners[1]) * WORLD_LAYER_HEIGHT,
(fz + corners[2]) * WORLD_LAYER_HEIGHT,
(fz + corners[3]) * WORLD_LAYER_HEIGHT
);
}
}
assertTrue(vertCount > 0, "Terrain build called with no terrain tiles.");
out->vertices = verts;
errorret_t ret = meshInit(
&out->mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, (int32_t)vertCount, verts
);
if(errorIsNotOk(ret)) {
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
out->meshInitialized = true;
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) {
meshDispose(&out->mesh);
out->meshInitialized = false;
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
memoryFree(out->vertices);
out->vertices = NULL;
#endif
errorOk();
}
errorret_t assetChunkLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetchunkoutput_t *out = &loading->entry->data.chunk;
switch(loading->loading.chunk.state) {
case ASSET_CHUNK_LOADING_STATE_INITIAL:
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
case ASSET_CHUNK_LOADING_STATE_PARSE:
break;
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
while(loading->loading.chunk.modelIndex < out->meshCount) {
uint8_t m = loading->loading.chunk.modelIndex;
if(out->modelEntries[m] == NULL) {
out->modelEntries[m] = assetLock(
out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
);
assertNotNull(out->modelEntries[m], "Failed to lock model.");
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
if(out->modelEntries[m]->state == ASSET_ENTRY_STATE_ERROR) {
assetLoaderErrorThrow(
loading, "Model failed to load: %s", out->modelNames[m]
);
}
if(out->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) {
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
loading->loading.chunk.modelIndex++;
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
default:
errorOk();
}
// --- PARSE: lock+require the chunk's own JSON as a sub-asset, exactly
// like assetmodelloader.c's ASSET_MODEL_LOADING_STATE_LOCK_ASSETS does
// for its model descriptor, then read everything out of it before
// unlocking (the yyjson_doc/vals only stay valid while jsonEntry does).
char_t jsonKey[ASSET_FILE_NAME_MAX];
stringFormat(jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name);
assetloaderinput_t jsonInput;
memoryZero(&jsonInput, sizeof(jsonInput));
stringCopy(
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
);
assetentry_t *jsonEntry =
assetLock(jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput);
errorret_t jsonRet = assetRequireLoaded(jsonEntry);
if(errorIsNotOk(jsonRet)) {
assetUnlockEntry(jsonEntry);
assetLoaderErrorThrow(
loading, "Failed to load chunk JSON: %s", loading->entry->name
);
}
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
tile_t tiles[CHUNK_TILE_COUNT];
assetChunkParseTiles(root, tiles);
out->hasTerrain = assetChunkTilesHaveTerrain(tiles);
uint8_t meshIndex = out->hasTerrain ? 1 : 0;
assetChunkParseMeshes(root, out, &meshIndex);
out->meshCount = meshIndex;
assetChunkParseEntities(root, out);
assetChunkParseAreas(root, out);
assetUnlockEntry(jsonEntry);
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
memoryCopy(out->tiles, tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
// Terrain: build the mesh, then wrap it in synthetic (non-cache,
// non-refcounted) mesh/model entries so modelEntries[0] behaves exactly
// like a real file-backed model entry for every downstream consumer -
// see the header's doc comment on hasTerrain/terrainMeshEntry.
if(out->hasTerrain) {
assetmeshoutput_t terrainMesh;
memoryZero(&terrainMesh, sizeof(terrainMesh));
errorret_t terrainRet = assetChunkBuildTerrain(out->tiles, &terrainMesh);
if(errorIsNotOk(terrainRet)) {
memoryFree(out->tiles);
out->tiles = NULL;
assetLoaderErrorThrow(
loading, "Failed to build chunk terrain mesh: %s",
loading->entry->name
);
}
assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
assetentry_t *texEntry = assetLock(
ASSET_CHUNK_TERRAIN_TEXTURE, ASSET_LOADER_TYPE_TEXTURE, &texInput
);
errorret_t texRet = assetRequireLoaded(texEntry);
if(errorIsNotOk(texRet)) {
assetUnlockEntry(texEntry);
meshDispose(&terrainMesh.mesh);
memoryFree(out->tiles);
out->tiles = NULL;
assetLoaderErrorThrow(
loading, "Failed to load chunk terrain texture: %s",
loading->entry->name
);
}
out->terrainTexEntry = texEntry;
out->terrainMeshEntry = memoryAllocate(sizeof(assetentry_t));
memoryZero(out->terrainMeshEntry, sizeof(assetentry_t));
out->terrainMeshEntry->type = ASSET_LOADER_TYPE_MESH;
out->terrainMeshEntry->state = ASSET_ENTRY_STATE_LOADED;
out->terrainMeshEntry->data.mesh = terrainMesh;
out->terrainModelEntry = memoryAllocate(sizeof(assetentry_t));
memoryZero(out->terrainModelEntry, sizeof(assetentry_t));
out->terrainModelEntry->type = ASSET_LOADER_TYPE_MODEL;
out->terrainModelEntry->state = ASSET_ENTRY_STATE_LOADED;
out->terrainModelEntry->data.model.meshEntry = out->terrainMeshEntry;
out->terrainModelEntry->data.model.texEntry = out->terrainTexEntry;
out->terrainModelEntry->data.model.color = COLOR_WHITE;
out->modelEntries[0] = out->terrainModelEntry;
}
if(out->meshCount == 0) {
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
loading->loading.chunk.modelIndex = out->hasTerrain ? 1 : 0;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetChunkDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetchunkoutput_t *out = &entry->data.chunk;
if(out->tiles != NULL) {
memoryFree(out->tiles);
out->tiles = NULL;
}
for(uint8_t m = 0; m < out->meshCount; m++) {
if(out->modelEntries[m] == NULL) continue;
if(out->hasTerrain && m == 0) continue; // synthetic - freed below.
assetUnlockEntry(out->modelEntries[m]);
out->modelEntries[m] = NULL;
}
if(out->hasTerrain) {
if(out->terrainMeshEntry != NULL) {
assetmeshoutput_t *meshOut = &out->terrainMeshEntry->data.mesh;
if(meshOut->meshInitialized) {
meshDispose(&meshOut->mesh);
meshOut->meshInitialized = false;
}
if(meshOut->vertices != NULL) {
memoryFree(meshOut->vertices);
meshOut->vertices = NULL;
}
memoryFree(out->terrainMeshEntry);
out->terrainMeshEntry = NULL;
}
if(out->terrainModelEntry != NULL) {
memoryFree(out->terrainModelEntry);
out->terrainModelEntry = NULL;
}
if(out->terrainTexEntry != NULL) {
assetUnlockEntry(out->terrainTexEntry);
out->terrainTexEntry = NULL;
}
out->modelEntries[0] = NULL;
out->hasTerrain = false;
}
errorOk();
}
@@ -1,120 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "rpg/overworld/chunk.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct {
void *nothing;
} assetchunkloaderinput_t;
typedef enum {
ASSET_CHUNK_LOADING_STATE_INITIAL,
ASSET_CHUNK_LOADING_STATE_PARSE,
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
ASSET_CHUNK_LOADING_STATE_DONE
} assetchunkloadingstate_t;
typedef struct {
assetchunkloadingstate_t state;
// Index into assetchunkoutput_t.modelNames/modelEntries the external
// (non-terrain) model polling loop has reached - see
// ASSET_CHUNK_LOADING_STATE_LOAD_MODELS.
uint8_t modelIndex;
} assetchunkloaderloading_t;
typedef enum {
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
CHUNK_ENTITY_SPAWN_KIND_ITEM
} chunkentityspawnkind_t;
typedef struct {
chunkentityspawnkind_t kind;
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
worldpos_t position;
} chunkentityspawn_t;
typedef struct {
worldpos_t min;
worldpos_t max;
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
uint8_t notify;
uint8_t trigger;
} chunkareaspawn_t;
typedef struct {
tile_t *tiles;
uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
// Terrain mesh generated at load time from the tile grid (no baked
// .dmf/.json - see assetChunkBuildTerrain in assetchunkloader.c). NULL
// (with hasTerrain false) for a chunk with no non-null tiles, in which
// case modelEntries[0] (if meshCount > 0) is the first external mesh
// instead - matches the old baked pipeline's "only write a terrain
// model if it has vertices" behavior.
//
// terrainMeshEntry/terrainModelEntry are heap-allocated, plain
// assetentry_t values that are never passed to assetLock/assetGetEntry/
// assetUnlockEntry (only a real, name-keyed, ref-counted entry may be)
// - they exist purely so modelEntries[0] can point at something with
// the same ->state/->data.model/->data.mesh.mesh shape every other
// model/mesh entry has, so chunk.c/map.c/sceneoverworld.c don't need to
// know terrain isn't file-backed. Freed directly (not assetUnlockEntry)
// in assetChunkDispose. terrainTexEntry is a real, ref-counted texture
// entry (the shared "tiles.png") and must be assetUnlockEntry'd there.
bool_t hasTerrain;
assetentry_t *terrainMeshEntry;
assetentry_t *terrainModelEntry;
assetentry_t *terrainTexEntry;
uint8_t entitySpawnCount;
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
uint8_t areaSpawnCount;
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
} assetchunkoutput_t;
/**
* Async loader stub for chunk assets. Chunk loading is performed entirely
* on the main thread (it delegates to the JSON asset loader and to
* meshInit/meshFlush, both main-thread-only); this callback is never
* reached.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for chunk assets. Locks the chunk's JSON descriptor
* as a sub-asset, parses tiles/external mesh references/entity spawns/
* area spawns, generates the terrain mesh directly from the tile grid,
* and locks/waits for every referenced model (and the terrain's shared
* tileset texture) to finish loading.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetChunkLoaderSync(assetloading_t *loading);
/**
* Disposer for chunk assets.
*
* @param entry Asset entry containing the chunk data to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetChunkDispose(assetentry_t *entry);
-4
View File
@@ -9,7 +9,6 @@
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include "ui/ui.h"
#include "system/system.h"
#include "console/console.h"
@@ -39,9 +38,6 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry));
// TODO : Trigger UI update.
errorChain(uiUpdateTranslations());
errorOk();
}
-1
View File
@@ -11,5 +11,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
)
# Subdirs
add_subdirectory(slot)
add_subdirectory(settings)
+1 -80
View File
@@ -8,7 +8,6 @@
#include "save.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "console/console.h"
save_t SAVE;
@@ -17,16 +16,9 @@ errorret_t saveInit() {
memoryZero(&SAVE, sizeof(save_t));
SAVE.deviceCurrent = 0xFF;// No current device.
SAVE.slotCurrent = 0xFF;// No current slot.
// Initialize the slots and settings
// Initialize the settings
saveSettingsInit(&SAVE.settings);
errorChain(saveSlotInit(&SAVE.slot));
// Update caches to match default data.
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
SAVE.caches[i] = SAVE.slot.cachedData;
}
// Start by initializing each of the save devices.
savedevice_t *device = &SAVE.devices[0];
@@ -67,10 +59,6 @@ errorret_t saveUpdate() {
} else {
// Device was found, cache in the data.
errorChain(saveLoadSettings());
errorChain(saveLoadAllSlots());
// Reset slot, implying no slot has been selected.
SAVE.slotCurrent = 0xFF;
// TEST: write settings back out immediately once a device is
// selected, to confirm the write path actually works on hardware.
@@ -166,10 +154,6 @@ void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user) {
SAVE.deviceCurrent = (uint8_t)(device - &SAVE.devices[0]);
}
bool_t saveCanSave() {
return CUTSCENE_SYSTEM.scene == NULL;
}
errorret_t saveSaveSettings() {
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
@@ -204,69 +188,6 @@ errorret_t saveLoadSettings() {
errorOk();
}
errorret_t saveSaveSlot() {
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
assertTrue(SAVE.slotCurrent < SAVE_SLOT_COUNT, "Invalid slot index");
errorChain(saveDeviceSlotWrite(
&SAVE.devices[SAVE.deviceCurrent],
&SAVE.slot,
SAVE.slotCurrent
));
// Update cache
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
SAVE.slotDirty = false;
errorOk();
}
errorret_t saveLoadSlot() {
assertTrue(SAVE.slotCurrent < SAVE_SLOT_COUNT, "Invalid slot index");
if(SAVE.deviceCurrent == 0xFF) {
// No save device available, so we assume an empty slot.
errorChain(saveSlotInit(&SAVE.slot));
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
errorOk();
}
errorret_t ret = saveDeviceSlotRead(
&SAVE.devices[SAVE.deviceCurrent],
&SAVE.slot,
SAVE.slotCurrent
);
if(errorIsNotOk(ret)) {
// Corrupt or incompatible (e.g. wrong/missing schema version) slot
// file - don't take the whole game down over it, treat it the same as
// an empty/unused slot.
errorCatch(errorPrint(ret));
consolePrint(
"Save slot %u could not be loaded, treating as empty.\n",
(uint32_t)SAVE.slotCurrent
);
errorChain(saveSlotInit(&SAVE.slot));
SAVE.slot.cachedData.corrupt = true;
}
// Update cache
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
SAVE.slotDirty = false;
errorOk();
}
errorret_t saveLoadAllSlots() {
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
SAVE.slotCurrent = i;
errorChain(saveSlotInit(&SAVE.slot));
errorChain(saveLoadSlot());// Load slot updates the cache.
}
errorOk();
}
errorret_t saveDispose() {
// Dispose each device.
savedevice_t *device = &SAVE.devices[0];
-37
View File
@@ -7,18 +7,12 @@
#pragma once
#include "savedevice.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
typedef struct {
// File state
savesettings_t settings;
saveslotcache_t caches[SAVE_SLOT_COUNT];
saveslot_t slot;
uint8_t slotCurrent;
bool_t settingsDirty;
bool_t slotDirty;
// Device state
savedevice_t devices[SAVE_DEVICE_COUNT];
@@ -67,14 +61,6 @@ void saveFindAvailableDevice(
*/
void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user);
/**
* Whether the game is in a state where saving is currently allowed.
* Currently just checks that no cutscene is running.
*
* @return True if saving is currently allowed, false otherwise.
*/
bool_t saveCanSave();
/**
* Saves the current settings to the current device.
*
@@ -89,29 +75,6 @@ errorret_t saveSaveSettings();
*/
errorret_t saveLoadSettings();
/**
* Saves the current slot to the current device, at the current slot index.
*
* @return Error state if any.
*/
errorret_t saveSaveSlot();
/**
* Loads the current slot from the current device, at the current slot index.
*
* @return Error state if any.
*/
errorret_t saveLoadSlot();
/**
* Resets and loads every slot from the current device, updating the caches
* along the way. Leaves SAVE.slotCurrent pointing at the last slot index
* once done.
*
* @return Error state if any.
*/
errorret_t saveLoadAllSlots();
/**
* Disposes of the save system.
*
-34
View File
@@ -473,40 +473,6 @@ errorret_t saveDeviceRawFetchItem(
#endif// defined(SAVE_DEVICE_DATA_RAW)
errorret_t saveDeviceSlotWrite(
savedevice_t *device,
saveslot_t *slot,
const uint8_t slotIndex
) {
assertNotNull(device, "device cannot be null");
assertNotNull(slot, "slot cannot be null");
#if defined(SAVE_DEVICE_DATA_RAW)
errorChain(saveDeviceRawStoreItem(device, NULL, slot, slotIndex));
#elif defined(saveDeviceSlotWritePlatform)
errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex));
#endif
errorOk();
}
errorret_t saveDeviceSlotRead(
savedevice_t *device,
saveslot_t *slot,
const uint8_t slotIndex
) {
assertNotNull(device, "device cannot be null");
assertNotNull(slot, "slot cannot be null");
#if defined(SAVE_DEVICE_DATA_RAW)
errorChain(saveDeviceRawFetchItem(device, NULL, slot, slotIndex));
#elif defined(saveDeviceSlotReadPlatform)
errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex));
#endif
errorOk();
}
errorret_t saveDeviceSettingsWrite(
savedevice_t *device,
savesettings_t *settings
-29
View File
@@ -44,7 +44,6 @@
typedef struct savedevice_s savedevice_t;
typedef struct savesettings_s savesettings_t;
typedef struct saveslot_s saveslot_t;
typedef void (*savedevicestatecallback_t)(savedevice_t *device, void *user);
@@ -102,34 +101,6 @@ void saveDeviceCheckAvailability(
void *user
);
/**
* Writes the given save slot to the device at the given slot index.
*
* @param device The save device to write to.
* @param slot The save slot to write.
* @param slotIndex The slot index to write to.
* @return Error state if any.
*/
errorret_t saveDeviceSlotWrite(
savedevice_t *device,
saveslot_t *slot,
const uint8_t slotIndex
);
/**
* Reads a save slot from the device at the given slot index.
*
* @param device The save device to read from.
* @param slot The save slot to read into.
* @param slotIndex The slot index to read from.
* @return Error state if any.
*/
errorret_t saveDeviceSlotRead(
savedevice_t *device,
saveslot_t *slot,
const uint8_t slotIndex
);
/**
* Writes the given save settings to the device.
*
-12
View File
@@ -1,12 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
saveslot.c
saveslotcurrent.c
saveslotver1.c
)
-92
View File
@@ -1,92 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "saveslot.h"
#include "save/slot/saveslotcurrent.h"
#include "save/save.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
errorret_t saveSlotInit(saveslot_t *slot) {
assertNotNull(slot, "Slot cannot be null");
// Zero the save slot.
memoryZero(slot, sizeof(saveslot_t));
slot->version = 1;
// Set the default story flags.
for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) {
slot->storyFlags[flag] = STORY_FLAG_MAP[flag].initial;
}
// Set the default party.
errorChain(partyInit(&slot->party));
// Set the default current cutscene.
stringCopy(
slot->currentCutscene, SAVE_SLOT_CUTSCENE_DEFAULT,
sizeof(slot->currentCutscene) - 1
);
errorOk();
}
errorret_t saveSlotNewGame(void) {
errorChain(saveSlotInit(&SAVE.slot));
stringCopy(
SAVE.slot.cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT,
sizeof(SAVE.slot.cachedData.mapName)
);
errorOk();
}
void saveSlotSetCurrentCutscene(const char_t *file) {
assertNotNull(file, "File cannot be null");
stringCopy(
SAVE.slot.currentCutscene, file, sizeof(SAVE.slot.currentCutscene) - 1
);
SAVE.slotDirty = true;
}
bool_t saveSlotInUse(saveslotcache_t *slot) {
assertNotNull(slot, "Slot cannot be null");
return slot->name[0] != '\0';
}
bool_t saveSlotHasSaved(saveslotcache_t *slot) {
assertNotNull(slot, "Slot cannot be null");
return slot->time.time != 0;
}
errorret_t saveSlotWriteJSON(
saveslot_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
errorChain(saveSlotCurrentWriteJSON(slot, doc, object));
errorOk();
}
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(object, "Object cannot be null");
// saveSlotInit() now takes slot directly, so this can reset through it
// rather than duplicating the memSet/version stub - saveSlotCurrent
// ReadJSON() fully overwrites every field it sets anyway (including
// party.members/order), so seeding a fresh party.jsonc roster here only
// to replace it is wasted work, but keeps this in one reset path rather
// than two subtly different ones.
errorChain(saveSlotInit(slot));
errorChain(saveSlotCurrentReadJSON(slot, object));
errorOk();
}
-117
View File
@@ -1,117 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savedevice.h"
#include "rpg/overworld/map.h"
#include "rpg/battle/party.h"
#include "rpg/story/story.h"
#include "time/timeepoch.h"
#include "asset/assetfile.h"
#include "yyjson.h"
#define SAVE_SLOT_NAME_LENGTH 8
#ifndef SAVE_SLOT_COUNT
#define SAVE_SLOT_COUNT 3
#endif
#define SAVE_SLOT_MAP_NAME_DEFAULT "overworld"
#define SAVE_SLOT_CUTSCENE_NAME_MAX ASSET_FILE_NAME_MAX
#define SAVE_SLOT_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc"
typedef struct {
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
dusktimeepoch_t time;
int32_t playerLevel;
char_t mapName[MAP_NAME_MAX];
bool_t corrupt;
} saveslotcache_t;
typedef struct saveslot_s {
uint8_t version;
uint8_t dataType;
saveslotcache_t cachedData;
party_t party;
storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX];
// Asset path of the cutscene currently in progress for this slot (e.g.
// resumed on load) - defaults to SAVE_SLOT_CUTSCENE_DEFAULT on a fresh
// slot (see saveSlotInit).
char_t currentCutscene[SAVE_SLOT_CUTSCENE_NAME_MAX];
} saveslot_t;
/**
* Inits slot with the default state, this is functionally "new game" but
* will not set the player name, as that is what we use to determine if a
* save slot is "in use" or not. Seeds the party roster from party.jsonc
* (see partyInit).
*
* @param slot The save slot to initialize.
* @return Error if the starting party roster (party.jsonc) fails to
* load/parse.
*/
errorret_t saveSlotInit(saveslot_t *slot);
/**
* Sets up the current save slot (SAVE.slot) for a brand new game: calls
* saveSlotInit (which seeds the starting party roster) and sets the
* default starting map. Use this (not saveSlotInit directly) whenever a
* new save is actually being created for a player. Callers creating a
* new save at a specific index should set SAVE.slotCurrent first.
*
* @return Error if saveSlotInit fails.
*/
errorret_t saveSlotNewGame(void);
/**
* Sets the current save slot's (SAVE.slot) in-progress cutscene and
* dirties the save (SAVE.slotDirty), same as storySetFlagValue - so the
* next save write persists it. Use this (not writing SAVE.slot.
* currentCutscene directly) anywhere gameplay needs to change it.
*
* @param file Asset path of the cutscene now in progress, or "" for none.
*/
void saveSlotSetCurrentCutscene(const char_t *file);
/**
* Checks if the save slot is in use, this is determined by checking if the
* player name is set or not.
*
* @param slot The save slot to check.
* @return True if the save slot is in use, false otherwise.
*/
bool_t saveSlotInUse(saveslotcache_t *slot);
/**
* Returns whether or not the given save slot has ever saved (has an epoc non 0)
*
* @param slot The save slot to check.
* @return True if the save slot has ever saved, false otherwise.
*/
bool_t saveSlotHasSaved(saveslotcache_t *slot);
/**
* Writes the given save slot out to the given JSON object.
*
* @param slot The save slot to write.
* @param doc The mutable JSON document that owns the object.
* @param object The mutable JSON object to write into.
* @return Error state if any.
*/
errorret_t saveSlotWriteJSON(
saveslot_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
);
/**
* Reads the given save slot in from the given JSON object.
*
* @param slot The save slot to read into.
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object);
-79
View File
@@ -1,79 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "saveslotcurrent.h"
#include "assert/assert.h"
#include "util/string.h"
#include "util/memory.h"
#include "time/time.h"
errorret_t saveSlotCurrentWriteJSON(
saveslot_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(doc, "Doc cannot be null");
assertNotNull(object, "Object cannot be null");
// Stamped on the stable struct (not the throwaway ver1 copy below) so the
// fresh timestamp survives the write - saveSlotHasSaved() depends on it.
slot->cachedData.time = timeGetEpoch();
saveslotver1_t ver1;
stringCopy(ver1.name, slot->cachedData.name, sizeof(ver1.name));
ver1.time = slot->cachedData.time;
ver1.playerLevel = slot->cachedData.playerLevel;
stringCopy(ver1.mapName, slot->cachedData.mapName, sizeof(ver1.mapName));
memoryCopy(
ver1.partyMembers, slot->party.members, sizeof(ver1.partyMembers)
);
memoryCopy(ver1.partyOrder, slot->party.order, sizeof(ver1.partyOrder));
memoryCopy(ver1.storyFlags, slot->storyFlags, sizeof(ver1.storyFlags));
stringCopy(
ver1.currentCutscene, slot->currentCutscene, sizeof(ver1.currentCutscene)
);
errorChain(saveSlotVer1WriteJSON(&ver1, doc, object));
errorOk();
}
errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(object, "Object cannot be null");
// Zeroed up front: saveSlotVer1ReadJSON returns early (leaving everything
// past "name" untouched) for a blank/never-used slot, so this must not be
// left as uninitialized stack garbage.
saveslotver1_t ver1;
memoryZero(&ver1, sizeof(ver1));
errorChain(saveSlotVer1ReadJSON(&ver1, object));
stringCopy(slot->cachedData.name, ver1.name, sizeof(slot->cachedData.name));
slot->cachedData.time = ver1.time;
slot->cachedData.playerLevel = ver1.playerLevel;
stringCopy(
slot->cachedData.mapName, ver1.mapName, sizeof(slot->cachedData.mapName)
);
memoryCopy(
slot->party.members, ver1.partyMembers, sizeof(slot->party.members)
);
memoryCopy(slot->party.order, ver1.partyOrder, sizeof(slot->party.order));
memoryCopy(slot->storyFlags, ver1.storyFlags, sizeof(slot->storyFlags));
stringCopy(
slot->currentCutscene, ver1.currentCutscene,
sizeof(slot->currentCutscene)
);
errorOk();
}
-37
View File
@@ -1,37 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/slot/saveslot.h"
#include "save/slot/saveslotver1.h"
/**
* Converts the given save slot to whichever schema SAVE_SLOT_CURRENT
* (saveslotver.h) selects and writes it out to the given JSON object.
* Update this (only) when bumping to a new SAVE_SLOT_VER_N.
*
* @param slot The save slot to write.
* @param doc The mutable JSON document that owns the object.
* @param object The mutable JSON object to write into.
* @return Error state if any.
*/
errorret_t saveSlotCurrentWriteJSON(
saveslot_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
);
/**
* Reads whichever schema SAVE_SLOT_CURRENT (saveslotver.h) selects from the
* given JSON object and converts it into the given save slot. Update this
* (only) when bumping to a new SAVE_SLOT_VER_N.
*
* @param slot The save slot to read into.
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object);
-17
View File
@@ -1,17 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
// One constant per save slot JSON schema this codebase has ever shipped.
// Never remove or renumber an existing entry - old save files on disk may
// still declare it.
#define SAVE_SLOT_VER_1 1
// The schema saveSlotCurrentWriteJSON()/saveSlotCurrentReadJSON() use today.
// Bump this to a new SAVE_SLOT_VER_N when the slot JSON shape changes.
#define SAVE_SLOT_CURRENT SAVE_SLOT_VER_1
-105
View File
@@ -1,105 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "saveslotver1.h"
#include "assert/assert.h"
#include "save/savejson.h"
errorret_t saveSlotVer1WriteJSON(
saveslotver1_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(doc, "Doc cannot be null");
assertNotNull(object, "Object cannot be null");
writeVersion(SAVE_SLOT_CURRENT);
writeString("name", slot->name);
writeTime("time", slot->time);
writeInt32("playerLevel", slot->playerLevel);
writeString("mapName", slot->mapName);
yyjson_mut_val *partyMembersArr = yyjson_mut_arr(doc);
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
yyjson_mut_val *memberObj =
battleFighterSerialize(&slot->partyMembers[i], doc);
yyjson_mut_arr_add_val(partyMembersArr, memberObj);
}
writeValue("partyMembers", partyMembersArr);
writeUInt8Array("partyOrder", slot->partyOrder, PARTY_ACTIVE_SIZE_MAX);
yyjson_mut_val *storyFlagsObj = yyjson_mut_obj_add_obj(doc, object, "storyFlags");
for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) {
yyjson_mut_obj_add_uint(
doc, storyFlagsObj, storyGetNameByFlag(flag), slot->storyFlags[flag]
);
}
writeString("currentCutscene", slot->currentCutscene);
errorOk();
}
errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(object, "Object cannot be null");
char_t saveJsonStringBuffer[SAVE_JSON_STRING_BUFFER_SIZE];
requireVersion(SAVE_SLOT_CURRENT);
readString("name", slot->name, "", SAVE_SLOT_VER1_NAME_LENGTH);
if(slot->name[0] == '\0') errorOk();
readTime("time", slot->time);
readInt32("playerLevel", slot->playerLevel, 1);
requireStrMin("mapName", 1);
readString(
"mapName", slot->mapName, "", SAVE_SLOT_VER1_MAP_NAME_MAX - 1
);
requireArrayOfSize(
"partyMembers", partyMembersArr, PARTY_MEMBER_COUNT_MAX
);
size_t memberIdx, memberLen;
yyjson_val *memberObj;
yyjson_arr_foreach(
partyMembersArr, memberIdx, memberLen, memberObj
) {
battlefighter_t *member = &slot->partyMembers[memberIdx];
member->id = (uint8_t)memberIdx;
errorChain(battleFighterParse(member, memberObj));
}
readUInt8Array(
"partyOrder", slot->partyOrder, PARTY_ACTIVE_SIZE_MAX,
PARTY_ORDER_EMPTY
);
yyjson_val *storyFlagsObj = yyjson_obj_get(object, "storyFlags");
if(storyFlagsObj != NULL) {
yyjson_obj_iter iter = yyjson_obj_iter_with(storyFlagsObj);
yyjson_val *flagKey;
while((flagKey = yyjson_obj_iter_next(&iter))) {
storyflag_t flag = storyGetFlagByName(yyjson_get_str(flagKey));
if(flag >= STORY_FLAG_COUNT) continue;// Flag no longer exists, ignore
slot->storyFlags[flag] =
(storyflagval_t)yyjson_get_uint(yyjson_obj_iter_get_val(flagKey));
}
}
readString(
"currentCutscene", slot->currentCutscene, SAVE_SLOT_VER1_CUTSCENE_DEFAULT,
SAVE_SLOT_VER1_CUTSCENE_NAME_MAX - 1
);
errorOk();
}
-86
View File
@@ -1,86 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "time/timeepoch.h"
#include "save/slot/saveslotver.h"
#include "rpg/battle/party.h"
#include "rpg/story/story.h"
#include "yyjson.h"
// This schema's own frozen copies of save/slot/saveslot.h's field sizes -
// never reference SAVE_SLOT_NAME_LENGTH/MAP_NAME_MAX (or any other live
// constant) here. Once shipped, this file's on-disk shape must never change
// underneath old save files, even if the live engine constants do. The
// party arrays below are the exception - saveSlotVer1ReadJSON() checks their
// JSON array lengths explicitly, so PARTY_MEMBER_COUNT_MAX/
// PARTY_ACTIVE_SIZE_MAX (rpg/battle/party.h) can be referenced directly.
// storyFlags is a further exception, for a different reason - it's written
// keyed by flag name (not position), so its on-disk shape is decoupled from
// STORY_FLAG_COUNT_MAX entirely; a save simply loses whatever flags no
// longer resolve by name (see storyGetFlagByName) if that cap ever shrinks.
//
// partyMembers is a THIRD exception, deliberately broken pre-release: its
// per-member shape now comes from battleFighterSerialize/battleFighterParse
// (rpg/battle/fighter/battlefighter.h) - the shared fighter JSON format
// used everywhere else (party.jsonc, BATTLE_START enemies) - instead of its
// own hand-rolled field set. This is a real on-disk break (different key
// names, no more "statAttack" etc.) with no migration - an old v1 save
// simply reads back as corrupt. Acceptable only because nothing has
// shipped yet; don't repeat this once real saves exist without a version
// bump instead.
#define SAVE_SLOT_VER1_NAME_LENGTH 8
#define SAVE_SLOT_VER1_MAP_NAME_MAX 10
#define SAVE_SLOT_VER1_CUTSCENE_NAME_MAX 128
// Own frozen copy of save/slot/saveslot.h's SAVE_SLOT_CUTSCENE_DEFAULT, for
// the same reason as the sizes above - never reference the live constant.
#define SAVE_SLOT_VER1_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc"
// Independent from saveslotcache_t (save/slot/saveslot.h) by design - this
// is what version 1 actually put on disk, frozen forever. saveSlotCurrent
// WriteJSON()/ReadJSON() (saveslotcurrent.c) manually copy field-by-field
// between the two, since they're separate types that merely happen to
// currently look alike.
typedef struct saveslotver1_s {
char_t name[SAVE_SLOT_VER1_NAME_LENGTH + 1];
dusktimeepoch_t time;
int32_t playerLevel;
char_t mapName[SAVE_SLOT_VER1_MAP_NAME_MAX];
battlefighter_t partyMembers[PARTY_MEMBER_COUNT_MAX];
uint8_t partyOrder[PARTY_ACTIVE_SIZE_MAX];
storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX];
char_t currentCutscene[SAVE_SLOT_VER1_CUTSCENE_NAME_MAX];
} saveslotver1_t;
/**
* Writes the given v1 save slot data out to the given JSON object, stamping
* SAVE_SLOT_CURRENT as the "version" field.
*
* @param slot The v1 save slot data to write.
* @param doc The mutable JSON document that owns the object.
* @param object The mutable JSON object to write into.
* @return Error state if any.
*/
errorret_t saveSlotVer1WriteJSON(
saveslotver1_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
);
/**
* Reads v1 save slot data in from the given JSON object. Throws if the
* JSON's "version" is missing or doesn't match SAVE_SLOT_CURRENT.
*
* @param slot The v1 save slot data to read into.
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object);
-87
View File
@@ -7,7 +7,6 @@
#include "save/savedevice.h"
#include "save/savedevicelinux.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
#include "save/savejson.h"
#include "asset/asset.h"
@@ -86,92 +85,6 @@ errorret_t saveDeviceLinuxGetSettingsPath(
errorOk();
}
errorret_t saveDeviceLinuxGetSlotPath(
char_t *dest,
const size_t destSize,
const uint8_t slotIndex
) {
assertNotNull(dest, "dest cannot be null");
char_t directory[SAVE_DEVICE_LINUX_PATH_MAX];
errorChain(saveDeviceLinuxGetDirectory(directory, sizeof(directory)));
char_t filename[SAVE_SLOT_NAME_LENGTH + 16];
stringFormat(
filename, sizeof(filename) - 1,
SAVE_DEVICE_LINUX_SLOT_FILENAME_FORMAT, (uint32_t)slotIndex
);
stringFormat(dest, destSize, "%s/%s", directory, filename);
errorOk();
}
errorret_t saveDeviceLinuxSlotWrite(
savedevice_t *device,
saveslot_t *slot,
uint8_t slotIndex
) {
assertNotNull(device, "device cannot be null");
assertNotNull(slot, "slot cannot be null");
char_t path[SAVE_DEVICE_LINUX_PATH_MAX];
errorChain(saveDeviceLinuxGetSlotPath(path, sizeof(path), slotIndex));
writeInit();
errorret_t writeResult = saveSlotWriteJSON(slot, doc, object);
if(errorIsNotOk(writeResult)) {
yyjson_mut_doc_free(doc);
errorChain(writeResult);
}
yyjson_write_err writeErr;
bool_t written = yyjson_mut_write_file(path, doc, 0, NULL, &writeErr);
yyjson_mut_doc_free(doc);
if(!written) {
errorThrow(
"Failed to write save slot file: %s (%s)", path, writeErr.msg
);
}
errorOk();
}
errorret_t saveDeviceLinuxSlotRead(
savedevice_t *device,
saveslot_t *slot,
uint8_t slotIndex
) {
assertNotNull(device, "device cannot be null");
assertNotNull(slot, "slot cannot be null");
char_t path[SAVE_DEVICE_LINUX_PATH_MAX];
errorChain(saveDeviceLinuxGetSlotPath(path, sizeof(path), slotIndex));
yyjson_read_err readErr;
yyjson_doc *jsonDoc = yyjson_read_file(path, 0, NULL, &readErr);
if(jsonDoc == NULL) {
// No save file at this slot yet, leave the slot as-is.
if(readErr.code == YYJSON_READ_ERROR_FILE_OPEN) errorOk();
errorThrow(
"Failed to parse save slot file: %s (%s)", path, readErr.msg
);
}
yyjson_val *object = yyjson_doc_get_root(jsonDoc);
if(object == NULL) {
yyjson_doc_free(jsonDoc);
errorThrow("Save slot file missing root object: %s", path);
}
errorret_t readResult = saveSlotReadJSON(slot, object);
yyjson_doc_free(jsonDoc);
errorChain(readResult);
errorOk();
}
errorret_t saveDeviceLinuxSettingsWrite(
savedevice_t *device,
savesettings_t *settings
-45
View File
@@ -14,7 +14,6 @@
// settings and each save slot as their own separate file.
#define SAVE_DEVICE_LINUX_SAVES_SUBDIR_NAME "saves"
#define SAVE_DEVICE_LINUX_SETTINGS_FILENAME "settings.json"
#define SAVE_DEVICE_LINUX_SLOT_FILENAME_FORMAT "slot%u.json"
#define SAVE_DEVICE_LINUX_PATH_MAX FILENAME_MAX
typedef struct {
@@ -22,7 +21,6 @@ typedef struct {
} savedeviceplatform_t;
typedef struct savedevice_s savedevice_t;
typedef struct saveslot_s saveslot_t;
typedef struct savesettings_s savesettings_t;
/**
@@ -71,49 +69,6 @@ errorret_t saveDeviceLinuxGetSettingsPath(
const size_t destSize
);
/**
* Builds the absolute path to the given save slot's file.
*
* @param dest The destination buffer to write the path into.
* @param destSize The size of the destination buffer, exc. null terminator.
* @param slotIndex The slot index to build the path for.
* @return Error state if any.
*/
errorret_t saveDeviceLinuxGetSlotPath(
char_t *dest,
const size_t destSize,
const uint8_t slotIndex
);
/**
* Writes the given save slot out to its file on disk.
*
* @param device The save device to write to.
* @param slot The save slot to write.
* @param slotIndex The slot index to write to.
* @return Error state if any.
*/
errorret_t saveDeviceLinuxSlotWrite(
savedevice_t *device,
saveslot_t *slot,
uint8_t slotIndex
);
/**
* Reads a save slot in from its file on disk. If no file exists yet for the
* given slot index, this is not an error - the slot is simply left as-is.
*
* @param device The save device to read from.
* @param slot The save slot to read into.
* @param slotIndex The slot index to read from.
* @return Error state if any.
*/
errorret_t saveDeviceLinuxSlotRead(
savedevice_t *device,
saveslot_t *slot,
uint8_t slotIndex
);
/**
* Writes the given save settings out to the settings file on disk.
*
-2
View File
@@ -14,7 +14,5 @@
#define saveDevicePlatformUpdate saveDeviceLinuxUpdate
#define saveDeviceCheckAvailabilityPlatform saveDeviceLinuxCheckAvailability
#define saveDevicePlatformDispose saveDeviceLinuxDispose
#define saveDeviceSlotWritePlatform saveDeviceLinuxSlotWrite
#define saveDeviceSlotReadPlatform saveDeviceLinuxSlotRead
#define saveDeviceSettingsWritePlatform saveDeviceLinuxSettingsWrite
#define saveDeviceSettingsReadPlatform saveDeviceLinuxSettingsRead
-2
View File
@@ -13,8 +13,6 @@ endif()
add_subdirectory(error)
add_subdirectory(thread)
add_subdirectory(display)
add_subdirectory(rpg)
add_subdirectory(item)
add_subdirectory(save)
add_subdirectory(time)
add_subdirectory(util)
-2
View File
@@ -7,8 +7,6 @@ include(dusktest)
# Tests
dusktest(test_savesettings.c)
dusktest(test_saveslot.c)
target_sources(test_saveslot PRIVATE savetestfixture.c)
dusktest(test_savedevicelinux.c)
target_sources(test_savedevicelinux PRIVATE savetestfixture.c)
-52
View File
@@ -10,9 +10,6 @@
#include "savetestfixture.h"
#include "assert/assert.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "util/memory.h"
#include "util/string.h"
#include <ftw.h>
@@ -25,51 +22,6 @@ static char_t SAVE_TEST_FIXTURE_HOME[PATH_MAX];
static char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX];
static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME;
static const char_t *SAVE_TEST_FIXTURE_PARTY_JSON =
"{\"members\":{\"hero\":{"
"\"attack\":10,\"defense\":5,\"magic\":0,\"speed\":10,\"luck\":0,"
"\"healthMax\":30,\"mpMax\":10"
"}}}";
static yyjson_doc *SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
errorret_t saveTestFixturePartyJsonLoadSyncStub(assetloading_t *loading) {
loading->entry->data.json = SAVE_TEST_FIXTURE_PARTY_DOC;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t saveTestFixturePartyJsonDisposeStub(assetentry_t *entry) {
errorOk();
}
void saveTestFixtureStubPartyAsset(void) {
memoryZero(&ASSET, sizeof(ASSET));
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) {
SAVE_TEST_FIXTURE_PARTY_DOC = yyjson_read(
SAVE_TEST_FIXTURE_PARTY_JSON, strlen(SAVE_TEST_FIXTURE_PARTY_JSON), 0
);
assertNotNull(
SAVE_TEST_FIXTURE_PARTY_DOC, "Failed to parse party JSON fixture"
);
}
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].loadSync =
saveTestFixturePartyJsonLoadSyncStub;
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].dispose =
saveTestFixturePartyJsonDisposeStub;
}
void saveTestFixtureUnstubPartyAsset(void) {
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) return;
yyjson_doc_free(SAVE_TEST_FIXTURE_PARTY_DOC);
SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
}
static int_t saveTestFixtureRemoveEntry(
const char *path,
const struct stat *statBuffer,
@@ -97,8 +49,6 @@ int saveTestFixtureSetup(void **state) {
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
saveTestFixtureStubPartyAsset();
errorret_t ret = saveInit();
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
@@ -106,8 +56,6 @@ int saveTestFixtureSetup(void **state) {
}
int saveTestFixtureTeardown(void **state) {
saveTestFixtureUnstubPartyAsset();
nftw(
SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16,
FTW_DEPTH | FTW_PHYS
-18
View File
@@ -35,21 +35,3 @@ int saveTestFixtureTeardown(void **state);
* filesystem permission tricks. Must be called after saveTestFixtureSetup.
*/
void saveTestFixtureBlockSaveDirectory(void);
/**
* Stubs the JSON asset loader's sync callback to hand back a small
* in-memory party.jsonc-shaped doc instead of doing a real file/zip read -
* saveInit()/saveSlotInit() seed the party roster via partyInit(), which
* loads party.jsonc through the real asset system, and there's no real
* asset bundle available under the test harness. No thread, no real I/O.
* Resets ASSET to a fresh, otherwise-empty state each call, so it's safe
* to call once per test. Called automatically by saveTestFixtureSetup.
*/
void saveTestFixtureStubPartyAsset(void);
/**
* Frees the in-memory doc created by saveTestFixtureStubPartyAsset. Safe
* to call even if that function was never called. Called automatically
* by saveTestFixtureTeardown.
*/
void saveTestFixtureUnstubPartyAsset(void);
-221
View File
@@ -8,7 +8,6 @@
#include "dusktest.h"
#include "savetestfixture.h"
#include "save/save.h"
#include "save/savedevicelinux.h"
#include "util/memory.h"
#include "util/string.h"
#include <stdio.h>
@@ -54,16 +53,10 @@ static void test_saveInit_setsDefaults(void **state) {
// saveTestFixtureSetup already called the real saveInit() - just assert
// on the state it left behind.
assert_int_equal(SAVE.deviceCurrent, 0xFF);
assert_int_equal(SAVE.slotCurrent, 0xFF);
assert_false(SAVE.findingAvailableDevice);
assert_false(SAVE.noAvailableDeviceFound);
assert_true(SAVE.settings.audioMasterVolume == 1.0f);
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_UNKNOWN);
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
assert_int_equal(SAVE.caches[i].name[0], '\0');
assert_int_equal(SAVE.caches[i].playerLevel, 0);
}
}
// ============================================================
@@ -281,180 +274,6 @@ static void test_saveLoadSettings_readsAndClearsDirtyFlag(void **state) {
assert_false(SAVE.settingsDirty);
}
// ============================================================
// saveSaveSlot / saveLoadSlot
// ============================================================
static void test_saveSaveSlot_noCurrentDeviceAsserts(void **state) {
expect_assert_failure(saveSaveSlot());
}
static void test_saveSaveSlot_invalidSlotIndexAsserts(void **state) {
makeDeviceAvailable();
// slotCurrent is still 0xFF (unset) from saveInit().
expect_assert_failure(saveSaveSlot());
}
static void test_saveSaveSlot_success(void **state) {
makeDeviceAvailable();
SAVE.slotCurrent = 1;
saveSlotInit(&SAVE.slot);
stringCopy(
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
);
SAVE.slot.cachedData.playerLevel = 12;
SAVE.slotDirty = true;
errorret_t ret = saveSaveSlot();
assert_true(errorIsOk(ret));
assert_false(SAVE.slotDirty);
assert_true(stringEquals(SAVE.caches[1].name, "Hero"));
assert_int_equal(SAVE.caches[1].playerLevel, 12);
// No pre-init needed - saveSlotReadJSON() (via saveDeviceSlotRead) fully
// resets/overwrites its destination struct regardless of prior content.
saveslot_t onDisk;
errorret_t readRet = saveDeviceSlotRead(&SAVE.devices[0], &onDisk, 1);
assert_true(errorIsOk(readRet));
assert_true(stringEquals(onDisk.cachedData.name, "Hero"));
assert_int_equal(onDisk.cachedData.playerLevel, 12);
}
static void test_saveLoadSlot_noCurrentDeviceAsserts(void **state) {
expect_assert_failure(saveLoadSlot());
}
static void test_saveLoadSlot_invalidSlotIndexAsserts(void **state) {
makeDeviceAvailable();
expect_assert_failure(saveLoadSlot());
}
static void test_saveLoadSlot_success(void **state) {
makeDeviceAvailable();
// Write slot 2's on-disk file via SAVE.slot itself (saveSlotInit() only
// ever operates on SAVE.slot now), then reset it again below - the
// write and the load-under-test don't overlap in time.
SAVE.slotCurrent = 2;
saveSlotInit(&SAVE.slot);
stringCopy(
SAVE.slot.cachedData.name, "Zelda", sizeof(SAVE.slot.cachedData.name)
);
SAVE.slot.cachedData.playerLevel = 30;
errorret_t writeRet = saveDeviceSlotWrite(&SAVE.devices[0], &SAVE.slot, 2);
assert_true(errorIsOk(writeRet));
SAVE.slotCurrent = 2;
saveSlotInit(&SAVE.slot);
SAVE.slotDirty = true;
errorret_t ret = saveLoadSlot();
assert_true(errorIsOk(ret));
assert_false(SAVE.slotDirty);
assert_true(stringEquals(SAVE.slot.cachedData.name, "Zelda"));
assert_int_equal(SAVE.slot.cachedData.playerLevel, 30);
assert_true(stringEquals(SAVE.caches[2].name, "Zelda"));
assert_int_equal(SAVE.caches[2].playerLevel, 30);
}
static void test_saveLoadSlot_deviceReadFails_cacheStaysStale(void **state) {
makeDeviceAvailable();
char_t slotPath[512];
errorret_t pathRet = saveDeviceLinuxGetSlotPath(
slotPath, sizeof(slotPath), 0
);
assert_true(errorIsOk(pathRet));
FILE *file = fopen(slotPath, "w");
assert_non_null(file);
fprintf(file, "not valid json {{{");
fclose(file);
SAVE.slotCurrent = 0;
saveSlotInit(&SAVE.slot);
SAVE.caches[0].playerLevel = 77;// sentinel
errorret_t ret = saveLoadSlot();
assert_true(errorIsNotOk(ret));
errorCatch(ret);
// The device read failed before the cache-sync line ever ran.
assert_int_equal(SAVE.caches[0].playerLevel, 77);
}
// ============================================================
// saveLoadAllSlots
// ============================================================
static void test_saveLoadAllSlots_loadsAllAndEndsAtLastIndex(void **state) {
makeDeviceAvailable();
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
SAVE.slotCurrent = i;
saveSlotInit(&SAVE.slot);
SAVE.slot.cachedData.playerLevel = (int32_t)(i + 1);
errorret_t saveRet = saveSaveSlot();
assert_true(errorIsOk(saveRet));
}
errorret_t ret = saveLoadAllSlots();
assert_true(errorIsOk(ret));
assert_int_equal(SAVE.slotCurrent, SAVE_SLOT_COUNT - 1);
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
assert_int_equal(SAVE.caches[i].playerLevel, (int32_t)(i + 1));
}
}
static void test_saveLoadAllSlots_missingFileLeavesInitDefaults(void **state) {
makeDeviceAvailable();
SAVE.slotCurrent = 0;
saveSlotInit(&SAVE.slot);
SAVE.slot.cachedData.playerLevel = 5;
errorret_t saveRet = saveSaveSlot();
assert_true(errorIsOk(saveRet));
// Slots 1 and 2 are never written - no file exists for them.
errorret_t ret = saveLoadAllSlots();
assert_true(errorIsOk(ret));
assert_int_equal(SAVE.caches[0].playerLevel, 5);
// Missing-file slots fall back to saveSlotInit()'s default (0), not the
// JSON-missing-field default of 1 that a corrupt-but-present file with no
// playerLevel key would produce - these are two different defaults.
assert_int_equal(SAVE.caches[1].playerLevel, 0);
assert_int_equal(SAVE.caches[2].playerLevel, 0);
}
static void test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex(
void **state
) {
makeDeviceAvailable();
SAVE.slotCurrent = 0;
saveSlotInit(&SAVE.slot);
errorret_t saveRet = saveSaveSlot();
assert_true(errorIsOk(saveRet));
char_t slotPath[512];
errorret_t pathRet = saveDeviceLinuxGetSlotPath(
slotPath, sizeof(slotPath), 1
);
assert_true(errorIsOk(pathRet));
FILE *file = fopen(slotPath, "w");
assert_non_null(file);
fprintf(file, "not valid json {{{");
fclose(file);
errorret_t ret = saveLoadAllSlots();
assert_true(errorIsNotOk(ret));
errorCatch(ret);
// The loop bails out as soon as slot 1 fails - it never reaches slot 2.
assert_int_equal(SAVE.slotCurrent, 1);
}
// ============================================================
// saveDispose
// ============================================================
@@ -566,46 +385,6 @@ int main(void) {
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveSaveSlot_noCurrentDeviceAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveSaveSlot_invalidSlotIndexAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveSaveSlot_success, saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveLoadSlot_noCurrentDeviceAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveLoadSlot_invalidSlotIndexAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveLoadSlot_success, saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveLoadSlot_deviceReadFails_cacheStaysStale,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveLoadAllSlots_loadsAllAndEndsAtLastIndex,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveLoadAllSlots_missingFileLeavesInitDefaults,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveDispose_succeeds, saveTestFixtureSetup, saveTestFixtureTeardown
),
+2 -60
View File
@@ -8,9 +8,7 @@
#include "dusktest.h"
#include "savetestfixture.h"
#include "save/savedevice.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
#include "save/save.h"
#include "util/memory.h"
#include "util/string.h"
@@ -150,28 +148,10 @@ static void test_saveDeviceCheckAvailability_resolvesSynchronouslyOnLinux(
}
// ============================================================
// saveDeviceSlotWrite / saveDeviceSlotRead / saveDeviceSettingsWrite /
// saveDeviceSettingsRead - dispatch to the platform's four hooks.
// saveDeviceSettingsWrite / saveDeviceSettingsRead - dispatch to the
// platform's hooks.
// ============================================================
static void test_saveDeviceSlotWrite_nullAsserts(void **state) {
savedevice_t device;
// Content is never touched - both calls assert on a NULL argument
// before either device or slot would actually be read.
saveslot_t slot;
expect_assert_failure(saveDeviceSlotWrite(NULL, &slot, 0));
expect_assert_failure(saveDeviceSlotWrite(&device, NULL, 0));
}
static void test_saveDeviceSlotRead_nullAsserts(void **state) {
savedevice_t device;
saveslot_t slot;
expect_assert_failure(saveDeviceSlotRead(NULL, &slot, 0));
expect_assert_failure(saveDeviceSlotRead(&device, NULL, 0));
}
static void test_saveDeviceSettingsWrite_nullAsserts(void **state) {
savedevice_t device;
savesettings_t settings;
@@ -190,32 +170,6 @@ static void test_saveDeviceSettingsRead_nullAsserts(void **state) {
expect_assert_failure(saveDeviceSettingsRead(&device, NULL));
}
static void test_saveDeviceSlot_dispatchesToPlatform(void **state) {
savedevice_t device;
errorret_t initRet = saveDeviceInit(&device);
assert_true(errorIsOk(initRet));
// The save directory must exist before a write can succeed - normally
// saveDeviceCheckAvailability does this via the platform's mkdirp.
saveDeviceCheckAvailability(&device, updateCallback, NULL);
saveDeviceUpdate(&device);
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
// write source here.
saveSlotInit(&SAVE.slot);
SAVE.slot.cachedData.playerLevel = 21;
errorret_t writeRet = saveDeviceSlotWrite(&device, &SAVE.slot, 0);
assert_true(errorIsOk(writeRet));
// No pre-init needed - saveSlotReadJSON() (via saveDeviceSlotRead) fully
// resets/overwrites its destination struct regardless of prior content.
saveslot_t read;
errorret_t readRet = saveDeviceSlotRead(&device, &read, 0);
assert_true(errorIsOk(readRet));
assert_int_equal(read.cachedData.playerLevel, 21);
}
static void test_saveDeviceSettings_dispatchesToPlatform(void **state) {
savedevice_t device;
errorret_t initRet = saveDeviceInit(&device);
@@ -303,14 +257,6 @@ int main(void) {
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveDeviceSlotWrite_nullAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveDeviceSlotRead_nullAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveDeviceSettingsWrite_nullAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
@@ -319,10 +265,6 @@ int main(void) {
test_saveDeviceSettingsRead_nullAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveDeviceSlot_dispatchesToPlatform,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_saveDeviceSettings_dispatchesToPlatform,
saveTestFixtureSetup, saveTestFixtureTeardown
-149
View File
@@ -9,9 +9,7 @@
#include "savetestfixture.h"
#include "save/savedevice.h"
#include "save/savedevicelinux.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
#include "save/save.h"
#include "util/memory.h"
#include "util/string.h"
#include <stdio.h>
@@ -86,27 +84,6 @@ static void test_getSettingsPath_homeUnset_errors(void **state) {
errorCatch(ret);
}
static void test_getSlotPath_success(void **state) {
char_t path[512];
errorret_t ret = saveDeviceLinuxGetSlotPath(path, sizeof(path), 2);
assert_true(errorIsOk(ret));
char_t expected[512];
stringFormat(
expected, sizeof(expected), "%s/.dusk/saves/slot2.json", getenv("HOME")
);
assert_true(stringEquals(path, expected));
}
static void test_getSlotPath_homeUnset_errors(void **state) {
unsetenv("HOME");
char_t path[512];
errorret_t ret = saveDeviceLinuxGetSlotPath(path, sizeof(path), 0);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
}
// ============================================================
// saveDeviceLinuxCheckAvailability
// ============================================================
@@ -175,105 +152,6 @@ static void test_checkAvailability_mkdirpFails(void **state) {
assert_true(g_callbackFired);
}
// ============================================================
// saveDeviceLinuxSlotWrite / saveDeviceLinuxSlotRead
// ============================================================
static void test_slotWrite_nullAsserts(void **state) {
savedevice_t device;
// Content is never touched - both calls assert on a NULL argument
// before either device or slot would actually be read.
saveslot_t slot;
expect_assert_failure(saveDeviceLinuxSlotWrite(NULL, &slot, 0));
expect_assert_failure(saveDeviceLinuxSlotWrite(&device, NULL, 0));
}
static void test_slotRead_nullAsserts(void **state) {
savedevice_t device;
saveslot_t slot;
expect_assert_failure(saveDeviceLinuxSlotRead(NULL, &slot, 0));
expect_assert_failure(saveDeviceLinuxSlotRead(&device, NULL, 0));
}
static void test_slotWriteRead_roundTrip(void **state) {
savedevice_t device;
memoryZero(&device, sizeof(device));
// The save directory must exist before a write can succeed - normally
// saveDeviceLinuxCheckAvailability does this via mkdirp.
checkAvailabilityAndDrain(&device);
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
// write source here.
saveSlotInit(&SAVE.slot);
stringCopy(
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
);
SAVE.slot.cachedData.playerLevel = 9;
errorret_t writeRet = saveDeviceLinuxSlotWrite(&device, &SAVE.slot, 1);
assert_true(errorIsOk(writeRet));
// No pre-init needed - saveSlotReadJSON() (via saveDeviceLinuxSlotRead)
// fully resets/overwrites its destination struct regardless of prior
// content.
saveslot_t read;
errorret_t readRet = saveDeviceLinuxSlotRead(&device, &read, 1);
assert_true(errorIsOk(readRet));
assert_true(stringEquals(read.cachedData.name, "Hero"));
assert_int_equal(read.cachedData.playerLevel, 9);
}
static void test_slotRead_noFileYet_leavesSlotUntouched(void **state) {
savedevice_t device;
memoryZero(&device, sizeof(device));
// Deliberately not saveSlotInit()'d - a missing slot file means
// saveDeviceLinuxSlotRead never even calls saveSlotReadJSON, so slot
// must come in exactly as the caller left it and go out unchanged.
saveslot_t slot;
memoryZero(&slot, sizeof(slot));
stringCopy(slot.cachedData.name, "sentinel", sizeof(slot.cachedData.name));
slot.cachedData.playerLevel = 555;
errorret_t ret = saveDeviceLinuxSlotRead(&device, &slot, 2);
assert_true(errorIsOk(ret));
assert_true(stringEquals(slot.cachedData.name, "sentinel"));
assert_int_equal(slot.cachedData.playerLevel, 555);
}
static void test_slotRead_corruptFile_errors(void **state) {
savedevice_t device;
memoryZero(&device, sizeof(device));
// The directory must exist before we can drop a file into it - normally
// saveDeviceLinuxCheckAvailability does this via mkdirp.
savedevice_t tmpDevice;
memoryZero(&tmpDevice, sizeof(tmpDevice));
tmpDevice.state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY;
saveDeviceLinuxCheckAvailability(&tmpDevice);
assert_int_equal(tmpDevice.state, SAVE_DEVICE_STATE_AVAILABLE);
char_t slotPath[512];
errorret_t pathRet =
saveDeviceLinuxGetSlotPath(slotPath, sizeof(slotPath), 0);
assert_true(errorIsOk(pathRet));
FILE *file = fopen(slotPath, "w");
assert_non_null(file);
fprintf(file, "not valid json {{{");
fclose(file);
saveslot_t slot;
errorret_t ret = saveDeviceLinuxSlotRead(&device, &slot, 0);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
}
// ============================================================
// saveDeviceLinuxSettingsWrite / saveDeviceLinuxSettingsRead
// ============================================================
@@ -381,14 +259,6 @@ int main(void) {
test_getSettingsPath_homeUnset_errors,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_getSlotPath_success, saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_getSlotPath_homeUnset_errors,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_checkAvailability_wrongStateAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown
@@ -410,25 +280,6 @@ int main(void) {
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_slotWrite_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_slotRead_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_slotWriteRead_roundTrip,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_slotRead_noFileYet_leavesSlotUntouched,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_slotRead_corruptFile_errors,
saveTestFixtureSetup, saveTestFixtureTeardown
),
cmocka_unit_test_setup_teardown(
test_settingsWrite_nullAsserts,
saveTestFixtureSetup, saveTestFixtureTeardown