Skip to main content

roblox_studio_utils/paths/
mod.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::Arc,
4};
5
6use crate::RobloxStudioResult;
7use crate::task::RobloxStudioTask;
8
9#[cfg(target_os = "linux")]
10mod linux;
11#[cfg(target_os = "macos")]
12mod macos;
13#[cfg(target_os = "windows")]
14mod windows;
15
16/**
17    References to discovered, validated paths to the current
18    Roblox Studio executable, content, and plugins directories.
19
20    Can be cheaply cloned and shared between threads.
21*/
22#[derive(Debug, Clone)]
23pub struct RobloxStudioPaths {
24    inner: Arc<RobloxStudioPathsInner>,
25}
26
27impl RobloxStudioPaths {
28    /**
29        Tries to locate the current Roblox Studio installation and directories.
30
31        # Errors
32
33        - If Roblox Studio is not installed.
34    */
35    pub fn new() -> RobloxStudioResult<Self> {
36        RobloxStudioPathsInner::new().map(Self::from)
37    }
38
39    /**
40        Returns the path to the Roblox Studio executable.
41    */
42    #[must_use]
43    pub fn exe(&self) -> &Path {
44        self.inner.exe.as_path()
45    }
46
47    /**
48        Returns the path to the Roblox Studio launcher executable,
49        if one is available.
50    */
51    #[must_use]
52    pub fn launcher(&self) -> Option<&Path> {
53        self.inner.launcher.as_deref()
54    }
55
56    /**
57        Returns the preferred executable path for the given task.
58    */
59    #[must_use]
60    pub(crate) fn exe_for_task(&self, task: Option<RobloxStudioTask>) -> &Path {
61        if cfg!(target_os = "windows")
62            && task.is_some_and(RobloxStudioTask::needs_launcher)
63            && self.inner.launcher.is_some()
64        {
65            self.inner
66                .launcher
67                .as_deref()
68                .expect("launcher path should exist")
69        } else {
70            self.exe()
71        }
72    }
73
74    /**
75        Returns the path to the Roblox Studio content directory.
76
77        This directory contains Roblox bundled assets, in sub-directories such as:
78
79        - `fonts` - bundled font files, typically in OpenType or TrueType format
80        - `sounds` - bundled basic sounds, such as the character reset sound
81        - `textures` - bundled texture files, typically used for `CoreGui`
82    */
83    #[must_use]
84    pub fn content(&self) -> &Path {
85        self.inner.content.as_path()
86    }
87
88    /**
89        Returns the path to the Roblox Studio **user plugins** directory.
90
91        For the path to built-in plugins, see [`RobloxStudioPaths::built_in_plugins`].
92
93        # Warning
94
95        This directory may or may not exist as it is created on demand,
96        either when a user opens it through the Roblox Studio settings,
97        or when they install their first plugin.
98    */
99    #[must_use]
100    pub fn user_plugins(&self) -> &Path {
101        self.inner.plugins_user.as_path()
102    }
103
104    /**
105        Returns the path to the Roblox Studio **built-in plugins** directory.
106
107        These plugins are bundled with Roblox Studio itself, and the directory is guaranteed
108        to exist unlike the user plugins directory ([`RobloxStudioPaths::user_plugins`]).
109    */
110    #[must_use]
111    pub fn built_in_plugins(&self) -> &Path {
112        self.inner.plugins_builtin.as_path()
113    }
114
115    /**
116        Returns the path to the current `GlobalSettings_<version>.xml`, the file Roblox Studio
117        stores its global settings in, if one is present.
118
119        The `<version>` suffix is a settings-schema version that Roblox increments over time (it has
120        been `_4`, `_8`, `_10`, `_13`, ...), so the highest-versioned file is returned rather than
121        assuming a fixed number.
122    */
123    #[must_use]
124    pub fn global_settings(&self) -> Option<PathBuf> {
125        let dir = self.inner.settings.as_ref()?;
126
127        let mut best: Option<(u32, PathBuf)> = None;
128        for entry in std::fs::read_dir(dir).ok()?.flatten() {
129            let file_name = entry.file_name();
130            let Some(name) = file_name.to_str() else {
131                continue;
132            };
133            let Some(version) = name
134                .strip_prefix("GlobalSettings_")
135                .and_then(|rest| rest.strip_suffix(".xml"))
136                .and_then(|version| version.parse::<u32>().ok())
137            else {
138                continue;
139            };
140            if best
141                .as_ref()
142                .is_none_or(|(best_version, _)| version > *best_version)
143            {
144                best = Some((version, entry.path()));
145            }
146        }
147
148        best.map(|(_, path)| path)
149    }
150}
151
152// Private inner struct to make RobloxStudioPaths cheaper to clone
153#[derive(Debug, Clone)]
154struct RobloxStudioPathsInner {
155    exe: PathBuf,
156    launcher: Option<PathBuf>,
157    content: PathBuf,
158    plugins_user: PathBuf,
159    plugins_builtin: PathBuf,
160    settings: Option<PathBuf>,
161}
162
163impl From<RobloxStudioPathsInner> for RobloxStudioPaths {
164    fn from(inner: RobloxStudioPathsInner) -> Self {
165        Self {
166            inner: Arc::new(inner),
167        }
168    }
169}