refactor: type game availability state

Game::availability used string labels that were carried through persisted
library data, protocol summaries, and the Tauri-facing game payload. That
allowed invalid states to exist and required legacy summary conversion code to
defensively map strings back into protocol availability values.

Move Availability to lanspread-db and re-export it from lanspread-proto so the
persisted Game type and wire GameSummary type share one serde enum. The JSON
spelling stays Ready, Downloading, or LocalOnly, so the serialized shape does
not change for current library indexes or peer payloads.

Add typed helpers for sentinel-derived download state. Game::set_downloaded
keeps downloaded and Ready/LocalOnly in lockstep and intentionally collapses
non-ready local state, including Downloading, back to LocalOnly. That matches
the current local-summary contract where active operations are suppressed
instead of advertised as Downloading. Game::normalized_availability keeps the
legacy Game-to-summary path from publishing an inconsistent Ready value when
downloaded is false.

Update the follow-up status note so typed availability is no longer listed as
open work.

Test Plan:
- just fmt
- just test
- just clippy
- just build

Refs: none
This commit is contained in:
2026-05-16 11:49:01 +02:00
parent fdad162240
commit be196f9e4b
8 changed files with 128 additions and 93 deletions
@@ -9,13 +9,7 @@ use std::{
use eyre::bail;
use lanspread_compat::eti::get_games;
use lanspread_db::db::{
AVAILABILITY_LOCAL_ONLY,
AVAILABILITY_READY,
Game,
GameDB,
GameFileDescription,
};
use lanspread_db::db::{Game, GameDB, GameFileDescription};
use lanspread_peer::{
ActiveOperation,
ActiveOperationKind,
@@ -372,15 +366,10 @@ fn update_game_installation_state(game: &mut Game, games_root: &Path) {
}
let downloaded = game_path.join("version.ini").is_file();
game.downloaded = downloaded;
game.set_downloaded(downloaded);
let installed = local_install_is_present(&game_path);
game.installed = installed;
game.availability = if downloaded {
AVAILABILITY_READY.to_string()
} else {
AVAILABILITY_LOCAL_ONLY.to_string()
};
// Size stays anchored to bundled game.db; skip expensive recalculation.
@@ -687,11 +676,8 @@ async fn update_local_games_in_db(local_games: Vec<Game>, app: AppHandle) {
// 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.set_downloaded(local_game.downloaded);
existing_game.installed = local_game.installed;
existing_game
.availability
.clone_from(&local_game.availability);
existing_game
.local_version
.clone_from(&local_game.local_version);
@@ -707,9 +693,8 @@ async fn update_local_games_in_db(local_games: Vec<Game>, app: AppHandle) {
"Game {} no longer exists locally, marking as uninstalled",
game.id
);
game.downloaded = false;
game.set_downloaded(false);
game.installed = false;
game.availability = AVAILABILITY_LOCAL_ONLY.to_string();
game.local_version = None;
}
}