Split SchoolWorker and the person card along existing seams without a second thread or public API.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
using System.Diagnostics;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Schedule;
|
||||
using HSchool.Simulation;
|
||||
|
||||
namespace HSchool.Server.Game;
|
||||
|
||||
internal sealed partial class SchoolWorker
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes pause/speed changes, at most once per <see cref="SimulationOptions.MinSaveInterval"/>.
|
||||
/// A single click still lands within that window; a burst collapses into one write.
|
||||
/// </summary>
|
||||
private void FlushSettings()
|
||||
{
|
||||
if (!_settingsDirty || Stopwatch.GetElapsedTime(_lastSettingsSave) < _options.MinSaveInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Persist();
|
||||
_settingsDirty = false;
|
||||
_lastSettingsSave = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
var school = _school;
|
||||
if (school is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Volatile.Write(
|
||||
ref _snapshot,
|
||||
new SchoolState(
|
||||
school.Id,
|
||||
school.Name,
|
||||
school.Clock.Time,
|
||||
school.Clock.IsRunning,
|
||||
(byte)school.Clock.SpeedIndex,
|
||||
school.Catalog?.PackIds ?? _modIds ?? [],
|
||||
school.PeopleSeed,
|
||||
_owner));
|
||||
Volatile.Write(ref _rosterSnapshot, school.Roster);
|
||||
Volatile.Write(ref _applicantSnapshot, school.Applicants);
|
||||
Volatile.Write(ref _timetableSnapshot, school.Timetable);
|
||||
Volatile.Write(ref _mapSnapshot, school.Map);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the composition file. Not called from the 30-second clock save — the roster and
|
||||
/// applicant pool change on create, hire, weekly refresh and yearly intake, not every tick.
|
||||
/// </summary>
|
||||
private void PersistPeople()
|
||||
{
|
||||
var school = _school;
|
||||
if (school?.Roster is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_store.SavePeople(school.Id, RosterDocument.From(school.PeopleSeed, school.Roster, school.Applicants));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not save people for school {SchoolId}; composition stays in memory.", _id);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 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)
|
||||
{
|
||||
BroadcastPresence();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 Persist()
|
||||
{
|
||||
var school = _school;
|
||||
if (school is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A full disk or a locked file must not end the school; the next save will try again.
|
||||
try
|
||||
{
|
||||
_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,
|
||||
CountryId = school.CountryId,
|
||||
ClimatePresetId = school.ClimatePresetId,
|
||||
NativeLanguage = _nativeLanguage,
|
||||
Presence = school.CapturePresence(),
|
||||
DressRules = school.DressRules,
|
||||
Owner = _owner,
|
||||
PortraitSettings = _portraitSettings,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not save school {SchoolId}; it keeps running unsaved.", _id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user