modde-games 0.1.0

Game plugin implementations for modde
Documentation
//! Per-game tool/overlay management.
//!
//! Each tool (MangoHud, vkBasalt, GameMode, ReShade, OptiScaler) implements the
//! [`GameTool`] trait. Tools are registered via [`all_tools`] and resolved by ID
//! via [`resolve_tool`], following the same pattern as [`crate::resolve_game_plugin`].

pub mod gamemode;
pub mod mangohud;
pub mod optiscaler;
pub mod reshade;
pub mod vkbasalt;

use std::path::{Path, PathBuf};

use anyhow::Result;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;

// ── Types ──────────────────────────────────────────────────────────────────

/// Category of gaming tool.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCategory {
    /// Performance HUD overlay (MangoHud).
    Overlay,
    /// Post-processing / shader injection (vkBasalt, ReShade).
    PostProcess,
    /// System performance tuning (GameMode).
    Performance,
    /// Upscaling / frame generation (OptiScaler).
    Upscaler,
}

impl std::fmt::Display for ToolCategory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Overlay => write!(f, "Overlay"),
            Self::PostProcess => write!(f, "Post-Processing"),
            Self::Performance => write!(f, "Performance"),
            Self::Upscaler => write!(f, "Upscaler"),
        }
    }
}

/// Whether a tool is available on the system.
#[derive(Debug, Clone)]
pub enum ToolAvailability {
    Available { version: Option<String> },
    NotInstalled { install_hint: String },
}

impl ToolAvailability {
    pub fn is_available(&self) -> bool {
        matches!(self, Self::Available { .. })
    }
}

/// A wrapper binary to chain before the game executable.
#[derive(Debug, Clone)]
pub struct WrapperEntry {
    pub exe: String,
    pub args: String,
}

/// Files applied by a tool to a game directory (for revert tracking).
#[derive(Debug, Clone, Default)]
pub struct AppliedFiles {
    /// Paths relative to the game directory.
    pub files: Vec<PathBuf>,
}

/// A config file generated by a tool.
#[derive(Debug, Clone)]
pub struct GeneratedConfig {
    /// Absolute path where the config should be written.
    pub path: PathBuf,
    /// File content.
    pub content: String,
}

/// Per-game tool configuration stored in the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolConfig {
    pub tool_id: String,
    pub enabled: bool,
    pub settings: serde_json::Value,
}

impl ToolConfig {
    pub fn new(tool_id: impl Into<String>) -> Self {
        Self {
            tool_id: tool_id.into(),
            enabled: false,
            settings: serde_json::Value::Object(serde_json::Map::new()),
        }
    }

    /// Get a string setting.
    pub fn get_str(&self, key: &str) -> Option<&str> {
        self.settings.get(key).and_then(|v| v.as_str())
    }

    /// Get a bool setting, defaulting to `false`.
    pub fn get_bool(&self, key: &str) -> bool {
        self.settings
            .get(key)
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
    }

    /// Get an integer setting.
    pub fn get_i64(&self, key: &str) -> Option<i64> {
        self.settings.get(key).and_then(|v| v.as_i64())
    }

    /// Set a setting value.
    pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
        if let serde_json::Value::Object(ref mut map) = self.settings {
            map.insert(key.into(), value);
        }
    }
}

// ── Trait ───────────────────────────────────────────────────────────────────

/// A gaming tool/overlay that can be managed per-game.
pub trait GameTool: Send + Sync {
    /// Unique identifier (e.g. `"mangohud"`, `"gamemode"`).
    fn tool_id(&self) -> &'static str;

    /// Human-readable display name.
    fn display_name(&self) -> &'static str;

    /// Tool category.
    fn category(&self) -> ToolCategory;

    /// Check if the tool is installed on the system.
    fn detect_available(&self) -> ToolAvailability;

    /// Environment variables to set when launching the game.
    fn env_vars(&self, config: &ToolConfig) -> SmallVec<[(String, String); 4]>;

    /// Wrapper command to chain before the game (e.g. `gamemoderun`).
    fn wrapper_command(&self, _config: &ToolConfig) -> Option<WrapperEntry> {
        None
    }

    /// Wine DLL overrides needed (e.g. `"dxgi"`, `"version"`).
    fn wine_dll_overrides(&self, _config: &ToolConfig) -> SmallVec<[String; 4]> {
        SmallVec::new()
    }

    /// Apply/install files into the game directory (DLLs, shaders, etc.).
    /// Returns a manifest of files written for revert tracking.
    fn apply(&self, _game_dir: &Path, _config: &ToolConfig) -> Result<AppliedFiles> {
        Ok(AppliedFiles::default())
    }

    /// Revert files previously applied by [`apply`].
    fn revert(&self, game_dir: &Path, applied: &AppliedFiles) -> Result<()> {
        for rel in &applied.files {
            let path = game_dir.join(rel);
            if path.exists() {
                std::fs::remove_file(&path)?;
            }
        }
        Ok(())
    }

    /// Generate a per-game config file (e.g. MangoHud.conf).
    fn generate_config(&self, _config: &ToolConfig) -> Option<GeneratedConfig> {
        None
    }

    /// Default configuration for a fresh enable.
    fn default_config(&self) -> ToolConfig;
}

// ── Registry ───────────────────────────────────────────────────────────────

/// All registered tools.
static ALL_TOOLS: [&dyn GameTool; 5] = [
    &mangohud::MANGOHUD,
    &vkbasalt::VKBASALT,
    &gamemode::GAMEMODE,
    &reshade::RESHADE,
    &optiscaler::OPTISCALER,
];

pub fn all_tools() -> &'static [&'static dyn GameTool] {
    &ALL_TOOLS
}

/// Resolve a tool by its ID string.
pub fn resolve_tool(tool_id: &str) -> Option<&'static dyn GameTool> {
    all_tools().iter().find(|t| t.tool_id() == tool_id).copied()
}

// ── Helpers ────────────────────────────────────────────────────────────────

/// Directory where modde stores per-game tool configs.
pub fn tool_config_dir(game_id: &str) -> PathBuf {
    modde_core::paths::modde_data_dir().join("tools").join(game_id)
}

/// Check if a binary is on `$PATH`.
///
/// Uses the `which` crate for cross-platform support (handles Windows
/// `%PATHEXT%` extensions like `.exe`, `.cmd`, `.bat` automatically).
pub(crate) fn which(binary: &str) -> Option<PathBuf> {
    which::which(binary).ok()
}