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}