172 lines
5.8 KiB
C#
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.");
|
|
}
|
|
}
|
|
}
|