Enhance school creation and staffing management with native language support
ci / server (push) Failing after 3m44s
ci / client (push) Successful in 14s

- Updated protocol documentation to include `nativeLanguages` in `nameSets` and added `nativeLanguage` to school creation options.
- Enhanced the UI for school creation to allow selection of native languages, improving user experience.
- Revised API interfaces to accommodate new native language features, ensuring proper data handling.
- Improved localization strings to support new native language functionalities in both English and Russian.
- Updated tests to validate the new native language features and ensure robust functionality in staffing scenarios.
This commit is contained in:
Leonid Pershin
2026-08-19 22:22:31 +03:00
parent 21d79cb49b
commit 5a00ad7746
48 changed files with 2064 additions and 312 deletions
+1
View File
@@ -17,6 +17,7 @@ internal abstract record GameCommand
IReadOnlyList<string>? ExtraModIds,
MapLayout? Map,
string? NameSetId,
string? NativeLanguage,
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
internal sealed record DeleteSchool(int SchoolId, TaskCompletionSource<bool> Result) : GameCommand;
+17 -1
View File
@@ -328,10 +328,23 @@ internal sealed class GameLoopService(
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))
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNativeLanguage));
return;
}
var id = _nextId++;
store.WriteNextId(_nextId);
var nativeLanguage = NativeLanguages.Pick(names, id, command.NativeLanguage, rollIfOmitted: true);
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId);
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId, nativeLanguage);
Track(worker);
worker.Start();
@@ -499,6 +512,7 @@ internal sealed class GameLoopService(
save.ModIds,
save.Map,
save.NameSetId,
save.NativeLanguage,
save.Presence);
worker.Start();
@@ -547,6 +561,7 @@ internal sealed class GameLoopService(
IReadOnlyList<string>? modIds,
MapLayout? map,
string? nameSetId,
string? nativeLanguage,
IReadOnlyList<PresenceSnapshot>? presence = null) =>
new(
id,
@@ -558,6 +573,7 @@ internal sealed class GameLoopService(
modIds,
map,
nameSetId,
nativeLanguage,
presence,
_options,
clients,
+3
View File
@@ -28,6 +28,8 @@ internal sealed class SchoolSave
public string? NameSetId { get; init; }
public string? NativeLanguage { get; init; }
public IReadOnlyList<PresenceSnapshot>? Presence { get; init; }
}
@@ -162,6 +164,7 @@ internal sealed class SchoolStore
ModIds = save.ModIds,
Map = save.Map,
NameSetId = save.NameSetId,
NativeLanguage = save.NativeLanguage,
Presence = save.Presence,
});
}
+32 -6
View File
@@ -33,6 +33,7 @@ internal sealed class SchoolWorker
private readonly IReadOnlyList<string>? _modIds;
private readonly MapLayout? _savedMap;
private readonly string? _nameSetId;
private string? _nativeLanguage;
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
private readonly Action<int> _onFailed;
@@ -65,6 +66,7 @@ internal sealed class SchoolWorker
IReadOnlyList<string>? modIds,
MapLayout? savedMap,
string? nameSetId,
string? nativeLanguage,
IReadOnlyList<PresenceSnapshot>? savedPresence,
SimulationOptions options,
ClientRegistry clients,
@@ -83,6 +85,7 @@ internal sealed class SchoolWorker
_modIds = modIds;
_savedMap = savedMap;
_nameSetId = nameSetId;
_nativeLanguage = nativeLanguage;
_savedPresence = savedPresence;
_options = options;
_clients = clients;
@@ -849,12 +852,15 @@ internal sealed class SchoolWorker
ApplicantPool applicants;
int seed;
var generated = false;
string? native;
if (_isNew)
{
seed = school.Id;
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time);
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time);
native = ResolveNative(catalog, nameSetId, 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);
generated = true;
}
else
@@ -863,13 +869,17 @@ internal sealed class SchoolWorker
if (loaded is null)
{
seed = school.Id;
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time);
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time);
native = ResolveNative(catalog, nameSetId, 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);
generated = true;
}
else
{
seed = loaded.Seed;
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: false);
_nativeLanguage = native;
roster = loaded.ToRoster();
if (loaded.Applicants is { Applicants.Count: > 0 })
{
@@ -877,7 +887,7 @@ internal sealed class SchoolWorker
}
else
{
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time);
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
generated = true;
}
}
@@ -889,7 +899,7 @@ internal sealed class SchoolWorker
$"School {_id} roster does not match its map; the people file was left untouched.");
}
school.InstallPeople(roster, seed, nameSetId, applicants);
school.InstallPeople(roster, seed, nameSetId, applicants, _nativeLanguage);
InstallTimetable(school);
school.ConfigurePresence(_options.SchoolWeekDays, _options.MaxDecisionsPerTick);
school.RestorePresence(_savedPresence);
@@ -916,6 +926,21 @@ internal sealed class SchoolWorker
return available.Contains(requested, StringComparer.Ordinal) ? requested : null;
}
private static string? ResolveNative(
DefCatalog catalog,
string nameSetId,
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));
}
private void Persist()
{
var school = _school;
@@ -938,6 +963,7 @@ internal sealed class SchoolWorker
ModIds = school.Catalog?.PackIds,
Map = school.Map,
NameSetId = _nameSetId,
NativeLanguage = _nativeLanguage,
Presence = school.CapturePresence(),
});
}