Empty layers keep furniture on the map; a known layer set and ColorDef palette reject unknown ids at load. Co-authored-by: Cursor <cursoragent@cursor.com>
637 lines
24 KiB
C#
637 lines
24 KiB
C#
using System.Text.Json.Nodes;
|
||
|
||
namespace HSchool.Content;
|
||
|
||
/// <summary>
|
||
/// Turns pack documents into a frozen <see cref="DefCatalog"/>. Callers supply already-read
|
||
/// files; this type never looks at the disk.
|
||
/// </summary>
|
||
public sealed class CatalogLoader
|
||
{
|
||
public const string CorePackId = "core";
|
||
|
||
public DefCatalog Load(
|
||
IReadOnlyList<string> packOrder,
|
||
IReadOnlyList<ContentDocument> documents,
|
||
IContentLog? log = null)
|
||
{
|
||
log ??= NullContentLog.Instance;
|
||
var order = NormalizePackOrder(packOrder);
|
||
|
||
var defs = new Dictionary<(DefKind Kind, string Name), RawDef>();
|
||
var localesRu = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
var localesEn = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
var patches = new List<(string PackId, PatchDocument Patch, string Source)>();
|
||
|
||
foreach (var packId in order)
|
||
{
|
||
foreach (var document in documents.Where(candidate => candidate.PackId == packId))
|
||
{
|
||
ReadDocument(document, defs, localesRu, localesEn, patches, log);
|
||
}
|
||
}
|
||
|
||
var resolved = ResolveInheritance(defs);
|
||
ApplyPatches(resolved, patches);
|
||
var catalog = Materialize(order, resolved, localesRu, localesEn);
|
||
ResolveReferences(catalog, log);
|
||
WarnMissingLabels(catalog, log);
|
||
return catalog;
|
||
}
|
||
|
||
public static IReadOnlyList<string> NormalizePackOrder(IReadOnlyList<string> packOrder)
|
||
{
|
||
var order = new List<string> { CorePackId };
|
||
foreach (var packId in packOrder)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(packId) || packId.Equals(CorePackId, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!order.Contains(packId, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
order.Add(packId);
|
||
}
|
||
}
|
||
|
||
return order;
|
||
}
|
||
|
||
public static MapLayout? LastDefaultMap(IReadOnlyList<string> packOrder, IReadOnlyList<ContentDocument> documents)
|
||
{
|
||
var order = NormalizePackOrder(packOrder);
|
||
MapLayout? map = null;
|
||
foreach (var packId in order)
|
||
{
|
||
foreach (var document in documents.Where(candidate => candidate.PackId == packId && PackPaths.IsDefaultMap(candidate.RelativePath)))
|
||
{
|
||
map = MapLayout.Parse(document.Text, $"{packId}:{document.RelativePath}");
|
||
}
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
private static void ReadDocument(
|
||
ContentDocument document,
|
||
Dictionary<(DefKind Kind, string Name), RawDef> defs,
|
||
Dictionary<string, string> localesRu,
|
||
Dictionary<string, string> localesEn,
|
||
List<(string PackId, PatchDocument Patch, string Source)> patches,
|
||
IContentLog log)
|
||
{
|
||
var source = $"{document.PackId}:{document.RelativePath}";
|
||
|
||
if (PackPaths.TryGetDefKind(document.RelativePath, out var kind))
|
||
{
|
||
foreach (var json in EnumerateObjects(document.Text, source))
|
||
{
|
||
var defName = ReadDefName(json, source);
|
||
var key = (kind, defName);
|
||
if (defs.TryGetValue(key, out var previous))
|
||
{
|
||
log.Warning($"Def {kind}:{defName} from pack '{document.PackId}' replaces '{previous.PackId}'.");
|
||
}
|
||
|
||
defs[key] = new RawDef(document.PackId, kind, defName, json, source);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (PackPaths.IsPatch(document.RelativePath))
|
||
{
|
||
foreach (var json in EnumerateObjects(document.Text, source))
|
||
{
|
||
var patch = Jsonc.Deserialize<PatchDocument>(json);
|
||
if (string.IsNullOrWhiteSpace(patch.Target))
|
||
{
|
||
throw new ContentLoadException($"Patch in {source} has no target.");
|
||
}
|
||
|
||
patches.Add((document.PackId, patch, source));
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (PackPaths.TryGetLocaleLanguage(document.RelativePath, out var language))
|
||
{
|
||
var table = language == "en" ? localesEn : localesRu;
|
||
MergeLocale(Jsonc.Parse(document.Text, source), table, document.PackId, log);
|
||
}
|
||
}
|
||
|
||
private static IEnumerable<JsonObject> EnumerateObjects(string text, string source)
|
||
{
|
||
var node = Jsonc.Parse(text, source);
|
||
switch (node)
|
||
{
|
||
case JsonObject obj:
|
||
yield return obj;
|
||
break;
|
||
|
||
case JsonArray array:
|
||
foreach (var item in array)
|
||
{
|
||
if (item is not JsonObject obj)
|
||
{
|
||
throw new ContentLoadException($"{source} has a non-object entry in an array.");
|
||
}
|
||
|
||
yield return obj;
|
||
}
|
||
|
||
break;
|
||
|
||
default:
|
||
throw new ContentLoadException($"{source} must be an object or an array of objects.");
|
||
}
|
||
}
|
||
|
||
private static string ReadDefName(JsonObject json, string source)
|
||
{
|
||
if (json["defName"] is JsonValue value && value.TryGetValue<string>(out var name) && !string.IsNullOrWhiteSpace(name))
|
||
{
|
||
return name;
|
||
}
|
||
|
||
throw new ContentLoadException($"A def in {source} is missing defName.");
|
||
}
|
||
|
||
private static void MergeLocale(JsonNode node, Dictionary<string, string> table, string packId, IContentLog log)
|
||
{
|
||
if (node is not JsonObject obj)
|
||
{
|
||
throw new ContentLoadException($"Localization in pack '{packId}' must be an object of strings.");
|
||
}
|
||
|
||
foreach (var property in obj)
|
||
{
|
||
if (property.Value is not JsonValue value || !value.TryGetValue<string>(out var text))
|
||
{
|
||
throw new ContentLoadException($"Localization key '{property.Key}' in pack '{packId}' is not a string.");
|
||
}
|
||
|
||
if (table.ContainsKey(property.Key))
|
||
{
|
||
log.Warning($"Locale key '{property.Key}' from pack '{packId}' replaces an earlier pack.");
|
||
}
|
||
|
||
table[property.Key] = text;
|
||
}
|
||
}
|
||
|
||
private static Dictionary<(DefKind Kind, string Name), JsonObject> ResolveInheritance(
|
||
Dictionary<(DefKind Kind, string Name), RawDef> defs)
|
||
{
|
||
var resolved = new Dictionary<(DefKind Kind, string Name), JsonObject>();
|
||
var visiting = new HashSet<(DefKind Kind, string Name)>();
|
||
|
||
foreach (var key in defs.Keys)
|
||
{
|
||
ResolveOne(key, defs, resolved, visiting);
|
||
}
|
||
|
||
return resolved;
|
||
}
|
||
|
||
private static JsonObject ResolveOne(
|
||
(DefKind Kind, string Name) key,
|
||
Dictionary<(DefKind Kind, string Name), RawDef> defs,
|
||
Dictionary<(DefKind Kind, string Name), JsonObject> resolved,
|
||
HashSet<(DefKind Kind, string Name)> visiting)
|
||
{
|
||
if (resolved.TryGetValue(key, out var already))
|
||
{
|
||
return already;
|
||
}
|
||
|
||
if (!defs.TryGetValue(key, out var raw))
|
||
{
|
||
throw new ContentLoadException($"Def {key.Kind}:{key.Name} was referenced as a parent but does not exist.");
|
||
}
|
||
|
||
if (!visiting.Add(key))
|
||
{
|
||
throw new ContentLoadException($"Def {key.Kind}:{key.Name} has a cyclic parent chain.");
|
||
}
|
||
|
||
JsonObject merged;
|
||
var parentName = raw.Json["parent"] is JsonValue parentValue && parentValue.TryGetValue<string>(out var name)
|
||
? name
|
||
: null;
|
||
|
||
if (string.IsNullOrWhiteSpace(parentName))
|
||
{
|
||
merged = (JsonObject)raw.Json.DeepClone();
|
||
}
|
||
else
|
||
{
|
||
var parentCandidates = defs.Keys.Where(candidate => candidate.Name.Equals(parentName, StringComparison.Ordinal)).ToList();
|
||
if (parentCandidates.Count == 0)
|
||
{
|
||
throw new ContentLoadException($"Def {raw.Kind}:{raw.DefName} parent '{parentName}' does not exist.");
|
||
}
|
||
|
||
if (!parentCandidates.Any(candidate => candidate.Kind == raw.Kind))
|
||
{
|
||
throw new ContentLoadException($"Def {raw.Kind}:{raw.DefName} cannot inherit from a different kind '{parentName}'.");
|
||
}
|
||
|
||
var parentJson = ResolveOne((raw.Kind, parentName), defs, resolved, visiting);
|
||
merged = Merge(parentJson, raw.Json);
|
||
}
|
||
|
||
visiting.Remove(key);
|
||
resolved[key] = merged;
|
||
return merged;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Child fields replace parent fields wholesale, including arrays.
|
||
/// <c>abstract</c> is a flag of this def, not inherited: a child of an abstract parent is
|
||
/// concrete unless it also says <c>abstract: true</c>.
|
||
/// </summary>
|
||
private static JsonObject Merge(JsonObject parent, JsonObject child)
|
||
{
|
||
var result = (JsonObject)parent.DeepClone();
|
||
foreach (var property in child)
|
||
{
|
||
result[property.Key] = property.Value?.DeepClone();
|
||
}
|
||
|
||
if (!child.ContainsKey("abstract"))
|
||
{
|
||
result.Remove("abstract");
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private static void ApplyPatches(
|
||
Dictionary<(DefKind Kind, string Name), JsonObject> resolved,
|
||
List<(string PackId, PatchDocument Patch, string Source)> patches)
|
||
{
|
||
foreach (var (_, patch, source) in patches)
|
||
{
|
||
var matches = resolved.Where(pair => pair.Key.Name.Equals(patch.Target, StringComparison.Ordinal)).ToList();
|
||
if (matches.Count == 0)
|
||
{
|
||
throw new ContentLoadException($"Patch target '{patch.Target}' was not found ({source}).");
|
||
}
|
||
|
||
foreach (var match in matches)
|
||
{
|
||
PatchApplier.Apply(match.Value, patch);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static DefCatalog Materialize(
|
||
IReadOnlyList<string> packIds,
|
||
Dictionary<(DefKind Kind, string Name), JsonObject> resolved,
|
||
Dictionary<string, string> ru,
|
||
Dictionary<string, string> en)
|
||
{
|
||
var actions = new Dictionary<string, ActionDef>(StringComparer.Ordinal);
|
||
var things = new Dictionary<string, ThingDef>(StringComparer.Ordinal);
|
||
var positions = new Dictionary<string, PositionDef>(StringComparer.Ordinal);
|
||
var works = new Dictionary<string, WorkDef>(StringComparer.Ordinal);
|
||
var rooms = new Dictionary<string, RoomDef>(StringComparer.Ordinal);
|
||
var buildings = new Dictionary<string, BuildingDef>(StringComparer.Ordinal);
|
||
var floors = new Dictionary<string, FloorDef>(StringComparer.Ordinal);
|
||
var territories = new Dictionary<string, TerritoryDef>(StringComparer.Ordinal);
|
||
var skills = new Dictionary<string, SkillDef>(StringComparer.Ordinal);
|
||
var traits = new Dictionary<string, TraitDef>(StringComparer.Ordinal);
|
||
var bodyAttributes = new Dictionary<string, BodyAttributeDef>(StringComparer.Ordinal);
|
||
var needs = new Dictionary<string, NeedDef>(StringComparer.Ordinal);
|
||
var nameSets = new Dictionary<string, NameSetDef>(StringComparer.Ordinal);
|
||
var subjects = new Dictionary<string, SubjectDef>(StringComparer.Ordinal);
|
||
var staffing = new Dictionary<string, StaffingDef>(StringComparer.Ordinal);
|
||
var dayFrames = new Dictionary<string, DayFrameDef>(StringComparer.Ordinal);
|
||
var holidays = new Dictionary<string, HolidayDef>(StringComparer.Ordinal);
|
||
var behavior = new Dictionary<string, BehaviorDef>(StringComparer.Ordinal);
|
||
var colors = new Dictionary<string, ColorDef>(StringComparer.Ordinal);
|
||
|
||
foreach (var (key, json) in resolved)
|
||
{
|
||
switch (key.Kind)
|
||
{
|
||
case DefKind.Action:
|
||
actions[key.Name] = Jsonc.Deserialize<ActionDef>(json);
|
||
break;
|
||
case DefKind.Thing:
|
||
things[key.Name] = Jsonc.Deserialize<ThingDef>(json);
|
||
break;
|
||
case DefKind.Position:
|
||
positions[key.Name] = Jsonc.Deserialize<PositionDef>(json);
|
||
break;
|
||
case DefKind.Work:
|
||
works[key.Name] = Jsonc.Deserialize<WorkDef>(json);
|
||
break;
|
||
case DefKind.Room:
|
||
rooms[key.Name] = Jsonc.Deserialize<RoomDef>(json);
|
||
break;
|
||
case DefKind.Building:
|
||
buildings[key.Name] = Jsonc.Deserialize<BuildingDef>(json);
|
||
break;
|
||
case DefKind.Floor:
|
||
floors[key.Name] = Jsonc.Deserialize<FloorDef>(json);
|
||
break;
|
||
case DefKind.Territory:
|
||
territories[key.Name] = Jsonc.Deserialize<TerritoryDef>(json);
|
||
break;
|
||
case DefKind.Skill:
|
||
skills[key.Name] = Jsonc.Deserialize<SkillDef>(json);
|
||
break;
|
||
case DefKind.Trait:
|
||
traits[key.Name] = Jsonc.Deserialize<TraitDef>(json);
|
||
break;
|
||
case DefKind.BodyAttribute:
|
||
bodyAttributes[key.Name] = Jsonc.Deserialize<BodyAttributeDef>(json);
|
||
break;
|
||
case DefKind.Need:
|
||
needs[key.Name] = Jsonc.Deserialize<NeedDef>(json);
|
||
break;
|
||
case DefKind.NameSet:
|
||
nameSets[key.Name] = Jsonc.Deserialize<NameSetDef>(json);
|
||
break;
|
||
case DefKind.Subject:
|
||
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
|
||
break;
|
||
case DefKind.Staffing:
|
||
staffing[key.Name] = Jsonc.Deserialize<StaffingDef>(json);
|
||
break;
|
||
case DefKind.DayFrame:
|
||
dayFrames[key.Name] = Jsonc.Deserialize<DayFrameDef>(json);
|
||
break;
|
||
case DefKind.Holiday:
|
||
holidays[key.Name] = Jsonc.Deserialize<HolidayDef>(json);
|
||
break;
|
||
case DefKind.Behavior:
|
||
behavior[key.Name] = Jsonc.Deserialize<BehaviorDef>(json);
|
||
break;
|
||
case DefKind.Color:
|
||
colors[key.Name] = Jsonc.Deserialize<ColorDef>(json);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return new DefCatalog(
|
||
packIds,
|
||
actions,
|
||
things,
|
||
positions,
|
||
works,
|
||
rooms,
|
||
buildings,
|
||
floors,
|
||
territories,
|
||
skills,
|
||
traits,
|
||
bodyAttributes,
|
||
needs,
|
||
nameSets,
|
||
subjects,
|
||
staffing,
|
||
dayFrames,
|
||
holidays,
|
||
behavior,
|
||
colors,
|
||
ru,
|
||
en);
|
||
}
|
||
|
||
private static void ResolveReferences(DefCatalog catalog, IContentLog log)
|
||
{
|
||
foreach (var thing in catalog.Things.Values)
|
||
{
|
||
if (thing.PupilSlots < 0 || thing.PupilSlots > byte.MaxValue)
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' pupilSlots must be 0–{byte.MaxValue}.");
|
||
}
|
||
|
||
foreach (var action in thing.Actions)
|
||
{
|
||
if (!catalog.Actions.ContainsKey(action))
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' references unknown ActionDef '{action}'.");
|
||
}
|
||
}
|
||
|
||
ValidateApparelFields(thing, catalog);
|
||
}
|
||
|
||
foreach (var color in catalog.Colors.Values)
|
||
{
|
||
ValidateColor(color);
|
||
}
|
||
|
||
foreach (var room in catalog.Rooms.Values)
|
||
{
|
||
foreach (var slot in room.Slots)
|
||
{
|
||
if (!catalog.Things.ContainsKey(slot.Thing))
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' slot '{slot.Key}' references unknown ThingDef '{slot.Thing}'.");
|
||
}
|
||
|
||
var slotted = catalog.Things[slot.Thing];
|
||
if (slotted.Layers.Count > 0 || slotted.Portable)
|
||
{
|
||
throw new ContentLoadException(
|
||
$"RoomDef '{room.DefName}' slot '{slot.Key}' cannot place '{slot.Thing}' on the map.");
|
||
}
|
||
|
||
if (slot.Count < 0 || slot.Count > byte.MaxValue)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' slot '{slot.Key}' count must be 0–{byte.MaxValue}.");
|
||
}
|
||
}
|
||
|
||
foreach (var position in room.Positions)
|
||
{
|
||
if (!catalog.Positions.ContainsKey(position))
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' references unknown PositionDef '{position}'.");
|
||
}
|
||
}
|
||
|
||
foreach (var work in room.Works)
|
||
{
|
||
if (!catalog.Works.ContainsKey(work))
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' references unknown WorkDef '{work}'.");
|
||
}
|
||
}
|
||
|
||
if (room.Homeroom)
|
||
{
|
||
if (room.Slots.Count > 0)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' is a homeroom and cannot declare named slots.");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(room.SeatThing) || !catalog.Things.TryGetValue(room.SeatThing, out var seat) || seat.Abstract)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' seatThing is missing or unknown.");
|
||
}
|
||
|
||
if (seat.PupilSlots <= 0)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' seatThing '{room.SeatThing}' must have pupilSlots.");
|
||
}
|
||
|
||
if (seat.Layers.Count > 0 || seat.Portable)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' seatThing '{room.SeatThing}' cannot be apparel or carried.");
|
||
}
|
||
|
||
if (room.DefaultSeats < 1 || room.DefaultSeats > byte.MaxValue)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' defaultSeats must be 1–{byte.MaxValue}.");
|
||
}
|
||
}
|
||
else if (!string.IsNullOrWhiteSpace(room.SeatThing) || room.DefaultSeats != 0)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' is not a homeroom and cannot set seatThing or defaultSeats.");
|
||
}
|
||
|
||
if (!room.Abstract && room.TravelMinutes <= 0)
|
||
{
|
||
throw new ContentLoadException($"RoomDef '{room.DefName}' travelMinutes must be positive.");
|
||
}
|
||
}
|
||
|
||
foreach (var territory in catalog.Territories.Values)
|
||
{
|
||
if (!territory.Abstract && territory.TravelMinutes <= 0)
|
||
{
|
||
throw new ContentLoadException($"TerritoryDef '{territory.DefName}' travelMinutes must be positive.");
|
||
}
|
||
}
|
||
|
||
PeopleDefValidator.Validate(catalog, log);
|
||
}
|
||
|
||
private static void WarnMissingLabels(DefCatalog catalog, IContentLog log)
|
||
{
|
||
foreach (var def in ConcreteDefs(catalog))
|
||
{
|
||
if (!catalog.HasText("ru", def.DefName) && !catalog.HasText("en", def.DefName))
|
||
{
|
||
log.Warning($"Def {DefCatalog.KindOf(def)}:{def.DefName} has no label in the pack locales.");
|
||
}
|
||
}
|
||
}
|
||
|
||
private static IEnumerable<Def> ConcreteDefs(DefCatalog catalog)
|
||
{
|
||
return Enumerate(catalog.Actions.Values)
|
||
.Concat(Enumerate(catalog.Things.Values))
|
||
.Concat(Enumerate(catalog.Positions.Values))
|
||
.Concat(Enumerate(catalog.Works.Values))
|
||
.Concat(Enumerate(catalog.Rooms.Values))
|
||
.Concat(Enumerate(catalog.Buildings.Values))
|
||
.Concat(Enumerate(catalog.Floors.Values))
|
||
.Concat(Enumerate(catalog.Territories.Values))
|
||
.Concat(Enumerate(catalog.Skills.Values))
|
||
.Concat(Enumerate(catalog.Traits.Values))
|
||
.Concat(Enumerate(catalog.BodyAttributes.Values))
|
||
.Concat(Enumerate(catalog.Needs.Values))
|
||
.Concat(Enumerate(catalog.NameSets.Values))
|
||
.Concat(Enumerate(catalog.Subjects.Values))
|
||
.Concat(Enumerate(catalog.Staffing.Values))
|
||
.Concat(Enumerate(catalog.DayFrames.Values))
|
||
.Concat(Enumerate(catalog.Holidays.Values))
|
||
.Concat(Enumerate(catalog.Behavior.Values))
|
||
.Concat(Enumerate(catalog.Colors.Values));
|
||
|
||
static IEnumerable<Def> Enumerate(IEnumerable<Def> defs) => defs.Where(def => !def.Abstract);
|
||
}
|
||
|
||
private static void ValidateColor(ColorDef color)
|
||
{
|
||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||
foreach (var tag in color.Tags)
|
||
{
|
||
if (!ColorTags.IsKnown(tag))
|
||
{
|
||
throw new ContentLoadException($"ColorDef '{color.DefName}' has unknown tag '{tag}'.");
|
||
}
|
||
|
||
if (!seen.Add(tag))
|
||
{
|
||
throw new ContentLoadException($"ColorDef '{color.DefName}' repeats tag '{tag}'.");
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void ValidateApparelFields(ThingDef thing, DefCatalog catalog)
|
||
{
|
||
if (thing.Mass < 0)
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' mass cannot be negative.");
|
||
}
|
||
|
||
if (thing.Insulation < 0)
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' insulation cannot be negative.");
|
||
}
|
||
|
||
if (thing.Formality is < 0 or > 100)
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' formality must be 0–100.");
|
||
}
|
||
|
||
if (thing.Age is { } age && age.Min > age.Max)
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' age min is above max.");
|
||
}
|
||
|
||
if (thing.Sex is not null
|
||
&& !thing.Sex.Equals("male", StringComparison.OrdinalIgnoreCase)
|
||
&& !thing.Sex.Equals("female", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' has unknown sex '{thing.Sex}'.");
|
||
}
|
||
|
||
if (thing.SkirtLength is not null && !SkirtLengths.IsKnown(thing.SkirtLength))
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' has unknown skirtLength '{thing.SkirtLength}'.");
|
||
}
|
||
|
||
var layers = new HashSet<string>(StringComparer.Ordinal);
|
||
foreach (var layer in thing.Layers)
|
||
{
|
||
if (!ApparelLayers.IsKnown(layer))
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' has unknown layer '{layer}'.");
|
||
}
|
||
|
||
if (!layers.Add(layer))
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' repeats layer '{layer}'.");
|
||
}
|
||
}
|
||
|
||
var colors = new HashSet<string>(StringComparer.Ordinal);
|
||
foreach (var color in thing.Colors)
|
||
{
|
||
if (!colors.Add(color))
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' repeats color '{color}'.");
|
||
}
|
||
|
||
if (!catalog.Colors.TryGetValue(color, out var def) || def.Abstract)
|
||
{
|
||
throw new ContentLoadException($"ThingDef '{thing.DefName}' references unknown ColorDef '{color}'.");
|
||
}
|
||
}
|
||
}
|
||
|
||
private sealed record RawDef(string PackId, DefKind Kind, string DefName, JsonObject Json, string Source);
|
||
}
|