[client] unpack game works!

This commit is contained in:
ddidderr 2024-11-15 11:20:35 +01:00
parent f9cd8471b4
commit bc70d6300b
Signed by: ddidderr
GPG Key ID: 3841F1C27E6F0E14
6 changed files with 156 additions and 18 deletions

View File

@ -2,14 +2,13 @@
use std::{fs::File, io::Write as _, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; 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_db::db::{Game, GameFileDescription};
use lanspread_proto::{Message as _, Request, Response}; use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr; use lanspread_utils::maybe_addr;
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient, Connection}; use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient, Connection};
use tokio::{ use tokio::{
io::AsyncWriteExt, io::AsyncWriteExt,
stream,
sync::{ sync::{
mpsc::{UnboundedReceiver, UnboundedSender}, mpsc::{UnboundedReceiver, UnboundedSender},
Mutex, Mutex,

View File

@ -1,14 +1,17 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
fs::File,
net::SocketAddr, net::SocketAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use eyre::bail;
use lanspread_client::{ClientCommand, ClientEvent}; use lanspread_client::{ClientCommand, ClientEvent};
use lanspread_db::db::{Game, GameDB}; use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE}; use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE};
use tauri::{AppHandle, Emitter as _, Manager}; use tauri::{AppHandle, Emitter as _, Manager};
use tauri_plugin_shell::{process::Command, ShellExt};
use tokio::sync::{mpsc::UnboundedSender, Mutex}; use tokio::sync::{mpsc::UnboundedSender, Mutex};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
@ -18,6 +21,7 @@ struct LanSpreadState {
client_ctrl: UnboundedSender<ClientCommand>, client_ctrl: UnboundedSender<ClientCommand>,
games: Arc<Mutex<GameDB>>, games: Arc<Mutex<GameDB>>,
games_in_download: Arc<Mutex<HashSet<String>>>, games_in_download: Arc<Mutex<HashSet<String>>>,
games_dir: Arc<Mutex<String>>,
} }
#[tauri::command] #[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(file_name) = file_name.to_str() {
if let Some(game) = game_db.get_mut_game_by_id(file_name) { if let Some(game) = game_db.get_mut_game_by_id(file_name) {
if installed { if installed {
log::error!("Game is installed: {game}"); log::info!("Game is installed: {game}");
} else { } else {
log::error!("Game is missing: {game}"); 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] #[tauri::command]
fn update_game_directory(app_handle: tauri::AppHandle, path: String) { fn update_game_directory(app_handle: tauri::AppHandle, path: String) {
log::info!("update_game_directory: {path}");
app_handle app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.client_ctrl .client_ctrl
.send(ClientCommand::SetGameDir(path.clone())) .send(ClientCommand::SetGameDir(path.clone()))
.unwrap(); .unwrap();
{
tauri::async_runtime::block_on(async {
let mut games_dir = app_handle
.state::<LanSpreadState>()
.inner()
.games_dir
.lock()
.await;
*games_dir = path.clone();
});
}
let path = PathBuf::from(path); let path = PathBuf::from(path);
if !path.exists() { if !path.exists() {
log::error!("game dir {path:?} does not exist"); log::error!("game dir {path:?} does not exist");
@ -165,6 +184,70 @@ async fn update_game_db(games: Vec<Game>, 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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let tauri_logger_builder = tauri_plugin_log::Builder::new() let tauri_logger_builder = tauri_plugin_log::Builder::new()
@ -190,6 +273,7 @@ pub fn run() {
client_ctrl: tx_client_control, client_ctrl: tx_client_control,
games: Arc::new(Mutex::new(GameDB::empty())), games: Arc::new(Mutex::new(GameDB::empty())),
games_in_download: Arc::new(Mutex::new(HashSet::new())), games_in_download: Arc::new(Mutex::new(HashSet::new())),
games_dir: Arc::new(Mutex::new("".to_string())),
}; };
tauri::Builder::default() tauri::Builder::default()
@ -257,9 +341,35 @@ pub fn run() {
} }
ClientEvent::DownloadGameFilesFinished { id } => { ClientEvent::DownloadGameFilesFinished { id } => {
log::info!("ClientEvent::DownloadGameFilesFinished received"); 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}"); log::error!("ClientEvent::DownloadGameFilesFinished: Failed to emit game-download-finished event: {e}");
} }
app_handle
.state::<LanSpreadState>()
.inner()
.games_in_download
.lock()
.await
.remove(&id.clone());
let games_dir = app_handle
.state::<LanSpreadState>()
.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}");
}
}
} }
} }
} }

View File

@ -30,6 +30,9 @@
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
],
"externalBin": [
"binaries/unrar"
] ]
} }
} }

View File

@ -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(() => { useEffect(() => {
if (gameDir) { if (gameDir) {
// store game directory in persistent storage // store game directory in persistent storage