Skip to main content

modde_games/tools/
proton.rs

1//! Proton / launcher integration exposed as a per-game tool.
2//!
3//! This does not install Proton. It stores game-specific launch integration
4//! preferences that participate in modde's existing env-var, DLL override, and
5//! wrapper collection pipeline.
6
7use 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}