Files
h-school/src/HSchool.Server/Game/SchoolWorker.Persist.cs
T

222 lines
6.8 KiB
C#

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,
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);
}
}
}