Implement per-school save functionality by introducing a dedicated save directory and updating the school management system to support loading and saving school states. Revise documentation to reflect these changes, including updates to the architecture and design documents, and enhance the API for reloading schools from disk. Update tests to ensure proper functionality of the new save and reload features.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
using System.Text.Json;
|
||||
using HSchool.Simulation;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HSchool.Server.Game;
|
||||
|
||||
/// <summary>On-disk record of one school. Extra JSON fields are ignored so later slices can grow it.</summary>
|
||||
internal sealed record SchoolSave(int Format, int Id, string Name, DateTime GameTime, bool Running, int SpeedIndex);
|
||||
|
||||
/// <summary>Allocates school ids that survive a process restart.</summary>
|
||||
internal sealed record SchoolSaveIndex(int NextId);
|
||||
|
||||
/// <summary>
|
||||
/// JSON files under <see cref="SimulationOptions.SavesDirectory"/>. The worker of a school is the
|
||||
/// only writer of that school's file; the supervisor reads the directory at start and on reload.
|
||||
/// </summary>
|
||||
internal sealed class SchoolStore
|
||||
{
|
||||
public const int CurrentFormat = 1;
|
||||
|
||||
private const string IndexFileName = "index.json";
|
||||
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private readonly ILogger<SchoolStore> _logger;
|
||||
|
||||
public SchoolStore(IOptions<SimulationOptions> options, IHostEnvironment environment, ILogger<SchoolStore> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var configured = options.Value.SavesDirectory;
|
||||
DirectoryPath = Path.IsPathRooted(configured)
|
||||
? configured
|
||||
: Path.GetFullPath(Path.Combine(environment.ContentRootPath, configured));
|
||||
|
||||
Directory.CreateDirectory(DirectoryPath);
|
||||
logger.LogInformation("School saves directory is {Directory}.", DirectoryPath);
|
||||
}
|
||||
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
public int ReadNextId()
|
||||
{
|
||||
var path = IndexPath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
var index = JsonSerializer.Deserialize<SchoolSaveIndex>(json, Json);
|
||||
return index is { NextId: > 0 } ? index.NextId : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not read {Path}; school ids will start from the files on disk.", path);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteNextId(int nextId)
|
||||
{
|
||||
WriteAtomic(IndexPath(), new SchoolSaveIndex(nextId));
|
||||
}
|
||||
|
||||
public IReadOnlyList<SchoolSave> LoadAll()
|
||||
{
|
||||
var saves = new List<SchoolSave>();
|
||||
|
||||
foreach (var path in Directory.EnumerateFiles(DirectoryPath, "*.json"))
|
||||
{
|
||||
if (string.Equals(Path.GetFileName(path), IndexFileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
var save = JsonSerializer.Deserialize<SchoolSave>(json, Json);
|
||||
if (save is null)
|
||||
{
|
||||
_logger.LogWarning("Save {Path} deserialized to nothing; leaving the file in place.", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!GameClock.IsValidStartDate(save.GameTime))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Save {Path} has a game time outside the supported range; leaving the file in place.",
|
||||
path);
|
||||
continue;
|
||||
}
|
||||
|
||||
saves.Add(save with { GameTime = DateTime.SpecifyKind(save.GameTime, DateTimeKind.Utc) });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not read save {Path}; leaving the file in place.", path);
|
||||
}
|
||||
}
|
||||
|
||||
saves.Sort((left, right) => left.Id.CompareTo(right.Id));
|
||||
return saves;
|
||||
}
|
||||
|
||||
public void Save(SchoolSave save)
|
||||
{
|
||||
WriteAtomic(SchoolPath(save.Id), save);
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
var path = SchoolPath(id);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private string SchoolPath(int id) => Path.Combine(DirectoryPath, $"{id}.json");
|
||||
|
||||
private string IndexPath() => Path.Combine(DirectoryPath, IndexFileName);
|
||||
|
||||
private static void WriteAtomic<T>(string path, T value)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value, Json);
|
||||
var temp = path + ".tmp";
|
||||
File.WriteAllText(temp, json);
|
||||
File.Move(temp, path, overwrite: true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user