namespace HSchool.Simulation; /// Why a school could not be created. public enum SchoolCreationError { None = 0, LimitReached, InvalidName, InvalidStartDate, } /// Outcome of : either the school or the reason there is none. public readonly record struct SchoolCreationResult(School? School, SchoolCreationError Error) { public bool Succeeded => Error == SchoolCreationError.None && School is not null; public static SchoolCreationResult Failed(SchoolCreationError error) => new(null, error); } /// /// Every school that currently exists, plus the cap from configuration. Not thread-safe by design — /// only the loop thread touches it, everything else goes through the command queue in the server. /// public sealed class SchoolRegistry : IDisposable { private readonly SimulationOptions _options; private readonly List _schools = []; private int _nextId = 1; private bool _disposed; public SchoolRegistry(SimulationOptions options) { _options = options; NameGenerator = new SchoolNameGenerator(); } public SchoolNameGenerator NameGenerator { get; } public int MaxSchools => _options.MaxSchools; public int Count => _schools.Count; public bool IsFull => _schools.Count >= _options.MaxSchools; /// Schools in creation order — the order the menu lists them in. public IReadOnlyList Schools => _schools; public School? Find(int id) => _schools.Find(school => school.Id == id); public SchoolCreationResult Create(string name, DateTime startDate) { ObjectDisposedException.ThrowIf(_disposed, this); if (IsFull) { return SchoolCreationResult.Failed(SchoolCreationError.LimitReached); } if (!TryNormalizeName(name, out var normalized)) { return SchoolCreationResult.Failed(SchoolCreationError.InvalidName); } if (!GameClock.IsValidStartDate(startDate)) { return SchoolCreationResult.Failed(SchoolCreationError.InvalidStartDate); } var school = new School(_nextId++, normalized, startDate); _schools.Add(school); return new SchoolCreationResult(school, SchoolCreationError.None); } public bool Delete(int id) { ObjectDisposedException.ThrowIf(_disposed, this); var school = Find(id); if (school is null) { return false; } _schools.Remove(school); school.Dispose(); return true; } /// Advances every running school by one fixed step. public void Tick() { ObjectDisposedException.ThrowIf(_disposed, this); foreach (var school in _schools) { school.Tick(_options.FixedDeltaTime, _options.GameMinutesPerRealSecond); } } /// A name the player has not used yet, for the "random" button in the creation form. public string SuggestName(SchoolNameLanguage language = SchoolNameLanguage.Russian) => NameGenerator.Next(_schools.Select(school => school.Name), language); /// Trims, strips control characters and enforces the length limit. public static bool TryNormalizeName(string? name, out string normalized) { normalized = string.Empty; if (string.IsNullOrWhiteSpace(name)) { return false; } var cleaned = new string(name.Where(character => !char.IsControl(character)).ToArray()).Trim(); if (cleaned.Length == 0 || cleaned.Length > School.MaxNameLength) { return false; } normalized = cleaned; return true; } public void Dispose() { if (_disposed) { return; } _disposed = true; foreach (var school in _schools) { school.Dispose(); } _schools.Clear(); } }