game_manager_lib/sources/
xbox.rs1use crate::errors::AppError;
25use crate::sources::providers::SourceGame;
26use serde::Deserialize;
27use std::fs;
28use std::path::{Path, PathBuf};
29
30#[derive(Debug, Deserialize)]
33#[serde(rename = "Game")]
34struct GameManifest {
35 #[serde(rename = "Identity")]
36 identity: Identity,
37 #[serde(rename = "StoreId", default)]
38 store_id: Option<String>,
39 #[serde(rename = "ShellVisuals", default)]
40 shell_visuals: Option<ShellVisuals>,
41 #[serde(rename = "ExecutableList", default)]
42 executable_list: Option<ExecutableList>,
43}
44
45#[derive(Debug, Deserialize)]
46struct Identity {
47 #[serde(rename = "@Name")]
48 name: String,
49}
50
51#[derive(Debug, Deserialize)]
52struct ShellVisuals {
53 #[serde(rename = "@DefaultDisplayName", default)]
54 default_display_name: Option<String>,
55}
56
57#[derive(Debug, Deserialize, Default)]
58struct ExecutableList {
59 #[serde(rename = "Executable", default)]
60 executables: Vec<Executable>,
61}
62
63#[derive(Debug, Deserialize)]
64struct Executable {
65 #[serde(rename = "@Name")]
66 name: String,
67 #[serde(rename = "@OverrideDisplayName", default)]
68 override_display_name: Option<String>,
69}
70
71pub fn import_installed() -> Result<Vec<SourceGame>, AppError> {
75 let mut games = Vec::new();
76
77 for drive in candidate_drives() {
78 let Some(games_folder_name) = read_gaming_root(&drive.join(".GamingRoot")) else {
79 continue; };
81
82 let games_root = drive.join(&games_folder_name);
83 if !games_root.is_dir() {
84 log::warn!(
85 "'.GamingRoot' aponta para '{games_folder_name}' mas a pasta não existe em {drive:?}"
86 );
87 continue;
88 }
89
90 let Ok(entries) = fs::read_dir(&games_root) else {
91 continue;
92 };
93
94 for entry in entries.flatten() {
95 let path = entry.path();
96 if !path.is_dir() {
97 continue;
98 }
99
100 match parse_game_folder(&path) {
101 Ok(Some(game)) => games.push(game),
102 Ok(None) => {} Err(err) => {
104 log::warn!("Erro ao processar manifesto Xbox em {path:?}: {err}");
105 }
106 }
107 }
108 }
109
110 Ok(games)
111}
112
113#[cfg(target_os = "windows")]
115fn candidate_drives() -> Vec<PathBuf> {
116 (b'A'..=b'Z')
117 .map(|b| PathBuf::from(format!("{}:\\", b as char)))
118 .filter(|p| p.exists())
119 .collect()
120}
121
122#[cfg(not(target_os = "windows"))]
123fn candidate_drives() -> Vec<PathBuf> {
124 Vec::new() }
126
127fn read_gaming_root(path: &Path) -> Option<String> {
130 let bytes = fs::read(path).ok()?;
131
132 if bytes.len() < 8 || &bytes[0..4] != b"RGBX" {
133 return None;
134 }
135
136 let payload = &bytes[8..];
137 let mut units = Vec::with_capacity(payload.len() / 2);
138 let mut i = 0;
139 while i + 1 < payload.len() {
140 let unit = u16::from_le_bytes([payload[i], payload[i + 1]]);
141 if unit == 0 {
142 break;
143 }
144 units.push(unit);
145 i += 2;
146 }
147
148 String::from_utf16(&units).ok().filter(|s| !s.is_empty())
149}
150
151fn parse_game_folder(game_folder: &Path) -> Result<Option<SourceGame>, AppError> {
154 let config_path = game_folder.join("content").join("MicrosoftGame.config");
155 if !config_path.exists() {
156 return Ok(None); }
158
159 let content = fs::read_to_string(&config_path)?;
160 let manifest: GameManifest = quick_xml::de::from_str(&content)
161 .map_err(|e| AppError::ParseError(format!("Falha ao parsear {config_path:?}: {e}")))?;
162
163 let Some(executable_list) = &manifest.executable_list else {
164 return Ok(None); };
166
167 let Some(executable) = executable_list.executables.first() else {
168 return Ok(None); };
170
171 let name = executable
172 .override_display_name
173 .clone()
174 .filter(|s| !s.trim().is_empty())
175 .or_else(|| {
176 manifest
177 .shell_visuals
178 .as_ref()
179 .and_then(|sv| sv.default_display_name.clone())
180 .filter(|s| !s.trim().is_empty())
181 })
182 .unwrap_or_else(|| manifest.identity.name.clone());
183
184 let executable_path = game_folder
187 .join("content")
188 .join(&executable.name)
189 .to_string_lossy()
190 .to_string();
191
192 let platform_game_id = manifest
193 .store_id
194 .clone()
195 .unwrap_or_else(|| manifest.identity.name.clone());
196
197 Ok(Some(SourceGame {
198 platform: "Xbox".to_string(),
199 platform_game_id,
200 name: Some(name),
201 installed: true,
202 executable_path: Some(executable_path),
203 install_path: Some(game_folder.join("content").to_string_lossy().to_string()),
204 playtime_minutes: None,
205 last_played: None,
206 }))
207}