Skip to main content

concinnity_host/store/paths/
tree.rs

1// The state tree as a value: a content root, the two roots that may be split
2// away from it, and the layout hanging off all three.
3//
4// Every directory and file name the tree is made of is spelled once, here. A
5// caller asks for `saves_dir()` or `build_cache_path()`; that `cache/1` is a
6// build's segment is this module's knowledge and nobody else's.
7
8use std::path::{Path, PathBuf};
9
10// The tree's layout, spelled once. Private: a caller asks the tree for a path
11// rather than for a segment's name, so nothing outside this module has to know
12// that a build's cache is `cache/1`.
13const ASSETS_DIR: &str = "assets";
14const DATA_DIR: &str = "data";
15const WORLDS_DIR: &str = "worlds";
16const SAVES_DIR: &str = "saves";
17const PREVIEW_SAVES_DIR: &str = "preview-saves";
18const SETTINGS_FILE: &str = "settings";
19const CRASHES_DIR: &str = "crashes";
20const EDITOR_SESSION_FILE: &str = "editor";
21const CACHE_DIR: &str = "cache";
22const RUNTIME_CACHE_SEGMENT: &str = "0";
23const BUILD_CACHE_SEGMENT: &str = "1";
24
25/// Where a project's state lives, and what hangs off it.
26///
27/// Built by whatever runs the process -- the dev CLI from its project
28/// directory, a shipped application from the directory beside its executable,
29/// an embedder from whatever its own layout implies -- and passed down. Library
30/// code is handed one; it never resolves a root for itself.
31///
32/// Three roots, because the three fall apart on real installs:
33///
34/// - the **content** root holds what a build produces and reads (`data/`,
35///   `assets/`, `worlds/`),
36/// - the **writable** root holds what the running application writes
37///   (`saves/`, `settings`, `crashes/`), split away when the content root is a
38///   read-only install such as Program Files,
39/// - the **cache** root holds the regenerable segments, split away when the
40///   caches should outlive (or be shared across) the content beside them.
41///
42/// An unsplit tree resolves all three at the content root, which is the
43/// single-folder layout a portable install and a dev checkout both use.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct StateTree {
46    content: PathBuf,
47    writable: Option<PathBuf>,
48    cache: Option<PathBuf>,
49}
50
51impl StateTree {
52    /// A tree with every root at `content`.
53    pub fn at<P: Into<PathBuf>>(content: P) -> Self {
54        Self {
55            content: content.into(),
56            writable: None,
57            cache: None,
58        }
59    }
60
61    /// Move the runtime-writable state to `dir`, leaving the content where it
62    /// is: a read-only install writes its saves and settings per-user.
63    #[must_use]
64    pub fn with_writable<P: Into<PathBuf>>(mut self, dir: P) -> Self {
65        self.writable = Some(dir.into());
66        self
67    }
68
69    /// Move both cache segments to `dir`. Anchors the regenerable artifacts
70    /// away from the content and the writable state, so a warm cache can sit
71    /// behind a content root that is read-only, or freshly built, or both.
72    #[must_use]
73    pub fn with_cache<P: Into<PathBuf>>(mut self, dir: P) -> Self {
74        self.cache = Some(dir.into());
75        self
76    }
77
78    /// The root holding what a build produces and reads.
79    pub fn content_root(&self) -> &Path {
80        &self.content
81    }
82
83    /// The root holding what the running application writes.
84    pub fn writable_root(&self) -> &Path {
85        self.writable.as_deref().unwrap_or(&self.content)
86    }
87
88    /// The state root's `assets/` directory.
89    pub fn assets_dir(&self) -> PathBuf {
90        self.content.join(ASSETS_DIR)
91    }
92
93    /// The state root's `data/` directory.
94    pub fn data_dir(&self) -> PathBuf {
95        self.content.join(DATA_DIR)
96    }
97
98    /// The state root's `worlds/` directory.
99    pub fn worlds_dir(&self) -> PathBuf {
100        self.content.join(WORLDS_DIR)
101    }
102
103    /// Directory holding the runtime save files. Created on first write by the
104    /// running application, never by a build.
105    pub fn saves_dir(&self) -> PathBuf {
106        self.writable_root().join(SAVES_DIR)
107    }
108
109    /// Sandboxed sibling of [`saves_dir`](Self::saves_dir) for preview
110    /// sessions: the save UI keeps working against this directory, the real
111    /// saves are never touched, and the sandbox is wiped at each session start.
112    pub fn preview_saves_dir(&self) -> PathBuf {
113        self.writable_root().join(PREVIEW_SAVES_DIR)
114    }
115
116    /// The mutable settings file, written by the in-engine settings menu and
117    /// never by a build.
118    pub fn settings_path(&self) -> PathBuf {
119        self.writable_root().join(SETTINGS_FILE)
120    }
121
122    /// Directory holding crash reports. Created on first write; capped by the
123    /// writer's retention pruning.
124    pub fn crashes_dir(&self) -> PathBuf {
125        self.writable_root().join(CRASHES_DIR)
126    }
127
128    /// The editor's session store: the per-project state an editor run carries
129    /// between launches, which is state rather than cache.
130    pub fn editor_session_path(&self) -> PathBuf {
131        self.writable_root().join(EDITOR_SESSION_FILE)
132    }
133
134    /// The segment a running application writes: one container holding every
135    /// regenerable artifact it produces for its own later launches, indexed by
136    /// producer and key.
137    ///
138    /// Deletable at any time; whatever is missing is recomputed. The
139    /// application writes this file and no other, so a concurrent build writing
140    /// a segment of its own never shares a file with it.
141    pub fn runtime_cache_path(&self) -> PathBuf {
142        self.cache_root_for_runtime()
143            .join(CACHE_DIR)
144            .join(RUNTIME_CACHE_SEGMENT)
145    }
146
147    /// The runtime segment a bundle ships, read-only. `cn export` warms it with
148    /// the shader binaries a first launch would otherwise compile; because those
149    /// artifacts are backend IR (DXBC / SPIR-V) rather than machine code, one
150    /// warmed at package time is valid on any machine.
151    ///
152    /// Always resolves against the content root, so it stays readable on a
153    /// read-only install. That is also the only thing separating it from
154    /// [`runtime_cache_path`](Self::runtime_cache_path): a bundle the player
155    /// can write to has one segment serving both roles.
156    pub fn bundled_runtime_cache_path(&self) -> PathBuf {
157        self.content.join(CACHE_DIR).join(RUNTIME_CACHE_SEGMENT)
158    }
159
160    /// The segment a build writes: one container holding every payload,
161    /// expansion, and baked thumbnail a cook produced, indexed by producer and
162    /// key.
163    ///
164    /// Deletable at any time; whatever is missing is recompiled.
165    pub fn build_cache_path(&self) -> PathBuf {
166        self.cache_root_for_build()
167            .join(CACHE_DIR)
168            .join(BUILD_CACHE_SEGMENT)
169    }
170
171    // Without a cache root the runtime segment follows what the application
172    // writes, which is what keeps it writable on a read-only install.
173    fn cache_root_for_runtime(&self) -> &Path {
174        self.cache
175            .as_deref()
176            .unwrap_or_else(|| self.writable_root())
177    }
178
179    // Without a cache root the build segment follows the content: a build
180    // writes the `data/` beside it, so a tree it cannot write is a tree it
181    // cannot cook into either.
182    fn cache_root_for_build(&self) -> &Path {
183        self.cache.as_deref().unwrap_or(&self.content)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    // An unsplit tree: one folder, everything under it. The portable install
192    // and the dev checkout.
193    #[test]
194    fn one_root_resolves_the_whole_layout() {
195        let tree = StateTree::at("/flat");
196        let root = Path::new("/flat");
197
198        assert_eq!(tree.content_root(), root);
199        assert_eq!(tree.writable_root(), root);
200        assert_eq!(tree.assets_dir(), root.join("assets"));
201        assert_eq!(tree.data_dir(), root.join("data"));
202        assert_eq!(tree.worlds_dir(), root.join("worlds"));
203        assert_eq!(tree.saves_dir(), root.join("saves"));
204        assert_eq!(tree.preview_saves_dir(), root.join("preview-saves"));
205        assert_eq!(tree.settings_path(), root.join("settings"));
206        assert_eq!(tree.crashes_dir(), root.join("crashes"));
207        assert_eq!(tree.editor_session_path(), root.join("editor"));
208        assert_eq!(tree.runtime_cache_path(), root.join("cache").join("0"));
209        assert_eq!(tree.build_cache_path(), root.join("cache").join("1"));
210        // One file in both runtime roles, which is what makes the bundled tier
211        // vacuous for a bundle the player can write to.
212        assert_eq!(tree.bundled_runtime_cache_path(), tree.runtime_cache_path());
213    }
214
215    // A read-only install: only what the application writes moves. The content
216    // (and the segment a build writes into it) stays put.
217    #[test]
218    fn a_writable_root_moves_only_what_the_application_writes() {
219        let content = Path::new("/opt/MyGame");
220        let writable = Path::new("/home/u/.local/share/MyGame");
221        let tree = StateTree::at(content).with_writable(writable);
222
223        assert_eq!(tree.content_root(), content);
224        assert_eq!(tree.writable_root(), writable);
225        assert_eq!(tree.saves_dir(), writable.join("saves"));
226        assert_eq!(tree.preview_saves_dir(), writable.join("preview-saves"));
227        assert_eq!(tree.settings_path(), writable.join("settings"));
228        assert_eq!(tree.crashes_dir(), writable.join("crashes"));
229        assert_eq!(tree.editor_session_path(), writable.join("editor"));
230        assert_eq!(tree.runtime_cache_path(), writable.join("cache").join("0"));
231
232        assert_eq!(tree.data_dir(), content.join("data"));
233        assert_eq!(tree.assets_dir(), content.join("assets"));
234        assert_eq!(tree.worlds_dir(), content.join("worlds"));
235        assert_eq!(tree.build_cache_path(), content.join("cache").join("1"));
236        // The shipped segment stays with the content, which is what keeps a
237        // read-only install's warmed artifacts readable.
238        assert_eq!(
239            tree.bundled_runtime_cache_path(),
240            content.join("cache").join("0")
241        );
242    }
243
244    // A cache root moves both regenerable segments and nothing else: the point
245    // of the split is a warm cache behind content that is fresh, read-only, or
246    // both.
247    #[test]
248    fn a_cache_root_moves_both_segments_and_nothing_else() {
249        let content = Path::new("/build/content");
250        let cache = Path::new("/var/cache/mygame");
251        let tree = StateTree::at(content).with_cache(cache);
252
253        assert_eq!(tree.runtime_cache_path(), cache.join("cache").join("0"));
254        assert_eq!(tree.build_cache_path(), cache.join("cache").join("1"));
255        // Still the shipped tier's own definition: beside the content.
256        assert_eq!(
257            tree.bundled_runtime_cache_path(),
258            content.join("cache").join("0")
259        );
260        assert_eq!(tree.data_dir(), content.join("data"));
261        assert_eq!(tree.saves_dir(), content.join("saves"));
262    }
263
264    // All three split: the case one knob could never express, and the reason
265    // the cache is a root of its own.
266    #[test]
267    fn all_three_roots_split_independently() {
268        let tree = StateTree::at("/opt/app")
269            .with_writable("/home/u/app")
270            .with_cache("/var/cache/app");
271
272        assert_eq!(tree.data_dir(), Path::new("/opt/app/data"));
273        assert_eq!(tree.saves_dir(), Path::new("/home/u/app/saves"));
274        assert_eq!(
275            tree.runtime_cache_path(),
276            Path::new("/var/cache/app/cache/0")
277        );
278        assert_eq!(tree.build_cache_path(), Path::new("/var/cache/app/cache/1"));
279    }
280
281    // The builders are independent: setting one leaves the others resolving at
282    // their own defaults.
283    #[test]
284    fn builders_do_not_disturb_each_other() {
285        let base = StateTree::at("/root");
286        assert_eq!(
287            base.clone().with_cache("/c").saves_dir(),
288            base.saves_dir(),
289            "a cache root leaves the writable state alone"
290        );
291        assert_eq!(
292            base.clone().with_writable("/w").build_cache_path(),
293            base.build_cache_path(),
294            "a writable root leaves the build segment with the content"
295        );
296    }
297}