Merge branch 'phase/38-session'
# Conflicts: # docs/phases/README.md # tests/HSchool.AppHost.Tests/PortraitApiTests.cs
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HSchool.Server.Game;
|
||||
|
||||
/// <summary>One registered player name, stored exactly as typed on first login.</summary>
|
||||
internal sealed record UserRecord(string Name);
|
||||
|
||||
/// <summary>Persistent user list beside school saves.</summary>
|
||||
internal sealed class UserStore
|
||||
{
|
||||
private const string UsersFileName = "users.json";
|
||||
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly ILogger<UserStore> _logger;
|
||||
private readonly string _path;
|
||||
private List<UserRecord> _users = [];
|
||||
|
||||
public UserStore(SchoolStore schools, ILogger<UserStore> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_path = Path.Combine(schools.DirectoryPath, UsersFileName);
|
||||
Load();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the canonical spelling for <paramref name="normalized"/> or registers
|
||||
/// <paramref name="displayName"/> on first use.
|
||||
/// </summary>
|
||||
public string ResolveOrRegister(string normalized, string displayName)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var existing = FindCanonicalLocked(normalized);
|
||||
if (existing is not null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
_users.Add(new UserRecord(displayName));
|
||||
SaveLocked();
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryFindCanonical(string normalized, out string canonical)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
canonical = FindCanonicalLocked(normalized) ?? "";
|
||||
return canonical.Length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private string? FindCanonicalLocked(string normalized)
|
||||
{
|
||||
foreach (var user in _users)
|
||||
{
|
||||
if (string.Equals(user.Name, normalized, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return user.Name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
if (!File.Exists(_path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var document = JsonSerializer.Deserialize<UserDocument>(File.ReadAllText(_path), Json);
|
||||
_users = document?.Users?.ToList() ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not read {Path}; starting with an empty user list.", _path);
|
||||
_users = [];
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveLocked()
|
||||
{
|
||||
WriteAtomic(_path, new UserDocument(_users));
|
||||
}
|
||||
|
||||
private static void WriteAtomic(string path, UserDocument document)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(document, Json);
|
||||
var temp = path + ".tmp";
|
||||
File.WriteAllText(temp, json);
|
||||
File.Move(temp, path, overwrite: true);
|
||||
}
|
||||
|
||||
private sealed record UserDocument(IReadOnlyList<UserRecord> Users);
|
||||
}
|
||||
Reference in New Issue
Block a user