Skip to main content

concinnity_host/store/paths/
root.rs

1// Anchoring: which directory the state tree hangs off.
2//
3// There is no default. A host installs a state directory before anything reads
4// the tree, and an uninstalled root resolves to `None` rather than a guess
5// against the working directory: a library that guessed would scatter a
6// project's settings and saves beside whatever directory its caller happened
7// to launch from.
8
9use std::path::{Path, PathBuf};
10use std::sync::{Mutex, OnceLock};
11
12fn installed_state_dir() -> &'static Mutex<Option<PathBuf>> {
13    static STATE: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
14    STATE.get_or_init(|| Mutex::new(None))
15}
16
17fn writable_state_override() -> &'static Mutex<Option<PathBuf>> {
18    static WRITABLE: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
19    WRITABLE.get_or_init(|| Mutex::new(None))
20}
21
22/// Anchor the state tree at `dir` for the rest of the process, so `data/`,
23/// `saves/`, and `settings` resolve under it. Every host installs one before
24/// reading project state: the dev CLI its project directory, a shipped
25/// application the directory beside its executable (or inside its app bundle),
26/// an embedder whatever its own layout implies.
27pub fn set_state_dir<P: Into<PathBuf>>(dir: P) {
28    *installed_state_dir().lock().unwrap() = Some(dir.into());
29}
30
31/// Remove an installed state dir, leaving the process with no state tree.
32pub fn clear_state_dir() {
33    *installed_state_dir().lock().unwrap() = None;
34}
35
36/// Anchor the runtime-writable state (`saves/` + `settings`) at `dir`, leaving
37/// the read-only content (`data/`) at the installed state dir. A shipped
38/// application installs this when its content dir is not writable (a read-only
39/// install such as Program Files), redirecting only what it writes at runtime
40/// to a per-user directory. When unset, writable state stays beside `data/`.
41pub fn set_writable_state_dir<P: Into<PathBuf>>(dir: P) {
42    *writable_state_override().lock().unwrap() = Some(dir.into());
43}
44
45/// Remove an installed writable-state dir, restoring writable state to the
46/// content root beside `data/`.
47pub fn clear_writable_state_dir() {
48    *writable_state_override().lock().unwrap() = None;
49}
50
51/// The state directory, or `None` when no host installed one.
52pub fn state_dir() -> Option<PathBuf> {
53    installed_state_dir().lock().unwrap().clone()
54}
55
56/// The directory holding runtime-writable state (`saves/` + `settings`): the
57/// writable override when one is installed, otherwise the state dir (writable
58/// state sits beside `data/`).
59pub fn writable_state_dir() -> Option<PathBuf> {
60    let over = writable_state_override().lock().unwrap().clone();
61    resolve_writable_dir(over.as_deref(), state_dir().as_deref())
62}
63
64// Pure resolution split out so the fallback rule is unit-testable without the
65// process-global override: the override verbatim, else the content state dir.
66fn resolve_writable_dir(over: Option<&Path>, state: Option<&Path>) -> Option<PathBuf> {
67    over.or(state).map(Path::to_path_buf)
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn resolve_writable_dir_prefers_override_then_falls_back() {
76        // An installed writable override wins verbatim, relocating only the
77        // writable state; without one, writable state stays with the content.
78        let state = Path::new("/game/MyGame");
79        let over = Path::new("/users/me/AppData/Local/MyGame");
80        assert_eq!(
81            resolve_writable_dir(Some(over), Some(state)).as_deref(),
82            Some(over)
83        );
84        assert_eq!(
85            resolve_writable_dir(None, Some(state)).as_deref(),
86            Some(state)
87        );
88    }
89
90    // An override with no content root behind it still resolves: a host may
91    // redirect its writable state without ever installing a state dir.
92    #[test]
93    fn resolve_writable_dir_without_a_state_dir() {
94        let over = Path::new("/users/me/MyGame");
95        assert_eq!(
96            resolve_writable_dir(Some(over), None).as_deref(),
97            Some(over)
98        );
99        assert_eq!(resolve_writable_dir(None, None), None);
100    }
101}