Compare commits

..

No commits in common. "366b6fbca7a5ad7e763a8de8e05eb9b949485040" and "d69cf115c8aa300e473b529d9de1acf426adb8cb" have entirely different histories.

2 changed files with 628 additions and 508 deletions

162
Cargo.lock generated
View File

@ -316,7 +316,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"rustc-hash", "rustc-hash 1.1.0",
"shlex", "shlex",
"syn 2.0.100", "syn 2.0.100",
"which", "which",
@ -539,9 +539,9 @@ dependencies = [
[[package]] [[package]]
name = "cargo_toml" name = "cargo_toml"
version = "0.22.1" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472"
dependencies = [ dependencies = [
"serde", "serde",
"toml", "toml",
@ -1575,8 +1575,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1586,9 +1588,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1886,6 +1890,24 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http",
"hyper",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.10" version = "0.1.10"
@ -3448,6 +3470,60 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quinn"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.1",
"rustls",
"socket2",
"thiserror 2.0.12",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
dependencies = [
"bytes",
"getrandom 0.3.2",
"rand 0.9.0",
"ring",
"rustc-hash 2.1.1",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@ -3673,6 +3749,7 @@ dependencies = [
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
@ -3681,11 +3758,16 @@ dependencies = [
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-service", "tower-service",
@ -3694,6 +3776,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots",
"windows-registry", "windows-registry",
] ]
@ -3793,6 +3876,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -3837,6 +3926,7 @@ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"log", "log",
"once_cell", "once_cell",
"ring",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki", "rustls-webpki",
"subtle", "subtle",
@ -3857,6 +3947,9 @@ name = "rustls-pki-types"
version = "1.11.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
dependencies = [
"web-time",
]
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
@ -4707,9 +4800,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.4.0" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "511dd38065a5d3b36c33cdba4362b99a40a5103bebcd4aebb930717e7c8ba292" checksum = "3be747b26bf28674977fac47bdf6963fd9c7578271c3fbeb25d8686de6596f35"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -4757,9 +4850,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.1.0" version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffa8732a66f90903f5a585215f3cf1e87988d0359bc88c18a502efe7572c1de" checksum = "51a2e96f3c0baa0581656bb58e6fdd0f7c9c31eaf6721a0c08689d938fe85f2d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@ -4779,9 +4872,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.1.0" version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c266a247f14d63f40c6282c2653a8bac5cc3d482ca562a003a88513653ea817a" checksum = "e357ec3daf8faad1029bc7109e7f5b308ceb63b6073d110d7388923a4cce5e55"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@ -4806,9 +4899,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.1.0" version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f47a1cf94b3bd6c4dc37dce1a43fc96120ff29a91757f0ab3cf713c7ad846e7c" checksum = "447ee4dd94690d77f1422f2b57e783c654ba75c535ad6f6e727887330804fff2"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -4820,9 +4913,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin" name = "tauri-plugin"
version = "2.1.0" version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9972871fcbddf16618f70412d965d4d845cd4b76d03fff168709961ef71e5cdf" checksum = "0ad3021d8e60ec7672f51ecb67c5e1a514a4d7a9a5ffc9d85090739378047502"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@ -4937,11 +5030,10 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.5.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9c7bce5153f1ca7bc45eba37349b31ba50e975e28edc8b5766c5ec02b0b63a" checksum = "e758a405ab39e25f4d1235c5f06fe563f44b01ee18bbe38ddec5356d4f581908"
dependencies = [ dependencies = [
"cookie",
"dpi", "dpi",
"gtk", "gtk",
"http", "http",
@ -4957,9 +5049,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.5.0" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087188020fd6facb8578fe9b38e81fa0fe5fb85744c73da51a299f94a530a1e3" checksum = "8b2beb90decade4c71e8b09c9e4a9245837a8a97693f945b77e32baf13f51fec"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@ -4984,11 +5076,10 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.3.0" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82dcced4014e59af9790cc22f5d271df3be09ecd6728ec68861642553c8d01b7" checksum = "107a959dbd5ff53d89a98f6f2e3e987c611334141a43630caae1d80e79446dd6"
dependencies = [ dependencies = [
"anyhow",
"brotli", "brotli",
"cargo_metadata", "cargo_metadata",
"ctor", "ctor",
@ -5198,6 +5289,16 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
dependencies = [
"rustls",
"tokio",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.17" version = "0.1.17"
@ -5731,6 +5832,16 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "webkit2gtk" name = "webkit2gtk"
version = "2.0.1" version = "2.0.1"
@ -5775,6 +5886,15 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-roots"
version = "0.26.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.36.0" version = "0.36.0"

View File

@ -1,487 +1,487 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
net::SocketAddr, net::SocketAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use eyre::bail; 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::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service}; use lanspread_mdns::{LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE, discover_service};
use tauri::{AppHandle, Emitter as _, Manager}; use tauri::{AppHandle, Emitter as _, Manager};
use tauri_plugin_shell::{ShellExt, process::Command}; use tauri_plugin_shell::{ShellExt, process::Command};
use tokio::sync::{Mutex, mpsc::UnboundedSender}; use tokio::sync::{Mutex, mpsc::UnboundedSender};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
struct LanSpreadState { struct LanSpreadState {
server_addr: Mutex<Option<SocketAddr>>, server_addr: Mutex<Option<SocketAddr>>,
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_folder: Arc<Mutex<String>>, games_folder: Arc<Mutex<String>>,
} }
#[tauri::command] #[tauri::command]
fn request_games(state: tauri::State<LanSpreadState>) { fn request_games(state: tauri::State<LanSpreadState>) {
log::debug!("request_games"); log::debug!("request_games");
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::ListGames) { if let Err(e) = state.inner().client_ctrl.send(ClientCommand::ListGames) {
log::error!("Failed to send message to client: {e:?}"); log::error!("Failed to send message to client: {e:?}");
} }
} }
#[tauri::command] #[tauri::command]
fn install_game(id: String, state: tauri::State<LanSpreadState>) -> bool { fn install_game(id: String, state: tauri::State<LanSpreadState>) -> bool {
let already_in_download = tauri::async_runtime::block_on(async { let already_in_download = tauri::async_runtime::block_on(async {
if state.inner().games_in_download.lock().await.contains(&id) { if state.inner().games_in_download.lock().await.contains(&id) {
log::warn!("Game is already downloading: {id}"); log::warn!("Game is already downloading: {id}");
return true; return true;
} }
false false
}); });
if already_in_download { if already_in_download {
return false; return false;
} }
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) { if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) {
log::error!("Failed to send message to client: {e:?}"); log::error!("Failed to send message to client: {e:?}");
} }
true true
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn run_as_admin(file: &str, params: &str, dir: &str) -> bool { fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
use std::{ffi::OsStr, os::windows::ffi::OsStrExt}; use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
use windows::{Win32::UI::Shell::ShellExecuteW, core::PCWSTR}; use windows::{Win32::UI::Shell::ShellExecuteW, core::PCWSTR};
let file_wide: Vec<u16> = OsStr::new(file).encode_wide().chain(Some(0)).collect(); 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 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 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 runas_wide: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
let result = unsafe { let result = unsafe {
ShellExecuteW( ShellExecuteW(
None, None,
PCWSTR::from_raw(runas_wide.as_ptr()), PCWSTR::from_raw(runas_wide.as_ptr()),
PCWSTR::from_raw(file_wide.as_ptr()), PCWSTR::from_raw(file_wide.as_ptr()),
PCWSTR::from_raw(params_wide.as_ptr()), PCWSTR::from_raw(params_wide.as_ptr()),
PCWSTR::from_raw(dir_wide.as_ptr()), PCWSTR::from_raw(dir_wide.as_ptr()),
windows::Win32::UI::WindowsAndMessaging::SW_HIDE, windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
) )
}; };
(result.0 as usize) > 32 // Success if greater than 32 (result.0 as usize) > 32 // Success if greater than 32
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn run_game_windows(id: String, state: tauri::State<LanSpreadState>) { fn run_game_windows(id: String, state: tauri::State<LanSpreadState>) {
use std::fs::File; use std::fs::File;
const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done"; const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done";
let games_folder = let games_folder =
tauri::async_runtime::block_on(async { state.inner().games_folder.lock().await.clone() }); tauri::async_runtime::block_on(async { state.inner().games_folder.lock().await.clone() });
let games_folder = PathBuf::from(games_folder); let games_folder = PathBuf::from(games_folder);
if !games_folder.exists() { if !games_folder.exists() {
log::error!("games_folder {games_folder:?} does not exist"); log::error!("games_folder {games_folder:?} does not exist");
return; return;
} }
let game_path = games_folder.join(id.clone()); let game_path = games_folder.join(id.clone());
let game_setup_bin = game_path.join("game_setup.cmd"); let game_setup_bin = game_path.join("game_setup.cmd");
let game_start_bin = game_path.join("game_start.cmd"); let game_start_bin = game_path.join("game_start.cmd");
let first_start_done_file = game_path.join(FIRST_START_DONE_FILE); let first_start_done_file = game_path.join(FIRST_START_DONE_FILE);
if !first_start_done_file.exists() && game_setup_bin.exists() { if !first_start_done_file.exists() && game_setup_bin.exists() {
let result = run_as_admin( let result = run_as_admin(
"cmd.exe", "cmd.exe",
&format!( &format!(
r#"/c "{} local {} de playername""#, r#"/c "{} local {} de playername""#,
game_setup_bin.display(), game_setup_bin.display(),
&id &id
), ),
&game_path.display().to_string(), &game_path.display().to_string(),
); );
if !result { if !result {
log::error!("failed to run game_setup.cmd"); log::error!("failed to run game_setup.cmd");
return; return;
} }
if let Err(e) = File::create(&first_start_done_file) { if let Err(e) = File::create(&first_start_done_file) {
log::error!("failed to create {first_start_done_file:?}: {e}"); log::error!("failed to create {first_start_done_file:?}: {e}");
} }
} }
if game_start_bin.exists() { if game_start_bin.exists() {
let result = run_as_admin( let result = run_as_admin(
"cmd.exe", "cmd.exe",
&format!( &format!(
r#"/c "{} local {} de playername""#, r#"/c "{} local {} de playername""#,
game_start_bin.display(), game_start_bin.display(),
&id &id
), ),
&game_path.display().to_string(), &game_path.display().to_string(),
); );
if !result { if !result {
log::error!("failed to run game_start.cmd"); log::error!("failed to run game_start.cmd");
} }
} }
} }
#[tauri::command] #[tauri::command]
fn run_game(id: String, state: tauri::State<LanSpreadState>) { fn run_game(id: String, state: tauri::State<LanSpreadState>) {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
run_game_windows(id, state); run_game_windows(id, state);
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
log::error!("run_game not implemented for this platform: id={id}, state={state:?}"); 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) { 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) = path.file_name() {
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::debug!("Game is installed: {game}"); log::debug!("Game is installed: {game}");
} else { } else {
log::error!("Game is missing: {game}"); log::error!("Game is missing: {game}");
} }
game.installed = installed; game.installed = 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}"); 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 { tauri::async_runtime::block_on(async {
let mut games_folder = app_handle let mut games_folder = app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.inner() .inner()
.games_folder .games_folder
.lock() .lock()
.await; .await;
*games_folder = path.clone(); *games_folder = 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");
} }
let entries = match path.read_dir() { let entries = match path.read_dir() {
Ok(entries) => entries, Ok(entries) => entries,
Err(e) => { Err(e) => {
log::error!("Failed to read game dir: {e}"); log::error!("Failed to read game dir: {e}");
return; return;
} }
}; };
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let mut game_db = app_handle let mut game_db = app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.inner() .inner()
.games .games
.lock() .lock()
.await; .await;
// Reset all games to uninstalled // Reset all games to uninstalled
game_db.set_all_uninstalled(); game_db.set_all_uninstalled();
// update game_db with installed games from real game directory // update game_db with installed games from real game directory
entries.into_iter().for_each(|entry| { entries.into_iter().for_each(|entry| {
if let Ok(entry) = entry { if let Ok(entry) = entry {
if let Ok(path_type) = entry.file_type() { if let Ok(path_type) = entry.file_type() {
if path_type.is_dir() { if path_type.is_dir() {
let path = entry.path(); let path = entry.path();
if path.join("version.ini").exists() { if path.join("version.ini").exists() {
set_game_install_state_from_path(&mut game_db, &path, true); 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())) { 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}"); log::error!("Failed to emit games-list-updated event: {e}");
} }
}); });
} }
async fn find_server(app: AppHandle) { async fn find_server(app: AppHandle) {
log::info!("Looking for server..."); log::info!("Looking for server...");
loop { loop {
match discover_service(LANSPREAD_SERVICE_TYPE, Some(LANSPREAD_INSTANCE_NAME)) { match discover_service(LANSPREAD_SERVICE_TYPE, Some(LANSPREAD_INSTANCE_NAME)) {
Ok(server_addr) => { Ok(server_addr) => {
log::info!("Found server at {server_addr}"); log::info!("Found server at {server_addr}");
let state: tauri::State<LanSpreadState> = app.state(); let state: tauri::State<LanSpreadState> = app.state();
{ {
// mutex scope // mutex scope
let mut addr = state.server_addr.lock().await; let mut addr = state.server_addr.lock().await;
*addr = Some(server_addr); *addr = Some(server_addr);
} }
state state
.client_ctrl .client_ctrl
.send(ClientCommand::ServerAddr(server_addr)) .send(ClientCommand::ServerAddr(server_addr))
.unwrap(); .unwrap();
request_games(state); request_games(state);
break; break;
} }
Err(e) => { Err(e) => {
log::warn!("Failed to find server: {e} - retrying..."); log::warn!("Failed to find server: {e} - retrying...");
} }
} }
} }
} }
async fn update_game_db(games: Vec<Game>, app: AppHandle) { async fn update_game_db(games: Vec<Game>, app: AppHandle) {
for game in &games { for game in &games {
log::trace!("client event ListGames iter: {game:?}"); log::trace!("client event ListGames iter: {game:?}");
} }
let state = app.state::<LanSpreadState>(); let state = app.state::<LanSpreadState>();
// Store games list // Store games list
let mut state_games = state.games.lock().await; let mut state_games = state.games.lock().await;
*state_games = GameDB::from(games.clone()); *state_games = GameDB::from(games.clone());
// Tell Frontend about new games list // Tell Frontend about new games list
if let Err(e) = app.emit("games-list-updated", Some(games)) { if let Err(e) = app.emit("games-list-updated", Some(games)) {
log::error!("Failed to emit games-list-updated event: {e}"); log::error!("Failed to emit games-list-updated event: {e}");
} else { } else {
log::info!("Emitted games-list-updated event"); log::info!("Emitted games-list-updated event");
} }
} }
fn add_final_slash(path: &str) -> String { fn add_final_slash(path: &str) -> String {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
const SLASH_CHAR: char = '\\'; const SLASH_CHAR: char = '\\';
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
const SLASH_CHAR: char = '/'; const SLASH_CHAR: char = '/';
if path.ends_with(SLASH_CHAR) { if path.ends_with(SLASH_CHAR) {
path.to_string() path.to_string()
} else { } else {
format!("{path}{SLASH_CHAR}") format!("{path}{SLASH_CHAR}")
} }
} }
async fn do_unrar(sidecar: Command, rar_file: &Path, dest_dir: &Path) -> eyre::Result<()> { 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(()) = std::fs::create_dir_all(dest_dir) {
if let Ok(rar_file) = rar_file.canonicalize() { if let Ok(rar_file) = rar_file.canonicalize() {
if let Ok(dest_dir) = dest_dir.canonicalize() { if let Ok(dest_dir) = dest_dir.canonicalize() {
let dest_dir = dest_dir let dest_dir = dest_dir
.to_str() .to_str()
.ok_or_else(|| eyre::eyre!("failed to get str of dest_dir"))?; .ok_or_else(|| eyre::eyre!("failed to get str of dest_dir"))?;
log::info!( log::info!(
"unrar game: {} to {}", "unrar game: {} to {}",
rar_file.canonicalize()?.display(), rar_file.canonicalize()?.display(),
dest_dir dest_dir
); );
let out = sidecar let out = sidecar
.arg("x") // extract files .arg("x") // extract files
.arg(rar_file.canonicalize()?) .arg(rar_file.canonicalize()?)
.arg("-y") // Assume Yes on all queries .arg("-y") // Assume Yes on all queries
.arg("-o") // Set overwrite mode .arg("-o") // Set overwrite mode
.arg(add_final_slash(dest_dir)) .arg(add_final_slash(dest_dir))
.output() .output()
.await?; .await?;
if !out.status.success() { if !out.status.success() {
log::error!("unrar stderr: {}", String::from_utf8_lossy(&out.stderr)); log::error!("unrar stderr: {}", String::from_utf8_lossy(&out.stderr));
} }
return Ok(()); return Ok(());
} else { } else {
log::error!("dest_dir canonicalize failed: {:?}", &dest_dir); log::error!("dest_dir canonicalize failed: {:?}", &dest_dir);
} }
} else { } else {
log::error!("rar_file canonicalize failed: {:?}", &rar_file); log::error!("rar_file canonicalize failed: {:?}", &rar_file);
} }
} else { } else {
log::error!("failed to create dest_dir: {:?}", &dest_dir); log::error!("failed to create dest_dir: {:?}", &dest_dir);
} }
bail!("failed to create directory: {dest_dir:?}"); bail!("failed to create directory: {dest_dir:?}");
} }
async fn unpack_game(id: &str, sidecar: Command, games_folder: String) { async fn unpack_game(id: &str, sidecar: Command, games_folder: String) {
let game_path = PathBuf::from(games_folder).join(id); let game_path = PathBuf::from(games_folder).join(id);
let eti_rar = game_path.join(format!("{id}.eti")); let eti_rar = game_path.join(format!("{id}.eti"));
let local_path = game_path.join("local"); let local_path = game_path.join("local");
if let Err(e) = do_unrar(sidecar, &eti_rar, &local_path).await { if let Err(e) = do_unrar(sidecar, &eti_rar, &local_path).await {
log::error!("{eti_rar:?} -> {local_path:?}: {e}"); log::error!("{eti_rar:?} -> {local_path:?}: {e}");
} }
} }
#[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()
.clear_targets() .clear_targets()
.target(tauri_plugin_log::Target::new( .target(tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::Stdout, tauri_plugin_log::TargetKind::Stdout,
)) ))
.level(log::LevelFilter::Info) .level(log::LevelFilter::Info)
.level_for("mdns_sd::service_daemon", log::LevelFilter::Off); .level_for("mdns_sd::service_daemon", log::LevelFilter::Off);
// channel to pass commands to the client // channel to pass commands to the client
let (tx_client_control, rx_client_control) = let (tx_client_control, rx_client_control) =
tokio::sync::mpsc::unbounded_channel::<ClientCommand>(); tokio::sync::mpsc::unbounded_channel::<ClientCommand>();
// channel to receive events from the client // channel to receive events from the client
let (tx_client_event, mut rx_client_event) = let (tx_client_event, mut rx_client_event) =
tokio::sync::mpsc::unbounded_channel::<ClientEvent>(); tokio::sync::mpsc::unbounded_channel::<ClientEvent>();
let lanspread_state = LanSpreadState { let lanspread_state = LanSpreadState {
server_addr: Mutex::new(None), server_addr: Mutex::new(None),
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_folder: Arc::new(Mutex::new("".to_string())), games_folder: Arc::new(Mutex::new("".to_string())),
}; };
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_logger_builder.build()) .plugin(tauri_logger_builder.build())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
request_games, request_games,
install_game, install_game,
run_game, run_game,
update_game_directory update_game_directory
]) ])
.manage(lanspread_state) .manage(lanspread_state)
.setup(|app| { .setup(|app| {
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
// discover server // discover server
tauri::async_runtime::spawn(async move { find_server(app_handle).await }); tauri::async_runtime::spawn(async move { find_server(app_handle).await });
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
lanspread_client::run(rx_client_control, tx_client_event).await lanspread_client::run(rx_client_control, tx_client_event).await
}); });
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
while let Some(event) = rx_client_event.recv().await { while let Some(event) = rx_client_event.recv().await {
match event { match event {
ClientEvent::ListGames(games) => { ClientEvent::ListGames(games) => {
log::info!("ClientEvent::ListGames received"); log::info!("ClientEvent::ListGames received");
update_game_db(games, app_handle.clone()).await; update_game_db(games, app_handle.clone()).await;
} }
ClientEvent::GotGameFiles { id, file_descriptions } => { ClientEvent::GotGameFiles { id, file_descriptions } => {
log::info!("ClientEvent::GotGameFiles received"); log::info!("ClientEvent::GotGameFiles received");
if let Err(e) = app_handle.emit( if let Err(e) = app_handle.emit(
"game-download-pre", "game-download-pre",
Some(id.clone()), Some(id.clone()),
) { ) {
log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}"); log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}");
} }
app_handle app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.inner() .inner()
.client_ctrl .client_ctrl
.send(ClientCommand::DownloadGameFiles{ .send(ClientCommand::DownloadGameFiles{
id, id,
file_descriptions, file_descriptions,
}) })
.unwrap(); .unwrap();
} }
ClientEvent::DownloadGameFilesBegin { id } => { ClientEvent::DownloadGameFilesBegin { id } => {
log::info!("ClientEvent::DownloadGameFilesBegin received"); log::info!("ClientEvent::DownloadGameFilesBegin received");
app_handle app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.inner() .inner()
.games_in_download .games_in_download
.lock() .lock()
.await .await
.insert(id.clone()); .insert(id.clone());
if let Err(e) = app_handle.emit("game-download-begin", Some(id)) { if let Err(e) = app_handle.emit("game-download-begin", Some(id)) {
log::error!("ClientEvent::DownloadGameFilesBegin: Failed to emit game-download-begin event: {e}"); log::error!("ClientEvent::DownloadGameFilesBegin: Failed to emit game-download-begin event: {e}");
} }
} }
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.clone())) { 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 app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.inner() .inner()
.games_in_download .games_in_download
.lock() .lock()
.await .await
.remove(&id.clone()); .remove(&id.clone());
let games_folder = app_handle let games_folder = app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.inner() .inner()
.games_folder .games_folder
.lock() .lock()
.await .await
.clone(); .clone();
if let Ok(sidecar) = app_handle.shell().sidecar("unrar") { if let Ok(sidecar) = app_handle.shell().sidecar("unrar") {
unpack_game(&id, sidecar, games_folder).await; unpack_game(&id, sidecar, games_folder).await;
log::info!("ClientEvent::UnpackGameFinished received"); log::info!("ClientEvent::UnpackGameFinished received");
if let Err(e) = app_handle.emit("game-unpack-finished", Some(id.clone())) { 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}"); log::error!("ClientEvent::UnpackGameFinished: Failed to emit game-unpack-finished event: {e}");
} }
} }
} }
ClientEvent::DownloadGameFilesFailed { id } => { ClientEvent::DownloadGameFilesFailed { id } => {
log::warn!("ClientEvent::DownloadGameFilesFailed received"); log::warn!("ClientEvent::DownloadGameFilesFailed received");
if let Err(e) = app_handle.emit("game-download-failed", Some(id.clone())) { if let Err(e) = app_handle.emit("game-download-failed", Some(id.clone())) {
log::error!("Failed to emit game-download-failed event: {e}"); log::error!("Failed to emit game-download-failed event: {e}");
} }
app_handle app_handle
.state::<LanSpreadState>() .state::<LanSpreadState>()
.inner() .inner()
.games_in_download .games_in_download
.lock() .lock()
.await .await
.remove(&id.clone()); .remove(&id.clone());
}, },
} }
} }
}); });
Ok(()) Ok(())
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }