use std::{ collections::HashSet, fs::File, net::SocketAddr, path::{Path, PathBuf}, sync::Arc, }; use eyre::bail; use lanspread_client::{ClientCommand, ClientEvent}; use lanspread_db::db::{Game, GameDB}; use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE}; use tauri::{AppHandle, Emitter as _, Manager}; use tauri_plugin_shell::{process::Command, ShellExt}; use tokio::sync::{mpsc::UnboundedSender, Mutex}; // 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_dir: 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 { log::error!("Running game with id {id}"); let already_in_download = tauri::async_runtime::block_on(async { if state.inner().games_in_download.lock().await.contains(&id) { log::error!("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 } #[tauri::command] fn run_game(id: String, state: tauri::State) { const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done"; log::error!("run_game {id}"); let games_dir = tauri::async_runtime::block_on(async { state.inner().games_dir.lock().await.clone() }); let games_dir = PathBuf::from(games_dir); if !games_dir.exists() { log::error!("games_dir {games_dir:?} does not exist"); return; } let game_path = games_dir.join(id); 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() { if let Err(e) = std::process::Command::new(game_setup_bin) .current_dir(&game_path) .spawn() { log::error!("failed to run game_setup.cmd: {e}"); return; } else if let Err(e) = std::fs::File::create(FIRST_START_DONE_FILE) { log::error!("failed to create {first_start_done_file:?}: {e}"); } } if game_start_bin.exists() { if let Err(e) = std::process::Command::new(game_start_bin) .current_dir(&game_path) .spawn() { log::error!("failed to run game_start.cmd: {e}"); } } } 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::info!("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_dir = app_handle .state::() .inner() .games_dir .lock() .await; *games_dir = 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(".softlan_game_installed").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::error!("SIDECARE: {:?}", &sidecar); 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?; 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_dir: String) { let game_path = PathBuf::from(games_dir).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}"); } else { let game_installed_file = game_path.join(".softlan_game_installed"); if let Err(e) = File::create(game_installed_file) { log::error!("failed to create game_installed_file: {e}"); } else { log::info!("game unpacked: {id}"); } } } #[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("lanspread_client", log::LevelFilter::Debug) .level_for("lanspread_tauri_leptos_lib", log::LevelFilter::Debug) .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_dir: 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(game_file_descs) => { log::info!("ClientEvent::GotGameFiles received"); if let Some(first_desc_file) = game_file_descs.first() { if let Err(e) = app_handle.emit( "game-download-pre", Some(first_desc_file.game_id.clone()), ) { log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}"); } app_handle .state::() .inner() .client_ctrl .send(ClientCommand::DownloadGameFiles(game_file_descs)) .unwrap(); } else { log::error!("ClientEvent::GotGameFiles: Got empty game files list"); } } 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_dir = app_handle .state::() .inner() .games_dir .lock() .await .clone(); if let Ok(sidecar) = app_handle.shell().sidecar("unrar") { unpack_game(&id, sidecar, games_dir).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}"); } } } } } }); Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }