Skip to main content

concinnity_host/store/paths/
mod.rs

1//! Project state root: where the engine's state tree is anchored, and the names
2//! of the directories hanging off it.
3//!
4//! Everything the engine writes for a project lives under one state directory:
5//! the compiled blobs (`data/`), the regenerable cache (`cache/`: `0` for the
6//! running application, `1` for a build, the baked asset thumbnails included),
7//! fetched source assets (`assets/`),
8//! named worlds (`worlds/`), the runtime save files (`saves/`), and the
9//! mutable settings file (`settings`).
10//!
11//! Nothing here has a default. A host installs the state directory via
12//! [`set_state_dir`] before anything reads the tree, and until it does every
13//! path below resolves to `None`. The naming of that directory is the host's
14//! business, not this crate's: the dev CLI hides it inside the project, a
15//! shipped application puts it beside its executable, and an embedder points
16//! it wherever its own layout implies. Reads that cannot proceed without a
17//! state tree report [`CnResult::NoStateRoot`](concinnity_core::result::CnResult);
18//! the caches and the settings file simply do nothing.
19//!
20//! The read-only content of the tree (`data/`) and the runtime-writable state
21//! (`saves/` + `settings`) usually share one root, but a shipped application
22//! installed in a read-only location (Program Files) cannot write beside its
23//! data. Such an application installs a separate writable root via
24//! [`set_writable_state_dir`] so only `saves/` and `settings` relocate to a
25//! per-user directory while `data/` stays beside the executable.
26//!
27//! Resolution touches no files: these functions compute paths. Reading the tree
28//! is `super::source` (finding a source asset) and `super::blob` (the compiled
29//! blob).
30
31use std::path::{Path, PathBuf};
32
33mod root;
34
35pub use root::{
36    clear_state_dir, clear_writable_state_dir, set_state_dir, set_writable_state_dir, state_dir,
37    writable_state_dir,
38};
39
40/// The state root's `assets/` directory.
41pub fn assets_dir() -> Option<PathBuf> {
42    state_dir().map(|d| d.join("assets"))
43}
44
45/// The state root's `data/` directory.
46pub fn data_dir() -> Option<PathBuf> {
47    state_dir().map(|d| d.join("data"))
48}
49
50/// Directory holding the runtime save files (`auto`, `save1` ..). Created on
51/// first write by the running application, never by a build. Resolves under the
52/// writable-state dir, which is the content root unless an application redirected it.
53pub fn saves_dir() -> Option<PathBuf> {
54    writable_state_dir().map(|d| d.join("saves"))
55}
56
57/// Sandboxed sibling of [saves_dir] for preview sessions (see the
58/// `TransientSaves` protocol resource): the save UI keeps working against this
59/// directory, but the real saves are never touched and the sandbox is wiped at
60/// each session start.
61pub fn preview_saves_dir() -> Option<PathBuf> {
62    writable_state_dir().map(|d| d.join("preview-saves"))
63}
64
65/// The mutable settings file (CBOR). Written by the in-engine settings menu,
66/// never by a build. A sibling of `data/` in the common case, or under the
67/// writable-state dir when a read-only install redirected it.
68pub fn settings_path() -> Option<PathBuf> {
69    writable_state_dir().map(|d| d.join("settings"))
70}
71
72/// Directory holding crash reports (and minidumps) written by the crash
73/// reporting machinery. Resolves under the writable-state dir like `saves/`,
74/// since a shipped install's content root may be read-only. Created on first
75/// write; capped by the writer's retention pruning, never by a build.
76pub fn crashes_dir() -> Option<PathBuf> {
77    writable_state_dir().map(|d| d.join("crashes"))
78}
79
80/// The state root's `worlds/` directory.
81pub fn worlds_dir() -> Option<PathBuf> {
82    state_dir().map(|d| d.join("worlds"))
83}
84
85/// The subdirectory a state tree keeps its cache segments in.
86///
87/// Named so a caller that has to reason about the tree's shape -- a test
88/// harness deciding what may survive between runs -- asks this crate rather
89/// than spelling the layout itself.
90pub const CACHE_DIR: &str = "cache";
91
92/// The runtime cache segment inside `state_dir`, for a caller naming a state
93/// tree other than the installed one: `cn export` warms the segment it writes
94/// into a bundle before that bundle is ever launched.
95pub fn runtime_cache_in(state_dir: &Path) -> PathBuf {
96    state_dir.join(CACHE_DIR).join("0")
97}
98
99/// The runtime cache segment, `cache/0`: one container holding every
100/// regenerable artifact the running application produces for its own later
101/// launches, indexed by producer and key. Resolves under the writable-state
102/// dir, since a shipped install's content root may be read-only.
103///
104/// Deletable at any time; whatever is missing is recomputed. The running
105/// application writes this file and no other, so a concurrent build writing a
106/// segment of its own never shares a file with it.
107pub fn runtime_cache_path() -> Option<PathBuf> {
108    writable_state_dir().map(|d| runtime_cache_in(&d))
109}
110
111/// The runtime cache segment a bundle ships, read-only. `cn export` warms it
112/// with the shader binaries a first launch would otherwise compile; because
113/// those artifacts are backend IR (DXBC / SPIR-V) rather than machine code, one
114/// warmed at package time is valid on any machine.
115///
116/// Resolves against the content root, so it stays readable on a read-only
117/// install. That is also the only layout where this differs from
118/// [`runtime_cache_path`]: a bundle the player can write to has one segment
119/// serving both roles.
120pub fn bundled_runtime_cache_path() -> Option<PathBuf> {
121    state_dir().map(|d| runtime_cache_in(&d))
122}
123
124/// The build cache segment, `cache/1`: one container holding every payload,
125/// expansion, and baked thumbnail a cook produced, indexed by producer and
126/// key. A build writes this
127/// file and no other, so a cook running against a live application never
128/// shares a file with the segment that application writes.
129///
130/// Resolves against the content root rather than the writable one: a build
131/// writes the `data/` beside it, so a tree it cannot write is a tree it cannot
132/// cook into either.
133///
134/// Deletable at any time; whatever is missing is recompiled.
135pub fn build_cache_path() -> Option<PathBuf> {
136    state_dir().map(|d| d.join(CACHE_DIR).join("1"))
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    // Exercises the process-global roots end to end. The single test that drives
144    // the globals, so its mutations never race another test that reads them.
145    #[test]
146    fn installed_roots_redirect_every_state_dir() {
147        let flat = Path::new("/tmp/flat-probe");
148        set_state_dir(flat);
149        assert_eq!(state_dir().as_deref(), Some(flat));
150        assert_eq!(data_dir().unwrap(), flat.join("data"));
151        assert_eq!(runtime_cache_path().unwrap(), flat.join("cache").join("0"));
152        assert_eq!(build_cache_path().unwrap(), flat.join("cache").join("1"));
153        assert_eq!(assets_dir().unwrap(), flat.join("assets"));
154        assert_eq!(worlds_dir().unwrap(), flat.join("worlds"));
155        // With no writable override, writable state stays beside the data.
156        assert_eq!(writable_state_dir().as_deref(), Some(flat));
157        assert_eq!(saves_dir().unwrap(), flat.join("saves"));
158        assert_eq!(settings_path().unwrap(), flat.join("settings"));
159        assert_eq!(crashes_dir().unwrap(), flat.join("crashes"));
160
161        // A writable override relocates only the runtime-writable state
162        // (`saves/`, `settings`, `crashes/`); `data/` (and assets/worlds) stay
163        // at the content root, and so does the segment a build writes.
164        let writable = Path::new("/tmp/per-user-probe");
165        set_writable_state_dir(writable);
166        assert_eq!(writable_state_dir().as_deref(), Some(writable));
167        assert_eq!(saves_dir().unwrap(), writable.join("saves"));
168        assert_eq!(settings_path().unwrap(), writable.join("settings"));
169        assert_eq!(crashes_dir().unwrap(), writable.join("crashes"));
170        assert_eq!(
171            runtime_cache_path().unwrap(),
172            writable.join("cache").join("0")
173        );
174        assert_eq!(build_cache_path().unwrap(), flat.join("cache").join("1"));
175        // The bundle's warmed segment stays with the content, which is what
176        // makes a read-only install's shipped artifacts still readable.
177        assert_eq!(
178            bundled_runtime_cache_path().unwrap(),
179            flat.join("cache").join("0")
180        );
181        assert_eq!(data_dir().unwrap(), flat.join("data"));
182        clear_writable_state_dir();
183        assert_eq!(saves_dir().unwrap(), flat.join("saves"));
184
185        // With nothing installed there is no state tree at all: no guess
186        // against the working directory, so a library writes nowhere.
187        clear_state_dir();
188        assert_eq!(state_dir(), None);
189        for path in [
190            data_dir(),
191            assets_dir(),
192            worlds_dir(),
193            saves_dir(),
194            preview_saves_dir(),
195            settings_path(),
196            crashes_dir(),
197            runtime_cache_path(),
198            bundled_runtime_cache_path(),
199            build_cache_path(),
200            writable_state_dir(),
201        ] {
202            assert_eq!(path, None);
203        }
204    }
205}