Skip to main content

game_manager_lib/utils/
launcher.rs

1use crate::models::{Game, Platform};
2use std::env;
3use std::path::PathBuf;
4use tracing::warn;
5
6#[derive(Debug, Clone)]
7pub enum LaunchResolution {
8    Protocol(String),   // steam://, battlenet:// — protocolos confiáveis e documentados
9    Executable(String), // Executa o jogo diretamente (Epic, GOG, etc)
10    Launcher(String),   //  Fallback: abre o launcher da plataforma
11    Store(String),      // Launcher não encontrado no disco — abre a página da loja
12    Unavailable,
13}
14
15pub struct PlatformFallback {
16    pub launcher_candidates: &'static [&'static str],
17    pub store_url: &'static str,
18}
19
20pub fn platform_fallback(platform: &Platform) -> PlatformFallback {
21    match platform {
22        Platform::Amazon => PlatformFallback {
23            launcher_candidates: &[r"%LOCALAPPDATA%\Amazon Games\App\Amazon Games.exe"],
24            store_url: "",
25        },
26        Platform::BattleNet => PlatformFallback {
27            launcher_candidates: &[
28                r"C:\Program Files (x86)\Battle.net\Battle.net.exe",
29                r"C:\Program Files (x86)\Battle.net\Battle.net Launcher.exe",
30            ],
31            store_url: "https://battle.net/",
32        },
33        Platform::EA => PlatformFallback {
34            launcher_candidates: &[
35                r"C:\Program Files\Electronic Arts\EA Desktop\EA Desktop\EADesktop.exe",
36                r"C:\Program Files\Electronic Arts\EA Desktop\EA Desktop\EALauncher.exe",
37            ],
38            store_url: "https://www.ea.com/",
39        },
40        Platform::Epic => PlatformFallback {
41            launcher_candidates: &[
42                r"C:\Program Files (x86)\Epic Games\Launcher\Portal\Binaries\Win32\EpicGamesLauncher.exe",
43                r"C:\Program Files (x86)\Epic Games\Launcher\Portal\Binaries\Win64\EpicGamesLauncher.exe",
44            ],
45            store_url: "https://store.epicgames.com/",
46        },
47        Platform::GOG => PlatformFallback {
48            launcher_candidates: &[r"C:\Program Files (x86)\GOG Galaxy\GalaxyClient.exe"],
49            store_url: "https://www.gog.com/",
50        },
51        Platform::Heroic => PlatformFallback {
52            launcher_candidates: &[
53                r"C:\Program Files\Heroic\Heroic.exe",
54                r"C:\Users\%USERNAME%\AppData\Local\Programs\heroic\Heroic.exe",
55            ],
56            store_url: "",
57        },
58        Platform::Indiegala => PlatformFallback {
59            launcher_candidates: &[r"C:\Program Files (x86)\IGClient\IGClient.exe"],
60            store_url: "https://www.indiegala.com/store",
61        },
62        Platform::Itch => PlatformFallback {
63            launcher_candidates: &[
64                r"C:\Users\%USERNAME%\AppData\Local\itch\itch.exe",
65                r"C:\Program Files (x86)\itch\itch.exe",
66            ],
67            store_url: "https://itch.io/",
68        },
69        Platform::LegacyGames => PlatformFallback {
70            launcher_candidates: &[
71                r"C:\Program Files (x86)\Legacy Games\Legacy Games Launcher\Legacy Games Launcher.exe",
72            ],
73            store_url: "https://legacygames.com/",
74        },
75        Platform::Steam => PlatformFallback {
76            launcher_candidates: &[r"C:\Program Files (x86)\Steam\Steam.exe"],
77            store_url: "https://store.steampowered.com/",
78        },
79        Platform::Ubisoft => PlatformFallback {
80            launcher_candidates: &[
81                r"C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\UbisoftConnect.exe",
82                r"C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\UbisoftGameLauncher.exe",
83            ],
84            store_url: "https://store.ubisoft.com/",
85        },
86        // Demais plataformas (Indie, Outra) sem launcher próprio, ou scan de pastas com jogos.
87        _ => PlatformFallback {
88            launcher_candidates: &[],
89            store_url: "",
90        },
91    }
92}
93
94/// `override_path`: valor cru vindo do localStorage via frontend (mesmo padrão de
95/// `gog_games_dir`/`ea_install_dir`). Ignorado se o caminho não existir mais no disco.
96pub fn resolve_launcher_path(platform: &Platform, override_path: Option<&str>) -> Option<PathBuf> {
97    // 1. Path customizado pelo usuário, se configurado e ainda existir
98    if let Some(custom) = override_path.filter(|s| !s.trim().is_empty()) {
99        let path = PathBuf::from(custom);
100        if path.exists() {
101            return Some(path);
102        }
103        warn!("Launcher path configurado para {platform:?} não existe mais: {path:?}");
104    }
105
106    // 2. Fallback: tenta os caminhos padrão conhecidos
107    platform_fallback(platform)
108        .launcher_candidates
109        .iter()
110        .filter_map(|template| expand_path_template(template))
111        .find(|p| p.exists())
112}
113
114/// Plataformas cujo executável, mesmo quando localizado, não é confiável o suficiente pra rodar direto,
115/// dependem do runtime/DRM/overlay do próprio launcher pra funcionar (anti-cheat, licenciamento, etc).
116/// Sempre abre o launcher da plataforma em vez de tentar o `.exe` diretamente.
117fn is_launcher_only(platform: &Platform) -> bool {
118    matches!(platform, Platform::EA | Platform::Ubisoft)
119}
120
121/// Plataformas cujo protocolo funciona mesmo com o jogo não instalado — o próprio client resolve a
122/// instalação sozinho quando necessário. Documentado oficialmente pela Valve: rungameid "instala se
123/// necessário" antes de rodar. Só Steam tem essa garantia confirmada;
124fn protocol_handles_install(platform: &Platform) -> bool {
125    matches!(platform, Platform::Steam)
126}
127
128/// Protocolos confiáveis por plataforma. Epic/GOG/EA/Amazon não têm protocolo documentado — usam executable_path.
129fn protocol_url_for(game: &Game) -> Option<String> {
130    match game.platform {
131        Platform::Steam => Some(format!("steam://rungameid/{}", game.platform_game_id)),
132        Platform::BattleNet => Some(format!("battlenet://{}", game.platform_game_id)),
133        _ => None,
134    }
135}
136
137/// Decide como iniciar um jogo, na seguinte ordem de prioridade:
138/// 1. Instalado + protocolo confiável (Steam, Battle.net)
139/// 2. Instalado + executável resolvido (Amazon, Epic, GOG, Itch.io, Xbox)
140/// 3. Launcher da plataforma encontrado no disco (EA/Ubisoft; demais quando não instalado, ou sem executável resolvido)
141/// 4. Site da loja (launcher não encontrado)
142pub fn resolve_launch(game: &Game, launcher_path_override: Option<&str>) -> LaunchResolution {
143    let protocol_bypasses_install_check =
144        protocol_handles_install(&game.platform) && protocol_url_for(game).is_some();
145
146    if (game.installed || protocol_bypasses_install_check) && !is_launcher_only(&game.platform) {
147        if let Some(url) = protocol_url_for(game) {
148            return LaunchResolution::Protocol(url);
149        }
150        if let Some(exec) = &game.executable_path {
151            return LaunchResolution::Executable(exec.clone());
152        }
153    }
154
155    if let Some(launcher_path) = resolve_launcher_path(&game.platform, launcher_path_override) {
156        return LaunchResolution::Launcher(launcher_path.to_string_lossy().to_string());
157    }
158
159    let store_url = platform_fallback(&game.platform).store_url;
160    if store_url.is_empty() {
161        return LaunchResolution::Unavailable;
162    }
163
164    LaunchResolution::Store(store_url.to_string())
165}
166
167/// Expande `%LOCALAPPDATA%` e `%USERNAME%` em templates de caminho
168fn expand_path_template(template: &str) -> Option<PathBuf> {
169    let mut resolved = template.to_string();
170
171    if resolved.contains("%LOCALAPPDATA%") {
172        let local_app_data = env::var("LOCALAPPDATA").ok()?;
173        resolved = resolved.replace("%LOCALAPPDATA%", &local_app_data);
174    }
175
176    if resolved.contains("%USERNAME%") {
177        let username = env::var("USERNAME").ok()?;
178        resolved = resolved.replace("%USERNAME%", &username);
179    }
180
181    Some(PathBuf::from(resolved))
182}