Introduce production-ready CLI for hosting Commonwealth Online servers in cloud and on-premises environments. Features: - Server orchestration service (server_service.py) wrapping relay lifecycle - JSON configuration system (config.py) for hosted deployments - Typer+Rich CLI (consumer_server_cli.py) with serve/status/clients/world commands - Windows launcher (start.bat) for one-click server startup - Auto-detection of LAN addresses and dependency installation - Machine-readable JSON output for monitoring and automation Cli commands: serve - Start server with optional config overrides status - Display server stats and packet counters clients - List connected players world time - Set in-game time for all clients world weather - Set weather for all clients config init - Generate default configuration file Documentation: - Updated docs/setup.md with CLI quick-start guide - Added server/README.md with usage instructions - Updated changelog and dev-log with test results Testing: - Verified config generation and loading - Verified server startup banner and LAN detection - Verified fake client connection and welcome packet - Verified CLI help and command routing Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
3.5 KiB
Python
135 lines
3.5 KiB
Python
"""
|
|
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
|