feat: prepare clusters for launch
This commit is contained in:
parent
2a0f53e107
commit
6c80a84972
34 changed files with 1395 additions and 371 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -1424,9 +1424,9 @@ checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
|||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.3.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
|
|
@ -2941,6 +2941,7 @@ dependencies = [
|
|||
"discord-rich-presence",
|
||||
"dunce",
|
||||
"futures",
|
||||
"futures-lite",
|
||||
"indicatif",
|
||||
"interpulse",
|
||||
"md-5",
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ sysinfo = { version = "0.31", features = [ "serde" ] }
|
|||
discord-rich-presence = { version = "0.2" }
|
||||
indicatif = { version = "0.17" }
|
||||
futures = { version = "0.3" }
|
||||
futures-lite = { version = "2.6" }
|
||||
async-trait = { version = "0.1" }
|
||||
whoami = { version = "1.5" }
|
||||
cc = { version = "1.1" }
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ sea-orm = { workspace = true }
|
|||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use onelauncher_core::{api::{self, proxy::ProxyEmpty}, error::LauncherResult, initialize_core, store::CoreOptions};
|
||||
use onelauncher_core::{api::{self, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::CoreOptions};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> LauncherResult<()> {
|
||||
initialize_core(CoreOptions::default(), ProxyEmpty::new()).await?;
|
||||
initialize_core(CoreOptions::default(), ProxyDynamic::new()).await?;
|
||||
|
||||
println!("syncing clusters");
|
||||
let missing = api::cluster::sync_clusters().await?;
|
||||
|
|
@ -22,5 +22,8 @@ async fn main() -> LauncherResult<()> {
|
|||
|
||||
println!("cluster created: {cluster:#?}");
|
||||
|
||||
println!("preparing cluster for launch");
|
||||
api::cluster::prepare_cluster(&cluster).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
use anyhow::anyhow;
|
||||
use onelauncher_core::{api::{game::metadata::{download_minecraft_ingressed, download_version_info}, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::{CoreOptions, State}};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> LauncherResult<()> {
|
||||
|
|
@ -8,7 +7,7 @@ async fn main() -> LauncherResult<()> {
|
|||
|
||||
let state = State::get().await?;
|
||||
let mut metadata = state.metadata.write().await;
|
||||
let versions = metadata.get_vanilla(GameLoader::Vanilla).await?;
|
||||
let versions = metadata.get_vanilla_or_fetch().await?;
|
||||
|
||||
let version = versions.versions.iter().find(|v| v.id == "1.20.4").ok_or_else(|| anyhow!("couldn't find 1.8.9 in version list"))?;
|
||||
let version_info = download_version_info(version, None, None, Some(true)).await?;
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ async fn main() -> LauncherResult<()> {
|
|||
let metadata = &mut state.metadata.write().await;
|
||||
|
||||
println!("Initialized: {}", metadata.initialized());
|
||||
println!("Minecraft: {:?}", metadata.get_vanilla(GameLoader::Vanilla).await.is_ok());
|
||||
println!("Forge: {:?}", metadata.get_modded(GameLoader::Forge).await.is_ok());
|
||||
println!("Minecraft: {:?}", metadata.get_vanilla_or_fetch().await.is_ok());
|
||||
println!("Forge: {:?}", metadata.get_modded_or_fetch(GameLoader::Forge).await.is_ok());
|
||||
println!("Initialized: {}", metadata.initialized());
|
||||
println!("NeoForge: {:?}", metadata.get_modded(GameLoader::NeoForge).await.is_ok());
|
||||
println!("Fabric: {:?}", metadata.get_modded(GameLoader::Fabric).await.is_ok());
|
||||
println!("Quilt: {:?}", metadata.get_modded(GameLoader::Quilt).await.is_ok());
|
||||
println!("NeoForge: {:?}", metadata.get_modded_or_fetch(GameLoader::NeoForge).await.is_ok());
|
||||
println!("Fabric: {:?}", metadata.get_modded_or_fetch(GameLoader::Fabric).await.is_ok());
|
||||
println!("Quilt: {:?}", metadata.get_modded_or_fetch(GameLoader::Quilt).await.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -53,8 +53,13 @@ pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> {
|
|||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
let model = get_cluster_by_id(id).await?.ok_or(DaoError::NotFound)?;
|
||||
model.delete(db).await?;
|
||||
let deleted = clusters::Entity::delete_by_id(id)
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
if deleted.rows_affected == 0 {
|
||||
return Err(DaoError::NotFound.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
use onelauncher_entity::clusters;
|
||||
use interpulse::api::minecraft::VersionInfo;
|
||||
use onelauncher_entity::cluster_stage::ClusterStage;
|
||||
use onelauncher_entity::{clusters, java_versions};
|
||||
use onelauncher_entity::icon::Icon;
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::api::game::arguments;
|
||||
use crate::api::game::metadata::{self, download_minecraft};
|
||||
use crate::api::ingress::IngressSendExt;
|
||||
use crate::api::{java, setting_profiles};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::Dirs;
|
||||
use crate::utils::io;
|
||||
use crate::store::ingress::{IngressType, SubIngress};
|
||||
use crate::store::{Dirs, State};
|
||||
use crate::utils::io::{self, IOError};
|
||||
|
||||
pub mod dao;
|
||||
|
||||
mod sync;
|
||||
pub use sync::*;
|
||||
|
||||
use super::ingress::init_ingress;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClusterError {
|
||||
#[error("version '{0}' was not found")]
|
||||
InvalidVersion(String),
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn sanitize_name(name: &str) -> String {
|
||||
let mut name = name.to_string();
|
||||
|
|
@ -79,4 +96,215 @@ pub async fn create_cluster(
|
|||
}
|
||||
}
|
||||
|
||||
/// Make a cluster playable (installs all necessary game files and dependencies required to play)
|
||||
pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
|
||||
const INGRESS_TOTAL: f64 = 100.0;
|
||||
const INGRESS_TASKS: f64 = 4.0;
|
||||
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
|
||||
|
||||
tracing::debug!("preparing cluster {}", cluster.name);
|
||||
|
||||
if cluster.stage == ClusterStage::Ready {
|
||||
tracing::info!("cluster is already ready");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ingress_id = init_ingress(
|
||||
IngressType::PrepareCluster {
|
||||
cluster_name: cluster.name.clone(),
|
||||
},
|
||||
"preparing cluster",
|
||||
INGRESS_TOTAL,
|
||||
)
|
||||
.await?;
|
||||
|
||||
dao::update_cluster_by_id(cluster.id, async |mut cluster| {
|
||||
cluster.stage = Set(ClusterStage::Downloading);
|
||||
Ok(cluster)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let result: LauncherResult<()> = {
|
||||
// TASK 1
|
||||
ingress_id.set_ingress_message("fetching data").await?;
|
||||
let setting_profile =
|
||||
setting_profiles::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut metadata = state.metadata.write().await;
|
||||
|
||||
let version = metadata
|
||||
.get_vanilla_or_fetch()
|
||||
.await?
|
||||
.versions
|
||||
.iter()
|
||||
.find(|v| v.id == cluster.mc_version)
|
||||
.ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))?;
|
||||
|
||||
let loader_version = metadata::get_loader_version(
|
||||
&cluster.mc_version,
|
||||
cluster.mc_loader,
|
||||
cluster.mc_loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TASK 2
|
||||
let mut version_info = metadata::download_version_info(
|
||||
version,
|
||||
loader_version.as_ref(),
|
||||
Some(&SubIngress::new(&ingress_id, INGRESS_STEP)),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
|
||||
version
|
||||
} else {
|
||||
// TODO: java runtime not found - should prob download
|
||||
let java = java::locate_java().await?;
|
||||
|
||||
if java.is_empty() {
|
||||
tracing::warn!("No Java installations found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (path, info) in java {
|
||||
java::dao::insert_java(path, info).await?;
|
||||
}
|
||||
|
||||
java::get_recommended_java(&version_info, Some(&setting_profile)).await?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("No Java installations found, please install a Java runtime")
|
||||
})?
|
||||
};
|
||||
|
||||
// TASK 3
|
||||
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), None).await?;
|
||||
|
||||
// TASK 4
|
||||
run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
tracing::error!("failed to prepare cluster: {}", err);
|
||||
dao::update_cluster_by_id(cluster.id, async |mut cluster| {
|
||||
cluster.stage = Set(ClusterStage::NotReady);
|
||||
Ok(cluster)
|
||||
})
|
||||
.await?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
dao::update_cluster_by_id(cluster.id, async |mut cluster| {
|
||||
cluster.stage = Set(ClusterStage::Ready);
|
||||
Ok(cluster)
|
||||
})
|
||||
.await?;
|
||||
tracing::debug!("cluster is ready");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run forge processors
|
||||
async fn run_forge_processors(
|
||||
cluster: &clusters::Model,
|
||||
version_info: &mut VersionInfo,
|
||||
java_version: java_versions::Model,
|
||||
ingress: &SubIngress<'_>,
|
||||
) -> LauncherResult<()> {
|
||||
let Some(processors) = &version_info.processors else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let dirs = Dirs::get().await?;
|
||||
let client = dirs
|
||||
.versions_dir()
|
||||
.join(format!("{}.jar", &version_info.id));
|
||||
let libraries = dirs.libraries_dir();
|
||||
|
||||
let Some(data) = &mut version_info.data else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
macro_rules! data_entry {
|
||||
($dest:expr; $($name:literal: client => $client:expr, server => $server:expr;)+) => {
|
||||
$(std::collections::HashMap::insert(
|
||||
$dest,
|
||||
String::from($name),
|
||||
interpulse::api::modded::SidedDataEntry {
|
||||
client: String::from($client),
|
||||
server: String::from($server),
|
||||
},
|
||||
);)+
|
||||
}
|
||||
}
|
||||
|
||||
data_entry! {
|
||||
data;
|
||||
"SIDE":
|
||||
client => "client",
|
||||
server => "";
|
||||
"MINECRAFT_JAR":
|
||||
client => client.to_string_lossy(),
|
||||
server => "";
|
||||
"MINECRAFT_VERSION":
|
||||
client => cluster.mc_version.clone(),
|
||||
server => "";
|
||||
"ROOT":
|
||||
client => cluster.path.clone(),
|
||||
server => "";
|
||||
"LIBRARY_DIR":
|
||||
client => libraries.to_string_lossy(),
|
||||
server => "";
|
||||
}
|
||||
|
||||
ingress.set_ingress_message("running forge processors").await?;
|
||||
let total_length = processors.len();
|
||||
for (index, processor) in processors.iter().enumerate() {
|
||||
if let Some(sides) = &processor.sides {
|
||||
if !sides.contains(&String::from("client")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut cp = processor.classpath.clone();
|
||||
cp.push(processor.jar.clone());
|
||||
|
||||
let child = Command::new(&java_version.absolute_path)
|
||||
.arg("-cp")
|
||||
.arg(arguments::get_classpath_library(
|
||||
&libraries,
|
||||
&cp,
|
||||
)?)
|
||||
.arg(
|
||||
arguments::main_class(arguments::get_library(&libraries, &processor.jar, false)?)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("failed to find processor main class for {}", processor.jar)
|
||||
})?,
|
||||
)
|
||||
.args(arguments::processor_arguments(
|
||||
&libraries,
|
||||
&processor.args,
|
||||
data,
|
||||
)?)
|
||||
.output()
|
||||
.await
|
||||
.map_err(IOError::from)
|
||||
.map_err(|err| anyhow::anyhow!("failed to run processor: {err}"))?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"error occured while running processor: {}",
|
||||
String::from_utf8_lossy(&child.stderr)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
ingress.set_ingress_message(&format!("running forge processors {index}/{total_length}")).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,19 +54,23 @@ pub async fn sync_clusters() -> LauncherResult<Vec<i32>> {
|
|||
// It iterates over the directories in the cluster directory
|
||||
// and checks if they are in the database
|
||||
let cluster_dir = Dirs::get_clusters_dir().await?;
|
||||
let mut stream = io::read_dir(cluster_dir).await?;
|
||||
while let Ok(Some(entry)) = stream.next_entry().await {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() || checked_paths.contains(&path) {
|
||||
// Skip if it's not a directory or if we've already checked it
|
||||
continue;
|
||||
}
|
||||
if cluster_dir.exists() {
|
||||
let mut stream = io::read_dir(cluster_dir).await?;
|
||||
while let Ok(Some(entry)) = stream.next_entry().await {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() || checked_paths.contains(&path) {
|
||||
// Skip if it's not a directory or if we've already checked it
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have a directory that is not in the database
|
||||
if let Err(err) = sync_from_fs_to_db(&path).await {
|
||||
tracing::error!("failed to sync cluster from fs to db: {}", err);
|
||||
}
|
||||
}
|
||||
// We have a directory that is not in the database
|
||||
if let Err(err) = sync_from_fs_to_db(&path).await {
|
||||
tracing::error!("failed to sync cluster from fs to db: {}", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
io::create_dir_all(cluster_dir).await?;
|
||||
}
|
||||
|
||||
Ok(missing_ids)
|
||||
}
|
||||
|
|
|
|||
369
packages/core/src/api/game/arguments.rs
Normal file
369
packages/core/src/api/game/arguments.rs
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
//! Programatic Minecraft argument parser which replaces variables in arguments
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use futures::AsyncReadExt;
|
||||
use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
|
||||
use interpulse::api::modded::SidedDataEntry;
|
||||
use interpulse::utils::get_path_from_artifact;
|
||||
|
||||
use crate::constants::{self, DUMMY_REPLACE_NEWLINE};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::Core;
|
||||
use crate::utils::io;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn java_arguments(
|
||||
arguments: Option<&[Argument]>,
|
||||
natives_path: &Path,
|
||||
libraries_path: &Path,
|
||||
classpaths: &str,
|
||||
version: &str,
|
||||
mem_min: usize,
|
||||
mem_max: usize,
|
||||
custom_args: Vec<String>,
|
||||
java_arch: &str,
|
||||
) -> LauncherResult<Vec<String>> {
|
||||
let mut parsed = Vec::new();
|
||||
if let Some(args) = arguments {
|
||||
parse_arguments(
|
||||
args,
|
||||
&mut parsed,
|
||||
|a| {
|
||||
parse_java_argument(
|
||||
a.to_string(),
|
||||
natives_path,
|
||||
libraries_path,
|
||||
classpaths,
|
||||
version,
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
)?;
|
||||
} else {
|
||||
parsed.push(format!(
|
||||
"-Djava.library.path={}",
|
||||
io::canonicalize(natives_path)
|
||||
.map_err(|_| anyhow::anyhow!(
|
||||
"specified natives path {} not found",
|
||||
natives_path.to_string_lossy()
|
||||
))?
|
||||
.to_string_lossy()
|
||||
));
|
||||
parsed.push("-cp".to_string());
|
||||
parsed.push(classpaths.to_string());
|
||||
}
|
||||
|
||||
parsed.push(format!("-Xmx{mem_max}M"));
|
||||
parsed.push(format!("-Xms{mem_min}M"));
|
||||
|
||||
for arg in custom_args {
|
||||
if !arg.is_empty() {
|
||||
parsed.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
// #[allow(clippy::too_many_arguments)]
|
||||
// pub fn minecraft_arguments(
|
||||
// args: Option<&[Argument]>,
|
||||
// legacy_args: Option<&str>,
|
||||
// creds: &MinecraftCredentials,
|
||||
// version: &str,
|
||||
// asset_index: &str,
|
||||
// game_directory: &Path,
|
||||
// assets_directory: &Path,
|
||||
// version_type: VersionType,
|
||||
// resolution: Resolution,
|
||||
// java_arch: &str,
|
||||
// ) -> LauncherResult<Vec<String>> {
|
||||
// if let Some(args) = args {
|
||||
// let mut parsed = Vec::new();
|
||||
// parse_arguments(
|
||||
// args,
|
||||
// &mut parsed,
|
||||
// |arg| {
|
||||
// parse_minecraft_argument(
|
||||
// arg,
|
||||
// &creds.access_token,
|
||||
// &creds.username,
|
||||
// creds.id,
|
||||
// version,
|
||||
// asset_index,
|
||||
// game_directory,
|
||||
// assets_directory,
|
||||
// version_type,
|
||||
// resolution,
|
||||
// )
|
||||
// },
|
||||
// java_arch,
|
||||
// )?;
|
||||
|
||||
// Ok(parsed)
|
||||
// } else if let Some(legacy_args) = legacy_args {
|
||||
// let mut parsed = Vec::new();
|
||||
// for arg in legacy_args.split(' ') {
|
||||
// parsed.push(parse_minecraft_argument(
|
||||
// &arg.replace(' ', DUMMY_REPLACE_NEWLINE),
|
||||
// &creds.access_token,
|
||||
// &creds.username,
|
||||
// creds.id,
|
||||
// version,
|
||||
// asset_index,
|
||||
// game_directory,
|
||||
// assets_directory,
|
||||
// version_type,
|
||||
// resolution,
|
||||
// )?);
|
||||
// }
|
||||
|
||||
// Ok(parsed)
|
||||
// } else {
|
||||
// Ok(Vec::new())
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn processor_arguments<T: AsRef<str>, S: ::std::hash::BuildHasher>(
|
||||
libraries_path: &Path,
|
||||
args: &[T],
|
||||
data: &HashMap<String, SidedDataEntry, S>,
|
||||
) -> LauncherResult<Vec<String>> {
|
||||
let mut parsed = Vec::new();
|
||||
|
||||
for arg in args {
|
||||
let a = &arg.as_ref()[1..arg.as_ref().len() - 1];
|
||||
if arg.as_ref().starts_with('{') {
|
||||
if let Some(entry) = data.get(a) {
|
||||
parsed.push(if entry.client.starts_with('[') {
|
||||
get_library(
|
||||
libraries_path,
|
||||
&entry.client[1..entry.client.len() - 1],
|
||||
true,
|
||||
)?
|
||||
} else {
|
||||
entry.client.clone()
|
||||
});
|
||||
}
|
||||
} else if arg.as_ref().starts_with('[') {
|
||||
parsed.push(get_library(libraries_path, a, true)?);
|
||||
} else {
|
||||
parsed.push(arg.as_ref().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub async fn main_class(path: String) -> LauncherResult<Option<String>> {
|
||||
// let zipfs = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?;
|
||||
// let mut archive = zip::ZipArchive::new(zipfs)
|
||||
// .map_err(|_| anyhow::anyhow!("failed to read main class {}", path))?;
|
||||
// let file = archive
|
||||
// .by_name("META-INF/MANIFEST.MF")
|
||||
// .map_err(|_| anyhow::anyhow!("failed to read manifest {}", path))?;
|
||||
let data = io::read(path).await?;
|
||||
let mut class_name: Option<String> = None;
|
||||
|
||||
io::read_zip_entries_bytes(data, async |_, entry, entry_reader| {
|
||||
if entry.dir()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if entry.filename().as_str()? != "META-INF/MANIFEST.MF" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
entry_reader.read_to_string(&mut buf).await?;
|
||||
|
||||
for line in buf.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("Main-Class:") {
|
||||
if let Some(class) = line.split(':').nth(1) {
|
||||
class_name = Some(class.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).await?;
|
||||
|
||||
Ok(class_name)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn parse_minecraft_argument(
|
||||
argument: &str,
|
||||
access_token: &str,
|
||||
username: &str,
|
||||
uuid: uuid::Uuid,
|
||||
version: &str,
|
||||
asset_index: &str,
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: VersionType,
|
||||
resolution: (u32, u32),
|
||||
) -> LauncherResult<String> {
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
Ok(argument
|
||||
.replace("${accessToken}", access_token)
|
||||
.replace("${auth_access_token}", access_token)
|
||||
.replace("${auth_session}", access_token)
|
||||
.replace("${auth_player_name}", username)
|
||||
.replace("${auth_xuid}", "0") // TODO: add auth xuid
|
||||
.replace("${auth_uuid}", &uuid.simple().to_string())
|
||||
.replace("${uuid}", &uuid.simple().to_string())
|
||||
.replace("${clientid}", &Core::get().msa_client_id)
|
||||
.replace("${user_properties}", "{}")
|
||||
.replace("${user_type}", "msa")
|
||||
.replace("${version_name}", version)
|
||||
.replace("${assets_index_name}", asset_index)
|
||||
.replace(
|
||||
"${game_directory}",
|
||||
&io::canonicalize(game_directory)?.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${assets_root}",
|
||||
&io::canonicalize(assets_directory)?.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${game_assets}",
|
||||
&io::canonicalize(assets_directory)?.to_string_lossy(),
|
||||
)
|
||||
.replace("${version_type}", version_type.as_str())
|
||||
.replace("${resolution_width}", &resolution.0.to_string())
|
||||
.replace("${resolution_height}", &resolution.1.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn parse_java_argument(
|
||||
mut argument: String,
|
||||
natives_path: &Path,
|
||||
libraries_path: &Path,
|
||||
classpaths: &str,
|
||||
version: &str,
|
||||
) -> LauncherResult<String> {
|
||||
argument.retain(|c| !c.is_whitespace());
|
||||
Ok(argument
|
||||
.replace(
|
||||
"${natives_directory}",
|
||||
&io::canonicalize(natives_path)?.to_string_lossy(),
|
||||
)
|
||||
.replace(
|
||||
"${library_directory}",
|
||||
&io::canonicalize(libraries_path)?.to_string_lossy(),
|
||||
)
|
||||
.replace("${classpath_separator}", constants::CLASSPATH_SEPARATOR)
|
||||
.replace("${launcher_name}", &Core::get().launcher_name)
|
||||
.replace("${launcher_version}", &Core::get().launcher_version)
|
||||
.replace("${version_name}", version)
|
||||
.replace("${classpath}", classpaths))
|
||||
}
|
||||
|
||||
pub fn classpaths(
|
||||
libraries_path: &Path,
|
||||
libraries: &[Library],
|
||||
client_path: &Path,
|
||||
java_arch: &str,
|
||||
updated: bool,
|
||||
) -> LauncherResult<String> {
|
||||
let mut classpaths = libraries
|
||||
.iter()
|
||||
.filter_map(|lib| {
|
||||
if let Some(rules) = &lib.rules {
|
||||
if !super::rules::validate_rules(rules, java_arch, updated) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !lib.include_in_classpath {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(get_library(libraries_path, &lib.name, false))
|
||||
})
|
||||
.collect::<Result<HashSet<_>, _>>()?;
|
||||
|
||||
classpaths.insert(
|
||||
io::canonicalize(client_path)?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
Ok(classpaths
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(constants::CLASSPATH_SEPARATOR))
|
||||
}
|
||||
|
||||
pub fn get_classpath_library<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
libraries: &[T],
|
||||
) -> LauncherResult<String> {
|
||||
let classpaths = libraries
|
||||
.iter()
|
||||
.map(|lib| get_library(libraries_path, lib.as_ref(), false))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(classpaths.join(constants::CLASSPATH_SEPARATOR))
|
||||
}
|
||||
|
||||
pub fn get_library(
|
||||
libraries_path: &Path,
|
||||
library: &str,
|
||||
error_exist: bool,
|
||||
) -> LauncherResult<String> {
|
||||
let mut path = libraries_path.to_path_buf();
|
||||
path.push(get_path_from_artifact(library)?);
|
||||
|
||||
if !path.exists() && error_exist {
|
||||
return Ok(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
let path = &io::canonicalize(&path)?;
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
fn parse_arguments<ParseFn>(
|
||||
args: &[Argument],
|
||||
parsed: &mut Vec<String>,
|
||||
parse_function: ParseFn,
|
||||
java_arch: &str,
|
||||
) -> LauncherResult<()>
|
||||
where
|
||||
ParseFn: Fn(&str) -> LauncherResult<String>,
|
||||
{
|
||||
for arg in args {
|
||||
match arg {
|
||||
Argument::Normal(a) => {
|
||||
let p = parse_function(&a.replace(' ', DUMMY_REPLACE_NEWLINE))?;
|
||||
for split_p in p.split(DUMMY_REPLACE_NEWLINE) {
|
||||
parsed.push(split_p.to_string());
|
||||
}
|
||||
}
|
||||
Argument::Ruled { rules, value } => {
|
||||
if super::rules::validate_rules(rules, java_arch, true) {
|
||||
match value {
|
||||
ArgumentValue::Single(arg) => {
|
||||
parsed.push(parse_function(&arg.replace(' ', DUMMY_REPLACE_NEWLINE))?);
|
||||
}
|
||||
ArgumentValue::Many(args) => {
|
||||
for arg in args {
|
||||
parsed.push(parse_function(
|
||||
&arg.replace(' ', DUMMY_REPLACE_NEWLINE),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
0
packages/core/src/api/game/launch.rs
Normal file
0
packages/core/src/api/game/launch.rs
Normal file
|
|
@ -1,35 +1,33 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use futures::{StreamExt, stream};
|
||||
use interpulse::api::minecraft::{AssetsIndex, DownloadType, Library, Os, OsRule, Rule, RuleAction, Version, VersionInfo};
|
||||
use interpulse::api::minecraft::{AssetsIndex, DownloadType, Library, Os, Version, VersionInfo};
|
||||
use interpulse::api::modded::LoaderVersion;
|
||||
use regex::Regex;
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use reqwest::Method;
|
||||
|
||||
use crate::api::ingress::{init_ingress, send_ingress_ref_opt};
|
||||
use crate::api::ingress::{init_ingress, IngressSendExt};
|
||||
use crate::error::{LauncherError, LauncherResult};
|
||||
use crate::store::ingress::{IngressRef, IngressType};
|
||||
use crate::store::{Core, Dirs};
|
||||
use crate::store::ingress::{IngressType, SubIngress, SubIngressExt};
|
||||
use crate::store::metadata::MetadataError;
|
||||
use crate::store::{Core, Dirs, State};
|
||||
use crate::utils::crypto::HashAlgorithm;
|
||||
use crate::utils::ext::OsExt;
|
||||
use crate::utils::http;
|
||||
use crate::utils::io;
|
||||
use crate::utils::os_ext::OsExt;
|
||||
use crate::utils::{http, io};
|
||||
|
||||
pub async fn download_minecraft_ingressed(
|
||||
version: &VersionInfo,
|
||||
java_arch: &str,
|
||||
force: Option<bool>
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<()> {
|
||||
let id = init_ingress(
|
||||
IngressType::MinecraftDownload,
|
||||
&format!("Downloading Minecraft {}", version.id),
|
||||
&format!("downloading Minecraft {}", version.id),
|
||||
100.0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ingress_ref = IngressRef::new(&id, 100.0);
|
||||
|
||||
download_minecraft(version, java_arch, Some(&ingress_ref), force).await
|
||||
download_minecraft(version, java_arch, Some(&SubIngress::new(&id, 100.0)), force).await
|
||||
}
|
||||
|
||||
// MARK: Main
|
||||
|
|
@ -37,19 +35,15 @@ pub async fn download_minecraft_ingressed(
|
|||
pub async fn download_minecraft(
|
||||
version: &VersionInfo,
|
||||
java_arch: &str,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
force: Option<bool>
|
||||
sub_ingress: Option<&SubIngress<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<()> {
|
||||
|
||||
const TASKS: f64 = 4.0;
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / TASKS));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
|
||||
let asset_index = download_assets_index(version, ingress_ref, force).await?;
|
||||
let asset_index = download_assets_index(version, sub_ingress, force).await?;
|
||||
tokio::try_join! {
|
||||
download_assets(version.assets == "legacy", &asset_index, ingress_ref, force),
|
||||
download_client(version, ingress_ref, force),
|
||||
download_libraries(&version.id, &version.libraries, java_arch, ingress_ref, force),
|
||||
download_assets(version.assets == "legacy", &asset_index, sub_ingress, force),
|
||||
download_client(version, sub_ingress, force),
|
||||
download_libraries(&version.id, &version.libraries, java_arch, sub_ingress, force),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -60,14 +54,15 @@ pub async fn download_minecraft(
|
|||
pub async fn download_version_info(
|
||||
version: &Version,
|
||||
loader: Option<&LoaderVersion>,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<VersionInfo> {
|
||||
const TASKS: f64 = 3.33;
|
||||
|
||||
tracing::debug!("loading version info for version {}", version.id);
|
||||
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 3.33));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
ingress.set_ingress_message("fetching version info").await?;
|
||||
|
||||
tracing::debug!("loading minecraft version info for minecraft version {version_id}");
|
||||
|
||||
|
|
@ -86,11 +81,12 @@ pub async fn download_version_info(
|
|||
);
|
||||
|
||||
let mut info =
|
||||
http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress_ref).await?;
|
||||
http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress)
|
||||
.await?;
|
||||
|
||||
if let Some(loader) = loader {
|
||||
let partial: interpulse::api::modded::PartialVersionInfo =
|
||||
http::fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress_ref)
|
||||
http::fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress)
|
||||
.await?;
|
||||
|
||||
info = interpulse::api::modded::merge_partial_version(partial, info);
|
||||
|
|
@ -98,12 +94,17 @@ pub async fn download_version_info(
|
|||
|
||||
info.id.clone_from(&version_id);
|
||||
|
||||
io::create_dir_all(&path.parent().unwrap_or_else(|| panic!("couldn't get version path parent"))).await?;
|
||||
io::create_dir_all(
|
||||
&path
|
||||
.parent()
|
||||
.unwrap_or_else(|| panic!("couldn't get version path parent")),
|
||||
)
|
||||
.await?;
|
||||
io::write(&path, &serde_json::to_vec(&info)?).await?;
|
||||
info
|
||||
};
|
||||
|
||||
send_ingress_ref_opt(ingress_ref).await?;
|
||||
ingress.send_ingress(ingress.ingress_total().map(|i| i / TASKS).unwrap_or_default()).await?;
|
||||
|
||||
tracing::debug!("loaded minecraft version info for minecraft version {version_id}");
|
||||
Ok(result)
|
||||
|
|
@ -113,7 +114,7 @@ pub async fn download_version_info(
|
|||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_assets_index(
|
||||
version: &VersionInfo,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<AssetsIndex> {
|
||||
tracing::debug!("loading assets index for version {}", version.id);
|
||||
|
|
@ -121,30 +122,34 @@ pub async fn download_assets_index(
|
|||
.await?
|
||||
.join(version.asset_index.id.clone());
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
send_ingress_ref_opt(ingress_ref).await?;
|
||||
// let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0));
|
||||
// let ingress_ref = ingress_ref.as_ref();
|
||||
// ingress_ref.map(async |i| send_ingress_ref_message(i, Some("fetching assets index")).await);
|
||||
let ingress_step = ingress.ingress_total().map(|i| i / 2.0).unwrap_or_default();
|
||||
ingress.set_ingress_message("fetching assets index").await?;
|
||||
|
||||
if path.exists() && !force.unwrap_or(false) {
|
||||
match io::read_json::<AssetsIndex>(&path).await {
|
||||
Ok(data) => {
|
||||
send_ingress_ref_opt(ingress_ref).await?;
|
||||
ingress.send_ingress(ingress_step).await?;
|
||||
return Ok(data);
|
||||
}
|
||||
Err(err) => tracing::error!(
|
||||
"failed to read assets index from cache: {err:?}, downloading...",
|
||||
),
|
||||
Err(err) => {
|
||||
tracing::error!("failed to read assets index from cache: {err:?}, downloading...",);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data = serde_json::from_slice(&http::download(
|
||||
Method::GET,
|
||||
&version.asset_index.url,
|
||||
path,
|
||||
None,
|
||||
ingress_ref,
|
||||
)
|
||||
.await?)?;
|
||||
let data = serde_json::from_slice(
|
||||
&http::download(
|
||||
Method::GET,
|
||||
&version.asset_index.url,
|
||||
path,
|
||||
None,
|
||||
ingress.map(|i| SubIngress::from_sub(i, ingress_step)).as_ref(),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
tracing::debug!("loaded assets index for version {}", version.id);
|
||||
|
||||
|
|
@ -157,7 +162,7 @@ pub async fn download_assets_index(
|
|||
pub async fn download_assets(
|
||||
legacy: bool,
|
||||
assets_index: &AssetsIndex,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<usize> {
|
||||
tracing::debug!("loading asssets");
|
||||
|
|
@ -171,8 +176,7 @@ pub async fn download_assets(
|
|||
|
||||
io::create_dir_all(&dir).await?;
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / len as f64));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
ingress.set_ingress_message("fetching assets").await?;
|
||||
|
||||
let requests = stream::iter(assets_index.objects.iter().map(|(name, asset)| async {
|
||||
let hash = &asset.hash;
|
||||
|
|
@ -191,9 +195,10 @@ pub async fn download_assets(
|
|||
None,
|
||||
None,
|
||||
Some((HashAlgorithm::Sha1, hash)),
|
||||
ingress_ref,
|
||||
ingress.ingress_sub(|total| total / len as f64).as_ref(),
|
||||
)
|
||||
.await.map(|_| ())
|
||||
.await
|
||||
.map(|_| ())
|
||||
} else {
|
||||
// TODO: Possibly check hash? (not sure if this is a good idea here)
|
||||
Ok(())
|
||||
|
|
@ -217,15 +222,20 @@ pub async fn download_assets(
|
|||
// MARK: Client
|
||||
pub async fn download_client(
|
||||
version: &VersionInfo,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<PathBuf> {
|
||||
tracing::debug!("loading client for version {}", version.id);
|
||||
let client = version.downloads
|
||||
let client = version
|
||||
.downloads
|
||||
.get(&DownloadType::Client)
|
||||
.ok_or_else(|| anyhow::anyhow!("no client downloads exist for {}", &version.id))?;
|
||||
|
||||
let path = Dirs::get_versions_dir().await?.join(version.id.clone()).join(format!("{}.jar", version.id));
|
||||
let path = Dirs::get_versions_dir()
|
||||
.await?
|
||||
.join(version.id.clone())
|
||||
.join(format!("{}.jar", version.id));
|
||||
|
||||
if path.exists() && !force.unwrap_or(false) {
|
||||
let calculated_hash = HashAlgorithm::Sha1
|
||||
.hash_file(&path)
|
||||
|
|
@ -240,10 +250,14 @@ pub async fn download_client(
|
|||
tracing::warn!("client hash mismatch, redownloading");
|
||||
}
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
|
||||
http::download(Method::GET, &client.url, &path, Some((HashAlgorithm::Sha1, &client.sha1)), ingress_ref).await?;
|
||||
http::download(
|
||||
Method::GET,
|
||||
&client.url,
|
||||
&path,
|
||||
Some((HashAlgorithm::Sha1, &client.sha1)),
|
||||
ingress,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
|
@ -254,7 +268,7 @@ pub async fn download_libraries(
|
|||
version: &str,
|
||||
libraries: &[Library],
|
||||
java_arch: &str,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<usize> {
|
||||
tracing::debug!("loading libraries for version {}", version);
|
||||
|
|
@ -266,13 +280,13 @@ pub async fn download_libraries(
|
|||
io::create_dir_all(&natives_dir).await?;
|
||||
|
||||
let num_files = libraries.len();
|
||||
|
||||
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / num_files as f64));
|
||||
let ingress_ref = ingress_ref.as_ref();
|
||||
let ingress = ingress.ingress_sub(|total| total / num_files as f64);
|
||||
let ingress = ingress.as_ref();
|
||||
ingress.set_ingress_message("fetching libraries").await?;
|
||||
|
||||
let requests = stream::iter(libraries.iter().map(|lib| async {
|
||||
if let Some(rules) = &lib.rules {
|
||||
if !validate_rules(rules, java_arch, lib.natives.is_some()) {
|
||||
if !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) {
|
||||
tracing::debug!("skipping library {} due to rules", lib.name);
|
||||
return Ok::<(), LauncherError>(());
|
||||
}
|
||||
|
|
@ -301,7 +315,7 @@ pub async fn download_libraries(
|
|||
&artifact.url,
|
||||
&path,
|
||||
Some((HashAlgorithm::Sha1, &artifact.sha1)),
|
||||
ingress_ref,
|
||||
ingress,
|
||||
).await?;
|
||||
|
||||
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
|
||||
|
|
@ -315,7 +329,7 @@ pub async fn download_libraries(
|
|||
&url,
|
||||
&path,
|
||||
None,
|
||||
ingress_ref,
|
||||
ingress,
|
||||
).await?;
|
||||
|
||||
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
|
||||
|
|
@ -335,7 +349,7 @@ pub async fn download_libraries(
|
|||
None,
|
||||
Some((HashAlgorithm::Sha1, &native.sha1)),
|
||||
None,
|
||||
ingress_ref,
|
||||
ingress,
|
||||
).await?;
|
||||
|
||||
let dest = natives_dir.join(version);
|
||||
|
|
@ -362,86 +376,37 @@ pub async fn download_libraries(
|
|||
Ok(failed)
|
||||
}
|
||||
|
||||
// MARK: Rules
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn validate_rules(rules: &[Rule], java_version: &str, updated: bool) -> bool {
|
||||
let mut rule = rules
|
||||
.iter()
|
||||
.map(|r| validate_rule(r, java_version, updated))
|
||||
.collect::<Vec<Option<bool>>>();
|
||||
if rules
|
||||
.iter()
|
||||
.all(|r| matches!(r.action, RuleAction::Disallow))
|
||||
{
|
||||
rule.push(Some(true));
|
||||
// MARK: Loader Version
|
||||
/// Gets the loader version for a given Minecraft version and loader
|
||||
/// If `loader_version` is `None`, it will return the latest stable version else
|
||||
/// it will return the specified version if found
|
||||
pub async fn get_loader_version(
|
||||
mc_version: &str,
|
||||
loader: GameLoader,
|
||||
loader_version: Option<&str>,
|
||||
) -> LauncherResult<Option<LoaderVersion>> {
|
||||
if loader == GameLoader::Vanilla {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
!(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(Option::is_none))
|
||||
}
|
||||
let state = State::get().await?;
|
||||
let metadata = state.metadata.read().await;
|
||||
let manifest = metadata.get_modded(loader)?;
|
||||
|
||||
/// Parses a Minecraft library feature or OS rule.
|
||||
/// Is disallowed -> Don't include it
|
||||
/// Is not allowed -> Don't include it
|
||||
/// Is allowed -> Include it
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn validate_rule(rule: &Rule, java_version: &str, updated: bool) -> Option<bool> {
|
||||
let result = match rule {
|
||||
Rule {
|
||||
os: Some(os), ..
|
||||
} => validate_os_rule(os, java_version, updated),
|
||||
Rule {
|
||||
features: Some(features),
|
||||
..
|
||||
} => {
|
||||
!features.is_demo_user.unwrap_or(true)
|
||||
|| features.has_custom_resolution.unwrap_or(false)
|
||||
|| !features.has_quick_plays_support.unwrap_or(true)
|
||||
|| !features.is_quick_play_multiplayer.unwrap_or(true)
|
||||
|| !features.is_quick_play_realms.unwrap_or(true)
|
||||
|| !features.is_quick_play_singleplayer.unwrap_or(true)
|
||||
}
|
||||
_ => return Some(true),
|
||||
let Some(loaders) = manifest.game_versions.iter().find(|it| {
|
||||
it.id
|
||||
.replace(interpulse::api::modded::DUMMY_REPLACE_STRING, mc_version)
|
||||
== mc_version
|
||||
}) else {
|
||||
return Err(MetadataError::NoMatchingVersion.into());
|
||||
};
|
||||
|
||||
match rule.action {
|
||||
RuleAction::Allow => {
|
||||
if result {
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
}
|
||||
RuleAction::Disallow => {
|
||||
if result {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
let loader_version = loaders
|
||||
.loaders
|
||||
.iter()
|
||||
.find(|it| loader_version.map_or(it.stable, |version| it.id == version))
|
||||
.cloned()
|
||||
.ok_or(MetadataError::NoMatchingLoader)?;
|
||||
|
||||
Ok(Some(loader_version))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
|
||||
let mut rule_match = true;
|
||||
|
||||
if let Some(ref arch) = rule.arch {
|
||||
rule_match &= !matches!(arch.as_str(), "x86" | "arm");
|
||||
}
|
||||
|
||||
if let Some(name) = &rule.name {
|
||||
if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) {
|
||||
rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name;
|
||||
} else {
|
||||
rule_match &= &Os::native_arch(java_arch) == name;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &rule.version {
|
||||
if let Ok(regex) = Regex::new(version.as_str()) {
|
||||
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
rule_match
|
||||
}
|
||||
|
|
@ -1 +1,4 @@
|
|||
pub mod metadata;
|
||||
pub mod metadata;
|
||||
pub mod launch;
|
||||
pub mod arguments;
|
||||
pub mod rules;
|
||||
85
packages/core/src/api/game/rules.rs
Normal file
85
packages/core/src/api/game/rules.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use interpulse::api::minecraft::{Os, OsRule, Rule, RuleAction};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::utils::os_ext::OsExt;
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub fn validate_rules(rules: &[Rule], java_arch: &str, updated: bool) -> bool {
|
||||
let mut rule = rules
|
||||
.iter()
|
||||
.map(|r| validate_rule(r, java_arch, updated))
|
||||
.collect::<Vec<Option<bool>>>();
|
||||
if rules
|
||||
.iter()
|
||||
.all(|r| matches!(r.action, RuleAction::Disallow))
|
||||
{
|
||||
rule.push(Some(true));
|
||||
}
|
||||
|
||||
!(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(Option::is_none))
|
||||
}
|
||||
|
||||
/// Parses a Minecraft library feature or OS rule.
|
||||
/// Is disallowed -> Don't include it
|
||||
/// Is not allowed -> Don't include it
|
||||
/// Is allowed -> Include it
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub fn validate_rule(rule: &Rule, java_arch: &str, updated: bool) -> Option<bool> {
|
||||
let result = match rule {
|
||||
Rule { os: Some(os), .. } => validate_os_rule(os, java_arch, updated),
|
||||
Rule {
|
||||
features: Some(features),
|
||||
..
|
||||
} => {
|
||||
!features.is_demo_user.unwrap_or(true)
|
||||
|| features.has_custom_resolution.unwrap_or(false)
|
||||
|| !features.has_quick_plays_support.unwrap_or(true)
|
||||
|| !features.is_quick_play_multiplayer.unwrap_or(true)
|
||||
|| !features.is_quick_play_realms.unwrap_or(true)
|
||||
|| !features.is_quick_play_singleplayer.unwrap_or(true)
|
||||
}
|
||||
_ => return Some(true),
|
||||
};
|
||||
|
||||
match rule.action {
|
||||
RuleAction::Allow => {
|
||||
if result {
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
}
|
||||
RuleAction::Disallow => {
|
||||
if result {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
|
||||
let mut rule_match = true;
|
||||
|
||||
if let Some(ref arch) = rule.arch {
|
||||
rule_match &= !matches!(arch.as_str(), "x86" | "arm");
|
||||
}
|
||||
|
||||
if let Some(name) = &rule.name {
|
||||
if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) {
|
||||
rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name;
|
||||
} else {
|
||||
rule_match &= &Os::native_arch(java_arch) == name;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &rule.version {
|
||||
if let Ok(regex) = Regex::new(version.as_str()) {
|
||||
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
rule_match
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
use crate::{store::{ingress::{IngressId, IngressRef, IngressType}, State}, LauncherResult};
|
||||
use crate::LauncherResult;
|
||||
use crate::store::State;
|
||||
use crate::store::ingress::{IngressId, IngressType, SubIngress};
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn init_ingress(
|
||||
|
|
@ -9,7 +11,9 @@ pub async fn init_ingress(
|
|||
let state = State::get().await?;
|
||||
let processor = &state.ingress_processor;
|
||||
|
||||
let id = processor.create(ingress_type, message.to_string(), total).await?;
|
||||
let id = processor
|
||||
.create(ingress_type, message.to_string(), total)
|
||||
.await?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
|
@ -28,69 +32,168 @@ pub async fn init_ingress_opt(
|
|||
}
|
||||
}
|
||||
|
||||
/// Sends an ingress update with the given increment and message
|
||||
#[tracing::instrument]
|
||||
pub async fn send_ingress(id: impl AsRef<IngressId> + std::fmt::Debug, increment: f64) -> LauncherResult<()> {
|
||||
pub async fn send_ingress_message(
|
||||
id: &IngressId,
|
||||
increment: f64,
|
||||
message: Option<&str>,
|
||||
) -> LauncherResult<()> {
|
||||
if !State::initialized() {
|
||||
tracing::debug!("attempted to send ingress when state is not initialized");
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let processor = &state.ingress_processor;
|
||||
|
||||
processor.send(id, increment).await?;
|
||||
processor
|
||||
.send(id, increment, message.map(String::from))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_ingress_opt(id: Option<impl AsRef<IngressId> + std::fmt::Debug>, increment: f64) -> LauncherResult<()> {
|
||||
if let Some(id) = id {
|
||||
send_ingress(id, increment).await
|
||||
} else {
|
||||
Ok(())
|
||||
/// Sends an ingress update with the given increment
|
||||
pub async fn send_ingress(
|
||||
id: &IngressId,
|
||||
increment: f64,
|
||||
) -> LauncherResult<()> {
|
||||
send_ingress_message(id, increment, None).await
|
||||
}
|
||||
|
||||
/// Sends an ingress update with the given message (does not increment)
|
||||
pub async fn set_ingress_message(
|
||||
id: &IngressId,
|
||||
message: &str,
|
||||
) -> LauncherResult<()> {
|
||||
send_ingress_message(id, 0.0, Some(message)).await
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait IngressSendExt {
|
||||
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()>;
|
||||
async fn send_ingress(&self, increment: f64) -> LauncherResult<()>;
|
||||
async fn set_ingress_message(&self, message: &str) -> LauncherResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IngressSendExt for IngressId {
|
||||
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
|
||||
send_ingress_message(self, increment, message).await
|
||||
}
|
||||
|
||||
async fn send_ingress(&self, increment: f64) -> LauncherResult<()> {
|
||||
send_ingress(self, increment).await
|
||||
}
|
||||
|
||||
async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> {
|
||||
set_ingress_message(self, message).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_ingress_ref(ingress_ref: &IngressRef<'_>) -> LauncherResult<()> {
|
||||
let state = State::get().await?;
|
||||
let processor = &state.ingress_processor;
|
||||
#[async_trait::async_trait]
|
||||
impl IngressSendExt for Option<&IngressId> {
|
||||
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
|
||||
if let Some(id) = self {
|
||||
id.send_ingress_message(increment, message).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
processor.send(ingress_ref, ingress_ref.increment_by).await?;
|
||||
Ok(())
|
||||
async fn send_ingress(&self, increment: f64) -> LauncherResult<()> {
|
||||
if let Some(id) = self {
|
||||
id.send_ingress(increment).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> {
|
||||
if let Some(id) = self {
|
||||
id.set_ingress_message(message).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_ingress_ref_opt(ingress_ref: Option<&IngressRef<'_>>) -> LauncherResult<()> {
|
||||
if let Some(ingress_ref) = ingress_ref {
|
||||
send_ingress_ref(ingress_ref).await
|
||||
} else {
|
||||
Ok(())
|
||||
#[async_trait::async_trait]
|
||||
impl IngressSendExt for &SubIngress<'_> {
|
||||
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
|
||||
self.id.send_ingress_message(increment, message).await
|
||||
}
|
||||
|
||||
async fn send_ingress(&self, increment: f64) -> LauncherResult<()> {
|
||||
self.id.send_ingress(increment).await
|
||||
}
|
||||
|
||||
async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> {
|
||||
self.id.set_ingress_message(message).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IngressSendExt for Option<&SubIngress<'_>> {
|
||||
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
|
||||
if let Some(id) = self {
|
||||
id.send_ingress_message(increment, message).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_ingress(&self, increment: f64) -> LauncherResult<()> {
|
||||
if let Some(id) = self {
|
||||
id.send_ingress(increment).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> {
|
||||
if let Some(id) = self {
|
||||
id.set_ingress_message(message).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::api::ingress::{init_ingress, IngressSendExt};
|
||||
use crate::api::proxy::ProxyDynamic;
|
||||
use crate::initialize_core;
|
||||
use crate::store::CoreOptions;
|
||||
use crate::store::ingress::IngressType;
|
||||
use std::time::Duration;
|
||||
use crate::{api::{ingress::{init_ingress, send_ingress}, proxy::ProxyDynamic}, initialize_core, store::{ingress::IngressType, CoreOptions}};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn create_and_update_ingress() -> crate::LauncherResult<()> {
|
||||
|
||||
initialize_core(CoreOptions::default(), ProxyDynamic::new()).await?;
|
||||
|
||||
let id = init_ingress(IngressType::Download { file_name: "Some-Mod-1.8.9.jar".into() }, "This is a test message", 100.0).await?;
|
||||
let id = init_ingress(
|
||||
IngressType::Download {
|
||||
file_name: "Some-Mod-1.8.9.jar".into(),
|
||||
},
|
||||
"this is a test message",
|
||||
100.0,
|
||||
)
|
||||
.await?;
|
||||
tokio::time::sleep(Duration::from_millis(2350)).await;
|
||||
|
||||
send_ingress(&id, 10.0).await?;
|
||||
id.send_ingress(10.0).await?;
|
||||
tokio::time::sleep(Duration::from_millis(2500)).await;
|
||||
|
||||
send_ingress(&id, 20.0).await?;
|
||||
id.send_ingress(20.0).await?;
|
||||
tokio::time::sleep(Duration::from_millis(2500)).await;
|
||||
|
||||
send_ingress(&id, 70.0).await?;
|
||||
id.send_ingress(70.0).await?;
|
||||
tokio::time::sleep(Duration::from_millis(2500)).await;
|
||||
|
||||
drop(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use onelauncher_entity::java_versions;
|
||||
use sea_orm::{ActiveValue::Set, ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder};
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder};
|
||||
|
||||
use crate::{error::LauncherResult, store::State};
|
||||
|
||||
use super::{JavaError, JavaInfo};
|
||||
|
||||
/// Returns the latest Java version
|
||||
pub async fn get_latest_java() -> LauncherResult<Option<java_versions::Model>> {
|
||||
let db = &State::get().await?.db;
|
||||
|
||||
Ok(java_versions::Entity::find()
|
||||
.order_by(java_versions::Column::MajorVersion, Order::Desc)
|
||||
.order_by(java_versions::Column::FullVersion, Order::Desc)
|
||||
.one(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`]
|
||||
pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult<java_versions::ActiveModel> {
|
||||
let major_version = if info.java_version[0..2].eq("1.") {
|
||||
|
|
@ -14,13 +25,16 @@ pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult<j
|
|||
} else {
|
||||
let split = info.java_version.split_once('.').unwrap_or_default();
|
||||
split.0.parse::<i32>()
|
||||
}.map_err(JavaError::from)?;
|
||||
}.or_else(
|
||||
|_| info.java_version.parse::<i32>()
|
||||
).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?;
|
||||
|
||||
Ok(java_versions::ActiveModel {
|
||||
absolute_path: Set(absolute_path.to_string_lossy().to_string()),
|
||||
major_version: Set(major_version),
|
||||
full_version: Set(info.java_version.clone()),
|
||||
vendor_name: Set(info.java_vendor.clone()),
|
||||
arch: Set(info.os_arch.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
|
@ -46,7 +60,7 @@ pub async fn get_java_by_id(id: i32) -> LauncherResult<Option<java_versions::Mod
|
|||
}
|
||||
|
||||
/// Returns all versions of Java for a given major version
|
||||
pub async fn get_java_by_major(major: i32) -> LauncherResult<Vec<java_versions::Model>> {
|
||||
pub async fn get_all_java_by_major(major: i32) -> LauncherResult<Vec<java_versions::Model>> {
|
||||
let db = &State::get().await?.db;
|
||||
|
||||
Ok(java_versions::Entity::find()
|
||||
|
|
@ -56,6 +70,17 @@ pub async fn get_java_by_major(major: i32) -> LauncherResult<Vec<java_versions::
|
|||
.await?)
|
||||
}
|
||||
|
||||
/// Returns the latest Java version for a given major version
|
||||
pub async fn get_latest_java_by_major(major: i32) -> LauncherResult<Option<java_versions::Model>> {
|
||||
let db = &State::get().await?.db;
|
||||
|
||||
Ok(java_versions::Entity::find()
|
||||
.filter(java_versions::Column::MajorVersion.eq(major))
|
||||
.order_by(java_versions::Column::FullVersion, Order::Desc)
|
||||
.one(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Accepts a path to a JRE folder and returns the inserted entry
|
||||
pub async fn insert_java(absolute_path: PathBuf, info: JavaInfo) -> LauncherResult<java_versions::Model> {
|
||||
let db = &State::get().await?.db;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use interpulse::api::minecraft::VersionInfo;
|
||||
use onelauncher_entity::{java_versions, setting_profiles};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{api::ingress::{init_ingress, init_ingress_opt, send_ingress, send_ingress_opt}, error::LauncherResult, store::ingress::IngressType, utils::io};
|
||||
use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::IngressType;
|
||||
use crate::utils::io;
|
||||
|
||||
pub mod dao;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JavaError {
|
||||
#[error("failed to parse version")]
|
||||
ParseVersion(#[from] std::num::ParseIntError),
|
||||
#[error("failed to parse version '{0}'")]
|
||||
ParseVersion(String, #[source] std::num::ParseIntError),
|
||||
#[error("failed to execute java command")]
|
||||
Execute(#[from] std::io::Error),
|
||||
}
|
||||
|
|
@ -46,16 +52,25 @@ pub struct JavaInfo {
|
|||
const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../../assets/java/JavaInfo.class");
|
||||
|
||||
/// Accepts a path to a java executable and returns the [`JavaInfo`]
|
||||
pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) -> LauncherResult<JavaInfo> {
|
||||
let id = init_ingress_opt(with_ingress, IngressType::JavaCheck, "Checking JRE information", 100.0).await?;
|
||||
pub async fn check_java_runtime(
|
||||
absolute_path: &PathBuf,
|
||||
with_ingress: bool,
|
||||
) -> LauncherResult<JavaInfo> {
|
||||
let id = init_ingress_opt(
|
||||
with_ingress,
|
||||
IngressType::JavaCheck,
|
||||
"checking JRE information",
|
||||
100.0,
|
||||
)
|
||||
.await?;
|
||||
let id = id.as_ref();
|
||||
|
||||
let dir = io::tempdir()?;
|
||||
let file = dir.path().join("JavaInfo.class");
|
||||
send_ingress_opt(id, 25.0).await?;
|
||||
id.map(async |id| send_ingress(id, 25.0).await);
|
||||
|
||||
io::write(&file, JAVA_INFO_CLASS).await?;
|
||||
send_ingress_opt(id, 25.0).await?;
|
||||
id.map(async |id| send_ingress(id, 25.0).await);
|
||||
|
||||
let java_info = tokio::process::Command::new(absolute_path)
|
||||
.arg("-cp")
|
||||
|
|
@ -67,9 +82,10 @@ pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) ->
|
|||
.map_err(JavaError::from)?;
|
||||
|
||||
let java_info = String::from_utf8_lossy(&java_info.stdout);
|
||||
send_ingress_opt(id, 50.0).await?;
|
||||
id.map(async |id| send_ingress(id, 50.0).await);
|
||||
|
||||
let info = java_info.lines()
|
||||
let info = java_info
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let mut parts = line.splitn(2, '=');
|
||||
let key = parts.next().unwrap_or("unknown");
|
||||
|
|
@ -80,17 +96,57 @@ pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) ->
|
|||
.collect::<HashMap<_, _>>();
|
||||
|
||||
Ok(JavaInfo {
|
||||
os_arch: info.get("os.arch").cloned().unwrap_or_else(|| String::from("unknown")),
|
||||
java_version: info.get("java.version").cloned().unwrap_or_else(|| String::from("unknown")),
|
||||
java_vendor: info.get("java.vendor").cloned().unwrap_or_else(|| String::from("unknown")),
|
||||
os_arch: info
|
||||
.get("os.arch")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from("unknown")),
|
||||
java_version: info
|
||||
.get("java.version")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from("unknown")),
|
||||
java_vendor: info
|
||||
.get("java.vendor")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from("unknown")),
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Recommended Java Version
|
||||
/// Gets the recommended java version for the given [`VersionInfo`] and optionally profile (excl. "Global").
|
||||
pub async fn get_recommended_java(
|
||||
info: &VersionInfo,
|
||||
profile: Option<&setting_profiles::Model>,
|
||||
) -> LauncherResult<Option<java_versions::Model>> {
|
||||
// Settings profile is an override (it has highest priority)
|
||||
if let Some(profile) = profile
|
||||
&& !profile.is_global()
|
||||
{
|
||||
if let Some(java_id) = profile.java_id {
|
||||
if let Ok(Some(java)) = dao::get_java_by_id(java_id).await {
|
||||
return Ok(Some(java));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the version info has a suggested version
|
||||
let Some(supported_ver) = &info.java_version else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
dao::get_latest_java_by_major(supported_ver.major_version as i32).await
|
||||
}
|
||||
|
||||
/// Attempts to scan common paths for Java installations.
|
||||
///
|
||||
/// **Can be heavy on I/O, don't run often!**
|
||||
pub async fn locate_java() -> LauncherResult<HashMap<PathBuf, JavaInfo>> {
|
||||
let id = init_ingress(IngressType::JavaLocate, "Locating java installations", 110.0).await?;
|
||||
let id = init_ingress(
|
||||
IngressType::JavaLocate,
|
||||
"locating java installations",
|
||||
110.0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let paths = internal_locate_java();
|
||||
let mut valid = HashMap::new();
|
||||
|
|
@ -103,7 +159,7 @@ pub async fn locate_java() -> LauncherResult<HashMap<PathBuf, JavaInfo>> {
|
|||
send_ingress(&id, step_amount).await?;
|
||||
|
||||
let Ok(info) = check_java_runtime(&path, false).await else {
|
||||
tracing::warn!("Java installation at '{}' is not valid", path.display());
|
||||
tracing::warn!("java installation at '{}' is not valid", path.display());
|
||||
continue;
|
||||
};
|
||||
|
||||
|
|
@ -171,13 +227,14 @@ fn internal_locate_java() -> Vec<PathBuf> {
|
|||
r"/opt/jdk",
|
||||
r"/opt/jdks",
|
||||
r"/app/jdk",
|
||||
r"~/.jdks", // Intellij downloaded jdks
|
||||
r"~/.gradle/jdks", // Gradle downloaded jdks
|
||||
r"~/.jdks", // Intellij downloaded jdks
|
||||
r"~/.gradle/jdks", // Gradle downloaded jdks
|
||||
r"~/.sdkman/candidates/java", // SDKMAN downloaded jdks
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
let path = PathBuf::from(path.replace('~', std::env::var("HOME").unwrap_or_default().as_str()));
|
||||
let path =
|
||||
PathBuf::from(path.replace('~', std::env::var("HOME").unwrap_or_default().as_str()));
|
||||
|
||||
if let Ok(children) = std::fs::read_dir(path) {
|
||||
for child in children.flatten() {
|
||||
|
|
@ -187,8 +244,7 @@ fn internal_locate_java() -> Vec<PathBuf> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ pub mod cluster;
|
|||
pub mod ingress;
|
||||
pub mod game;
|
||||
pub mod proxy;
|
||||
pub mod java;
|
||||
pub mod java;
|
||||
pub mod setting_profiles;
|
||||
60
packages/core/src/api/setting_profiles.rs
Normal file
60
packages/core/src/api/setting_profiles.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use onelauncher_entity::setting_profiles;
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait};
|
||||
|
||||
use crate::{error::{DaoError, LauncherResult}, store::State};
|
||||
|
||||
pub async fn get_all_profiles() -> LauncherResult<Vec<setting_profiles::Model>> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
Ok(setting_profiles::Entity::find()
|
||||
.all(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult<setting_profiles::Model> {
|
||||
if let Some(name) = name {
|
||||
let profile = get_profile_by_name(name).await?;
|
||||
if let Some(profile) = profile {
|
||||
return Ok(profile);
|
||||
}
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let settings = &state.settings.read().await;
|
||||
Ok(settings.global_game_settings.clone())
|
||||
}
|
||||
|
||||
pub async fn get_profile_by_name(name: &str) -> LauncherResult<Option<setting_profiles::Model>> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
Ok(setting_profiles::Entity::find_by_id(name)
|
||||
.one(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::Model> {
|
||||
if let Some(name) = profile.name.try_as_ref() {
|
||||
if name.is_empty() || profile.is_global() {
|
||||
return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into());
|
||||
}
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
Ok(profile.insert(db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?;
|
||||
|
||||
if deleted.rows_affected == 0 {
|
||||
return Err(DaoError::NotFound.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -80,3 +80,4 @@ pub const ARCH_WIDTH: &str = if cfg!(target_pointer_width = "64") {
|
|||
|
||||
/// Evaluates to `\r\n` on windows, and `\n` on everything else.
|
||||
pub const LINE_ENDING: &str = if cfg!(windows) { "\r\n" } else { "\n" };
|
||||
pub const CLASSPATH_SEPARATOR: &str = if cfg!(windows) { ";" } else { ":" };
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ pub enum LauncherError {
|
|||
MetadataError(#[from] crate::store::metadata::MetadataError),
|
||||
#[error(transparent)]
|
||||
DaoError(#[from] DaoError),
|
||||
#[error(transparent)]
|
||||
ClusterError(#[from] crate::api::cluster::ClusterError),
|
||||
|
||||
#[error(transparent)]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
|
|
@ -42,5 +44,7 @@ pub enum DaoError {
|
|||
#[error("entity was not found")]
|
||||
NotFound,
|
||||
#[error("entity already exists")]
|
||||
AlreadyExists
|
||||
AlreadyExists,
|
||||
#[error("invalid value '{0}' for {1}")]
|
||||
InvalidValue(String, String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use onelauncher_migration::MigratorTrait;
|
||||
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
|
||||
|
||||
use crate::{utils::io::IOError, LauncherResult};
|
||||
|
|
@ -20,6 +21,8 @@ pub async fn create_pool_from_path(path: PathBuf) -> LauncherResult<DatabaseConn
|
|||
|
||||
let db = Database::connect(opts).await?;
|
||||
|
||||
onelauncher_migration::Migrator::up(&db, None).await?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use uuid::Uuid;
|
|||
|
||||
use crate::LauncherResult;
|
||||
|
||||
use super::{proxy::ProxyState, State};
|
||||
use super::State;
|
||||
use super::proxy::ProxyState;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct IngressProcessor {
|
||||
|
|
@ -32,7 +33,12 @@ impl IngressProcessor {
|
|||
processor
|
||||
}
|
||||
|
||||
pub async fn create(&self, ingress_type: IngressType, message: String, total: f64) -> LauncherResult<IngressId> {
|
||||
pub async fn create(
|
||||
&self,
|
||||
ingress_type: IngressType,
|
||||
message: String,
|
||||
total: f64,
|
||||
) -> LauncherResult<IngressId> {
|
||||
let mut feeds = self.ingress_feeds.write().await;
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
|
|
@ -52,80 +58,51 @@ impl IngressProcessor {
|
|||
// Drop the write lock to prevent deadlock
|
||||
drop(feeds);
|
||||
|
||||
self.send(&ingress_id, 0.0).await?;
|
||||
self.send(&ingress_id, 0.0, None).await?;
|
||||
|
||||
Ok(ingress_id)
|
||||
}
|
||||
|
||||
pub async fn send(&self, id: impl AsRef<IngressId>, increment: f64) -> LauncherResult<()> {
|
||||
pub async fn send(
|
||||
&self,
|
||||
id: &IngressId,
|
||||
increment: f64,
|
||||
message: Option<String>,
|
||||
) -> LauncherResult<()> {
|
||||
let mut feeds = self.ingress_feeds.write().await;
|
||||
let uuid = &id.as_ref().0;
|
||||
let uuid = &id.0;
|
||||
let ingress = feeds.get_mut(uuid).ok_or(IngressError::NotFound)?;
|
||||
|
||||
let proxy = ProxyState::get()?;
|
||||
|
||||
ingress.current += increment;
|
||||
proxy.send_ingress(IngressPayload {
|
||||
id: uuid.to_owned(),
|
||||
message: ingress.message.clone(),
|
||||
ingress_type: ingress.ingress_type.clone(),
|
||||
percent: Some(ingress.current / ingress.total),
|
||||
total: ingress.total,
|
||||
}).await?;
|
||||
if let Some(message) = message {
|
||||
ingress.message = message;
|
||||
}
|
||||
|
||||
proxy
|
||||
.send_ingress(IngressPayload {
|
||||
id: uuid.to_owned(),
|
||||
message: ingress.message.clone(),
|
||||
ingress_type: ingress.ingress_type.clone(),
|
||||
percent: Some(ingress.current / ingress.total),
|
||||
total: ingress.total,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_message(&self, id: &IngressId, message: String) -> LauncherResult<()> {
|
||||
self.send(id, 0.0, Some(message)).await
|
||||
}
|
||||
|
||||
pub async fn remove(&self, uuid: Uuid) -> LauncherResult<Ingress> {
|
||||
let mut feeds = self.ingress_feeds.write().await;
|
||||
Ok(feeds.remove(&uuid).ok_or(IngressError::NotFound)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IngressId(pub Uuid);
|
||||
|
||||
impl AsRef<Self> for IngressId {
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to pass around an ingress id and the increment value, usually passed in helper / utility functions
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IngressRef<'a> {
|
||||
pub ingress_id: &'a IngressId,
|
||||
pub increment_by: f64,
|
||||
}
|
||||
|
||||
unsafe impl Send for IngressRef<'_> {}
|
||||
unsafe impl Sync for IngressRef<'_> {}
|
||||
|
||||
impl<'a> IngressRef<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(ingress_id: &'a IngressId, increment_by: f64) -> Self {
|
||||
Self {
|
||||
ingress_id,
|
||||
increment_by,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn with_increment(&self, increment_by: f64) -> Self {
|
||||
Self {
|
||||
ingress_id: self.ingress_id,
|
||||
increment_by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<IngressId> for IngressRef<'_> {
|
||||
fn as_ref(&self) -> &IngressId {
|
||||
self.ingress_id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ingress {
|
||||
pub id: Uuid,
|
||||
|
|
@ -149,12 +126,84 @@ pub struct IngressPayload {
|
|||
#[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub enum IngressType {
|
||||
Download {
|
||||
file_name: String,
|
||||
},
|
||||
Download { file_name: String },
|
||||
JavaCheck,
|
||||
JavaLocate,
|
||||
MinecraftDownload,
|
||||
PrepareCluster { cluster_name: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IngressId(pub Uuid);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubIngress<'a> {
|
||||
pub id: &'a IngressId,
|
||||
pub total: f64,
|
||||
}
|
||||
|
||||
// TODO: Rewrite the ingress system to use a proper parent <-> child system
|
||||
impl<'a> SubIngress<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(id: &'a IngressId, total: f64) -> Self {
|
||||
Self { id, total }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_sub(sub: &'a SubIngress<'_>, total: f64) -> Self {
|
||||
Self { id: sub.id, total }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SubIngressExt {
|
||||
type Target<'sub>
|
||||
where
|
||||
Self: 'sub;
|
||||
|
||||
type Total;
|
||||
|
||||
fn ingress_total(&self) -> Self::Total;
|
||||
fn ingress_sub<F>(&self, total: F) -> Self::Target<'_>
|
||||
where
|
||||
F: FnOnce(f64) -> f64;
|
||||
}
|
||||
|
||||
impl SubIngressExt for SubIngress<'_> {
|
||||
type Target<'sub>
|
||||
= SubIngress<'sub>
|
||||
where
|
||||
Self: 'sub;
|
||||
|
||||
type Total = f64;
|
||||
|
||||
fn ingress_total(&self) -> Self::Total {
|
||||
self.total
|
||||
}
|
||||
|
||||
fn ingress_sub<F>(&self, total: F) -> Self::Target<'_>
|
||||
where
|
||||
F: FnOnce(f64) -> f64 {
|
||||
SubIngress::from_sub(self, total(self.total))
|
||||
}
|
||||
}
|
||||
|
||||
impl SubIngressExt for Option<&SubIngress<'_>> {
|
||||
type Target<'sub>
|
||||
= Option<SubIngress<'sub>>
|
||||
where
|
||||
Self: 'sub;
|
||||
|
||||
type Total = Option<f64>;
|
||||
|
||||
fn ingress_total(&self) -> Self::Total {
|
||||
self.map(|sub| sub.total)
|
||||
}
|
||||
|
||||
fn ingress_sub<F>(&self, total: F) -> Self::Target<'_>
|
||||
where
|
||||
F: FnOnce(f64) -> f64 {
|
||||
self.map(|sub| SubIngress::from_sub(sub, total(sub.total)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for IngressId {
|
||||
|
|
@ -170,22 +219,29 @@ impl Drop for IngressId {
|
|||
|
||||
let proxy = ProxyState::get()?;
|
||||
|
||||
proxy.send_ingress(IngressPayload {
|
||||
id: ingress.id,
|
||||
message: "Completed".into(),
|
||||
ingress_type: ingress.ingress_type,
|
||||
percent: None,
|
||||
total: ingress.total,
|
||||
}).await?;
|
||||
proxy
|
||||
.send_ingress(IngressPayload {
|
||||
id: ingress.id,
|
||||
message: "Completed".into(),
|
||||
ingress_type: ingress.ingress_type,
|
||||
percent: None,
|
||||
total: ingress.total,
|
||||
})
|
||||
.await?;
|
||||
|
||||
tracing::trace!("exited at {}% for ingress {:?}", ingress.current / ingress.total * 100.0, ingress_uuid);
|
||||
tracing::trace!(
|
||||
"exited at {}% for ingress {:?}",
|
||||
ingress.current / ingress.total * 100.0,
|
||||
ingress_uuid
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}.await;
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::error!("failed to finish ingress: {:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ pub enum MetadataError {
|
|||
NotVanillaManifest(GameLoader),
|
||||
#[error("failed to parse metadata")]
|
||||
ParseError(#[from] serde_json::Error),
|
||||
#[error("no matching loader found")]
|
||||
NoMatchingLoader,
|
||||
#[error("no matching version found")]
|
||||
NoMatchingVersion,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
|
|
@ -50,33 +54,38 @@ impl Metadata {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn initialize() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_vanilla(&mut self, loader: GameLoader) -> LauncherResult<&VanillaManifest> {
|
||||
if loader.is_mod_loader() {
|
||||
return Err(MetadataError::NotVanillaManifest(loader).into());
|
||||
}
|
||||
|
||||
pub async fn get_vanilla_or_fetch(&mut self) -> LauncherResult<&VanillaManifest> {
|
||||
if !self.initialized() {
|
||||
self.load().await?;
|
||||
self.initialize().await?;
|
||||
}
|
||||
|
||||
match loader {
|
||||
GameLoader::Vanilla => self.inner.minecraft.as_ref(),
|
||||
_ => None,
|
||||
}.ok_or_else(|| MetadataError::FetchError.into())
|
||||
self.get_vanilla()
|
||||
}
|
||||
|
||||
pub async fn get_modded(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> {
|
||||
if !loader.is_mod_loader() {
|
||||
pub async fn get_modded_or_fetch(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> {
|
||||
if !loader.is_modded() {
|
||||
return Err(MetadataError::NotModdedManifest(loader).into());
|
||||
}
|
||||
|
||||
if !self.initialized() {
|
||||
self.load().await?;
|
||||
self.initialize().await?;
|
||||
}
|
||||
|
||||
self.get_modded(loader)
|
||||
}
|
||||
|
||||
pub fn get_vanilla(&self) -> LauncherResult<&VanillaManifest> {
|
||||
self.inner.minecraft.as_ref().ok_or_else(|| MetadataError::FetchError.into())
|
||||
}
|
||||
|
||||
pub fn get_modded(&self, loader: GameLoader) -> LauncherResult<&ModdedManifest> {
|
||||
if !loader.is_modded() {
|
||||
return Err(MetadataError::NotModdedManifest(loader).into());
|
||||
}
|
||||
|
||||
match loader {
|
||||
|
|
@ -88,8 +97,8 @@ impl Metadata {
|
|||
}.ok_or_else(|| MetadataError::FetchError.into())
|
||||
}
|
||||
|
||||
|
||||
async fn load(&mut self) -> LauncherResult<()> {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn initialize(&mut self) -> LauncherResult<()> {
|
||||
let path = Dirs::get_caches_dir().await?.join("metadata.json");
|
||||
let mut save_file = false;
|
||||
|
||||
|
|
@ -178,10 +187,10 @@ impl Metadata {
|
|||
let ($($var),*) = tokio::join!(
|
||||
$(
|
||||
async move {
|
||||
if let Ok(loader) = GameLoader::from_str(stringify!(minecraft)) {
|
||||
if let Ok(loader) = GameLoader::from_str(stringify!($var)) {
|
||||
fetch_manifest!(loader, $is_modded).map_err(|e| $crate::send_error!("couldn't fetch {loader} manifest: {e}")).ok()
|
||||
} else {
|
||||
tracing::error!("failed to fetch {} manifest", stringify!($var));
|
||||
tracing::error!("loader '{}' is not supported", stringify!($var));
|
||||
None
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub struct Settings {
|
|||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
global_game_settings: default_global_game_settings(),
|
||||
global_game_settings: setting_profiles::Model::default_global_profile(),
|
||||
allow_parallel_running_clusters: false,
|
||||
discord_enabled: false,
|
||||
enable_gamemode: false,
|
||||
|
|
@ -55,20 +55,4 @@ impl Settings {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_global_game_settings() -> setting_profiles::Model {
|
||||
setting_profiles::Model {
|
||||
name: "Global".into(),
|
||||
force_fullscreen: Some(false),
|
||||
hook_post: None,
|
||||
hook_pre: None,
|
||||
hook_wrapper: None,
|
||||
java_id: None,
|
||||
launch_args: None,
|
||||
launch_env: None,
|
||||
mem_max: Some(3072),
|
||||
res_h: None,
|
||||
res_w: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ impl State {
|
|||
let ingress_processor = IngressProcessor::new();
|
||||
let settings = Settings::new().await;
|
||||
let db = super::db::create_pool().await?;
|
||||
let metadata = Metadata::initialize();
|
||||
let metadata = Metadata::new();
|
||||
|
||||
let rpc = match DiscordRPC::initialize() {
|
||||
Ok(rpc) => Some(rpc),
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ use serde::Deserialize;
|
|||
use tokio::sync::Semaphore;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::api::ingress::send_ingress;
|
||||
use crate::api::ingress::IngressSendExt;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::IngressRef;
|
||||
use crate::store::ingress::{SubIngress, SubIngressExt};
|
||||
use crate::store::semaphore::SemaphoreStore;
|
||||
use crate::store::Core;
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
|||
.expect("failed to build reqwest client!")
|
||||
});
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(body, headers, ingress_ref))]
|
||||
#[tracing::instrument(level = "debug", skip(body, headers, ingress))]
|
||||
#[onelauncher_macro::pin]
|
||||
pub async fn fetch_advanced(
|
||||
method: Method,
|
||||
|
|
@ -57,7 +57,7 @@ pub async fn fetch_advanced(
|
|||
body: Option<serde_json::Value>,
|
||||
hash: Option<(HashAlgorithm, &str)>,
|
||||
headers: Option<HashMap<&str, &str>>,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
) -> LauncherResult<Bytes> {
|
||||
let semaphore = SemaphoreStore::fetch().await;
|
||||
let permit = semaphore.acquire().await;
|
||||
|
|
@ -95,7 +95,7 @@ pub async fn fetch_advanced(
|
|||
// Drop the fetch permit as we are no longer fetching
|
||||
drop(permit);
|
||||
|
||||
let bytes = if let Some(ingress_ref) = ingress_ref {
|
||||
let bytes = if let Some(ingress) = ingress {
|
||||
let length = res.content_length();
|
||||
if let Some(total_size) = length {
|
||||
let mut stream = res.bytes_stream();
|
||||
|
|
@ -106,11 +106,9 @@ pub async fn fetch_advanced(
|
|||
item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?;
|
||||
bytes.append(&mut chunk.to_vec());
|
||||
|
||||
send_ingress(
|
||||
ingress_ref,
|
||||
(chunk.len() as f64 / total_size as f64) * ingress_ref.increment_by,
|
||||
)
|
||||
.await?;
|
||||
ingress.send_ingress(
|
||||
(chunk.len() as f64 / total_size as f64) * ingress.total,
|
||||
).await?;
|
||||
}
|
||||
|
||||
Ok(Bytes::from(bytes))
|
||||
|
|
@ -153,9 +151,9 @@ pub async fn fetch_json<T: for<'de> Deserialize<'de>>(
|
|||
method: Method,
|
||||
url: &str,
|
||||
body: Option<serde_json::Value>,
|
||||
ingress_ref: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
) -> LauncherResult<T> {
|
||||
fetch_json_advanced(method, url, body, None, None, ingress_ref).await
|
||||
fetch_json_advanced(method, url, body, None, None, ingress).await
|
||||
}
|
||||
|
||||
pub async fn fetch_json_advanced<T: for<'de> Deserialize<'de>>(
|
||||
|
|
@ -164,7 +162,7 @@ pub async fn fetch_json_advanced<T: for<'de> Deserialize<'de>>(
|
|||
body: Option<serde_json::Value>,
|
||||
headers: Option<HashMap<&str, &str>>,
|
||||
hash: Option<(HashAlgorithm, &str)>,
|
||||
ingress: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
) -> LauncherResult<T> {
|
||||
let bytes = fetch_advanced(method, url, body, hash, headers, ingress).await?;
|
||||
Ok(serde_json::from_slice(&bytes)?)
|
||||
|
|
@ -177,22 +175,23 @@ pub async fn download_advanced(
|
|||
body: Option<serde_json::Value>,
|
||||
headers: Option<HashMap<&str, &str>>,
|
||||
hash: Option<(HashAlgorithm, &str)>,
|
||||
ingress: Option<&IngressRef<'_>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
) -> LauncherResult<Bytes> {
|
||||
const TASKS: f64 = 3.0;
|
||||
let ingress_step = ingress.ingress_total().map(|total| total / TASKS).unwrap_or_default();
|
||||
|
||||
let bytes = fetch_advanced(
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
hash,
|
||||
headers,
|
||||
ingress.map(|i| i.with_increment(i.increment_by / 2.0)).as_ref(),
|
||||
ingress.ingress_sub(|total| total / TASKS).as_ref(),
|
||||
)
|
||||
.await?;
|
||||
let path = path.as_ref();
|
||||
|
||||
if let Some(ingress_ref) = ingress {
|
||||
send_ingress(ingress_ref, ingress_ref.increment_by / 4.0).await?;
|
||||
}
|
||||
ingress.send_ingress(ingress_step).await?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
io::create_dir_all(parent).await?;
|
||||
|
|
@ -200,9 +199,7 @@ pub async fn download_advanced(
|
|||
|
||||
io::write(path, &bytes).await?;
|
||||
|
||||
if let Some(ingress_ref) = ingress {
|
||||
send_ingress(ingress_ref, ingress_ref.increment_by / 4.0).await?;
|
||||
}
|
||||
ingress.send_ingress(ingress_step).await?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
|
@ -212,7 +209,7 @@ pub async fn download(
|
|||
url: &str,
|
||||
path: impl AsRef<Path>,
|
||||
hash: Option<(HashAlgorithm, &str)>,
|
||||
ingress_ref: Option<&IngressRef<'_>>
|
||||
ingress: Option<&SubIngress<'_>>
|
||||
) -> LauncherResult<Bytes> {
|
||||
download_advanced(method, url, path, None, None, hash, ingress_ref).await
|
||||
download_advanced(method, url, path, None, None, hash, ingress).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use async_compression::tokio::bufread::GzipDecoder;
|
||||
use async_zip::StoredZipEntry;
|
||||
use async_zip::base::read::WithoutEntry;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tempfile::TempDir;
|
||||
|
|
@ -234,24 +236,49 @@ pub fn tempfile() -> Result<std::fs::File, IOError> {
|
|||
Ok(tempfile::tempfile()?)
|
||||
}
|
||||
|
||||
/// Unzips a zip archive from a byte array
|
||||
pub async fn unzip_bytes(
|
||||
/// Reads a zip archive from a byte array
|
||||
pub async fn read_zip_entries_bytes<F>(
|
||||
data: Vec<u8>,
|
||||
dest_path: impl AsRef<std::path::Path>
|
||||
) -> Result<(), IOError> {
|
||||
mut f: F,
|
||||
) -> Result<(), IOError>
|
||||
where
|
||||
F: AsyncFnMut(
|
||||
usize,
|
||||
&StoredZipEntry,
|
||||
&mut async_zip::base::read::ZipEntryReader<
|
||||
'_,
|
||||
futures_lite::io::Cursor<&[u8]>,
|
||||
WithoutEntry,
|
||||
>,
|
||||
) -> Result<(), IOError>,
|
||||
{
|
||||
let reader = async_zip::base::read::mem::ZipFileReader::new(data).await?;
|
||||
|
||||
let entries = reader.file().entries();
|
||||
for index in 0..entries.len() {
|
||||
let entry = entries.get(index).expect("expected more zip entries");
|
||||
let path = dest_path.as_ref().join(sanitize_path(entry.filename().as_str()?));
|
||||
|
||||
let is_dir = entry.dir()?;
|
||||
|
||||
let mut entry_reader = reader
|
||||
.reader_without_entry(index)
|
||||
.await
|
||||
.expect("Failed to read ZipEntry");
|
||||
.unwrap_or_else(|_| panic!("expected zip entry at index '{index}'"));
|
||||
|
||||
f(index, entry, &mut entry_reader).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unzips a zip archive from a byte array
|
||||
pub async fn unzip_bytes(
|
||||
data: Vec<u8>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
read_zip_entries_bytes(data, async |_, entry, entry_reader| {
|
||||
let path = dest_path
|
||||
.as_ref()
|
||||
.join(sanitize_path(entry.filename().as_str()?));
|
||||
|
||||
let is_dir = entry.dir()?;
|
||||
|
||||
if is_dir {
|
||||
create_dir_all(path).await?;
|
||||
|
|
@ -262,16 +289,19 @@ pub async fn unzip_bytes(
|
|||
|
||||
let file = tokio::fs::File::create(&path).await?;
|
||||
let mut writer = tokio::io::BufWriter::new(file);
|
||||
|
||||
let mut buf = vec![0; 8192];
|
||||
loop {
|
||||
let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?;
|
||||
let n = futures::AsyncReadExt::read(entry_reader, &mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&buf[..n]).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -288,9 +318,7 @@ pub async fn unzip_file(
|
|||
let entries = reader.file().entries();
|
||||
|
||||
for index in 0..entries.len() {
|
||||
let entry = entries
|
||||
.get(index)
|
||||
.expect("expected more zip entries");
|
||||
let entry = entries.get(index).expect("expected more zip entries");
|
||||
|
||||
let path = dest_path.join(sanitize_path(entry.filename().as_str()?));
|
||||
let is_dir = entry.dir()?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
pub mod crypto;
|
||||
pub mod ext;
|
||||
pub mod os_ext;
|
||||
pub mod http;
|
||||
pub mod io;
|
||||
|
|
@ -20,8 +20,8 @@ pub enum GameLoader {
|
|||
impl GameLoader {
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_mod_loader(&self) -> bool {
|
||||
matches!(self, Self::Forge | Self::NeoForge | Self::Quilt | Self::Fabric | Self::LegacyFabric)
|
||||
pub const fn is_modded(&self) -> bool {
|
||||
!matches!(self, Self::Vanilla)
|
||||
}
|
||||
|
||||
pub const fn get_format_version(&self) -> usize {
|
||||
|
|
@ -50,7 +50,7 @@ impl TryFrom<String> for GameLoader {
|
|||
|
||||
fn try_from(s: String) -> Result<GameLoader, String> {
|
||||
Ok(match s.to_lowercase().replace(['_', '.', ' '], "").as_str() {
|
||||
"vanilla" => Self::Vanilla,
|
||||
"vanilla" | "minecraft" => Self::Vanilla,
|
||||
"forge" => Self::Forge,
|
||||
"neoforge" | "neo" => Self::NeoForge,
|
||||
"quilt" => Self::Quilt,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ pub struct Model {
|
|||
pub absolute_path: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub full_version: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub arch: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
|||
|
|
@ -53,3 +53,33 @@ impl Related<super::java_versions::Entity> for Entity {
|
|||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
const GLOBAL_NAME: &str = "Global";
|
||||
|
||||
impl ActiveModel {
|
||||
pub fn is_global(&self) -> bool {
|
||||
self.name.try_as_ref().map(|name| name == GLOBAL_NAME).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn is_global(&self) -> bool {
|
||||
self.name == GLOBAL_NAME
|
||||
}
|
||||
|
||||
pub fn default_global_profile() -> Self {
|
||||
Self {
|
||||
name: GLOBAL_NAME.into(),
|
||||
force_fullscreen: Some(false),
|
||||
hook_post: None,
|
||||
hook_pre: None,
|
||||
hook_wrapper: None,
|
||||
java_id: None,
|
||||
launch_args: None,
|
||||
launch_env: None,
|
||||
mem_max: Some(3072),
|
||||
res_h: None,
|
||||
res_w: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ create table java_versions (
|
|||
vendor_name text not null,
|
||||
absolute_path text not null,
|
||||
full_version text not null,
|
||||
arch text not null,
|
||||
constraint java_versions_pk primary key (id autoincrement)
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue