game sizes?
This commit is contained in:
Generated
+1
@@ -2261,6 +2261,7 @@ dependencies = [
|
|||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"walkdir",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ It is designed to run headless – other crates (most notably
|
|||||||
|
|
||||||
## Runtime Overview
|
## Runtime Overview
|
||||||
|
|
||||||
- `start_peer(game_dir, tx_events)` boots the asynchronous runtime in the
|
- `start_peer(game_dir, tx_events, peer_game_db)` boots the asynchronous runtime in the
|
||||||
background and returns an `UnboundedSender<PeerCommand>` that the caller uses
|
background and returns an `UnboundedSender<PeerCommand>` that the caller uses
|
||||||
for control. The function immediately forwards the supplied game directory via
|
for control. The function immediately forwards the supplied game directory via
|
||||||
`PeerCommand::SetGameDir`.
|
`PeerCommand::SetGameDir` and keeps using the provided `PeerGameDB` so the UI
|
||||||
|
layer can observe live peer metadata.
|
||||||
- `PeerCommand` represents the small control surface exposed to the UI layer:
|
- `PeerCommand` represents the small control surface exposed to the UI layer:
|
||||||
`ListGames`, `GetGame`, `DownloadGameFiles`, and `SetGameDir`.
|
`ListGames`, `GetGame`, `DownloadGameFiles`, and `SetGameDir`.
|
||||||
- `PeerEvent` enumerates everything the peer runtime reports back to the UI:
|
- `PeerEvent` enumerates everything the peer runtime reports back to the UI:
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ impl From<eyre::Report> for PeerError {
|
|||||||
pub fn start_peer(
|
pub fn start_peer(
|
||||||
game_dir: String,
|
game_dir: String,
|
||||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||||
|
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||||
) -> eyre::Result<UnboundedSender<PeerCommand>> {
|
) -> eyre::Result<UnboundedSender<PeerCommand>> {
|
||||||
log::info!("Starting peer system with game directory: {game_dir}");
|
log::info!("Starting peer system with game directory: {game_dir}");
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ pub fn start_peer(
|
|||||||
// Start the peer in a background task
|
// Start the peer in a background task
|
||||||
let tx_control_clone = tx_control.clone();
|
let tx_control_clone = tx_control.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = run_peer(rx_control, tx_notify_ui).await {
|
if let Err(e) = run_peer(rx_control, tx_notify_ui, peer_game_db).await {
|
||||||
log::error!("Peer system failed: {e}");
|
log::error!("Peer system failed: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -342,6 +343,27 @@ impl PeerGameDB {
|
|||||||
seen.into_values().collect()
|
seen.into_values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn majority_game_size(&self, game_id: &str) -> Option<u64> {
|
||||||
|
let mut size_counts: HashMap<u64, usize> = HashMap::new();
|
||||||
|
|
||||||
|
for peer in self.peers.values() {
|
||||||
|
if let Some(game) = peer.games.get(game_id) {
|
||||||
|
if game.size == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*size_counts.entry(game.size).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_counts
|
||||||
|
.into_iter()
|
||||||
|
.max_by(|(size_a, count_a), (size_b, count_b)| {
|
||||||
|
count_a.cmp(count_b).then_with(|| size_a.cmp(size_b))
|
||||||
|
})
|
||||||
|
.map(|(size, _)| size)
|
||||||
|
}
|
||||||
|
|
||||||
/// Validates file sizes across all peers and returns only the files with majority consensus
|
/// Validates file sizes across all peers and returns only the files with majority consensus
|
||||||
/// Returns a tuple of (`validated_files`, `peer_whitelist`, `file_peer_map`) where
|
/// Returns a tuple of (`validated_files`, `peer_whitelist`, `file_peer_map`) where
|
||||||
/// `peer_whitelist` contains peers that have at least one majority-approved file and
|
/// `peer_whitelist` contains peers that have at least one majority-approved file and
|
||||||
@@ -1344,12 +1366,13 @@ impl std::fmt::Debug for PeerCtx {
|
|||||||
pub async fn run_peer(
|
pub async fn run_peer(
|
||||||
mut rx_control: UnboundedReceiver<PeerCommand>,
|
mut rx_control: UnboundedReceiver<PeerCommand>,
|
||||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||||
|
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
// peer context
|
// peer context
|
||||||
let ctx = Ctx {
|
let ctx = Ctx {
|
||||||
game_dir: Arc::new(RwLock::new(None)),
|
game_dir: Arc::new(RwLock::new(None)),
|
||||||
local_game_db: Arc::new(RwLock::new(None)),
|
local_game_db: Arc::new(RwLock::new(None)),
|
||||||
peer_game_db: Arc::new(RwLock::new(PeerGameDB::new())),
|
peer_game_db: peer_game_db.clone(),
|
||||||
local_peer_addr: Arc::new(RwLock::new(None)),
|
local_peer_addr: Arc::new(RwLock::new(None)),
|
||||||
downloading_games: Arc::new(RwLock::new(HashSet::new())),
|
downloading_games: Arc::new(RwLock::new(HashSet::new())),
|
||||||
active_downloads: Arc::new(RwLock::new(HashMap::new())),
|
active_downloads: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ tauri-plugin-shell = { workspace = true }
|
|||||||
tauri-plugin-dialog = { workspace = true }
|
tauri-plugin-dialog = { workspace = true }
|
||||||
tauri-plugin-store = { workspace = true }
|
tauri-plugin-store = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
walkdir = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { workspace = true }
|
windows = { workspace = true }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use std::{
|
|||||||
use eyre::bail;
|
use eyre::bail;
|
||||||
use lanspread_compat::eti::get_games;
|
use lanspread_compat::eti::get_games;
|
||||||
use lanspread_db::db::{Game, GameDB};
|
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::{AppHandle, Emitter as _, Manager};
|
||||||
use tauri_plugin_shell::{ShellExt, process::Command};
|
use tauri_plugin_shell::{ShellExt, process::Command};
|
||||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||||
@@ -21,6 +21,8 @@ struct LanSpreadState {
|
|||||||
games: Arc<RwLock<GameDB>>,
|
games: Arc<RwLock<GameDB>>,
|
||||||
games_in_download: Arc<RwLock<HashSet<String>>>,
|
games_in_download: Arc<RwLock<HashSet<String>>>,
|
||||||
games_folder: Arc<RwLock<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")]
|
#[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);
|
let installed = downloaded && local_install_is_ready(&game_path);
|
||||||
game.installed = installed;
|
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 {
|
if installed {
|
||||||
log::debug!("Set {game} to installed");
|
log::debug!("Set {game} to installed");
|
||||||
match lanspread_db::db::read_version_from_ini(&game_path) {
|
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) {
|
async fn refresh_games_list(app_handle: &AppHandle) {
|
||||||
let state = app_handle.state::<LanSpreadState>();
|
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;
|
refresh_games_list(&app).await;
|
||||||
@@ -686,11 +796,14 @@ pub fn run() {
|
|||||||
// channel to receive events from the peer
|
// channel to receive events from the peer
|
||||||
let (tx_peer_event, mut rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();
|
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 {
|
let lanspread_state = LanSpreadState {
|
||||||
peer_ctrl: Arc::new(RwLock::new(None)),
|
peer_ctrl: Arc::new(RwLock::new(None)),
|
||||||
games: Arc::new(RwLock::new(GameDB::empty())),
|
games: Arc::new(RwLock::new(GameDB::empty())),
|
||||||
games_in_download: Arc::new(RwLock::new(HashSet::new())),
|
games_in_download: Arc::new(RwLock::new(HashSet::new())),
|
||||||
games_folder: Arc::new(RwLock::new(String::new())),
|
games_folder: Arc::new(RwLock::new(String::new())),
|
||||||
|
peer_game_db: peer_game_db.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -709,10 +822,13 @@ pub fn run() {
|
|||||||
.manage(lanspread_state)
|
.manage(lanspread_state)
|
||||||
.setup({
|
.setup({
|
||||||
let tx_peer_event_clone = tx_peer_event.clone();
|
let tx_peer_event_clone = tx_peer_event.clone();
|
||||||
|
let peer_game_db_clone = peer_game_db.clone();
|
||||||
move |app| {
|
move |app| {
|
||||||
// Initialize peer system ONLY when games directory is set (games directory is mandatory)
|
// Initialize peer system ONLY when games directory is set (games directory is mandatory)
|
||||||
// But the UI is responsive immediately - no blocking server discovery
|
// But the UI is responsive immediately - no blocking server discovery
|
||||||
let app_handle_clone = app.handle().clone();
|
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 {
|
tauri::async_runtime::spawn(async move {
|
||||||
// Wait for games directory to be set by user (this is mandatory)
|
// Wait for games directory to be set by user (this is mandatory)
|
||||||
loop {
|
loop {
|
||||||
@@ -752,7 +868,11 @@ pub fn run() {
|
|||||||
refresh_games_list(&app_handle_clone).await;
|
refresh_games_list(&app_handle_clone).await;
|
||||||
|
|
||||||
// Only start peer system when we have a valid games directory
|
// 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) => {
|
Ok(peer_ctrl) => {
|
||||||
let state = app_handle_clone.state::<LanSpreadState>();
|
let state = app_handle_clone.state::<LanSpreadState>();
|
||||||
*state.peer_ctrl.write().await = Some(peer_ctrl);
|
*state.peer_ctrl.write().await = Some(peer_ctrl);
|
||||||
|
|||||||
Reference in New Issue
Block a user