use std::path::{Path, PathBuf};
const ASSETS_DIR: &str = "assets";
const DATA_DIR: &str = "data";
const WORLDS_DIR: &str = "worlds";
const SAVES_DIR: &str = "saves";
const PREVIEW_SAVES_DIR: &str = "preview-saves";
const SETTINGS_FILE: &str = "settings";
const CRASHES_DIR: &str = "crashes";
const EDITOR_SESSION_FILE: &str = "editor";
const CACHE_DIR: &str = "cache";
const RUNTIME_CACHE_SEGMENT: &str = "0";
const BUILD_CACHE_SEGMENT: &str = "1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateTree {
content: PathBuf,
writable: Option<PathBuf>,
cache: Option<PathBuf>,
}
impl StateTree {
pub fn at<P: Into<PathBuf>>(content: P) -> Self {
Self {
content: content.into(),
writable: None,
cache: None,
}
}
#[must_use]
pub fn with_writable<P: Into<PathBuf>>(mut self, dir: P) -> Self {
self.writable = Some(dir.into());
self
}
#[must_use]
pub fn with_cache<P: Into<PathBuf>>(mut self, dir: P) -> Self {
self.cache = Some(dir.into());
self
}
pub fn content_root(&self) -> &Path {
&self.content
}
pub fn writable_root(&self) -> &Path {
self.writable.as_deref().unwrap_or(&self.content)
}
pub fn assets_dir(&self) -> PathBuf {
self.content.join(ASSETS_DIR)
}
pub fn data_dir(&self) -> PathBuf {
self.content.join(DATA_DIR)
}
pub fn worlds_dir(&self) -> PathBuf {
self.content.join(WORLDS_DIR)
}
pub fn saves_dir(&self) -> PathBuf {
self.writable_root().join(SAVES_DIR)
}
pub fn preview_saves_dir(&self) -> PathBuf {
self.writable_root().join(PREVIEW_SAVES_DIR)
}
pub fn settings_path(&self) -> PathBuf {
self.writable_root().join(SETTINGS_FILE)
}
pub fn crashes_dir(&self) -> PathBuf {
self.writable_root().join(CRASHES_DIR)
}
pub fn editor_session_path(&self) -> PathBuf {
self.writable_root().join(EDITOR_SESSION_FILE)
}
pub fn runtime_cache_path(&self) -> PathBuf {
self.cache_root_for_runtime()
.join(CACHE_DIR)
.join(RUNTIME_CACHE_SEGMENT)
}
pub fn bundled_runtime_cache_path(&self) -> PathBuf {
self.content.join(CACHE_DIR).join(RUNTIME_CACHE_SEGMENT)
}
pub fn build_cache_path(&self) -> PathBuf {
self.cache_root_for_build()
.join(CACHE_DIR)
.join(BUILD_CACHE_SEGMENT)
}
fn cache_root_for_runtime(&self) -> &Path {
self.cache
.as_deref()
.unwrap_or_else(|| self.writable_root())
}
fn cache_root_for_build(&self) -> &Path {
self.cache.as_deref().unwrap_or(&self.content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_root_resolves_the_whole_layout() {
let tree = StateTree::at("/flat");
let root = Path::new("/flat");
assert_eq!(tree.content_root(), root);
assert_eq!(tree.writable_root(), root);
assert_eq!(tree.assets_dir(), root.join("assets"));
assert_eq!(tree.data_dir(), root.join("data"));
assert_eq!(tree.worlds_dir(), root.join("worlds"));
assert_eq!(tree.saves_dir(), root.join("saves"));
assert_eq!(tree.preview_saves_dir(), root.join("preview-saves"));
assert_eq!(tree.settings_path(), root.join("settings"));
assert_eq!(tree.crashes_dir(), root.join("crashes"));
assert_eq!(tree.editor_session_path(), root.join("editor"));
assert_eq!(tree.runtime_cache_path(), root.join("cache").join("0"));
assert_eq!(tree.build_cache_path(), root.join("cache").join("1"));
assert_eq!(tree.bundled_runtime_cache_path(), tree.runtime_cache_path());
}
#[test]
fn a_writable_root_moves_only_what_the_application_writes() {
let content = Path::new("/opt/MyGame");
let writable = Path::new("/home/u/.local/share/MyGame");
let tree = StateTree::at(content).with_writable(writable);
assert_eq!(tree.content_root(), content);
assert_eq!(tree.writable_root(), writable);
assert_eq!(tree.saves_dir(), writable.join("saves"));
assert_eq!(tree.preview_saves_dir(), writable.join("preview-saves"));
assert_eq!(tree.settings_path(), writable.join("settings"));
assert_eq!(tree.crashes_dir(), writable.join("crashes"));
assert_eq!(tree.editor_session_path(), writable.join("editor"));
assert_eq!(tree.runtime_cache_path(), writable.join("cache").join("0"));
assert_eq!(tree.data_dir(), content.join("data"));
assert_eq!(tree.assets_dir(), content.join("assets"));
assert_eq!(tree.worlds_dir(), content.join("worlds"));
assert_eq!(tree.build_cache_path(), content.join("cache").join("1"));
assert_eq!(
tree.bundled_runtime_cache_path(),
content.join("cache").join("0")
);
}
#[test]
fn a_cache_root_moves_both_segments_and_nothing_else() {
let content = Path::new("/build/content");
let cache = Path::new("/var/cache/mygame");
let tree = StateTree::at(content).with_cache(cache);
assert_eq!(tree.runtime_cache_path(), cache.join("cache").join("0"));
assert_eq!(tree.build_cache_path(), cache.join("cache").join("1"));
assert_eq!(
tree.bundled_runtime_cache_path(),
content.join("cache").join("0")
);
assert_eq!(tree.data_dir(), content.join("data"));
assert_eq!(tree.saves_dir(), content.join("saves"));
}
#[test]
fn all_three_roots_split_independently() {
let tree = StateTree::at("/opt/app")
.with_writable("/home/u/app")
.with_cache("/var/cache/app");
assert_eq!(tree.data_dir(), Path::new("/opt/app/data"));
assert_eq!(tree.saves_dir(), Path::new("/home/u/app/saves"));
assert_eq!(
tree.runtime_cache_path(),
Path::new("/var/cache/app/cache/0")
);
assert_eq!(tree.build_cache_path(), Path::new("/var/cache/app/cache/1"));
}
#[test]
fn builders_do_not_disturb_each_other() {
let base = StateTree::at("/root");
assert_eq!(
base.clone().with_cache("/c").saves_dir(),
base.saves_dir(),
"a cache root leaves the writable state alone"
);
assert_eq!(
base.clone().with_writable("/w").build_cache_path(),
base.build_cache_path(),
"a writable root leaves the build segment with the content"
);
}
}