namespace HSchool.Content;
/// Kind of a map-tree node, matching the snapshot wire values.
public enum MapNodeKind : byte
{
Territory = 0,
Building = 1,
Floor = 2,
Room = 3,
}
/// One labelled node of a school's map, ready to put on the wire or in a test.
public sealed class MapViewNode
{
public required MapNodeKind Kind { get; init; }
public required string Id { get; init; }
/// Empty for the yard.
public required string ParentId { get; init; }
public required string Name { get; init; }
public IReadOnlyList Items { get; init; } = [];
/// Sum of ThingDef.PupilSlots × fill count for this node. Zero off rooms.
public int PupilSlots { get; init; }
public IReadOnlyList Positions { get; init; } = [];
}
/// One stacked thing in a room. The client formats Парта ×16; this is the data.
public sealed record MapViewItem(string Name, int Count);
///
/// Builds the location tree the client draws. Labels come from the frozen catalog in the
/// requested locale; people and in-place activities are not part of this view.
///
public static class MapView
{
public static IReadOnlyList Build(DefCatalog catalog, MapLayout map, string locale)
{
if (map.Territory is not { } territory)
{
return [];
}
var nodes = new List
{
Node(
MapNodeKind.Territory,
territory.Id,
parentId: string.Empty,
LabelOf(catalog, locale, DefKind.Territory, territory.Def),
items: [],
pupilSlots: 0,
PositionsOf(catalog, locale, DefKind.Territory, territory.Def)),
};
foreach (var building in map.Buildings)
{
nodes.Add(Node(
MapNodeKind.Building,
building.Id,
territory.Id,
LabelOf(catalog, locale, DefKind.Building, building.Def),
[],
pupilSlots: 0,
PositionsOf(catalog, locale, DefKind.Building, building.Def)));
}
foreach (var floor in map.Floors)
{
nodes.Add(Node(
MapNodeKind.Floor,
floor.Id,
floor.Building,
FloorName(catalog, locale, floor),
[],
pupilSlots: 0,
PositionsOf(catalog, locale, DefKind.Floor, floor.Def)));
}
foreach (var room in map.Rooms)
{
var (items, pupilSlots) = RoomContents(catalog, locale, room);
nodes.Add(Node(
MapNodeKind.Room,
room.Id,
room.Floor,
RoomName(catalog, locale, room),
items,
pupilSlots,
PositionsOf(catalog, locale, DefKind.Room, room.Def)));
}
return nodes;
}
private static MapViewNode Node(
MapNodeKind kind,
string id,
string parentId,
string name,
IReadOnlyList items,
int pupilSlots,
IReadOnlyList positions) =>
new()
{
Kind = kind,
Id = id,
ParentId = parentId,
Name = name,
Items = items,
PupilSlots = pupilSlots,
Positions = positions,
};
private static (IReadOnlyList Items, int PupilSlots) RoomContents(
DefCatalog catalog,
string locale,
RoomNode room)
{
if (catalog.Rooms.TryGetValue(room.Def, out var def) && def.Homeroom && !string.IsNullOrWhiteSpace(def.SeatThing))
{
var seats = RoomOccupancy.Quantity(room.Seats > 0 ? room.Seats : def.DefaultSeats);
return (
[new MapViewItem(LabelOf(catalog, locale, DefKind.Thing, def.SeatThing), seats)],
RoomOccupancy.PupilSlots(catalog, room));
}
var items = new List(room.Slots.Count);
foreach (var fill in room.Slots)
{
items.Add(new MapViewItem(
LabelOf(catalog, locale, DefKind.Thing, fill.Thing),
RoomOccupancy.Quantity(fill.Count)));
}
return (items, RoomOccupancy.PupilSlots(catalog, room));
}
///
/// A floor's is its designation, not a replacement name — the
/// vanilla map says "1". On its own that is a stray digit in the client's tree, so it follows
/// the localized def label: "Этаж 1", "Floor 1". Mirrored by floorName in the editor.
///
private static string FloorName(DefCatalog catalog, string locale, FloorNode floor)
{
var defLabel = LabelOf(catalog, locale, DefKind.Floor, floor.Def);
return string.IsNullOrWhiteSpace(floor.Label) ? defLabel : $"{defLabel} {floor.Label}";
}
///
/// Same rule as floors: is "101", not a replacement for "Classroom".
/// Do not treat "label equals defName" as "untranslated" — English classrooms are named Classroom.
///
private static string RoomName(DefCatalog catalog, string locale, RoomNode room)
{
var defLabel = LabelOf(catalog, locale, DefKind.Room, room.Def);
return string.IsNullOrWhiteSpace(room.Label) ? defLabel : $"{defLabel} {room.Label}";
}
private static string LabelOf(DefCatalog catalog, string locale, DefKind kind, string defName) =>
catalog.TryGet(kind, defName, out var def) ? catalog.Label(locale, def) : defName;
private static IReadOnlyList PositionsOf(DefCatalog catalog, string locale, DefKind kind, string defName)
{
var names = catalog.PositionsFor(kind, defName);
if (names.Count == 0)
{
return [];
}
var labels = new string[names.Count];
for (var i = 0; i < names.Count; i++)
{
labels[i] = LabelOf(catalog, locale, DefKind.Position, names[i]);
}
return labels;
}
}