Add HSchool.Content project for JSONC definitions, catalog, and map validation. Update solution structure to include new content and tests projects. Enhance school management to support mod packs and map instances, ensuring proper loading and validation. Revise documentation to reflect these changes and update tests for new functionality.
ci / server (push) Failing after 3m36s
ci / client (push) Successful in 13s

This commit is contained in:
Leonid Pershin
2026-08-18 14:35:09 +03:00
parent 37c39a3beb
commit 1bc75244e8
55 changed files with 2289 additions and 59 deletions
+51 -11
View File
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Threading.Channels;
using HSchool.Content;
using HSchool.Protocol;
using HSchool.Server.Net;
using HSchool.Simulation;
@@ -8,8 +9,8 @@ namespace HSchool.Server.Game;
/// <summary>
/// Dedicated thread for one school: fixed-step clock, that school's Arch world, that school's
/// save file. Awaits are resolved with <c>GetResult</c> so <see cref="School.Tick"/> stays on
/// this thread instead of hopping back onto the pool.
/// frozen catalog, that school's save file. Awaits are resolved with <c>GetResult</c> so
/// <see cref="School.Tick"/> stays on this thread instead of hopping back onto the pool.
/// </summary>
internal sealed class SchoolWorker
{
@@ -19,12 +20,15 @@ internal sealed class SchoolWorker
private readonly ClientRegistry _clients;
private readonly GameMetrics _metrics;
private readonly SchoolStore _store;
private readonly ModContent _mods;
private readonly ILogger _logger;
private readonly Channel<WorkerCommand> _mailbox = Channel.CreateUnbounded<WorkerCommand>(
new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
private readonly TaskCompletionSource _started = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly CancellationTokenSource _stopping = new();
private readonly bool _isNew;
private readonly IReadOnlyList<string>? _modIds;
private readonly MapLayout? _savedMap;
private readonly int _id;
private readonly string _name;
@@ -44,10 +48,13 @@ internal sealed class SchoolWorker
bool running,
int speedIndex,
bool isNew,
IReadOnlyList<string>? modIds,
MapLayout? savedMap,
SimulationOptions options,
ClientRegistry clients,
GameMetrics metrics,
SchoolStore store,
ModContent mods,
ILogger logger)
{
_id = id;
@@ -56,10 +63,13 @@ internal sealed class SchoolWorker
_running = running;
_speedIndex = speedIndex;
_isNew = isNew;
_modIds = modIds;
_savedMap = savedMap;
_options = options;
_clients = clients;
_metrics = metrics;
_store = store;
_mods = mods;
_logger = logger;
_snapshot = new SchoolState(id, name, time, running, (byte)speedIndex);
}
@@ -113,6 +123,11 @@ internal sealed class SchoolWorker
{
RunLoop(_stopping.Token);
}
catch (SchoolContentUnavailableException ex)
{
_logger.LogWarning(ex, "School {SchoolId} was not started; the save file is unchanged.", _id);
_started.TrySetException(ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "School {SchoolId} worker died.", _id);
@@ -122,9 +137,30 @@ internal sealed class SchoolWorker
private void RunLoop(CancellationToken cancellationToken)
{
var packIds = _mods.NormalizePackIds(_modIds);
foreach (var packId in packIds)
{
if (!_mods.PackExists(packId))
{
throw new SchoolContentUnavailableException(
$"School {_id} needs mod '{packId}', but that folder is missing.");
}
}
var catalog = _mods.LoadCatalog(packIds, _logger);
var map = _mods.LoadMap(packIds, _savedMap);
try
{
MapValidator.Validate(map, catalog);
}
catch (MapValidationException ex)
{
throw new SchoolContentUnavailableException(ex.Message, ex);
}
var school = _isNew
? School.Create(_id, _name, _time)
: School.Load(_id, _name, _time, _running, _speedIndex);
? School.Create(_id, _name, _time, catalog, map)
: School.Load(_id, _name, _time, _running, _speedIndex, catalog, map);
_school = school;
PublishSnapshot();
@@ -298,13 +334,17 @@ internal sealed class SchoolWorker
return;
}
_store.Save(new SchoolSave(
SchoolStore.CurrentFormat,
school.Id,
school.Name,
school.Clock.Time,
school.Clock.IsRunning,
school.Clock.SpeedIndex));
_store.Save(new SchoolSave
{
Format = SchoolStore.CurrentFormat,
Id = school.Id,
Name = school.Name,
GameTime = school.Clock.Time,
Running = school.Clock.IsRunning,
SpeedIndex = school.Clock.SpeedIndex,
ModIds = school.Catalog?.PackIds,
Map = school.Map,
});
}
private void BroadcastClock()