WIP phase 39: school owners, my/others menu, guest restrictions.

This commit is contained in:
Leonid Pershin
2026-08-20 07:58:12 +03:00
parent 40929aa3ce
commit 26be7c87f8
27 changed files with 919 additions and 81 deletions
+73 -13
View File
@@ -84,6 +84,38 @@ internal sealed class GameLoopService(
return null;
}
public SchoolState? FindSchool(int schoolId)
{
foreach (var worker in Volatile.Read(ref _publishedWorkers))
{
if (worker.Id == schoolId)
{
return worker.Snapshot;
}
}
if (_incompatible.TryGetValue(schoolId, out var broken))
{
return broken;
}
return null;
}
public int CountOwnedBy(string normalizedUserName)
{
var count = 0;
foreach (var school in SchoolsState.Schools)
{
if (SchoolOwnership.IsOwner(school, normalizedUserName))
{
count++;
}
}
return count;
}
public Task ReloadFromDiskAsync(CancellationToken cancellationToken)
{
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -94,8 +126,9 @@ internal sealed class GameLoopService(
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation(
"School supervisor starting at {TickRate} Hz, up to {MaxSchools} schools, saves in {Directory}.",
"School supervisor starting at {TickRate} Hz, up to {MaxSchoolsTotal} schools ({MaxSchools} per player), saves in {Directory}.",
_options.TickRate,
_options.MaxSchoolsTotal,
_options.MaxSchools,
store.DirectoryPath);
@@ -166,15 +199,15 @@ internal sealed class GameLoopService(
break;
case GameCommand.SetRunning setRunning:
RouteOpenSchool(setRunning.PlayerId, new WorkerCommand.SetRunning(setRunning.Running));
HandleClockCommand(setRunning.PlayerId, setRunning.NormalizedUserName, new WorkerCommand.SetRunning(setRunning.Running));
break;
case GameCommand.SetSpeed setSpeed:
RouteOpenSchool(setSpeed.PlayerId, new WorkerCommand.SetSpeed(setSpeed.SpeedIndex));
HandleClockCommand(setSpeed.PlayerId, setSpeed.NormalizedUserName, new WorkerCommand.SetSpeed(setSpeed.SpeedIndex));
break;
case GameCommand.SkipEmpty skipEmpty:
RouteOpenSchool(skipEmpty.PlayerId, new WorkerCommand.SkipEmpty());
HandleClockCommand(skipEmpty.PlayerId, skipEmpty.NormalizedUserName, new WorkerCommand.SkipEmpty());
break;
case GameCommand.ReloadSaves reload:
@@ -326,7 +359,13 @@ internal sealed class GameLoopService(
{
try
{
if (_workers.Count >= _options.MaxSchools)
if (_workers.Count >= _options.MaxSchoolsTotal)
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.ServerFull));
return;
}
if (CountOwnedBy(command.Owner) >= _options.MaxSchools)
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.LimitReached));
return;
@@ -414,7 +453,7 @@ internal sealed class GameLoopService(
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, countryId, climatePresetId, nativeLanguage, seed);
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, countryId, climatePresetId, nativeLanguage, seed, owner: command.Owner);
Track(worker);
worker.Start();
@@ -558,6 +597,23 @@ internal sealed class GameLoopService(
Route(schoolId, command);
}
private void HandleClockCommand(uint playerId, string normalizedUserName, WorkerCommand command)
{
var client = clients.Find(playerId);
if (client?.OpenSchoolId is not { } schoolId)
{
return;
}
if (!_workers.TryGetValue(schoolId, out var worker)
|| !SchoolOwnership.CanManage(worker.Snapshot, normalizedUserName))
{
return;
}
Route(schoolId, command);
}
private async Task StartWorkersFromDiskAsync()
{
var saves = store.LoadAll();
@@ -582,19 +638,19 @@ internal sealed class GameLoopService(
}
}
if (startable.Count > _options.MaxSchools)
if (startable.Count > _options.MaxSchoolsTotal)
{
logger.LogWarning(
"Found {Count} runnable school saves but the limit is {Max}; starting the first {Max}.",
startable.Count,
_options.MaxSchools,
_options.MaxSchools);
foreach (var extra in startable.Skip(_options.MaxSchools))
_options.MaxSchoolsTotal,
_options.MaxSchoolsTotal);
foreach (var extra in startable.Skip(_options.MaxSchoolsTotal))
{
RememberIncompatible(FromSave(extra));
}
startable = [.. startable.Take(_options.MaxSchools)];
startable = [.. startable.Take(_options.MaxSchoolsTotal)];
}
foreach (var save in startable)
@@ -613,7 +669,8 @@ internal sealed class GameLoopService(
save.NativeLanguage,
createSeed: null,
save.Presence,
save.DressRules);
save.DressRules,
save.Owner);
worker.Start();
try
@@ -669,7 +726,8 @@ internal sealed class GameLoopService(
string? nativeLanguage,
int? createSeed = null,
IReadOnlyList<PresenceSnapshot>? presence = null,
SchoolDressRules? dressRules = null) =>
SchoolDressRules? dressRules = null,
string? owner = null) =>
new(
id,
name,
@@ -685,6 +743,7 @@ internal sealed class GameLoopService(
createSeed,
presence,
dressRules,
owner,
_options,
clients,
metrics,
@@ -760,6 +819,7 @@ internal sealed class GameLoopService(
(byte)Math.Clamp(save.SpeedIndex, 0, 255),
save.ModIds ?? [],
SeedOf(save.Id),
Owner: string.IsNullOrWhiteSpace(save.Owner) ? null : save.Owner,
Incompatible: true);
private int SeedOf(int schoolId)