using System.Text.Json; namespace HSchool.Server.Game; /// One registered player name, stored exactly as typed on first login. internal sealed record UserRecord(string Name); /// Persistent user list beside school saves. 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 _logger; private readonly string _path; private List _users = []; public UserStore(SchoolStore schools, ILogger logger) { _logger = logger; _path = Path.Combine(schools.DirectoryPath, UsersFileName); Load(); } /// /// Returns the canonical spelling for or registers /// on first use. /// 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(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 Users); }