validate.c (10749B)
1 #include "engine/validate.h" 2 3 #include <stdarg.h> 4 #include <stdio.h> 5 6 static int 7 ph_invalid(char *error, size_t size, const char *format, ...) 8 { 9 va_list args; 10 11 va_start(args, format); 12 if (error && size) vsnprintf(error, size, format, args); 13 va_end(args); 14 return -1; 15 } 16 17 #define FAIL(...) return ph_invalid(error, size, __VA_ARGS__) 18 19 static int 20 ph_tile_symbol(const PhAreaCatalog *catalog, unsigned char symbol) 21 { 22 int i; 23 24 for (i = 0; i < catalog->tile_count; ++i) 25 if (catalog->tiles[i].symbol == symbol) return i; 26 return -1; 27 } 28 29 static int 30 ph_validate_map(const PhWorldSetup *setup, const PhAreaDef *def, 31 char *error, size_t size) 32 { 33 const PhAreaCatalog *catalog = setup->areas; 34 PhArea area; 35 int players = 0, entities = 0, items = 0; 36 int i, j, result = -1; 37 size_t tile; 38 39 if (ph_area_catalog_load(&area, catalog, def->id) < 0) 40 FAIL("area %d (%s): cannot load map", def->id, def->path); 41 for (tile = 0; tile < (size_t)area.width * area.height; ++tile) { 42 if (area.tiles[tile] < catalog->tile_count) continue; 43 ph_invalid(error, size, "area %d (%s), tile (%zu,%zu): invalid tile index %u", 44 def->id, def->path, tile % area.width, tile / area.width, area.tiles[tile]); 45 goto done; 46 } 47 for (i = 0; i < area.marker_count; ++i) { 48 const PhAreaMarker *marker = &area.markers[i]; 49 const PhMarkerDef *entry = ph_area_catalog_marker(catalog, def->id, marker->symbol); 50 const char *reason = NULL; 51 52 if (!entry) reason = "undefined marker"; 53 else if (area.tiles[(size_t)marker->tile_y * area.width + marker->tile_x] != 54 ph_tile_symbol(catalog, entry->tile_symbol)) reason = "terrain differs from definition"; 55 else if (ph_area_tile_blocked(&area, marker->tile_x, marker->tile_y)) 56 reason = "marker is on blocked terrain"; 57 for (j = 0; j < i && !reason; ++j) { 58 if (area.markers[j].tile_x == marker->tile_x && 59 area.markers[j].tile_y == marker->tile_y) reason = "overlapping markers"; 60 if (entry->kind == PH_MARKER_ARRIVAL && area.markers[j].symbol == marker->symbol) 61 reason = "ambiguous arrival marker"; 62 } 63 if (reason) { 64 ph_invalid(error, size, "area %d (%s), marker '%c' at (%d,%d): %s", 65 def->id, def->path, marker->symbol, marker->tile_x, marker->tile_y, reason); 66 goto done; 67 } 68 players += entry->kind == PH_MARKER_PLAYER; 69 entities += entry->kind == PH_MARKER_ENTITY; 70 items += entry->kind == PH_MARKER_ITEM; 71 } 72 if (players != (def->id == setup->start_area_id) || 73 entities + 1 > PH_MAX_ENTITIES || items > PH_MAX_GROUND_ITEMS) { 74 ph_invalid(error, size, "area %d (%s): invalid spawn counts (players=%d actors=%d items=%d)", 75 def->id, def->path, players, entities, items); 76 goto done; 77 } 78 /* Check incoming portals against actual placements, not just definitions. */ 79 for (i = 0; i < catalog->marker_count; ++i) { 80 const PhMarkerDef *portal = &catalog->markers[i]; 81 int found = 0; 82 83 if (portal->kind != PH_MARKER_PORTAL || portal->destination_area_id != def->id) continue; 84 for (j = 0; j < area.marker_count; ++j) 85 found += area.markers[j].symbol == portal->destination_symbol; 86 if (found == 1) continue; 87 ph_invalid(error, size, "area %d portal '%c': destination area %d (%s) needs one arrival '%c' (found %d)", 88 portal->area_id, portal->symbol, def->id, def->path, portal->destination_symbol, found); 89 goto done; 90 } 91 result = 0; 92 done: 93 ph_area_free(&area); 94 return result; 95 } 96 97 int 98 ph_validate_world(const PhWorldSetup *setup, char *error, size_t size) 99 { 100 const PhAreaCatalog *catalog; 101 PhWorld lookup = { 0 }; 102 int i, j; 103 104 if (error && size) *error = '\0'; 105 if (!setup || !setup->areas) FAIL("world: missing setup or area catalog"); 106 catalog = setup->areas; 107 lookup.content = setup->content; 108 if (setup->content.entity_count < 1 || setup->content.entity_count > PH_MAX_ENTITY_TYPES || 109 !setup->content.entities || setup->content.item_count < 0 || 110 setup->content.item_count > PH_MAX_ITEM_TYPES || 111 (setup->content.item_count && !setup->content.items) || 112 setup->equipment_count < 0 || (setup->equipment_count && !setup->equipment)) 113 FAIL("world: invalid content counts or missing arrays"); 114 if (catalog->area_count < 1 || !catalog->areas || catalog->tile_count < 1 || 115 catalog->tile_count > 256 || !catalog->tiles || catalog->marker_count < 0 || 116 (catalog->marker_count && !catalog->markers)) 117 FAIL("world: invalid area catalog counts or missing arrays"); 118 for (i = 0; i < setup->content.item_count; ++i) { 119 const PhItemDef *item = &setup->content.items[i]; 120 121 if (item->id <= 0 || !item->name || !item->description || 122 item->equip_slot < PH_EQUIP_NONE || item->equip_slot >= PH_EQUIP_COUNT || 123 item->use < PH_ITEM_USE_NONE || item->use > PH_ITEM_USE_RESTORE_MP || 124 item->buy_value < 0 || item->sell_value < 0 || item->use_power < 0) 125 FAIL("item %d: invalid definition", item->id); 126 for (j = 0; j < i; ++j) 127 if (setup->content.items[j].id == item->id) FAIL("item %d: duplicate ID", item->id); 128 } 129 for (i = 0; i < setup->content.entity_count; ++i) { 130 const PhEntityDef *entity = &setup->content.entities[i]; 131 132 if (entity->id <= 0 || !entity->name || entity->kind < PH_ENTITY_PLAYER || 133 entity->kind > PH_ENTITY_NPC || entity->stats.max_hp < 0 || 134 (entity->kind != PH_ENTITY_NPC && entity->stats.max_hp == 0) || 135 entity->stats.max_mp < 0 || entity->move_speed < 0 || entity->vision < 0 || 136 entity->animation_frames < 0 || entity->animation_frames > PH_ANIMATION_FRAMES || 137 entity->starting_gold < 0 || entity->reward.xp < 0 || entity->reward.amount < 0) 138 FAIL("entity %d: invalid definition", entity->id); 139 for (j = 0; j < i; ++j) 140 if (setup->content.entities[j].id == entity->id) FAIL("entity %d: duplicate ID", entity->id); 141 if ((entity->reward.item_id && (!ph_world_item_def(&lookup, entity->reward.item_id) || 142 entity->reward.amount <= 0)) || (!entity->reward.item_id && entity->reward.amount)) 143 FAIL("entity %d: invalid reward item %d or amount", entity->id, entity->reward.item_id); 144 } 145 for (i = 0; i < setup->equipment_count; ++i) { 146 const PhItemDef *item; 147 148 if (!setup->equipment[i]) continue; 149 item = ph_world_item_def(&lookup, setup->equipment[i]); 150 if (!item || item->equip_slot == PH_EQUIP_NONE) 151 FAIL("starting equipment entry %d: invalid item %d", i, setup->equipment[i]); 152 for (j = 0; j < i; ++j) { 153 const PhItemDef *other = ph_world_item_def(&lookup, setup->equipment[j]); 154 if (other && other->equip_slot == item->equip_slot) 155 FAIL("starting equipment entry %d: duplicate slot %d", i, item->equip_slot); 156 } 157 } 158 for (i = 0; i < catalog->tile_count; ++i) { 159 const PhTileDef *tile = &catalog->tiles[i]; 160 161 if (!tile->symbol || tile->autotile < PH_TILE_AUTOTILE_NONE || 162 tile->autotile > PH_TILE_AUTOTILE_FIXED || 163 (tile->autotile == PH_TILE_AUTOTILE_FIXED && 164 (tile->autotile_width <= 0 || tile->autotile_height <= 0))) 165 FAIL("tile '%c' (index %d): invalid definition", tile->symbol, i); 166 for (j = 0; j < i; ++j) 167 if (catalog->tiles[j].symbol == tile->symbol) FAIL("tile '%c': duplicate symbol", tile->symbol); 168 } 169 for (i = 0; i < catalog->area_count; ++i) { 170 const PhAreaDef *area = &catalog->areas[i]; 171 172 if (area->id <= 0 || !area->path || !*area->path) FAIL("area %d: invalid definition", area->id); 173 for (j = 0; j < i; ++j) 174 if (catalog->areas[j].id == area->id) FAIL("area %d: duplicate ID", area->id); 175 } 176 if (!ph_area_catalog_area(catalog, setup->start_area_id)) 177 FAIL("world: unknown start area %d", setup->start_area_id); 178 for (i = 0; i < catalog->marker_count; ++i) { 179 const PhMarkerDef *marker = &catalog->markers[i]; 180 const PhEntityDef *entity; 181 const PhMarkerDef *arrival; 182 183 if (!ph_area_catalog_area(catalog, marker->area_id) || !marker->symbol || 184 ph_tile_symbol(catalog, marker->tile_symbol) < 0 || marker->territory_radius < 0 || 185 marker->kind < PH_MARKER_ENTITY || marker->kind > PH_MARKER_ARRIVAL) 186 FAIL("area %d marker '%c': invalid definition", marker->area_id, marker->symbol); 187 for (j = 0; j < i; ++j) 188 if (catalog->markers[j].area_id == marker->area_id && catalog->markers[j].symbol == marker->symbol) 189 FAIL("area %d marker '%c': duplicate definition", marker->area_id, marker->symbol); 190 if (marker->kind == PH_MARKER_ENTITY || marker->kind == PH_MARKER_PLAYER) { 191 entity = ph_world_entity_def(&lookup, marker->content_id); 192 if (!entity || (marker->kind == PH_MARKER_PLAYER && entity->kind != PH_ENTITY_PLAYER)) 193 FAIL("area %d marker '%c': invalid entity %d", marker->area_id, marker->symbol, marker->content_id); 194 } else if (marker->kind == PH_MARKER_ITEM) { 195 if (!ph_world_item_def(&lookup, marker->content_id) || marker->amount <= 0) 196 FAIL("area %d marker '%c': invalid item %d or amount", marker->area_id, marker->symbol, marker->content_id); 197 } else if (marker->kind == PH_MARKER_PORTAL) { 198 arrival = ph_area_catalog_marker(catalog, marker->destination_area_id, marker->destination_symbol); 199 if (!arrival || arrival->kind != PH_MARKER_ARRIVAL) 200 FAIL("area %d portal '%c': invalid destination %d marker '%c'", marker->area_id, 201 marker->symbol, marker->destination_area_id, marker->destination_symbol); 202 } 203 } 204 for (i = 0; i < catalog->area_count; ++i) 205 if (ph_validate_map(setup, &catalog->areas[i], error, size) < 0) return -1; 206 return 0; 207 } 208 209 int 210 ph_validate_spells(const PhSpellBook *spells, char *error, size_t size) 211 { 212 int i, j; 213 214 if (error && size) *error = '\0'; 215 if (!spells || spells->def_count < 0 || spells->def_count > PH_SPELL_MAX || 216 (spells->def_count && !spells->defs) || spells->learned_count < 0 || 217 spells->learned_count > PH_SPELL_MAX) FAIL("spellbook: invalid counts or missing definitions"); 218 for (i = 0; i < spells->def_count; ++i) { 219 const PhSpellDef *spell = &spells->defs[i]; 220 221 if (spell->id <= 0 || !spell->name || !spell->description || spell->mp_cost < 0 || 222 spell->power < 0 || spell->buy_value < 0 || spell->delay_percent <= 0 || 223 spell->battle_use < PH_SPELL_BATTLE_NONE || spell->battle_use > PH_SPELL_BATTLE_HEAL || 224 spell->field_use < PH_SPELL_FIELD_NONE || spell->field_use > PH_SPELL_FIELD_HEAL || 225 spell->target < PH_SPELL_TARGET_NONE || spell->target > PH_SPELL_TARGET_ALLY || 226 (spell->battle_use != PH_SPELL_BATTLE_NONE && spell->target == PH_SPELL_TARGET_NONE) || 227 spell->accuracy.value < 0 || spell->accuracy.value > 100 || spell->accuracy.turns < 0) 228 FAIL("spell %d: invalid definition", spell->id); 229 for (j = 0; j < i; ++j) 230 if (spells->defs[j].id == spell->id) FAIL("spell %d: duplicate ID", spell->id); 231 } 232 for (i = 0; i < spells->learned_count; ++i) { 233 if (!ph_spellbook_def(spells, spells->learned[i])) FAIL("spellbook: unknown learned spell %d", spells->learned[i]); 234 for (j = 0; j < i; ++j) 235 if (spells->learned[j] == spells->learned[i]) FAIL("spellbook: duplicate learned spell %d", spells->learned[i]); 236 } 237 return 0; 238 }