phantasia

Phantasia - 2D SDL3 RPG prototype.
git clone git://git.beep.wimdupont.com/phantasia.git
Log | Files | Refs | README | LICENSE

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 }