//! Command handlers for peer commands. use std::{net::SocketAddr, path::PathBuf, sync::Arc}; use lanspread_db::db::GameFileDescription; use tokio::sync::{RwLock, mpsc::UnboundedSender}; use crate::{ PeerEvent, context::Ctx, download::download_game_files, events, identity::FEATURE_LIBRARY_DELTA, local_games::{ LocalLibraryScan, get_game_file_descriptions, local_download_available, scan_local_library, }, network::{announce_games_to_peer, request_game_details_from_peer, send_library_delta}, peer_db::PeerGameDB, remote_peer::ensure_peer_id_for_addr, }; // ============================================================================= // Command handlers // ============================================================================= /// Handles the `ListGames` command. pub async fn handle_list_games_command(ctx: &Ctx, tx_notify_ui: &UnboundedSender) { log::info!("ListGames command received"); events::emit_peer_game_list(&ctx.peer_game_db, tx_notify_ui).await; } /// Tries to serve a game from local files. async fn try_serve_local_game( ctx: &Ctx, tx_notify_ui: &UnboundedSender, id: &str, ) -> bool { let game_dir = { ctx.game_dir.read().await.clone() }; let downloading = ctx.downloading_games.read().await; if !local_download_available(&game_dir, id, &downloading).await { return false; } drop(downloading); match get_game_file_descriptions(id, &game_dir).await { Ok(file_descriptions) => { log::info!("Serving game {id} from local files"); if let Err(e) = tx_notify_ui.send(PeerEvent::GotGameFiles { id: id.to_string(), file_descriptions, }) { log::error!("Failed to send GotGameFiles event: {e}"); } true } Err(e) => { log::error!("Failed to enumerate local file descriptions for {id}: {e}"); false } } } /// Handles the `GetGame` command. pub async fn handle_get_game_command( ctx: &Ctx, tx_notify_ui: &UnboundedSender, id: String, ) { if try_serve_local_game(ctx, tx_notify_ui, &id).await { return; } log::info!("Requesting game from peers: {id}"); let peers = { ctx.peer_game_db.read().await.peers_with_game(&id) }; if peers.is_empty() { log::warn!("No peers have game {id}"); if let Err(e) = tx_notify_ui.send(PeerEvent::NoPeersHaveGame { id: id.clone() }) { log::error!("Failed to send NoPeersHaveGame event: {e}"); } return; } let peer_game_db = ctx.peer_game_db.clone(); let tx_notify_ui = tx_notify_ui.clone(); tokio::spawn(async move { let mut fetched_any = false; for peer_addr in peers { match request_game_details_and_update(peer_addr, &id, peer_game_db.clone()).await { Ok(_) => { log::info!("Fetched game file list for {id} from peer {peer_addr}"); fetched_any = true; } Err(e) => { log::error!("Failed to fetch game files for {id} from {peer_addr}: {e}"); } } } if fetched_any { let aggregated_files = { peer_game_db.read().await.aggregated_game_files(&id) }; if let Err(e) = tx_notify_ui.send(PeerEvent::GotGameFiles { id: id.clone(), file_descriptions: aggregated_files, }) { log::error!("Failed to send GotGameFiles event: {e}"); } } else { log::warn!("Failed to retrieve game files for {id} from any peer"); } }); } /// Requests game details from a peer and updates the peer game database. async fn request_game_details_and_update( peer_addr: SocketAddr, game_id: &str, peer_game_db: Arc>, ) -> eyre::Result> { let (file_descriptions, _) = request_game_details_from_peer(peer_addr, game_id).await?; let peer_id = ensure_peer_id_for_addr(&peer_game_db, peer_addr).await; { let mut db = peer_game_db.write().await; db.update_peer_game_files(&peer_id, game_id, file_descriptions.clone()); } Ok(file_descriptions) } /// Handles the `DownloadGameFiles` command. #[allow(clippy::too_many_lines)] pub async fn handle_download_game_files_command( ctx: &Ctx, tx_notify_ui: &UnboundedSender, id: String, file_descriptions: Vec, ) { log::info!("Got PeerCommand::DownloadGameFiles"); let games_folder = { ctx.game_dir.read().await.clone() }; // Use majority validation to get trusted file descriptions and peer whitelist let (validated_descriptions, peer_whitelist, file_peer_map) = { match ctx .peer_game_db .read() .await .validate_file_sizes_majority(&id) { Ok((files, peers, file_peer_map)) => { log::info!( "Majority validation: {} validated files, {} trusted peers for game {id}", files.len(), peers.len() ); (files, peers, file_peer_map) } Err(e) => { log::error!("File size majority validation failed for {id}: {e}"); if let Err(send_err) = tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed { id: id.clone() }) { log::error!("Failed to send DownloadGameFilesFailed event: {send_err}"); } return; } } }; let resolved_descriptions = if file_descriptions.is_empty() { validated_descriptions } else { // If user provided specific descriptions, still validate them against majority // but keep user's selection (they might want specific files) file_descriptions }; if resolved_descriptions.is_empty() { log::error!( "No validated file descriptions available to download game {id}; request metadata first" ); return; } let local_dl_available = { let downloading = ctx.downloading_games.read().await; local_download_available(&games_folder, &id, &downloading).await }; if peer_whitelist.is_empty() { if local_dl_available { log::info!("Using locally downloaded files for game {id}; skipping peer transfer"); if let Err(e) = tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin { id: id.clone() }) { log::error!("Failed to send DownloadGameFilesBegin event: {e}"); } if let Err(e) = tx_notify_ui.send(PeerEvent::DownloadGameFilesFinished { id: id.clone() }) { log::error!("Failed to send DownloadGameFilesFinished event: {e}"); } } else { log::error!("No trusted peers available after majority validation for game {id}"); } return; } { let mut in_progress = ctx.downloading_games.write().await; if !in_progress.insert(id.clone()) { log::warn!("Download for {id} already in progress; ignoring new request"); return; } } let downloading_games = ctx.downloading_games.clone(); let active_downloads = ctx.active_downloads.clone(); let tx_notify_ui_clone = tx_notify_ui.clone(); let download_id = id.clone(); let handle = tokio::spawn(async move { let result = download_game_files( &download_id, resolved_descriptions, games_folder, peer_whitelist, file_peer_map, tx_notify_ui_clone.clone(), ) .await; { let mut guard = downloading_games.write().await; guard.remove(&download_id); } if let Err(e) = result { log::error!("Download failed for {download_id}: {e}"); if let Err(send_err) = tx_notify_ui_clone.send(PeerEvent::DownloadGameFilesFailed { id: download_id.clone(), }) { log::error!("Failed to send DownloadGameFilesFailed event: {send_err}"); } } let _ = active_downloads.write().await.remove(&download_id); }); ctx.active_downloads.write().await.insert(id, handle); } /// Handles the `SetGameDir` command. pub async fn handle_set_game_dir_command( ctx: &Ctx, tx_notify_ui: &UnboundedSender, game_dir: PathBuf, ) { *ctx.game_dir.write().await = game_dir.clone(); log::info!("Game directory set to: {}", game_dir.display()); let tx_notify_ui = tx_notify_ui.clone(); let ctx_clone = ctx.clone(); tokio::spawn(async move { match load_local_library(&ctx_clone, &tx_notify_ui).await { Ok(()) => log::info!("Local game database loaded successfully"), Err(e) => { log::error!("Failed to load local game database: {e}"); } } }); } /// Loads the configured local library and announces the result. pub async fn load_local_library( ctx: &Ctx, tx_notify_ui: &UnboundedSender, ) -> eyre::Result<()> { let game_dir = { ctx.game_dir.read().await.clone() }; let scan = scan_local_library(&game_dir).await?; update_and_announce_games(ctx, tx_notify_ui, scan).await; Ok(()) } /// Handles the `GetPeerCount` command. pub async fn handle_get_peer_count_command(ctx: &Ctx, tx_notify_ui: &UnboundedSender) { log::info!("GetPeerCount command received"); events::emit_peer_count(&ctx.peer_game_db, tx_notify_ui).await; } // ============================================================================= // Game announcement helpers // ============================================================================= /// Updates the local game database and announces changes to peers. pub async fn update_and_announce_games( ctx: &Ctx, tx_notify_ui: &UnboundedSender, scan: LocalLibraryScan, ) { let LocalLibraryScan { game_db, summaries, revision, } = scan; let delta = { let mut library_guard = ctx.local_library.write().await; library_guard.update_from_scan(summaries, revision) }; let Some(delta) = delta else { return; }; { let mut db_guard = ctx.local_game_db.write().await; *db_guard = Some(game_db.clone()); } let all_games = game_db.all_games().into_iter().cloned().collect::>(); if let Err(e) = tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games.clone())) { log::error!("Failed to send LocalGamesUpdated event: {e}"); } let peer_targets = { let db = ctx.peer_game_db.read().await; db.peer_identities() .into_iter() .map(|(peer_id, addr)| { let features = db.peer_features(&peer_id); (peer_id, addr, features) }) .collect::>() }; for (_peer_id, peer_addr, features) in peer_targets { if features .iter() .any(|feature| feature == FEATURE_LIBRARY_DELTA) { let delta = delta.clone(); tokio::spawn(async move { if let Err(e) = send_library_delta(peer_addr, delta).await { log::warn!("Failed to send library delta to {peer_addr}: {e}"); } }); } else { let games_clone = all_games.clone(); tokio::spawn(async move { if let Err(e) = announce_games_to_peer(peer_addr, games_clone).await { log::warn!("Failed to announce games to {peer_addr}: {e}"); } }); } } }