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 fe9a88b9b2
commit 4728ea177c
38 changed files with 1193 additions and 573 deletions
+29 -7
View File
@@ -54,9 +54,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.100"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
name = "arc-swap"
@@ -607,9 +607,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde",
]
@@ -1465,9 +1465,10 @@ dependencies = [
name = "drop-app"
version = "0.4.0"
dependencies = [
"anyhow",
"async-trait",
"atomic-instant-full",
"bitcode",
"boxcar",
"bytes",
"cacache 13.1.0",
"chrono",
@@ -2072,11 +2073,10 @@ dependencies = [
"futures-util",
"hex 0.4.3",
"log",
"native_model",
"pot",
"rayon",
"remote",
"reqwest 0.12.28",
"rustix 1.1.3",
"serde",
"serde_json",
"serde_with",
@@ -2471,6 +2471,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -4591,6 +4602,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pot"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf741fa415952eb20f27fbc210dc85f31cc7cdc80aa3ce81d5e27d28a6f45dc2"
dependencies = [
"byteorder",
"half",
"serde",
]
[[package]]
name = "potential_utf"
version = "0.1.4"
+2 -1
View File
@@ -29,9 +29,10 @@ rustflags = ["-C", "target-feature=+aes,+sse2"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.101"
async-trait = "0.1.89"
atomic-instant-full = "0.1.0"
bitcode = "0.6.6"
boxcar = "0.2.7"
bytes = "1.10.1"
cacache = "13.1.0"
chrono = "0.4.38"
+2 -1
View File
@@ -2,10 +2,11 @@ use serde::Serialize;
use crate::{app_status::AppStatus, user::User};
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, PartialEq, Eq)]
pub enum UmuState {
NotNeeded,
NotInstalled,
NoDefault,
Installed,
}
+5
View File
@@ -23,6 +23,7 @@ pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = LazyLock::new(|| {
)
});
/*
pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| {
let entry = Entry::new("drop", "database_key").expect("failed to open keyring");
let mut key = entry.get_secret().unwrap_or_else(|_| {
@@ -38,3 +39,7 @@ pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| {
iv[0..16].try_into().expect("iv wrong length"),
)
});
*/
// TODO: fix keyring
pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| ([0; 16], [0; 16]));
+25 -9
View File
@@ -12,6 +12,7 @@ pub mod data {
pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v1::GameDownloadStatus;
pub type InstalledGameType = v1::InstalledGameType;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
/**
* Need to be universally accessible by the ID, and the version is just a couple sprinkles on top
@@ -19,6 +20,7 @@ pub mod data {
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v1::DatabaseApplications;
pub type UserConfiguration = v1::UserConfiguration;
use std::collections::HashMap;
@@ -77,6 +79,7 @@ pub mod data {
UserConfiguration {
launch_template: "{}".to_owned(),
override_proton_path: None,
enable_updates: false,
}
}
@@ -85,6 +88,13 @@ pub mod data {
pub struct UserConfiguration {
pub launch_template: String,
pub override_proton_path: Option<String>,
pub enable_updates: bool,
}
impl Default for UserConfiguration {
fn default() -> Self {
default_template()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@@ -156,25 +166,31 @@ pub mod data {
}
}
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum InstalledGameType {
SetupRequired,
Installed,
PartiallyInstalled {
#[serde(skip)]
configuration: UserConfiguration,
},
}
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_type: InstalledGameType,
version_id: String,
install_dir: String,
update_available: bool,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ApplicationTransientStatus {
Queued { version_id: String },
Downloading { version_id: String },
@@ -5,7 +5,7 @@ use std::{
};
use futures_util::StreamExt;
use log::warn;
use log::{info, warn};
use remote::{
error::RemoteAccessError,
requests::{generate_url, make_authenticated_get},
@@ -108,6 +108,8 @@ impl DepotManager {
})
.collect::<Vec<Depot>>();
info!("syncing {} depots...", new_depots.len());
for depot in &mut new_depots {
if let Err(sync_error) = self.sync_depot(depot).await {
warn!("failed to sync depot {}: {:?}", depot.endpoint, sync_error);
+7 -3
View File
@@ -1,8 +1,6 @@
use humansize::{BINARY, format_size};
use std::{
fmt::{Display, Formatter},
io,
sync::{Arc, mpsc::SendError},
fmt::{Display, Formatter}, io, path::StripPrefixError, sync::{Arc, mpsc::SendError}
};
use remote::error::RemoteAccessError;
@@ -85,4 +83,10 @@ impl From<RemoteAccessError> for ApplicationDownloadError {
fn from(value: RemoteAccessError) -> Self {
ApplicationDownloadError::Communication(value)
}
}
impl From<StripPrefixError> for ApplicationDownloadError {
fn from(value: StripPrefixError) -> Self {
ApplicationDownloadError::IoError(Arc::new(io::Error::other(value)))
}
}
+1 -4
View File
@@ -18,13 +18,10 @@ droplet-rs = { git = "https://github.com/Drop-OSS/droplet-rs" }
futures-util = "*"
hex = "0.4.3"
log = "0.4.28"
native_model = { git = "https://github.com/Drop-OSS/native_model.git", version = "0.6.4", features = [
"rmp_serde_1_3"
] }
pot = "3.0.1"
rayon = "1.11.0"
remote = { path = "../remote", version = "0.1.0" }
reqwest = "0.12.23"
rustix = "1.1.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
serde_with = "3.15.0"
+52 -22
View File
@@ -1,4 +1,5 @@
use async_trait::async_trait;
use database::models::data::UserConfiguration;
use database::{
ApplicationTransientStatus, DownloadableMetadata, borrow_db_checked, borrow_db_mut_checked,
};
@@ -22,6 +23,8 @@ use remote::utils::DROP_CLIENT_ASYNC;
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt::Debug;
use std::fs::{create_dir_all, remove_file};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Instant;
@@ -49,6 +52,7 @@ pub struct DownloadInformation {
pub struct GameDownloadAgent {
pub metadata: DownloadableMetadata,
pub configuration: UserConfiguration,
pub control_flag: DownloadThreadControl,
pub dl_info: Mutex<Option<DownloadInformation>>,
pub download_progress: Arc<ProgressObject>,
@@ -66,41 +70,33 @@ impl Debug for GameDownloadAgent {
}
impl GameDownloadAgent {
pub async fn new_from_index(
metadata: DownloadableMetadata,
target_download_dir: usize,
sender: Sender<DownloadManagerSignal>,
depot_manager: Arc<DepotManager>,
) -> Result<Self, ApplicationDownloadError> {
let base_dir = {
let db_lock = borrow_db_checked();
db_lock.applications.install_dirs[target_download_dir].clone()
};
Self::new(metadata, base_dir, sender, depot_manager).await
}
pub async fn new(
metadata: DownloadableMetadata,
base_dir: PathBuf,
sender: Sender<DownloadManagerSignal>,
depot_manager: Arc<DepotManager>,
configuration: UserConfiguration,
) -> Result<Self, ApplicationDownloadError> {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let game_name = get_cached_object::<Game>(&format!("game/{}", metadata.id)).map(|v| v.library_path).unwrap_or(metadata.id.clone());
let game_name = get_cached_object::<Game>(&format!("game/{}", metadata.id))
.map(|v| v.library_path)
.unwrap_or(metadata.id.clone());
let base_dir_path = Path::new(&base_dir);
info!("base dir {}", base_dir_path.display());
let data_base_dir_path = base_dir_path.join(game_name);
info!("data dir path {}", data_base_dir_path.display());
create_dir_all(data_base_dir_path.clone())?;
let stored_manifest = DropData::generate(
metadata.id.clone(),
metadata.version.clone(),
metadata.target_platform,
data_base_dir_path.clone(),
configuration.clone(),
);
let result = Self {
@@ -123,6 +119,7 @@ impl GameDownloadAgent {
dropdata: stored_manifest,
status: Mutex::new(DownloadStatus::Queued),
depot_manager,
configuration,
};
result.ensure_manifest_exists().await?;
@@ -141,6 +138,21 @@ impl GameDownloadAgent {
Ok(result)
}
fn scan_filetree(&self, path: &Path) -> Result<Vec<PathBuf>, io::Error> {
if !path.is_dir() {
return Ok(vec![path.into()]);
};
let subdirs = path.read_dir()?;
let mut results = Vec::new();
for subdir in subdirs {
let subdir = subdir?;
let subfiles = self.scan_filetree(&subdir.path())?;
results.extend(subfiles);
}
Ok(results)
}
// Blocking
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
let mut db_lock = borrow_db_mut_checked();
@@ -199,6 +211,13 @@ impl GameDownloadAgent {
&[
("id", &self.metadata.id),
("version", &self.metadata.version),
(
"previous",
self.dropdata
.previously_installed_version
.as_ref()
.map_or("", |v| v),
),
],
)
.map_err(ApplicationDownloadError::Communication)?;
@@ -277,13 +296,25 @@ impl GameDownloadAgent {
let completed_chunks = lock!(self.dropdata.contexts);
completed_chunks.clone()
};
info!("started with {} existing chunks", completed_chunks.len());
let chunk_len = manifests_chunks.iter().map(|v| v.1.len()).sum::<usize>();
let mut max_download_threads = borrow_db_checked().settings.max_download_threads;
if max_download_threads <= 0 {
if max_download_threads == 0 {
max_download_threads = 1;
}
let file_list = &file_list;
let base_path = &self.dropdata.base_path;
let current_file_tree = self.scan_filetree(base_path)?;
for file in current_file_tree {
let filename = file.strip_prefix(base_path)?.to_string_lossy().to_string();
let needed = file_list.contains_key(&filename) || filename == ".dropdata";
if !needed {
debug!("deleted {}", file.display());
remove_file(file)?;
}
}
let local_completed_chunks = completed_chunks.clone();
@@ -299,7 +330,7 @@ impl GameDownloadAgent {
}
Ok(())
}
Err(err) => return Err(err),
Err(err) => Err(err),
};
let mut index = 0;
@@ -310,10 +341,8 @@ impl GameDownloadAgent {
self.download_progress.get(index),
self.download_progress.clone(),
);
let disk_progress_handle = ProgressHandle::new(
self.disk_progress.get(index),
self.disk_progress.clone(),
);
let disk_progress_handle =
ProgressHandle::new(self.disk_progress.get(index), self.disk_progress.clone());
index += 1;
let chunk_length = chunk_data.files.iter().map(|v| v.length).sum();
@@ -344,7 +373,6 @@ impl GameDownloadAgent {
}
chunk_completions.push(async move {
for i in 0..RETRY_COUNT {
let base_path = self.dropdata.base_path.clone();
match download_game_chunk(
&self.metadata.id,
&local_version_id,
@@ -503,6 +531,7 @@ impl GameDownloadAgent {
&self.metadata(),
self.dropdata.base_path.display().to_string(),
Some(app_handle),
self.configuration.clone(),
);
self.dropdata.write();
@@ -573,6 +602,7 @@ impl Downloadable for GameDownloadAgent {
async fn on_complete(&self, app_handle: &tauri::AppHandle) {
match on_game_complete(
&self.metadata(),
self.configuration.clone(),
self.dropdata.base_path.to_string_lossy().to_string(),
app_handle,
)
@@ -3,7 +3,7 @@ use std::fs::{Permissions, set_permissions};
use std::io::SeekFrom;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
@@ -37,7 +37,7 @@ pub async fn download_game_chunk(
key: &[u8; 16],
chunk_data: &ChunkData,
file_list: &HashMap<String, String>,
base_path: PathBuf,
base_path: &Path,
control_flag: &DownloadThreadControl,
// How much we're downloading
download_progress: &ProgressHandle,
@@ -95,7 +95,7 @@ pub async fn download_game_chunk(
let stream = response
.bytes_stream()
.map(|v| v.map_err(|err| std::io::Error::other(err)));
.map(|v| v.map_err(std::io::Error::other));
let mut stream_reader = StreamReader::new(stream);
//let mut stream_reader = response;
+45 -11
View File
@@ -5,9 +5,8 @@ use std::{
path::{Path, PathBuf},
};
use database::platform::Platform;
use database::{models::data::UserConfiguration, platform::Platform};
use log::error;
use native_model::{Decode, Encode};
use utils::lock;
pub type DropData = v1::DropData;
@@ -17,38 +16,73 @@ pub static DROPDATA_PATH: &str = ".dropdata";
pub mod v1 {
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
use database::platform::Platform;
use native_model::native_model;
use database::{models::data::UserConfiguration, platform::Platform};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DropData {
pub game_id: String,
pub game_version: String,
pub target_platform: Platform,
#[serde(default)]
pub configuration: UserConfiguration,
pub contexts: Mutex<HashMap<String, bool>>,
pub base_path: PathBuf,
pub previously_installed_version: Option<String>,
}
impl DropData {
pub fn new(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self {
pub fn new(
game_id: String,
game_version: String,
target_platform: Platform,
base_path: PathBuf,
configuration: UserConfiguration,
previously_installed_version: Option<String>,
) -> Self {
Self {
base_path,
game_id,
game_version,
target_platform,
contexts: Mutex::new(HashMap::new()),
configuration,
previously_installed_version,
}
}
}
}
impl DropData {
pub fn generate(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self {
pub fn generate(
game_id: String,
game_version: String,
target_platform: Platform,
base_path: PathBuf,
configuration: UserConfiguration,
) -> Self {
match DropData::read(&base_path) {
Ok(v) => v,
Err(_) => DropData::new(game_id, game_version, target_platform, base_path),
Ok(v) => {
if v.game_id != game_id || v.game_version != game_version {
return DropData::new(
game_id,
game_version,
target_platform,
base_path,
configuration,
Some(v.game_version),
);
}
v
}
Err(_) => DropData::new(
game_id,
game_version,
target_platform,
base_path,
configuration,
None,
),
}
}
pub fn read(base_path: &Path) -> Result<Self, io::Error> {
@@ -57,7 +91,7 @@ impl DropData {
let mut s = Vec::new();
file.read_to_end(&mut s)?;
native_model::rmp_serde_1_3::RmpSerde::decode(s).map_err(|e| {
pot::from_slice(&s).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to decode drop data: {e}"),
@@ -65,7 +99,7 @@ impl DropData {
})
}
pub fn write(&self) {
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
let manifest_raw = match pot::to_vec(&self) {
Ok(data) => data,
Err(_) => return,
};
+34 -37
View File
@@ -2,12 +2,14 @@ use bitcode::{Decode, Encode};
use database::{
ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion,
borrow_db_checked, borrow_db_mut_checked,
models::data::{InstalledGameType, UserConfiguration},
};
use log::{debug, error, warn};
use remote::{
auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url,
utils::DROP_CLIENT_ASYNC,
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::fs::remove_dir_all;
use std::thread::spawn;
@@ -76,8 +78,9 @@ pub fn set_partially_installed(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
configuration: UserConfiguration,
) {
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle, configuration);
}
pub fn set_partially_installed_db(
@@ -85,13 +88,16 @@ pub fn set_partially_installed_db(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
configuration: UserConfiguration,
) {
db_lock.applications.transient_statuses.remove(meta);
db_lock.applications.game_statuses.insert(
meta.id.clone(),
GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.clone(),
GameDownloadStatus::Installed {
install_type: InstalledGameType::PartiallyInstalled { configuration },
version_id: meta.version.clone(),
install_dir,
update_available: false,
},
);
db_lock
@@ -135,16 +141,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
if let Some((_, install_dir)) = match previous_state {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_type: _,
version_id: version_name,
install_dir,
update_available: _,
} => Some((version_name, install_dir)),
_ => None,
} {
@@ -197,6 +197,7 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
pub async fn on_game_complete(
meta: &DownloadableMetadata,
configuration: UserConfiguration,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
@@ -211,7 +212,12 @@ pub async fn on_game_complete(
.send()
.await?;
let game_version: GameVersion = response.json().await?;
if !response.status().is_success() {
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
}
let mut game_version: GameVersion = response.json().await?;
game_version.user_configuration = configuration;
let mut handle = borrow_db_mut_checked();
handle
@@ -230,16 +236,15 @@ pub async fn on_game_complete(
.iter()
.find(|v| v.platform == meta.target_platform);
let status = if setup_configuration.is_none() {
GameDownloadStatus::Installed {
version_name: meta.version.clone(),
install_dir,
}
} else {
GameDownloadStatus::SetupRequired {
version_name: meta.version.clone(),
install_dir,
}
let status = GameDownloadStatus::Installed {
version_id: meta.version.clone(),
install_dir,
install_type: if setup_configuration.is_none() {
InstalledGameType::Installed
} else {
InstalledGameType::SetupRequired
},
update_available: false,
};
let mut db_handle = borrow_db_mut_checked();
@@ -247,10 +252,7 @@ pub async fn on_game_complete(
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
db_handle
.applications
.transient_statuses
.remove(meta);
db_handle.applications.transient_statuses.remove(meta);
drop(db_handle);
app_emit!(
app_handle,
@@ -273,8 +275,10 @@ pub fn push_game_update(
version: Option<GameVersion>,
status: GameStatusWithTransient,
) {
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
&status.0
if let Some(GameDownloadStatus::Installed {
install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired,
..
}) = &status.0
&& version.is_none()
{
panic!("pushed game for installed game that doesn't have version information");
@@ -289,11 +293,4 @@ pub fn push_game_update(
version,
}
);
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendGameOptions {
pub launch_string: String,
pub override_proton_path: Option<String>,
}
}
+16 -7
View File
@@ -19,14 +19,22 @@ pub fn scan_install_dirs() {
if !drop_data_file.exists() {
continue;
}
let Ok(drop_data) = DropData::read(&game.path()) else {
warn!(
".dropdata exists for {}, but couldn't read it. is it corrupted?",
game.file_name().display()
);
continue;
let drop_data = match DropData::read(&game.path()) {
Ok(v) => v,
Err(err) => {
warn!(
".dropdata exists for {}, but couldn't read it. is it corrupted? {:?}",
game.file_name().display(),
err
);
continue;
}
};
if db_lock.applications.game_statuses.contains_key(&drop_data.game_id) {
if db_lock
.applications
.game_statuses
.contains_key(&drop_data.game_id)
{
continue;
}
@@ -41,6 +49,7 @@ pub fn scan_install_dirs() {
&metadata,
drop_data.base_path.to_str().unwrap().to_string(),
None,
drop_data.configuration,
);
}
}
+67 -32
View File
@@ -25,8 +25,53 @@ impl ProcessHandler for NativeGameLauncher {
}
}
pub struct UMULauncher;
impl ProcessHandler for UMULauncher {
pub struct UMUNativeLauncher;
impl ProcessHandler for UMUNativeLauncher {
fn create_launch_process(
&self,
meta: &DownloadableMetadata,
launch_command: String,
game_version: &GameVersion,
_current_dir: &str,
_database: &Database,
) -> Result<String, ProcessError> {
let umu_id_override = game_version
.launches
.iter()
.find(|v| v.platform == meta.target_platform)
.and_then(|v| v.umu_id_override.as_ref())
.map_or("", |v| v);
let game_id = if umu_id_override.is_empty() {
&game_version.version_id
} else {
umu_id_override
};
let pfx_dir = DATA_ROOT_DIR.join("pfx");
let pfx_dir = pfx_dir.join(meta.id.clone());
create_dir_all(&pfx_dir)?;
Ok(format!(
"GAMEID={game_id} UMU_NO_PROTON=1 WINEPREFIX={} {umu:?} {launch}",
pfx_dir.to_string_lossy(),
umu = UMU_LAUNCHER_EXECUTABLE
.as_ref()
.expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
launch = launch_command,
))
}
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
let Some(compat_info) = &*COMPAT_INFO else {
return false;
};
compat_info.umu_installed
}
}
pub struct UMUCompatLauncher;
impl ProcessHandler for UMUCompatLauncher {
fn create_launch_process(
&self,
meta: &DownloadableMetadata,
@@ -52,39 +97,29 @@ impl ProcessHandler for UMULauncher {
let pfx_dir = pfx_dir.join(meta.id.clone());
create_dir_all(&pfx_dir)?;
let no_proton = match meta.target_platform {
Platform::Linux => Some("UMU_NO_PROTON=1"),
_ => None,
};
let proton_path = game_version
.user_configuration
.override_proton_path
.as_ref()
.or(database.applications.default_proton_path.as_ref())
.ok_or(ProcessError::NoCompat)?;
let proton_env = if no_proton.is_none() {
let proton_path = game_version
.user_configuration
.override_proton_path
.as_ref()
.or(database.applications.default_proton_path.as_ref())
.ok_or(ProcessError::NoCompat)?;
#[cfg(target_os = "linux")]
let proton_valid = crate::compat::read_proton_path(PathBuf::from(proton_path))
.ok()
.flatten()
.is_some();
#[cfg(not(target_os = "linux"))]
let proton_valid = false;
if !proton_valid {
return Err(ProcessError::NoCompat);
}
Some(format!("PROTONPATH={}", proton_path))
} else {
None
};
#[cfg(target_os = "linux")]
let proton_valid = crate::compat::read_proton_path(PathBuf::from(proton_path))
.ok()
.flatten()
.is_some();
#[cfg(not(target_os = "linux"))]
let proton_valid = false;
if !proton_valid {
return Err(ProcessError::NoCompat);
}
let proton_env = format!("PROTONPATH={}", proton_path);
Ok(format!(
"GAMEID={game_id} {} WINEPREFIX={} {} {umu:?} {launch}",
proton_env.unwrap_or(String::new()),
"GAMEID={game_id} {} WINEPREFIX={} {umu:?} {launch}",
proton_env,
pfx_dir.to_string_lossy(),
no_proton.unwrap_or(""),
umu = UMU_LAUNCHER_EXECUTABLE
.as_ref()
.expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
@@ -110,7 +145,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
current_dir: &str,
database: &Database,
) -> Result<String, ProcessError> {
let umu_launcher = UMULauncher {};
let umu_launcher = UMUCompatLauncher {};
let umu_string = umu_launcher.create_launch_process(
meta,
launch_command,
+29 -37
View File
@@ -11,7 +11,8 @@ use std::{
use database::{
ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion,
borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, platform::Platform,
borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, models::data::InstalledGameType,
platform::Platform,
};
use dynfmt::Format;
use dynfmt::SimpleCurlyFormat;
@@ -26,7 +27,9 @@ use crate::{
error::ProcessError,
format::DropFormatArgs,
parser::{LaunchParameters, ParsedCommand},
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
process_handlers::{
AsahiMuvmLauncher, NativeGameLauncher, UMUCompatLauncher, UMUNativeLauncher,
},
};
pub struct RunningProcess {
@@ -75,7 +78,7 @@ impl ProcessManager<'_> {
),
(
(Platform::Linux, Platform::Linux),
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
&UMUNativeLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
(
(Platform::macOS, Platform::macOS),
@@ -87,7 +90,7 @@ impl ProcessManager<'_> {
),
(
(Platform::Linux, Platform::Windows),
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
&UMUCompatLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
],
app_handle,
@@ -145,21 +148,15 @@ impl ProcessManager<'_> {
.unwrap_or_else(|| panic!("Could not get installed version of {}", &game_id));
db_handle.applications.transient_statuses.remove(&meta);
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
if let Some(GameDownloadStatus::SetupRequired {
version_name,
install_dir,
let current_state = db_handle.applications.game_statuses.get_mut(&game_id);
if let Some(GameDownloadStatus::Installed {
install_type,
..
}) = current_state
&& let Ok(exit_code) = result
&& exit_code.success()
{
db_handle.applications.game_statuses.insert(
game_id.clone(),
GameDownloadStatus::Installed {
version_name: version_name.to_string(),
install_dir: install_dir.to_string(),
},
);
*install_type = InstalledGameType::Installed;
}
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
@@ -268,12 +265,10 @@ impl ProcessManager<'_> {
let (version_name, install_dir) = match game_status {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => (version_name, install_dir),
GameDownloadStatus::SetupRequired {
version_name,
version_id: version_name,
install_dir,
install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired,
..
} => (version_name, install_dir),
_ => return Err(ProcessError::NotInstalled),
};
@@ -323,8 +318,8 @@ impl ProcessManager<'_> {
let (target_command, emulator) = match game_status {
GameDownloadStatus::Installed {
version_name: _,
install_dir: _,
install_type: InstalledGameType::Installed,
..
} => {
let (_, launch_config) = game_version
.launches
@@ -338,9 +333,9 @@ impl ProcessManager<'_> {
launch_config.emulator.as_ref(),
)
}
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir: _,
GameDownloadStatus::Installed {
install_type: InstalledGameType::SetupRequired,
..
} => {
let setup_config = game_version
.setups
@@ -375,12 +370,12 @@ impl ProcessManager<'_> {
let emulator_install_dir = match emulator_game_status {
GameDownloadStatus::Installed {
version_name: _,
install_dir,
install_type: InstalledGameType::Installed,
..
} => Ok(install_dir),
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir: _,
GameDownloadStatus::Installed {
install_type: InstalledGameType::SetupRequired,
..
} => todo!(),
_ => Err(err.clone()),
}?;
@@ -407,8 +402,6 @@ impl ProcessManager<'_> {
*v = v.replace("{rom}", &target_command.command);
});
process_handler.create_launch_process(
emulator_metadata,
exe_command.reconstruct(),
@@ -417,8 +410,6 @@ impl ProcessManager<'_> {
&db_lock,
)?
} else {
process_handler.create_launch_process(
&meta,
target_command.reconstruct(),
@@ -474,9 +465,10 @@ impl ProcessManager<'_> {
.map(|e| e.split("=").map(|v| v.to_string()).collect::<Vec<String>>())
{
if let Some(key) = parts.first()
&& let Some(value) = parts.get(1) {
command.env(key, value);
}
&& let Some(value) = parts.get(1)
{
command.env(key, value);
}
}
command
};
+14 -21
View File
@@ -49,28 +49,21 @@ impl Middleware for AutoOfflineMiddleware {
match res {
Ok(res) => {
tauri::async_runtime::spawn(async move {
let lock = DROP_APP_HANDLE.try_lock();
if let Ok(lock) = lock {
if let Some(app_handle) = &*lock {
let state = app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
let state_lock = state.try_lock();
if let Ok(mut state_lock) = state_lock {
if state_lock.status == AppStatus::Offline {
state_lock.status = AppStatus::SignedIn;
app_handle
.emit("update_state", &*state_lock)
.expect("failed to emit state update");
}
} else {
warn!("failed to lock app state - {}", url.as_str());
let lock = DROP_APP_HANDLE.lock().await;
if let Some(app_handle) = &*lock {
let state = app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
let state_lock = state.try_lock();
if let Ok(mut state_lock) = state_lock {
if state_lock.status == AppStatus::Offline {
state_lock.status = AppStatus::SignedIn;
app_handle
.emit("update_state", &*state_lock)
.expect("failed to emit state update");
}
};
} else {
warn!(
"failed to lock app handle for offline/online middleware - {}",
url.as_str()
);
}
} else {
warn!("failed to lock app state - {}", url.as_str());
}
};
});
Ok(res)
+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(())
}
}