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 payload cache (`cache/`), fetched source
6//! assets (`assets/`), named worlds (`worlds/`), the runtime save files
7//! (`saves/`), and the mutable settings file (`settings`).
8//!
9//! Nothing here has a default. A host installs the state directory via
10//! [`set_state_dir`] before anything reads the tree, and until it does every
11//! path below resolves to `None`. The naming of that directory is the host's
12//! business, not this crate's: the dev CLI hides it inside the project, a
13//! shipped application puts it beside its executable, and an embedder points
14//! it wherever its own layout implies. Reads that cannot proceed without a
15//! state tree report [`CnResult::NoStateRoot`](concinnity_core::result::CnResult);
16//! the caches and the settings file simply do nothing.
17//!
18//! The read-only content of the tree (`data/`) and the runtime-writable state
19//! (`saves/` + `settings`) usually share one root, but a shipped application
20//! installed in a read-only location (Program Files) cannot write beside its
21//! data. Such an application installs a separate writable root via
22//! [`set_writable_state_dir`] so only `saves/` and `settings` relocate to a
23//! per-user directory while `data/` stays beside the executable.
24//!
25//! Resolution touches no files: these functions compute paths. Reading the tree
26//! is `super::source` (finding a source asset) and `super::blob` (the compiled
27//! blob).
28
29use std::path::PathBuf;
30
31mod root;
32
33pub use root::{
34    clear_state_dir, clear_writable_state_dir, set_state_dir, set_writable_state_dir, state_dir,
35    writable_state_dir,
36};
37
38/// The state root's `assets/` directory.
39pub fn assets_dir() -> Option<PathBuf> {
40    state_dir().map(|d| d.join("assets"))
41}
42
43/// The state root's `data/` directory.
44pub fn data_dir() -> Option<PathBuf> {
45    state_dir().map(|d| d.join("data"))
46}
47
48/// Directory holding the runtime save files (`auto`, `save1` ..). Created on
49/// first write by the running application, never by a build. Resolves under the
50/// writable-state dir, which is the content root unless an application redirected it.
51pub fn saves_dir() -> Option<PathBuf> {
52    writable_state_dir().map(|d| d.join("saves"))
53}
54
55/// Sandboxed sibling of [saves_dir] for preview sessions (see the
56/// `TransientSaves` protocol resource): the save UI keeps working against this
57/// directory, but the real saves are never touched and the sandbox is wiped at
58/// each session start.
59pub fn preview_saves_dir() -> Option<PathBuf> {
60    writable_state_dir().map(|d| d.join("preview-saves"))
61}
62
63/// The mutable settings file (CBOR). Written by the in-engine settings menu,
64/// never by a build. A sibling of `data/` in the common case, or under the
65/// writable-state dir when a read-only install redirected it.
66pub fn settings_path() -> Option<PathBuf> {
67    writable_state_dir().map(|d| d.join("settings"))
68}
69
70/// Directory holding crash reports (and minidumps) written by the crash
71/// reporting machinery. Resolves under the writable-state dir like `saves/`,
72/// since a shipped install's content root may be read-only. Created on first
73/// write; capped by the writer's retention pruning, never by a build.
74pub fn crashes_dir() -> Option<PathBuf> {
75    writable_state_dir().map(|d| d.join("crashes"))
76}
77
78/// The state root's `worlds/` directory.
79pub fn worlds_dir() -> Option<PathBuf> {
80    state_dir().map(|d| d.join("worlds"))
81}
82
83/// The state root's `cache/` directory.
84pub fn cache_dir() -> Option<PathBuf> {
85    state_dir().map(|d| d.join("cache"))
86}
87
88/// Directory holding baked asset thumbnails: content-addressed `<sha256>.png`
89/// files plus an `index.json` mapping asset names to keys. Deterministic
90/// products of the build like [`cache_dir`]'s payloads, but kept apart so they
91/// can be listed and cleared independently (and never ship: `cn export` copies
92/// neither).
93pub fn thumbnails_dir() -> Option<PathBuf> {
94    state_dir().map(|d| d.join("thumbnails"))
95}
96
97/// Directory the renderer writes compiled built-in shader binaries to, keyed by
98/// a hash of their compile inputs. Resolves under the writable-state dir, since
99/// a shipped install's content root may be read-only. Distinct from
100/// [`cache_dir`], which holds cooked asset payloads: these artifacts belong to
101/// the machine's shader compiler, not to the build.
102pub fn shader_cache_dir() -> Option<PathBuf> {
103    writable_state_dir().map(|d| d.join("shader-cache"))
104}
105
106/// Directory the renderer persists driver pipeline blobs to (a serialized
107/// VkPipelineCache, a D3D12 pipeline library), keyed per adapter. Unlike
108/// [`shader_cache_dir`] artifacts these are machine code tied to one GPU and
109/// driver, so they resolve under the writable-state dir only and never ship in
110/// a bundle. A sibling of `shader-cache/` rather than a subdirectory, since the
111/// shader cache prunes its directory by age and would reclaim these.
112pub fn pipeline_cache_dir() -> Option<PathBuf> {
113    writable_state_dir().map(|d| d.join("pipeline-cache"))
114}
115
116/// Directory holding shader binaries shipped inside a bundle, read-only. `cn
117/// export` warms this so a player's first launch does not pay the compile;
118/// because the artifacts are backend IR (DXBC / SPIR-V) rather than machine
119/// code, one warmed at package time is valid on any machine.
120///
121/// Equal to [`shader_cache_dir`] whenever the content root is writable (the
122/// portable-folder case). The two diverge only for a read-only install, which
123/// redirects writable state to a per-user directory: the bundled artifacts then
124/// stay readable here while new ones land in the writable dir.
125pub fn bundled_shader_cache_dir() -> Option<PathBuf> {
126    state_dir().map(|d| d.join("shader-cache"))
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use std::path::Path;
133
134    // Exercises the process-global roots end to end. The single test that drives
135    // the globals, so its mutations never race another test that reads them.
136    #[test]
137    fn installed_roots_redirect_every_state_dir() {
138        let flat = Path::new("/tmp/flat-probe");
139        set_state_dir(flat);
140        assert_eq!(state_dir().as_deref(), Some(flat));
141        assert_eq!(data_dir().unwrap(), flat.join("data"));
142        assert_eq!(cache_dir().unwrap(), flat.join("cache"));
143        assert_eq!(assets_dir().unwrap(), flat.join("assets"));
144        assert_eq!(worlds_dir().unwrap(), flat.join("worlds"));
145        // With no writable override, writable state stays beside the data.
146        assert_eq!(writable_state_dir().as_deref(), Some(flat));
147        assert_eq!(saves_dir().unwrap(), flat.join("saves"));
148        assert_eq!(settings_path().unwrap(), flat.join("settings"));
149        assert_eq!(crashes_dir().unwrap(), flat.join("crashes"));
150
151        // A writable override relocates only the runtime-writable state
152        // (`saves/`, `settings`, `crashes/`); `data/` (and cache/assets/worlds)
153        // stay at the content root.
154        let writable = Path::new("/tmp/per-user-probe");
155        set_writable_state_dir(writable);
156        assert_eq!(writable_state_dir().as_deref(), Some(writable));
157        assert_eq!(saves_dir().unwrap(), writable.join("saves"));
158        assert_eq!(settings_path().unwrap(), writable.join("settings"));
159        assert_eq!(crashes_dir().unwrap(), writable.join("crashes"));
160        assert_eq!(shader_cache_dir().unwrap(), writable.join("shader-cache"));
161        // The bundled shader cache stays with the content, which is what makes
162        // a read-only install's warmed artifacts still readable.
163        assert_eq!(
164            bundled_shader_cache_dir().unwrap(),
165            flat.join("shader-cache")
166        );
167        assert_eq!(data_dir().unwrap(), flat.join("data"));
168        clear_writable_state_dir();
169        assert_eq!(saves_dir().unwrap(), flat.join("saves"));
170
171        // With nothing installed there is no state tree at all: no guess
172        // against the working directory, so a library writes nowhere.
173        clear_state_dir();
174        assert_eq!(state_dir(), None);
175        for path in [
176            data_dir(),
177            cache_dir(),
178            assets_dir(),
179            worlds_dir(),
180            saves_dir(),
181            preview_saves_dir(),
182            settings_path(),
183            crashes_dir(),
184            thumbnails_dir(),
185            shader_cache_dir(),
186            pipeline_cache_dir(),
187            bundled_shader_cache_dir(),
188            writable_state_dir(),
189        ] {
190            assert_eq!(path, None);
191        }
192    }
193}