aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/game_finder/mod.rs
blob: 600751a7f95ffed782531ea4b5eeee123ae07012 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//! Game detection module
//!
//! Provides unified game detection across multiple launchers:
//! - Steam (native, Flatpak, Snap)
//! - Heroic (GOG, Epic)
//! - Bottles

// Allow unused items - this is a public API module
#![allow(dead_code)]
#![allow(unused_imports)]

mod bottles;
mod heroic;
pub mod known_games;
mod registry;
mod steam;
mod vdf;

use std::path::PathBuf;

pub use bottles::detect_bottles_games;
pub use heroic::detect_heroic_games;
pub use known_games::{
    find_by_epic_id, find_by_gog_id, find_by_name, find_by_steam_id, find_by_title, KnownGame,
    KNOWN_GAMES,
};
pub use registry::{read_registry_value, wine_path_to_linux};
pub use steam::{detect_steam_games, find_game_install_path, find_game_prefix_path, get_known_game};

// ============================================================================
// Core Types
// ============================================================================

/// The launcher/store a game was installed from
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Launcher {
    Steam { is_flatpak: bool, is_snap: bool },
    Heroic { store: HeroicStore },
    Bottles,
}

impl Launcher {
    pub fn display_name(&self) -> &'static str {
        match self {
            Launcher::Steam { is_flatpak: true, .. } => "Steam (Flatpak)",
            Launcher::Steam { is_snap: true, .. } => "Steam (Snap)",
            Launcher::Steam { .. } => "Steam",
            Launcher::Heroic { store: HeroicStore::GOG } => "Heroic (GOG)",
            Launcher::Heroic { store: HeroicStore::Epic } => "Heroic (Epic)",
            Launcher::Bottles => "Bottles",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeroicStore {
    GOG,
    Epic,
}

/// A detected game installation
#[derive(Debug, Clone)]
pub struct Game {
    pub name: String,
    pub app_id: String,
    pub install_path: PathBuf,
    pub prefix_path: Option<PathBuf>,
    pub launcher: Launcher,
    pub my_games_folder: Option<String>,
    pub appdata_local_folder: Option<String>,
    pub appdata_roaming_folder: Option<String>,
    pub registry_path: Option<String>,
    pub registry_value: Option<String>,
}

impl Game {
    pub fn has_prefix(&self) -> bool {
        self.prefix_path.is_some()
    }

    pub fn get_prefix_user_path(&self) -> Option<PathBuf> {
        let prefix = self.prefix_path.as_ref()?;
        let users_dir = prefix.join("drive_c/users");

        if let Ok(entries) = std::fs::read_dir(&users_dir) {
            for entry in entries.flatten() {
                let name = entry.file_name().to_string_lossy().to_string();
                if name != "Public" && name != "root" {
                    return Some(users_dir.join(name));
                }
            }
        }

        Some(users_dir.join("steamuser"))
    }

    pub fn get_prefix_documents_path(&self) -> Option<PathBuf> {
        self.get_prefix_user_path().map(|p| p.join("Documents"))
    }

    pub fn get_prefix_my_games_path(&self) -> Option<PathBuf> {
        let docs = self.get_prefix_documents_path()?;
        let folder = self.my_games_folder.as_ref()?;
        Some(docs.join("My Games").join(folder))
    }

    pub fn get_prefix_appdata_local_path(&self) -> Option<PathBuf> {
        let user = self.get_prefix_user_path()?;
        let folder = self.appdata_local_folder.as_ref()?;
        Some(user.join("AppData/Local").join(folder))
    }

    pub fn get_prefix_appdata_roaming_path(&self) -> Option<PathBuf> {
        let user = self.get_prefix_user_path()?;
        let folder = self.appdata_roaming_folder.as_ref()?;
        Some(user.join("AppData/Roaming").join(folder))
    }
}

// ============================================================================
// Scan Results
// ============================================================================

#[derive(Debug, Default)]
pub struct GameScanResult {
    pub games: Vec<Game>,
    pub steam_count: usize,
    pub heroic_count: usize,
    pub bottles_count: usize,
}

impl GameScanResult {
    pub fn games_with_prefixes(&self) -> impl Iterator<Item = &Game> {
        self.games.iter().filter(|g| g.has_prefix())
    }

    pub fn games_by_launcher(&self, launcher_type: &str) -> Vec<&Game> {
        self.games
            .iter()
            .filter(|g| {
                matches!(
                    (&g.launcher, launcher_type),
                    (Launcher::Steam { .. }, "steam")
                        | (Launcher::Heroic { .. }, "heroic")
                        | (Launcher::Bottles, "bottles")
                )
            })
            .collect()
    }

    pub fn find_by_name(&self, name: &str) -> Option<&Game> {
        let name_lower = name.to_lowercase();
        self.games
            .iter()
            .find(|g| g.name.to_lowercase() == name_lower)
    }

    pub fn find_by_app_id(&self, app_id: &str) -> Option<&Game> {
        self.games.iter().find(|g| g.app_id == app_id)
    }
}

// ============================================================================
// Public API
// ============================================================================

/// Detect all installed games from all supported launchers.
///
/// Games are detected in priority order: Steam -> Heroic (GOG -> Epic) -> Bottles.
/// When the same game is found from multiple launchers, the higher-priority
/// detection is kept and duplicates are skipped. This ensures registry entries
/// (which are shared across storefronts) prefer Steam paths, then GOG, then Epic.
pub fn detect_all_games() -> GameScanResult {
    let mut result = GameScanResult::default();

    // Detect in priority order: Steam first, then GOG (via Heroic), then Epic, then Bottles.
    let steam_games = detect_steam_games();
    result.steam_count = steam_games.len();
    result.games.extend(steam_games);

    let heroic_games = detect_heroic_games();
    result.heroic_count = heroic_games.len();
    result.games.extend(heroic_games);

    let bottles_games = detect_bottles_games();
    result.bottles_count = bottles_games.len();
    result.games.extend(bottles_games);

    // Deduplicate: keep the first occurrence of each game (by registry_path),
    // which respects the detection order (Steam > GOG > Epic > Bottles).
    deduplicate_games(&mut result);

    result
}

/// Remove duplicate game detections, keeping the first (highest-priority) entry.
/// Two games are considered duplicates if they have the same registry_path.
fn deduplicate_games(result: &mut GameScanResult) {
    let mut seen_registry_paths = std::collections::HashSet::new();
    result.games.retain(|game| {
        if let Some(ref reg_path) = game.registry_path {
            seen_registry_paths.insert(reg_path.clone())
        } else {
            // Games without registry paths are always kept (no risk of conflict)
            true
        }
    });
}

/// Detect only Steam games
pub fn detect_steam_only() -> GameScanResult {
    let steam_games = detect_steam_games();
    GameScanResult {
        steam_count: steam_games.len(),
        games: steam_games,
        ..Default::default()
    }
}