130 lines
3.5 KiB
Rust
130 lines
3.5 KiB
Rust
use std::{
|
|
collections::{HashMap, VecDeque},
|
|
hash::{Hash, Hasher},
|
|
};
|
|
|
|
use lanspread_proto::{GameSummary, LibraryDelta, LibrarySnapshot, LibrarySummary};
|
|
|
|
const MAX_DELTA_HISTORY: usize = 8;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct LocalLibraryState {
|
|
pub revision: u64,
|
|
pub digest: u64,
|
|
pub games: HashMap<String, GameSummary>,
|
|
pub recent_deltas: VecDeque<LibraryDelta>,
|
|
}
|
|
|
|
impl LocalLibraryState {
|
|
pub fn empty() -> Self {
|
|
Self {
|
|
revision: 0,
|
|
digest: 0,
|
|
games: HashMap::new(),
|
|
recent_deltas: VecDeque::new(),
|
|
}
|
|
}
|
|
|
|
pub fn update_from_scan(
|
|
&mut self,
|
|
summaries: HashMap<String, GameSummary>,
|
|
revision: u64,
|
|
) -> Option<LibraryDelta> {
|
|
let new_digest = compute_library_digest(&summaries);
|
|
let changed =
|
|
self.revision != revision || self.digest != new_digest || self.games != summaries;
|
|
|
|
if !changed {
|
|
return None;
|
|
}
|
|
|
|
let delta = compute_library_delta(self.revision, revision, &self.games, &summaries);
|
|
self.revision = revision;
|
|
self.digest = new_digest;
|
|
self.games = summaries;
|
|
self.recent_deltas.push_back(delta.clone());
|
|
while self.recent_deltas.len() > MAX_DELTA_HISTORY {
|
|
self.recent_deltas.pop_front();
|
|
}
|
|
Some(delta)
|
|
}
|
|
|
|
pub fn delta_since(&self, from_rev: u64) -> Option<LibraryDelta> {
|
|
self.recent_deltas
|
|
.iter()
|
|
.find(|delta| delta.from_rev == from_rev)
|
|
.cloned()
|
|
}
|
|
}
|
|
|
|
pub fn compute_library_digest(games: &HashMap<String, GameSummary>) -> u64 {
|
|
let mut entries: Vec<&GameSummary> = games.values().collect();
|
|
entries.sort_by(|a, b| a.id.cmp(&b.id));
|
|
|
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
for summary in entries {
|
|
summary.id.hash(&mut hasher);
|
|
summary.name.hash(&mut hasher);
|
|
summary.size.hash(&mut hasher);
|
|
summary.downloaded.hash(&mut hasher);
|
|
summary.installed.hash(&mut hasher);
|
|
summary.eti_version.hash(&mut hasher);
|
|
summary.manifest_hash.hash(&mut hasher);
|
|
summary.availability.hash(&mut hasher);
|
|
}
|
|
hasher.finish()
|
|
}
|
|
|
|
pub fn build_library_summary(state: &LocalLibraryState) -> LibrarySummary {
|
|
LibrarySummary {
|
|
library_rev: state.revision,
|
|
library_digest: state.digest,
|
|
game_count: state.games.len(),
|
|
}
|
|
}
|
|
|
|
pub fn build_library_snapshot(state: &LocalLibraryState) -> LibrarySnapshot {
|
|
let mut games: Vec<GameSummary> = state.games.values().cloned().collect();
|
|
games.sort_by(|a, b| a.id.cmp(&b.id));
|
|
LibrarySnapshot {
|
|
library_rev: state.revision,
|
|
games,
|
|
}
|
|
}
|
|
|
|
pub fn compute_library_delta(
|
|
from_rev: u64,
|
|
to_rev: u64,
|
|
previous: &HashMap<String, GameSummary>,
|
|
next: &HashMap<String, GameSummary>,
|
|
) -> LibraryDelta {
|
|
let mut added = Vec::new();
|
|
let mut updated = Vec::new();
|
|
let mut removed = Vec::new();
|
|
|
|
for (game_id, summary) in next {
|
|
match previous.get(game_id) {
|
|
None => added.push(summary.clone()),
|
|
Some(existing) => {
|
|
if existing != summary {
|
|
updated.push(summary.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for game_id in previous.keys() {
|
|
if !next.contains_key(game_id) {
|
|
removed.push(game_id.clone());
|
|
}
|
|
}
|
|
|
|
LibraryDelta {
|
|
from_rev,
|
|
to_rev,
|
|
added,
|
|
updated,
|
|
removed,
|
|
}
|
|
}
|