[client] no cmd window when running games or scripts
This commit is contained in:
parent
d80dece8a7
commit
366b6fbca7
@ -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<Option<SocketAddr>>,
|
||||
client_ctrl: UnboundedSender<ClientCommand>,
|
||||
games: Arc<Mutex<GameDB>>,
|
||||
games_in_download: Arc<Mutex<HashSet<String>>>,
|
||||
games_folder: Arc<Mutex<String>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn request_games(state: tauri::State<LanSpreadState>) {
|
||||
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<LanSpreadState>) -> 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<u16> = OsStr::new(file).encode_wide().chain(Some(0)).collect();
|
||||
let params_wide: Vec<u16> = OsStr::new(params).encode_wide().chain(Some(0)).collect();
|
||||
let dir_wide: Vec<u16> = OsStr::new(dir).encode_wide().chain(Some(0)).collect();
|
||||
let runas_wide: Vec<u16> = 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<LanSpreadState>) {
|
||||
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<LanSpreadState>) {
|
||||
#[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::<LanSpreadState>()
|
||||
.client_ctrl
|
||||
.send(ClientCommand::SetGameDir(path.clone()))
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
tauri::async_runtime::block_on(async {
|
||||
let mut games_folder = app_handle
|
||||
.state::<LanSpreadState>()
|
||||
.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::<LanSpreadState>()
|
||||
.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<LanSpreadState> = 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<Game>, app: AppHandle) {
|
||||
for game in &games {
|
||||
log::trace!("client event ListGames iter: {game:?}");
|
||||
}
|
||||
|
||||
let state = app.state::<LanSpreadState>();
|
||||
|
||||
// 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::<ClientCommand>();
|
||||
|
||||
// channel to receive events from the client
|
||||
let (tx_client_event, mut rx_client_event) =
|
||||
tokio::sync::mpsc::unbounded_channel::<ClientEvent>();
|
||||
|
||||
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::<LanSpreadState>()
|
||||
.inner()
|
||||
.client_ctrl
|
||||
.send(ClientCommand::DownloadGameFiles{
|
||||
id,
|
||||
file_descriptions,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
}
|
||||
ClientEvent::DownloadGameFilesBegin { id } => {
|
||||
log::info!("ClientEvent::DownloadGameFilesBegin received");
|
||||
|
||||
app_handle
|
||||
.state::<LanSpreadState>()
|
||||
.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::<LanSpreadState>()
|
||||
.inner()
|
||||
.games_in_download
|
||||
.lock()
|
||||
.await
|
||||
.remove(&id.clone());
|
||||
|
||||
|
||||
let games_folder = app_handle
|
||||
.state::<LanSpreadState>()
|
||||
.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::<LanSpreadState>()
|
||||
.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<Option<SocketAddr>>,
|
||||
client_ctrl: UnboundedSender<ClientCommand>,
|
||||
games: Arc<Mutex<GameDB>>,
|
||||
games_in_download: Arc<Mutex<HashSet<String>>>,
|
||||
games_folder: Arc<Mutex<String>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn request_games(state: tauri::State<LanSpreadState>) {
|
||||
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<LanSpreadState>) -> 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<u16> = OsStr::new(file).encode_wide().chain(Some(0)).collect();
|
||||
let params_wide: Vec<u16> = OsStr::new(params).encode_wide().chain(Some(0)).collect();
|
||||
let dir_wide: Vec<u16> = OsStr::new(dir).encode_wide().chain(Some(0)).collect();
|
||||
let runas_wide: Vec<u16> = 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<LanSpreadState>) {
|
||||
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<LanSpreadState>) {
|
||||
#[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::<LanSpreadState>()
|
||||
.client_ctrl
|
||||
.send(ClientCommand::SetGameDir(path.clone()))
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
tauri::async_runtime::block_on(async {
|
||||
let mut games_folder = app_handle
|
||||
.state::<LanSpreadState>()
|
||||
.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::<LanSpreadState>()
|
||||
.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<LanSpreadState> = 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<Game>, app: AppHandle) {
|
||||
for game in &games {
|
||||
log::trace!("client event ListGames iter: {game:?}");
|
||||
}
|
||||
|
||||
let state = app.state::<LanSpreadState>();
|
||||
|
||||
// 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::<ClientCommand>();
|
||||
|
||||
// channel to receive events from the client
|
||||
let (tx_client_event, mut rx_client_event) =
|
||||
tokio::sync::mpsc::unbounded_channel::<ClientEvent>();
|
||||
|
||||
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::<LanSpreadState>()
|
||||
.inner()
|
||||
.client_ctrl
|
||||
.send(ClientCommand::DownloadGameFiles{
|
||||
id,
|
||||
file_descriptions,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
}
|
||||
ClientEvent::DownloadGameFilesBegin { id } => {
|
||||
log::info!("ClientEvent::DownloadGameFilesBegin received");
|
||||
|
||||
app_handle
|
||||
.state::<LanSpreadState>()
|
||||
.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::<LanSpreadState>()
|
||||
.inner()
|
||||
.games_in_download
|
||||
.lock()
|
||||
.await
|
||||
.remove(&id.clone());
|
||||
|
||||
|
||||
let games_folder = app_handle
|
||||
.state::<LanSpreadState>()
|
||||
.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::<LanSpreadState>()
|
||||
.inner()
|
||||
.games_in_download
|
||||
.lock()
|
||||
.await
|
||||
.remove(&id.clone());
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user