mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-07-22 07:53:02 +10:00
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:
Generated
+29
-7
@@ -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"
|
||||
|
||||
@@ -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,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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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]));
|
||||
@@ -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);
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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