Tile artwork

Source: src/dungeon_tiles.c · 128 lines · 8 functions.

Module responsibility

Converts generated cell shapes and variants into artwork paths and validates the required tile files.

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

singleExitSuffix() · source line 8
static const char *singleExitSuffix(uint8_t exits)
  1. Select a caseexits

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

      1. Case / labelcase DUNGEON_EXIT_NORTH:
      2. Exit this functionreturn "n";
      3. Case / labelcase DUNGEON_EXIT_EAST:
      4. Exit this functionreturn "e";
      5. Case / labelcase DUNGEON_EXIT_SOUTH:
      6. Exit this functionreturn "s";
      7. Case / labelcase DUNGEON_EXIT_WEST:
      8. Exit this functionreturn "w";
      9. Case / labeldefault:
      10. Exit this functionreturn NULL;
Read original function source
static const char *singleExitSuffix(uint8_t exits)
{
    switch (exits) {
        case DUNGEON_EXIT_NORTH: return "n";
        case DUNGEON_EXIT_EAST: return "e";
        case DUNGEON_EXIT_SOUTH: return "s";
        case DUNGEON_EXIT_WEST: return "w";
        default: return NULL;
    }
}
roundRoomExitSuffix() · source line 19
static const char *roundRoomExitSuffix(uint8_t exits)

Calls into this documentation:

  1. Decisionexits == DUNGEON_EXIT_EAST
    True
    1. Exit this functionreturn "w";
    False

    Continue after this decision.

  2. Decisionexits == DUNGEON_EXIT_WEST
    True
    1. Exit this functionreturn "e";
    False

    Continue after this decision.

  3. Exit this functionreturn singleExitSuffix(exits);
Read original function source
static const char *roundRoomExitSuffix(uint8_t exits)
{
    if (exits == DUNGEON_EXIT_EAST) return "w";
    if (exits == DUNGEON_EXIT_WEST) return "e";
    return singleExitSuffix(exits);
}
passageSuffix() · source line 26
static const char *passageSuffix(uint8_t exits)
  1. Select a caseexits

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

      1. Case / labelcase DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST:
      2. Exit this functionreturn "ne";
      3. Case / labelcase DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH:
      4. Exit this functionreturn "ns";
      5. Case / labelcase DUNGEON_EXIT_NORTH | DUNGEON_EXIT_WEST:
      6. Exit this functionreturn "nw";
      7. Case / labelcase DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH:
      8. Exit this functionreturn "es";
      9. Case / labelcase DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST:
      10. Exit this functionreturn "ew";
      11. Case / labelcase DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST:
      12. Exit this functionreturn "sw";
      13. Case / labelcase DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH:
      14. Exit this functionreturn "nes";
      15. Case / labelcase DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST:
      16. Exit this functionreturn "new";
      17. Case / labelcase DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST:
      18. Exit this functionreturn "nsw";
      19. Case / labelcase DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST:
      20. Exit this functionreturn "esw";
      21. Case / labelcase DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST:
      22. Exit this functionreturn "nsew";
      23. Case / labeldefault:
      24. Exit this functionreturn NULL;
Read original function source
static const char *passageSuffix(uint8_t exits)
{
    switch (exits) {
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST: return "ne";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH: return "ns";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_WEST: return "nw";
        case DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH: return "es";
        case DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST: return "ew";
        case DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "sw";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH: return "nes";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST: return "new";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "nsw";
        case DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "esw";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "nsew";
        default: return NULL;
    }
}
straightSuffix() · source line 44
static const char *straightSuffix(uint8_t exits)
  1. Decisionexits == (DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH)
    True
    1. Exit this functionreturn "ns";
    False

    Continue after this decision.

  2. Decisionexits == (DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST)
    True
    1. Exit this functionreturn "ew";
    False

    Continue after this decision.

  3. Exit this functionreturn NULL;
Read original function source
static const char *straightSuffix(uint8_t exits)
{
    if (exits == (DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH)) return "ns";
    if (exits == (DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST)) return "ew";
    return NULL;
}
makePath() · source line 51
static bool makePath(char *path, size_t path_size, const char *format, const char *suffix)
  1. Decisionpath == NULL || path_size == 0 || suffix == NULL
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executeconst int written = snprintf(path, path_size, format, suffix);
  3. Exit this functionreturn written > 0 && (size_t)written < path_size;
Read original function source
static bool makePath(char *path, size_t path_size, const char *format, const char *suffix)
{
    if (path == NULL || path_size == 0 || suffix == NULL) return false;
    const int written = snprintf(path, path_size, format, suffix);
    return written > 0 && (size_t)written < path_size;
}
resolveDungeonTilePath() · source line 58
bool resolveDungeonTilePath(const DungeonCell *cell, char *path, size_t path_size)

Calls into this documentation:

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

    Continue after this decision.

  2. Executeconst char *suffix = NULL;
  3. Select a casecell->tileType

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

      1. Case / labelcase DUNGEON_TILE_PASSAGE:
      2. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/Passage_way/Pass_%s_result.jpg", passageSuffix(cell->exits));
      3. Case / labelcase DUNGEON_TILE_ROOM_OPEN:
      4. Executesuffix = cell->exits == (DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST) ? "nsew" : straightSuffix(cell->exits);
      5. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/Room_open/Room_%s_result.jpg", suffix);
      6. Case / labelcase DUNGEON_TILE_DEAD_END_PASSAGE:
      7. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_passage_way/Dend_%s_result.jpg", singleExitSuffix(cell->exits));
      8. Case / labelcase DUNGEON_TILE_DEAD_END_ROOM_ROUND:
      9. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room_rnd/DE_room-rnd_%s_result.jpg", roundRoomExitSuffix(cell->exits));
      10. Case / labelcase DUNGEON_TILE_DEAD_END_ROOM_SQUARE:
      11. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room_sqr/R1_DE_%s_result.jpg", singleExitSuffix(cell->exits));
      12. Case / labelcase DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT:
      13. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room1_rnd/DE_room1_rnd_%s_result.jpg", singleExitSuffix(cell->exits));
      14. Case / labelcase DUNGEON_TILE_ENTRANCE:
      15. Case / labelcase DUNGEON_TILE_EXIT:
      16. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/Ent_Exit/EE_%s_result.jpg", singleExitSuffix(cell->exits));
      17. Case / labelcase DUNGEON_TILE_STAIRS_UP:
      18. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_U_%s_result.jpg", straightSuffix(cell->exits));
      19. Case / labelcase DUNGEON_TILE_STAIRS_DOWN:
      20. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_D_%s_result.jpg", straightSuffix(cell->exits));
      21. Case / labelcase DUNGEON_TILE_STAIRS_BOTH:
      22. Exit this functionreturn makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_UD_%s_result.jpg", straightSuffix(cell->exits));
      23. Case / labeldefault:
      24. Exit this functionreturn false;
Read original function source
bool resolveDungeonTilePath(const DungeonCell *cell, char *path, size_t path_size)
{
    if (cell == NULL) return false;
    const char *suffix = NULL;
    switch (cell->tileType) {
        case DUNGEON_TILE_PASSAGE:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Passage_way/Pass_%s_result.jpg", passageSuffix(cell->exits));
        case DUNGEON_TILE_ROOM_OPEN:
            suffix = cell->exits == (DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST) ? "nsew" : straightSuffix(cell->exits);
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Room_open/Room_%s_result.jpg", suffix);
        case DUNGEON_TILE_DEAD_END_PASSAGE:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_passage_way/Dend_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_DEAD_END_ROOM_ROUND:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room_rnd/DE_room-rnd_%s_result.jpg", roundRoomExitSuffix(cell->exits));
        case DUNGEON_TILE_DEAD_END_ROOM_SQUARE:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room_sqr/R1_DE_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room1_rnd/DE_room1_rnd_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_ENTRANCE:
        case DUNGEON_TILE_EXIT:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Ent_Exit/EE_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_STAIRS_UP:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_U_%s_result.jpg", straightSuffix(cell->exits));
        case DUNGEON_TILE_STAIRS_DOWN:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_D_%s_result.jpg", straightSuffix(cell->exits));
        case DUNGEON_TILE_STAIRS_BOTH:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_UD_%s_result.jpg", straightSuffix(cell->exits));
        default: return false;
    }
}
validateDungeonTiles() · source line 89
bool validateDungeonTiles(DungeonTileValidationStats *stats)

Calls into this documentation:

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

    Continue after this decision.

  2. Executememset(stats, 0, sizeof(*stats));
  3. Executebool checked[DUNGEON_TILE_STAIRS_BOTH + 1U][16] = {{false}};
  4. ExecuteDungeonWorld *world = getDungeonWorld();
  5. Executechar path[sizeof(stats->problemPath)];
  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. Decisioncell->tileType > DUNGEON_TILE_STAIRS_BOTH || cell->exits > 0x0FU || !resolveDungeonTilePath(cell, path, sizeof(path))
            True
              1. Execute++stats->unsupportedCells;
              2. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. Execute++stats->resolvedCells;
          4. Decisionchecked[cell->tileType][cell->exits]
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          5. Executechecked[cell->tileType][cell->exits] = true;
          6. Execute++stats->uniqueImages;
          7. Executestruct stat information;
          8. Decisionstat(path, &information) == 0
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          9. Execute++stats->missingImages;
          10. Decisionstats->problemPath[0] == '\0'
            True
              1. Executestrncpy(stats->problemPath, path, sizeof(stats->problemPath) - 1U);
              2. Executestats->problemPath[sizeof(stats->problemPath) - 1U] = '\0';
            False

            Continue after this decision.

  7. Executestats->error = stats->unsupportedCells ? DUNGEON_TILE_UNSUPPORTED : stats->missingImages ? DUNGEON_TILE_IMAGE_MISSING : DUNGEON_TILE_OK;
  8. Executestats->valid = stats->error == DUNGEON_TILE_OK;
  9. Exit this functionreturn stats->valid;
Read original function source
bool validateDungeonTiles(DungeonTileValidationStats *stats)
{
    if (stats == NULL) return false;
    memset(stats, 0, sizeof(*stats));
    bool checked[DUNGEON_TILE_STAIRS_BOTH + 1U][16] = {{false}};
    DungeonWorld *world = getDungeonWorld();
    char path[sizeof(stats->problemPath)];
    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];
        if (cell->tileType > DUNGEON_TILE_STAIRS_BOTH || cell->exits > 0x0FU || !resolveDungeonTilePath(cell, path, sizeof(path))) {
            ++stats->unsupportedCells;
            continue;
        }
        ++stats->resolvedCells;
        if (checked[cell->tileType][cell->exits]) continue;
        checked[cell->tileType][cell->exits] = true;
        ++stats->uniqueImages;
        struct stat information;
        if (stat(path, &information) == 0) continue;
        ++stats->missingImages;
        if (stats->problemPath[0] == '\0') {
            strncpy(stats->problemPath, path, sizeof(stats->problemPath) - 1U);
            stats->problemPath[sizeof(stats->problemPath) - 1U] = '\0';
        }
    }
    stats->error = stats->unsupportedCells ? DUNGEON_TILE_UNSUPPORTED :
                   stats->missingImages ? DUNGEON_TILE_IMAGE_MISSING : DUNGEON_TILE_OK;
    stats->valid = stats->error == DUNGEON_TILE_OK;
    return stats->valid;
}
dungeonTileErrorText() · source line 120
const char *dungeonTileErrorText(DungeonTileError error)
  1. Select a caseerror

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

      1. Case / labelcase DUNGEON_TILE_OK:
      2. Exit this functionreturn "OK";
      3. Case / labelcase DUNGEON_TILE_UNSUPPORTED:
      4. Exit this functionreturn "Unsupported tile";
      5. Case / labelcase DUNGEON_TILE_IMAGE_MISSING:
      6. Exit this functionreturn "Missing image";
      7. Case / labeldefault:
      8. Exit this functionreturn "Unknown error";
Read original function source
const char *dungeonTileErrorText(DungeonTileError error)
{
    switch (error) {
        case DUNGEON_TILE_OK: return "OK";
        case DUNGEON_TILE_UNSUPPORTED: return "Unsupported tile";
        case DUNGEON_TILE_IMAGE_MISSING: return "Missing image";
        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_tiles.h"
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>

#define DUNGEON_TILE_ROOT "/spiffs/dungeon/dungeon_tiles"

static const char *singleExitSuffix(uint8_t exits)
{
    switch (exits) {
        case DUNGEON_EXIT_NORTH: return "n";
        case DUNGEON_EXIT_EAST: return "e";
        case DUNGEON_EXIT_SOUTH: return "s";
        case DUNGEON_EXIT_WEST: return "w";
        default: return NULL;
    }
}

static const char *roundRoomExitSuffix(uint8_t exits)
{
    if (exits == DUNGEON_EXIT_EAST) return "w";
    if (exits == DUNGEON_EXIT_WEST) return "e";
    return singleExitSuffix(exits);
}

static const char *passageSuffix(uint8_t exits)
{
    switch (exits) {
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST: return "ne";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH: return "ns";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_WEST: return "nw";
        case DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH: return "es";
        case DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST: return "ew";
        case DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "sw";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH: return "nes";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST: return "new";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "nsw";
        case DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "esw";
        case DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST: return "nsew";
        default: return NULL;
    }
}

static const char *straightSuffix(uint8_t exits)
{
    if (exits == (DUNGEON_EXIT_NORTH | DUNGEON_EXIT_SOUTH)) return "ns";
    if (exits == (DUNGEON_EXIT_EAST | DUNGEON_EXIT_WEST)) return "ew";
    return NULL;
}

static bool makePath(char *path, size_t path_size, const char *format, const char *suffix)
{
    if (path == NULL || path_size == 0 || suffix == NULL) return false;
    const int written = snprintf(path, path_size, format, suffix);
    return written > 0 && (size_t)written < path_size;
}

bool resolveDungeonTilePath(const DungeonCell *cell, char *path, size_t path_size)
{
    if (cell == NULL) return false;
    const char *suffix = NULL;
    switch (cell->tileType) {
        case DUNGEON_TILE_PASSAGE:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Passage_way/Pass_%s_result.jpg", passageSuffix(cell->exits));
        case DUNGEON_TILE_ROOM_OPEN:
            suffix = cell->exits == (DUNGEON_EXIT_NORTH | DUNGEON_EXIT_EAST | DUNGEON_EXIT_SOUTH | DUNGEON_EXIT_WEST) ? "nsew" : straightSuffix(cell->exits);
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Room_open/Room_%s_result.jpg", suffix);
        case DUNGEON_TILE_DEAD_END_PASSAGE:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_passage_way/Dend_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_DEAD_END_ROOM_ROUND:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room_rnd/DE_room-rnd_%s_result.jpg", roundRoomExitSuffix(cell->exits));
        case DUNGEON_TILE_DEAD_END_ROOM_SQUARE:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room_sqr/R1_DE_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/DE_room1_rnd/DE_room1_rnd_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_ENTRANCE:
        case DUNGEON_TILE_EXIT:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Ent_Exit/EE_%s_result.jpg", singleExitSuffix(cell->exits));
        case DUNGEON_TILE_STAIRS_UP:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_U_%s_result.jpg", straightSuffix(cell->exits));
        case DUNGEON_TILE_STAIRS_DOWN:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_D_%s_result.jpg", straightSuffix(cell->exits));
        case DUNGEON_TILE_STAIRS_BOTH:
            return makePath(path, path_size, DUNGEON_TILE_ROOT "/Stairs/Stairs_UD_%s_result.jpg", straightSuffix(cell->exits));
        default: return false;
    }
}

bool validateDungeonTiles(DungeonTileValidationStats *stats)
{
    if (stats == NULL) return false;
    memset(stats, 0, sizeof(*stats));
    bool checked[DUNGEON_TILE_STAIRS_BOTH + 1U][16] = {{false}};
    DungeonWorld *world = getDungeonWorld();
    char path[sizeof(stats->problemPath)];
    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];
        if (cell->tileType > DUNGEON_TILE_STAIRS_BOTH || cell->exits > 0x0FU || !resolveDungeonTilePath(cell, path, sizeof(path))) {
            ++stats->unsupportedCells;
            continue;
        }
        ++stats->resolvedCells;
        if (checked[cell->tileType][cell->exits]) continue;
        checked[cell->tileType][cell->exits] = true;
        ++stats->uniqueImages;
        struct stat information;
        if (stat(path, &information) == 0) continue;
        ++stats->missingImages;
        if (stats->problemPath[0] == '\0') {
            strncpy(stats->problemPath, path, sizeof(stats->problemPath) - 1U);
            stats->problemPath[sizeof(stats->problemPath) - 1U] = '\0';
        }
    }
    stats->error = stats->unsupportedCells ? DUNGEON_TILE_UNSUPPORTED :
                   stats->missingImages ? DUNGEON_TILE_IMAGE_MISSING : DUNGEON_TILE_OK;
    stats->valid = stats->error == DUNGEON_TILE_OK;
    return stats->valid;
}

const char *dungeonTileErrorText(DungeonTileError error)
{
    switch (error) {
        case DUNGEON_TILE_OK: return "OK";
        case DUNGEON_TILE_UNSUPPORTED: return "Unsupported tile";
        case DUNGEON_TILE_IMAGE_MISSING: return "Missing image";
        default: return "Unknown error";
    }
}