using System.Diagnostics; using HSchool.Content; using HSchool.People; using HSchool.Schedule; using HSchool.Simulation; namespace HSchool.Server.Game; internal sealed partial class SchoolWorker { /// /// Writes pause/speed changes, at most once per . /// A single click still lands within that window; a burst collapses into one write. /// 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); } /// /// 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. /// 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(); } } /// /// 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. /// 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, SpeechRules = school.SpeechRules, Owner = _owner, PortraitSettings = _portraitSettings, Notices = _notices.ToSave(), }); } catch (Exception ex) { _logger.LogError(ex, "Could not save school {SchoolId}; it keeps running unsaved.", _id); } } }