use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ServerState { Installing, Stopped, Starting, Running, Stopping, Error, } impl std::fmt::Display for ServerState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Installing => write!(f, "installing"), Self::Stopped => write!(f, "stopped"), Self::Starting => write!(f, "starting"), Self::Running => write!(f, "running"), Self::Stopping => write!(f, "stopping"), Self::Error => write!(f, "error"), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PortMap { pub host_port: u16, pub container_port: u16, pub protocol: String, // "tcp" or "udp" } /// Per-game runtime knobs supplied by the panel. Mirrored into Docker labels so /// they survive a daemon restart (see `docker::container`). #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ServerRuntime { /// Mount point of the data directory inside the container. `None` means /// "derive it from the image". pub data_mount_path: Option, /// In-game command that shuts the server down cleanly (e.g. `stop`, `quit`). pub stop_command: Option, /// Total budget for a graceful shutdown before the container gets killed. pub stop_timeout_seconds: Option, } impl ServerRuntime { pub fn from_request( data_mount_path: String, stop_command: String, stop_timeout_seconds: i32, ) -> Self { Self { data_mount_path: non_empty(data_mount_path), stop_command: non_empty(stop_command), stop_timeout_seconds: if stop_timeout_seconds > 0 { Some(stop_timeout_seconds as i64) } else { None }, } } } fn non_empty(value: String) -> Option { let trimmed = value.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerSpec { pub uuid: String, pub docker_image: String, pub memory_limit: i64, // bytes pub disk_limit: i64, // bytes pub cpu_limit: i32, // percentage (100 = 1 core) pub startup_command: String, pub environment: HashMap, pub ports: Vec, pub data_path: PathBuf, pub state: ServerState, pub container_id: Option, #[serde(default)] pub runtime: ServerRuntime, } impl ServerSpec { /// Check if the server can transition to the requested state. pub fn can_transition_to(&self, target: &ServerState) -> bool { matches!( (&self.state, target), (ServerState::Installing, ServerState::Stopped) | (ServerState::Installing, ServerState::Error) | (ServerState::Stopped, ServerState::Starting) | (ServerState::Starting, ServerState::Running) | (ServerState::Starting, ServerState::Error) | (ServerState::Running, ServerState::Stopping) | (ServerState::Running, ServerState::Error) | (ServerState::Stopping, ServerState::Stopped) | (ServerState::Stopping, ServerState::Error) | (ServerState::Error, ServerState::Starting) | (ServerState::Error, ServerState::Stopped) ) } }