feat: prepare clusters for launch

This commit is contained in:
LynithDev 2025-04-11 14:24:00 +02:00
parent 2a0f53e107
commit 6c80a84972
34 changed files with 1395 additions and 371 deletions

5
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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 }

View file

@ -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(())
}

View file

@ -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?;

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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)
}

View 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(())
}

View file

View 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
}

View file

@ -1 +1,4 @@
pub mod metadata;
pub mod metadata;
pub mod launch;
pub mod arguments;
pub mod rules;

View 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
}

View file

@ -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(())
}
}
}

View file

@ -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;

View file

@ -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
}
}

View file

@ -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;

View 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(())
}

View file

@ -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 { ":" };

View file

@ -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),
}

View file

@ -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)
}

View file

@ -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);
}
});
}
}
}

View file

@ -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
}
},

View file

@ -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,
}
}

View file

@ -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),

View file

@ -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
}

View file

@ -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()?;

View file

@ -1,4 +1,4 @@
pub mod crypto;
pub mod ext;
pub mod os_ext;
pub mod http;
pub mod io;

View file

@ -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,

View file

@ -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)]

View file

@ -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,
}
}
}

View file

@ -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)
);