1use std::path::PathBuf;
8use std::process::Command;
9
10use smallvec::SmallVec;
11
12use super::{GameTool, ToolAvailability, ToolCategory, ToolConfig, ToolGameContext, which};
13
14pub static PROTON: Proton = Proton;
15const GE_PROTON_REPO: &str = "GloriousEggroll/proton-ge-custom";
16
17pub struct Proton;
18
19impl GameTool for Proton {
20 fn tool_id(&self) -> &'static str {
21 "proton"
22 }
23
24 fn display_name(&self) -> &'static str {
25 "Proton"
26 }
27
28 fn category(&self) -> ToolCategory {
29 ToolCategory::Performance
30 }
31
32 fn description(&self) -> &'static str {
33 "Per-game Proton and launcher compatibility settings used by modde launch wrappers."
34 }
35
36 fn settings_schema(&self) -> Vec<super::ToolSettingSpec> {
37 let config = self.default_config();
38 self.settings_schema_for(None, &config)
39 }
40
41 fn settings_schema_for(
42 &self,
43 _context: Option<&ToolGameContext>,
44 _config: &ToolConfig,
45 ) -> Vec<super::ToolSettingSpec> {
46 let mut specs = vec![
47 super::ToolSettingSpec::select(
48 "version_mode",
49 "Version mode",
50 "How modde should choose the Proton runner for this game.",
51 &[
52 "launcher_default",
53 "installed_version",
54 "install_with_protonup_rs",
55 ],
56 ),
57 super::ToolSettingSpec::select(
58 "selected_version",
59 "Proton version",
60 "Installed or requested GEProton version.",
61 &proton_version_options()
62 .iter()
63 .map(String::as_str)
64 .collect::<Vec<_>>(),
65 ),
66 super::ToolSettingSpec::select(
67 "install_target",
68 "Install target",
69 "Target application passed to protonup-rs.",
70 &["steam"],
71 ),
72 super::ToolSettingSpec::read_only(
73 "derived_launcher",
74 "Launcher",
75 "Derived from the selected game.",
76 ),
77 super::ToolSettingSpec::read_only(
78 "derived_steam_app_id",
79 "Steam app id",
80 "Detected from launcher metadata when available.",
81 ),
82 super::ToolSettingSpec::path(
83 "prefix_path_override",
84 "Prefix override",
85 "Optional Proton/Wine prefix override. Leave blank to use launcher detection.",
86 ),
87 super::ToolSettingSpec::text(
88 "extra_env",
89 "Extra environment",
90 "Additional KEY=VALUE lines exported when launching the game.",
91 ),
92 super::ToolSettingSpec::bool("steamdeck", "Steam Deck mode", "Export SteamDeck=1."),
93 super::ToolSettingSpec::bool(
94 "proton_enable_hdr",
95 "Proton HDR",
96 "Export PROTON_ENABLE_HDR=1.",
97 ),
98 super::ToolSettingSpec::bool("enable_hdr_wsi", "HDR WSI", "Export ENABLE_HDR_WSI=1."),
99 super::ToolSettingSpec::bool(
100 "proton_enable_wayland",
101 "Proton Wayland",
102 "Export PROTON_ENABLE_WAYLAND=1.",
103 ),
104 super::ToolSettingSpec::bool("proton_log", "Proton log", "Export PROTON_LOG=1."),
105 super::ToolSettingSpec::bool(
106 "proton_use_sdl",
107 "Proton SDL",
108 "Export PROTON_USE_SDL=1.",
109 ),
110 super::ToolSettingSpec::bool(
111 "radv_perftest_rt",
112 "RADV RT",
113 "Export RADV_PERFTEST=rt,emulate_rt.",
114 ),
115 super::ToolSettingSpec::bool(
116 "proton_hide_nvidia_gpu",
117 "Hide NVIDIA GPU",
118 "Export PROTON_HIDE_NVIDIA_GPU=1.",
119 ),
120 super::ToolSettingSpec::bool(
121 "proton_enable_nvapi",
122 "Enable NVAPI",
123 "Export PROTON_ENABLE_NVAPI=1.",
124 ),
125 super::ToolSettingSpec::bool(
126 "proton_use_wined3d",
127 "Use WINED3D",
128 "Export PROTON_USE_WINED3D=1.",
129 ),
130 super::ToolSettingSpec::bool(
131 "mesa_loader_zink",
132 "Mesa Zink",
133 "Export MESA_LOADER_DRIVER_OVERRIDE=zink.",
134 ),
135 super::ToolSettingSpec::bool(
136 "glx_vendor_mesa",
137 "GLX Mesa",
138 "Export __GLX_VENDOR_LIBRARY_NAME=mesa.",
139 ),
140 super::ToolSettingSpec::bool(
141 "radv_debug_nofastclears",
142 "RADV no fast clears",
143 "Export RADV_DEBUG=nofastclears.",
144 ),
145 super::ToolSettingSpec::bool(
146 "proton_fsr4_upgrade",
147 "FSR4 upgrade",
148 "Export PROTON_FSR4_UPGRADE=1.",
149 ),
150 super::ToolSettingSpec::bool(
151 "proton_dlss_upgrade",
152 "DLSS upgrade",
153 "Export PROTON_DLSS_UPGRADE=1.",
154 ),
155 super::ToolSettingSpec::bool(
156 "proton_xess_upgrade",
157 "XeSS upgrade",
158 "Export PROTON_XESS_UPGRADE=1.",
159 ),
160 super::ToolSettingSpec::bool(
161 "proton_priority_high",
162 "High priority",
163 "Export PROTON_PRIORITY_HIGH=1.",
164 ),
165 super::ToolSettingSpec::bool("proton_use_wow64", "WOW64", "Export PROTON_USE_WOW64=1."),
166 super::ToolSettingSpec::bool(
167 "proton_force_large_address_aware",
168 "Large address aware",
169 "Export PROTON_FORCE_LARGE_ADDRESS_AWARE=1.",
170 ),
171 super::ToolSettingSpec::bool(
172 "staging_shared_memory",
173 "Shared memory",
174 "Export STAGING_SHARED_MEMORY=1.",
175 ),
176 super::ToolSettingSpec::bool(
177 "proton_no_ntsync",
178 "Disable NTSYNC",
179 "Export PROTON_NO_NTSYNC=1.",
180 ),
181 super::ToolSettingSpec::bool(
182 "proton_heap_delay_free",
183 "Heap delay free",
184 "Export PROTON_HEAP_DELAY_FREE=1.",
185 ),
186 super::ToolSettingSpec::bool(
187 "enable_mesa_antilag",
188 "Mesa Anti-Lag",
189 "Export ENABLE_LAYER_MESA_ANTI_LAG=1.",
190 ),
191 super::ToolSettingSpec::select(
192 "dll_override_mode",
193 "DLL override mode",
194 "How Proton should contribute forced DLL overrides.",
195 &["auto", "forced", "off"],
196 ),
197 super::ToolSettingSpec::text(
198 "forced_dll_overrides",
199 "Forced DLL overrides",
200 "Comma or whitespace separated DLL base names, such as dxgi, winmm.",
201 ),
202 super::ToolSettingSpec::select(
203 "wrapper_order",
204 "Wrapper order",
205 "Where Proton-specific wrapper integration should appear in the launch chain.",
206 &["after-modde", "before-tools"],
207 ),
208 ];
209 for spec in &mut specs {
210 spec.section = proton_setting_section(spec.key);
211 }
212 specs
213 }
214
215 fn detect_available(&self) -> ToolAvailability {
216 if let Some(path) = detect_protonup_rs() {
217 let count = installed_ge_proton_versions().len();
218 ToolAvailability::Available {
219 version: Some(format!(
220 "protonup-rs at {}; {count} GEProton install(s)",
221 path.display()
222 )),
223 }
224 } else {
225 ToolAvailability::NotInstalled {
226 install_hint: "Install protonup-rs to manage GEProton versions; launcher/default settings still work.".into(),
227 }
228 }
229 }
230
231 fn env_vars(&self, config: &ToolConfig) -> SmallVec<[(String, String); 4]> {
232 let mut vars = SmallVec::new();
233
234 if let Some(prefix) = config
235 .get_str("prefix_path_override")
236 .or_else(|| config.get_str("prefix_path"))
237 && !prefix.trim().is_empty()
238 {
239 vars.push(("WINEPREFIX".into(), prefix.trim().into()));
240 }
241
242 if let Some(extra) = config.get_str("extra_env") {
243 for line in extra.lines() {
244 let line = line.trim();
245 if line.is_empty() || line.starts_with('#') {
246 continue;
247 }
248 if let Some((key, value)) = line.split_once('=') {
249 let key = key.trim();
250 if !key.is_empty() {
251 vars.push((key.into(), value.trim().into()));
252 }
253 }
254 }
255 }
256
257 for (setting, key, value) in GOVERLAY_ENV_TOGGLES {
258 if config.get_bool(setting) {
259 vars.push(((*key).into(), (*value).into()));
260 }
261 }
262
263 vars
264 }
265
266 fn wine_dll_overrides(&self, config: &ToolConfig) -> SmallVec<[String; 4]> {
267 if config.get_str("dll_override_mode") == Some("off") {
268 return SmallVec::new();
269 }
270 let Some(raw) = config.get_str("forced_dll_overrides") else {
271 return SmallVec::new();
272 };
273
274 raw.split([',', ';', ' ', '\n', '\t'])
275 .filter_map(|part| {
276 let value = part.trim().trim_end_matches(".dll");
277 (!value.is_empty()).then(|| value.to_string())
278 })
279 .collect()
280 }
281
282 fn default_config(&self) -> ToolConfig {
283 let mut config = ToolConfig::new("proton");
284 config.set("version_mode", serde_json::json!("launcher_default"));
285 config.set("selected_version", serde_json::json!("latest"));
286 config.set("install_target", serde_json::json!("steam"));
287 config.set("prefix_path_override", serde_json::json!(""));
288 config.set("extra_env", serde_json::json!(""));
289 for (setting, _, _) in GOVERLAY_ENV_TOGGLES {
290 config.set(*setting, serde_json::json!(false));
291 }
292 config.set("dll_override_mode", serde_json::json!("auto"));
293 config.set("forced_dll_overrides", serde_json::json!(""));
294 config.set("wrapper_order", serde_json::json!("after-modde"));
295 config
296 }
297}
298
299const GOVERLAY_ENV_TOGGLES: &[(&str, &str, &str)] = &[
300 ("steamdeck", "SteamDeck", "1"),
301 ("proton_enable_hdr", "PROTON_ENABLE_HDR", "1"),
302 ("enable_hdr_wsi", "ENABLE_HDR_WSI", "1"),
303 ("proton_enable_wayland", "PROTON_ENABLE_WAYLAND", "1"),
304 ("proton_log", "PROTON_LOG", "1"),
305 ("proton_use_sdl", "PROTON_USE_SDL", "1"),
306 ("radv_perftest_rt", "RADV_PERFTEST", "rt,emulate_rt"),
307 ("proton_hide_nvidia_gpu", "PROTON_HIDE_NVIDIA_GPU", "1"),
308 ("proton_enable_nvapi", "PROTON_ENABLE_NVAPI", "1"),
309 ("proton_use_wined3d", "PROTON_USE_WINED3D", "1"),
310 ("mesa_loader_zink", "MESA_LOADER_DRIVER_OVERRIDE", "zink"),
311 ("glx_vendor_mesa", "__GLX_VENDOR_LIBRARY_NAME", "mesa"),
312 ("radv_debug_nofastclears", "RADV_DEBUG", "nofastclears"),
313 ("proton_fsr4_upgrade", "PROTON_FSR4_UPGRADE", "1"),
314 ("proton_dlss_upgrade", "PROTON_DLSS_UPGRADE", "1"),
315 ("proton_xess_upgrade", "PROTON_XESS_UPGRADE", "1"),
316 ("proton_priority_high", "PROTON_PRIORITY_HIGH", "1"),
317 ("proton_use_wow64", "PROTON_USE_WOW64", "1"),
318 (
319 "proton_force_large_address_aware",
320 "PROTON_FORCE_LARGE_ADDRESS_AWARE",
321 "1",
322 ),
323 ("staging_shared_memory", "STAGING_SHARED_MEMORY", "1"),
324 ("proton_no_ntsync", "PROTON_NO_NTSYNC", "1"),
325 ("proton_heap_delay_free", "PROTON_HEAP_DELAY_FREE", "1"),
326 ("enable_mesa_antilag", "ENABLE_LAYER_MESA_ANTI_LAG", "1"),
327];
328
329fn proton_setting_section(key: &str) -> &'static str {
330 match key {
331 "version_mode" | "selected_version" | "install_target" => "Runner",
332 "derived_launcher" | "derived_steam_app_id" => "Detected Game",
333 "prefix_path_override" | "extra_env" => "Environment",
334 "dll_override_mode" | "forced_dll_overrides" => "DLL Overrides",
335 "wrapper_order" => "Wrapper",
336 _ => "Compatibility Toggles",
337 }
338}
339
340#[must_use]
341pub fn detect_protonup_rs() -> Option<PathBuf> {
342 which("protonup-rs")
343}
344
345#[must_use]
346pub fn compatibilitytools_dirs() -> Vec<PathBuf> {
347 let home = std::env::var_os("HOME").map(PathBuf::from);
348 let mut dirs = Vec::new();
349 if let Some(home) = &home {
350 dirs.push(home.join(".steam/root/compatibilitytools.d"));
351 dirs.push(home.join(".local/share/Steam/compatibilitytools.d"));
352 dirs.push(home.join(".var/app/com.valvesoftware.Steam/data/Steam/compatibilitytools.d"));
353 }
354 dirs.retain(|d| d.is_dir());
355 dirs
356}
357
358#[must_use]
359pub fn installed_ge_proton_versions() -> Vec<String> {
360 let mut versions = Vec::new();
361 for dir in compatibilitytools_dirs() {
362 let Ok(entries) = std::fs::read_dir(dir) else {
363 continue;
364 };
365 for entry in entries.flatten() {
366 let Ok(file_type) = entry.file_type() else {
367 continue;
368 };
369 if !file_type.is_dir() {
370 continue;
371 }
372 let name = entry.file_name().to_string_lossy().to_string();
373 if name.starts_with("GE-Proton") || name.starts_with("Proton-GE") {
374 versions.push(name);
375 }
376 }
377 }
378 versions.sort();
379 versions.dedup();
380 versions
381}
382
383#[must_use]
384pub fn proton_version_options() -> Vec<String> {
385 merge_proton_version_options(std::iter::empty(), installed_ge_proton_versions())
386}
387
388#[must_use]
389pub fn is_ge_proton_version(value: &str) -> bool {
390 let trimmed = value.trim();
391 !trimmed.is_empty() && (trimmed.starts_with("GE-Proton") || trimmed.starts_with("Proton-GE"))
392}
393
394#[must_use]
395pub fn merge_proton_version_options<C, I>(catalog_versions: C, installed_versions: I) -> Vec<String>
396where
397 C: IntoIterator<Item = String>,
398 I: IntoIterator<Item = String>,
399{
400 super::release::prepend_latest_dedup(catalog_versions.into_iter().chain(installed_versions))
401}
402
403pub async fn list_ge_proton_versions() -> anyhow::Result<Vec<String>> {
404 let releases = super::release::list_github_releases(GE_PROTON_REPO).await?;
405 let catalog_versions = releases
406 .into_iter()
407 .map(|release| release.tag)
408 .filter(|tag| is_ge_proton_version(tag))
409 .collect::<Vec<_>>();
410 Ok(merge_proton_version_options(
411 catalog_versions,
412 installed_ge_proton_versions(),
413 ))
414}
415
416#[must_use]
417pub fn protonup_rs_install_args(version: &str, target: &str) -> Vec<String> {
418 vec![
419 "--tool".to_string(),
420 "GEProton".to_string(),
421 "--version".to_string(),
422 version.to_string(),
423 "--for".to_string(),
424 target.to_string(),
425 ]
426}
427
428pub fn install_ge_proton_with_protonup_rs(version: &str, target: &str) -> anyhow::Result<()> {
429 let Some(binary) = detect_protonup_rs() else {
430 anyhow::bail!("protonup-rs is not installed or not on PATH");
431 };
432 let status = Command::new(binary)
433 .args(protonup_rs_install_args(version, target))
434 .status()?;
435 if !status.success() {
436 anyhow::bail!("protonup-rs failed with status {status}");
437 }
438 Ok(())
439}