Merge phase 29 country.
ci / server (push) Failing after 3m53s
ci / client (push) Successful in 19s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 02:59:46 +03:00
co-authored by Cursor
59 changed files with 903 additions and 478 deletions
+11 -5
View File
@@ -307,7 +307,8 @@ public sealed class CatalogLoader
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 countries = new Dictionary<string, CountryDef>(StringComparer.Ordinal);
var climatePresets = new Dictionary<string, ClimatePresetDef>(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);
@@ -355,8 +356,11 @@ public sealed class CatalogLoader
case DefKind.Need:
needs[key.Name] = Jsonc.Deserialize<NeedDef>(json);
break;
case DefKind.NameSet:
nameSets[key.Name] = Jsonc.Deserialize<NameSetDef>(json);
case DefKind.Country:
countries[key.Name] = Jsonc.Deserialize<CountryDef>(json);
break;
case DefKind.ClimatePreset:
climatePresets[key.Name] = Jsonc.Deserialize<ClimatePresetDef>(json);
break;
case DefKind.Subject:
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
@@ -393,7 +397,8 @@ public sealed class CatalogLoader
traits,
bodyAttributes,
needs,
nameSets,
countries,
climatePresets,
subjects,
staffing,
dayFrames,
@@ -541,7 +546,8 @@ public sealed class CatalogLoader
.Concat(Enumerate(catalog.Traits.Values))
.Concat(Enumerate(catalog.BodyAttributes.Values))
.Concat(Enumerate(catalog.Needs.Values))
.Concat(Enumerate(catalog.NameSets.Values))
.Concat(Enumerate(catalog.Countries.Values))
.Concat(Enumerate(catalog.ClimatePresets.Values))
.Concat(Enumerate(catalog.Subjects.Values))
.Concat(Enumerate(catalog.Staffing.Values))
.Concat(Enumerate(catalog.DayFrames.Values))
+24 -5
View File
@@ -20,7 +20,8 @@ public sealed class DefCatalog
IReadOnlyDictionary<string, TraitDef> traits,
IReadOnlyDictionary<string, BodyAttributeDef> bodyAttributes,
IReadOnlyDictionary<string, NeedDef> needs,
IReadOnlyDictionary<string, NameSetDef> nameSets,
IReadOnlyDictionary<string, CountryDef> countries,
IReadOnlyDictionary<string, ClimatePresetDef> climatePresets,
IReadOnlyDictionary<string, SubjectDef> subjects,
IReadOnlyDictionary<string, StaffingDef> staffing,
IReadOnlyDictionary<string, DayFrameDef> dayFrames,
@@ -43,7 +44,8 @@ public sealed class DefCatalog
Traits = traits;
BodyAttributes = bodyAttributes;
Needs = needs;
NameSets = nameSets;
Countries = countries;
ClimatePresets = climatePresets;
Subjects = subjects;
Staffing = staffing;
DayFrames = dayFrames;
@@ -91,7 +93,9 @@ public sealed class DefCatalog
public IReadOnlyDictionary<string, NeedDef> Needs { get; }
public IReadOnlyDictionary<string, NameSetDef> NameSets { get; }
public IReadOnlyDictionary<string, CountryDef> Countries { get; }
public IReadOnlyDictionary<string, ClimatePresetDef> ClimatePresets { get; }
public IReadOnlyDictionary<string, SubjectDef> Subjects { get; }
@@ -133,7 +137,8 @@ public sealed class DefCatalog
DefKind.Trait => Traits.GetValueOrDefault(defName),
DefKind.BodyAttribute => BodyAttributes.GetValueOrDefault(defName),
DefKind.Need => Needs.GetValueOrDefault(defName),
DefKind.NameSet => NameSets.GetValueOrDefault(defName),
DefKind.Country => Countries.GetValueOrDefault(defName),
DefKind.ClimatePreset => ClimatePresets.GetValueOrDefault(defName),
DefKind.Subject => Subjects.GetValueOrDefault(defName),
DefKind.Staffing => Staffing.GetValueOrDefault(defName),
DefKind.DayFrame => DayFrames.GetValueOrDefault(defName),
@@ -212,7 +217,8 @@ public sealed class DefCatalog
TraitDef => DefKind.Trait,
BodyAttributeDef => DefKind.BodyAttribute,
NeedDef => DefKind.Need,
NameSetDef => DefKind.NameSet,
CountryDef => DefKind.Country,
ClimatePresetDef => DefKind.ClimatePreset,
SubjectDef => DefKind.Subject,
StaffingDef => DefKind.Staffing,
DayFrameDef => DefKind.DayFrame,
@@ -245,4 +251,17 @@ public sealed class DefCatalog
set.Remove(defName);
return set;
}
/// <summary>Nested names of a concrete country, or false when the id is missing or abstract.</summary>
public bool TryGetCountryNames(string countryId, out NameSetDef names)
{
if (Countries.TryGetValue(countryId, out var country) && !country.Abstract)
{
names = country.Names;
return true;
}
names = null!;
return false;
}
}
+2 -1
View File
@@ -14,7 +14,8 @@ public enum DefKind
Trait,
BodyAttribute,
Need,
NameSet,
Country,
ClimatePreset,
Subject,
Staffing,
DayFrame,
+5 -2
View File
@@ -105,8 +105,11 @@ internal static class PackPaths
case "needs":
kind = DefKind.Need;
return true;
case "namesets":
kind = DefKind.NameSet;
case "countries":
kind = DefKind.Country;
return true;
case "climates":
kind = DefKind.ClimatePreset;
return true;
case "subjects":
kind = DefKind.Subject;
+45 -16
View File
@@ -25,9 +25,9 @@ internal static class PeopleDefValidator
ValidateNeed(need);
}
foreach (var names in catalog.NameSets.Values)
foreach (var country in catalog.Countries.Values)
{
ValidateNameSet(names, catalog);
ValidateCountry(country, catalog);
}
foreach (var subject in catalog.Subjects.Values)
@@ -547,61 +547,90 @@ internal static class PeopleDefValidator
}
}
private static void ValidateNameSet(NameSetDef names, DefCatalog catalog)
private static void ValidateCountry(CountryDef country, DefCatalog catalog)
{
if (country.Abstract)
{
return;
}
if (country.ClimatePresets.Count == 0)
{
throw new ContentLoadException($"CountryDef '{country.DefName}' needs at least one climate preset.");
}
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var presetId in country.ClimatePresets)
{
if (string.IsNullOrWhiteSpace(presetId) || !seen.Add(presetId))
{
throw new ContentLoadException($"CountryDef '{country.DefName}' has a missing or duplicate climate preset.");
}
if (!catalog.ClimatePresets.TryGetValue(presetId, out var preset) || preset.Abstract)
{
throw new ContentLoadException($"CountryDef '{country.DefName}' references unknown ClimatePresetDef '{presetId}'.");
}
}
ValidateNameSet(country.Names ?? new NameSetDef(), catalog, country.DefName);
}
private static void ValidateNameSet(NameSetDef names, DefCatalog catalog, string countryDefName)
{
if (!NameGrammar.IsKnownPatronymic(names.PatronymicRule))
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' has unknown patronymicRule '{names.PatronymicRule}'.");
throw new ContentLoadException($"CountryDef '{countryDefName}' has unknown patronymicRule '{names.PatronymicRule}'.");
}
foreach (var native in names.Spoken)
{
if (!catalog.Skills.TryGetValue(native, out var language) || language.Abstract)
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' native language '{native}' is not a SkillDef.");
throw new ContentLoadException($"CountryDef '{countryDefName}' native language '{native}' is not a SkillDef.");
}
}
if (names.RelatedLanguageChance is < 0 or > 1)
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageChance must be 01.");
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageChance must be 01.");
}
if (names.RelatedLanguageStdDev < 0)
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageStdDev must not be negative.");
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageStdDev must not be negative.");
}
if (names.RelatedLanguageMean < 0)
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageMean must not be negative.");
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageMean must not be negative.");
}
if (names.RelatedLanguageMax < 0)
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' relatedLanguageMax must not be negative.");
throw new ContentLoadException($"CountryDef '{countryDefName}' relatedLanguageMax must not be negative.");
}
if (!NameGrammar.IsKnownGiven(names.DefaultGivenDeclension))
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' has unknown defaultGivenDeclension.");
throw new ContentLoadException($"CountryDef '{countryDefName}' has unknown defaultGivenDeclension.");
}
if (!NameGrammar.IsKnownSurname(names.DefaultSurnameDeclension))
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' has unknown defaultSurnameDeclension.");
throw new ContentLoadException($"CountryDef '{countryDefName}' has unknown defaultSurnameDeclension.");
}
if (names.MaleGiven.Count == 0 || names.FemaleGiven.Count == 0 || names.Surnames.Count == 0)
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' needs male names, female names and surnames.");
throw new ContentLoadException($"CountryDef '{countryDefName}' needs male names, female names and surnames.");
}
foreach (var given in names.MaleGiven.Concat(names.FemaleGiven))
{
if (string.IsNullOrWhiteSpace(given.Form))
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' has an empty given name.");
throw new ContentLoadException($"CountryDef '{countryDefName}' has an empty given name.");
}
if (given.Cases is null)
@@ -611,7 +640,7 @@ internal static class PeopleDefValidator
: given.Declension;
if (!NameGrammar.IsKnownGiven(model))
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' given '{given.Form}' has unknown declension '{model}'.");
throw new ContentLoadException($"CountryDef '{countryDefName}' given '{given.Form}' has unknown declension '{model}'.");
}
}
}
@@ -620,7 +649,7 @@ internal static class PeopleDefValidator
{
if (string.IsNullOrWhiteSpace(surname.Male) || string.IsNullOrWhiteSpace(surname.Female))
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' has a surname missing a gendered form.");
throw new ContentLoadException($"CountryDef '{countryDefName}' has a surname missing a gendered form.");
}
if (surname.MaleCases is null && surname.FemaleCases is null)
@@ -630,7 +659,7 @@ internal static class PeopleDefValidator
: surname.Declension;
if (!NameGrammar.IsKnownSurname(model))
{
throw new ContentLoadException($"NameSetDef '{names.DefName}' surname '{surname.Male}' has unknown declension '{model}'.");
throw new ContentLoadException($"CountryDef '{countryDefName}' surname '{surname.Male}' has unknown declension '{model}'.");
}
}
}
+22 -2
View File
@@ -359,7 +359,10 @@ public sealed class SurnameEntry
public CaseTable? FemaleCases { get; init; }
}
public sealed class NameSetDef : Def
/// <summary>
/// Nested name grammar of a <see cref="CountryDef"/>. Not a selectable catalog kind of its own.
/// </summary>
public sealed class NameSetDef
{
public string PatronymicRule { get; init; } = NameGrammar.SlavicPatronymic;
@@ -368,7 +371,7 @@ public sealed class NameSetDef : Def
public string DefaultSurnameDeclension { get; init; } = NameGrammar.Ov;
/// <summary>
/// Languages this set can speak natively. Slavic names cover Russian, Belarusian and
/// Languages this country can speak natively. Slavic names cover Russian, Belarusian and
/// Ukrainian; the school picks one at create. Singular <see cref="NativeLanguage"/> is still
/// accepted in JSONC for a one-language pack.
/// </summary>
@@ -401,3 +404,20 @@ public sealed class NameSetDef : Def
public IReadOnlyList<SurnameEntry> Surnames { get; init; } = [];
}
/// <summary>
/// What the player picks at create: nested names plus climate-preset ids. Weather numbers live
/// on <see cref="ClimatePresetDef"/> and stay unused until phase 32.
/// </summary>
public sealed class CountryDef : Def
{
public IReadOnlyList<string> ClimatePresets { get; init; } = [];
public NameSetDef Names { get; init; } = new();
}
/// <summary>
/// Outdoor climate a country may roll. Monthly temperatures land in phase 32; the id is enough
/// to persist which preset a school was born with.
/// </summary>
public sealed class ClimatePresetDef : Def;