From 8fe68f95748b2ca19b7fa3f17037ccdee16dc6d2 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 13 Nov 2025 21:43:20 +0100 Subject: [PATCH] wip --- crates/lanspread-compat/src/eti.rs | 1 + crates/lanspread-db/src/db.rs | 5 + crates/lanspread-peer/src/lib.rs | 36 ++++- .../src-tauri/src/lib.rs | 133 +++++++++--------- crates/lanspread-tauri-deno-ts/src/App.tsx | 47 +++++-- 5 files changed, 142 insertions(+), 80 deletions(-) diff --git a/crates/lanspread-compat/src/eti.rs b/crates/lanspread-compat/src/eti.rs index 00ea833..4a7382c 100644 --- a/crates/lanspread-compat/src/eti.rs +++ b/crates/lanspread-compat/src/eti.rs @@ -61,6 +61,7 @@ impl From for Game { #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64, thumbnail: None, + downloaded: false, installed: false, eti_game_version: None, local_version: None, diff --git a/crates/lanspread-db/src/db.rs b/crates/lanspread-db/src/db.rs index d8f778d..491c485 100644 --- a/crates/lanspread-db/src/db.rs +++ b/crates/lanspread-db/src/db.rs @@ -54,6 +54,9 @@ pub struct Game { pub size: u64, /// thumbnail image pub thumbnail: Option, + /// indicates that the ETI bundle exists locally + #[serde(default)] + pub downloaded: bool, /// only relevant for client (yeah... I know) pub installed: bool, /// ETI game version from version.ini (YYYYMMDD format) (server) @@ -165,7 +168,9 @@ impl GameDB { pub fn set_all_uninstalled(&mut self) { for game in self.games.values_mut() { + game.downloaded = false; game.installed = false; + game.local_version = None; } } } diff --git a/crates/lanspread-peer/src/lib.rs b/crates/lanspread-peer/src/lib.rs index 6f588cc..6c38666 100644 --- a/crates/lanspread-peer/src/lib.rs +++ b/crates/lanspread-peer/src/lib.rs @@ -1111,7 +1111,16 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result { if path.is_dir() && let Some(game_id) = path.file_name().and_then(|n| n.to_str()) { - // Check if this game has a version.ini file + let eti_path = path.join(format!("{game_id}.eti")); + let downloaded = tokio::fs::metadata(&eti_path).await.is_ok(); + if !downloaded { + continue; + } + + if !local_dir_has_content(&path).await { + continue; + } + if let Ok(version) = lanspread_db::db::read_version_from_ini(&path) { let size = calculate_directory_size(&path).await?; let game = Game { @@ -1125,6 +1134,7 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result { genre: String::new(), size, thumbnail: None, + downloaded, installed: true, eti_game_version: version.clone(), local_version: version, @@ -1138,6 +1148,30 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result { Ok(GameDB::from(games)) } +async fn local_dir_has_content(path: &Path) -> bool { + let local_dir = path.join("local"); + if tokio::fs::metadata(&local_dir).await.is_err() { + return false; + } + + let mut entries = match tokio::fs::read_dir(&local_dir).await { + Ok(entries) => entries, + Err(e) => { + log::warn!("Failed to read local dir {}: {e}", local_dir.display()); + return false; + } + }; + + match entries.next_entry().await { + Ok(Some(_)) => true, + Ok(None) => false, + Err(e) => { + log::warn!("Failed to iterate local dir {}: {e}", local_dir.display()); + false + } + } +} + async fn calculate_directory_size(dir: &Path) -> eyre::Result { let mut total_size = 0u64; let mut entries = tokio::fs::read_dir(dir).await?; 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 ba476a9..135152a 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -233,10 +233,6 @@ async fn run_game_windows( id: String, state: tauri::State<'_, LanSpreadState>, ) -> tauri::Result<()> { - use std::fs::File; - - const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done"; - let games_folder_lock = state.inner().games_folder.clone(); let games_folder = { let guard = games_folder_lock.read().await; @@ -254,8 +250,8 @@ async fn run_game_windows( 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 local_ready = local_install_is_ready(&game_path); + if !local_ready && game_setup_bin.exists() { let result = run_as_admin( "cmd.exe", &format!( @@ -270,10 +266,6 @@ async fn run_game_windows( log::error!("failed to run game_setup.cmd"); return Ok(()); } - - 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() { @@ -311,31 +303,71 @@ async fn run_game(id: String, state: tauri::State<'_, LanSpreadState>) -> tauri: Ok(()) } -fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed: bool) { - if let Some(file_name) = path.file_name() - && let Some(file_name) = file_name.to_str() - && let Some(game) = game_db.get_mut_game_by_id(file_name) - { - if installed { - log::debug!("Set {game} to installed"); - } else { - log::debug!("Set {game} to uninstalled"); - } - game.installed = installed; +fn eti_package_exists(game_path: &Path, game_id: &str) -> bool { + game_path.is_dir() && game_path.join(format!("{game_id}.eti")).is_file() +} - // Read local version.ini if installed - if installed { - if let Ok(version) = lanspread_db::db::read_version_from_ini(path) { +fn local_install_is_ready(game_path: &Path) -> bool { + let local_dir = game_path.join("local"); + if !local_dir.is_dir() { + return false; + } + + match std::fs::read_dir(&local_dir) { + Ok(mut entries) => match entries.next() { + Some(Ok(_)) => true, + Some(Err(e)) => { + log::warn!( + "Failed to inspect entry in local dir {}: {e}", + local_dir.display() + ); + false + } + None => false, + }, + Err(e) => { + log::warn!( + "Failed to enumerate local dir for game {}: {e}", + game_path.display() + ); + false + } + } +} + +fn update_game_installation_state(game: &mut Game, games_root: &Path) { + let game_path = games_root.join(&game.id); + if !game_path.is_dir() { + return; + } + + let downloaded = eti_package_exists(&game_path, &game.id); + game.downloaded = downloaded; + + let installed = downloaded && local_install_is_ready(&game_path); + game.installed = installed; + + if installed { + log::debug!("Set {game} to installed"); + match lanspread_db::db::read_version_from_ini(&game_path) { + Ok(version) => { game.local_version = version; if let Some(ref version) = game.local_version { log::debug!("Read local version for game {}: {}", game.id, version); } - } else { - log::warn!("Failed to read local version.ini for game: {}", game.id); } - } else { - // Clear local version when uninstalled - game.local_version = None; + Err(e) => { + log::warn!("Failed to read local version.ini for game {}: {e}", game.id); + game.local_version = None; + } + } + } else { + game.local_version = None; + if downloaded { + log::debug!( + "Game {} is downloaded but awaiting local install contents", + game.id + ); } } } @@ -355,41 +387,6 @@ async fn refresh_games_list(app_handle: &AppHandle) { let path = PathBuf::from(&games_folder); - let mut installed_paths: Vec = Vec::new(); - if path.exists() { - match std::fs::read_dir(&path) { - Ok(entries) => { - for entry_result in entries { - match entry_result { - Ok(entry) => match entry.file_type() { - Ok(file_type) if file_type.is_dir() => { - let dir_path = entry.path(); - if dir_path.join("version.ini").exists() { - installed_paths.push(dir_path); - } - } - Ok(_) => {} - Err(e) => { - log::warn!( - "Failed to read file type for entry {}: {e}", - entry.path().display() - ); - } - }, - Err(e) => { - log::warn!("Failed to read directory entry in {}: {e}", path.display()); - } - } - } - } - Err(e) => { - log::error!("Failed to read game dir {}: {e}", path.display()); - } - } - } else { - log::error!("game dir {} does not exist", path.display()); - } - let mut game_db = games_db_lock.write().await; if game_db.games.is_empty() { @@ -399,8 +396,12 @@ async fn refresh_games_list(app_handle: &AppHandle) { game_db.set_all_uninstalled(); - for dir_path in &installed_paths { - set_game_install_state_from_path(&mut game_db, dir_path, true); + if path.exists() { + for game in game_db.games.values_mut() { + update_game_installation_state(game, &path); + } + } else { + log::error!("game dir {} does not exist", path.display()); } let games_to_emit = game_db diff --git a/crates/lanspread-tauri-deno-ts/src/App.tsx b/crates/lanspread-tauri-deno-ts/src/App.tsx index 850804a..424d8c4 100644 --- a/crates/lanspread-tauri-deno-ts/src/App.tsx +++ b/crates/lanspread-tauri-deno-ts/src/App.tsx @@ -29,6 +29,7 @@ interface Game { description: string; size: number; thumbnail: Uint8Array | number[]; + downloaded: boolean; installed: boolean; install_status: InstallStatus; eti_game_version?: string; @@ -49,7 +50,7 @@ const App = () => { switch (filter) { case 'available': // Show union of installed games and games with peers - return games.filter(game => game.installed || game.peer_count > 0); + return games.filter(game => game.installed || game.downloaded || game.peer_count > 0); case 'installed': return games.filter(game => game.installed); case 'all': @@ -381,6 +382,36 @@ const App = () => { return false; }; + const getInProgressLabel = (game: Game): string | undefined => { + switch (game.install_status) { + case InstallStatus.CheckingPeers: + return 'Checking peers...'; + case InstallStatus.Downloading: + return 'Downloading...'; + case InstallStatus.Unpacking: + return 'Unpacking...'; + default: + return undefined; + } + }; + + const getActionLabel = (game: Game): string => { + const inProgress = getInProgressLabel(game); + if (inProgress) { + return inProgress; + } + + if (!game.installed) { + return game.downloaded ? 'Install' : 'Download'; + } + + if (needsUpdate(game)) { + return 'Update'; + } + + return 'Play'; + }; + const dialogGameDir = async () => { const file = await open({ multiple: false, @@ -484,21 +515,11 @@ const App = () => { runGame(item.id); } }}> - {!item.installed - ? item.install_status === InstallStatus.CheckingPeers ? 'Checking peers...' - : item.install_status === InstallStatus.Downloading ? 'Downloading...' - : item.install_status === InstallStatus.Unpacking ? 'Unpacking...' - : 'Install' - : needsUpdate(item) - ? item.install_status === InstallStatus.CheckingPeers ? 'Checking peers...' - : item.install_status === InstallStatus.Downloading ? 'Downloading...' - : item.install_status === InstallStatus.Unpacking ? 'Unpacking...' - : 'Update' - : 'Play'} + {getActionLabel(item)}
- {item.status_message && item.peer_count === 0 && !item.installed ? item.status_message : ''} + {item.status_message && item.peer_count === 0 && !item.installed && !item.downloaded ? item.status_message : ''}
{item.peer_count > 0 && (