diff --git a/crates/lanspread-client/src/lib.rs b/crates/lanspread-client/src/lib.rs index 48794d2..54bfdc8 100644 --- a/crates/lanspread-client/src/lib.rs +++ b/crates/lanspread-client/src/lib.rs @@ -2,14 +2,13 @@ use std::{fs::File, io::Write as _, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; -use bytes::{Bytes, BytesMut}; +use bytes::BytesMut; use lanspread_db::db::{Game, GameFileDescription}; use lanspread_proto::{Message as _, Request, Response}; use lanspread_utils::maybe_addr; use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient, Connection}; use tokio::{ io::AsyncWriteExt, - stream, sync::{ mpsc::{UnboundedReceiver, UnboundedSender}, Mutex, diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-pc-windows-msvc.exe b/crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-pc-windows-msvc.exe new file mode 100755 index 0000000..17aefa9 Binary files /dev/null and b/crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-pc-windows-msvc.exe differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-unknown-linux-gnu b/crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-unknown-linux-gnu new file mode 100755 index 0000000..d824a4c Binary files /dev/null and b/crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-unknown-linux-gnu differ 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 edde1f3..d0ab4eb 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -1,14 +1,17 @@ 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/ @@ -18,6 +21,7 @@ struct LanSpreadState { client_ctrl: UnboundedSender, games: Arc>, games_in_download: Arc>>, + games_dir: Arc>, } #[tauri::command] @@ -57,7 +61,7 @@ fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed 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::error!("Game is installed: {game}"); + log::info!("Game is installed: {game}"); } else { log::error!("Game is missing: {game}"); } @@ -69,12 +73,27 @@ fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, 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"); @@ -165,6 +184,70 @@ async fn update_game_db(games: Vec, app: AppHandle) { } } +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() @@ -190,6 +273,7 @@ pub fn run() { 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() @@ -257,9 +341,35 @@ pub fn run() { } ClientEvent::DownloadGameFilesFinished { id } => { log::info!("ClientEvent::DownloadGameFilesFinished received"); - if let Err(e) = app_handle.emit("game-download-finished", Some(id)) { + 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}"); + } + } } } } diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/tauri.conf.json b/crates/lanspread-tauri-deno-ts/src-tauri/tauri.conf.json index 60ac4e9..414923e 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/tauri.conf.json +++ b/crates/lanspread-tauri-deno-ts/src-tauri/tauri.conf.json @@ -30,6 +30,9 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" + ], + "externalBin": [ + "binaries/unrar" ] } } diff --git a/crates/lanspread-tauri-deno-ts/src/App.tsx b/crates/lanspread-tauri-deno-ts/src/App.tsx index 384e440..ec0c4a1 100644 --- a/crates/lanspread-tauri-deno-ts/src/App.tsx +++ b/crates/lanspread-tauri-deno-ts/src/App.tsx @@ -1,6 +1,6 @@ -import {useEffect, useState} from 'react'; -import {invoke} from '@tauri-apps/api/core'; -import {listen} from '@tauri-apps/api/event'; +import { useEffect, useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; import { open } from '@tauri-apps/plugin-dialog'; import { load } from '@tauri-apps/plugin-store'; @@ -48,6 +48,32 @@ const App = () => { } }; + useEffect(() => { + // Listen for game-unpack-finished events specifically + const setupUnpackListener = async () => { + const unlisten = await listen('game-unpack-finished', (event) => { + const game_id = event.payload as string; + console.log(`🗲 game-unpack-finished ${game_id} event received`); + console.log('Current gameDir in listener:', gameDir); // Add this log + setGameItems(prev => prev.map(item => item.id === game_id + ? {...item, install_status: InstallStatus.Installed} + : item)); + + // Convert to string explicitly and verify it's not empty + const pathString = String(gameDir); + if (!pathString) { + console.error('gameDir is empty before invoke!'); + return; + } + invoke('update_game_directory', { path: pathString }) + .catch(error => console.error('❌ Error updating game directory:', error)); + }); + return unlisten; + }; + + setupUnpackListener(); + }, [gameDir]); + useEffect(() => { if (gameDir) { // store game directory in persistent storage @@ -88,7 +114,7 @@ const App = () => { const game_id = event.payload as string; console.log(`🗲 game-download-begin ${game_id} event received`); setGameItems(prev => prev.map(item => item.id === game_id - ? {...item, install_status: InstallStatus.Downloading} + ? { ...item, install_status: InstallStatus.Downloading } : item)); }); @@ -97,7 +123,7 @@ const App = () => { const game_id = event.payload as string; console.log(`🗲 game-download-finished ${game_id} event received`); setGameItems(prev => prev.map(item => item.id === game_id - ? {...item, install_status: InstallStatus.Unpacking} + ? { ...item, install_status: InstallStatus.Unpacking } : item)); }); @@ -128,7 +154,7 @@ const App = () => { const runGame = async (id: string) => { console.log(`🎯 Running game with id=${id}`); try { - const result = await invoke('run_game', {id}); + const result = await invoke('run_game', { id }); console.log(`✅ Game started, result=${result}`); } catch (error) { console.error('❌ Error running game:', error); @@ -138,12 +164,12 @@ const App = () => { const installGame = async (id: string) => { console.log(`🎯 Installing game with id=${id}`); try { - const success = await invoke('install_game', {id}); + const success = await invoke('install_game', { id }); if (success) { console.log(`✅ Game install for id=${id} started...`); // update install status in gameItems for this game setGameItems(prev => prev.map(item => item.id === id - ? {...item, install_status: InstallStatus.CheckingServer} + ? { ...item, install_status: InstallStatus.CheckingServer } : item)); } else { // game is already being installed @@ -203,14 +229,14 @@ const App = () => { {item.size.toString()}
item.installed - ? runGame(item.id) - : installGame(item.id)}> + onClick={() => item.installed + ? runGame(item.id) + : installGame(item.id)}> {item.installed ? 'Play' : item.install_status === InstallStatus.CheckingServer ? 'Checking server...' - : item.install_status === InstallStatus.Downloading ? 'Downloading...' - : item.install_status === InstallStatus.Unpacking ? 'Unpacking...' - : 'Install'} + : item.install_status === InstallStatus.Downloading ? 'Downloading...' + : item.install_status === InstallStatus.Unpacking ? 'Unpacking...' + : 'Install'}
);