Give each school its own people seed so composition no longer follows create order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 00:28:13 +03:00
co-authored by Cursor
parent 0c3eb219c8
commit 48f0b15221
17 changed files with 233 additions and 35 deletions
+5 -3
View File
@@ -55,6 +55,7 @@ internal static class SchoolEndpoints
request.Map,
request.NameSetId,
request.NativeLanguage,
request.Seed,
NewCompletion<SchoolCreationOutcome>());
commands.Enqueue(command);
@@ -490,12 +491,13 @@ internal sealed record CreateSchoolRequest(
IReadOnlyList<string>? ModIds,
MapLayout? Map,
string? NameSetId,
string? NativeLanguage);
string? NativeLanguage,
int? Seed);
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex)
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex, int Seed)
{
public static SchoolResponse From(SchoolState school) =>
new(school.Id, school.Name, school.GameTime, school.Running, school.SpeedIndex);
new(school.Id, school.Name, school.GameTime, school.Running, school.SpeedIndex, school.Seed);
}
/// <summary>Everything the main menu needs in one request.</summary>
+1
View File
@@ -18,6 +18,7 @@ internal abstract record GameCommand
MapLayout? Map,
string? NameSetId,
string? NativeLanguage,
int? Seed,
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
internal sealed record DeleteSchool(int SchoolId, TaskCompletionSource<bool> Result) : GameCommand;
+7 -3
View File
@@ -342,9 +342,10 @@ internal sealed class GameLoopService(
var id = _nextId++;
store.WriteNextId(_nextId);
var nativeLanguage = NativeLanguages.Pick(names, id, command.NativeLanguage, rollIfOmitted: true);
var seed = command.Seed ?? Random.Shared.Next();
var nativeLanguage = NativeLanguages.Pick(names, seed, command.NativeLanguage, rollIfOmitted: true);
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId, nativeLanguage);
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId, nativeLanguage, seed);
Track(worker);
worker.Start();
@@ -366,7 +367,7 @@ internal sealed class GameLoopService(
throw;
}
logger.LogInformation("School {SchoolId} \"{Name}\" created.", id, normalized);
logger.LogInformation("School {SchoolId} \"{Name}\" created with seed {Seed}.", id, normalized, seed);
command.Result.TrySetResult(new SchoolCreationOutcome(worker.Snapshot, SchoolCreationError.None));
}
catch (Exception ex)
@@ -513,6 +514,7 @@ internal sealed class GameLoopService(
save.Map,
save.NameSetId,
save.NativeLanguage,
createSeed: null,
save.Presence);
worker.Start();
@@ -562,6 +564,7 @@ internal sealed class GameLoopService(
MapLayout? map,
string? nameSetId,
string? nativeLanguage,
int? createSeed = null,
IReadOnlyList<PresenceSnapshot>? presence = null) =>
new(
id,
@@ -574,6 +577,7 @@ internal sealed class GameLoopService(
map,
nameSetId,
nativeLanguage,
createSeed,
presence,
_options,
clients,
+1 -1
View File
@@ -4,7 +4,7 @@ namespace HSchool.Server.Game;
/// Immutable copy of a school, safe to hand to request threads. The live <c>School</c> object
/// never leaves its worker thread.
/// </summary>
internal sealed record SchoolState(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex);
internal sealed record SchoolState(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex, int Seed);
/// <summary>Everything the main menu needs in one read.</summary>
internal sealed record SchoolsState(int MaxSchools, IReadOnlyList<SchoolState> Schools);
+12 -3
View File
@@ -34,6 +34,7 @@ internal sealed class SchoolWorker
private readonly MapLayout? _savedMap;
private readonly string? _nameSetId;
private string? _nativeLanguage;
private readonly int? _createSeed;
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
private readonly Action<int> _onFailed;
@@ -67,6 +68,7 @@ internal sealed class SchoolWorker
MapLayout? savedMap,
string? nameSetId,
string? nativeLanguage,
int? createSeed,
IReadOnlyList<PresenceSnapshot>? savedPresence,
SimulationOptions options,
ClientRegistry clients,
@@ -86,6 +88,7 @@ internal sealed class SchoolWorker
_savedMap = savedMap;
_nameSetId = nameSetId;
_nativeLanguage = nativeLanguage;
_createSeed = createSeed;
_savedPresence = savedPresence;
_options = options;
_clients = clients;
@@ -94,7 +97,7 @@ internal sealed class SchoolWorker
_mods = mods;
_onFailed = onFailed;
_logger = logger;
_snapshot = new SchoolState(id, name, time, running, (byte)speedIndex);
_snapshot = new SchoolState(id, name, time, running, (byte)speedIndex, createSeed ?? 0);
}
public int Id => _id;
@@ -577,7 +580,8 @@ internal sealed class SchoolWorker
school.Name,
school.Clock.Time,
school.Clock.IsRunning,
(byte)school.Clock.SpeedIndex));
(byte)school.Clock.SpeedIndex,
school.PeopleSeed));
Volatile.Write(ref _rosterSnapshot, school.Roster);
Volatile.Write(ref _applicantSnapshot, school.Applicants);
Volatile.Write(ref _timetableSnapshot, school.Timetable);
@@ -856,7 +860,12 @@ internal sealed class SchoolWorker
if (_isNew)
{
seed = school.Id;
if (_createSeed is not int createSeed)
{
throw new InvalidOperationException($"School {_id} was created without a people seed.");
}
seed = createSeed;
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
_nativeLanguage = native;
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);