using ActionBlocks.Game; using ActionBlocks.Game.Items; using Microsoft.Extensions.Primitives; namespace ActionBlocks.Server.Config; /// Settings and items for new matches. A match keeps the instance it started with. public sealed record GameConfig(GameSettings Settings, ItemRegistry Items); /// /// Binds the "Game" configuration section and follows changes to config/game.json. /// An invalid config at startup stops the server; an invalid edit later is logged and the last good config stays. /// public sealed class GameConfigProvider : IDisposable { private readonly IConfiguration _configuration; private readonly GameConfigFile _file; private readonly ILogger _logger; private readonly IDisposable _subscription; private volatile GameConfig _current; public GameConfigProvider(IConfiguration configuration, GameConfigFile file, ILogger logger) { _configuration = configuration; _file = file; _logger = logger; var (config, errors) = Load(); if (config is null) { throw new InvalidOperationException($"Invalid game config ({file.Path ?? "no file"}):\n {string.Join("\n ", errors)}"); } _current = config; if (file.Path is null) { _logger.LogInformation("No {File} found, using built-in game settings", "config/game.json"); } LogValues("Game config loaded", config); _subscription = ChangeToken.OnChange(configuration.GetReloadToken, Reload); } public GameConfig Current => _current; private void Reload() { if (_file.Path is not null && !_file.Exists) { _logger.LogWarning("Game config {Path} is missing, keeping the previous values", _file.Path); return; } var (config, errors) = Load(); if (config is null) { _logger.LogError( "Game config change ignored, keeping the previous values:\n {Errors}", string.Join("\n ", errors)); return; } _current = config; LogValues("Game config reloaded, used from the next match", config); } private (GameConfig? Config, IReadOnlyList Errors) Load() { if (_file.TakeLoadError() is { } loadError) { return (null, [loadError]); } GameSettings settings; try { settings = _configuration.GetSection("Game").Get() ?? new GameSettings(); } catch (InvalidOperationException e) { // A value of the wrong type, e.g. "Weight": "high". return (null, [e.InnerException?.Message ?? e.Message]); } var errors = settings.Validate(); return errors.Count > 0 ? (null, errors) : (new GameConfig(settings, ItemRegistry.FromSettings(settings)), errors); } private void LogValues(string message, GameConfig config) { var items = config.Items.All.Select(i => $"{i.Id} {config.Items.Share(i.Id):P0}" + (i.DurationSeconds is { } d ? $" {d}s" : "")); _logger.LogInformation( "{Message}: item chance {Chance} per block; {Items}", message, config.Settings.ItemSpawnChance, string.Join(", ", items)); } public void Dispose() => _subscription.Dispose(); }