""" Configuration model and loading for Commonwealth Online server. Supports JSON-based configuration files for hosted deployment. """ from __future__ import annotations import json import os from dataclasses import dataclass from pathlib import Path from typing import Any @dataclass class Config: """Server configuration model.""" host: str = "0.0.0.0" port: int = 7777 server_name: str = "Commonwealth Online Server" max_players: int = 16 log_verbosity: str = "info" def to_dict(self) -> dict[str, Any]: """Convert config to dictionary.""" return { "host": self.host, "port": self.port, "server_name": self.server_name, "max_players": self.max_players, "log_verbosity": self.log_verbosity, } @classmethod def from_dict(cls, data: dict[str, Any]) -> Config: """Create config from dictionary.""" return cls( host=data.get("host", "0.0.0.0"), port=int(data.get("port", 7777)), server_name=data.get("server_name", "Commonwealth Online Server"), max_players=int(data.get("max_players", 16)), log_verbosity=data.get("log_verbosity", "info"), ) def load_config(config_path: str | None = None) -> Config: """ Load configuration from file or use defaults. Args: config_path: Path to config.json file. If None, returns defaults. Returns: Config instance. Raises: FileNotFoundError: If config_path is provided but file does not exist. json.JSONDecodeError: If config file is invalid JSON. """ if config_path is None: return Config() path = Path(config_path) if not path.exists(): raise FileNotFoundError(f"Config file not found: {config_path}") with open(path, "r") as f: data = json.load(f) if not isinstance(data, dict): raise ValueError("Config file must contain a JSON object at root level.") return Config.from_dict(data) def save_config(config: Config, config_path: str) -> None: """ Save configuration to a JSON file. Args: config: Config instance to save. config_path: Path to write config.json. """ path = Path(config_path) path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w") as f: json.dump(config.to_dict(), f, indent=2) def generate_default_config(config_path: str) -> Config: """ Generate and save a default configuration file. Args: config_path: Path where default config.json will be written. Returns: The generated Config instance. """ config = Config() save_config(config, config_path) return config def validate_config(config: Config) -> tuple[bool, list[str]]: """ Validate configuration values. Args: config: Config to validate. Returns: (is_valid, list_of_errors). Empty list if valid. """ errors = [] if not config.host: errors.append("host cannot be empty") if config.port < 1 or config.port > 65535: errors.append(f"port must be 1-65535, got {config.port}") if not config.server_name: errors.append("server_name cannot be empty") if config.max_players < 1: errors.append(f"max_players must be >= 1, got {config.max_players}") if config.log_verbosity not in ("debug", "info", "warning", "error"): errors.append(f"log_verbosity must be debug/info/warning/error, got {config.log_verbosity}") return len(errors) == 0, errors