Skip to main content

modde_games/tools/
mod.rs

1//! Per-game tool/overlay management.
2//!
3//! Each tool (MangoHud, vkBasalt, GameMode, ReShade, OptiScaler) implements the
4//! [`GameTool`] trait. Tools are registered via [`all_tools`] and resolved by ID
5//! via [`resolve_tool`], following the same pattern as [`crate::resolve_game_plugin`].
6
7pub mod gamemode;
8pub mod mangohud;
9pub mod optiscaler;
10pub mod reshade;
11pub mod vkbasalt;
12
13use std::path::{Path, PathBuf};
14
15use anyhow::Result;
16use serde::{Deserialize, Serialize};
17use smallvec::SmallVec;
18
19// ── Types ──────────────────────────────────────────────────────────────────
20
21/// Category of gaming tool.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ToolCategory {
25    /// Performance HUD overlay (MangoHud).
26    Overlay,
27    /// Post-processing / shader injection (vkBasalt, ReShade).
28    PostProcess,
29    /// System performance tuning (GameMode).
30    Performance,
31    /// Upscaling / frame generation (OptiScaler).
32    Upscaler,
33}
34
35impl std::fmt::Display for ToolCategory {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            Self::Overlay => write!(f, "Overlay"),
39            Self::PostProcess => write!(f, "Post-Processing"),
40            Self::Performance => write!(f, "Performance"),
41            Self::Upscaler => write!(f, "Upscaler"),
42        }
43    }
44}
45
46/// Whether a tool is available on the system.
47#[derive(Debug, Clone)]
48pub enum ToolAvailability {
49    Available { version: Option<String> },
50    NotInstalled { install_hint: String },
51}
52
53impl ToolAvailability {
54    pub fn is_available(&self) -> bool {
55        matches!(self, Self::Available { .. })
56    }
57}
58
59/// A wrapper binary to chain before the game executable.
60#[derive(Debug, Clone)]
61pub struct WrapperEntry {
62    pub exe: String,
63    pub args: String,
64}
65
66/// Files applied by a tool to a game directory (for revert tracking).
67#[derive(Debug, Clone, Default)]
68pub struct AppliedFiles {
69    /// Paths relative to the game directory.
70    pub files: Vec<PathBuf>,
71}
72
73/// A config file generated by a tool.
74#[derive(Debug, Clone)]
75pub struct GeneratedConfig {
76    /// Absolute path where the config should be written.
77    pub path: PathBuf,
78    /// File content.
79    pub content: String,
80}
81
82/// Per-game tool configuration stored in the database.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ToolConfig {
85    pub tool_id: String,
86    pub enabled: bool,
87    pub settings: serde_json::Value,
88}
89
90impl ToolConfig {
91    pub fn new(tool_id: impl Into<String>) -> Self {
92        Self {
93            tool_id: tool_id.into(),
94            enabled: false,
95            settings: serde_json::Value::Object(serde_json::Map::new()),
96        }
97    }
98
99    /// Get a string setting.
100    pub fn get_str(&self, key: &str) -> Option<&str> {
101        self.settings.get(key).and_then(|v| v.as_str())
102    }
103
104    /// Get a bool setting, defaulting to `false`.
105    pub fn get_bool(&self, key: &str) -> bool {
106        self.settings
107            .get(key)
108            .and_then(|v| v.as_bool())
109            .unwrap_or(false)
110    }
111
112    /// Get an integer setting.
113    pub fn get_i64(&self, key: &str) -> Option<i64> {
114        self.settings.get(key).and_then(|v| v.as_i64())
115    }
116
117    /// Set a setting value.
118    pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
119        if let serde_json::Value::Object(ref mut map) = self.settings {
120            map.insert(key.into(), value);
121        }
122    }
123}
124
125// ── Trait ───────────────────────────────────────────────────────────────────
126
127/// A gaming tool/overlay that can be managed per-game.
128pub trait GameTool: Send + Sync {
129    /// Unique identifier (e.g. `"mangohud"`, `"gamemode"`).
130    fn tool_id(&self) -> &'static str;
131
132    /// Human-readable display name.
133    fn display_name(&self) -> &'static str;
134
135    /// Tool category.
136    fn category(&self) -> ToolCategory;
137
138    /// Check if the tool is installed on the system.
139    fn detect_available(&self) -> ToolAvailability;
140
141    /// Environment variables to set when launching the game.
142    fn env_vars(&self, config: &ToolConfig) -> SmallVec<[(String, String); 4]>;
143
144    /// Wrapper command to chain before the game (e.g. `gamemoderun`).
145    fn wrapper_command(&self, _config: &ToolConfig) -> Option<WrapperEntry> {
146        None
147    }
148
149    /// Wine DLL overrides needed (e.g. `"dxgi"`, `"version"`).
150    fn wine_dll_overrides(&self, _config: &ToolConfig) -> SmallVec<[String; 4]> {
151        SmallVec::new()
152    }
153
154    /// Apply/install files into the game directory (DLLs, shaders, etc.).
155    /// Returns a manifest of files written for revert tracking.
156    fn apply(&self, _game_dir: &Path, _config: &ToolConfig) -> Result<AppliedFiles> {
157        Ok(AppliedFiles::default())
158    }
159
160    /// Revert files previously applied by [`apply`].
161    fn revert(&self, game_dir: &Path, applied: &AppliedFiles) -> Result<()> {
162        for rel in &applied.files {
163            let path = game_dir.join(rel);
164            if path.exists() {
165                std::fs::remove_file(&path)?;
166            }
167        }
168        Ok(())
169    }
170
171    /// Generate a per-game config file (e.g. MangoHud.conf).
172    fn generate_config(&self, _config: &ToolConfig) -> Option<GeneratedConfig> {
173        None
174    }
175
176    /// Default configuration for a fresh enable.
177    fn default_config(&self) -> ToolConfig;
178}
179
180// ── Registry ───────────────────────────────────────────────────────────────
181
182/// All registered tools.
183static ALL_TOOLS: [&dyn GameTool; 5] = [
184    &mangohud::MANGOHUD,
185    &vkbasalt::VKBASALT,
186    &gamemode::GAMEMODE,
187    &reshade::RESHADE,
188    &optiscaler::OPTISCALER,
189];
190
191pub fn all_tools() -> &'static [&'static dyn GameTool] {
192    &ALL_TOOLS
193}
194
195/// Resolve a tool by its ID string.
196pub fn resolve_tool(tool_id: &str) -> Option<&'static dyn GameTool> {
197    all_tools().iter().find(|t| t.tool_id() == tool_id).copied()
198}
199
200// ── Helpers ────────────────────────────────────────────────────────────────
201
202/// Directory where modde stores per-game tool configs.
203pub fn tool_config_dir(game_id: &str) -> PathBuf {
204    modde_core::paths::modde_data_dir().join("tools").join(game_id)
205}
206
207/// Check if a binary is on `$PATH`.
208///
209/// Uses the `which` crate for cross-platform support (handles Windows
210/// `%PATHEXT%` extensions like `.exe`, `.cmd`, `.bat` automatically).
211pub(crate) fn which(binary: &str) -> Option<PathBuf> {
212    which::which(binary).ok()
213}