Update wire protocol to version 6 and enhance timetable functionality
- Bumped the wire protocol version to 6, reflecting changes in the communication structure.
- Expanded the timetable API with new endpoints for fetching and managing lesson schedules, including `GET /api/schools/{id}/timetable` and `POST /api/schools/{id}/timetable/pin`.
- Updated the protocol documentation to include detailed descriptions of the new timetable features and message structures.
- Enhanced the client-side implementation to support the new timetable functionalities, including lesson pinning and unpinning.
- Revised server-side logic to handle timetable operations and ensure proper integration with existing school management features.
- Added tests to validate the new timetable functionalities and ensure robustness in handling lesson data.
This commit is contained in:
@@ -69,4 +69,21 @@ internal abstract record GameCommand
|
||||
string PersonId,
|
||||
string Subject,
|
||||
TaskCompletionSource<StaffingOutcome> Result) : GameCommand;
|
||||
|
||||
internal sealed record PinLesson(
|
||||
int SchoolId,
|
||||
string ClassId,
|
||||
string Subject,
|
||||
string RoomId,
|
||||
int Day,
|
||||
int Period,
|
||||
TaskCompletionSource<TimetableOutcome> Result) : GameCommand;
|
||||
|
||||
internal sealed record UnpinLesson(
|
||||
int SchoolId,
|
||||
string ClassId,
|
||||
string Subject,
|
||||
int Day,
|
||||
int Period,
|
||||
TaskCompletionSource<TimetableOutcome> Result) : GameCommand;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Protocol;
|
||||
using HSchool.Schedule;
|
||||
using HSchool.Server.Api;
|
||||
using HSchool.Server.Net;
|
||||
using HSchool.Simulation;
|
||||
@@ -64,7 +65,12 @@ internal sealed class GameLoopService(
|
||||
{
|
||||
if (worker.Id == schoolId)
|
||||
{
|
||||
return new PublishedSchoolPeople(worker.Snapshot, worker.RosterSnapshot, worker.ApplicantSnapshot, worker.CatalogSnapshot);
|
||||
return new PublishedSchoolPeople(
|
||||
worker.Snapshot,
|
||||
worker.RosterSnapshot,
|
||||
worker.ApplicantSnapshot,
|
||||
worker.CatalogSnapshot,
|
||||
worker.TimetableSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +198,20 @@ internal sealed class GameLoopService(
|
||||
new WorkerCommand.UnassignSubject(unassign.PersonId, unassign.Subject, unassign.Result),
|
||||
unassign.Result);
|
||||
break;
|
||||
|
||||
case GameCommand.PinLesson pin:
|
||||
HandleTimetable(
|
||||
pin.SchoolId,
|
||||
new WorkerCommand.PinLesson(pin.ClassId, pin.Subject, pin.RoomId, pin.Day, pin.Period, pin.Result),
|
||||
pin.Result);
|
||||
break;
|
||||
|
||||
case GameCommand.UnpinLesson unpin:
|
||||
HandleTimetable(
|
||||
unpin.SchoolId,
|
||||
new WorkerCommand.UnpinLesson(unpin.ClassId, unpin.Subject, unpin.Day, unpin.Period, unpin.Result),
|
||||
unpin.Result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +232,14 @@ internal sealed class GameLoopService(
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTimetable(int schoolId, WorkerCommand command, TaskCompletionSource<TimetableOutcome> result)
|
||||
{
|
||||
if (!_workers.TryGetValue(schoolId, out var worker) || !worker.Post(command))
|
||||
{
|
||||
result.TrySetResult(TimetableOutcome.Fail(TimetableError.UnknownSchool));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A school's thread died. Drop it from the table so the menu stops drawing a card whose clock
|
||||
/// never moves again, and tell anybody watching it to go back to the menu. The save file stays
|
||||
@@ -610,4 +638,9 @@ internal sealed class GameLoopService(
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PublishedSchoolPeople(SchoolState School, Roster? Roster, ApplicantPool? Applicants, DefCatalog? Catalog);
|
||||
internal sealed record PublishedSchoolPeople(
|
||||
SchoolState School,
|
||||
Roster? Roster,
|
||||
ApplicantPool? Applicants,
|
||||
DefCatalog? Catalog,
|
||||
Timetable? Timetable);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Protocol;
|
||||
using HSchool.Schedule;
|
||||
using HSchool.Simulation;
|
||||
|
||||
namespace HSchool.Server.Game;
|
||||
|
||||
/// <summary>
|
||||
/// Overlays the current lesson onto map-tree nodes. Occupancy is derived from the timetable and
|
||||
/// the clock; it is not stored on the map.
|
||||
/// </summary>
|
||||
internal static class MapOccupancy
|
||||
{
|
||||
public static void Apply(
|
||||
MapSnapshotNode[] nodes,
|
||||
School school,
|
||||
int weekDays,
|
||||
string locale)
|
||||
{
|
||||
if (school.Timetable is null || school.Roster is null || school.Catalog is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var occurring = TimetableClock.OccurringAt(
|
||||
school.Timetable,
|
||||
school.Catalog,
|
||||
school.Clock.Time,
|
||||
weekDays);
|
||||
if (occurring.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var catalog = school.Catalog;
|
||||
var classes = school.Roster.Classes.ToDictionary(item => item.Id, StringComparer.Ordinal);
|
||||
var people = school.Roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||
var byRoom = occurring.ToDictionary(lesson => lesson.RoomId, StringComparer.Ordinal);
|
||||
|
||||
for (var i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
if (!byRoom.TryGetValue(nodes[i].Id, out var lesson))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
classes.TryGetValue(lesson.ClassId, out var schoolClass);
|
||||
var subjectLabel = catalog.Subjects.TryGetValue(lesson.Subject, out var subject)
|
||||
? catalog.Label(locale, subject)
|
||||
: lesson.Subject;
|
||||
var classLabel = schoolClass is null
|
||||
? lesson.ClassId
|
||||
: $"{schoolClass.Year}{schoolClass.Letter}";
|
||||
|
||||
nodes[i] = nodes[i] with
|
||||
{
|
||||
ActivitySubject = subjectLabel,
|
||||
ActivityClass = classLabel,
|
||||
Characters = NamesOf(lesson, schoolClass, people),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] NamesOf(
|
||||
LessonPlacement lesson,
|
||||
SchoolClass? schoolClass,
|
||||
IReadOnlyDictionary<string, Person> people)
|
||||
{
|
||||
var names = new List<string>();
|
||||
if (people.TryGetValue(lesson.TeacherId, out var teacher))
|
||||
{
|
||||
names.Add(teacher.Name.Full);
|
||||
}
|
||||
else if (lesson.TeacherId.Length > 0)
|
||||
{
|
||||
names.Add(lesson.TeacherId);
|
||||
}
|
||||
|
||||
if (schoolClass is not null)
|
||||
{
|
||||
foreach (var pupilId in schoolClass.PupilIds)
|
||||
{
|
||||
if (people.TryGetValue(pupilId, out var pupil))
|
||||
{
|
||||
names.Add(pupil.Name.Full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return names.Count > byte.MaxValue ? [.. names.Take(byte.MaxValue)] : [.. names];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Schedule;
|
||||
using HSchool.Simulation;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -104,7 +105,8 @@ internal sealed class SchoolStore
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
if (string.Equals(fileName, IndexFileName, StringComparison.OrdinalIgnoreCase)
|
||||
|| fileName.EndsWith(".people.json", StringComparison.OrdinalIgnoreCase))
|
||||
|| fileName.EndsWith(".people.json", StringComparison.OrdinalIgnoreCase)
|
||||
|| fileName.EndsWith(".timetable.json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -188,6 +190,12 @@ internal sealed class SchoolStore
|
||||
{
|
||||
File.Delete(people);
|
||||
}
|
||||
|
||||
var timetable = TimetablePath(id);
|
||||
if (File.Exists(timetable))
|
||||
{
|
||||
File.Delete(timetable);
|
||||
}
|
||||
}
|
||||
|
||||
public RosterDocument? TryReadPeople(int id)
|
||||
@@ -215,10 +223,38 @@ internal sealed class SchoolStore
|
||||
WriteAtomic(PeoplePath(id), document, RosterJson.Options);
|
||||
}
|
||||
|
||||
public Timetable? TryReadTimetable(int id)
|
||||
{
|
||||
var path = TimetablePath(id);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonSerializer.Deserialize<Timetable>(json, Json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new SchoolContentUnavailableException(
|
||||
$"School {id} timetable file could not be read.",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveTimetable(int id, Timetable table)
|
||||
{
|
||||
WriteAtomic(TimetablePath(id), table);
|
||||
}
|
||||
|
||||
private string SchoolPath(int id) => Path.Combine(DirectoryPath, $"{id}.json");
|
||||
|
||||
private string PeoplePath(int id) => Path.Combine(DirectoryPath, $"{id}.people.json");
|
||||
|
||||
private string TimetablePath(int id) => Path.Combine(DirectoryPath, $"{id}.timetable.json");
|
||||
|
||||
private string IndexPath() => Path.Combine(DirectoryPath, IndexFileName);
|
||||
|
||||
private static void WriteAtomic<T>(string path, T value, JsonSerializerOptions? options = null)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Threading.Channels;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Protocol;
|
||||
using HSchool.Schedule;
|
||||
using HSchool.Server.Api;
|
||||
using HSchool.Server.Net;
|
||||
using HSchool.Simulation;
|
||||
@@ -44,6 +45,8 @@ internal sealed class SchoolWorker
|
||||
private Roster? _rosterSnapshot;
|
||||
private ApplicantPool? _applicantSnapshot;
|
||||
private DefCatalog? _catalogSnapshot;
|
||||
private Timetable? _timetableSnapshot;
|
||||
private OccupancyKey _occupancyKey;
|
||||
private School? _school;
|
||||
private Task? _run;
|
||||
private bool _persistOnStop = true;
|
||||
@@ -103,6 +106,9 @@ internal sealed class SchoolWorker
|
||||
/// <summary>Frozen catalog for this school. Safe to read from HTTP; it never mutates after load.</summary>
|
||||
public DefCatalog? CatalogSnapshot => Volatile.Read(ref _catalogSnapshot);
|
||||
|
||||
/// <summary>Last built timetable. Published like the roster — HTTP never reads the live school.</summary>
|
||||
public Timetable? TimetableSnapshot => Volatile.Read(ref _timetableSnapshot);
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_run = Task.Factory.StartNew(
|
||||
@@ -284,12 +290,17 @@ internal sealed class SchoolWorker
|
||||
if (peopleChanged)
|
||||
{
|
||||
PersistPeople();
|
||||
if (school.TimetableDirty)
|
||||
{
|
||||
RebuildTimetable(school);
|
||||
}
|
||||
}
|
||||
|
||||
if (steps > 0)
|
||||
{
|
||||
PublishSnapshot();
|
||||
BroadcastClock();
|
||||
MaybeBroadcastOccupancy(school);
|
||||
}
|
||||
|
||||
FlushSettings();
|
||||
@@ -402,6 +413,16 @@ internal sealed class SchoolWorker
|
||||
case WorkerCommand.UnassignSubject unassign:
|
||||
unassign.Result.TrySetResult(ApplyUnassign(school, unassign.PersonId, unassign.Subject));
|
||||
break;
|
||||
|
||||
case WorkerCommand.PinLesson pin:
|
||||
pin.Result.TrySetResult(
|
||||
ApplyPin(school, pin.ClassId, pin.Subject, pin.RoomId, pin.Day, pin.Period));
|
||||
break;
|
||||
|
||||
case WorkerCommand.UnpinLesson unpin:
|
||||
unpin.Result.TrySetResult(
|
||||
ApplyUnpin(school, unpin.ClassId, unpin.Subject, unpin.Day, unpin.Period));
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -443,6 +464,12 @@ internal sealed class SchoolWorker
|
||||
case WorkerCommand.UnassignSubject unassign:
|
||||
unassign.Result.TrySetResult(Staffing.UnknownSchool());
|
||||
break;
|
||||
case WorkerCommand.PinLesson pin:
|
||||
pin.Result.TrySetResult(TimetableOutcome.Fail(TimetableError.UnknownSchool));
|
||||
break;
|
||||
case WorkerCommand.UnpinLesson unpin:
|
||||
unpin.Result.TrySetResult(TimetableOutcome.Fail(TimetableError.UnknownSchool));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +489,12 @@ internal sealed class SchoolWorker
|
||||
case WorkerCommand.UnassignSubject unassign:
|
||||
unassign.Result.TrySetException(exception);
|
||||
break;
|
||||
case WorkerCommand.PinLesson pin:
|
||||
pin.Result.TrySetException(exception);
|
||||
break;
|
||||
case WorkerCommand.UnpinLesson unpin:
|
||||
unpin.Result.TrySetException(exception);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +524,7 @@ internal sealed class SchoolWorker
|
||||
{
|
||||
school.ApplyStaffing(outcome.Roster, outcome.Pool);
|
||||
PersistPeople();
|
||||
PublishSnapshot();
|
||||
RebuildTimetable(school);
|
||||
}
|
||||
|
||||
return outcome;
|
||||
@@ -531,6 +564,7 @@ internal sealed class SchoolWorker
|
||||
(byte)school.Clock.SpeedIndex));
|
||||
Volatile.Write(ref _rosterSnapshot, school.Roster);
|
||||
Volatile.Write(ref _applicantSnapshot, school.Applicants);
|
||||
Volatile.Write(ref _timetableSnapshot, school.Timetable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -555,6 +589,238 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
}
|
||||
|
||||
private void InstallTimetable(School school)
|
||||
{
|
||||
if (!_isNew)
|
||||
{
|
||||
var saved = _store.TryReadTimetable(_id);
|
||||
if (saved is not null)
|
||||
{
|
||||
var restored = RestoreTimetable(school, saved);
|
||||
school.SetTimetable(restored);
|
||||
if (!saved.Lessons.SequenceEqual(restored.Lessons)
|
||||
|| !saved.Uncovered.SequenceEqual(restored.Uncovered))
|
||||
{
|
||||
PersistTimetable(school);
|
||||
}
|
||||
|
||||
RememberOccupancy(school);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
RebuildTimetable(school, broadcast: false);
|
||||
}
|
||||
|
||||
private Timetable RestoreTimetable(School school, Timetable saved)
|
||||
{
|
||||
if (school.Catalog is null || school.Map is null || school.Roster is null)
|
||||
{
|
||||
return saved;
|
||||
}
|
||||
|
||||
var classIds = school.Roster.Classes.Select(item => item.Id).ToHashSet(StringComparer.Ordinal);
|
||||
var peopleIds = school.Roster.People.Select(person => person.Id).ToHashSet(StringComparer.Ordinal);
|
||||
var valid = saved.Lessons
|
||||
.Where(lesson => classIds.Contains(lesson.ClassId) && peopleIds.Contains(lesson.TeacherId))
|
||||
.ToArray();
|
||||
if (valid.Length == saved.Lessons.Count)
|
||||
{
|
||||
return saved;
|
||||
}
|
||||
|
||||
var locks = valid.Where(lesson => lesson.Locked).ToArray();
|
||||
return SchoolTimetables.Build(
|
||||
school.Catalog,
|
||||
school.Map,
|
||||
school.Roster,
|
||||
locks,
|
||||
_options.SchoolWeekDays);
|
||||
}
|
||||
|
||||
private void RebuildTimetable(School school, bool broadcast = true)
|
||||
{
|
||||
if (school.Catalog is null || school.Map is null || school.Roster is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var locks = school.Timetable?.Lessons.Where(lesson => lesson.Locked).ToArray() ?? [];
|
||||
ApplyTable(
|
||||
school,
|
||||
SchoolTimetables.Build(school.Catalog, school.Map, school.Roster, locks, _options.SchoolWeekDays),
|
||||
broadcast);
|
||||
}
|
||||
|
||||
private TimetableOutcome ApplyPin(
|
||||
School school,
|
||||
string classId,
|
||||
string subject,
|
||||
string roomId,
|
||||
int day,
|
||||
int period)
|
||||
{
|
||||
if (school.Catalog is null || school.Map is null || school.Roster is null)
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.UnknownSchool);
|
||||
}
|
||||
|
||||
if (school.Roster.Classes.All(item => item.Id != classId))
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.UnknownClass);
|
||||
}
|
||||
|
||||
if (!school.Catalog.Subjects.TryGetValue(subject, out var subjectDef) || subjectDef.Abstract)
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.UnknownSubject);
|
||||
}
|
||||
|
||||
if (school.Map.Rooms.All(room => room.Id != roomId))
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.UnknownRoom);
|
||||
}
|
||||
|
||||
var teacherId = TeacherFor(school, classId, subject);
|
||||
if (teacherId is null)
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.NoTeacher);
|
||||
}
|
||||
|
||||
var pin = new LessonPlacement(classId, subject, teacherId, roomId, day, period, Locked: true);
|
||||
var locks = (school.Timetable?.Lessons.Where(lesson => lesson.Locked) ?? [])
|
||||
.Where(lesson => lesson.ClassId != classId || lesson.Subject != subject
|
||||
|| lesson.Day != day || lesson.Period != period)
|
||||
.Append(pin)
|
||||
.ToArray();
|
||||
var table = SchoolTimetables.Build(
|
||||
school.Catalog,
|
||||
school.Map,
|
||||
school.Roster,
|
||||
locks,
|
||||
_options.SchoolWeekDays);
|
||||
if (!table.Lessons.Any(lesson =>
|
||||
lesson.Locked
|
||||
&& lesson.ClassId == classId
|
||||
&& lesson.Subject == subject
|
||||
&& lesson.RoomId == roomId
|
||||
&& lesson.Day == day
|
||||
&& lesson.Period == period))
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.PinRejected);
|
||||
}
|
||||
|
||||
ApplyTable(school, table, broadcast: true);
|
||||
return TimetableOutcome.Ok(table);
|
||||
}
|
||||
|
||||
private TimetableOutcome ApplyUnpin(School school, string classId, string subject, int day, int period)
|
||||
{
|
||||
if (school.Catalog is null || school.Map is null || school.Roster is null)
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.UnknownSchool);
|
||||
}
|
||||
|
||||
var locks = school.Timetable?.Lessons.Where(lesson => lesson.Locked).ToArray() ?? [];
|
||||
var match = locks.FirstOrDefault(lesson =>
|
||||
lesson.ClassId == classId && lesson.Subject == subject && lesson.Day == day && lesson.Period == period);
|
||||
if (match is null)
|
||||
{
|
||||
return TimetableOutcome.Fail(TimetableError.UnknownLesson);
|
||||
}
|
||||
|
||||
var next = SchoolTimetables.Build(
|
||||
school.Catalog,
|
||||
school.Map,
|
||||
school.Roster,
|
||||
locks.Where(lesson => lesson != match).ToArray(),
|
||||
_options.SchoolWeekDays);
|
||||
ApplyTable(school, next, broadcast: true);
|
||||
return TimetableOutcome.Ok(next);
|
||||
}
|
||||
|
||||
private static string? TeacherFor(School school, string classId, string subject)
|
||||
{
|
||||
var existing = school.Timetable?.Lessons.FirstOrDefault(lesson =>
|
||||
lesson.ClassId == classId && lesson.Subject == subject);
|
||||
if (existing is not null)
|
||||
{
|
||||
return existing.TeacherId;
|
||||
}
|
||||
|
||||
return school.Roster?.People
|
||||
.Where(person => person.IsStaff && person.Subjects.Contains(subject, StringComparer.Ordinal))
|
||||
.OrderBy(person => person.Id, StringComparer.Ordinal)
|
||||
.Select(person => person.Id)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void ApplyTable(School school, Timetable table, bool broadcast)
|
||||
{
|
||||
school.SetTimetable(table);
|
||||
PersistTimetable(school);
|
||||
PublishSnapshot();
|
||||
if (broadcast)
|
||||
{
|
||||
MaybeBroadcastOccupancy(school, force: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
RememberOccupancy(school);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the lesson table. Not called from the 30-second clock save — the table changes on
|
||||
/// hire, unassign, pin and yearly intake, not every tick.
|
||||
/// </summary>
|
||||
private void PersistTimetable(School school)
|
||||
{
|
||||
if (school.Timetable is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_store.SaveTimetable(school.Id, school.Timetable);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not save the timetable for school {SchoolId}; it stays in memory.", _id);
|
||||
}
|
||||
}
|
||||
|
||||
private void RememberOccupancy(School school)
|
||||
{
|
||||
if (school.Catalog is not null)
|
||||
{
|
||||
_occupancyKey = TimetableClock.Key(school.Catalog, school.Clock.Time, _options.SchoolWeekDays);
|
||||
}
|
||||
}
|
||||
|
||||
private void MaybeBroadcastOccupancy(School school, bool force = false)
|
||||
{
|
||||
if (school.Catalog is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = TimetableClock.Key(school.Catalog, school.Clock.Time, _options.SchoolWeekDays);
|
||||
if (!force && key == _occupancyKey)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_occupancyKey = key;
|
||||
foreach (var client in _clients.All)
|
||||
{
|
||||
if (client.IsReady && client.OpenSchoolId == _id)
|
||||
{
|
||||
SendMapSnapshot(client, school);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool InstallPeople(School school, DefCatalog catalog, MapLayout map)
|
||||
{
|
||||
var nameSetId = ResolveNameSetId(catalog, _nameSetId);
|
||||
@@ -609,6 +875,7 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
|
||||
school.InstallPeople(roster, seed, nameSetId, applicants);
|
||||
InstallTimetable(school);
|
||||
return generated;
|
||||
}
|
||||
|
||||
@@ -708,6 +975,8 @@ internal sealed class SchoolWorker
|
||||
node.Positions);
|
||||
}
|
||||
|
||||
MapOccupancy.Apply(nodes, school, _options.SchoolWeekDays, locale);
|
||||
|
||||
// Sized from the message, not from the inbound frame limit: a map the player enlarged in
|
||||
// the create editor outgrows 8 KiB somewhere past sixty furnished rooms.
|
||||
var message = new ServerMapSnapshotMessage(school.Id, nodes);
|
||||
|
||||
@@ -37,4 +37,19 @@ internal abstract record WorkerCommand
|
||||
string PersonId,
|
||||
string Subject,
|
||||
TaskCompletionSource<StaffingOutcome> Result) : WorkerCommand;
|
||||
|
||||
internal sealed record PinLesson(
|
||||
string ClassId,
|
||||
string Subject,
|
||||
string RoomId,
|
||||
int Day,
|
||||
int Period,
|
||||
TaskCompletionSource<TimetableOutcome> Result) : WorkerCommand;
|
||||
|
||||
internal sealed record UnpinLesson(
|
||||
string ClassId,
|
||||
string Subject,
|
||||
int Day,
|
||||
int Period,
|
||||
TaskCompletionSource<TimetableOutcome> Result) : WorkerCommand;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user