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:
@@ -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, .. }
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user