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