namespace HSchool.Content; /// /// Checks that a map instance is a connected undirected graph rooted at a yard, with every /// def present and non-abstract in the school's catalog. /// 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(StringComparer.Ordinal); AddId(ids, territory.Id, "territory"); var buildings = new Dictionary(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(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(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(StringComparer.Ordinal) { territory.Id }; foreach (var roomId in rooms.Keys) { walkable.Add(roomId); } var adjacency = walkable.ToDictionary(id => id, _ => new List(), 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(StringComparer.Ordinal); var queue = new Queue(); 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 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."); } } }