Player progression

Source: src/dungeon_player.c · 257 lines · 22 functions.

Module responsibility

Owns player statistics, saved progress, experience requirements, levelling choices and validation.

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

updateChecksum() · source line 18
static void updateChecksum(uint32_t *checksum, uint8_t value)
  1. Execute*checksum = (*checksum ^ value) * CHECKSUM_PRIME;
Read original function source
static void updateChecksum(uint32_t *checksum, uint8_t value) { *checksum = (*checksum ^ value) * CHECKSUM_PRIME; }
updateChecksumU16() · source line 19
static void updateChecksumU16(uint32_t *checksum, uint16_t value)

Calls into this documentation:

  1. ExecuteupdateChecksum(checksum, (uint8_t)value);
  2. ExecuteupdateChecksum(checksum, (uint8_t)(value >> 8));
Read original function source
static void updateChecksumU16(uint32_t *checksum, uint16_t value) { updateChecksum(checksum, (uint8_t)value); updateChecksum(checksum, (uint8_t)(value >> 8)); }
updateChecksumU32() · source line 20
static void updateChecksumU32(uint32_t *checksum, uint32_t value)

Calls into this documentation:

  1. ExecuteupdateChecksum(checksum, (uint8_t)value);
  2. ExecuteupdateChecksum(checksum, (uint8_t)(value >> 8));
  3. ExecuteupdateChecksum(checksum, (uint8_t)(value >> 16));
  4. ExecuteupdateChecksum(checksum, (uint8_t)(value >> 24));
Read original function source
static void updateChecksumU32(uint32_t *checksum, uint32_t value)
{
    updateChecksum(checksum, (uint8_t)value);
    updateChecksum(checksum, (uint8_t)(value >> 8));
    updateChecksum(checksum, (uint8_t)(value >> 16));
    updateChecksum(checksum, (uint8_t)(value >> 24));
}
writeByte() · source line 28
static bool writeByte(FILE *file, uint8_t value, DungeonPlayerResult *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, DungeonPlayerResult *result)
{
    if (fwrite(&value, 1, 1, file) != 1U) return false;
    ++result->bytes;
    return true;
}
writeU16() · source line 35
static bool writeU16(FILE *file, uint16_t value, DungeonPlayerResult *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, DungeonPlayerResult *result)
{
    return writeByte(file, (uint8_t)value, result) && writeByte(file, (uint8_t)(value >> 8), result);
}
writeU32() · source line 40
static bool writeU32(FILE *file, uint32_t value, DungeonPlayerResult *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, DungeonPlayerResult *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);
}
readByte() · source line 46
static bool readByte(FILE *file, uint8_t *value, DungeonPlayerResult *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, DungeonPlayerResult *result)
{
    if (fread(value, 1, 1, file) != 1U) return false;
    ++result->bytes;
    return true;
}
readU16() · source line 53
static bool readU16(FILE *file, uint16_t *value, DungeonPlayerResult *result)

Calls into this documentation:

  1. Executeuint8_t low, high;
  2. Decision!readByte(file, &low, result) || !readByte(file, &high, result)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Execute*value = (uint16_t)low | ((uint16_t)high << 8);
  4. Exit this functionreturn true;
Read original function source
static bool readU16(FILE *file, uint16_t *value, DungeonPlayerResult *result)
{
    uint8_t low, high;
    if (!readByte(file, &low, result) || !readByte(file, &high, result)) return false;
    *value = (uint16_t)low | ((uint16_t)high << 8);
    return true;
}
readU32() · source line 61
static bool readU32(FILE *file, uint32_t *value, DungeonPlayerResult *result)

Calls into this documentation:

  1. Executeuint8_t b0, b1, b2, b3;
  2. Decision!readByte(file, &b0, result) || !readByte(file, &b1, result) || !readByte(file, &b2, result) || !readByte(file, &b3, result)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Execute*value = (uint32_t)b0 | ((uint32_t)b1 << 8) | ((uint32_t)b2 << 16) | ((uint32_t)b3 << 24);
  4. Exit this functionreturn true;
Read original function source
static bool readU32(FILE *file, uint32_t *value, DungeonPlayerResult *result)
{
    uint8_t b0, b1, b2, b3;
    if (!readByte(file, &b0, result) || !readByte(file, &b1, result) || !readByte(file, &b2, result) || !readByte(file, &b3, result)) return false;
    *value = (uint32_t)b0 | ((uint32_t)b1 << 8) | ((uint32_t)b2 << 16) | ((uint32_t)b3 << 24);
    return true;
}
setDefaults() · source line 69
static void setDefaults(void)

Calls into this documentation:

  1. Executeconst DungeonConfig *config = getDungeonConfig();
  2. Executememset(&player, 0, sizeof(player));
  3. Executeplayer.level = 1U;
  4. Executeplayer.health = config->startHealth;
  5. Executeplayer.maxHealth = config->startHealth;
  6. Executeplayer.attack = config->startAttack;
  7. Executeplayer.defence = config->startDefence;
  8. Executeplayer.position = getDungeonWorld()->entrance;
  9. Executeplayer.enteredFrom = DUNGEON_EXIT_NONE;
Read original function source
static void setDefaults(void)
{
    const DungeonConfig *config = getDungeonConfig();
    memset(&player, 0, sizeof(player));
    player.level = 1U;
    player.health = config->startHealth;
    player.maxHealth = config->startHealth;
    player.attack = config->startAttack;
    player.defence = config->startDefence;
    player.position = getDungeonWorld()->entrance;
    player.enteredFrom = DUNGEON_EXIT_NONE;
}
validPlayer() · source line 82
static bool validPlayer(void)

Calls into this documentation:

  1. Executeconst DungeonConfig *config = getDungeonConfig();
  2. Decisionplayer.position.floor >= DUNGEON_FLOOR_COUNT || player.position.x >= DUNGEON_WIDTH || player.position.y >= DUNGEON_HEIGHT
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Executeconst DungeonCell *cell = &getDungeonWorld()->floors[player.position.floor].cells[player.position.y][player.position.x];
  4. Exit this functionreturn cell->tileType != DUNGEON_TILE_EMPTY && cell->exits != DUNGEON_EXIT_NONE && player.level >= 1U && player.maxHealth >= config->startHealth && player.health <= player.maxHealth && player.attack >= config->startAttack && player.defence >= config->startDefence;
Read original function source
static bool validPlayer(void)
{
    const DungeonConfig *config = getDungeonConfig();
    if (player.position.floor >= DUNGEON_FLOOR_COUNT || player.position.x >= DUNGEON_WIDTH || player.position.y >= DUNGEON_HEIGHT) return false;
    const DungeonCell *cell = &getDungeonWorld()->floors[player.position.floor].cells[player.position.y][player.position.x];
    return cell->tileType != DUNGEON_TILE_EMPTY && cell->exits != DUNGEON_EXIT_NONE && player.level >= 1U &&
           player.maxHealth >= config->startHealth && player.health <= player.maxHealth &&
           player.attack >= config->startAttack && player.defence >= config->startDefence;
}
playerChecksum() · source line 92
static uint32_t playerChecksum(uint32_t world_checksum, const uint8_t *reserved)

Calls into this documentation:

  1. Executeuint32_t checksum = CHECKSUM_START;
  2. ExecuteupdateChecksumU32(&checksum, world_checksum);
  3. ExecuteupdateChecksum(&checksum, player.position.floor);
  4. ExecuteupdateChecksum(&checksum, player.position.x);
  5. ExecuteupdateChecksum(&checksum, player.position.y);
  6. ExecuteupdateChecksum(&checksum, player.enteredFrom);
  7. ExecuteupdateChecksumU16(&checksum, player.level);
  8. ExecuteupdateChecksumU32(&checksum, player.experience);
  9. ExecuteupdateChecksumU16(&checksum, player.health);
  10. ExecuteupdateChecksumU16(&checksum, player.maxHealth);
  11. ExecuteupdateChecksumU16(&checksum, player.attack);
  12. ExecuteupdateChecksumU16(&checksum, player.defence);
  13. ExecuteupdateChecksumU32(&checksum, player.gold);
  14. ExecuteupdateChecksumU16(&checksum, player.keys);
  15. ExecuteupdateChecksumU16(&checksum, player.healthPotions);
  16. ExecuteupdateChecksumU16(&checksum, player.unspentStatPoints);
  17. ExecuteupdateChecksumU32(&checksum, player.inventoryFlags);
  18. ExecuteupdateChecksumU16(&checksum, player.shopPurchaseCount);
  19. ExecuteupdateChecksumU32(&checksum, player.runFlags);
  20. ExecuteupdateChecksumU32(&checksum, player.moves);
  21. ExecuteupdateChecksumU32(&checksum, player.timePlayedSeconds);
  22. FOR loopunsigned index = 0; index < PLAYER_RESERVED_SIZE; ++index

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

    1. ExecuteupdateChecksum(&checksum, reserved[index]);
  23. Exit this functionreturn checksum;
Read original function source
static uint32_t playerChecksum(uint32_t world_checksum, const uint8_t *reserved)
{
    uint32_t checksum = CHECKSUM_START;
    updateChecksumU32(&checksum, world_checksum);
    updateChecksum(&checksum, player.position.floor); updateChecksum(&checksum, player.position.x); updateChecksum(&checksum, player.position.y); updateChecksum(&checksum, player.enteredFrom);
    updateChecksumU16(&checksum, player.level); updateChecksumU32(&checksum, player.experience);
    updateChecksumU16(&checksum, player.health); updateChecksumU16(&checksum, player.maxHealth); updateChecksumU16(&checksum, player.attack); updateChecksumU16(&checksum, player.defence);
    updateChecksumU32(&checksum, player.gold); updateChecksumU16(&checksum, player.keys); updateChecksumU16(&checksum, player.healthPotions); updateChecksumU16(&checksum, player.unspentStatPoints);
    updateChecksumU32(&checksum, player.inventoryFlags); updateChecksumU16(&checksum, player.shopPurchaseCount); updateChecksumU32(&checksum, player.runFlags);
    updateChecksumU32(&checksum, player.moves); updateChecksumU32(&checksum, player.timePlayedSeconds);
    for (unsigned index = 0; index < PLAYER_RESERVED_SIZE; ++index) updateChecksum(&checksum, reserved[index]);
    return checksum;
}
saveDungeonPlayer() · source line 106
bool saveDungeonPlayer(DungeonPlayerResult *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_PLAYER_OPEN_WRITE_FAILED;
  4. Executeerrno = 0;
  5. ExecuteFILE *file = fopen(PLAYER_FILE, "wb");
  6. Decisionfile == NULL
    True
      1. Executeresult->errno_value = errno;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  7. Executeconst uint8_t magic[] = { 'M', 'P', 'L', 'Y' };
  8. Executeuint8_t reserved[PLAYER_RESERVED_SIZE] = {0};
  9. Executereserved[0] = (uint8_t)player.startedAtEpoch;
  10. Executereserved[1] = (uint8_t)(player.startedAtEpoch >> 8);
  11. Executereserved[2] = (uint8_t)(player.startedAtEpoch >> 16);
  12. Executereserved[3] = (uint8_t)(player.startedAtEpoch >> 24);
  13. Executereserved[4] = (uint8_t)player.finishedAtEpoch;
  14. Executereserved[5] = (uint8_t)(player.finishedAtEpoch >> 8);
  15. Executereserved[6] = (uint8_t)(player.finishedAtEpoch >> 16);
  16. Executereserved[7] = (uint8_t)(player.finishedAtEpoch >> 24);
  17. Executeconst uint32_t world_checksum = getDungeonWorldChecksum();
  18. Executeconst uint32_t checksum = playerChecksum(world_checksum, reserved);
  19. Executebool written = fwrite(magic, 1, sizeof(magic), file) == sizeof(magic);
  20. Executeresult->bytes += written ? sizeof(magic) : 0U;
  21. Executewritten = written && writeU16(file, PLAYER_VERSION, result) && writeU16(file, PLAYER_PAYLOAD_SIZE, result) && writeU32(file, checksum, result) && writeU32(file, world_checksum, result) && writeByte(file, player.position.floor, result) && writeByte(file, player.position.x, result) && writeByte(file, player.position.y, result) && writeByte(file, player.enteredFrom, result) && writeU16(file, player.level, result) && writeU32(file, player.experience, result) && writeU16(file, player.health, result) && writeU16(file, player.maxHealth, result) && writeU16(file, player.attack, result) && writeU16(file, player.defence, result) && writeU32(file, player.gold, result) && writeU16(file, player.keys, result) && writeU16(file, player.healthPotions, result) && writeU16(file, player.unspentStatPoints, result) && writeU32(file, player.inventoryFlags, result) && writeU16(file, player.shopPurchaseCount, result) && writeU32(file, player.runFlags, result) && writeU32(file, player.moves, result) && writeU32(file, player.timePlayedSeconds, result) && fwrite(reserved, 1, sizeof(reserved), file) == sizeof(reserved);
  22. Decisionwritten
    True
    1. Executeresult->bytes += sizeof(reserved);
    False

    Continue after this decision.

  23. Executeconst int close_result = fclose(file);
  24. Decision!written || close_result != 0 || result->bytes != PLAYER_FILE_SIZE
    True
      1. Executeresult->error = DUNGEON_PLAYER_WRITE_FAILED;
      2. Executeresult->errno_value = errno;
      3. Exit this functionreturn false;
    False

    Continue after this decision.

  25. Executeresult->error = DUNGEON_PLAYER_OK;
  26. Exit this functionreturn true;
Read original function source
bool saveDungeonPlayer(DungeonPlayerResult *result)
{
    if (result == NULL) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_PLAYER_OPEN_WRITE_FAILED;
    errno = 0;
    FILE *file = fopen(PLAYER_FILE, "wb");
    if (file == NULL) { result->errno_value = errno; return false; }
    const uint8_t magic[] = { 'M', 'P', 'L', 'Y' };
    uint8_t reserved[PLAYER_RESERVED_SIZE] = {0};
    reserved[0] = (uint8_t)player.startedAtEpoch; reserved[1] = (uint8_t)(player.startedAtEpoch >> 8);
    reserved[2] = (uint8_t)(player.startedAtEpoch >> 16); reserved[3] = (uint8_t)(player.startedAtEpoch >> 24);
    reserved[4] = (uint8_t)player.finishedAtEpoch; reserved[5] = (uint8_t)(player.finishedAtEpoch >> 8);
    reserved[6] = (uint8_t)(player.finishedAtEpoch >> 16); reserved[7] = (uint8_t)(player.finishedAtEpoch >> 24);
    const uint32_t world_checksum = getDungeonWorldChecksum();
    const uint32_t checksum = playerChecksum(world_checksum, reserved);
    bool written = fwrite(magic, 1, sizeof(magic), file) == sizeof(magic);
    result->bytes += written ? sizeof(magic) : 0U;
    written = written && writeU16(file, PLAYER_VERSION, result) && writeU16(file, PLAYER_PAYLOAD_SIZE, result) &&
              writeU32(file, checksum, result) && writeU32(file, world_checksum, result) &&
              writeByte(file, player.position.floor, result) && writeByte(file, player.position.x, result) && writeByte(file, player.position.y, result) && writeByte(file, player.enteredFrom, result) &&
              writeU16(file, player.level, result) && writeU32(file, player.experience, result) && writeU16(file, player.health, result) && writeU16(file, player.maxHealth, result) &&
              writeU16(file, player.attack, result) && writeU16(file, player.defence, result) && writeU32(file, player.gold, result) &&
              writeU16(file, player.keys, result) && writeU16(file, player.healthPotions, result) && writeU16(file, player.unspentStatPoints, result) &&
              writeU32(file, player.inventoryFlags, result) && writeU16(file, player.shopPurchaseCount, result) && writeU32(file, player.runFlags, result) &&
              writeU32(file, player.moves, result) && writeU32(file, player.timePlayedSeconds, result) &&
              fwrite(reserved, 1, sizeof(reserved), file) == sizeof(reserved);
    if (written) result->bytes += sizeof(reserved);
    const int close_result = fclose(file);
    if (!written || close_result != 0 || result->bytes != PLAYER_FILE_SIZE) { result->error = DUNGEON_PLAYER_WRITE_FAILED; result->errno_value = errno; return false; }
    result->error = DUNGEON_PLAYER_OK;
    return true;
}
loadPlayer() · source line 140
static bool loadPlayer(DungeonPlayerResult *result)

Calls into this documentation:

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

    Continue after this decision.

  5. Decisionfseek(file, 0L, SEEK_END) != 0 || ftell(file) != (long)PLAYER_FILE_SIZE
    True
      1. Executeresult->error = DUNGEON_PLAYER_BAD_FILE_SIZE;
      2. Executefclose(file);
      3. Exit this functionreturn false;
    False

    Continue after this decision.

  6. Executerewind(file);
  7. Executeuint8_t magic[4], reserved[PLAYER_RESERVED_SIZE];
  8. Executeuint16_t version, payload;
  9. Executeuint32_t checksum, world_checksum;
  10. Executebool read = fread(magic, 1, sizeof(magic), file) == sizeof(magic);
  11. Executeresult->bytes += read ? sizeof(magic) : 0U;
  12. Executeread = read && readU16(file, &version, result) && readU16(file, &payload, result) && readU32(file, &checksum, result) && readU32(file, &world_checksum, result) && readByte(file, &player.position.floor, result) && readByte(file, &player.position.x, result) && readByte(file, &player.position.y, result) && readByte(file, &player.enteredFrom, result) && readU16(file, &player.level, result) && readU32(file, &player.experience, result) && readU16(file, &player.health, result) && readU16(file, &player.maxHealth, result) && readU16(file, &player.attack, result) && readU16(file, &player.defence, result) && readU32(file, &player.gold, result) && readU16(file, &player.keys, result) && readU16(file, &player.healthPotions, result) && readU16(file, &player.unspentStatPoints, result) && readU32(file, &player.inventoryFlags, result) && readU16(file, &player.shopPurchaseCount, result) && readU32(file, &player.runFlags, result) && readU32(file, &player.moves, result) && readU32(file, &player.timePlayedSeconds, result) && fread(reserved, 1, sizeof(reserved), file) == sizeof(reserved);
  13. Decisionread
    True
    1. Executeresult->bytes += sizeof(reserved);
    False

    Continue after this decision.

  14. Executefclose(file);
  15. Decision!read || result->bytes != PLAYER_FILE_SIZE
    True
      1. Executeresult->error = DUNGEON_PLAYER_READ_FAILED;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  16. Decisionmemcmp(magic, "MPLY", sizeof(magic)) != 0
    True
      1. Executeresult->error = DUNGEON_PLAYER_BAD_SIGNATURE;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  17. Decisionversion != PLAYER_VERSION
    True
      1. Executeresult->error = DUNGEON_PLAYER_BAD_VERSION;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  18. Decisionpayload != PLAYER_PAYLOAD_SIZE
    True
      1. Executeresult->error = DUNGEON_PLAYER_BAD_FILE_SIZE;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  19. Decisionworld_checksum != getDungeonWorldChecksum()
    True
      1. Executeresult->error = DUNGEON_PLAYER_WRONG_DUNGEON;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  20. Decisionchecksum != playerChecksum(world_checksum, reserved)
    True
      1. Executeresult->error = DUNGEON_PLAYER_CHECKSUM_FAILED;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  21. Executeplayer.startedAtEpoch = (uint32_t)reserved[0] | ((uint32_t)reserved[1] << 8) | ((uint32_t)reserved[2] << 16) | ((uint32_t)reserved[3] << 24);
  22. Executeplayer.finishedAtEpoch = (uint32_t)reserved[4] | ((uint32_t)reserved[5] << 8) | ((uint32_t)reserved[6] << 16) | ((uint32_t)reserved[7] << 24);
  23. Decision!validPlayer()
    True
      1. Executeresult->error = DUNGEON_PLAYER_INVALID_STATE;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  24. Executeresult->error = DUNGEON_PLAYER_OK;
  25. Exit this functionreturn true;
Read original function source
static bool loadPlayer(DungeonPlayerResult *result)
{
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_PLAYER_OPEN_READ_FAILED;
    FILE *file = fopen(PLAYER_FILE, "rb");
    if (file == NULL) return false;
    if (fseek(file, 0L, SEEK_END) != 0 || ftell(file) != (long)PLAYER_FILE_SIZE) { result->error = DUNGEON_PLAYER_BAD_FILE_SIZE; fclose(file); return false; }
    rewind(file);
    uint8_t magic[4], reserved[PLAYER_RESERVED_SIZE];
    uint16_t version, payload;
    uint32_t checksum, world_checksum;
    bool read = fread(magic, 1, sizeof(magic), file) == sizeof(magic);
    result->bytes += read ? sizeof(magic) : 0U;
    read = read && readU16(file, &version, result) && readU16(file, &payload, result) && readU32(file, &checksum, result) && readU32(file, &world_checksum, result) &&
           readByte(file, &player.position.floor, result) && readByte(file, &player.position.x, result) && readByte(file, &player.position.y, result) && readByte(file, &player.enteredFrom, result) &&
           readU16(file, &player.level, result) && readU32(file, &player.experience, result) && readU16(file, &player.health, result) && readU16(file, &player.maxHealth, result) &&
           readU16(file, &player.attack, result) && readU16(file, &player.defence, result) && readU32(file, &player.gold, result) &&
           readU16(file, &player.keys, result) && readU16(file, &player.healthPotions, result) && readU16(file, &player.unspentStatPoints, result) &&
           readU32(file, &player.inventoryFlags, result) && readU16(file, &player.shopPurchaseCount, result) && readU32(file, &player.runFlags, result) &&
           readU32(file, &player.moves, result) && readU32(file, &player.timePlayedSeconds, result) && fread(reserved, 1, sizeof(reserved), file) == sizeof(reserved);
    if (read) result->bytes += sizeof(reserved);
    fclose(file);
    if (!read || result->bytes != PLAYER_FILE_SIZE) { result->error = DUNGEON_PLAYER_READ_FAILED; return false; }
    if (memcmp(magic, "MPLY", sizeof(magic)) != 0) { result->error = DUNGEON_PLAYER_BAD_SIGNATURE; return false; }
    if (version != PLAYER_VERSION) { result->error = DUNGEON_PLAYER_BAD_VERSION; return false; }
    if (payload != PLAYER_PAYLOAD_SIZE) { result->error = DUNGEON_PLAYER_BAD_FILE_SIZE; return false; }
    if (world_checksum != getDungeonWorldChecksum()) { result->error = DUNGEON_PLAYER_WRONG_DUNGEON; return false; }
    if (checksum != playerChecksum(world_checksum, reserved)) { result->error = DUNGEON_PLAYER_CHECKSUM_FAILED; return false; }
    player.startedAtEpoch = (uint32_t)reserved[0] | ((uint32_t)reserved[1] << 8) | ((uint32_t)reserved[2] << 16) | ((uint32_t)reserved[3] << 24);
    player.finishedAtEpoch = (uint32_t)reserved[4] | ((uint32_t)reserved[5] << 8) | ((uint32_t)reserved[6] << 16) | ((uint32_t)reserved[7] << 24);
    if (!validPlayer()) { result->error = DUNGEON_PLAYER_INVALID_STATE; return false; }
    result->error = DUNGEON_PLAYER_OK;
    return true;
}
initDungeonPlayer() · source line 175
void initDungeonPlayer(void)
  1. Executememset(&player, 0, sizeof(player));
Read original function source
void initDungeonPlayer(void) { memset(&player, 0, sizeof(player)); }
prepareDungeonPlayer() · source line 177
bool prepareDungeonPlayer(DungeonPlayerResult *result)

Calls into this documentation:

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

    Continue after this decision.

  2. DecisionloadPlayer(result)
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  3. Decisionresult->error != DUNGEON_PLAYER_OPEN_READ_FAILED
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  4. ExecutesetDefaults();
  5. Decision!saveDungeonPlayer(result)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  6. Executeresult->created = true;
  7. Exit this functionreturn true;
Read original function source
bool prepareDungeonPlayer(DungeonPlayerResult *result)
{
    if (result == NULL) return false;
    if (loadPlayer(result)) return true;
    if (result->error != DUNGEON_PLAYER_OPEN_READ_FAILED) return false;
    setDefaults();
    if (!saveDungeonPlayer(result)) return false;
    result->created = true;
    return true;
}
resetDungeonPlayer() · source line 188
bool resetDungeonPlayer(DungeonPlayerResult *result)

Calls into this documentation:

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

    Continue after this decision.

  2. ExecutesetDefaults();
  3. Exit this functionreturn saveDungeonPlayer(result);
Read original function source
bool resetDungeonPlayer(DungeonPlayerResult *result)
{
    if (result == NULL) return false;
    setDefaults();
    return saveDungeonPlayer(result);
}
getDungeonPlayerState() · source line 195
DungeonPlayerState *getDungeonPlayerState(void)
  1. Exit this functionreturn &player;
Read original function source
DungeonPlayerState *getDungeonPlayerState(void) { return &player; }
dungeonExperienceNeeded() · source line 197
uint32_t dungeonExperienceNeeded(uint16_t level)

Calls into this documentation:

  1. Executeconst DungeonConfig *config = getDungeonConfig();
  2. Executeuint32_t required = config->firstLevelXp;
  3. FOR loopuint16_t current = 1U; current < level; ++current

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

      1. Executeconst uint64_t next = ((uint64_t)required * (1000U + config->xpGrowthTenthsPercent) + 500U) / 1000U;
      2. Decisionnext > UINT32_MAX
        True
        1. Exit this functionreturn UINT32_MAX;
        False

        Continue after this decision.

      3. Executerequired = (uint32_t)next;
  4. Exit this functionreturn required;
Read original function source
uint32_t dungeonExperienceNeeded(uint16_t level)
{
    const DungeonConfig *config = getDungeonConfig();
    uint32_t required = config->firstLevelXp;
    for (uint16_t current = 1U; current < level; ++current) {
        const uint64_t next = ((uint64_t)required * (1000U + config->xpGrowthTenthsPercent) + 500U) / 1000U;
        if (next > UINT32_MAX) return UINT32_MAX;
        required = (uint32_t)next;
    }
    return required;
}
addDungeonPlayerExperience() · source line 209
uint16_t addDungeonPlayerExperience(uint32_t amount)

Calls into this documentation:

  1. Executeconst DungeonConfig *config = getDungeonConfig();
  2. Executeuint64_t experience = (uint64_t)player.experience + amount;
  3. Executeuint16_t gained = 0U;
  4. WHILE loopplayer.level < UINT16_MAX && experience >= dungeonExperienceNeeded(player.level)

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

      1. Executeexperience -= dungeonExperienceNeeded(player.level);
      2. Execute++player.level;
      3. Executeplayer.maxHealth = player.maxHealth <= UINT16_MAX - config->levelHealthGain ? player.maxHealth + config->levelHealthGain : UINT16_MAX;
      4. Executeplayer.attack = player.attack <= UINT16_MAX - config->levelAttackGain ? player.attack + config->levelAttackGain : UINT16_MAX;
      5. Executeplayer.defence = player.defence <= UINT16_MAX - config->levelDefenceGain ? player.defence + config->levelDefenceGain : UINT16_MAX;
      6. Decisionplayer.unspentStatPoints < UINT16_MAX
        True
        1. Execute++player.unspentStatPoints;
        False

        Continue after this decision.

      7. Decisiongained < UINT16_MAX
        True
        1. Execute++gained;
        False

        Continue after this decision.

  5. Executeplayer.experience = experience > UINT32_MAX ? UINT32_MAX : (uint32_t)experience;
  6. Exit this functionreturn gained;
Read original function source
uint16_t addDungeonPlayerExperience(uint32_t amount)
{
    const DungeonConfig *config = getDungeonConfig();
    uint64_t experience = (uint64_t)player.experience + amount;
    uint16_t gained = 0U;
    while (player.level < UINT16_MAX && experience >= dungeonExperienceNeeded(player.level)) {
        experience -= dungeonExperienceNeeded(player.level);
        ++player.level;
        player.maxHealth = player.maxHealth <= UINT16_MAX - config->levelHealthGain ? player.maxHealth + config->levelHealthGain : UINT16_MAX;
        player.attack = player.attack <= UINT16_MAX - config->levelAttackGain ? player.attack + config->levelAttackGain : UINT16_MAX;
        player.defence = player.defence <= UINT16_MAX - config->levelDefenceGain ? player.defence + config->levelDefenceGain : UINT16_MAX;
        if (player.unspentStatPoints < UINT16_MAX) ++player.unspentStatPoints;
        if (gained < UINT16_MAX) ++gained;
    }
    player.experience = experience > UINT32_MAX ? UINT32_MAX : (uint32_t)experience;
    return gained;
}
applyDungeonPlayerStatChoice() · source line 227
bool applyDungeonPlayerStatChoice(uint8_t choice)

Calls into this documentation:

  1. Decisionplayer.unspentStatPoints == 0U
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executeconst DungeonConfig *config = getDungeonConfig();
  3. Decisionchoice == 0U
    True
    1. Executeplayer.maxHealth = player.maxHealth <= UINT16_MAX - config->chosenHealthGain ? player.maxHealth + config->chosenHealthGain : UINT16_MAX;
    False
    1. Decisionchoice == 1U
      True
      1. Executeplayer.attack = player.attack <= UINT16_MAX - config->chosenAttackGain ? player.attack + config->chosenAttackGain : UINT16_MAX;
      False
      1. Decisionchoice == 2U
        True
        1. Executeplayer.defence = player.defence <= UINT16_MAX - config->chosenDefenceGain ? player.defence + config->chosenDefenceGain : UINT16_MAX;
        False
        1. Exit this functionreturn false;
  4. Execute--player.unspentStatPoints;
  5. Executeplayer.health = player.maxHealth;
  6. Exit this functionreturn true;
Read original function source
bool applyDungeonPlayerStatChoice(uint8_t choice)
{
    if (player.unspentStatPoints == 0U) return false;
    const DungeonConfig *config = getDungeonConfig();
    if (choice == 0U) player.maxHealth = player.maxHealth <= UINT16_MAX - config->chosenHealthGain ? player.maxHealth + config->chosenHealthGain : UINT16_MAX;
    else if (choice == 1U) player.attack = player.attack <= UINT16_MAX - config->chosenAttackGain ? player.attack + config->chosenAttackGain : UINT16_MAX;
    else if (choice == 2U) player.defence = player.defence <= UINT16_MAX - config->chosenDefenceGain ? player.defence + config->chosenDefenceGain : UINT16_MAX;
    else return false;
    --player.unspentStatPoints;
    player.health = player.maxHealth;
    return true;
}
dungeonPlayerErrorText() · source line 240
const char *dungeonPlayerErrorText(DungeonPlayerError error)
  1. Select a caseerror

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

      1. Case / labelcase DUNGEON_PLAYER_OK:
      2. Exit this functionreturn "OK";
      3. Case / labelcase DUNGEON_PLAYER_OPEN_READ_FAILED:
      4. Exit this functionreturn "Cannot open player";
      5. Case / labelcase DUNGEON_PLAYER_OPEN_WRITE_FAILED:
      6. Exit this functionreturn "Cannot create player";
      7. Case / labelcase DUNGEON_PLAYER_READ_FAILED:
      8. Exit this functionreturn "Player read failed";
      9. Case / labelcase DUNGEON_PLAYER_WRITE_FAILED:
      10. Exit this functionreturn "Player save failed";
      11. Case / labelcase DUNGEON_PLAYER_BAD_SIGNATURE:
      12. Exit this functionreturn "Bad player signature";
      13. Case / labelcase DUNGEON_PLAYER_BAD_VERSION:
      14. Exit this functionreturn "Bad player version";
      15. Case / labelcase DUNGEON_PLAYER_BAD_FILE_SIZE:
      16. Exit this functionreturn "Bad player file size";
      17. Case / labelcase DUNGEON_PLAYER_CHECKSUM_FAILED:
      18. Exit this functionreturn "Player checksum failed";
      19. Case / labelcase DUNGEON_PLAYER_WRONG_DUNGEON:
      20. Exit this functionreturn "Player is for another dungeon";
      21. Case / labelcase DUNGEON_PLAYER_INVALID_POSITION:
      22. Exit this functionreturn "Bad player position";
      23. Case / labelcase DUNGEON_PLAYER_INVALID_STATE:
      24. Exit this functionreturn "Bad player state";
      25. Case / labeldefault:
      26. Exit this functionreturn "Player error";
Read original function source
const char *dungeonPlayerErrorText(DungeonPlayerError error)
{
    switch (error) {
        case DUNGEON_PLAYER_OK: return "OK";
        case DUNGEON_PLAYER_OPEN_READ_FAILED: return "Cannot open player";
        case DUNGEON_PLAYER_OPEN_WRITE_FAILED: return "Cannot create player";
        case DUNGEON_PLAYER_READ_FAILED: return "Player read failed";
        case DUNGEON_PLAYER_WRITE_FAILED: return "Player save failed";
        case DUNGEON_PLAYER_BAD_SIGNATURE: return "Bad player signature";
        case DUNGEON_PLAYER_BAD_VERSION: return "Bad player version";
        case DUNGEON_PLAYER_BAD_FILE_SIZE: return "Bad player file size";
        case DUNGEON_PLAYER_CHECKSUM_FAILED: return "Player checksum failed";
        case DUNGEON_PLAYER_WRONG_DUNGEON: return "Player is for another dungeon";
        case DUNGEON_PLAYER_INVALID_POSITION: return "Bad player position";
        case DUNGEON_PLAYER_INVALID_STATE: return "Bad player state";
        default: return "Player error";
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "dungeon_player.h"
#include "dungeon_config.h"
#include "dungeon_storage.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define PLAYER_FILE "/spiffs/dungeon_player.dat"
#define PLAYER_VERSION 6U
#define PLAYER_PAYLOAD_SIZE 114U
#define PLAYER_FILE_SIZE 126U
#define PLAYER_RESERVED_SIZE 64U
#define CHECKSUM_START 2166136261UL
#define CHECKSUM_PRIME 16777619UL

static DungeonPlayerState player;

static void updateChecksum(uint32_t *checksum, uint8_t value) { *checksum = (*checksum ^ value) * CHECKSUM_PRIME; }
static void updateChecksumU16(uint32_t *checksum, uint16_t value) { updateChecksum(checksum, (uint8_t)value); updateChecksum(checksum, (uint8_t)(value >> 8)); }
static void updateChecksumU32(uint32_t *checksum, uint32_t value)
{
    updateChecksum(checksum, (uint8_t)value);
    updateChecksum(checksum, (uint8_t)(value >> 8));
    updateChecksum(checksum, (uint8_t)(value >> 16));
    updateChecksum(checksum, (uint8_t)(value >> 24));
}

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

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

static bool writeU32(FILE *file, uint32_t value, DungeonPlayerResult *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 bool readByte(FILE *file, uint8_t *value, DungeonPlayerResult *result)
{
    if (fread(value, 1, 1, file) != 1U) return false;
    ++result->bytes;
    return true;
}

static bool readU16(FILE *file, uint16_t *value, DungeonPlayerResult *result)
{
    uint8_t low, high;
    if (!readByte(file, &low, result) || !readByte(file, &high, result)) return false;
    *value = (uint16_t)low | ((uint16_t)high << 8);
    return true;
}

static bool readU32(FILE *file, uint32_t *value, DungeonPlayerResult *result)
{
    uint8_t b0, b1, b2, b3;
    if (!readByte(file, &b0, result) || !readByte(file, &b1, result) || !readByte(file, &b2, result) || !readByte(file, &b3, result)) return false;
    *value = (uint32_t)b0 | ((uint32_t)b1 << 8) | ((uint32_t)b2 << 16) | ((uint32_t)b3 << 24);
    return true;
}

static void setDefaults(void)
{
    const DungeonConfig *config = getDungeonConfig();
    memset(&player, 0, sizeof(player));
    player.level = 1U;
    player.health = config->startHealth;
    player.maxHealth = config->startHealth;
    player.attack = config->startAttack;
    player.defence = config->startDefence;
    player.position = getDungeonWorld()->entrance;
    player.enteredFrom = DUNGEON_EXIT_NONE;
}

static bool validPlayer(void)
{
    const DungeonConfig *config = getDungeonConfig();
    if (player.position.floor >= DUNGEON_FLOOR_COUNT || player.position.x >= DUNGEON_WIDTH || player.position.y >= DUNGEON_HEIGHT) return false;
    const DungeonCell *cell = &getDungeonWorld()->floors[player.position.floor].cells[player.position.y][player.position.x];
    return cell->tileType != DUNGEON_TILE_EMPTY && cell->exits != DUNGEON_EXIT_NONE && player.level >= 1U &&
           player.maxHealth >= config->startHealth && player.health <= player.maxHealth &&
           player.attack >= config->startAttack && player.defence >= config->startDefence;
}

static uint32_t playerChecksum(uint32_t world_checksum, const uint8_t *reserved)
{
    uint32_t checksum = CHECKSUM_START;
    updateChecksumU32(&checksum, world_checksum);
    updateChecksum(&checksum, player.position.floor); updateChecksum(&checksum, player.position.x); updateChecksum(&checksum, player.position.y); updateChecksum(&checksum, player.enteredFrom);
    updateChecksumU16(&checksum, player.level); updateChecksumU32(&checksum, player.experience);
    updateChecksumU16(&checksum, player.health); updateChecksumU16(&checksum, player.maxHealth); updateChecksumU16(&checksum, player.attack); updateChecksumU16(&checksum, player.defence);
    updateChecksumU32(&checksum, player.gold); updateChecksumU16(&checksum, player.keys); updateChecksumU16(&checksum, player.healthPotions); updateChecksumU16(&checksum, player.unspentStatPoints);
    updateChecksumU32(&checksum, player.inventoryFlags); updateChecksumU16(&checksum, player.shopPurchaseCount); updateChecksumU32(&checksum, player.runFlags);
    updateChecksumU32(&checksum, player.moves); updateChecksumU32(&checksum, player.timePlayedSeconds);
    for (unsigned index = 0; index < PLAYER_RESERVED_SIZE; ++index) updateChecksum(&checksum, reserved[index]);
    return checksum;
}

bool saveDungeonPlayer(DungeonPlayerResult *result)
{
    if (result == NULL) return false;
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_PLAYER_OPEN_WRITE_FAILED;
    errno = 0;
    FILE *file = fopen(PLAYER_FILE, "wb");
    if (file == NULL) { result->errno_value = errno; return false; }
    const uint8_t magic[] = { 'M', 'P', 'L', 'Y' };
    uint8_t reserved[PLAYER_RESERVED_SIZE] = {0};
    reserved[0] = (uint8_t)player.startedAtEpoch; reserved[1] = (uint8_t)(player.startedAtEpoch >> 8);
    reserved[2] = (uint8_t)(player.startedAtEpoch >> 16); reserved[3] = (uint8_t)(player.startedAtEpoch >> 24);
    reserved[4] = (uint8_t)player.finishedAtEpoch; reserved[5] = (uint8_t)(player.finishedAtEpoch >> 8);
    reserved[6] = (uint8_t)(player.finishedAtEpoch >> 16); reserved[7] = (uint8_t)(player.finishedAtEpoch >> 24);
    const uint32_t world_checksum = getDungeonWorldChecksum();
    const uint32_t checksum = playerChecksum(world_checksum, reserved);
    bool written = fwrite(magic, 1, sizeof(magic), file) == sizeof(magic);
    result->bytes += written ? sizeof(magic) : 0U;
    written = written && writeU16(file, PLAYER_VERSION, result) && writeU16(file, PLAYER_PAYLOAD_SIZE, result) &&
              writeU32(file, checksum, result) && writeU32(file, world_checksum, result) &&
              writeByte(file, player.position.floor, result) && writeByte(file, player.position.x, result) && writeByte(file, player.position.y, result) && writeByte(file, player.enteredFrom, result) &&
              writeU16(file, player.level, result) && writeU32(file, player.experience, result) && writeU16(file, player.health, result) && writeU16(file, player.maxHealth, result) &&
              writeU16(file, player.attack, result) && writeU16(file, player.defence, result) && writeU32(file, player.gold, result) &&
              writeU16(file, player.keys, result) && writeU16(file, player.healthPotions, result) && writeU16(file, player.unspentStatPoints, result) &&
              writeU32(file, player.inventoryFlags, result) && writeU16(file, player.shopPurchaseCount, result) && writeU32(file, player.runFlags, result) &&
              writeU32(file, player.moves, result) && writeU32(file, player.timePlayedSeconds, result) &&
              fwrite(reserved, 1, sizeof(reserved), file) == sizeof(reserved);
    if (written) result->bytes += sizeof(reserved);
    const int close_result = fclose(file);
    if (!written || close_result != 0 || result->bytes != PLAYER_FILE_SIZE) { result->error = DUNGEON_PLAYER_WRITE_FAILED; result->errno_value = errno; return false; }
    result->error = DUNGEON_PLAYER_OK;
    return true;
}

static bool loadPlayer(DungeonPlayerResult *result)
{
    memset(result, 0, sizeof(*result));
    result->error = DUNGEON_PLAYER_OPEN_READ_FAILED;
    FILE *file = fopen(PLAYER_FILE, "rb");
    if (file == NULL) return false;
    if (fseek(file, 0L, SEEK_END) != 0 || ftell(file) != (long)PLAYER_FILE_SIZE) { result->error = DUNGEON_PLAYER_BAD_FILE_SIZE; fclose(file); return false; }
    rewind(file);
    uint8_t magic[4], reserved[PLAYER_RESERVED_SIZE];
    uint16_t version, payload;
    uint32_t checksum, world_checksum;
    bool read = fread(magic, 1, sizeof(magic), file) == sizeof(magic);
    result->bytes += read ? sizeof(magic) : 0U;
    read = read && readU16(file, &version, result) && readU16(file, &payload, result) && readU32(file, &checksum, result) && readU32(file, &world_checksum, result) &&
           readByte(file, &player.position.floor, result) && readByte(file, &player.position.x, result) && readByte(file, &player.position.y, result) && readByte(file, &player.enteredFrom, result) &&
           readU16(file, &player.level, result) && readU32(file, &player.experience, result) && readU16(file, &player.health, result) && readU16(file, &player.maxHealth, result) &&
           readU16(file, &player.attack, result) && readU16(file, &player.defence, result) && readU32(file, &player.gold, result) &&
           readU16(file, &player.keys, result) && readU16(file, &player.healthPotions, result) && readU16(file, &player.unspentStatPoints, result) &&
           readU32(file, &player.inventoryFlags, result) && readU16(file, &player.shopPurchaseCount, result) && readU32(file, &player.runFlags, result) &&
           readU32(file, &player.moves, result) && readU32(file, &player.timePlayedSeconds, result) && fread(reserved, 1, sizeof(reserved), file) == sizeof(reserved);
    if (read) result->bytes += sizeof(reserved);
    fclose(file);
    if (!read || result->bytes != PLAYER_FILE_SIZE) { result->error = DUNGEON_PLAYER_READ_FAILED; return false; }
    if (memcmp(magic, "MPLY", sizeof(magic)) != 0) { result->error = DUNGEON_PLAYER_BAD_SIGNATURE; return false; }
    if (version != PLAYER_VERSION) { result->error = DUNGEON_PLAYER_BAD_VERSION; return false; }
    if (payload != PLAYER_PAYLOAD_SIZE) { result->error = DUNGEON_PLAYER_BAD_FILE_SIZE; return false; }
    if (world_checksum != getDungeonWorldChecksum()) { result->error = DUNGEON_PLAYER_WRONG_DUNGEON; return false; }
    if (checksum != playerChecksum(world_checksum, reserved)) { result->error = DUNGEON_PLAYER_CHECKSUM_FAILED; return false; }
    player.startedAtEpoch = (uint32_t)reserved[0] | ((uint32_t)reserved[1] << 8) | ((uint32_t)reserved[2] << 16) | ((uint32_t)reserved[3] << 24);
    player.finishedAtEpoch = (uint32_t)reserved[4] | ((uint32_t)reserved[5] << 8) | ((uint32_t)reserved[6] << 16) | ((uint32_t)reserved[7] << 24);
    if (!validPlayer()) { result->error = DUNGEON_PLAYER_INVALID_STATE; return false; }
    result->error = DUNGEON_PLAYER_OK;
    return true;
}

void initDungeonPlayer(void) { memset(&player, 0, sizeof(player)); }

bool prepareDungeonPlayer(DungeonPlayerResult *result)
{
    if (result == NULL) return false;
    if (loadPlayer(result)) return true;
    if (result->error != DUNGEON_PLAYER_OPEN_READ_FAILED) return false;
    setDefaults();
    if (!saveDungeonPlayer(result)) return false;
    result->created = true;
    return true;
}

bool resetDungeonPlayer(DungeonPlayerResult *result)
{
    if (result == NULL) return false;
    setDefaults();
    return saveDungeonPlayer(result);
}

DungeonPlayerState *getDungeonPlayerState(void) { return &player; }

uint32_t dungeonExperienceNeeded(uint16_t level)
{
    const DungeonConfig *config = getDungeonConfig();
    uint32_t required = config->firstLevelXp;
    for (uint16_t current = 1U; current < level; ++current) {
        const uint64_t next = ((uint64_t)required * (1000U + config->xpGrowthTenthsPercent) + 500U) / 1000U;
        if (next > UINT32_MAX) return UINT32_MAX;
        required = (uint32_t)next;
    }
    return required;
}

uint16_t addDungeonPlayerExperience(uint32_t amount)
{
    const DungeonConfig *config = getDungeonConfig();
    uint64_t experience = (uint64_t)player.experience + amount;
    uint16_t gained = 0U;
    while (player.level < UINT16_MAX && experience >= dungeonExperienceNeeded(player.level)) {
        experience -= dungeonExperienceNeeded(player.level);
        ++player.level;
        player.maxHealth = player.maxHealth <= UINT16_MAX - config->levelHealthGain ? player.maxHealth + config->levelHealthGain : UINT16_MAX;
        player.attack = player.attack <= UINT16_MAX - config->levelAttackGain ? player.attack + config->levelAttackGain : UINT16_MAX;
        player.defence = player.defence <= UINT16_MAX - config->levelDefenceGain ? player.defence + config->levelDefenceGain : UINT16_MAX;
        if (player.unspentStatPoints < UINT16_MAX) ++player.unspentStatPoints;
        if (gained < UINT16_MAX) ++gained;
    }
    player.experience = experience > UINT32_MAX ? UINT32_MAX : (uint32_t)experience;
    return gained;
}

bool applyDungeonPlayerStatChoice(uint8_t choice)
{
    if (player.unspentStatPoints == 0U) return false;
    const DungeonConfig *config = getDungeonConfig();
    if (choice == 0U) player.maxHealth = player.maxHealth <= UINT16_MAX - config->chosenHealthGain ? player.maxHealth + config->chosenHealthGain : UINT16_MAX;
    else if (choice == 1U) player.attack = player.attack <= UINT16_MAX - config->chosenAttackGain ? player.attack + config->chosenAttackGain : UINT16_MAX;
    else if (choice == 2U) player.defence = player.defence <= UINT16_MAX - config->chosenDefenceGain ? player.defence + config->chosenDefenceGain : UINT16_MAX;
    else return false;
    --player.unspentStatPoints;
    player.health = player.maxHealth;
    return true;
}

const char *dungeonPlayerErrorText(DungeonPlayerError error)
{
    switch (error) {
        case DUNGEON_PLAYER_OK: return "OK";
        case DUNGEON_PLAYER_OPEN_READ_FAILED: return "Cannot open player";
        case DUNGEON_PLAYER_OPEN_WRITE_FAILED: return "Cannot create player";
        case DUNGEON_PLAYER_READ_FAILED: return "Player read failed";
        case DUNGEON_PLAYER_WRITE_FAILED: return "Player save failed";
        case DUNGEON_PLAYER_BAD_SIGNATURE: return "Bad player signature";
        case DUNGEON_PLAYER_BAD_VERSION: return "Bad player version";
        case DUNGEON_PLAYER_BAD_FILE_SIZE: return "Bad player file size";
        case DUNGEON_PLAYER_CHECKSUM_FAILED: return "Player checksum failed";
        case DUNGEON_PLAYER_WRONG_DUNGEON: return "Player is for another dungeon";
        case DUNGEON_PLAYER_INVALID_POSITION: return "Bad player position";
        case DUNGEON_PLAYER_INVALID_STATE: return "Bad player state";
        default: return "Player error";
    }
}