From 366b6fbca7a5ad7e763a8de8e05eb9b949485040 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 20 Mar 2025 23:02:09 +0100 Subject: [PATCH] [client] no cmd window when running games or scripts --- .../src-tauri/src/lib.rs | 974 +++++++++--------- 1 file changed, 487 insertions(+), 487 deletions(-) diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index 5558811..eca6f7b 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -1,487 +1,487 @@ -use std::{ - collections::HashSet, - net::SocketAddr, - path::{Path, PathBuf}, - sync::Arc, -}; - -use eyre::bail; -use lanspread_client::{ClientCommand, ClientEvent}; -use lanspread_db::db::{Game, GameDB}; -use lanspread_mdns::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service}; -use tauri::{AppHandle, Emitter as _, Manager}; -use tauri_plugin_shell::{ShellExt, process::Command}; -use tokio::sync::{Mutex, mpsc::UnboundedSender}; - -// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ - -struct LanSpreadState { - server_addr: Mutex>, - client_ctrl: UnboundedSender, - games: Arc>, - games_in_download: Arc>>, - games_folder: Arc>, -} - -#[tauri::command] -fn request_games(state: tauri::State) { - log::debug!("request_games"); - - if let Err(e) = state.inner().client_ctrl.send(ClientCommand::ListGames) { - log::error!("Failed to send message to client: {e:?}"); - } -} - -#[tauri::command] -fn install_game(id: String, state: tauri::State) -> bool { - let already_in_download = tauri::async_runtime::block_on(async { - if state.inner().games_in_download.lock().await.contains(&id) { - log::warn!("Game is already downloading: {id}"); - return true; - } - false - }); - - if already_in_download { - return false; - } - - if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) { - log::error!("Failed to send message to client: {e:?}"); - } - - true -} - -#[cfg(target_os = "windows")] -fn run_as_admin(file: &str, params: &str, dir: &str) -> bool { - use std::{ffi::OsStr, os::windows::ffi::OsStrExt}; - - use windows::{Win32::UI::Shell::ShellExecuteW, core::PCWSTR}; - - let file_wide: Vec = OsStr::new(file).encode_wide().chain(Some(0)).collect(); - let params_wide: Vec = OsStr::new(params).encode_wide().chain(Some(0)).collect(); - let dir_wide: Vec = OsStr::new(dir).encode_wide().chain(Some(0)).collect(); - let runas_wide: Vec = OsStr::new("runas").encode_wide().chain(Some(0)).collect(); - - let result = unsafe { - ShellExecuteW( - None, - PCWSTR::from_raw(runas_wide.as_ptr()), - PCWSTR::from_raw(file_wide.as_ptr()), - PCWSTR::from_raw(params_wide.as_ptr()), - PCWSTR::from_raw(dir_wide.as_ptr()), - windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL, - ) - }; - - (result.0 as usize) > 32 // Success if greater than 32 -} - -#[cfg(target_os = "windows")] -fn run_game_windows(id: String, state: tauri::State) { - use std::fs::File; - - const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done"; - - let games_folder = - tauri::async_runtime::block_on(async { state.inner().games_folder.lock().await.clone() }); - - let games_folder = PathBuf::from(games_folder); - if !games_folder.exists() { - log::error!("games_folder {games_folder:?} does not exist"); - return; - } - - let game_path = games_folder.join(id.clone()); - - let game_setup_bin = game_path.join("game_setup.cmd"); - let game_start_bin = game_path.join("game_start.cmd"); - - let first_start_done_file = game_path.join(FIRST_START_DONE_FILE); - if !first_start_done_file.exists() && game_setup_bin.exists() { - let result = run_as_admin( - "cmd.exe", - &format!( - r#"/c "{} local {} de playername""#, - game_setup_bin.display(), - &id - ), - &game_path.display().to_string(), - ); - - if !result { - log::error!("failed to run game_setup.cmd"); - return; - } - - if let Err(e) = File::create(&first_start_done_file) { - log::error!("failed to create {first_start_done_file:?}: {e}"); - } - } - - if game_start_bin.exists() { - let result = run_as_admin( - "cmd.exe", - &format!( - r#"/c "{} local {} de playername""#, - game_start_bin.display(), - &id - ), - &game_path.display().to_string(), - ); - - if !result { - log::error!("failed to run game_start.cmd"); - } - } -} - -#[tauri::command] -fn run_game(id: String, state: tauri::State) { - #[cfg(target_os = "windows")] - run_game_windows(id, state); - - #[cfg(not(target_os = "windows"))] - log::error!("run_game not implemented for this platform: id={id}, state={state:?}"); -} - -fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed: bool) { - if let Some(file_name) = path.file_name() { - if let Some(file_name) = file_name.to_str() { - if let Some(game) = game_db.get_mut_game_by_id(file_name) { - if installed { - log::debug!("Game is installed: {game}"); - } else { - log::error!("Game is missing: {game}"); - } - game.installed = installed; - } - } - } -} - -#[tauri::command] -fn update_game_directory(app_handle: tauri::AppHandle, path: String) { - log::info!("update_game_directory: {path}"); - - app_handle - .state::() - .client_ctrl - .send(ClientCommand::SetGameDir(path.clone())) - .unwrap(); - - { - tauri::async_runtime::block_on(async { - let mut games_folder = app_handle - .state::() - .inner() - .games_folder - .lock() - .await; - - *games_folder = path.clone(); - }); - } - - let path = PathBuf::from(path); - if !path.exists() { - log::error!("game dir {path:?} does not exist"); - } - - let entries = match path.read_dir() { - Ok(entries) => entries, - Err(e) => { - log::error!("Failed to read game dir: {e}"); - return; - } - }; - - tauri::async_runtime::spawn(async move { - let mut game_db = app_handle - .state::() - .inner() - .games - .lock() - .await; - - // Reset all games to uninstalled - game_db.set_all_uninstalled(); - - // update game_db with installed games from real game directory - entries.into_iter().for_each(|entry| { - if let Ok(entry) = entry { - if let Ok(path_type) = entry.file_type() { - if path_type.is_dir() { - let path = entry.path(); - if path.join("version.ini").exists() { - set_game_install_state_from_path(&mut game_db, &path, true); - } - } - } - } - }); - - if let Err(e) = app_handle.emit("games-list-updated", Some(game_db.all_games())) { - log::error!("Failed to emit games-list-updated event: {e}"); - } - }); -} - -async fn find_server(app: AppHandle) { - log::info!("Looking for server..."); - - loop { - match discover_service(LANSPREAD_SERVICE_TYPE, Some(LANSPREAD_INSTANCE_NAME)) { - Ok(server_addr) => { - log::info!("Found server at {server_addr}"); - let state: tauri::State = app.state(); - { - // mutex scope - let mut addr = state.server_addr.lock().await; - *addr = Some(server_addr); - } - state - .client_ctrl - .send(ClientCommand::ServerAddr(server_addr)) - .unwrap(); - request_games(state); - break; - } - Err(e) => { - log::warn!("Failed to find server: {e} - retrying..."); - } - } - } -} - -async fn update_game_db(games: Vec, app: AppHandle) { - for game in &games { - log::trace!("client event ListGames iter: {game:?}"); - } - - let state = app.state::(); - - // Store games list - let mut state_games = state.games.lock().await; - *state_games = GameDB::from(games.clone()); - - // Tell Frontend about new games list - if let Err(e) = app.emit("games-list-updated", Some(games)) { - log::error!("Failed to emit games-list-updated event: {e}"); - } else { - log::info!("Emitted games-list-updated event"); - } -} - -fn add_final_slash(path: &str) -> String { - #[cfg(target_os = "windows")] - const SLASH_CHAR: char = '\\'; - - #[cfg(not(target_os = "windows"))] - const SLASH_CHAR: char = '/'; - - if path.ends_with(SLASH_CHAR) { - path.to_string() - } else { - format!("{path}{SLASH_CHAR}") - } -} - -async fn do_unrar(sidecar: Command, rar_file: &Path, dest_dir: &Path) -> eyre::Result<()> { - if let Ok(()) = std::fs::create_dir_all(dest_dir) { - if let Ok(rar_file) = rar_file.canonicalize() { - if let Ok(dest_dir) = dest_dir.canonicalize() { - let dest_dir = dest_dir - .to_str() - .ok_or_else(|| eyre::eyre!("failed to get str of dest_dir"))?; - - log::info!( - "unrar game: {} to {}", - rar_file.canonicalize()?.display(), - dest_dir - ); - - let out = sidecar - .arg("x") // extract files - .arg(rar_file.canonicalize()?) - .arg("-y") // Assume Yes on all queries - .arg("-o") // Set overwrite mode - .arg(add_final_slash(dest_dir)) - .output() - .await?; - - if !out.status.success() { - log::error!("unrar stderr: {}", String::from_utf8_lossy(&out.stderr)); - } - - return Ok(()); - } else { - log::error!("dest_dir canonicalize failed: {:?}", &dest_dir); - } - } else { - log::error!("rar_file canonicalize failed: {:?}", &rar_file); - } - } else { - log::error!("failed to create dest_dir: {:?}", &dest_dir); - } - - bail!("failed to create directory: {dest_dir:?}"); -} - -async fn unpack_game(id: &str, sidecar: Command, games_folder: String) { - let game_path = PathBuf::from(games_folder).join(id); - let eti_rar = game_path.join(format!("{id}.eti")); - let local_path = game_path.join("local"); - - if let Err(e) = do_unrar(sidecar, &eti_rar, &local_path).await { - log::error!("{eti_rar:?} -> {local_path:?}: {e}"); - } -} - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - let tauri_logger_builder = tauri_plugin_log::Builder::new() - .clear_targets() - .target(tauri_plugin_log::Target::new( - tauri_plugin_log::TargetKind::Stdout, - )) - .level(log::LevelFilter::Info) - .level_for("mdns_sd::service_daemon", log::LevelFilter::Off); - - // channel to pass commands to the client - let (tx_client_control, rx_client_control) = - tokio::sync::mpsc::unbounded_channel::(); - - // channel to receive events from the client - let (tx_client_event, mut rx_client_event) = - tokio::sync::mpsc::unbounded_channel::(); - - let lanspread_state = LanSpreadState { - server_addr: Mutex::new(None), - client_ctrl: tx_client_control, - games: Arc::new(Mutex::new(GameDB::empty())), - games_in_download: Arc::new(Mutex::new(HashSet::new())), - games_folder: Arc::new(Mutex::new("".to_string())), - }; - - tauri::Builder::default() - .plugin(tauri_plugin_store::Builder::new().build()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_logger_builder.build()) - .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![ - request_games, - install_game, - run_game, - update_game_directory - ]) - .manage(lanspread_state) - .setup(|app| { - let app_handle = app.handle().clone(); - // discover server - tauri::async_runtime::spawn(async move { find_server(app_handle).await }); - tauri::async_runtime::spawn(async move { - lanspread_client::run(rx_client_control, tx_client_event).await - }); - - let app_handle = app.handle().clone(); - tauri::async_runtime::spawn(async move { - while let Some(event) = rx_client_event.recv().await { - match event { - ClientEvent::ListGames(games) => { - log::info!("ClientEvent::ListGames received"); - update_game_db(games, app_handle.clone()).await; - } - ClientEvent::GotGameFiles { id, file_descriptions } => { - log::info!("ClientEvent::GotGameFiles received"); - - if let Err(e) = app_handle.emit( - "game-download-pre", - Some(id.clone()), - ) { - log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}"); - } - - app_handle - .state::() - .inner() - .client_ctrl - .send(ClientCommand::DownloadGameFiles{ - id, - file_descriptions, - }) - .unwrap(); - - } - ClientEvent::DownloadGameFilesBegin { id } => { - log::info!("ClientEvent::DownloadGameFilesBegin received"); - - app_handle - .state::() - .inner() - .games_in_download - .lock() - .await - .insert(id.clone()); - - if let Err(e) = app_handle.emit("game-download-begin", Some(id)) { - log::error!("ClientEvent::DownloadGameFilesBegin: Failed to emit game-download-begin event: {e}"); - } - } - ClientEvent::DownloadGameFilesFinished { id } => { - log::info!("ClientEvent::DownloadGameFilesFinished received"); - if let Err(e) = app_handle.emit("game-download-finished", Some(id.clone())) { - log::error!("ClientEvent::DownloadGameFilesFinished: Failed to emit game-download-finished event: {e}"); - } - - app_handle - .state::() - .inner() - .games_in_download - .lock() - .await - .remove(&id.clone()); - - - let games_folder = app_handle - .state::() - .inner() - .games_folder - .lock() - .await - .clone(); - - if let Ok(sidecar) = app_handle.shell().sidecar("unrar") { - unpack_game(&id, sidecar, games_folder).await; - - log::info!("ClientEvent::UnpackGameFinished received"); - if let Err(e) = app_handle.emit("game-unpack-finished", Some(id.clone())) { - log::error!("ClientEvent::UnpackGameFinished: Failed to emit game-unpack-finished event: {e}"); - } - } - } - ClientEvent::DownloadGameFilesFailed { id } => { - log::warn!("ClientEvent::DownloadGameFilesFailed received"); - - if let Err(e) = app_handle.emit("game-download-failed", Some(id.clone())) { - log::error!("Failed to emit game-download-failed event: {e}"); - } - - app_handle - .state::() - .inner() - .games_in_download - .lock() - .await - .remove(&id.clone()); - }, - } - } - }); - - Ok(()) - }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} +use std::{ + collections::HashSet, + net::SocketAddr, + path::{Path, PathBuf}, + sync::Arc, +}; + +use eyre::bail; +use lanspread_client::{ClientCommand, ClientEvent}; +use lanspread_db::db::{Game, GameDB}; +use lanspread_mdns::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service}; +use tauri::{AppHandle, Emitter as _, Manager}; +use tauri_plugin_shell::{ShellExt, process::Command}; +use tokio::sync::{Mutex, mpsc::UnboundedSender}; + +// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ + +struct LanSpreadState { + server_addr: Mutex>, + client_ctrl: UnboundedSender, + games: Arc>, + games_in_download: Arc>>, + games_folder: Arc>, +} + +#[tauri::command] +fn request_games(state: tauri::State) { + log::debug!("request_games"); + + if let Err(e) = state.inner().client_ctrl.send(ClientCommand::ListGames) { + log::error!("Failed to send message to client: {e:?}"); + } +} + +#[tauri::command] +fn install_game(id: String, state: tauri::State) -> bool { + let already_in_download = tauri::async_runtime::block_on(async { + if state.inner().games_in_download.lock().await.contains(&id) { + log::warn!("Game is already downloading: {id}"); + return true; + } + false + }); + + if already_in_download { + return false; + } + + if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) { + log::error!("Failed to send message to client: {e:?}"); + } + + true +} + +#[cfg(target_os = "windows")] +fn run_as_admin(file: &str, params: &str, dir: &str) -> bool { + use std::{ffi::OsStr, os::windows::ffi::OsStrExt}; + + use windows::{Win32::UI::Shell::ShellExecuteW, core::PCWSTR}; + + let file_wide: Vec = OsStr::new(file).encode_wide().chain(Some(0)).collect(); + let params_wide: Vec = OsStr::new(params).encode_wide().chain(Some(0)).collect(); + let dir_wide: Vec = OsStr::new(dir).encode_wide().chain(Some(0)).collect(); + let runas_wide: Vec = OsStr::new("runas").encode_wide().chain(Some(0)).collect(); + + let result = unsafe { + ShellExecuteW( + None, + PCWSTR::from_raw(runas_wide.as_ptr()), + PCWSTR::from_raw(file_wide.as_ptr()), + PCWSTR::from_raw(params_wide.as_ptr()), + PCWSTR::from_raw(dir_wide.as_ptr()), + windows::Win32::UI::WindowsAndMessaging::SW_HIDE, + ) + }; + + (result.0 as usize) > 32 // Success if greater than 32 +} + +#[cfg(target_os = "windows")] +fn run_game_windows(id: String, state: tauri::State) { + use std::fs::File; + + const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done"; + + let games_folder = + tauri::async_runtime::block_on(async { state.inner().games_folder.lock().await.clone() }); + + let games_folder = PathBuf::from(games_folder); + if !games_folder.exists() { + log::error!("games_folder {games_folder:?} does not exist"); + return; + } + + let game_path = games_folder.join(id.clone()); + + let game_setup_bin = game_path.join("game_setup.cmd"); + let game_start_bin = game_path.join("game_start.cmd"); + + let first_start_done_file = game_path.join(FIRST_START_DONE_FILE); + if !first_start_done_file.exists() && game_setup_bin.exists() { + let result = run_as_admin( + "cmd.exe", + &format!( + r#"/c "{} local {} de playername""#, + game_setup_bin.display(), + &id + ), + &game_path.display().to_string(), + ); + + if !result { + log::error!("failed to run game_setup.cmd"); + return; + } + + if let Err(e) = File::create(&first_start_done_file) { + log::error!("failed to create {first_start_done_file:?}: {e}"); + } + } + + if game_start_bin.exists() { + let result = run_as_admin( + "cmd.exe", + &format!( + r#"/c "{} local {} de playername""#, + game_start_bin.display(), + &id + ), + &game_path.display().to_string(), + ); + + if !result { + log::error!("failed to run game_start.cmd"); + } + } +} + +#[tauri::command] +fn run_game(id: String, state: tauri::State) { + #[cfg(target_os = "windows")] + run_game_windows(id, state); + + #[cfg(not(target_os = "windows"))] + log::error!("run_game not implemented for this platform: id={id}, state={state:?}"); +} + +fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed: bool) { + if let Some(file_name) = path.file_name() { + if let Some(file_name) = file_name.to_str() { + if let Some(game) = game_db.get_mut_game_by_id(file_name) { + if installed { + log::debug!("Game is installed: {game}"); + } else { + log::error!("Game is missing: {game}"); + } + game.installed = installed; + } + } + } +} + +#[tauri::command] +fn update_game_directory(app_handle: tauri::AppHandle, path: String) { + log::info!("update_game_directory: {path}"); + + app_handle + .state::() + .client_ctrl + .send(ClientCommand::SetGameDir(path.clone())) + .unwrap(); + + { + tauri::async_runtime::block_on(async { + let mut games_folder = app_handle + .state::() + .inner() + .games_folder + .lock() + .await; + + *games_folder = path.clone(); + }); + } + + let path = PathBuf::from(path); + if !path.exists() { + log::error!("game dir {path:?} does not exist"); + } + + let entries = match path.read_dir() { + Ok(entries) => entries, + Err(e) => { + log::error!("Failed to read game dir: {e}"); + return; + } + }; + + tauri::async_runtime::spawn(async move { + let mut game_db = app_handle + .state::() + .inner() + .games + .lock() + .await; + + // Reset all games to uninstalled + game_db.set_all_uninstalled(); + + // update game_db with installed games from real game directory + entries.into_iter().for_each(|entry| { + if let Ok(entry) = entry { + if let Ok(path_type) = entry.file_type() { + if path_type.is_dir() { + let path = entry.path(); + if path.join("version.ini").exists() { + set_game_install_state_from_path(&mut game_db, &path, true); + } + } + } + } + }); + + if let Err(e) = app_handle.emit("games-list-updated", Some(game_db.all_games())) { + log::error!("Failed to emit games-list-updated event: {e}"); + } + }); +} + +async fn find_server(app: AppHandle) { + log::info!("Looking for server..."); + + loop { + match discover_service(LANSPREAD_SERVICE_TYPE, Some(LANSPREAD_INSTANCE_NAME)) { + Ok(server_addr) => { + log::info!("Found server at {server_addr}"); + let state: tauri::State = app.state(); + { + // mutex scope + let mut addr = state.server_addr.lock().await; + *addr = Some(server_addr); + } + state + .client_ctrl + .send(ClientCommand::ServerAddr(server_addr)) + .unwrap(); + request_games(state); + break; + } + Err(e) => { + log::warn!("Failed to find server: {e} - retrying..."); + } + } + } +} + +async fn update_game_db(games: Vec, app: AppHandle) { + for game in &games { + log::trace!("client event ListGames iter: {game:?}"); + } + + let state = app.state::(); + + // Store games list + let mut state_games = state.games.lock().await; + *state_games = GameDB::from(games.clone()); + + // Tell Frontend about new games list + if let Err(e) = app.emit("games-list-updated", Some(games)) { + log::error!("Failed to emit games-list-updated event: {e}"); + } else { + log::info!("Emitted games-list-updated event"); + } +} + +fn add_final_slash(path: &str) -> String { + #[cfg(target_os = "windows")] + const SLASH_CHAR: char = '\\'; + + #[cfg(not(target_os = "windows"))] + const SLASH_CHAR: char = '/'; + + if path.ends_with(SLASH_CHAR) { + path.to_string() + } else { + format!("{path}{SLASH_CHAR}") + } +} + +async fn do_unrar(sidecar: Command, rar_file: &Path, dest_dir: &Path) -> eyre::Result<()> { + if let Ok(()) = std::fs::create_dir_all(dest_dir) { + if let Ok(rar_file) = rar_file.canonicalize() { + if let Ok(dest_dir) = dest_dir.canonicalize() { + let dest_dir = dest_dir + .to_str() + .ok_or_else(|| eyre::eyre!("failed to get str of dest_dir"))?; + + log::info!( + "unrar game: {} to {}", + rar_file.canonicalize()?.display(), + dest_dir + ); + + let out = sidecar + .arg("x") // extract files + .arg(rar_file.canonicalize()?) + .arg("-y") // Assume Yes on all queries + .arg("-o") // Set overwrite mode + .arg(add_final_slash(dest_dir)) + .output() + .await?; + + if !out.status.success() { + log::error!("unrar stderr: {}", String::from_utf8_lossy(&out.stderr)); + } + + return Ok(()); + } else { + log::error!("dest_dir canonicalize failed: {:?}", &dest_dir); + } + } else { + log::error!("rar_file canonicalize failed: {:?}", &rar_file); + } + } else { + log::error!("failed to create dest_dir: {:?}", &dest_dir); + } + + bail!("failed to create directory: {dest_dir:?}"); +} + +async fn unpack_game(id: &str, sidecar: Command, games_folder: String) { + let game_path = PathBuf::from(games_folder).join(id); + let eti_rar = game_path.join(format!("{id}.eti")); + let local_path = game_path.join("local"); + + if let Err(e) = do_unrar(sidecar, &eti_rar, &local_path).await { + log::error!("{eti_rar:?} -> {local_path:?}: {e}"); + } +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let tauri_logger_builder = tauri_plugin_log::Builder::new() + .clear_targets() + .target(tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::Stdout, + )) + .level(log::LevelFilter::Info) + .level_for("mdns_sd::service_daemon", log::LevelFilter::Off); + + // channel to pass commands to the client + let (tx_client_control, rx_client_control) = + tokio::sync::mpsc::unbounded_channel::(); + + // channel to receive events from the client + let (tx_client_event, mut rx_client_event) = + tokio::sync::mpsc::unbounded_channel::(); + + let lanspread_state = LanSpreadState { + server_addr: Mutex::new(None), + client_ctrl: tx_client_control, + games: Arc::new(Mutex::new(GameDB::empty())), + games_in_download: Arc::new(Mutex::new(HashSet::new())), + games_folder: Arc::new(Mutex::new("".to_string())), + }; + + tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::new().build()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_logger_builder.build()) + .plugin(tauri_plugin_shell::init()) + .invoke_handler(tauri::generate_handler![ + request_games, + install_game, + run_game, + update_game_directory + ]) + .manage(lanspread_state) + .setup(|app| { + let app_handle = app.handle().clone(); + // discover server + tauri::async_runtime::spawn(async move { find_server(app_handle).await }); + tauri::async_runtime::spawn(async move { + lanspread_client::run(rx_client_control, tx_client_event).await + }); + + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + while let Some(event) = rx_client_event.recv().await { + match event { + ClientEvent::ListGames(games) => { + log::info!("ClientEvent::ListGames received"); + update_game_db(games, app_handle.clone()).await; + } + ClientEvent::GotGameFiles { id, file_descriptions } => { + log::info!("ClientEvent::GotGameFiles received"); + + if let Err(e) = app_handle.emit( + "game-download-pre", + Some(id.clone()), + ) { + log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}"); + } + + app_handle + .state::() + .inner() + .client_ctrl + .send(ClientCommand::DownloadGameFiles{ + id, + file_descriptions, + }) + .unwrap(); + + } + ClientEvent::DownloadGameFilesBegin { id } => { + log::info!("ClientEvent::DownloadGameFilesBegin received"); + + app_handle + .state::() + .inner() + .games_in_download + .lock() + .await + .insert(id.clone()); + + if let Err(e) = app_handle.emit("game-download-begin", Some(id)) { + log::error!("ClientEvent::DownloadGameFilesBegin: Failed to emit game-download-begin event: {e}"); + } + } + ClientEvent::DownloadGameFilesFinished { id } => { + log::info!("ClientEvent::DownloadGameFilesFinished received"); + if let Err(e) = app_handle.emit("game-download-finished", Some(id.clone())) { + log::error!("ClientEvent::DownloadGameFilesFinished: Failed to emit game-download-finished event: {e}"); + } + + app_handle + .state::() + .inner() + .games_in_download + .lock() + .await + .remove(&id.clone()); + + + let games_folder = app_handle + .state::() + .inner() + .games_folder + .lock() + .await + .clone(); + + if let Ok(sidecar) = app_handle.shell().sidecar("unrar") { + unpack_game(&id, sidecar, games_folder).await; + + log::info!("ClientEvent::UnpackGameFinished received"); + if let Err(e) = app_handle.emit("game-unpack-finished", Some(id.clone())) { + log::error!("ClientEvent::UnpackGameFinished: Failed to emit game-unpack-finished event: {e}"); + } + } + } + ClientEvent::DownloadGameFilesFailed { id } => { + log::warn!("ClientEvent::DownloadGameFilesFailed received"); + + if let Err(e) = app_handle.emit("game-download-failed", Some(id.clone())) { + log::error!("Failed to emit game-download-failed event: {e}"); + } + + app_handle + .state::() + .inner() + .games_in_download + .lock() + .await + .remove(&id.clone()); + }, + } + } + }); + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +}