Replace selectable name sets with a country that owns names and climate presets.
Create picks a country; climate is rolled from the school seed and stored. Old Slavic saves lift as Russia. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,7 +16,7 @@ internal abstract record GameCommand
|
||||
DateTime StartDate,
|
||||
IReadOnlyList<string>? ExtraModIds,
|
||||
MapLayout? Map,
|
||||
string? NameSetId,
|
||||
string? CountryId,
|
||||
string? NativeLanguage,
|
||||
int? Seed,
|
||||
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
|
||||
|
||||
@@ -357,20 +357,14 @@ internal sealed class GameLoopService(
|
||||
return;
|
||||
}
|
||||
|
||||
var nameSetId = ResolveNameSetId(catalog, command.NameSetId);
|
||||
if (nameSetId is null)
|
||||
var countryId = ResolveCountryId(catalog, command.CountryId);
|
||||
if (countryId is null || !catalog.Countries.TryGetValue(countryId, out var country) || country.Abstract)
|
||||
{
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNameSet));
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownCountry));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
{
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNameSet));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(command.NativeLanguage) && !NativeLanguages.Allows(names, command.NativeLanguage))
|
||||
if (!string.IsNullOrWhiteSpace(command.NativeLanguage) && !NativeLanguages.Allows(country.Names, command.NativeLanguage))
|
||||
{
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNativeLanguage));
|
||||
return;
|
||||
@@ -379,9 +373,10 @@ internal sealed class GameLoopService(
|
||||
var id = _nextId++;
|
||||
store.WriteNextId(_nextId);
|
||||
var seed = command.Seed ?? Random.Shared.Next();
|
||||
var nativeLanguage = NativeLanguages.Pick(names, seed, command.NativeLanguage, rollIfOmitted: true);
|
||||
var nativeLanguage = NativeLanguages.Pick(country.Names, seed, command.NativeLanguage, rollIfOmitted: true);
|
||||
var climatePresetId = CountryClimate.Pick(country, seed, rollIfOmitted: true);
|
||||
|
||||
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId, nativeLanguage, seed);
|
||||
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, countryId, climatePresetId, nativeLanguage, seed);
|
||||
Track(worker);
|
||||
worker.Start();
|
||||
|
||||
@@ -548,7 +543,8 @@ internal sealed class GameLoopService(
|
||||
isNew: false,
|
||||
save.ModIds,
|
||||
save.Map,
|
||||
save.NameSetId,
|
||||
save.CountryId,
|
||||
save.ClimatePresetId,
|
||||
save.NativeLanguage,
|
||||
createSeed: null,
|
||||
save.Presence);
|
||||
@@ -598,7 +594,8 @@ internal sealed class GameLoopService(
|
||||
bool isNew,
|
||||
IReadOnlyList<string>? modIds,
|
||||
MapLayout? map,
|
||||
string? nameSetId,
|
||||
string? countryId,
|
||||
string? climatePresetId,
|
||||
string? nativeLanguage,
|
||||
int? createSeed = null,
|
||||
IReadOnlyList<PresenceSnapshot>? presence = null) =>
|
||||
@@ -611,7 +608,8 @@ internal sealed class GameLoopService(
|
||||
isNew,
|
||||
modIds,
|
||||
map,
|
||||
nameSetId,
|
||||
countryId,
|
||||
climatePresetId,
|
||||
nativeLanguage,
|
||||
createSeed,
|
||||
presence,
|
||||
@@ -624,12 +622,12 @@ internal sealed class GameLoopService(
|
||||
loggerFactory.CreateLogger($"HSchool.Server.Game.SchoolWorker.{id}"));
|
||||
|
||||
/// <summary>
|
||||
/// Empty request uses the first placeable set (core's Slavic). A named id must exist in the
|
||||
/// catalog already loaded for this pack list — unknown extras were rejected above.
|
||||
/// Empty request uses the first placeable country (core's Russia). A named id must exist in
|
||||
/// the catalog already loaded for this pack list — unknown extras were rejected above.
|
||||
/// </summary>
|
||||
internal static string? ResolveNameSetId(DefCatalog catalog, string? requested)
|
||||
internal static string? ResolveCountryId(DefCatalog catalog, string? requested)
|
||||
{
|
||||
var available = catalog.NameSets.Values
|
||||
var available = catalog.Countries.Values
|
||||
.Where(def => !def.Abstract)
|
||||
.Select(def => def.DefName)
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
|
||||
@@ -26,6 +26,11 @@ internal sealed class SchoolSave
|
||||
|
||||
public MapLayout? Map { get; init; }
|
||||
|
||||
public string? CountryId { get; init; }
|
||||
|
||||
public string? ClimatePresetId { get; init; }
|
||||
|
||||
/// <summary>Legacy field. Format 2 and older; mapped to <see cref="CountryId"/> on load.</summary>
|
||||
public string? NameSetId { get; init; }
|
||||
|
||||
public string? NativeLanguage { get; init; }
|
||||
@@ -42,7 +47,7 @@ internal sealed record SchoolSaveIndex(int NextId);
|
||||
/// </summary>
|
||||
internal sealed class SchoolStore
|
||||
{
|
||||
public const int CurrentFormat = 2;
|
||||
public const int CurrentFormat = 3;
|
||||
|
||||
private const string IndexFileName = "index.json";
|
||||
|
||||
@@ -178,6 +183,8 @@ internal sealed class SchoolStore
|
||||
SpeedIndex = save.SpeedIndex,
|
||||
ModIds = save.ModIds,
|
||||
Map = save.Map,
|
||||
CountryId = save.CountryId,
|
||||
ClimatePresetId = save.ClimatePresetId,
|
||||
NameSetId = save.NameSetId,
|
||||
NativeLanguage = save.NativeLanguage,
|
||||
Presence = save.Presence,
|
||||
@@ -194,11 +201,40 @@ internal sealed class SchoolStore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Named upgrade seam for saves older than <see cref="CurrentFormat"/>. Empty on purpose:
|
||||
/// missing fields already default and extra fields are ignored. Put a migration here when a
|
||||
/// format bump actually needs one.
|
||||
/// Format 2 stored a name set. Slavic becomes Russia; any other id is tried as a country
|
||||
/// (the example pack kept its defName). Climate is filled on the worker from the country's
|
||||
/// first preset so the file is not rewritten until the school saves itself.
|
||||
/// </summary>
|
||||
internal static SchoolSave UpgradeOlderSave(SchoolSave save) => save;
|
||||
internal static SchoolSave UpgradeOlderSave(SchoolSave save)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(save.CountryId))
|
||||
{
|
||||
return save;
|
||||
}
|
||||
|
||||
var countryId = save.NameSetId switch
|
||||
{
|
||||
"Slavic" or null or "" => "Russia",
|
||||
_ => save.NameSetId,
|
||||
};
|
||||
|
||||
return new SchoolSave
|
||||
{
|
||||
Format = save.Format,
|
||||
Id = save.Id,
|
||||
Name = save.Name,
|
||||
GameTime = save.GameTime,
|
||||
Running = save.Running,
|
||||
SpeedIndex = save.SpeedIndex,
|
||||
ModIds = save.ModIds,
|
||||
Map = save.Map,
|
||||
CountryId = countryId,
|
||||
ClimatePresetId = save.ClimatePresetId,
|
||||
NameSetId = save.NameSetId,
|
||||
NativeLanguage = save.NativeLanguage,
|
||||
Presence = save.Presence,
|
||||
};
|
||||
}
|
||||
|
||||
public void Save(SchoolSave save)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,8 @@ internal sealed class SchoolWorker
|
||||
private readonly bool _isNew;
|
||||
private readonly IReadOnlyList<string>? _modIds;
|
||||
private readonly MapLayout? _savedMap;
|
||||
private readonly string? _nameSetId;
|
||||
private readonly string? _countryId;
|
||||
private string? _climatePresetId;
|
||||
private string? _nativeLanguage;
|
||||
private readonly int? _createSeed;
|
||||
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
|
||||
@@ -66,7 +67,8 @@ internal sealed class SchoolWorker
|
||||
bool isNew,
|
||||
IReadOnlyList<string>? modIds,
|
||||
MapLayout? savedMap,
|
||||
string? nameSetId,
|
||||
string? countryId,
|
||||
string? climatePresetId,
|
||||
string? nativeLanguage,
|
||||
int? createSeed,
|
||||
IReadOnlyList<PresenceSnapshot>? savedPresence,
|
||||
@@ -86,7 +88,8 @@ internal sealed class SchoolWorker
|
||||
_isNew = isNew;
|
||||
_modIds = modIds;
|
||||
_savedMap = savedMap;
|
||||
_nameSetId = nameSetId;
|
||||
_countryId = countryId;
|
||||
_climatePresetId = climatePresetId;
|
||||
_nativeLanguage = nativeLanguage;
|
||||
_createSeed = createSeed;
|
||||
_savedPresence = savedPresence;
|
||||
@@ -857,12 +860,19 @@ internal sealed class SchoolWorker
|
||||
|
||||
private bool InstallPeople(School school, DefCatalog catalog, MapLayout map)
|
||||
{
|
||||
var nameSetId = ResolveNameSetId(catalog, _nameSetId);
|
||||
if (nameSetId is null)
|
||||
var countryId = ResolveCountryId(catalog, _countryId);
|
||||
if (countryId is null)
|
||||
{
|
||||
throw new SchoolContentUnavailableException($"School {_id} has no name set in its catalog.");
|
||||
throw new SchoolContentUnavailableException($"School {_id} has no country in its catalog.");
|
||||
}
|
||||
|
||||
if (!catalog.Countries.TryGetValue(countryId, out var country) || country.Abstract)
|
||||
{
|
||||
throw new SchoolContentUnavailableException($"School {_id} has no country in its catalog.");
|
||||
}
|
||||
|
||||
_climatePresetId = ResolveClimatePreset(country, _climatePresetId);
|
||||
|
||||
var demand = SchoolDemand.From(catalog, map);
|
||||
Roster roster;
|
||||
ApplicantPool applicants;
|
||||
@@ -878,10 +888,10 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
|
||||
seed = createSeed;
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
||||
native = ResolveNative(country, seed, _nativeLanguage, generating: true);
|
||||
_nativeLanguage = native;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, countryId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
else
|
||||
@@ -890,16 +900,16 @@ internal sealed class SchoolWorker
|
||||
if (loaded is null)
|
||||
{
|
||||
seed = school.Id;
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
||||
native = ResolveNative(country, seed, _nativeLanguage, generating: true);
|
||||
_nativeLanguage = native;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, countryId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
seed = loaded.Seed;
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: false);
|
||||
native = ResolveNative(country, seed, _nativeLanguage, generating: false);
|
||||
_nativeLanguage = native;
|
||||
roster = loaded.ToRoster();
|
||||
if (loaded.Applicants is { Applicants.Count: > 0 })
|
||||
@@ -908,7 +918,7 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
else
|
||||
{
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, countryId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
}
|
||||
@@ -920,47 +930,41 @@ internal sealed class SchoolWorker
|
||||
$"School {_id} roster does not match its map; the people file was left untouched.");
|
||||
}
|
||||
|
||||
school.InstallPeople(roster, seed, nameSetId, applicants, _nativeLanguage);
|
||||
school.InstallPeople(roster, seed, countryId, applicants, _nativeLanguage, _climatePresetId);
|
||||
InstallTimetable(school);
|
||||
school.ConfigurePresence(_options.SchoolWeekDays, _options.MaxDecisionsPerTick);
|
||||
school.RestorePresence(_savedPresence);
|
||||
return generated;
|
||||
}
|
||||
|
||||
private static string? ResolveNameSetId(DefCatalog catalog, string? requested)
|
||||
private static string? ResolveCountryId(DefCatalog catalog, string? requested)
|
||||
{
|
||||
var available = catalog.NameSets.Values
|
||||
.Where(def => !def.Abstract)
|
||||
.Select(def => def.DefName)
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (available.Length == 0)
|
||||
if (string.IsNullOrWhiteSpace(requested))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(requested))
|
||||
return catalog.Countries.TryGetValue(requested, out var country) && !country.Abstract
|
||||
? requested
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string? ResolveClimatePreset(CountryDef country, string? requested)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(requested) && country.ClimatePresets.Contains(requested, StringComparer.Ordinal))
|
||||
{
|
||||
return available[0];
|
||||
return requested;
|
||||
}
|
||||
|
||||
return available.Contains(requested, StringComparer.Ordinal) ? requested : null;
|
||||
return CountryClimate.Pick(country, schoolSeed: 0, rollIfOmitted: false);
|
||||
}
|
||||
|
||||
private static string? ResolveNative(
|
||||
DefCatalog catalog,
|
||||
string nameSetId,
|
||||
CountryDef country,
|
||||
int schoolSeed,
|
||||
string? requested,
|
||||
bool generating)
|
||||
{
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return NativeLanguages.Pick(names, schoolSeed, requested, rollIfOmitted: generating && string.IsNullOrWhiteSpace(requested));
|
||||
}
|
||||
bool generating) =>
|
||||
NativeLanguages.Pick(country.Names, schoolSeed, requested, rollIfOmitted: generating && string.IsNullOrWhiteSpace(requested));
|
||||
|
||||
private void Persist()
|
||||
{
|
||||
@@ -983,7 +987,8 @@ internal sealed class SchoolWorker
|
||||
SpeedIndex = school.Clock.SpeedIndex,
|
||||
ModIds = school.Catalog?.PackIds,
|
||||
Map = school.Map,
|
||||
NameSetId = _nameSetId,
|
||||
CountryId = school.CountryId,
|
||||
ClimatePresetId = school.ClimatePresetId,
|
||||
NativeLanguage = _nativeLanguage,
|
||||
Presence = school.CapturePresence(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user