[client] unpack game works!
This commit is contained in:
parent
f9cd8471b4
commit
bc70d6300b
@ -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,
|
||||
|
Binary file not shown.
BIN
crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-unknown-linux-gnu
Executable file
BIN
crates/lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-unknown-linux-gnu
Executable file
Binary file not shown.
@ -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<ClientCommand>,
|
||||
games: Arc<Mutex<GameDB>>,
|
||||
games_in_download: Arc<Mutex<HashSet<String>>>,
|
||||
games_dir: Arc<Mutex<String>>,
|
||||
}
|
||||
|
||||
#[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::<LanSpreadState>()
|
||||
.client_ctrl
|
||||
.send(ClientCommand::SetGameDir(path.clone()))
|
||||
.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);
|
||||
if !path.exists() {
|
||||
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)]
|
||||
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::<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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,9 @@
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/unrar"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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 = () => {
|
||||
<span className="size-text">{item.size.toString()}</span>
|
||||
</div>
|
||||
<div className="play-button"
|
||||
onClick={() => 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'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user