game sizes?

This commit is contained in:
2025-11-14 08:12:09 +01:00
parent 6eec74f0f6
commit 567d293455
5 changed files with 152 additions and 6 deletions
@@ -45,6 +45,7 @@ tauri-plugin-shell = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-store = { workspace = true }
tokio = { workspace = true }
walkdir = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
@@ -9,7 +9,7 @@ use std::{
use eyre::bail;
use lanspread_compat::eti::get_games;
use lanspread_db::db::{Game, GameDB};
use lanspread_peer::{PeerCommand, PeerEvent, start_peer};
use lanspread_peer::{PeerCommand, PeerEvent, PeerGameDB, start_peer};
use tauri::{AppHandle, Emitter as _, Manager};
use tauri_plugin_shell::{ShellExt, process::Command};
use tokio::sync::{RwLock, mpsc::UnboundedSender};
@@ -21,6 +21,8 @@ struct LanSpreadState {
games: Arc<RwLock<GameDB>>,
games_in_download: Arc<RwLock<HashSet<String>>>,
games_folder: Arc<RwLock<String>>,
// Add access to peer game database for size calculations
peer_game_db: Arc<RwLock<PeerGameDB>>,
}
#[cfg(target_os = "windows")]
@@ -425,6 +427,24 @@ fn update_game_installation_state(game: &mut Game, games_root: &Path) {
let installed = downloaded && local_install_is_ready(&game_path);
game.installed = installed;
// Calculate size from local files if available (highest priority)
if downloaded || installed {
match calculate_directory_size_sync(&game_path) {
Ok(local_size) => {
game.size = local_size;
log::debug!(
"Updated size for game {} from local files: {} bytes",
game.id,
local_size
);
}
Err(e) => {
log::warn!("Failed to calculate local size for game {}: {e}", game.id);
// Keep the existing size (will fallback to peer/game.db later)
}
}
}
if installed {
log::debug!("Set {game} to installed");
match lanspread_db::db::read_version_from_ini(&game_path) {
@@ -450,6 +470,92 @@ fn update_game_installation_state(game: &mut Game, games_root: &Path) {
}
}
/// Synchronous version of `calculate_directory_size` for use in non-async contexts
fn calculate_directory_size_sync(dir: &Path) -> eyre::Result<u64> {
let mut total_size = 0u64;
for entry in walkdir::WalkDir::new(dir) {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let metadata = std::fs::metadata(path)?;
total_size += metadata.len();
}
}
Ok(total_size)
}
/// Calculate total size from a list of file descriptions (used for peer majority calculation)
fn calculate_size_from_file_descriptions(
file_descriptions: &[lanspread_db::db::GameFileDescription],
) -> u64 {
file_descriptions
.iter()
.filter(|desc| !desc.is_dir) // Only count files, not directories
.map(|desc| desc.size)
.sum()
}
/// Update game sizes from peer majority files when local files are not available
/// This implements the second priority: "we don't have the game but 1-n peers have. Use the majority files for that game, calculate a total size"
async fn update_game_sizes_from_peers(
games: &mut std::collections::HashMap<String, Game>,
peer_game_db: &Arc<RwLock<PeerGameDB>>,
) {
log::debug!("Updating game sizes from peer data where local files are not available");
let peer_db = peer_game_db.read().await;
for game in games.values_mut() {
// Only update sizes for games that don't have local files
if !game.downloaded && !game.installed {
// Check if any peers have this game
let peer_files_for_game = peer_db.aggregated_game_files(&game.id);
if peer_files_for_game.is_empty() {
if let Some(peer_size) = peer_db.majority_game_size(&game.id) {
if peer_size > 0 {
game.size = peer_size;
log::debug!(
"Updated size for game {} from peer-reported totals: {} bytes",
game.id,
peer_size
);
} else {
log::debug!(
"Peer-reported size for game {} is 0; keeping previous value",
game.id
);
}
} else {
log::debug!("No peer size data available for game {}", game.id);
// Keep existing size (will fallback to game.db sizes)
}
} else {
// Calculate size from peer majority files
let peer_size = calculate_size_from_file_descriptions(&peer_files_for_game);
if peer_size > 0 {
game.size = peer_size;
log::debug!(
"Updated size for game {} from peer majority files: {} bytes (from {} files)",
game.id,
peer_size,
peer_files_for_game.len()
);
} else {
log::debug!(
"Peer files for game {} exist but calculated size is 0",
game.id
);
}
}
}
}
}
async fn refresh_games_list(app_handle: &AppHandle) {
let state = app_handle.state::<LanSpreadState>();
@@ -565,6 +671,10 @@ async fn update_game_db(games: Vec<Game>, app: AppHandle) {
);
}
}
// Update game sizes from peer data (second priority)
// This will update sizes for games that don't have local files but are available from peers
update_game_sizes_from_peers(&mut game_db.games, &state.peer_game_db).await;
}
refresh_games_list(&app).await;
@@ -686,11 +796,14 @@ pub fn run() {
// channel to receive events from the peer
let (tx_peer_event, mut rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
let lanspread_state = LanSpreadState {
peer_ctrl: Arc::new(RwLock::new(None)),
games: Arc::new(RwLock::new(GameDB::empty())),
games_in_download: Arc::new(RwLock::new(HashSet::new())),
games_folder: Arc::new(RwLock::new(String::new())),
peer_game_db: peer_game_db.clone(),
};
tauri::Builder::default()
@@ -709,10 +822,13 @@ pub fn run() {
.manage(lanspread_state)
.setup({
let tx_peer_event_clone = tx_peer_event.clone();
let peer_game_db_clone = peer_game_db.clone();
move |app| {
// Initialize peer system ONLY when games directory is set (games directory is mandatory)
// But the UI is responsive immediately - no blocking server discovery
let app_handle_clone = app.handle().clone();
let tx_peer_event_for_spawn = tx_peer_event_clone.clone();
let peer_game_db_for_spawn = peer_game_db_clone.clone();
tauri::async_runtime::spawn(async move {
// Wait for games directory to be set by user (this is mandatory)
loop {
@@ -752,7 +868,11 @@ pub fn run() {
refresh_games_list(&app_handle_clone).await;
// Only start peer system when we have a valid games directory
match start_peer(games_folder, tx_peer_event_clone) {
match start_peer(
games_folder,
tx_peer_event_for_spawn.clone(),
peer_game_db_for_spawn.clone(),
) {
Ok(peer_ctrl) => {
let state = app_handle_clone.state::<LanSpreadState>();
*state.peer_ctrl.write().await = Some(peer_ctrl);