detect if a game is deleted, added, modified locally

This commit is contained in:
2025-11-14 01:12:01 +01:00
parent 4764bb9fd3
commit 8432030292
2 changed files with 136 additions and 1 deletions
+93 -1
View File
@@ -5,7 +5,7 @@ mod peer;
use std::{ use std::{
cmp::Reverse, cmp::Reverse,
collections::{HashMap, VecDeque}, collections::{HashMap, HashSet, VecDeque},
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@@ -130,6 +130,7 @@ pub enum PeerEvent {
PeerDiscovered(SocketAddr), PeerDiscovered(SocketAddr),
PeerLost(SocketAddr), PeerLost(SocketAddr),
PeerCountUpdated(usize), PeerCountUpdated(usize),
LocalGamesUpdated(Vec<Game>),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -1265,6 +1266,7 @@ async fn local_download_available(game_dir: &str, game_id: &str) -> bool {
!local_dir_has_content(game_path.as_path()).await !local_dir_has_content(game_path.as_path()).await
} }
#[derive(Clone)]
struct Ctx { struct Ctx {
game_dir: Arc<RwLock<Option<String>>>, game_dir: Arc<RwLock<Option<String>>>,
local_game_db: Arc<RwLock<Option<GameDB>>>, local_game_db: Arc<RwLock<Option<GameDB>>>,
@@ -1345,6 +1347,13 @@ pub async fn run_peer(
run_ping_service(tx_notify_ui_ping, peer_game_db_ping).await; run_ping_service(tx_notify_ui_ping, peer_game_db_ping).await;
}); });
// Start local game directory monitoring task
let tx_notify_ui_monitor = tx_notify_ui.clone();
let ctx_monitor = ctx.clone();
tokio::spawn(async move {
run_local_game_monitor(tx_notify_ui_monitor, ctx_monitor).await;
});
// Handle client commands // Handle client commands
loop { loop {
let Some(cmd) = rx_control.recv().await else { let Some(cmd) = rx_control.recv().await else {
@@ -2170,6 +2179,89 @@ async fn run_ping_service(
} }
} }
/// Monitor local game directory for changes and update the local game database
async fn run_local_game_monitor(tx_notify_ui: UnboundedSender<PeerEvent>, ctx: Ctx) {
log::info!("Starting local game directory monitor (5s interval)");
let mut interval = tokio::time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let game_dir = {
let guard = ctx.game_dir.read().await;
guard.clone()
};
if let Some(ref game_dir) = game_dir {
match scan_local_games(game_dir).await {
Ok(current_games) => {
let local_game_db = ctx.local_game_db.clone();
let mut db_guard = local_game_db.write().await;
let previous_games = db_guard
.as_ref()
.map(|db| db.games.keys().cloned().collect::<HashSet<_>>())
.unwrap_or_default();
let current_game_ids =
current_games.games.keys().cloned().collect::<HashSet<_>>();
// Check if any games were removed
let removed_games: Vec<String> = previous_games
.difference(&current_game_ids)
.cloned()
.collect();
if removed_games.is_empty() {
// Check if any games were added or updated
if previous_games != current_game_ids {
log::debug!(
"Local games directory structure changed, updating database"
);
*db_guard = Some(current_games);
let all_games = db_guard
.as_ref()
.map(|db| {
db.all_games().into_iter().cloned().collect::<Vec<Game>>()
})
.unwrap_or_default();
if let Err(e) =
tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games))
{
log::error!("Failed to send LocalGamesUpdated event: {e}");
}
}
} else {
log::info!("Detected removed games: {removed_games:?}");
*db_guard = Some(current_games);
// Notify UI about the change
let all_games = db_guard
.as_ref()
.map(|db| db.all_games().into_iter().cloned().collect::<Vec<Game>>())
.unwrap_or_default();
if let Err(e) = tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games)) {
log::error!("Failed to send LocalGamesUpdated event: {e}");
}
}
}
Err(e) => {
log::error!("Failed to scan local games directory: {e}");
}
}
}
}
}
/// Scan the local games directory and return a `GameDB` with current games
async fn scan_local_games(game_dir: &str) -> eyre::Result<GameDB> {
load_local_game_db(game_dir).await
}
async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result<bool> { async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result<bool> {
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?; let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;
@@ -525,6 +525,45 @@ async fn update_game_db(games: Vec<Game>, app: AppHandle) {
refresh_games_list(&app).await; refresh_games_list(&app).await;
} }
async fn update_local_games_in_db(local_games: Vec<Game>, app: AppHandle) {
let state = app.state::<LanSpreadState>();
// Collect local game IDs first to avoid move issues
let local_game_ids: HashSet<String> = local_games.iter().map(|g| g.id.clone()).collect();
{
let mut game_db = state.games.write().await;
// Update installation status for games that exist locally
for local_game in &local_games {
if let Some(existing_game) = game_db.get_mut_game_by_id(&local_game.id) {
existing_game.downloaded = local_game.downloaded;
existing_game.installed = local_game.installed;
existing_game
.local_version
.clone_from(&local_game.local_version);
log::debug!("Updated local game status for: {}", local_game.id);
}
}
// For games in the main DB that are not in the local list,
// mark them as not downloaded/installed (they were deleted)
for game in game_db.games.values_mut() {
if !local_game_ids.contains(&game.id) && (game.downloaded || game.installed) {
log::info!(
"Game {} no longer exists locally, marking as uninstalled",
game.id
);
game.downloaded = false;
game.installed = false;
game.local_version = None;
}
}
}
refresh_games_list(&app).await;
}
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 = '\\';
@@ -699,6 +738,10 @@ pub fn run() {
log::info!("PeerEvent::ListGames received"); log::info!("PeerEvent::ListGames received");
update_game_db(games, app_handle.clone()).await; update_game_db(games, app_handle.clone()).await;
} }
PeerEvent::LocalGamesUpdated(local_games) => {
log::info!("PeerEvent::LocalGamesUpdated received");
update_local_games_in_db(local_games, app_handle.clone()).await;
}
PeerEvent::GotGameFiles { id, file_descriptions } => { PeerEvent::GotGameFiles { id, file_descriptions } => {
log::info!("PeerEvent::GotGameFiles received"); log::info!("PeerEvent::GotGameFiles received");