1use eframe::egui;
2use egui_extras::install_image_loaders;
3
4use crate::config_manager::ConfigManager;
5use crate::launcher_config::LauncherConfig;
6use crate::news::{NewsManager, NewsStatus};
7use crate::pages::{
8 ModsPageState, MusicPageState, ReplaysPageState, RidersPageState, SnowboardsPageState,
9 TrainerPageState, TroubleshootPageState, trainer, troubleshoot,
10};
11use crate::state::{Page, UnifiedOperation};
12use crate::tasks::TaskHandle;
13use crate::updater::{UpdateStatus, Updater};
14
15mod logic;
16mod ui;
17
18pub use crate::utils::get_game_path;
19
20pub struct LauncherApp {
21 pub current_page: Page,
22 pub storage: supreme_fs::GameStorage,
23
24 pub config_manager: ConfigManager,
26
27 pub error_message: Option<String>,
29
30 pub unified_operation: UnifiedOperation,
32 pub dependency_receiver: Option<TaskHandle<()>>,
33 pub troubleshoot_receiver: Option<TaskHandle<troubleshoot::TroubleshootData>>,
34 pub trainer_receiver: Option<TaskHandle<trainer::TrainerData>>,
35
36 pub troubleshoot_state: TroubleshootPageState,
38 pub trainer_state: TrainerPageState,
39 pub replays_state: ReplaysPageState,
40 pub music_state: MusicPageState,
41 pub snowboards_state: SnowboardsPageState,
42 pub riders_state: RidersPageState,
43 pub mods_state: ModsPageState,
44
45 pub updater: Updater,
47 pub update_logs: UpdateLogs,
48 pub last_status: UpdateStatus,
49
50 pub has_focused_play: bool,
52
53 pub is_supreme_initiator: bool,
55
56 pub launcher_config: LauncherConfig,
58 pub news_manager: NewsManager,
59 pub markdown_cache: egui_commonmark::CommonMarkCache,
60 pub show_news: bool,
61 pub current_article_index: usize,
62}
63
64#[derive(Default, serde::Serialize)]
65pub struct UpdateLogs {
66 pub check: Option<String>,
67 pub download: Option<String>,
68 pub patch: Option<String>,
69}
70
71#[cfg(windows)]
89#[must_use]
90fn parent_and_current_dirs_equal() -> Option<bool> {
91 use std::ffi::OsString;
92 use std::mem;
93 use std::os::windows::ffi::OsStringExt;
94 use std::path::Path;
95 use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
96 use windows::Win32::System::Diagnostics::ToolHelp::{
97 CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
98 TH32CS_SNAPPROCESS,
99 };
100 use windows::Win32::System::Threading::{
101 GetCurrentProcessId, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
102 };
103 use windows::core::PWSTR;
104
105 let my_pid = unsafe { GetCurrentProcessId() };
107
108 let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?;
110 if snapshot == INVALID_HANDLE_VALUE {
111 return None;
112 }
113
114 let mut entry: PROCESSENTRY32W = unsafe { mem::zeroed() };
117 entry.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
118
119 let mut parent_pid: Option<u32> = None;
123 if unsafe { Process32FirstW(snapshot, &mut entry) }.is_ok() {
124 loop {
125 if entry.th32ProcessID == my_pid {
126 parent_pid = Some(entry.th32ParentProcessID);
127 break;
128 }
129 if unsafe { Process32NextW(snapshot, &mut entry) }.is_err() {
130 break;
131 }
132 }
133 }
134
135 let current_exe = std::env::current_exe().ok();
137 let current_dir = current_exe
138 .as_ref()
139 .and_then(|p| p.parent().map(|d| d.to_path_buf()));
140
141 if current_dir.is_none() || parent_pid.is_none() {
143 unsafe {
145 if let Err(e) = CloseHandle(snapshot) {
146 eprintln!("Failed to close process snapshot handle: {e}");
147 }
148 }
149 return None;
150 }
151
152 if let Some(ppid) = parent_pid {
155 let proc_handle: HANDLE =
158 match unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, ppid) } {
159 Ok(h) => h,
160 Err(_) => {
161 unsafe {
163 if let Err(e) = CloseHandle(snapshot) {
164 eprintln!("Failed to close process snapshot handle: {e}");
165 }
166 }
167 return None;
168 }
169 };
170
171 let mut buf: Vec<u16> = vec![0u16; 32768];
174 let mut size: u32 = buf.len() as u32;
175
176 let qres = unsafe {
179 use windows::Win32::System::Threading::{
180 PROCESS_NAME_FORMAT, QueryFullProcessImageNameW,
181 };
182
183 QueryFullProcessImageNameW(
184 proc_handle,
185 PROCESS_NAME_FORMAT(0), PWSTR(buf.as_mut_ptr()),
187 &mut size,
188 )
189 };
190
191 let parent_dir_opt = if qres.is_ok() {
192 buf.truncate(size as usize);
196 let os = OsString::from_wide(&buf);
197 Path::new(&os).parent().map(|p| p.to_path_buf())
199 } else {
200 None
201 };
202
203 unsafe {
205 if let Err(e) = CloseHandle(proc_handle) {
206 eprintln!("Failed to close process handle: {e}");
207 }
208 if let Err(e) = CloseHandle(snapshot) {
209 eprintln!("Failed to close process snapshot handle: {e}");
210 }
211 }
212
213 let parent_dir = parent_dir_opt?;
215 let current_dir = current_dir?;
216
217 let canonical_parent = std::fs::canonicalize(&parent_dir).ok();
220 let canonical_current = std::fs::canonicalize(¤t_dir).ok();
221
222 let equal = match (canonical_parent, canonical_current) {
224 (Some(cp), Some(cc)) => {
225 let a = cp.to_string_lossy().to_lowercase();
226 let b = cc.to_string_lossy().to_lowercase();
227 a == b
228 }
229 _ => {
230 let a = parent_dir.to_string_lossy().to_lowercase();
231 let b = current_dir.to_string_lossy().to_lowercase();
232 a == b
233 }
234 };
235
236 Some(equal)
237 } else {
238 unsafe {
240 if let Err(e) = CloseHandle(snapshot) {
241 eprintln!("Failed to close process snapshot handle: {e}");
242 }
243 }
244 None
245 }
246}
247
248#[cfg(not(windows))]
249fn parent_and_current_dirs_equal() -> Option<bool> {
250 None
251}
252
253impl LauncherApp {
254 #[must_use]
255 pub fn new(cc: &eframe::CreationContext<'_>, launcher_config: LauncherConfig) -> Self {
256 crate::theme::install(&cc.egui_ctx);
259 for theme in [egui::Theme::Dark, egui::Theme::Light] {
260 let mut style = (*cc.egui_ctx.style_of(theme)).clone();
261 style.spacing.item_spacing = egui::vec2(8.0, 8.0);
262 cc.egui_ctx.set_style_of(theme, style);
263 }
264
265 let storage = crate::utils::create_storage();
266 let game_path = storage.game_path().to_path_buf();
267
268 let is_supreme_initiator = parent_and_current_dirs_equal().unwrap_or(false);
270
271 let game_exe_path = std::fs::read_dir(&game_path)
273 .ok()
274 .into_iter()
275 .flatten()
276 .flatten()
277 .find(|entry| {
278 let filename = entry.file_name().to_string_lossy().to_lowercase();
279 filename.contains("supreme") && filename.contains("exe")
280 })
281 .map(|entry| entry.path());
282
283 let mut spawn_error = None;
284 if !is_supreme_initiator && let Some(game_exe) = &game_exe_path {
287 match std::process::Command::new(game_exe)
288 .current_dir(&game_path)
289 .spawn()
290 {
291 Ok(mut child) => {
292 std::thread::sleep(std::time::Duration::from_millis(200));
294 match child.try_wait() {
295 Ok(None) => {
296 std::process::exit(0);
298 }
299 Ok(Some(status)) => {
300 if status.success() {
301 std::process::exit(0);
303 } else {
304 spawn_error =
305 Some(format!("Supreme.exe failed to start: {status}"));
306 }
307 }
308 Err(e) => {
309 spawn_error = Some(format!("Failed to verify Supreme.exe status: {e}"));
310 }
311 }
312 }
313 Err(e) => {
314 spawn_error = Some(format!("Failed to auto-launch Supreme.exe: {e}"));
315 }
316 }
317 }
318
319 let config_manager = ConfigManager::load(&storage);
320
321 let mut updater = Updater::new();
323 if !std::env::var("SSCL_DISABLE_UPDATES").is_ok_and(|v| &v == "1") {
324 updater.check_for_updates();
325 }
326
327 let mut news_manager = NewsManager::new(&game_path, &launcher_config);
328 if news_manager.is_check_due(&launcher_config) {
329 news_manager.start_check(&launcher_config.news.last_read);
330 }
331
332 Self {
333 current_page: Page::Display,
334 storage,
335 config_manager,
336 error_message: spawn_error,
337 unified_operation: UnifiedOperation::default(),
338 dependency_receiver: None,
339 troubleshoot_receiver: None,
340 trainer_receiver: None,
341 troubleshoot_state: TroubleshootPageState::default(),
342 trainer_state: TrainerPageState::default(),
343 replays_state: ReplaysPageState::default(),
344 music_state: MusicPageState::default(),
345 snowboards_state: SnowboardsPageState::default(),
346 riders_state: RidersPageState::default(),
347 mods_state: ModsPageState::default(),
348 updater,
349 update_logs: UpdateLogs::default(),
350 last_status: UpdateStatus::Idle,
351 has_focused_play: false,
352 is_supreme_initiator,
353 launcher_config,
354 news_manager,
355 markdown_cache: egui_commonmark::CommonMarkCache::default(),
356 show_news: false,
357 current_article_index: 0,
358 }
359 }
360}
361
362impl eframe::App for LauncherApp {
363 fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
364 self.save_all_configs();
366 }
367
368 fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
369 let update_status = self.updater.poll().unwrap_or(self.last_status.clone());
370
371 if let Some(status) = self.news_manager.poll() {
372 if let NewsStatus::Success {
373 last_news_check, ..
374 } = status
375 {
376 self.launcher_config.news.last_check = Some(last_news_check);
377 if let Err(e) = self.launcher_config.save() {
378 eprintln!("Failed to save launcher config: {e}");
379 }
380 }
381 ui.request_repaint();
382 }
383
384 install_image_loaders(ui);
385
386 if self.show_news {
387 self.show_news_window(ui);
388 }
389
390 if std::mem::discriminant(&update_status) != std::mem::discriminant(&self.last_status) {
392 match &update_status {
393 UpdateStatus::UpdateAvailable(info) => {
394 self.update_logs.check = Some(format!("Update found: {}", info.tag_name));
395
396 let current_exe = std::env::current_exe().ok();
398 if let Some(exe) = current_exe
399 && let Err(e) = std::process::Command::new(exe)
400 .arg("--self-update")
401 .stdin(std::process::Stdio::null())
402 .stdout(std::process::Stdio::null())
403 .stderr(std::process::Stdio::null())
404 .spawn()
405 {
406 eprintln!("Failed to spawn self-update process: {e}");
407 }
408 }
409 UpdateStatus::NoUpdateAvailable => {
410 self.update_logs.check = Some("No update available.".to_string());
411 }
412 UpdateStatus::ReadyToApply(path) => {
413 self.update_logs.download =
414 Some(format!("Update downloaded and ready at {}", path.display()));
415
416 match Updater::apply_update(path) {
418 Ok(()) => {
419 self.update_logs.patch =
420 Some(format!("SUCCESS: Update applied from {}", path.display()));
421 }
422 Err(e) => {
423 self.update_logs.patch = Some(format!(
424 "ERROR: Failed to apply update from {}: {e}",
425 path.display()
426 ));
427 }
428 }
429
430 if let Err(e) = self.storage.write_report("update_log", &self.update_logs) {
431 eprintln!("Failed to write update_log report: {e}");
432 }
433 }
434 UpdateStatus::Error(e) => {
435 if self.update_logs.check.is_none() {
436 self.update_logs.check = Some(format!("Error: {e}"));
437 } else {
438 self.update_logs.download = Some(format!("Error: {e}"));
439 }
440 }
441 _ => {}
442 }
443 self.last_status = update_status.clone();
444 }
445
446 self.update_unified_operations();
447
448 let sheet = crate::theme::palette(ui.ctx());
454 let visuals = ui.visuals().clone();
455 let light = ui.ctx().theme() != egui::Theme::Dark;
456 let (rail_fill, content_fill) = if light {
457 (sheet.bg, sheet.card)
458 } else {
459 (sheet.card, sheet.bg)
460 };
461 egui::Panel::left("sidebar")
462 .resizable(false)
463 .min_size(180.0)
464 .frame(
465 egui::Frame::new()
466 .fill(rail_fill)
467 .stroke(visuals.widgets.inactive.bg_stroke)
468 .inner_margin(egui::Margin::same(10)),
469 )
470 .show(ui, |ui| {
471 self.show_sidebar(ui, &update_status);
472 self.show_footer(ui);
473 });
474
475 self.render_operation_progress(ui);
477
478 egui::CentralPanel::default()
480 .frame(egui::Frame::new().inner_margin(8).fill(content_fill))
481 .show(ui, |ui| {
482 egui::ScrollArea::vertical()
483 .auto_shrink([false, false])
484 .stick_to_bottom(true)
485 .show(ui, |ui| {
486 self.render_page_content(ui);
487 });
488 });
489 }
490}