Files
h-school/src/HSchool.Content/MapValidator.cs
T

172 lines
5.8 KiB
C#

namespace HSchool.Content;
/// <summary>
/// Checks that a map instance is a connected undirected graph rooted at a yard, with every
/// <c>def</c> present and non-abstract in the school's catalog.
/// </summary>
public static class MapValidator
{
public static void Validate(MapLayout map, DefCatalog catalog)
{
if (map.Territory is not { Id.Length: > 0, Def.Length: > 0 } territory)
{
throw new MapValidationException("The map has no territory.");
}
if (map.Rooms.Count == 0)
{
throw new MapValidationException("A map needs a territory and at least one room.");
}
RequireConcrete(catalog, DefKind.Territory, territory.Def, territory.Id);
var ids = new Dictionary<string, string>(StringComparer.Ordinal);
AddId(ids, territory.Id, "territory");
var buildings = new Dictionary<string, BuildingNode>(StringComparer.Ordinal);
foreach (var building in map.Buildings)
{
AddId(ids, building.Id, "building");
RequireConcrete(catalog, DefKind.Building, building.Def, building.Id);
buildings[building.Id] = building;
}
var floors = new Dictionary<string, FloorNode>(StringComparer.Ordinal);
foreach (var floor in map.Floors)
{
AddId(ids, floor.Id, "floor");
RequireConcrete(catalog, DefKind.Floor, floor.Def, floor.Id);
if (!buildings.ContainsKey(floor.Building))
{
throw new MapValidationException($"Floor '{floor.Id}' references unknown building '{floor.Building}'.");
}
floors[floor.Id] = floor;
}
var rooms = new Dictionary<string, RoomNode>(StringComparer.Ordinal);
foreach (var room in map.Rooms)
{
AddId(ids, room.Id, "room");
RequireConcrete(catalog, DefKind.Room, room.Def, room.Id);
if (!buildings.ContainsKey(room.Building))
{
throw new MapValidationException($"Room '{room.Id}' references unknown building '{room.Building}'.");
}
if (!floors.TryGetValue(room.Floor, out var floor))
{
throw new MapValidationException($"Room '{room.Id}' references unknown floor '{room.Floor}'.");
}
if (!floor.Building.Equals(room.Building, StringComparison.Ordinal))
{
throw new MapValidationException($"Room '{room.Id}' is on floor '{room.Floor}', which belongs to another building.");
}
ValidateSlotFills(room, catalog);
rooms[room.Id] = room;
}
var walkable = new HashSet<string>(StringComparer.Ordinal) { territory.Id };
foreach (var roomId in rooms.Keys)
{
walkable.Add(roomId);
}
var adjacency = walkable.ToDictionary(id => id, _ => new List<string>(), StringComparer.Ordinal);
foreach (var link in map.Links)
{
if (!walkable.Contains(link.A) || !walkable.Contains(link.B))
{
throw new MapValidationException($"Link '{link.A}' → '{link.B}' points at an unknown node.");
}
if (link.A.Equals(link.B, StringComparison.Ordinal))
{
throw new MapValidationException($"Link '{link.A}' → '{link.B}' is a self-loop.");
}
adjacency[link.A].Add(link.B);
adjacency[link.B].Add(link.A);
}
foreach (var (id, neighbours) in adjacency)
{
if (neighbours.Count == 0)
{
throw new MapValidationException($"Node '{id}' is isolated.");
}
}
var seen = new HashSet<string>(StringComparer.Ordinal);
var queue = new Queue<string>();
queue.Enqueue(territory.Id);
seen.Add(territory.Id);
while (queue.Count > 0)
{
var id = queue.Dequeue();
foreach (var next in adjacency[id])
{
if (seen.Add(next))
{
queue.Enqueue(next);
}
}
}
if (seen.Count != walkable.Count)
{
throw new MapValidationException("The map graph is not connected.");
}
}
private static void ValidateSlotFills(RoomNode room, DefCatalog catalog)
{
if (!catalog.Rooms.TryGetValue(room.Def, out var roomDef))
{
return;
}
var keys = roomDef.Slots.Select(slot => slot.Key).ToHashSet(StringComparer.Ordinal);
foreach (var fill in room.Slots)
{
if (!keys.Contains(fill.Key))
{
throw new MapValidationException($"Room '{room.Id}' fills unknown slot '{fill.Key}'.");
}
if (!catalog.Things.TryGetValue(fill.Thing, out var thing) || thing.Abstract)
{
throw new MapValidationException($"Room '{room.Id}' slot '{fill.Key}' uses unknown or abstract ThingDef '{fill.Thing}'.");
}
}
}
private static void RequireConcrete(DefCatalog catalog, DefKind kind, string defName, string nodeId)
{
if (!catalog.TryGet(kind, defName, out var def))
{
throw new MapValidationException($"Unknown {kind} '{defName}' on '{nodeId}'.");
}
if (def.Abstract)
{
throw new MapValidationException($"Abstract def '{defName}' cannot be placed on the map ('{nodeId}').");
}
}
private static void AddId(Dictionary<string, string> ids, string id, string kind)
{
if (string.IsNullOrWhiteSpace(id))
{
throw new MapValidationException($"A {kind} is missing an id.");
}
if (!ids.TryAdd(id, kind))
{
throw new MapValidationException($"Map id '{id}' is used more than once.");
}
}
}