Files
Commonwealth-Online-Public/server/config.py
T
andrewandCursor 7a3e0855b0 Add consumer server CLI with Typer + Rich and Windows launcher
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>
2026-07-09 20:44:40 +12:00

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