Skip to main content

lighty_core/
app_state.rs

1//! Process-wide launcher paths resolved via the `dirs` crate.
2
3use std::path::{Path, PathBuf};
4
5use once_cell::sync::OnceCell;
6
7use crate::errors::{AppStateError, AppStateResult};
8
9const CLIENT_ID_FILE: &str = "client_id";
10
11/// Resolved per-launcher paths.
12#[derive(Debug, Clone)]
13pub struct LauncherPaths {
14    pub name: String,
15    pub data_dir: PathBuf,
16    pub config_dir: PathBuf,
17    pub cache_dir: PathBuf,
18}
19
20static PATHS: OnceCell<LauncherPaths> = OnceCell::new();
21static CLIENT_ID: OnceCell<String> = OnceCell::new();
22
23/// Zero-sized handle for the global launcher paths.
24pub struct AppState;
25
26impl AppState {
27    /// Initialises the global launcher paths. Call once at startup.
28    ///
29    /// `name` becomes the per-launcher subdirectory under the OS-
30    /// standard data/config/cache bases. Returns
31    /// [`AppStateError::AlreadyInitialized`] on a second call.
32    pub fn init(name: impl Into<String>) -> AppStateResult<()> {
33        let name = name.into();
34        let data_dir = dirs::data_dir()
35            .ok_or(AppStateError::MissingPlatformDir("data"))?
36            .join(&name);
37        let config_dir = dirs::config_dir()
38            .ok_or(AppStateError::MissingPlatformDir("config"))?
39            .join(&name);
40        let cache_dir = dirs::cache_dir()
41            .ok_or(AppStateError::MissingPlatformDir("cache"))?
42            .join(&name);
43        PATHS
44            .set(LauncherPaths { name, data_dir, config_dir, cache_dir })
45            .map_err(|_| AppStateError::AlreadyInitialized)
46    }
47
48    /// Returns the resolved launcher paths.
49    ///
50    /// Panics with a clear message if [`Self::init`] hasn't been
51    /// called — that's a programmer error, not a runtime condition.
52    pub fn paths() -> &'static LauncherPaths {
53        PATHS.get().expect(
54            "AppState::init(\"<launcher-name>\") must be called once at startup",
55        )
56    }
57
58    /// Launcher name as supplied to [`Self::init`].
59    pub fn name() -> &'static str {
60        &Self::paths().name
61    }
62
63    /// Persistent data directory (instances live here).
64    pub fn data_dir() -> &'static Path {
65        &Self::paths().data_dir
66    }
67
68    /// User configuration directory (the bundled JRE lives here).
69    pub fn config_dir() -> &'static Path {
70        &Self::paths().config_dir
71    }
72
73    /// Disposable cache directory.
74    pub fn cache_dir() -> &'static Path {
75        &Self::paths().cache_dir
76    }
77
78    /// Application version derived from `CARGO_PKG_VERSION`.
79    pub fn app_version() -> &'static str {
80        env!("CARGO_PKG_VERSION")
81    }
82
83    /// Per-install launcher client id, surfaced to the JVM as `${clientid}`.
84    ///
85    /// Persisted at `<config_dir>/client_id` so crash reports and Mojang
86    /// telemetry stay correlated across sessions. On first call we read it
87    /// from disk; on a missing/unreadable file we mint a fresh UUID v4
88    /// (RFC 4122) and write it back. Subsequent calls in the same process
89    /// hit the in-memory cache.
90    pub fn client_id() -> &'static str {
91        CLIENT_ID.get_or_init(|| {
92            let path = Self::config_dir().join(CLIENT_ID_FILE);
93
94            // Trim avoids trailing \n from manual edits or POSIX conventions.
95            if let Ok(raw) = std::fs::read_to_string(&path) {
96                let trimmed = raw.trim();
97                if !trimmed.is_empty() {
98                    return trimmed.to_string();
99                }
100            }
101
102            let fresh = generate_uuid_v4();
103
104            // Best-effort write: if config dir is unwritable we still return
105            // a valid id so launches go through; next run will regenerate.
106            if let Some(parent) = path.parent() {
107                let _ = std::fs::create_dir_all(parent);
108            }
109            if let Err(e) = std::fs::write(&path, &fresh) {
110                crate::trace_debug!(
111                    error = %e,
112                    path = %path.display(),
113                    "Could not persist client_id; continuing with in-memory value"
114                );
115            }
116
117            fresh
118        })
119    }
120}
121
122/// Generates a RFC 4122 v4 UUID string from `fastrand`.
123fn generate_uuid_v4() -> String {
124    let mut bytes = [0u8; 16];
125    for b in bytes.iter_mut() {
126        *b = fastrand::u8(..);
127    }
128    // Version 4 (random): high nibble of byte 6 = 0b0100
129    bytes[6] = (bytes[6] & 0x0f) | 0x40;
130    // Variant RFC 4122: top two bits of byte 8 = 0b10
131    bytes[8] = (bytes[8] & 0x3f) | 0x80;
132
133    format!(
134        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
135        bytes[0], bytes[1], bytes[2], bytes[3],
136        bytes[4], bytes[5],
137        bytes[6], bytes[7],
138        bytes[8], bytes[9],
139        bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
140    )
141}