Game updates (#187)

* refactor: split umu launcher

* feat: latest version picker + fixes

* feat: frontend latest changes

* feat: game update detection w/ setting

* feat: fixes and refactor for game update

* fix: windows ui

* fix: deps

* feat: update modifications

* feat: missing ui and lock update

* fix: create install dir on init

* fix: clippy

* fix: clippy x2

* feat: add configuration option to toggle updates

* feat: uninstall dropdown on partiallyinstalled
This commit is contained in:
DecDuck
2026-02-25 23:27:30 +11:00
committed by GitHub
parent d7ec7fc25c
commit 82b9912bd0
38 changed files with 1193 additions and 573 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
use std::sync::nonpoison::Mutex;
use client::app_state::AppState;
use database::{GameDownloadStatus, borrow_db_checked};
use database::{GameDownloadStatus, borrow_db_checked, models::data::InstalledGameType};
use games::collections::collection::Collections;
use remote::{
cache::{cache_object, get_cached_object},
@@ -57,7 +57,7 @@ pub async fn fetch_collections_offline(
.game_statuses
.get(&v.game_id)
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
GameDownloadStatus::Installed { install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired, .. }
)
});
}
+35 -23
View File
@@ -1,7 +1,9 @@
use std::{path::PathBuf, sync::Arc};
use database::{
DownloadType, DownloadableMetadata, GameDownloadStatus, borrow_db_checked, platform::Platform,
DownloadType, DownloadableMetadata, GameDownloadStatus, borrow_db_checked,
models::data::{InstalledGameType, UserConfiguration},
platform::Platform,
};
use download_manager::{
DOWNLOAD_MANAGER, downloadable::Downloadable, error::ApplicationDownloadError,
@@ -14,20 +16,8 @@ pub async fn download_game(
version_id: String,
target_platform: Platform,
install_dir: usize,
enable_updates: bool,
) -> Result<(), ApplicationDownloadError> {
{
let db = borrow_db_checked();
let status = db
.applications
.game_statuses
.get(&game_id)
.unwrap_or(&GameDownloadStatus::Remote {});
if matches!(status, GameDownloadStatus::Installed { .. }) {
return Ok(());
}
};
let sender = { DOWNLOAD_MANAGER.get_sender().clone() };
let meta = DownloadableMetadata {
@@ -37,11 +27,32 @@ pub async fn download_game(
download_type: DownloadType::Game,
};
let game_download_agent = GameDownloadAgent::new_from_index(
{
let db = borrow_db_checked();
let status = db.applications.transient_statuses.get(&meta);
if status.is_some() {
return Ok(());
}
};
let configuration = UserConfiguration {
enable_updates,
..Default::default()
};
let base_dir = {
let db_lock = borrow_db_checked();
db_lock.applications.install_dirs[install_dir].clone()
};
let game_download_agent = GameDownloadAgent::new(
meta,
install_dir,
base_dir,
sender,
DOWNLOAD_MANAGER.clone_depot_manager(),
configuration,
)
.await?;
@@ -58,7 +69,7 @@ pub async fn download_game(
#[tauri::command]
pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadError> {
let (meta, install_dir) = {
let (meta, (install_dir, configuration)) = {
let db_lock = borrow_db_checked();
let status = db_lock
.applications
@@ -75,12 +86,12 @@ pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadE
.clone();
let install_dir = match status {
GameDownloadStatus::Remote {} => Err(ApplicationDownloadError::InvalidCommand),
GameDownloadStatus::SetupRequired { .. } => {
Err(ApplicationDownloadError::InvalidCommand)
}
GameDownloadStatus::Installed { .. } => Err(ApplicationDownloadError::InvalidCommand),
GameDownloadStatus::PartiallyInstalled { install_dir, .. } => Ok(install_dir),
GameDownloadStatus::Installed {
install_type: InstalledGameType::PartiallyInstalled { configuration },
install_dir,
..
} => Ok((install_dir, configuration)),
_ => Err(ApplicationDownloadError::InvalidCommand),
}?;
(meta, install_dir)
};
@@ -98,6 +109,7 @@ pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadE
install_dir.to_path_buf(),
sender,
DOWNLOAD_MANAGER.clone_depot_manager(),
configuration,
)
.await?,
) as Box<dyn Downloadable + Send + Sync>);
+26 -26
View File
@@ -3,12 +3,12 @@ use std::sync::nonpoison::Mutex;
use bitcode::{Decode, Encode};
use database::{
DownloadableMetadata, GameDownloadStatus, borrow_db_checked, borrow_db_mut_checked,
platform::Platform,
models::data::{InstalledGameType, UserConfiguration}, platform::Platform,
};
use games::{
collections::collection::Collection,
downloads::error::LibraryError,
library::{FetchGameStruct, FrontendGameOptions, Game, get_current_meta, uninstall_game_logic},
library::{FetchGameStruct, Game, get_current_meta, uninstall_game_logic},
state::{GameStatusManager, GameStatusWithTransient},
};
use log::warn;
@@ -168,25 +168,20 @@ pub async fn fetch_library_logic_offline(
.game_statuses
.get(game.id())
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
GameDownloadStatus::Installed {
install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired,
..
}
)
};
response.library.retain(retain_filter);
response.other.retain(retain_filter);
response.missing.retain(retain_filter);
response.collections.iter_mut().for_each(|k| {
k.entries.retain(|object| {
matches!(
&db_handle
.applications
.game_statuses
.get(object.game.id())
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
)
})
});
response
.collections
.iter_mut()
.for_each(|k| k.entries.retain(|object| retain_filter(&object.game)));
Ok(response)
}
@@ -272,11 +267,11 @@ struct VersionDownloadOptionRequiredContent {
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionDownloadOption {
game_id: String,
version_id: String,
pub game_id: String,
pub version_id: String,
display_name: Option<String>,
version_path: String,
platform: Platform,
pub platform: Platform,
size: GameSize,
required_content: Vec<VersionDownloadOptionRequiredContent>,
}
@@ -293,7 +288,16 @@ pub async fn fetch_game_version_options_logic(
) -> Result<Vec<VersionDownloadOption>, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game", &game_id, "versions"], &[])?;
let previous_id = borrow_db_checked()
.applications
.installed_game_version
.get(&game_id)
.map(|v| v.version.clone());
let response = generate_url(
&["/api/v1/client/game", &game_id, "versions"],
&[("previous", &previous_id.unwrap_or(String::new()))],
)?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
@@ -310,7 +314,7 @@ pub async fn fetch_game_version_options_logic(
let state_lock = state.lock();
let process_manager_lock = PROCESS_MANAGER.lock();
let data = data
let data: Vec<VersionDownloadOption> = data
.into_iter()
.filter(|v| process_manager_lock.valid_platform(&v.platform))
.collect();
@@ -387,7 +391,7 @@ pub async fn fetch_game_version_options(
#[tauri::command]
pub fn update_game_configuration(
game_id: String,
options: FrontendGameOptions,
options: UserConfiguration,
) -> Result<(), LibraryError> {
let mut handle = borrow_db_mut_checked();
let installed_version = handle
@@ -406,11 +410,7 @@ pub fn update_game_configuration(
.unwrap()
.clone();
// Add more options in here
existing_configuration.user_configuration.launch_template = options.launch_string;
existing_configuration.user_configuration.override_proton_path = options.override_proton_path;
// Add no more options past here
existing_configuration.user_configuration = options;
handle
.applications
+10 -17
View File
@@ -57,7 +57,9 @@ mod downloads;
mod games;
mod process;
mod remote;
mod scheduler;
mod settings;
mod updates;
use client::*;
use download_manager::*;
@@ -67,6 +69,8 @@ use process::*;
use remote::*;
use settings::*;
use crate::scheduler::scheduler_task;
async fn setup(handle: AppHandle) -> AppState {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
@@ -101,6 +105,9 @@ async fn setup(handle: AppHandle) -> AppState {
ProcessManagerWrapper::init(handle.clone());
DownloadManagerWrapper::init(handle.clone());
debug!("checking if database is set up");
let is_set_up = DB.database_is_set_up();
#[cfg(not(target_os = "linux"))]
let umu_state = UmuState::NotNeeded;
@@ -110,9 +117,6 @@ async fn setup(handle: AppHandle) -> AppState {
false => UmuState::NotInstalled,
};
debug!("checking if database is set up");
let is_set_up = DB.database_is_set_up();
scan_install_dirs();
if !is_set_up {
@@ -136,20 +140,7 @@ async fn setup(handle: AppHandle) -> AppState {
for (game_id, status) in statuses {
match status {
GameDownloadStatus::Remote {} => {}
GameDownloadStatus::PartiallyInstalled { .. } => {}
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir,
} => {
let install_dir_path = Path::new(&install_dir);
if !install_dir_path.exists() {
missing_games.push(game_id);
}
}
GameDownloadStatus::Installed {
version_name: _,
install_dir,
} => {
GameDownloadStatus::Installed { install_dir, .. } => {
let install_dir_path = Path::new(&install_dir);
if !install_dir_path.exists() {
missing_games.push(game_id);
@@ -416,6 +407,8 @@ pub fn run() {
.show(|_| {});
}
}
tokio::spawn(async move { scheduler_task().await });
});
Ok(())
+43
View File
@@ -0,0 +1,43 @@
use std::{time::Duration};
use async_trait::async_trait;
use log::warn;
use tokio::time;
use crate::updates::GameUpdater;
#[async_trait]
pub trait ScheduleTask {
/// Returns how many minutes between calls
fn timeframe(&mut self) -> usize;
async fn call(&mut self) -> Result<(), anyhow::Error>;
}
struct TaskData {
task: Box<dyn ScheduleTask + Send + Sync>,
updates_since_call: usize,
}
pub async fn scheduler_task() -> ! {
let mut interval = time::interval(Duration::from_mins(1));
interval.tick().await;
let mut tasks = vec![TaskData {
task: Box::new(GameUpdater::new()),
updates_since_call: usize::MAX - 1,
}];
loop {
for task in &mut tasks {
task.updates_since_call += 1;
if task.task.timeframe() <= task.updates_since_call {
let result = task.task.call().await;
if let Err(err) = result {
warn!("background task returned error: {err:?}");
}
task.updates_since_call = 0;
}
}
interval.tick().await;
}
}
+121
View File
@@ -0,0 +1,121 @@
use std::sync::nonpoison::Mutex;
use async_trait::async_trait;
use client::{app_state::AppState, app_status::AppStatus};
use database::{
GameDownloadStatus, GameVersion, borrow_db_checked, borrow_db_mut_checked,
};
use log::warn;
use process::PROCESS_MANAGER;
use remote::utils::DROP_APP_HANDLE;
use tauri::Manager;
use crate::{
games::{VersionDownloadOption, fetch_game_version_options},
scheduler::ScheduleTask,
};
pub struct GameUpdater {
no_internet: bool,
}
impl GameUpdater {
pub fn new() -> Self {
GameUpdater { no_internet: false }
}
}
/*
This implementation is kinda inefficient because we can't hold the locks across await boundaries,
which means we constantly lock and unlock certain objects. It doesn't matter though, because this
doesn't have to be fast.
*/
#[async_trait]
impl ScheduleTask for GameUpdater {
fn timeframe(&mut self) -> usize {
if self.no_internet { 5 } else { 30 }
}
async fn call(&mut self) -> Result<(), anyhow::Error> {
let app_handle = DROP_APP_HANDLE.lock().await;
let app_handle = app_handle
.as_ref()
.ok_or(anyhow::anyhow!("game update task ran before setup"))?;
let state = app_handle.state::<Mutex<AppState>>();
{
let state_lock = state.lock();
if state_lock.status == AppStatus::Offline {
self.no_internet = true;
return Ok(());
};
};
self.no_internet = false;
let to_check: Vec<GameVersion> = {
let db_lock = borrow_db_checked();
db_lock
.applications
.game_statuses
.values()
.map(|v| match v {
GameDownloadStatus::Installed { version_id, .. } => Some(version_id),
_ => None,
})
.map(|v| {
v.and_then(|version_id| db_lock.applications.game_versions.get(version_id))
})
.filter(|v| {
v.map(|v| v.user_configuration.enable_updates)
.unwrap_or(false)
})
.map(|v| v.cloned().unwrap())
.collect()
};
for version in to_check {
let version_options =
match fetch_game_version_options(version.game_id.clone(), state.clone()).await {
Ok(v) => v,
Err(err) => {
warn!(
"failed to check for update for game id {}: {:?}",
version.game_id, err
);
continue;
}
};
let process_manager_lock = PROCESS_MANAGER.lock();
let valid_options: Vec<VersionDownloadOption> = version_options
.into_iter()
.filter(|v| process_manager_lock.valid_platform(&v.platform))
.collect();
let latest = match valid_options.first() {
Some(v) => v,
None => {
warn!("found no versions for game id: {}", version.game_id);
continue;
}
};
let mut db_lock = borrow_db_mut_checked();
let game_status = db_lock
.applications
.game_statuses
.get_mut(&version.game_id)
.ok_or(anyhow::anyhow!(""))?;
if let GameDownloadStatus::Installed {
update_available, ..
} = game_status {
*update_available = latest.version_id != version.version_id;
};
}
Ok(())
}
}