World storage

Source: src/dungeon_storage.c · 286 lines · 18 functions.

Module responsibility

Loads and saves generated worlds, serialises positions and cells, validates checksums and deletes run data.

See how this module fits into the complete Dungeon system →

Module entry points

These functions are exposed to the rest of the Dungeon system. Open one to follow its decisions and helper calls.

How to read these diagrams

Each function has an expandable structural diagram derived from the current C source. Decisions split into True and False, loop bodies are indented, and arrows show statement order.

These are source diagrams before preprocessing, not runtime traces. Callback execution, task scheduling and macro expansion are not inferred.

Functions in this file

deleteDungeonDataFiles() · source line 29
bool deleteDungeonDataFiles(DungeonDeleteResult *result)
  1. Executestatic const char *const files[] = { DUNGEON_FILE, DUNGEON_PLAYER_FILE, DUNGEON_EXPLORED_FILE, DUNGEON_HISTORY_FILE, };
  2. Decisionresult == NULL
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Executememset(result, 0, sizeof(*result));
  4. Executeresult->error = DUNGEON_DELETE_OK;
  5. FOR loopunsigned index = 0U; index < sizeof(files) / sizeof(files[0]); ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeerrno = 0;
      2. Decisionremove(files[index]) == 0
        True
          1. Execute++result->deleted;
        False
        1. Decisionerrno == ENOENT
          True
            1. Execute++result->missing;
          False
            1. Executeresult->error = DUNGEON_DELETE_FAILED;
            2. Executeresult->errno_value = errno;
            3. Executeresult->failed_path = files[index];
            4. Exit this functionreturn false;
  6. Exit this functionreturn true;
Read original function source
bool deleteDungeonDataFiles(DungeonDeleteResult *result)
{
    static const char *const files[] = {
        DUNGEON_FILE,
        DUNGEON_PLAYER_FILE,
        DUNGEON_EXPLORED_FILE,
        DUNGEON_HISTORY_FILE,
    };
    if (result == NULL) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_DELETE_OK;
    for (unsigned index = 0U; index < sizeof(files) / sizeof(files[0]); ++index) {
        errno = 0;
        if (remove(files[index]) == 0) {
            ++result->deleted;
            /* ESP_LOGI("dungeon_storage", "DELETE %s OK", files[index]); */
        } else if (errno == ENOENT) {
            ++result->missing;
            /* ESP_LOGI("dungeon_storage", "DELETE %s MISSING", files[index]); */
        } else {
            result->error = DUNGEON_DELETE_FAILED;
            result->errno_value = errno;
            result->failed_path = files[index];
            /* ESP_LOGI("dungeon_storage", "DELETE %s FAILED errno=%d", files[index], errno); */
            return false;
        }
    }
    /* ESP_LOGI("dungeon_storage", "DELETE COMPLETE deleted=%u missing=%u", (unsigned)result->deleted, (unsigned)result->missing); */
    return true;
}
prepareNewDungeonWorldStorage() · source line 60
void prepareNewDungeonWorldStorage(void)
  1. Executeloaded_version = DUNGEON_FILE_VERSION;
  2. Executememset(file_tail, 0, sizeof(file_tail));
Read original function source
void prepareNewDungeonWorldStorage(void)
{
    loaded_version = DUNGEON_FILE_VERSION;
    memset(file_tail, 0, sizeof(file_tail));
}
readByte() · source line 66
static bool readByte(FILE *file, uint8_t *value, DungeonStorageResult *result)
  1. Decisionfread(value, 1, 1, file) != 1U
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Execute++result->bytes;
  3. Exit this functionreturn true;
Read original function source
static bool readByte(FILE *file, uint8_t *value, DungeonStorageResult *result)
{
    if (fread(value, 1, 1, file) != 1U) return false;
    ++result->bytes;
    return true;
}
readU16() · source line 73
static bool readU16(FILE *file, uint16_t *value, DungeonStorageResult *result)
  1. Executeuint8_t bytes[2];
  2. Decisionfread(bytes, 1, sizeof(bytes), file) != sizeof(bytes)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Executeresult->bytes += sizeof(bytes);
  4. Execute*value = (uint16_t)bytes[0] | ((uint16_t)bytes[1] << 8);
  5. Exit this functionreturn true;
Read original function source
static bool readU16(FILE *file, uint16_t *value, DungeonStorageResult *result)
{
    uint8_t bytes[2];
    if (fread(bytes, 1, sizeof(bytes), file) != sizeof(bytes)) return false;
    result->bytes += sizeof(bytes);
    *value = (uint16_t)bytes[0] | ((uint16_t)bytes[1] << 8);
    return true;
}
readU32() · source line 82
static bool readU32(FILE *file, uint32_t *value, DungeonStorageResult *result)
  1. Executeuint8_t bytes[4];
  2. Decisionfread(bytes, 1, sizeof(bytes), file) != sizeof(bytes)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Executeresult->bytes += sizeof(bytes);
  4. Execute*value = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24);
  5. Exit this functionreturn true;
Read original function source
static bool readU32(FILE *file, uint32_t *value, DungeonStorageResult *result)
{
    uint8_t bytes[4];
    if (fread(bytes, 1, sizeof(bytes), file) != sizeof(bytes)) return false;
    result->bytes += sizeof(bytes);
    *value = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24);
    return true;
}
writeByte() · source line 91
static bool writeByte(FILE *file, uint8_t value, DungeonStorageResult *result)
  1. Decisionfwrite(&value, 1, 1, file) != 1U
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Execute++result->bytes;
  3. Exit this functionreturn true;
Read original function source
static bool writeByte(FILE *file, uint8_t value, DungeonStorageResult *result)
{
    if (fwrite(&value, 1, 1, file) != 1U) return false;
    ++result->bytes;
    return true;
}
writeU16() · source line 98
static bool writeU16(FILE *file, uint16_t value, DungeonStorageResult *result)

Calls into this documentation:

  1. Exit this functionreturn writeByte(file, (uint8_t)value, result) && writeByte(file, (uint8_t)(value >> 8), result);
Read original function source
static bool writeU16(FILE *file, uint16_t value, DungeonStorageResult *result)
{
    return writeByte(file, (uint8_t)value, result) && writeByte(file, (uint8_t)(value >> 8), result);
}
writeU32() · source line 103
static bool writeU32(FILE *file, uint32_t value, DungeonStorageResult *result)

Calls into this documentation:

  1. Exit this functionreturn writeByte(file, (uint8_t)value, result) && writeByte(file, (uint8_t)(value >> 8), result) && writeByte(file, (uint8_t)(value >> 16), result) && writeByte(file, (uint8_t)(value >> 24), result);
Read original function source
static bool writeU32(FILE *file, uint32_t value, DungeonStorageResult *result)
{
    return writeByte(file, (uint8_t)value, result) && writeByte(file, (uint8_t)(value >> 8), result) &&
           writeByte(file, (uint8_t)(value >> 16), result) && writeByte(file, (uint8_t)(value >> 24), result);
}
updateChecksum() · source line 109
static void updateChecksum(uint32_t *checksum, uint8_t value)
  1. Execute*checksum ^= value;
  2. Execute*checksum *= CHECKSUM_PRIME;
Read original function source
static void updateChecksum(uint32_t *checksum, uint8_t value)
{
    *checksum ^= value;
    *checksum *= CHECKSUM_PRIME;
}
readPayloadByte() · source line 115
static bool readPayloadByte(FILE *file, uint8_t *value, DungeonStorageResult *result, uint32_t *checksum)

Calls into this documentation:

  1. Exit this functionreturn readByte(file, value, result) ? (updateChecksum(checksum, *value), true) : false;
Read original function source
static bool readPayloadByte(FILE *file, uint8_t *value, DungeonStorageResult *result, uint32_t *checksum)
{
    return readByte(file, value, result) ? (updateChecksum(checksum, *value), true) : false;
}
writePayloadByte() · source line 120
static bool writePayloadByte(FILE *file, uint8_t value, DungeonStorageResult *result, uint32_t *checksum)

Calls into this documentation:

  1. Decision!writeByte(file, value, result)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. ExecuteupdateChecksum(checksum, value);
  3. Exit this functionreturn true;
Read original function source
static bool writePayloadByte(FILE *file, uint8_t value, DungeonStorageResult *result, uint32_t *checksum)
{
    if (!writeByte(file, value, result)) return false;
    updateChecksum(checksum, value);
    return true;
}
writePayloadU16() · source line 127
static bool writePayloadU16(FILE *file, uint16_t value, DungeonStorageResult *result, uint32_t *checksum)

Calls into this documentation:

  1. Exit this functionreturn writePayloadByte(file, (uint8_t)value, result, checksum) && writePayloadByte(file, (uint8_t)(value >> 8), result, checksum);
Read original function source
static bool writePayloadU16(FILE *file, uint16_t value, DungeonStorageResult *result, uint32_t *checksum)
{
    return writePayloadByte(file, (uint8_t)value, result, checksum) && writePayloadByte(file, (uint8_t)(value >> 8), result, checksum);
}
readPayloadU16() · source line 132
static bool readPayloadU16(FILE *file, uint16_t *value, DungeonStorageResult *result, uint32_t *checksum)

Calls into this documentation:

  1. Decision!readU16(file, value, result)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. ExecuteupdateChecksum(checksum, (uint8_t)(*value & 0xFFU));
  3. ExecuteupdateChecksum(checksum, (uint8_t)(*value >> 8));
  4. Exit this functionreturn true;
Read original function source
static bool readPayloadU16(FILE *file, uint16_t *value, DungeonStorageResult *result, uint32_t *checksum)
{
    if (!readU16(file, value, result)) return false;
    updateChecksum(checksum, (uint8_t)(*value & 0xFFU));
    updateChecksum(checksum, (uint8_t)(*value >> 8));
    return true;
}
readPosition() · source line 140
static bool readPosition(FILE *file, DungeonPosition *position, DungeonStorageResult *result, uint32_t *checksum)

Calls into this documentation:

  1. Exit this functionreturn readPayloadByte(file, &position->floor, result, checksum) && readPayloadByte(file, &position->x, result, checksum) && readPayloadByte(file, &position->y, result, checksum);
Read original function source
static bool readPosition(FILE *file, DungeonPosition *position, DungeonStorageResult *result, uint32_t *checksum)
{
    return readPayloadByte(file, &position->floor, result, checksum) && readPayloadByte(file, &position->x, result, checksum) && readPayloadByte(file, &position->y, result, checksum);
}
loadDungeonWorld() · source line 145
bool loadDungeonWorld(DungeonStorageResult *result)

Calls into this documentation:

  1. Decisionresult == NULL
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executememset(result, 0, sizeof(*result));
  3. Executeresult->error = DUNGEON_STORAGE_OPEN_READ_FAILED;
  4. ExecuteFILE *file = fopen(DUNGEON_FILE, "rb");
  5. Decisionfile == NULL
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  6. Decisionfseek(file, 0L, SEEK_END) != 0
    True
    1. Jump to labelgoto read_failed;
    False

    Continue after this decision.

  7. Executeconst long file_size = ftell(file);
  8. Decisionfile_size != (long)LEGACY_FILE_SIZE && file_size != (long)FILE_SIZE
    True
      1. Executeresult->error = DUNGEON_STORAGE_BAD_FILE_SIZE;
      2. Jump to labelgoto done;
    False

    Continue after this decision.

  9. Executerewind(file);
  10. Executeuint8_t magic[4], floors, width, height, record_size;
  11. Executeuint16_t version;
  12. Executeuint32_t payload_size, stored_checksum;
  13. Decisionfread(magic, 1, sizeof(magic), file) != sizeof(magic)
    True
    1. Jump to labelgoto read_failed;
    False

    Continue after this decision.

  14. Executeresult->bytes += sizeof(magic);
  15. Decision!readU16(file, &version, result) || !readByte(file, &floors, result) || !readByte(file, &width, result) || !readByte(file, &height, result) || !readByte(file, &record_size, result) || !readU32(file, &payload_size, result) || !readU32(file, &stored_checksum, result)
    True
    1. Jump to labelgoto read_failed;
    False

    Continue after this decision.

  16. Decisionmemcmp(magic, "MAZE", sizeof(magic)) != 0
    True
      1. Executeresult->error = DUNGEON_STORAGE_BAD_SIGNATURE;
      2. Jump to labelgoto done;
    False

    Continue after this decision.

  17. Decisionversion != DUNGEON_LEGACY_VERSION && version != DUNGEON_FILE_VERSION
    True
      1. Executeresult->error = DUNGEON_STORAGE_BAD_VERSION;
      2. Jump to labelgoto done;
    False

    Continue after this decision.

  18. Decisionfloors != DUNGEON_FLOOR_COUNT || width != DUNGEON_WIDTH || height != DUNGEON_HEIGHT || record_size != CELL_RECORD_SIZE
    True
      1. Executeresult->error = DUNGEON_STORAGE_BAD_DIMENSIONS;
      2. Jump to labelgoto done;
    False

    Continue after this decision.

  19. Executeconst bool legacy = version == DUNGEON_LEGACY_VERSION && payload_size == WORLD_PAYLOAD_SIZE && file_size == (long)LEGACY_FILE_SIZE;
  20. Executeconst bool version2 = version == DUNGEON_FILE_VERSION && payload_size == WORLD_PAYLOAD_SIZE + CONFIG_RECORD_SIZE + RESERVED_SIZE && file_size == (long)FILE_SIZE;
  21. Decision!legacy && !version2
    True
      1. Executeresult->error = DUNGEON_STORAGE_BAD_FILE_SIZE;
      2. Jump to labelgoto done;
    False

    Continue after this decision.

  22. ExecuteinitDungeonMap();
  23. ExecuteinitDungeonConfig();
  24. ExecuteDungeonWorld *world = getDungeonWorld();
  25. Executeuint32_t checksum = CHECKSUM_START;
  26. Executebool read = readPosition(file, &world->entrance, result, &checksum) && readPosition(file, &world->exit, result, &checksum);
  27. FOR loopunsigned index = 0; read && index < DUNGEON_FLOOR_COUNT - 1U; ++index

    Repeat body while the loop condition permits; then continue below.

    1. Executeread = readPosition(file, &world->stairLinks[index].upper, result, &checksum) && readPosition(file, &world->stairLinks[index].lower, result, &checksum);
  28. FOR loopunsigned floor = 0; read && floor < DUNGEON_FLOOR_COUNT; ++floor

    Repeat body while the loop condition permits; then continue below.

    1. FOR loopunsigned y = 0; read && y < DUNGEON_HEIGHT; ++y

      Repeat body while the loop condition permits; then continue below.

      1. FOR loopunsigned x = 0; read && x < DUNGEON_WIDTH; ++x

        Repeat body while the loop condition permits; then continue below.

          1. ExecuteDungeonCell *cell = &world->floors[floor].cells[y][x];
          2. Executeread = readPayloadByte(file, &cell->exits, result, &checksum) && readPayloadByte(file, &cell->tileType, result, &checksum) && readPayloadByte(file, &cell->variant, result, &checksum) && readPayloadByte(file, &cell->explored, result, &checksum) && readPayloadU16(file, &cell->eventFlags, result, &checksum) && readPayloadU16(file, &cell->stateFlags, result, &checksum);
  29. FOR loopunsigned index = 0; read && version2 && index < TAIL_SIZE; ++index

    Repeat body while the loop condition permits; then continue below.

    1. Executeread = readPayloadByte(file, &file_tail[index], result, &checksum);
  30. Decision!read || result->bytes != (uint32_t)file_size
    True
    1. Jump to labelgoto read_failed;
    False

    Continue after this decision.

  31. Decisionchecksum != stored_checksum
    True
      1. Executeresult->error = DUNGEON_STORAGE_CHECKSUM_FAILED;
      2. Jump to labelgoto done;
    False

    Continue after this decision.

  32. Executeloaded_version = version;
  33. Executeresult->error = DUNGEON_STORAGE_OK;
  34. Executefclose(file);
  35. Exit this functionreturn true;
  36. Executeread_failed: result->error = DUNGEON_STORAGE_READ_FAILED;
  37. Executedone: fclose(file);
  38. Exit this functionreturn false;
Read original function source
bool loadDungeonWorld(DungeonStorageResult *result)
{
    if (result == NULL) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_STORAGE_OPEN_READ_FAILED;
    FILE *file = fopen(DUNGEON_FILE, "rb");
    if (file == NULL) return false;
    if (fseek(file, 0L, SEEK_END) != 0) goto read_failed;
    const long file_size = ftell(file);
    if (file_size != (long)LEGACY_FILE_SIZE && file_size != (long)FILE_SIZE) { result->error = DUNGEON_STORAGE_BAD_FILE_SIZE; goto done; }
    rewind(file);
    uint8_t magic[4], floors, width, height, record_size;
    uint16_t version;
    uint32_t payload_size, stored_checksum;
    if (fread(magic, 1, sizeof(magic), file) != sizeof(magic)) goto read_failed;
    result->bytes += sizeof(magic);
    if (!readU16(file, &version, result) || !readByte(file, &floors, result) || !readByte(file, &width, result) || !readByte(file, &height, result) || !readByte(file, &record_size, result) || !readU32(file, &payload_size, result) || !readU32(file, &stored_checksum, result)) goto read_failed;
    if (memcmp(magic, "MAZE", sizeof(magic)) != 0) { result->error = DUNGEON_STORAGE_BAD_SIGNATURE; goto done; }
    if (version != DUNGEON_LEGACY_VERSION && version != DUNGEON_FILE_VERSION) { result->error = DUNGEON_STORAGE_BAD_VERSION; goto done; }
    if (floors != DUNGEON_FLOOR_COUNT || width != DUNGEON_WIDTH || height != DUNGEON_HEIGHT || record_size != CELL_RECORD_SIZE) { result->error = DUNGEON_STORAGE_BAD_DIMENSIONS; goto done; }
    const bool legacy = version == DUNGEON_LEGACY_VERSION && payload_size == WORLD_PAYLOAD_SIZE && file_size == (long)LEGACY_FILE_SIZE;
    const bool version2 = version == DUNGEON_FILE_VERSION && payload_size == WORLD_PAYLOAD_SIZE + CONFIG_RECORD_SIZE + RESERVED_SIZE && file_size == (long)FILE_SIZE;
    if (!legacy && !version2) { result->error = DUNGEON_STORAGE_BAD_FILE_SIZE; goto done; }
    initDungeonMap();
    initDungeonConfig();
    DungeonWorld *world = getDungeonWorld();
    uint32_t checksum = CHECKSUM_START;
    bool read = readPosition(file, &world->entrance, result, &checksum) && readPosition(file, &world->exit, result, &checksum);
    for (unsigned index = 0; read && index < DUNGEON_FLOOR_COUNT - 1U; ++index) read = readPosition(file, &world->stairLinks[index].upper, result, &checksum) && readPosition(file, &world->stairLinks[index].lower, result, &checksum);
    for (unsigned floor = 0; read && floor < DUNGEON_FLOOR_COUNT; ++floor) for (unsigned y = 0; read && y < DUNGEON_HEIGHT; ++y) for (unsigned x = 0; read && x < DUNGEON_WIDTH; ++x) {
        DungeonCell *cell = &world->floors[floor].cells[y][x];
        read = readPayloadByte(file, &cell->exits, result, &checksum) && readPayloadByte(file, &cell->tileType, result, &checksum) && readPayloadByte(file, &cell->variant, result, &checksum) && readPayloadByte(file, &cell->explored, result, &checksum) && readPayloadU16(file, &cell->eventFlags, result, &checksum) && readPayloadU16(file, &cell->stateFlags, result, &checksum);
    }
    for (unsigned index = 0; read && version2 && index < TAIL_SIZE; ++index) read = readPayloadByte(file, &file_tail[index], result, &checksum);
    if (!read || result->bytes != (uint32_t)file_size) goto read_failed;
    if (checksum != stored_checksum) { result->error = DUNGEON_STORAGE_CHECKSUM_FAILED; goto done; }
    loaded_version = version;
    result->error = DUNGEON_STORAGE_OK;
    fclose(file);
    return true;

read_failed:
    result->error = DUNGEON_STORAGE_READ_FAILED;
done:
    fclose(file);
    return false;
}
saveDungeonWorld() · source line 193
bool saveDungeonWorld(DungeonStorageResult *result)

Calls into this documentation:

  1. Decisionresult == NULL || (loaded_version != DUNGEON_LEGACY_VERSION && loaded_version != DUNGEON_FILE_VERSION)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executememset(result, 0, sizeof(*result));
  3. Executeresult->error = DUNGEON_STORAGE_OPEN_WRITE_FAILED;
  4. Executeerrno = 0;
  5. ExecuteFILE *file = fopen(DUNGEON_FILE, "wb");
  6. Decisionfile == NULL
    True
      1. Executeresult->errno_value = errno;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  7. Executeconst bool version2 = loaded_version == DUNGEON_FILE_VERSION;
  8. Executeconst uint32_t payload_size = WORLD_PAYLOAD_SIZE + (version2 ? TAIL_SIZE : 0U);
  9. Executeconst uint32_t file_size = HEADER_SIZE + payload_size;
  10. Executeconst uint8_t magic[] = { 'M', 'A', 'Z', 'E' };
  11. Executeconst DungeonWorld *world = getDungeonWorld();
  12. Executeuint32_t checksum = CHECKSUM_START;
  13. Executebool written = fwrite(magic, 1, sizeof(magic), file) == sizeof(magic);
  14. Executeresult->bytes += written ? sizeof(magic) : 0U;
  15. Executewritten = written && writeU16(file, loaded_version, result) && writeByte(file, DUNGEON_FLOOR_COUNT, result) && writeByte(file, DUNGEON_WIDTH, result) && writeByte(file, DUNGEON_HEIGHT, result) && writeByte(file, CELL_RECORD_SIZE, result) && writeU32(file, payload_size, result) && writeU32(file, 0U, result);
  16. Executeconst long checksum_offset = 14L;
  17. Executeconst DungeonPosition *positions[] = { &world->entrance, &world->exit };
  18. FOR loopunsigned index = 0; written && index < sizeof(positions) / sizeof(positions[0]); ++index

    Repeat body while the loop condition permits; then continue below.

    1. Executewritten = writePayloadByte(file, positions[index]->floor, result, &checksum) && writePayloadByte(file, positions[index]->x, result, &checksum) && writePayloadByte(file, positions[index]->y, result, &checksum);
  19. FOR loopunsigned index = 0; written && index < DUNGEON_FLOOR_COUNT - 1U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst DungeonStairLink *link = &world->stairLinks[index];
      2. Executewritten = writePayloadByte(file, link->upper.floor, result, &checksum) && writePayloadByte(file, link->upper.x, result, &checksum) && writePayloadByte(file, link->upper.y, result, &checksum) && writePayloadByte(file, link->lower.floor, result, &checksum) && writePayloadByte(file, link->lower.x, result, &checksum) && writePayloadByte(file, link->lower.y, result, &checksum);
  20. FOR loopunsigned floor = 0; written && floor < DUNGEON_FLOOR_COUNT; ++floor

    Repeat body while the loop condition permits; then continue below.

    1. FOR loopunsigned y = 0; written && y < DUNGEON_HEIGHT; ++y

      Repeat body while the loop condition permits; then continue below.

      1. FOR loopunsigned x = 0; written && x < DUNGEON_WIDTH; ++x

        Repeat body while the loop condition permits; then continue below.

          1. Executeconst DungeonCell *cell = &world->floors[floor].cells[y][x];
          2. Executewritten = writePayloadByte(file, cell->exits, result, &checksum) && writePayloadByte(file, cell->tileType, result, &checksum) && writePayloadByte(file, cell->variant, result, &checksum) && writePayloadByte(file, cell->explored, result, &checksum) && writePayloadU16(file, cell->eventFlags, result, &checksum) && writePayloadU16(file, cell->stateFlags, result, &checksum);
  21. FOR loopunsigned index = 0; written && version2 && index < TAIL_SIZE; ++index

    Repeat body while the loop condition permits; then continue below.

    1. Executewritten = writePayloadByte(file, file_tail[index], result, &checksum);
  22. Decisionwritten && fseek(file, checksum_offset, SEEK_SET) == 0
    True
      1. Executeuint8_t bytes[] = { (uint8_t)checksum, (uint8_t)(checksum >> 8), (uint8_t)(checksum >> 16), (uint8_t)(checksum >> 24) };
      2. Executewritten = fwrite(bytes, 1, sizeof(bytes), file) == sizeof(bytes);
    False
    1. Executewritten = false;
  23. Executeconst int close_result = fclose(file);
  24. Decision!written || close_result != 0 || result->bytes != file_size
    True
      1. Executeresult->error = DUNGEON_STORAGE_WRITE_FAILED;
      2. Executeresult->errno_value = errno;
      3. Exit this functionreturn false;
    False

    Continue after this decision.

  25. Executeresult->error = DUNGEON_STORAGE_OK;
  26. Exit this functionreturn true;
Read original function source
bool saveDungeonWorld(DungeonStorageResult *result)
{
    if (result == NULL || (loaded_version != DUNGEON_LEGACY_VERSION && loaded_version != DUNGEON_FILE_VERSION)) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_STORAGE_OPEN_WRITE_FAILED;
    errno = 0;
    FILE *file = fopen(DUNGEON_FILE, "wb");
    if (file == NULL) { result->errno_value = errno; return false; }
    const bool version2 = loaded_version == DUNGEON_FILE_VERSION;
    const uint32_t payload_size = WORLD_PAYLOAD_SIZE + (version2 ? TAIL_SIZE : 0U);
    const uint32_t file_size = HEADER_SIZE + payload_size;
    const uint8_t magic[] = { 'M', 'A', 'Z', 'E' };
    const DungeonWorld *world = getDungeonWorld();
    uint32_t checksum = CHECKSUM_START;
    bool written = fwrite(magic, 1, sizeof(magic), file) == sizeof(magic);
    result->bytes += written ? sizeof(magic) : 0U;
    written = written && writeU16(file, loaded_version, result) && writeByte(file, DUNGEON_FLOOR_COUNT, result) && writeByte(file, DUNGEON_WIDTH, result) && writeByte(file, DUNGEON_HEIGHT, result) && writeByte(file, CELL_RECORD_SIZE, result) && writeU32(file, payload_size, result) && writeU32(file, 0U, result);
    const long checksum_offset = 14L;
    const DungeonPosition *positions[] = { &world->entrance, &world->exit };
    for (unsigned index = 0; written && index < sizeof(positions) / sizeof(positions[0]); ++index)
        written = writePayloadByte(file, positions[index]->floor, result, &checksum) && writePayloadByte(file, positions[index]->x, result, &checksum) && writePayloadByte(file, positions[index]->y, result, &checksum);
    for (unsigned index = 0; written && index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        written = writePayloadByte(file, link->upper.floor, result, &checksum) && writePayloadByte(file, link->upper.x, result, &checksum) && writePayloadByte(file, link->upper.y, result, &checksum) && writePayloadByte(file, link->lower.floor, result, &checksum) && writePayloadByte(file, link->lower.x, result, &checksum) && writePayloadByte(file, link->lower.y, result, &checksum);
    }
    for (unsigned floor = 0; written && floor < DUNGEON_FLOOR_COUNT; ++floor) for (unsigned y = 0; written && y < DUNGEON_HEIGHT; ++y) for (unsigned x = 0; written && x < DUNGEON_WIDTH; ++x) {
        const DungeonCell *cell = &world->floors[floor].cells[y][x];
        written = writePayloadByte(file, cell->exits, result, &checksum) && writePayloadByte(file, cell->tileType, result, &checksum) && writePayloadByte(file, cell->variant, result, &checksum) && writePayloadByte(file, cell->explored, result, &checksum) && writePayloadU16(file, cell->eventFlags, result, &checksum) && writePayloadU16(file, cell->stateFlags, result, &checksum);
    }
    for (unsigned index = 0; written && version2 && index < TAIL_SIZE; ++index) written = writePayloadByte(file, file_tail[index], result, &checksum);
    if (written && fseek(file, checksum_offset, SEEK_SET) == 0) {
        uint8_t bytes[] = { (uint8_t)checksum, (uint8_t)(checksum >> 8), (uint8_t)(checksum >> 16), (uint8_t)(checksum >> 24) };
        written = fwrite(bytes, 1, sizeof(bytes), file) == sizeof(bytes);
    } else written = false;
    const int close_result = fclose(file);
    if (!written || close_result != 0 || result->bytes != file_size) {
        result->error = DUNGEON_STORAGE_WRITE_FAILED;
        result->errno_value = errno;
        return false;
    }
    result->error = DUNGEON_STORAGE_OK;
    return true;
}
getDungeonWorldChecksum() · source line 237
uint32_t getDungeonWorldChecksum(void)

Calls into this documentation:

  1. Executeconst DungeonWorld *world = getDungeonWorld();
  2. Executeuint32_t checksum = CHECKSUM_START;
  3. Executeconst DungeonPosition *positions[] = { &world->entrance, &world->exit };
  4. FOR loopunsigned index = 0; index < sizeof(positions) / sizeof(positions[0]); ++index

    Repeat body while the loop condition permits; then continue below.

      1. ExecuteupdateChecksum(&checksum, positions[index]->floor);
      2. ExecuteupdateChecksum(&checksum, positions[index]->x);
      3. ExecuteupdateChecksum(&checksum, positions[index]->y);
  5. FOR loopunsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst DungeonStairLink *link = &world->stairLinks[index];
      2. Executeconst DungeonPosition *stairs[] = { &link->upper, &link->lower };
      3. FOR loopunsigned position = 0; position < sizeof(stairs) / sizeof(stairs[0]); ++position

        Repeat body while the loop condition permits; then continue below.

          1. ExecuteupdateChecksum(&checksum, stairs[position]->floor);
          2. ExecuteupdateChecksum(&checksum, stairs[position]->x);
          3. ExecuteupdateChecksum(&checksum, stairs[position]->y);
  6. FOR loopunsigned floor = 0; floor < DUNGEON_FLOOR_COUNT; ++floor

    Repeat body while the loop condition permits; then continue below.

      1. FOR loopunsigned y = 0; y < DUNGEON_HEIGHT; ++y

        Repeat body while the loop condition permits; then continue below.

          1. FOR loopunsigned x = 0; x < DUNGEON_WIDTH; ++x

            Repeat body while the loop condition permits; then continue below.

              1. Executeconst DungeonCell *cell = &world->floors[floor].cells[y][x];
              2. ExecuteupdateChecksum(&checksum, cell->exits);
              3. ExecuteupdateChecksum(&checksum, cell->tileType);
              4. ExecuteupdateChecksum(&checksum, cell->variant);
              5. ExecuteupdateChecksum(&checksum, (uint8_t)(cell->eventFlags & 0xFFU));
              6. ExecuteupdateChecksum(&checksum, (uint8_t)(cell->eventFlags >> 8));
  7. Exit this functionreturn checksum;
Read original function source
uint32_t getDungeonWorldChecksum(void)
{
    const DungeonWorld *world = getDungeonWorld();
    uint32_t checksum = CHECKSUM_START;
    const DungeonPosition *positions[] = { &world->entrance, &world->exit };
    for (unsigned index = 0; index < sizeof(positions) / sizeof(positions[0]); ++index) {
        updateChecksum(&checksum, positions[index]->floor);
        updateChecksum(&checksum, positions[index]->x);
        updateChecksum(&checksum, positions[index]->y);
    }
    for (unsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        const DungeonPosition *stairs[] = { &link->upper, &link->lower };
        for (unsigned position = 0; position < sizeof(stairs) / sizeof(stairs[0]); ++position) {
            updateChecksum(&checksum, stairs[position]->floor);
            updateChecksum(&checksum, stairs[position]->x);
            updateChecksum(&checksum, stairs[position]->y);
        }
    }
    for (unsigned floor = 0; floor < DUNGEON_FLOOR_COUNT; ++floor) {
        for (unsigned y = 0; y < DUNGEON_HEIGHT; ++y) {
            for (unsigned x = 0; x < DUNGEON_WIDTH; ++x) {
                const DungeonCell *cell = &world->floors[floor].cells[y][x];
                updateChecksum(&checksum, cell->exits);
                updateChecksum(&checksum, cell->tileType);
                updateChecksum(&checksum, cell->variant);
                updateChecksum(&checksum, (uint8_t)(cell->eventFlags & 0xFFU));
                updateChecksum(&checksum, (uint8_t)(cell->eventFlags >> 8));
            }
        }
    }
    return checksum;
}
dungeonStorageErrorText() · source line 271
const char *dungeonStorageErrorText(DungeonStorageError error)
  1. Select a caseerror

    Enter matching case; follow source fall-through until a break, return or end.

      1. Case / labelcase DUNGEON_STORAGE_OK:
      2. Exit this functionreturn "OK";
      3. Case / labelcase DUNGEON_STORAGE_OPEN_READ_FAILED:
      4. Exit this functionreturn "Cannot open file";
      5. Case / labelcase DUNGEON_STORAGE_READ_FAILED:
      6. Exit this functionreturn "Read failed";
      7. Case / labelcase DUNGEON_STORAGE_OPEN_WRITE_FAILED:
      8. Exit this functionreturn "Cannot create file";
      9. Case / labelcase DUNGEON_STORAGE_WRITE_FAILED:
      10. Exit this functionreturn "File save failed";
      11. Case / labelcase DUNGEON_STORAGE_BAD_SIGNATURE:
      12. Exit this functionreturn "Bad signature";
      13. Case / labelcase DUNGEON_STORAGE_BAD_VERSION:
      14. Exit this functionreturn "Bad version";
      15. Case / labelcase DUNGEON_STORAGE_BAD_DIMENSIONS:
      16. Exit this functionreturn "Bad dimensions";
      17. Case / labelcase DUNGEON_STORAGE_BAD_FILE_SIZE:
      18. Exit this functionreturn "Bad file size";
      19. Case / labelcase DUNGEON_STORAGE_CHECKSUM_FAILED:
      20. Exit this functionreturn "Checksum failed";
      21. Case / labeldefault:
      22. Exit this functionreturn "Unknown error";
Read original function source
const char *dungeonStorageErrorText(DungeonStorageError error)
{
    switch (error) {
        case DUNGEON_STORAGE_OK: return "OK";
        case DUNGEON_STORAGE_OPEN_READ_FAILED: return "Cannot open file";
        case DUNGEON_STORAGE_READ_FAILED: return "Read failed";
        case DUNGEON_STORAGE_OPEN_WRITE_FAILED: return "Cannot create file";
        case DUNGEON_STORAGE_WRITE_FAILED: return "File save failed";
        case DUNGEON_STORAGE_BAD_SIGNATURE: return "Bad signature";
        case DUNGEON_STORAGE_BAD_VERSION: return "Bad version";
        case DUNGEON_STORAGE_BAD_DIMENSIONS: return "Bad dimensions";
        case DUNGEON_STORAGE_BAD_FILE_SIZE: return "Bad file size";
        case DUNGEON_STORAGE_CHECKSUM_FAILED: return "Checksum failed";
        default: return "Unknown error";
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "dungeon_storage.h"
#include "dungeon_config.h"
#include "dungeon_map.h"
#include <errno.h>
#include "esp_log.h"
#include <stdio.h>
#include <string.h>

#define DUNGEON_FILE "/spiffs/dungeon.dat"
#define DUNGEON_PLAYER_FILE "/spiffs/dungeon_player.dat"
#define DUNGEON_EXPLORED_FILE "/spiffs/dungeon_explored.dat"
#define DUNGEON_HISTORY_FILE "/spiffs/dungeon_highscores.dat"
#define DUNGEON_LEGACY_VERSION 1U
#define DUNGEON_FILE_VERSION 2U
#define CELL_RECORD_SIZE 8U
#define WORLD_PAYLOAD_SIZE (6U + ((DUNGEON_FLOOR_COUNT - 1U) * 6U) + (DUNGEON_FLOOR_COUNT * DUNGEON_WIDTH * DUNGEON_HEIGHT * CELL_RECORD_SIZE))
#define CONFIG_RECORD_SIZE 159U
#define RESERVED_SIZE 128U
#define TAIL_SIZE (CONFIG_RECORD_SIZE + RESERVED_SIZE)
#define HEADER_SIZE 18U
#define LEGACY_FILE_SIZE (HEADER_SIZE + WORLD_PAYLOAD_SIZE)
#define FILE_SIZE (LEGACY_FILE_SIZE + CONFIG_RECORD_SIZE + RESERVED_SIZE)
#define CHECKSUM_START 2166136261UL
#define CHECKSUM_PRIME 16777619UL

static uint8_t file_tail[TAIL_SIZE];
static uint16_t loaded_version;

bool deleteDungeonDataFiles(DungeonDeleteResult *result)
{
    static const char *const files[] = {
        DUNGEON_FILE,
        DUNGEON_PLAYER_FILE,
        DUNGEON_EXPLORED_FILE,
        DUNGEON_HISTORY_FILE,
    };
    if (result == NULL) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_DELETE_OK;
    for (unsigned index = 0U; index < sizeof(files) / sizeof(files[0]); ++index) {
        errno = 0;
        if (remove(files[index]) == 0) {
            ++result->deleted;
            /* ESP_LOGI("dungeon_storage", "DELETE %s OK", files[index]); */
        } else if (errno == ENOENT) {
            ++result->missing;
            /* ESP_LOGI("dungeon_storage", "DELETE %s MISSING", files[index]); */
        } else {
            result->error = DUNGEON_DELETE_FAILED;
            result->errno_value = errno;
            result->failed_path = files[index];
            /* ESP_LOGI("dungeon_storage", "DELETE %s FAILED errno=%d", files[index], errno); */
            return false;
        }
    }
    /* ESP_LOGI("dungeon_storage", "DELETE COMPLETE deleted=%u missing=%u", (unsigned)result->deleted, (unsigned)result->missing); */
    return true;
}

void prepareNewDungeonWorldStorage(void)
{
    loaded_version = DUNGEON_FILE_VERSION;
    memset(file_tail, 0, sizeof(file_tail));
}

static bool readByte(FILE *file, uint8_t *value, DungeonStorageResult *result)
{
    if (fread(value, 1, 1, file) != 1U) return false;
    ++result->bytes;
    return true;
}

static bool readU16(FILE *file, uint16_t *value, DungeonStorageResult *result)
{
    uint8_t bytes[2];
    if (fread(bytes, 1, sizeof(bytes), file) != sizeof(bytes)) return false;
    result->bytes += sizeof(bytes);
    *value = (uint16_t)bytes[0] | ((uint16_t)bytes[1] << 8);
    return true;
}

static bool readU32(FILE *file, uint32_t *value, DungeonStorageResult *result)
{
    uint8_t bytes[4];
    if (fread(bytes, 1, sizeof(bytes), file) != sizeof(bytes)) return false;
    result->bytes += sizeof(bytes);
    *value = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24);
    return true;
}

static bool writeByte(FILE *file, uint8_t value, DungeonStorageResult *result)
{
    if (fwrite(&value, 1, 1, file) != 1U) return false;
    ++result->bytes;
    return true;
}

static bool writeU16(FILE *file, uint16_t value, DungeonStorageResult *result)
{
    return writeByte(file, (uint8_t)value, result) && writeByte(file, (uint8_t)(value >> 8), result);
}

static bool writeU32(FILE *file, uint32_t value, DungeonStorageResult *result)
{
    return writeByte(file, (uint8_t)value, result) && writeByte(file, (uint8_t)(value >> 8), result) &&
           writeByte(file, (uint8_t)(value >> 16), result) && writeByte(file, (uint8_t)(value >> 24), result);
}

static void updateChecksum(uint32_t *checksum, uint8_t value)
{
    *checksum ^= value;
    *checksum *= CHECKSUM_PRIME;
}

static bool readPayloadByte(FILE *file, uint8_t *value, DungeonStorageResult *result, uint32_t *checksum)
{
    return readByte(file, value, result) ? (updateChecksum(checksum, *value), true) : false;
}

static bool writePayloadByte(FILE *file, uint8_t value, DungeonStorageResult *result, uint32_t *checksum)
{
    if (!writeByte(file, value, result)) return false;
    updateChecksum(checksum, value);
    return true;
}

static bool writePayloadU16(FILE *file, uint16_t value, DungeonStorageResult *result, uint32_t *checksum)
{
    return writePayloadByte(file, (uint8_t)value, result, checksum) && writePayloadByte(file, (uint8_t)(value >> 8), result, checksum);
}

static bool readPayloadU16(FILE *file, uint16_t *value, DungeonStorageResult *result, uint32_t *checksum)
{
    if (!readU16(file, value, result)) return false;
    updateChecksum(checksum, (uint8_t)(*value & 0xFFU));
    updateChecksum(checksum, (uint8_t)(*value >> 8));
    return true;
}

static bool readPosition(FILE *file, DungeonPosition *position, DungeonStorageResult *result, uint32_t *checksum)
{
    return readPayloadByte(file, &position->floor, result, checksum) && readPayloadByte(file, &position->x, result, checksum) && readPayloadByte(file, &position->y, result, checksum);
}

bool loadDungeonWorld(DungeonStorageResult *result)
{
    if (result == NULL) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_STORAGE_OPEN_READ_FAILED;
    FILE *file = fopen(DUNGEON_FILE, "rb");
    if (file == NULL) return false;
    if (fseek(file, 0L, SEEK_END) != 0) goto read_failed;
    const long file_size = ftell(file);
    if (file_size != (long)LEGACY_FILE_SIZE && file_size != (long)FILE_SIZE) { result->error = DUNGEON_STORAGE_BAD_FILE_SIZE; goto done; }
    rewind(file);
    uint8_t magic[4], floors, width, height, record_size;
    uint16_t version;
    uint32_t payload_size, stored_checksum;
    if (fread(magic, 1, sizeof(magic), file) != sizeof(magic)) goto read_failed;
    result->bytes += sizeof(magic);
    if (!readU16(file, &version, result) || !readByte(file, &floors, result) || !readByte(file, &width, result) || !readByte(file, &height, result) || !readByte(file, &record_size, result) || !readU32(file, &payload_size, result) || !readU32(file, &stored_checksum, result)) goto read_failed;
    if (memcmp(magic, "MAZE", sizeof(magic)) != 0) { result->error = DUNGEON_STORAGE_BAD_SIGNATURE; goto done; }
    if (version != DUNGEON_LEGACY_VERSION && version != DUNGEON_FILE_VERSION) { result->error = DUNGEON_STORAGE_BAD_VERSION; goto done; }
    if (floors != DUNGEON_FLOOR_COUNT || width != DUNGEON_WIDTH || height != DUNGEON_HEIGHT || record_size != CELL_RECORD_SIZE) { result->error = DUNGEON_STORAGE_BAD_DIMENSIONS; goto done; }
    const bool legacy = version == DUNGEON_LEGACY_VERSION && payload_size == WORLD_PAYLOAD_SIZE && file_size == (long)LEGACY_FILE_SIZE;
    const bool version2 = version == DUNGEON_FILE_VERSION && payload_size == WORLD_PAYLOAD_SIZE + CONFIG_RECORD_SIZE + RESERVED_SIZE && file_size == (long)FILE_SIZE;
    if (!legacy && !version2) { result->error = DUNGEON_STORAGE_BAD_FILE_SIZE; goto done; }
    initDungeonMap();
    initDungeonConfig();
    DungeonWorld *world = getDungeonWorld();
    uint32_t checksum = CHECKSUM_START;
    bool read = readPosition(file, &world->entrance, result, &checksum) && readPosition(file, &world->exit, result, &checksum);
    for (unsigned index = 0; read && index < DUNGEON_FLOOR_COUNT - 1U; ++index) read = readPosition(file, &world->stairLinks[index].upper, result, &checksum) && readPosition(file, &world->stairLinks[index].lower, result, &checksum);
    for (unsigned floor = 0; read && floor < DUNGEON_FLOOR_COUNT; ++floor) for (unsigned y = 0; read && y < DUNGEON_HEIGHT; ++y) for (unsigned x = 0; read && x < DUNGEON_WIDTH; ++x) {
        DungeonCell *cell = &world->floors[floor].cells[y][x];
        read = readPayloadByte(file, &cell->exits, result, &checksum) && readPayloadByte(file, &cell->tileType, result, &checksum) && readPayloadByte(file, &cell->variant, result, &checksum) && readPayloadByte(file, &cell->explored, result, &checksum) && readPayloadU16(file, &cell->eventFlags, result, &checksum) && readPayloadU16(file, &cell->stateFlags, result, &checksum);
    }
    for (unsigned index = 0; read && version2 && index < TAIL_SIZE; ++index) read = readPayloadByte(file, &file_tail[index], result, &checksum);
    if (!read || result->bytes != (uint32_t)file_size) goto read_failed;
    if (checksum != stored_checksum) { result->error = DUNGEON_STORAGE_CHECKSUM_FAILED; goto done; }
    loaded_version = version;
    result->error = DUNGEON_STORAGE_OK;
    fclose(file);
    return true;

read_failed:
    result->error = DUNGEON_STORAGE_READ_FAILED;
done:
    fclose(file);
    return false;
}

bool saveDungeonWorld(DungeonStorageResult *result)
{
    if (result == NULL || (loaded_version != DUNGEON_LEGACY_VERSION && loaded_version != DUNGEON_FILE_VERSION)) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_STORAGE_OPEN_WRITE_FAILED;
    errno = 0;
    FILE *file = fopen(DUNGEON_FILE, "wb");
    if (file == NULL) { result->errno_value = errno; return false; }
    const bool version2 = loaded_version == DUNGEON_FILE_VERSION;
    const uint32_t payload_size = WORLD_PAYLOAD_SIZE + (version2 ? TAIL_SIZE : 0U);
    const uint32_t file_size = HEADER_SIZE + payload_size;
    const uint8_t magic[] = { 'M', 'A', 'Z', 'E' };
    const DungeonWorld *world = getDungeonWorld();
    uint32_t checksum = CHECKSUM_START;
    bool written = fwrite(magic, 1, sizeof(magic), file) == sizeof(magic);
    result->bytes += written ? sizeof(magic) : 0U;
    written = written && writeU16(file, loaded_version, result) && writeByte(file, DUNGEON_FLOOR_COUNT, result) && writeByte(file, DUNGEON_WIDTH, result) && writeByte(file, DUNGEON_HEIGHT, result) && writeByte(file, CELL_RECORD_SIZE, result) && writeU32(file, payload_size, result) && writeU32(file, 0U, result);
    const long checksum_offset = 14L;
    const DungeonPosition *positions[] = { &world->entrance, &world->exit };
    for (unsigned index = 0; written && index < sizeof(positions) / sizeof(positions[0]); ++index)
        written = writePayloadByte(file, positions[index]->floor, result, &checksum) && writePayloadByte(file, positions[index]->x, result, &checksum) && writePayloadByte(file, positions[index]->y, result, &checksum);
    for (unsigned index = 0; written && index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        written = writePayloadByte(file, link->upper.floor, result, &checksum) && writePayloadByte(file, link->upper.x, result, &checksum) && writePayloadByte(file, link->upper.y, result, &checksum) && writePayloadByte(file, link->lower.floor, result, &checksum) && writePayloadByte(file, link->lower.x, result, &checksum) && writePayloadByte(file, link->lower.y, result, &checksum);
    }
    for (unsigned floor = 0; written && floor < DUNGEON_FLOOR_COUNT; ++floor) for (unsigned y = 0; written && y < DUNGEON_HEIGHT; ++y) for (unsigned x = 0; written && x < DUNGEON_WIDTH; ++x) {
        const DungeonCell *cell = &world->floors[floor].cells[y][x];
        written = writePayloadByte(file, cell->exits, result, &checksum) && writePayloadByte(file, cell->tileType, result, &checksum) && writePayloadByte(file, cell->variant, result, &checksum) && writePayloadByte(file, cell->explored, result, &checksum) && writePayloadU16(file, cell->eventFlags, result, &checksum) && writePayloadU16(file, cell->stateFlags, result, &checksum);
    }
    for (unsigned index = 0; written && version2 && index < TAIL_SIZE; ++index) written = writePayloadByte(file, file_tail[index], result, &checksum);
    if (written && fseek(file, checksum_offset, SEEK_SET) == 0) {
        uint8_t bytes[] = { (uint8_t)checksum, (uint8_t)(checksum >> 8), (uint8_t)(checksum >> 16), (uint8_t)(checksum >> 24) };
        written = fwrite(bytes, 1, sizeof(bytes), file) == sizeof(bytes);
    } else written = false;
    const int close_result = fclose(file);
    if (!written || close_result != 0 || result->bytes != file_size) {
        result->error = DUNGEON_STORAGE_WRITE_FAILED;
        result->errno_value = errno;
        return false;
    }
    result->error = DUNGEON_STORAGE_OK;
    return true;
}

uint32_t getDungeonWorldChecksum(void)
{
    const DungeonWorld *world = getDungeonWorld();
    uint32_t checksum = CHECKSUM_START;
    const DungeonPosition *positions[] = { &world->entrance, &world->exit };
    for (unsigned index = 0; index < sizeof(positions) / sizeof(positions[0]); ++index) {
        updateChecksum(&checksum, positions[index]->floor);
        updateChecksum(&checksum, positions[index]->x);
        updateChecksum(&checksum, positions[index]->y);
    }
    for (unsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        const DungeonPosition *stairs[] = { &link->upper, &link->lower };
        for (unsigned position = 0; position < sizeof(stairs) / sizeof(stairs[0]); ++position) {
            updateChecksum(&checksum, stairs[position]->floor);
            updateChecksum(&checksum, stairs[position]->x);
            updateChecksum(&checksum, stairs[position]->y);
        }
    }
    for (unsigned floor = 0; floor < DUNGEON_FLOOR_COUNT; ++floor) {
        for (unsigned y = 0; y < DUNGEON_HEIGHT; ++y) {
            for (unsigned x = 0; x < DUNGEON_WIDTH; ++x) {
                const DungeonCell *cell = &world->floors[floor].cells[y][x];
                updateChecksum(&checksum, cell->exits);
                updateChecksum(&checksum, cell->tileType);
                updateChecksum(&checksum, cell->variant);
                updateChecksum(&checksum, (uint8_t)(cell->eventFlags & 0xFFU));
                updateChecksum(&checksum, (uint8_t)(cell->eventFlags >> 8));
            }
        }
    }
    return checksum;
}

const char *dungeonStorageErrorText(DungeonStorageError error)
{
    switch (error) {
        case DUNGEON_STORAGE_OK: return "OK";
        case DUNGEON_STORAGE_OPEN_READ_FAILED: return "Cannot open file";
        case DUNGEON_STORAGE_READ_FAILED: return "Read failed";
        case DUNGEON_STORAGE_OPEN_WRITE_FAILED: return "Cannot create file";
        case DUNGEON_STORAGE_WRITE_FAILED: return "File save failed";
        case DUNGEON_STORAGE_BAD_SIGNATURE: return "Bad signature";
        case DUNGEON_STORAGE_BAD_VERSION: return "Bad version";
        case DUNGEON_STORAGE_BAD_DIMENSIONS: return "Bad dimensions";
        case DUNGEON_STORAGE_BAD_FILE_SIZE: return "Bad file size";
        case DUNGEON_STORAGE_CHECKSUM_FAILED: return "Checksum failed";
        default: return "Unknown error";
    }
}