use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub const PLAY_VERSION: u32 = 1;
pub const DEFAULT_PORT: u16 = 15702;
fn default_port() -> u16 {
DEFAULT_PORT
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PlayConfig {
pub version: u32,
pub command: Vec<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default = "default_port")]
pub port: u16,
}
pub fn path_for_project(root: &Path) -> PathBuf {
root.join("play.ron")
}
impl PlayConfig {
pub fn from_ron(text: &str) -> Result<PlayConfig, String> {
let c: PlayConfig = ron::from_str(text).map_err(|e| format!("play: parse: {e}"))?;
if c.version != PLAY_VERSION {
return Err(format!("play: version {} is not the {PLAY_VERSION} this build reads", c.version));
}
if c.command.first().is_none_or(|p| p.trim().is_empty()) {
return Err("play: `command` is empty, so there is no game to start".to_owned());
}
Ok(c)
}
pub fn load(root: &Path) -> Result<PlayConfig, String> {
let path = path_for_project(root);
match std::fs::read_to_string(&path) {
Ok(text) => PlayConfig::from_ron(&text).map_err(|e| format!("{}: {e}", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(format!(
"{} does not exist. Write `(version: 1, command: [\"cargo\", \"run\", \"--\", \"{{map}}\"])` there: how to run this game.",
path.display()
)),
Err(e) => Err(format!("play: {}: {e}", path.display())),
}
}
pub fn argv(&self, root: &Path, map: &str, camera: Option<[f32; 3]>) -> Vec<String> {
let cam = camera.map(|c| format!("{},{},{}", c[0], c[1], c[2])).unwrap_or_default();
self.command
.iter()
.map(|a| {
a.replace("{map}", map).replace("{root}", &root.to_string_lossy()).replace("{camera}", &cam)
})
.collect()
}
pub fn dir(&self, root: &Path) -> PathBuf {
match &self.cwd {
Some(c) => root.join(c),
None => root.to_path_buf(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_command_takes_the_map_and_camera() {
let c = PlayConfig::from_ron(r#"(version: 1, command: ["g", "--map", "{map}", "--at={camera}"])"#).unwrap();
assert_eq!(c.port, DEFAULT_PORT);
assert_eq!(c.argv(Path::new("/p"), "town", Some([1.0, 2.5, -3.0])), ["g", "--map", "town", "--at=1,2.5,-3"]);
}
#[test]
fn an_empty_command_is_refused() {
assert!(PlayConfig::from_ron("(version: 1, command: [])").is_err());
}
}