Let the catalog describe clothes and carryables on ThingDef without dressing anyone.

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>
This commit is contained in:
Leonid Pershin
2026-08-20 02:47:23 +03:00
co-authored by Cursor
parent 450495f666
commit abcf67eaab
16 changed files with 716 additions and 15 deletions
+106 -1
View File
@@ -313,6 +313,7 @@ public sealed class CatalogLoader
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)
{
@@ -372,6 +373,9 @@ public sealed class CatalogLoader
case DefKind.Behavior:
behavior[key.Name] = Jsonc.Deserialize<BehaviorDef>(json);
break;
case DefKind.Color:
colors[key.Name] = Jsonc.Deserialize<ColorDef>(json);
break;
}
}
@@ -395,6 +399,7 @@ public sealed class CatalogLoader
dayFrames,
holidays,
behavior,
colors,
ru,
en);
}
@@ -415,6 +420,13 @@ public sealed class CatalogLoader
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)
@@ -426,6 +438,13 @@ public sealed class CatalogLoader
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}.");
@@ -465,6 +484,11 @@ public sealed class CatalogLoader
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}.");
@@ -522,10 +546,91 @@ public sealed class CatalogLoader
.Concat(Enumerate(catalog.Staffing.Values))
.Concat(Enumerate(catalog.DayFrames.Values))
.Concat(Enumerate(catalog.Holidays.Values))
.Concat(Enumerate(catalog.Behavior.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 0100.");
}
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);
}