feat: desktop crate is ready to be fixed

This commit is contained in:
LynithDev 2025-05-01 13:29:26 +02:00
parent f939169aa6
commit cb1c8d54de
102 changed files with 1992 additions and 15574 deletions

1
.clippy.toml Normal file
View file

@ -0,0 +1 @@
cognitive-complexity-threshold = 30

View file

@ -12,3 +12,12 @@ use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
unstable_features = true
deprecated = "warn"
deprecated_in_future = "forbid"
rust_2018_idioms = { level = "warn", priority = -1 }
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_allocation = "warn"
unused_import_braces = "warn"
unused_must_use = "deny"
unused_qualifications = "warn"

2278
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
[workspace]
members = [
"apps/desktop",
"packages/core",
"packages/entity",
"packages/gamemode",
@ -10,7 +11,10 @@ members = [
resolver = "2" # virtual workspace resolver so we don't need a root package
[workspace.package]
authors = [ "Polyfrost", "Polyfrost Contributors" ]
authors = [
"Polyfrost",
"Polyfrost Contributors"
]
documentation = "https://docs.polyfrost.org/onelauncher"
edition = "2024"
homepage = "https://polyfrost.org/projects/onelauncher"
@ -28,67 +32,74 @@ onelauncher_macro = { path = "./packages/macro" }
onelauncher_migration = { path = "./packages/migration" }
serde_json_path_to_error = { path = "./packages/serde_json_path_to_error" }
# core
interpulse = { version = "=1.2.2", features = [ "specta" ] }
# tauri
tauri = { version = "2.0", features = [
tauri = { version = "=2.5.1", features = [
"rustls-tls",
"tracing",
"specta",
"uuid",
"protocol-asset",
] }
tauri-build = { version = "2.0" }
tauri-plugin-clipboard-manager = { version = "2.0" }
tauri-plugin-deep-link = { version = "2.0" }
tauri-plugin-dialog = { version = "2.0" }
tauri-plugin-shell = { version = "2.0" }
tauri-plugin-single-instance = { version = "2.0" }
tauri-plugin-updater = { version = "2.0" }
tauri-plugin-window-state = { version = "2.0" }
# core
interpulse = { version = "=1.2.2", features = [ "specta" ] }
tauri-build = { version = "=2.2.0" }
tauri-plugin-clipboard-manager = { version = "=2.2.2" }
tauri-plugin-deep-link = { version = "=2.2.1" }
tauri-plugin-dialog = { version = "=2.2.1" }
tauri-plugin-single-instance = { version = "=2.2.3" }
tauri-plugin-updater = { version = "=2.7.1" }
tauri-plugin-window-state = { version = "=2.2.2" }
# code gen
specta = { version = "2.0.0-rc.20", features = [
strum = { version = "=0.27.1", features = [ "derive" ] }
specta = { version = "=2.0.0-rc.22", features = [
"chrono",
"uuid",
"serde",
"serde_json",
"function",
"derive",
"time",
"tokio"
] }
specta-typescript = { version = "=0.0.7" }
tauri-specta = { version = "2.0.0-rc.20", features = [ "javascript", "typescript", "derive" ] }
# security
iota_stronghold = { version = "2.1" }
zeroize = { version = "1.8", features = [ "zeroize_derive" ] }
specta-typescript = { version = "=0.0.9" }
tauri-specta = { version = "=2.0.0-rc.21", features = [ "derive" ] }
# logging
tracing = { version = "0.1" }
tracing-appender = { version = "0.2" }
tracing-error = { version = "0.2" }
tracing-subscriber = { version = "0.3", features = [ "chrono", "env-filter" ] }
tracing-subscriber = { version = "0.3", features = [
"chrono",
"env-filter"
] }
# serialization & text
base64 = { version = "0.22" }
byteorder = { version = "1.5.0" }
base64 = { version = "=0.22.1" }
byteorder = { version = "=1.5.0" }
chrono = { version = "0.4", features = [ "serde" ] }
nom = { version = "7.1" }
p256 = { version = "0.13.2", features = [ "ecdsa" ] }
rand = { version = "0.8.5" }
regex = { version = "1.10" }
nom = { version = "=8.0.0" }
rand = { version = "0.8" }
regex = { version = "1.11" }
serde = { version = "1.0", features = [ "derive" ] }
serde_json = { version = "1.0" }
serde_with = { version = "=3.12.0", features = [ "chrono_0_4", "json", "macros" ] }
strum = { version = "0.26", features = [ "derive" ] }
serde_json = { version = "=1.0" }
serde_with = { version = "=3.12.0", features = [
"chrono_0_4",
"json",
"macros"
] }
toml = { version = "0.8" }
url = { version = "2.5.2" }
uuid = { version = "1.10", features = [ "serde", "v4" ] }
url = { version = "2.5" }
uuid = { version = "1.16", features = [
"serde",
"v4"
] }
# network
async-tungstenite = { version = "0.28", features = [ "tokio-runtime", "tokio-rustls-webpki-roots" ] }
# async-tungstenite = { version = "0.28", features = [
# "tokio-runtime",
# "tokio-rustls-webpki-roots"
# ] }
reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
@ -102,30 +113,31 @@ reqwest = { version = "0.12", default-features = false, features = [
"gzip",
"brotli",
] }
webbrowser = { version = "1.0" }
webbrowser = { version = "=1.0.4" }
# io
async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] }
async_zip = { version = "0.0.17", features = [ "full" ] }
bytes = { version = "1.7" }
dirs = { version = "5.0" }
dunce = { version = "1.0.5" }
flate2 = { version = "1.0" }
notify = { version = "6.1", default-features = false }
notify-debouncer-mini = { version = "0.4", default-features = false }
sanitize-filename = { version = "0.6" }
tempfile = { version = "3.19" }
tokio = { version = "1.40", features = [ "full" ] }
tokio-stream = { version = "0.1.16", features = [ "fs" ] }
async-compression = { version = "=0.4.23", features = [
"flate2",
"tokio"
] }
async_zip = { version = "=0.0.17", features = [ "full" ] }
bytes = { version = "=1.10.1" }
dirs = { version = "=6.0.0" }
dunce = { version = "=1.0.5" }
flate2 = { version = "=1.1.1" }
sanitize-filename = { version = "=0.6.0" }
tempfile = { version = "=3.19.1" }
tokio = { version = "1", features = [ "full" ] }
tokio-stream = { version = "0.1", features = [ "fs" ] }
tokio-util = { version = "0.7", features = [ "compat" ] }
zip = { version = "2.2.0" }
# crypto
digest = { version = "0.10" }
md-5 = { version = "0.10" }
murmur2 = { version = "0.1.0" }
murmur2 = { version = "=0.1.0" }
sha1 = { version = "0.10" }
sha2 = { version = "0.10" }
p256 = { version = "0.13", features = [ "ecdsa" ] }
# db
sea-orm = { version = "1.1", features = [
@ -145,23 +157,29 @@ sea-orm-migration = { version = "1.1", features = [
] }
# utils
anyhow = { version = "1.0" }
async-trait = { version = "0.1" }
cc = { version = "1.1" }
discord-rich-presence = { version = "0.2" }
anyhow = { version = "1" }
async-trait = { version = "=0.1.88" }
cc = { version = "1" }
discord-rich-presence = { version = "=0.2.5" }
futures = { version = "0.3" }
futures-lite = { version = "2.6" }
indicatif = { version = "0.17" }
libc = { version = "0.2" }
merge = { version = "0.2" }
sysinfo = { version = "0.31", features = [ "serde" ] }
thiserror = { version = "1.0" }
whoami = { version = "1.5" }
merge = { version = "=0.2.0" }
sysinfo = { version = "=0.34.2", features = [ "serde" ] }
thiserror = { version = "2" }
whoami = { version = "=1.6.0" }
# macro
paste = { version = "1" }
quote = { version = "1" }
syn = { version = "2" }
proc-macro2 = { version = "1" }
[patch.crates-io]
# tauri-specta = { git = "https://github.com/specta-rs/tauri-specta", rev = "6fe9532a44b8b683b4dacd178b0120ee585bea23" }
specta = { git = "https://github.com/specta-rs/specta", rev = "30661263250e7b6f9b36cdc56e6c843ac5abb226" }
specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "30661263250e7b6f9b36cdc56e6c843ac5abb226" }
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
@ -186,17 +204,6 @@ style = { level = "warn", priority = -1 }
suspicious = { level = "warn", priority = -1 }
unnecessary_cast = "warn"
[workspace.lints.rust]
deprecated = "warn"
deprecated_in_future = "forbid"
rust_2018_idioms = { level = "warn", priority = -1 }
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_allocation = "warn"
unused_import_braces = "warn"
unused_must_use = "deny"
unused_qualifications = "warn"
[profile.dev]
# add debug symbols/logic and optimize for reduced build time (parallel)
codegen-units = 256 # make parallel incremental builds faster

View file

@ -13,7 +13,11 @@ authors = { workspace = true }
[lib]
name = "onelauncher_gui"
crate-type = [ "staticlib", "cdylib", "lib" ]
crate-type = [
"staticlib",
"cdylib",
"lib"
]
[lints]
workspace = true
@ -22,31 +26,40 @@ workspace = true
tauri-build = { workspace = true }
[dependencies]
onelauncher = { workspace = true, features = [ "tauri", "specta" ] }
onelauncher_macros = { workspace = true }
tauri-plugin-shell = { workspace = true }
# core
onelauncher_core = { workspace = true, features = [ "tauri" ] }
interpulse = { workspace = true }
# tauri
tauri-plugin-window-state = { workspace = true }
tauri-plugin-single-instance = { workspace = true }
tauri-plugin-updater = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-deep-link = { workspace = true }
# code gen
tauri-specta = { workspace = true }
specta = { workspace = true }
specta-typescript = { workspace = true }
interpulse = { workspace = true }
strum = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
# logging
tracing = { workspace = true }
tracing-error = { workspace = true }
mimalloc = { workspace = true }
# serialization & text
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
# io
tokio = { workspace = true }
# utils
thiserror = { workspace = true }
anyhow = { workspace = true }
[dependencies.tauri]
workspace = true
@ -58,18 +71,10 @@ features = [
"specta"
]
[dev-dependencies.tauri]
workspace = true
features = [ "test" ]
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = { workspace = true }
objc = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
webkit2gtk = { workspace = true }
[features]
default = [ "custom-protocol", "devtools" ]
default = [
"custom-protocol",
"devtools"
]
devtools = [ "tauri/devtools" ]
custom-protocol = [ "tauri/custom-protocol" ]

View file

@ -26,7 +26,6 @@
"core:webview:allow-internal-toggle-devtools",
"core:event:default",
"shell:allow-open",
"clipboard-manager:allow-write-text",
"dialog:allow-open",
"dialog:allow-save",

View file

@ -13,8 +13,7 @@
"allow": [
"$DATA/OneLauncher/**",
"$RESOURCE/**"
],
"deny": ["$DATA/OneLauncher/db/*.stronghold"]
]
}
},
"freezePrototype": true,
@ -79,7 +78,7 @@
"desktopTemplate": "../../packages/distribution/linux/org.polyfrost.launcher.desktop.template"
},
"appimage": {
}
},
"shortDescription": "OneLauncher",

View file

@ -8,4 +8,10 @@ export default defineConfig({
rules: {
'ts/no-use-before-define': 'off',
},
toml: {
overrides: {
'toml/array-element-newline': ['error', 'always'],
'toml/no-mixed-type-in-array': ['off'],
},
},
});

View file

@ -14,14 +14,18 @@ authors = { workspace = true }
path = "src/lib.rs"
[features]
default = [ "cli" ]
default = [ "tauri" ]
tauri = [
"dep:tauri",
"dep:tauri-specta",
"specta",
"gui"
]
specta = [ "dep:specta", "interpulse/specta", "onelauncher_entity/specta" ]
specta = [
"dep:specta",
"interpulse/specta",
"onelauncher_entity/specta"
]
cli = [ "dep:indicatif" ]
gui = []
@ -80,7 +84,6 @@ md-5 = { workspace = true }
p256 = { workspace = true }
# io
zip = { workspace = true }
async_zip = { workspace = true }
dirs = { workspace = true }
bytes = { workspace = true }

View file

@ -1,7 +1,8 @@
use onelauncher_core::store::Dirs;
use onelauncher_core::store::{Core, CoreOptions, Dirs};
#[tokio::main]
async fn main() {
Core::initialize(CoreOptions::default()).await.expect("couldn't initialize core");
let dirs = Dirs::get().await.expect("couldn't initialize dirs");
println!(" Base dir : {}", dirs.base_dir().display());

View file

@ -14,6 +14,7 @@
"cargo": "cargo",
"binary": "cargo run --bin",
"example": "cargo run --example",
"test": "cargo test"
"test": "cargo test",
"lint:rs": "cargo clippy --all-targets"
}
}

View file

@ -108,10 +108,6 @@ 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: &mut clusters::Model, force: Option<bool>) -> LauncherResult<&clusters::Model> {
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 {
@ -119,14 +115,6 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>)
// return Ok(cluster);
// }
let ingress_id = init_ingress(
IngressType::PrepareCluster {
cluster_name: cluster.name.clone(),
},
"preparing cluster",
INGRESS_TOTAL,
)
.await?;
dao::update_cluster(cluster, async |mut cluster| {
cluster.stage = Set(ClusterStage::Downloading);
@ -134,62 +122,7 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>)
})
.await?;
let result: LauncherResult<()> = {
// TASK 1
ingress_id.set_ingress_message("fetching data").await?;
let setting_profile =
setting_profiles::dao::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()))?
.clone();
drop(metadata);
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)),
force,
)
.await?;
let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
version
} else {
let Some(ver) = &version_info.java_version else {
return Err(ClusterError::MissingJavaVersion.into());
};
java::prepare_java(ver.major_version).await?
};
// TASK 3
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).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 {
if let Err(err) = install_cluster(cluster, force).await {
tracing::error!("failed to prepare cluster: {}", err);
dao::update_cluster(cluster, async |mut cluster| {
cluster.stage = Set(ClusterStage::NotReady);
@ -209,6 +142,74 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>)
Ok(cluster)
}
async fn install_cluster(cluster: &clusters::Model, force: Option<bool>) -> LauncherResult<()> {
const INGRESS_TOTAL: f64 = 100.0;
const INGRESS_TASKS: f64 = 4.0;
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
let ingress_id = init_ingress(
IngressType::PrepareCluster {
cluster_name: cluster.name.clone(),
},
"preparing cluster",
INGRESS_TOTAL,
)
.await?;
// TASK 1
ingress_id.set_ingress_message("fetching data").await?;
let setting_profile =
setting_profiles::dao::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()))?
.clone();
drop(metadata);
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)),
force,
)
.await?;
let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
version
} else {
let Some(ver) = &version_info.java_version else {
return Err(ClusterError::MissingJavaVersion.into());
};
java::prepare_java(ver.major_version).await?
};
// TASK 3
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).await?;
// TASK 4
run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?;
Ok(())
}
/// Run forge processors
async fn run_forge_processors(
cluster: &clusters::Model,
@ -265,11 +266,10 @@ async fn run_forge_processors(
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")) {
if let Some(sides) = &processor.sides
&& !sides.contains(&String::from("client")) {
continue;
}
}
let mut cp = processor.classpath.clone();
cp.push(processor.jar.clone());

View file

@ -179,12 +179,11 @@ pub async fn main_class(path: String) -> LauncherResult<Option<String>> {
for line in buf.lines() {
let line = line.trim();
if line.starts_with("Main-Class:") {
if let Some(class) = line.split(':').nth(1) {
if line.starts_with("Main-Class:")
&& let Some(class) = line.split(':').nth(1) {
class_name = Some(class.to_string());
return Ok(());
}
}
}
Ok(())
@ -272,11 +271,10 @@ pub fn classpaths(
let mut classpaths = libraries
.iter()
.filter_map(|lib| {
if let Some(rules) = &lib.rules {
if !super::rules::validate_rules(rules, java_arch, updated) {
if let Some(rules) = &lib.rules
&& !super::rules::validate_rules(rules, java_arch, updated) {
return None;
}
}
if !lib.include_in_classpath {
return None;

View file

@ -32,11 +32,10 @@ pub async fn launch_minecraft(
prepare_cluster(cluster, force).await?;
let mut settings = get_global_profile().await;
if let Some(name) = &cluster.setting_profile_name {
if let Some(profile) = get_profile_by_name(name).await? {
if let Some(name) = &cluster.setting_profile_name
&& let Some(profile) = get_profile_by_name(name).await? {
settings.merge(profile);
}
}
let state = State::get().await?;
let dirs = Dirs::get().await?;

View file

@ -285,12 +285,11 @@ pub async fn download_libraries(
ingress.set_ingress_message("fetching libraries").await?;
let requests = stream::iter(libraries.iter().map(|lib| async {
if let Some(rules) = &lib.rules {
if !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) {
if let Some(rules) = &lib.rules
&& !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) {
tracing::debug!("skipping library {} due to rules", lib.name);
return Ok::<(), LauncherError>(());
}
}
if !lib.downloadable {
tracing::debug!("skipping library {} due to downloadability", lib.name);
@ -309,8 +308,8 @@ pub async fn download_libraries(
async {
if let Some(interpulse::api::minecraft::LibraryDownloads {
artifact: Some(ref artifact), ..
}) = lib.downloads {
if !artifact.url.is_empty() {
}) = lib.downloads
&& !artifact.url.is_empty() {
http::download(
Method::GET,
&artifact.url,
@ -322,7 +321,6 @@ pub async fn download_libraries(
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
return Ok::<_, LauncherError>(());
}
}
let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
http::download(

View file

@ -75,11 +75,10 @@ pub fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
}
}
if let Some(version) = &rule.version {
if let Ok(regex) = Regex::new(version.as_str()) {
if let Some(version) = &rule.version
&& let Ok(regex) = Regex::new(version.as_str()) {
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
}
}
rule_match
}

View file

@ -5,7 +5,7 @@ use crate::LauncherResult;
use crate::store::State;
use crate::store::ingress::{IngressId, IngressType, SubIngress};
#[onelauncher_macro::specta(with_event)]
#[onelauncher_macro::specta]
#[derive(Serialize, Debug, Clone)]
pub struct IngressPayload {
pub id: Uuid,

View file

@ -122,13 +122,10 @@ pub async fn get_recommended_java(
// 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 {
&& let Some(java_id) = profile.java_id
&& 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 {

View file

@ -2,7 +2,8 @@ use serde::Serialize;
use crate::store::processes::Process;
#[derive(Debug, Serialize)]
#[onelauncher_macro::specta]
#[derive(Debug, Clone, Serialize)]
pub enum ProcessPayload {
Starting {
command: String,

View file

@ -2,8 +2,8 @@ use serde::Serialize;
use crate::{error::LauncherResult, store::proxy::ProxyState};
#[onelauncher_macro::specta(with_event)]
#[derive(Debug, Serialize)]
#[onelauncher_macro::specta(event)]
#[derive(Debug, Clone, Serialize)]
pub enum LauncherEvent {
Ingress(crate::api::ingress::IngressPayload),
Message(crate::api::proxy::message::MessagePayload),

View file

@ -1,6 +1,6 @@
#[cfg(feature = "tauri")] mod proxy_tauri;
#[cfg(feature = "cli")] mod proxy_cli;
mod proxy_empty;
#[cfg(feature = "tauri")] pub mod proxy_tauri;
#[cfg(feature = "cli")] pub mod proxy_cli;
pub mod proxy_empty;
#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri;
#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli;

View file

@ -34,13 +34,13 @@ impl LauncherProxy for ProxyEmpty {
}
#[cfg(feature = "gui")]
fn hide_main_window(&self) -> crate::LauncherResult<()> {
fn hide_main_window(&self) -> LauncherResult<()> {
println!("hidden window");
Ok(())
}
#[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()> {
fn show_main_window(&self) -> LauncherResult<()> {
println!("shown window");
Ok(())
}

View file

@ -2,7 +2,7 @@ use tauri::{AppHandle, Emitter, Manager};
use tauri_specta::Event;
use tracing::{error, warn};
use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy, message::MessageLevel}, LauncherResult};
use crate::{api::proxy::{event::LauncherEvent, LauncherProxy, message::MessageLevel}, LauncherResult};
#[derive(Debug)]
pub struct ProxyTauri {
@ -21,19 +21,16 @@ impl ProxyTauri {
#[async_trait::async_trait]
impl LauncherProxy for ProxyTauri {
async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> {
match event {
LauncherEvent::Message(message) => {
match message.level {
MessageLevel::Info => {},
MessageLevel::Warning => warn!("{}", message.message),
MessageLevel::Error => error!("{}", message.message),
};
},
_ => {}
if let LauncherEvent::Message(message) = &event {
match message.level {
MessageLevel::Info => {},
MessageLevel::Warn => warn!("{}", message.message),
MessageLevel::Error => error!("{}", message.message),
}
}
self.handle
.emit_all(LauncherEvent::NAME, event)
.emit(LauncherEvent::NAME, event)
.map_err(Into::into)
}

View file

@ -2,7 +2,7 @@ use serde::Serialize;
use super::event::send_event;
#[onelauncher_macro::specta(with_event)]
#[onelauncher_macro::specta]
#[derive(Serialize, Debug, Clone)]
pub struct MessagePayload {
pub level: MessageLevel,

View file

@ -1,24 +1,26 @@
use onelauncher_entity::setting_profiles;
use sea_orm::ActiveValue::Set;
use crate::{error::LauncherResult, store::State};
use crate::error::LauncherResult;
use crate::store::State;
pub mod dao {
use onelauncher_entity::setting_profiles;
use sea_orm::{ActiveModelTrait, EntityTrait};
use crate::{error::{DaoError, LauncherResult}, store::State};
use crate::error::{DaoError, LauncherResult};
use crate::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?)
Ok(setting_profiles::Entity::find().all(db).await?)
}
pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult<setting_profiles::Model> {
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 {
@ -31,20 +33,22 @@ pub mod dao {
Ok(settings.global_game_settings.clone())
}
pub async fn get_profile_by_name(name: &str) -> LauncherResult<Option<setting_profiles::Model>> {
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?)
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());
}
pub async fn insert_profile(
profile: setting_profiles::ActiveModel,
) -> LauncherResult<setting_profiles::Model> {
if let Some(name) = profile.name.try_as_ref()
&& (name.is_empty() || profile.is_global())
{
return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into());
}
let state = State::get().await?;
@ -57,7 +61,9 @@ pub mod dao {
let state = State::get().await?;
let db = &state.db;
let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?;
let deleted = setting_profiles::Entity::delete_by_id(name)
.exec(db)
.await?;
if deleted.rows_affected == 0 {
return Err(DaoError::NotFound.into());
@ -78,7 +84,9 @@ pub async fn get_global_profile() -> setting_profiles::Model {
/// Creates a new setting profile and inserts it into the database. Returns the inserted entry
pub async fn create_profile<F>(name: &str, block: F) -> LauncherResult<setting_profiles::Model>
where F: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel> {
where
F: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel>,
{
let model = setting_profiles::ActiveModel {
name: Set(name.to_string()),
..Default::default()

View file

@ -365,8 +365,8 @@ pub struct SaveDeviceToken {
}
/// Core variables passed throughout the Minecraft login flow.
#[onelauncher_macro::specta]
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct MinecraftLogin {
pub verify: String,
/// The OAuth challenge generated at the start of the flow.
@ -378,8 +378,8 @@ pub struct MinecraftLogin {
}
/// A structure of all needed Minecraft credentials for logging in and account management.
#[onelauncher_macro::specta]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct MinecraftCredentials {
/// The uuid of the credentials.
pub id: Uuid,

View file

@ -14,15 +14,15 @@ use uuid::Uuid;
use crate::api::cluster;
use crate::api::cluster::dao::ClusterId;
use crate::api::game::log::{censor_line, CensorMap};
use crate::api::game::log::{CensorMap, censor_line};
use crate::api::processes::ProcessPayload;
use crate::api::proxy::event::{send_event, LauncherEvent};
use crate::api::proxy::event::{LauncherEvent, send_event};
use crate::error::LauncherResult;
use crate::send_error;
use crate::utils::io::IOError;
use super::credentials::MinecraftCredentials;
use super::State;
use super::credentials::MinecraftCredentials;
#[derive(Debug, thiserror::Error)]
pub enum ProcessError {
@ -37,7 +37,8 @@ pub struct ProcessStore {
processes: RwLock<HashMap<u32, Arc<RwLock<Process>>>>,
}
#[derive(Debug, Serialize)]
#[onelauncher_macro::specta]
#[derive(Debug, Clone, Serialize)]
pub struct Process {
pub pid: u32,
pub started_at: DateTime<Utc>,
@ -47,7 +48,7 @@ pub struct Process {
#[serde(skip)]
pub child_type: Arc<RwLock<ChildType>>,
#[serde(skip)]
process_loop: Option<JoinHandle<LauncherResult<i32>>>,
process_loop: Option<Arc<JoinHandle<LauncherResult<i32>>>>,
}
#[derive(Debug, Serialize)]
@ -112,10 +113,7 @@ impl ProcessStore {
}
/// Returns a list of all running processes for a given cluster.
pub async fn get_running_by_cluster(
&self,
cluster_id: ClusterId,
) -> Vec<Arc<RwLock<Process>>> {
pub async fn get_running_by_cluster(&self, cluster_id: ClusterId) -> Vec<Arc<RwLock<Process>>> {
let mut list = Vec::new();
let read_guard = self.processes.read().await;
@ -129,10 +127,7 @@ impl ProcessStore {
}
/// Returns a list of all running processes for a given account.
pub async fn get_running_by_account(
&self,
account_id: Uuid,
) -> Vec<Arc<RwLock<Process>>> {
pub async fn get_running_by_account(&self, account_id: Uuid) -> Vec<Arc<RwLock<Process>>> {
let mut list = Vec::new();
let read_guard = self.processes.read().await;
@ -180,16 +175,18 @@ impl ProcessStore {
settings: &setting_profiles::Model,
mut command: Command,
) -> LauncherResult<Arc<RwLock<Process>>> {
send_event(LauncherEvent::Process(ProcessPayload::Starting { command: format!("{command:?}") })).await;
send_event(LauncherEvent::Process(ProcessPayload::Starting {
command: format!("{command:?}"),
}))
.await;
if let Some(pre_hook) = &settings.hook_pre {
if let Err(err) = run_hook(pre_hook, cwd.clone()).await {
send_error!("{err:?}");
}
if let Some(pre_hook) = &settings.hook_pre
&& let Err(err) = run_hook(pre_hook, cwd.clone()).await
{
send_error!("{err:?}");
}
command
.stdout(Stdio::piped())
command.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
@ -205,10 +202,13 @@ impl ProcessStore {
#[cfg(target_os = "linux")]
{
if settings.os_extra.as_ref().is_some_and(|s| s.enable_gamemode.is_some_and(|x| x)) {
if let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid) {
tracing::warn!("failed to enable gamemode, continuing: {}", err);
}
if settings
.os_extra
.as_ref()
.is_some_and(|s| s.enable_gamemode.is_some_and(|x| x))
&& let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid)
{
tracing::warn!("failed to enable gamemode, continuing: {}", err);
}
}
@ -225,7 +225,8 @@ impl ProcessStore {
send_event(LauncherEvent::Process(ProcessPayload::Output {
pid,
output: line,
})).await;
}))
.await;
}
});
@ -237,9 +238,12 @@ impl ProcessStore {
post_hook: settings.hook_post.clone(),
account_id: creds.id,
child_type: child_type.clone(),
process_loop: Some(tokio::task::spawn(Self::process_loop(
cluster_id, cwd, settings.hook_post.clone(), child_type,
))),
process_loop: Some(Arc::new(tokio::task::spawn(Self::process_loop(
cluster_id,
cwd,
settings.hook_post.clone(),
child_type,
)))),
};
let process = Arc::new(RwLock::new(process));
@ -262,27 +266,8 @@ impl ProcessStore {
child: Arc<RwLock<ChildType>>,
) -> LauncherResult<i32> {
let pid = child.read().await.id();
let mut last_updated = Utc::now();
let exit_code = loop {
let mut child = child.write().await;
if let Some(status) = child.try_wait()? {
tracing::info!("process exited with status: {:?}", status);
break status;
}
// Every 60 seconds, update the playtime of the process
let duration = Utc::now().signed_duration_since(last_updated).num_seconds();
if duration > 60 {
last_updated = Utc::now();
if let Err(err) = cluster::update_playtime(cluster_id, duration).await {
tracing::error!("failed to update playtime: {}", err);
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
};
let exit_code = wait_for_exit(cluster_id, &child).await?;
let state = State::get().await?;
let store = &state.processes;
@ -292,13 +277,17 @@ impl ProcessStore {
write_guard.remove(&pid);
tracing::debug!("removed process with pid {}", pid);
if let Some(post_hook) = post_hook {
if let Err(err) = run_hook(&post_hook, cwd).await {
send_error!("{err:?}");
}
if let Some(post_hook) = post_hook
&& let Err(err) = run_hook(&post_hook, cwd).await
{
send_error!("{err:?}");
}
send_event(LauncherEvent::Process(ProcessPayload::Stopped { pid, exit_code })).await;
send_event(LauncherEvent::Process(ProcessPayload::Stopped {
pid,
exit_code,
}))
.await;
Ok(exit_code)
}
@ -332,4 +321,28 @@ async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult<Option<i32>> {
} else {
Err(ProcessError::HookUnsuccessful(exit_code).into())
}
}
}
async fn wait_for_exit(cluster_id: ClusterId, child: &Arc<RwLock<ChildType>>) -> LauncherResult<i32> {
let mut last_updated = Utc::now();
loop {
let mut child = child.write().await;
if let Some(status) = child.try_wait()? {
tracing::info!("process exited with status: {:?}", status);
return Ok(status);
}
// Every 60 seconds, update the playtime of the process
let duration = Utc::now().signed_duration_since(last_updated).num_seconds();
if duration > 60 {
last_updated = Utc::now();
if let Err(err) = cluster::update_playtime(cluster_id, duration).await {
tracing::error!("failed to update playtime: {}", err);
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}

View file

@ -23,8 +23,6 @@ pub enum IOError {
path: String,
},
#[error(transparent)]
ZipError(#[from] zip::result::ZipError),
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error("json deserialization error: {0}")]
DeserializeError(#[from] serde_json::Error),
@ -295,11 +293,10 @@ pub async fn unzip_bytes_filtered(
read_zip_entries_bytes(data, async |_, entry, entry_reader| {
let file_name = entry.filename().as_str()?;
if let Some(filter) = &filter_entries {
if !filter(file_name) {
if let Some(filter) = &filter_entries
&& !filter(file_name) {
return Ok(());
}
}
let path = dest_path.as_ref().join(sanitize_path(file_name));

View file

@ -1,92 +0,0 @@
[package]
name = "onelauncher"
description = "The core of OneLauncher"
version = { workspace = true }
license = { workspace = true }
edition = { workspace = true }
repository = { workspace = true }
documentation = { workspace = true }
readme = { workspace = true }
homepage = { workspace = true }
authors = { workspace = true }
[features]
default = []
# feature flag allowing us to interact with tauri (for loading animations, errors showing on gui, etc)
# also allows us to compile onelauncher core without tauri
tauri = [ "dep:tauri", "dep:tauri-specta", "specta" ]
specta = [ "dep:specta" ]
cli = [ "dep:indicatif" ]
[lints]
workspace = true
[dependencies]
# features[default]
onelauncher_macros = { workspace = true }
onelauncher_utils = { workspace = true }
interpulse = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-error = { workspace = true }
tracing-appender = { workspace = true }
notify = { workspace = true }
notify-debouncer-mini = { workspace = true }
iota_stronghold = { workspace = true }
zeroize = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_ini = { workspace = true }
serde_repr = { workspace = true }
serde_with = { workspace = true }
paste = { workspace = true }
futures = { workspace = true }
zip = { workspace = true }
uuid = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
reqwest = { workspace = true }
dirs = { workspace = true }
sha1_smol = { workspace = true }
sha2 = { workspace = true }
murmur2 = { workspace = true }
md5 = { workspace = true }
async_zip = { workspace = true }
discord-rich-presence = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
whoami = { workspace = true }
lazy_static = { workspace = true }
async-tungstenite = { workspace = true }
async-recursion = { workspace = true }
tokio-stream = { workspace = true }
bytes = { workspace = true }
tempfile = { workspace = true }
rand = { workspace = true }
p256 = { workspace = true }
byteorder = { workspace = true }
base64 = { workspace = true }
flate2 = { workspace = true }
toml = { workspace = true }
url = { workspace = true }
# features[tauri]
tauri = { workspace = true, optional = true }
tauri-specta = { workspace = true, optional = true }
# features[specta]
specta = { workspace = true, optional = true }
# features[cli]
indicatif = { workspace = true, optional = true }
[build-dependencies]
dotenvy = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
winreg = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
onelauncher_gamemode = { workspace = true }

View file

@ -1,14 +0,0 @@
{
"name": "@onelauncher/core_old",
"version": "0.1.0-alpha.2",
"description": "Next-generation open source Minecraft launcher core",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": "packages/core_old"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {}
}

View file

@ -1,372 +0,0 @@
//! `OneLauncher` log management
use crate::data::{Credentials, Directories, MinecraftCredentials};
use crate::prelude::ClusterPath;
use crate::utils::http;
use futures::TryFutureExt;
use onelauncher_utils::io::{self, IOError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::SystemTime;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
// TODO: put this in the global store
/// Core logging state and reader for `OneLauncher`.
#[derive(Serialize, Debug)]
pub struct LogManager {
/// Type log type associated with this log file.
pub log_type: LogType,
/// The age of this log as a [`u64`] in seconds.
pub age: u64,
/// The log file to read as a [`String`].
pub log_file: String,
/// The parsed and censored output of the logfile.
pub output: Option<LogOutput>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub enum LogType {
Info,
Crash,
}
impl LogManager {
/// Initialize a new [`LogManager`].
async fn initialize(
log_type: LogType,
age: SystemTime,
cluster: &ClusterPath,
log_file: String,
clear: Option<bool>,
) -> crate::Result<Self> {
Ok(Self {
log_type,
age: age
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs(),
output: if clear.unwrap_or(false) {
None
} else {
Some(get_output_by_file(cluster, log_type, &log_file).await?)
},
log_file,
})
}
}
/// Verify all [`LogManager`]s of a certain [`LogType`]
#[tracing::instrument]
pub async fn get_logs_by_type(
cluster_path: &ClusterPath,
log_type: LogType,
clear: Option<bool>,
logs: &mut Vec<crate::Result<LogManager>>,
) -> crate::Result<()> {
let logs_folder = match log_type {
LogType::Info => Directories::cluster_logs_dir(cluster_path).await?,
LogType::Crash => Directories::crash_reports_dir(cluster_path).await?,
};
if logs_folder.exists() {
for entry in
std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))?
{
let entry: std::fs::DirEntry =
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
let age = entry
.metadata()?
.created()
.unwrap_or(SystemTime::UNIX_EPOCH);
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_name() {
let name = name.to_string_lossy().to_string();
logs.push(LogManager::initialize(log_type, age, cluster_path, name, clear).await);
}
}
}
Ok(())
}
/// Get all [`LogManager`]s in the global [`State`].
pub async fn get_logs(
cluster_path: &ClusterPath,
clear: Option<bool>,
) -> crate::Result<Vec<LogManager>> {
let dir = Directories::cluster_logs_dir(cluster_path).await?;
if !dir.exists() {
io::create_dir(dir).await?;
return Ok(vec![]);
}
let mut logs = Vec::new();
get_logs_by_type(cluster_path, LogType::Info, clear, &mut logs).await?;
get_logs_by_type(cluster_path, LogType::Crash, clear, &mut logs).await?;
let mut logs = logs
.into_iter()
.collect::<crate::Result<Vec<LogManager>>>()?;
logs.sort_by(|a, b| match (a.log_file.as_str(), b.log_file.as_str()) {
("latest.log", _) => std::cmp::Ordering::Less,
(_, "latest.log") => std::cmp::Ordering::Greater,
_ => b.age.cmp(&a.age).then(b.log_file.cmp(&a.log_file)),
});
Ok(logs)
}
/// Delete all stored logs from a specific [`ClusterPath`].
#[tracing::instrument]
pub async fn delete_logs(cluster_path: ClusterPath) -> crate::Result<()> {
let cluster_path = cluster_path.cluster_path().await?;
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? {
let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
let path = entry.path();
if path.is_dir() {
io::remove_dir_all(&path).await?;
}
}
Ok(())
}
/// Get the [`LogManager`] for a specific [`ClusterPath`] log file.
#[tracing::instrument]
pub async fn get_logs_by_file(
cluster_path: ClusterPath,
log_type: LogType,
log_file: String,
) -> crate::Result<LogManager> {
let cluster_path = cluster_path.cluster_path().await?;
let path = match log_type {
LogType::Info => Directories::cluster_logs_dir(&cluster_path).await,
LogType::Crash => Directories::crash_reports_dir(&cluster_path).await,
}?
.join(&log_file);
let metadata = std::fs::metadata(&path)?;
let age = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
LogManager::initialize(log_type, age, &cluster_path, log_file, Some(true)).await
}
/// Get the default [`LogCursor`] for a [`ClusterPath`].
#[tracing::instrument]
pub async fn get_log_cursor(cluster_path: ClusterPath, cursor: u64) -> crate::Result<LogCursor> {
get_live_log_cursor(cluster_path, "latest.log", cursor).await
}
/// Get a live [`LogCursor`] for a [`ClusterPath`]'s log file.
#[tracing::instrument]
pub async fn get_live_log_cursor(
cluster_path: ClusterPath,
log_file: &str,
mut cursor: u64,
) -> crate::Result<LogCursor> {
let cluster_path = cluster_path.cluster_path().await?;
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
let path = logs_folder.join(log_file);
if !path.exists() {
return Ok(LogCursor {
cursor: 0,
new: false,
output: LogOutput(String::new()),
});
}
let mut file = tokio::fs::File::open(&path)
.await
.map_err(|e| IOError::with_path(e, &path))?;
let metadata = file
.metadata()
.await
.map_err(|e| IOError::with_path(e, &path))?;
let new = if cursor > metadata.len() {
cursor = 0;
true
} else {
false
};
let mut buf = Vec::new();
file.seek(std::io::SeekFrom::Start(cursor))
.map_err(|e| IOError::with_path(e, &path))
.await?;
let bytes_read = file
.read_to_end(&mut buf)
.map_err(|e| IOError::with_path(e, &path))
.await?;
let output = String::from_utf8_lossy(&buf).to_string();
let cursor = cursor + bytes_read as u64;
let creds: Vec<MinecraftCredentials> = crate::State::get()
.await?
.users
.read()
.await
.users
.clone()
.into_values()
.collect();
let output = LogOutput::censor_secrets(output, &creds, &None);
Ok(LogCursor {
cursor,
output,
new,
})
}
/// Delete a specific minecraft log file.
#[tracing::instrument]
pub async fn delete_logs_by_file(
cluster_path: ClusterPath,
log_type: LogType,
log_file: &str,
) -> crate::Result<()> {
let cluster_path = cluster_path.cluster_path().await?;
let logs_folder = match log_type {
LogType::Info => Directories::cluster_logs_dir(&cluster_path).await?,
LogType::Crash => Directories::crash_reports_dir(&cluster_path).await?,
};
let path = logs_folder.join(log_file);
io::remove_file(&path).await?;
Ok(())
}
pub async fn read_log_to_string(path: &std::path::PathBuf) -> crate::Result<String> {
if let Some(ext) = path.extension() {
let mut result = String::new();
if ext == "gz" {
result = io::read_gz_to_string(path).await?;
} else if ext == "log" || ext == "txt" {
// On Java 17 and older, UTF-8 is not the default charset (https://openjdk.org/jeps/400)
// Minecraft on Windows on older versions sometimes likes to output the log
// in an encoding other than UTF-8, which would make OneLauncher throw an error
// when attempting to read it into a String (Rust Strings are UTF-8)
// TODO: Potentially explore a better solution for non UTF-8 log reading?
let bytes = io::read(path).await?;
result = String::from_utf8_lossy(&bytes).to_string();
}
return Ok(result);
}
Err(anyhow::anyhow!("log file extension {} not supported", path.display()).into())
}
/// Get a specific [`ClusterPath`] log file's [`LogOutput`].
#[tracing::instrument]
pub async fn get_output_by_file(
cluster_path: &ClusterPath,
log_type: LogType,
log_file: &str,
) -> crate::Result<LogOutput> {
let logs_folder = match log_type {
LogType::Info => Directories::cluster_logs_dir(cluster_path).await?,
LogType::Crash => Directories::crash_reports_dir(cluster_path).await?,
};
let path = logs_folder.join(log_file);
let credentials: Vec<MinecraftCredentials> = crate::State::get()
.await?
.users
.read()
.await
.users
.clone()
.into_values()
.collect();
let result = read_log_to_string(&path).await?;
return Ok(LogOutput::censor_secrets(result, &credentials, &None));
}
/// The log cursor used to parse logs passed into [`LogManager`].
#[derive(Serialize, Debug)]
pub struct LogCursor {
/// The cursor ID.
pub cursor: u64,
/// The [`LogOutput`] associated with this cursor.
pub output: LogOutput,
/// Whether or not this corresponds is a new log file.
pub new: bool,
}
/// The log output, a wrapper around [`String`], with utilities for parsing and censoring log contents.
#[derive(Serialize, Debug)]
#[serde(transparent)]
pub struct LogOutput(pub String);
impl LogOutput {
/// Censor user secrets because sometimes mclogs misses them, including username, realname and minecraft credentials.
pub fn censor_secrets(
mut output: String,
credentials: &Vec<MinecraftCredentials>,
_credentials_store: &Option<Credentials>,
) -> Self {
let username = whoami::username();
let realname = whoami::realname();
output = output.replace(&format!("/{username}/"), "/{ENV_USERNAME}/");
output = output.replace(&format!("\\{username}\\"), "\\{ENV_USERNAME}\\");
output = output.replace(&format!("/{realname}/"), "/{ENV_REALNAME}/");
output = output.replace(&format!("\\{realname}\\"), "\\{ENV_REALNAME}\\");
for cred in credentials {
output = output.replace(&cred.access_token, "{MC_ACCESS_TOKEN}");
output = output.replace(&cred.username, "{MC_USERNAME}");
output = output.replace(&cred.id.as_simple().to_string(), "{MC_UUID}");
output = output.replace(&cred.id.as_hyphenated().to_string(), "{MC_UUID}");
}
Self(output)
}
}
#[derive(serde::Deserialize)]
struct McLogsResponse {
pub success: bool,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub error: Option<String>,
}
/// Upload a log to https://mclo.gs/. If successful, it returns the log id
#[tracing::instrument]
pub async fn upload_log(path: &ClusterPath, log: LogOutput) -> crate::Result<String> {
let log = log.0;
let mut form = HashMap::new();
form.insert("content", log);
let parsed = serde_json::from_slice::<McLogsResponse>(
&http::REQWEST_CLIENT
.post(format!("{}/log", crate::constants::MCLOGS_API_URL))
.header("Content-Type", "application/x-www-form-urlencoded")
.form(&form)
.send()
.await?
.bytes()
.await?,
)?;
if parsed.success {
parsed.id.map_or_else(
|| Err(anyhow::anyhow!("failed to get log id from mclo.gs").into()),
Ok,
)
} else {
Err(anyhow::anyhow!(
"failed to upload log to mclo.gs: {}",
parsed.error.unwrap_or_default()
)
.into())
}
}

View file

@ -1,4 +0,0 @@
pub mod logger;
pub mod package;
pub mod screenshots;
pub mod worlds;

View file

@ -1,297 +0,0 @@
#![allow(clippy::significant_drop_tightening)]
use crate::data::{Loader, ManagedPackage, ManagedVersion, PackageType};
use crate::prelude::PackagePath;
use crate::processor::Cluster;
use crate::proxy::send::{init_ingress, send_ingress, send_internet};
use crate::store::{ClusterPath, ManagedVersionFile, Package, PackageMetadata};
use crate::utils::http;
use crate::{Result, State};
use onelauncher_utils::io;
use reqwest::Method;
// TODO: Implement proper error handling
/// Find a managed version using filters
/// - Game Version (Default: Cluster's MC Version)
/// - Loader (Default: Cluster's chosen Loader)
/// - Package Version (Default: Latest version available)
#[tracing::instrument]
pub async fn find_managed_version(
package: &ManagedPackage,
game_version: Option<String>,
loader: Option<Loader>,
package_version: Option<String>,
) -> Result<ManagedVersion> {
let provider = package.provider.clone();
let versions = provider
.get_all_versions(
&package.id,
game_version.as_ref().map(|v| vec![v.to_owned()]).clone(),
loader.map(|l| vec![l]).clone(),
None,
None,
)
.await?;
Ok(versions.0
.iter()
.find(|v| {
if let Some(package_version) = package_version.as_ref() {
return v.id == *package_version;
}
let check_game_version = game_version
.as_ref()
.is_some_and(|gv| v.game_versions.iter().any(|gv2| *gv2 == *gv));
let check_loader = loader
.as_ref()
.is_some_and(|loader| v.loaders.iter().any(|l| *l == *loader));
check_game_version && check_loader
})
.ok_or_else(|| anyhow::anyhow!("no matching version found"))
.cloned()?)
}
/// Download a package to a cluster. Supports filtering by:
/// - Game Version (Default: Cluster's MC Version)
/// - Loader (Default: Cluster's chosen Loader)
/// - Package Version (Default: Latest version available)
#[tracing::instrument(skip(package, cluster))]
pub async fn download_package(
package: &ManagedPackage,
cluster: &mut Cluster,
game_version: Option<String>,
loader: Option<Loader>,
package_version: Option<String>,
) -> Result<(PackagePath, Package)> {
tracing::info!(
"preparing package '{}' for cluster '{}'",
package.title,
cluster.meta.name
);
send_internet(crate::proxy::InternetPayload::InstallPackage {
id: package.id.clone(),
})
.await?;
let loader = loader.unwrap_or(cluster.meta.loader);
let game_version = game_version.unwrap_or_else(|| cluster.meta.mc_version.clone());
let managed_version =
find_managed_version(package, Some(game_version), Some(loader), package_version).await?;
let file = managed_version
.get_primary_file()
.ok_or_else(|| anyhow::anyhow!("no primary file found"))?;
let package_path = download_file(package, &managed_version, file, &package.package_type, cluster).await?;
let sha1 = file.hashes.get("sha1").unwrap_or(&String::new()).clone();
let package = Package {
file_name: file.file_name.clone(),
sha1,
meta: PackageMetadata::from_managed_package(package.clone(), managed_version),
disabled: false,
};
Ok((package_path, package))
}
/// Download a file to a cluster from a managed version file.
#[tracing::instrument(skip_all)]
async fn download_file(
package: &ManagedPackage,
version: &ManagedVersion,
file: &ManagedVersionFile,
package_type: &PackageType,
cluster: &Cluster,
) -> Result<PackagePath> {
// TODO: Implement hash checking
let cluster_path = &cluster.get_full_path().await?;
let path = &cluster_path
.join(package_type.get_folder())
.join(&file.file_name);
let ingress_id = init_ingress(
crate::IngressType::DownloadPackage {
cluster_path: cluster_path.to_owned(),
package_name: package.title.clone(),
icon: None,
package_id: Some(package.id.clone()),
package_version: Some(version.id.clone()),
}, 100.0, "downloading package").await?;
tracing::info!(
"downloading file '{}' version '{}'",
file.file_name,
version.name
);
let state = State::get().await?;
let bytes = http::fetch_advanced(
Method::GET,
&file.url,
file.hashes.get("sha1").map(String::as_str),
None,
None,
Some((&ingress_id, 90.0)),
&state.fetch_semaphore,
)
.await?;
if let Err(err) = send_ingress(&ingress_id, 5.0, Some("saving file")).await {
tracing::error!("{err}");
}
if let Err(err) = http::write(&path, &bytes, &state.io_semaphore).await {
tracing::error!("failed to write file to cluster: {err}");
if path.exists() {
let _ = io::remove_file(&path).await;
}
return Err(err);
};
if let Err(err) = send_ingress(&ingress_id, 100.0, Some(format!("downloaded package {}", package.title).as_str())).await {
tracing::error!("{err}");
}
drop(state);
Ok(PackagePath::from_path(path).await?)
}
/// Add a package to a cluster.
#[tracing::instrument]
pub async fn add_package(
cluster_path: &ClusterPath,
package_path: PackagePath,
package: Package,
) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
let manager = manager
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager
.add_package(package_path, package)
.await?;
Ok(())
}
/// Remove a package from a cluster.
#[tracing::instrument]
pub async fn remove_package(
cluster_path: &ClusterPath,
package_path: &PackagePath,
) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
let manager = manager
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager.remove_package(package_path).await?;
Ok(())
}
#[tracing::instrument]
pub async fn set_package_enabled(
cluster_path: &ClusterPath,
package_path: &PackagePath,
enabled: bool,
) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
let manager = manager
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager.set_package_enabled(package_path, enabled).await?;
Ok(())
}
/// Get a package from a cluster.
#[tracing::instrument]
pub async fn get_package(
cluster_path: &ClusterPath,
package_path: &PackagePath,
package_type: PackageType,
) -> Result<Package> {
let state = State::get().await?;
let mut store = state.packages.write().await;
let manager = store
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
Ok(manager
.get(package_type)
.await
.packages
.get(package_path)
.cloned()
.ok_or_else(|| anyhow::anyhow!("package not found"))?)
}
/// Get packages from a cluster.
#[tracing::instrument]
pub async fn get_packages(
cluster_path: &ClusterPath,
package_type: PackageType,
) -> Result<Vec<Package>> {
let state = State::get().await?;
let mut store = state.packages.write().await;
let manager = store
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
Ok(manager
.get(package_type)
.await
.packages
.values()
.cloned()
.collect())
}
/// Sync packages from a cluster.
#[tracing::instrument]
pub async fn sync_packages(cluster_path: &ClusterPath) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
let manager = manager
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager.sync_packages(&state.directories).await;
Ok(())
}
/// Sync packages from a cluster.
#[tracing::instrument]
pub async fn sync_packages_by_type(
cluster_path: &ClusterPath,
package_type: PackageType,
clear: Option<bool>
) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
let manager = manager
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager
.sync_packages_by_type(&state.directories, package_type, clear)
.await?;
Ok(())
}

View file

@ -1,23 +0,0 @@
use crate::Result;
use onelauncher_utils::io;
use crate::store::ClusterPath;
/// Gets a list of screenshot file names from the [`ClusterPath`]
#[tracing::instrument]
pub async fn get_screenshots(cluster: &ClusterPath) -> Result<Vec<String>> {
let dir = cluster.full_path().await?.join("screenshots");
if !dir.exists() {
io::create_dir(dir).await?;
return Ok(vec![]);
}
let mut list = vec![];
let mut files = io::read_dir(dir).await?;
while let Some(file) = files.next_entry().await? {
list.push(file.file_name().to_string_lossy().to_string());
}
Ok(list)
}

View file

@ -1,23 +0,0 @@
use crate::Result;
use onelauncher_utils::io;
use crate::store::ClusterPath;
/// Gets a list of screenshot file names from the [`ClusterPath`]
#[tracing::instrument]
pub async fn get_worlds(cluster: &ClusterPath) -> Result<Vec<String>> {
let dir = cluster.full_path().await?.join("saves");
if !dir.exists() {
io::create_dir(dir).await?;
return Ok(vec![]);
}
let mut list = vec![];
let mut files = io::read_dir(dir).await?;
while let Some(file) = files.next_entry().await? {
list.push(file.file_name().to_string_lossy().to_string());
}
Ok(list)
}

View file

@ -1,333 +0,0 @@
//! Utilities for creating Clusters
use crate::data::Loader;
use crate::package::from::CreatePackCluster;
use crate::proxy::send::send_cluster;
use crate::proxy::ClusterPayloadType;
pub use crate::store::{Cluster, ClusterPath, JavaOptions, PackageData, State};
use crate::{cluster, package};
use interpulse::api::modded::LoaderVersion;
use onelauncher_utils::io::{self, canonicalize};
use std::path::PathBuf;
/// Creates an unfinished [`Cluster`] used for building onto it and adds it to the memory [`State`].
/// Returns a relative filepath ([`ClusterPath`]) which can be used to access the cluster.
#[tracing::instrument]
#[onelauncher_macros::memory]
#[allow(clippy::too_many_arguments)]
pub async fn create_unfinished_cluster(
mut name: String,
) -> crate::Result<ClusterPath> {
name = cluster::sanitize_cluster_name(&name);
tracing::trace!("creating new cluster {}", name);
let state = State::get().await?;
let uuid = uuid::Uuid::new_v4();
let mut path = state.directories.clusters_dir().await.join(&name);
// i hate it here
if path.exists() {
let mut new_name;
let mut new_path;
let mut which = 1;
loop {
new_name = format!("{name} ({which})");
new_path = state.directories.clusters_dir().await.join(&new_name);
if !new_path.exists() {
break;
}
which += 1;
}
tracing::debug!(
"collision while creating new cluster: {}, renaming to {}",
path.display(),
new_path.display()
);
path = new_path;
name = new_name;
}
io::create_dir_all(&path).await?;
tracing::info!(
"creating cluster at path {}",
&canonicalize(&path)?.display()
);
let cluster = Cluster::new(uuid, name, String::from("none")).await?;
let result = async {
{
state
.clusters
.write()
.await
.insert(cluster.clone(), false)
.await?;
}
State::sync().await?;
let state = State::get().await?;
let mut packages = state.packages.write().await;
packages.add_cluster(cluster.cluster_path());
Ok(cluster.cluster_path())
}
.await;
match result {
Ok(cluster) => Ok(cluster),
Err(err) => {
let _ = cluster::remove(&cluster.cluster_path()).await;
Err(err)
}
}
}
/// Creates a [`Cluster`] and adds it to the memory [`State`].
/// Returns a relative filepath ([`ClusterPath`]) which can be used to access the cluster.
#[tracing::instrument]
#[onelauncher_macros::memory]
#[allow(clippy::too_many_arguments)]
pub async fn create_cluster(
name: String,
mc_version: String,
mod_loader: Loader,
loader_version: Option<String>,
icon: Option<PathBuf>,
icon_url: Option<String>,
package_data: Option<PackageData>,
skip: Option<bool>,
skip_watch: Option<bool>,
) -> crate::Result<ClusterPath> {
let mut cluster = cluster::get(&create_unfinished_cluster(name).await?)
.await?
.ok_or(anyhow::anyhow!("failed to get cluster"))?;
let state = State::get().await?;
let loader = if mod_loader == Loader::Vanilla {
None
} else {
get_loader_version(mc_version.clone(), mod_loader, loader_version).await?
};
let result = async {
cluster.meta.mc_version = mc_version;
cluster.meta.loader = mod_loader;
cluster.meta.loader_version = loader.clone();
if let Some(ref icon) = icon {
let bytes = io::read(state.directories.caches_dir().await.join(icon)).await?;
cluster
.set_icon_bytes(
&state.directories.caches_dir().await,
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await?;
}
cluster.meta.icon_url = icon_url;
if let Some(loader_version) = loader {
cluster.meta.loader = mod_loader;
cluster.meta.loader_version = Some(loader_version);
}
cluster.meta.package_data = package_data;
if let Some(package_data) = &mut cluster.meta.package_data {
package_data.locked =
Some(package_data.package_id.is_some() && package_data.version_id.is_some());
}
send_cluster(
cluster.uuid,
&cluster.cluster_path(),
&cluster.meta.name,
ClusterPayloadType::Created,
)
.await?;
{
state
.clusters
.write()
.await
.insert(cluster.clone(), skip_watch.unwrap_or_default())
.await?;
}
if !skip.unwrap_or(false) {
// let cluster = cluster.clone();
// tokio::task::spawn(async move {
if let Err(err) = crate::game::install_minecraft(&cluster, None, false).await {
tracing::error!(
"failed to install Minecraft while creating cluster {}: {}",
cluster.uuid,
err
);
}
// });
}
State::sync().await?;
let state = State::get().await?;
let mut packages = state.packages.write().await;
packages.add_cluster(cluster.cluster_path());
Ok(cluster.cluster_path())
}
.await;
match result {
Ok(cluster) => Ok(cluster),
Err(err) => {
let _ = cluster::remove(&cluster.cluster_path()).await;
Err(err)
}
}
}
/// Create a [`Cluster`] from a [`CreatePackCluster`] cluster, returning a [`ClusterPath`].
pub async fn create_cluster_from_package(cluster: CreatePackCluster) -> crate::Result<ClusterPath> {
create_cluster(
cluster.name,
cluster.mc_version,
cluster.mod_loader,
cluster.loader_version,
cluster.icon,
cluster.icon_url,
cluster.package_data,
cluster.skip,
cluster.skip_watch,
)
.await
}
/// Create a duplicate [`Cluster`] from another [`ClusterPath`], returning a new [`ClusterPath`]
pub async fn create_cluster_from_duplicate(from: ClusterPath) -> crate::Result<ClusterPath> {
let cluster = cluster::get(&from).await?.ok_or_else(|| {
anyhow::anyhow!("failed to get unmanaged cluster from {}", from.to_string())
})?;
let cluster_path = create_cluster(
cluster.meta.name.clone(),
cluster.meta.mc_version.clone(),
cluster.meta.loader,
cluster.meta.loader_version.clone().map(|it| it.id),
cluster.meta.icon.clone(),
cluster.meta.icon_url.clone(),
cluster.meta.package_data.clone(),
Some(true),
Some(true),
)
.await?;
let copied = package::import::copy_minecraft(
cluster_path.clone(),
from.full_path().await?,
&State::get().await?.io_semaphore,
None,
)
.await?;
let duplicated = cluster::get(&cluster_path).await?.ok_or_else(|| {
anyhow::anyhow!(
"failed to get unmanaged cluster from {}",
cluster_path.to_string()
)
})?;
crate::game::install_minecraft(&duplicated, Some(copied), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?;
}
send_cluster(
cluster.uuid,
&cluster.cluster_path(),
&cluster.meta.name,
ClusterPayloadType::Edited,
)
.await?;
State::sync().await?;
Ok(cluster_path)
}
/// Get the latest [`LoaderVersion`] from a [`Loader`].
#[tracing::instrument]
#[onelauncher_macros::memory]
pub(crate) async fn get_loader_version(
mc_version: String,
loader: Loader,
loader_version: Option<String>,
) -> crate::Result<Option<LoaderVersion>> {
let state = State::get().await?;
let metadata = state.metadata.read().await;
let version = loader_version.unwrap_or_else(|| "latest".to_string());
let filter = |it: &LoaderVersion| match version.as_str() {
"latest" => true,
"stable" => it.stable,
id => {
it.id == *id
|| format!("{mc_version}-{id}") == it.id
|| format!("{mc_version}-{id}-{mc_version}") == it.id
}
};
let loader_meta = match loader {
Loader::Forge => &metadata.forge,
Loader::Fabric => &metadata.fabric,
Loader::Quilt => &metadata.quilt,
Loader::NeoForge => &metadata.neoforge,
// Loader::LegacyFabric => &metadata.legacy_fabric,
_ => return Err(CreateClusterError::MissingManifest(loader.to_string()).into()),
};
let loader_meta = loader_meta
.to_owned()
.ok_or_else(|| anyhow::anyhow!("couldn't get game versions"))?;
let loaders = &loader_meta
.game_versions
.iter()
.find(|it| {
it.id
.replace(interpulse::api::modded::DUMMY_REPLACE_STRING, &mc_version)
== mc_version
})
.ok_or_else(|| {
CreateClusterError::UnsupportedLoader(loader.to_string(), mc_version.clone())
})?
.loaders;
let loader_version = loaders
.iter()
.find(|&it| filter(it))
.cloned()
.or_else(|| {
if version == "stable" {
loaders.iter().next().cloned()
} else {
None
}
})
.ok_or_else(|| CreateClusterError::InvalidLoaderVersion(version, loader.to_string()))?;
Ok(Some(loader_version))
}
#[derive(thiserror::Error, Debug)]
pub enum CreateClusterError {
#[error("Loader {0} is unsupported on Minecraft version {1}")]
UnsupportedLoader(String, String),
#[error("Invalid Loader version {0} for Loader {1}")]
InvalidLoaderVersion(String, String),
#[error("Failed to get Loader manifest for {0}.")]
MissingManifest(String),
#[error("Failed to handle I/O operations: {0}")]
IOError(#[from] std::io::Error),
}

View file

@ -1,436 +0,0 @@
//! **`OneLauncher` Cluster**
//!
//! API for creating our managed Minecraft instances, Clusters.
// TODO: (pauline) fully implement the cluster::self APIs.
use crate::proxy::send::send_cluster;
use crate::prelude::{ClusterPath, JavaVersion, PackagePath};
use crate::proxy::ClusterPayloadType;
pub use crate::store::{Cluster, JavaOptions, State};
use crate::store::{MinecraftCredentials, ProcessorChild};
use onelauncher_utils::io::{self, IOError};
use std::collections::HashMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::process::Command;
use tokio::sync::RwLock;
pub mod content;
pub mod create;
pub mod update;
/// get a cluster by its specified [`ClusterPath`].
#[tracing::instrument]
pub async fn get(path: &ClusterPath) -> crate::Result<Option<Cluster>> {
let state = State::get().await?;
let clusters = state.clusters.read().await;
let cluster = clusters.0.get(path).cloned();
Ok(cluster)
}
/// get a cluster by its specified [`ClusterPath`].
#[tracing::instrument]
pub async fn get_by_name(name: &str) -> crate::Result<Option<Cluster>> {
let state = State::get().await?;
let clusters = state.clusters.read().await;
let cluster = clusters.0.values().find(|c| c.meta.name == name).cloned();
Ok(cluster)
}
/// get a list of all [`Cluster`]s
#[tracing::instrument]
pub async fn list() -> crate::Result<Vec<Cluster>> {
let state = State::get().await?;
let clusters = state.clusters.read().await;
Ok(clusters.0.clone().into_iter().map(|it| it.1).collect())
}
/// get a Map of [`Cluster`] group names containing lists of [`Cluster`]s
#[tracing::instrument]
pub async fn list_grouped() -> crate::Result<HashMap<String, Vec<Cluster>>> {
// TODO: This can 100% be made better
let list = list().await?;
let mut map = HashMap::<String, Vec<Cluster>>::new();
for cluster in list {
let group = cluster
.meta
.group
.clone()
.unwrap_or_else(|| String::from("Ungrouped"));
if let Some(items) = map.get_mut(&group) {
items.push(cluster);
} else {
map.insert(group, vec![cluster]);
}
}
Ok(map)
}
/// run a Minecraft [`Cluster`] using the default credentials.
#[tracing::instrument]
pub async fn run_default(path: &ClusterPath) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
let state = State::get().await?;
let default = {
let mut users = state.users.write().await;
users
.get_default()
.await?
.ok_or_else(|| anyhow::anyhow!("no default credentials found!"))?
};
run_credentials(path, &default).await
}
/// run a Minecraft [`Cluster`] using [`MinecraftCredentials`] for authentication.
/// returns an [`Arc`] pointer to [`RwLock`] to [`ProcessorChild`].
#[tracing::instrument(skip(creds))]
#[onelauncher_macros::memory]
pub async fn run(
path: &ClusterPath,
creds: Option<&MinecraftCredentials>,
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
if let Some(creds) = creds {
run_credentials(path, creds).await
} else {
run_default(path).await
}
}
/// run a Minecraft [`Cluster`] using [`MinecraftCredentials`] for authentication.
/// returns an [`Arc`] pointer to [`RwLock`] to [`ProcessorChild`].
#[tracing::instrument(skip(creds))]
#[onelauncher_macros::memory]
pub async fn run_credentials(
path: &ClusterPath,
creds: &MinecraftCredentials,
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
let state = State::get().await?;
let settings = state.settings.read().await;
let cluster = get(path)
.await?
.ok_or_else(|| anyhow::anyhow!("failed to run a nonexistent cluster at path {}", path))?;
let pre = &cluster
.init_hooks
.as_ref()
.unwrap_or(&settings.init_hooks)
.pre
.as_ref()
.and_then(|it| {
if it.is_empty() {
None
} else {
Some(it.clone())
}
});
if let Some(hook) = pre {
// TODO: hook parameters
let mut cmd = hook.split(' ');
if let Some(command) = cmd.next() {
let full_path = path.full_path().await?;
let result = Command::new(command)
.args(cmd)
.current_dir(&full_path)
.spawn()
.map_err(|e| IOError::with_path(e, &full_path))?
.wait()
.await
.map_err(IOError::from)?;
if !result.success() {
return Err(anyhow::anyhow!(
"non-zero exit code for pre-launch hook: {}",
result.code().unwrap_or(-1)
)
.into());
}
}
}
let java_args = cluster
.java
.as_ref()
.and_then(|it| it.custom_arguments.as_ref())
.unwrap_or(&settings.custom_java_args);
let wrapper = cluster
.init_hooks
.as_ref()
.map_or(&settings.init_hooks.wrapper, |it| &it.wrapper)
.as_ref()
.and_then(|it| {
if it.is_empty() {
None
} else {
Some(it.clone())
}
});
let memory = cluster.memory.unwrap_or(settings.memory);
let resolution = cluster.resolution.unwrap_or(settings.resolution);
let env_args = cluster
.java
.as_ref()
.and_then(|it| it.custom_env_arguments.as_ref())
.unwrap_or(&settings.custom_env_args);
let post = cluster
.init_hooks
.as_ref()
.unwrap_or(&settings.init_hooks)
.post
.clone();
let mut mc_options: Vec<(String, String)> = vec![];
if let Some(fullscreen) = cluster.force_fullscreen {
mc_options.push(("fullscreen".to_string(), fullscreen.to_string()));
} else if settings.force_fullscreen {
mc_options.push(("fullscreen".to_string(), "true".to_string()));
}
let process = crate::game::launch_minecraft(
&cluster,
java_args,
env_args,
&mc_options,
post,
creds,
&resolution,
&memory,
&wrapper,
)
.await?;
Ok(process)
}
/// remove a specified cluster from it's [`ClusterPath`].
#[tracing::instrument]
pub async fn remove(path: &ClusterPath) -> crate::Result<()> {
let state = State::get().await?;
let mut clusters = state.clusters.write().await;
if let Some(cluster) = clusters.remove(path).await? {
send_cluster(
cluster.uuid,
path,
&cluster.meta.name,
ClusterPayloadType::Deleted,
)
.await?;
}
Ok(())
}
/// get a cluster by it's [`uuid::Uuid`]
#[tracing::instrument]
pub async fn get_by_uuid(uuid: uuid::Uuid) -> crate::Result<Option<Cluster>> {
let state = State::get().await?;
let clusters = state.clusters.read().await;
let cluster = clusters.0.values().find(|c| c.uuid == uuid).cloned();
Ok(cluster)
}
/// get a cluster's full path by it's [`ClusterPath`].
#[tracing::instrument]
pub async fn get_full_path(path: &ClusterPath) -> crate::Result<PathBuf> {
let _ = get(path).await?.ok_or_else(|| {
anyhow::anyhow!("failed to get the full path of cluster at path {}", path)
})?;
let full_path = io::canonicalize(path.full_path().await?)?;
Ok(full_path)
}
/// get a specific mod's full path in the filesystem by it's [`ClusterPath`] and [`PackagePath`].
#[tracing::instrument]
pub async fn get_package_full_path(
cluster_path: &ClusterPath,
package_path: &PackagePath,
) -> crate::Result<PathBuf> {
if get(cluster_path).await?.is_some() {
let full_path = io::canonicalize(package_path.full_path(cluster_path).await?)?;
return Ok(full_path);
}
Err(anyhow::anyhow!(
"failed to get the full path of a cluster at path {}",
package_path
.full_path(cluster_path)
.await?
.display()
)
.into())
}
/// edit a cluster with an async closure and it's [`ClusterPath`]
pub async fn edit<FutFn>(
path: &ClusterPath,
action: impl Fn(&mut Cluster) -> FutFn,
) -> crate::Result<()>
where
FutFn: Future<Output = crate::Result<()>>,
{
let state = State::get().await?;
let mut clusters = state.clusters.write().await;
match clusters.0.get_mut(path) {
Some(ref mut cluster) => {
action(cluster).await?;
send_cluster(
cluster.uuid,
path,
&cluster.meta.name,
ClusterPayloadType::Edited,
)
.await?;
Ok(())
}
None => Err(anyhow::anyhow!("unmanaged cluster edited at {}", path.to_string()).into()),
}
}
/// update a [`Cluster`]'s icon
pub async fn edit_icon(path: &ClusterPath, icon_path: Option<&Path>) -> crate::Result<()> {
let state = State::get().await?;
let result = if let Some(icon) = icon_path {
let bytes = io::read(icon).await?;
let mut clusters = state.clusters.write().await;
match clusters.0.get_mut(path) {
Some(ref mut cluster) => {
cluster
.set_icon_bytes(
&state.directories.caches_dir().await,
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await?;
send_cluster(
cluster.uuid,
path,
&cluster.meta.name,
ClusterPayloadType::Edited,
)
.await?;
Ok(())
}
None => Err(anyhow::anyhow!(
"failed to update unmanaged cluster at {}",
path.to_string()
)
.into()),
}
} else {
edit(path, |cluster| {
cluster.meta.icon = None;
async { Ok(()) }
})
.await?;
State::sync().await?;
Ok(())
};
State::sync().await?;
result
}
/// gets the optimal java version for a given [`Cluster`].
pub async fn get_optimal_java_version(path: &ClusterPath) -> crate::Result<Option<JavaVersion>> {
let state = State::get().await?;
if let Some(cluster) = get(path).await? {
let metadata = state.metadata.read().await;
let minecraft_metadata = metadata
.minecraft
.clone()
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft metadata"))?;
let version = minecraft_metadata
.versions
.iter()
.find(|it| it.id == cluster.meta.mc_version)
.ok_or_else(|| {
anyhow::anyhow!(
"invalid or unknown Minecraft version {}",
cluster.meta.mc_version
)
})?;
let version_info = crate::game::metadata::download_version_info(
&state,
version,
cluster.meta.loader_version.as_ref(),
None,
None,
)
.await?;
let version = crate::game::java_version_from_cluster(&cluster, &version_info).await?;
Ok(version)
} else {
Err(anyhow::anyhow!(
"failed to get the java version of unmanaged cluster at {}",
path.to_string()
)
.into())
}
}
/// Try to update a [`Cluster`]'s playtime.
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn update_playtime(path: &ClusterPath) -> crate::Result<()> {
let state = State::get().await?;
let cluster = get(path)
.await?
.ok_or_else(|| anyhow::anyhow!("failed to update playtime at path {}", path))?;
let recent_playtime = cluster.meta.recently_played;
/*
* todo
*/
let mut clusters = state.clusters.write().await;
if let Some(cluster) = clusters.0.get_mut(path) {
cluster.meta.overall_played += recent_playtime;
cluster.meta.recently_played = 0;
}
drop(clusters);
State::sync().await?;
Ok(())
}
/// This forces the cluster to be verified (and possibly reinstalled).
pub async fn repair_cluster(path: &ClusterPath) -> crate::Result<()> {
let cluster = get(path)
.await?
.ok_or_else(|| anyhow::anyhow!("failed to repair cluster at path {}", path))?;
crate::game::install_minecraft(&cluster, None, true).await
}
/// Sanitize a user-inputted [`Cluster`] name.
#[must_use]
pub fn sanitize_cluster_name(input: &str) -> String {
input.replace(['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>'], "_")
}

View file

@ -1,3 +0,0 @@
//! Utilities for updating Clusters
// TODO: requires `store/package.rs` to be implemented.

View file

@ -1,42 +0,0 @@
use std::path::PathBuf;
use crate::api::prelude::InternetPayload;
use crate::proxy::send::{send_internet, send_message};
use onelauncher_utils::io;
pub async fn parse_uri(cmd: &str) -> crate::Result<()> {
let cmd = handle_cmd(cmd).await?;
send_internet(cmd).await?;
Ok(())
}
pub async fn handle_cmd(cmd: &str) -> crate::Result<InternetPayload> {
tracing::debug!("parsing command {}", &cmd);
if let Some(sub) = cmd.strip_prefix("onelauncher://") {
Ok(handle_uri(sub).await?)
} else {
let path = io::canonicalize(PathBuf::from(cmd))?;
if let Some(extension) = path.extension() {
if extension == "zip" {
return Ok(InternetPayload::InstallPath { path });
}
}
send_message(&format!("invalid command filetype {}", path.display())).await?;
Err(anyhow::anyhow!("invalid command filename {}", path.display()).into())
}
}
pub async fn handle_uri(sub: &str) -> crate::Result<InternetPayload> {
Ok(match sub.split_once('/') {
// ://pkg/{id} - installs package id
Some(("pkg", id)) => InternetPayload::InstallPackage { id: id.to_string() },
// ://pack/{id} - installs pack id
Some(("pack", id)) => InternetPayload::InstallPack { id: id.to_string() },
// unknown uri command: error
_ => {
send_message(&format!("invalid command {sub}")).await?;
return Err(anyhow::anyhow!("invalid command {sub}").into());
}
})
}

View file

@ -1,5 +0,0 @@
use crate::store::{IngressProcessType, IngressProcessor};
pub async fn check_ingress_feeds() -> crate::Result<bool> {
IngressProcessor::finished(IngressProcessType::IngressFeed).await
}

View file

@ -1,148 +0,0 @@
use std::path::PathBuf;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::proxy::send::{init_ingress, send_ingress};
use crate::utils::http::{fetch_advanced, fetch_json};
use crate::utils::java::{self, get_java_version, JavaVersion};
use crate::State;
use onelauncher_utils::io::{self, IOError};
pub async fn filter_java_version(java_version: Option<u32>) -> crate::Result<Vec<JavaVersion>> {
let java = java::locate_java().await?;
Ok(if let Some(java_version) = java_version {
java.into_iter()
.filter(|j| {
let jre_version = get_java_version(&j.version);
jre_version.map_or(false, |jre_version| jre_version.1 == java_version)
})
.collect()
} else {
java
})
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize)]
pub struct JavaZuluPackage {
pub download_url: String,
pub name: PathBuf,
pub java_version: Vec<u32>,
}
pub async fn get_zulu_packages() -> crate::Result<Vec<JavaZuluPackage>> {
let state = State::get().await?;
fetch_json::<Vec<JavaZuluPackage>>(
Method::GET,
format!(
"https://api.azul.com/metadata/v1/zulu/packages/?os={}&arch={}&archive_type=zip&java_package_type=jre&javafx_bundled=false&latest=true&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100",
std::env::consts::OS,
std::env::consts::ARCH
).as_str(),
None,
None,
&state.fetch_semaphore,
)
.await
}
// TODO: support more than just zulu ?
#[onelauncher_macros::memory]
pub async fn install_java_from_major(java_version: u32) -> crate::Result<PathBuf> {
let packages = get_zulu_packages().await?;
let package = packages
.into_iter()
.find(|p| p.java_version.contains(&java_version))
.ok_or_else(|| anyhow::anyhow!(
"Could not find a java package for version {}",
java_version
))?;
install_java_from_package(package).await
}
#[onelauncher_macros::memory]
pub async fn install_java_from_package(download: JavaZuluPackage) -> crate::Result<PathBuf> {
let state = State::get().await?;
let java_version = *download.java_version.first().unwrap_or(&0);
let ingress = init_ingress(
crate::IngressType::DownloadJava {
version: java_version,
},
100.0,
"downloading java version",
)
.await?;
send_ingress(&ingress, 0.0, Some("downloading java version")).await?;
let file = fetch_advanced(
Method::GET,
&download.download_url,
None,
None,
None,
Some((&ingress, 80.0)),
&state.fetch_semaphore,
)
.await?;
let path = state.directories.java_dir().await;
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(file)).map_err(IOError::from_zip)?;
if let Some(file) = archive.file_names().next() {
if let Some(dir) = file.split('/').next() {
let path = path.join(dir);
if path.exists() {
io::remove_dir_all(path).await?;
}
}
}
send_ingress(&ingress, 0.0, Some("extracing java binary")).await?;
archive.extract(&path).map_err(IOError::from_zip)?;
send_ingress(&ingress, 10.0, Some("extracted java binary")).await?;
let mut base_path = path.join(
download
.name
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
#[cfg(target_os = "macos")]
{
base_path = base_path
.join(format!("zulu-{java_version}.jre"))
.join("Contents")
.join("Home")
.join("bin")
.join("java");
}
#[cfg(not(target_os = "macos"))]
{
base_path = base_path.join("bin").join(crate::constants::JAVA_BIN);
}
send_ingress(&ingress, 100.0, Some("installed java binary")).await?;
Ok(base_path)
}
pub async fn check_java(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
Ok(java::check_java_instance(&path).await)
}
pub async fn test_java(path: PathBuf, major: u32) -> crate::Result<bool> {
let Some(jvm) = java::check_java_instance(&path).await else {
return Ok(false);
};
let (maj, _) = get_java_version(&jvm.version)?;
Ok(maj == major)
}

View file

@ -1,98 +0,0 @@
//! Metadata management interface
use crate::State;
pub use interpulse::api::minecraft::VersionManifest;
pub use interpulse::api::modded::Manifest;
// TODO: better error handling
// TODO: fix this lmao
/// Get a [`VersionManifest`] for all available Minecraft versions.
#[tracing::instrument]
pub async fn get_minecraft_versions() -> crate::Result<VersionManifest> {
let state = State::get().await?;
let meta = state
.metadata
.read()
.await
.minecraft
.clone()
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft metadata"))?;
Ok(meta)
}
/// Get a [`Manifest`] for all available Fabric versions.
#[tracing::instrument]
pub async fn get_fabric_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let meta = state
.metadata
.read()
.await
.fabric
.clone()
.ok_or_else(|| anyhow::anyhow!("couldn't get fabric metadata"))?;
Ok(meta)
}
/// Get a [`Manifest`] for all available Quilt versions.
#[tracing::instrument]
pub async fn get_quilt_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let meta = state
.metadata
.read()
.await
.quilt
.clone()
.ok_or_else(|| anyhow::anyhow!("couldn't get quilt metadata"))?;
Ok(meta)
}
/// Get a [`Manifest`] for all available Forge versions.
#[tracing::instrument]
pub async fn get_forge_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let meta = state
.metadata
.read()
.await
.forge
.clone()
.ok_or_else(|| anyhow::anyhow!("couldn't get forge metadata"))?;
Ok(meta)
}
/// Get a [`Manifest`] for all available NeoForge versions.
#[tracing::instrument]
pub async fn get_neoforge_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let meta = state
.metadata
.read()
.await
.neoforge
.clone()
.ok_or_else(|| anyhow::anyhow!("couldn't get neoforge metadata"))?;
Ok(meta)
}
// /// Get a [`Manifest`] for all available Legacy Fabric versions.
// #[tracing::instrument]
// pub async fn get_legacy_fabric_versions() -> crate::Result<Manifest> {
// let state = State::get().await?;
// let meta = state
// .metadata
// .read()
// .await
// .legacy_fabric
// .clone()
// .ok_or_else(|| anyhow!("missing legacyfabric metadata"))?;
// Ok(meta)
// }

View file

@ -1,87 +0,0 @@
//! Authentication flow manager
use crate::store::{MinecraftCredentials, MinecraftLogin, State};
/// Begin a Microsoft authentication flow.
#[tracing::instrument]
pub async fn begin() -> crate::Result<MinecraftLogin> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.begin().await
}
/// Complete a Microsoft authentication flow to recieve [`MinecraftCredentials`].
#[tracing::instrument]
pub async fn finish(code: &str, flow: MinecraftLogin) -> crate::Result<MinecraftCredentials> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.finish(code, flow).await
}
/// Get the current default user if it exists by [`uuid::Uuid`].
#[tracing::instrument]
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.default_user)
}
/// Set the current default user by [`uuid::Uuid`].
#[tracing::instrument]
pub async fn set_default_user(user: Option<uuid::Uuid>) -> crate::Result<()> {
let user = match user {
Some(user) => Some(get_user(user).await?.id),
None => None,
};
let state = State::get().await?;
let mut users = state.users.write().await;
users.default_user = user;
users.save().await?;
Ok(())
}
/// Remove a user account by its [`uuid::Uuid`] from the global store.
#[tracing::instrument]
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.remove(user).await?;
if users.default_user == Some(user) {
set_default_user(Some(user)).await?;
}
Ok(())
}
/// Get a list of user [`MinecraftCredentials`].
#[tracing::instrument]
pub async fn users() -> crate::Result<Vec<MinecraftCredentials>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.users.values().cloned().collect())
}
/// Get a specifc user's [`MinecraftCredentials`] by their [`uuid::Uuid`].
/// Use [`crate::store::MinecraftState#refresh`] instead.
#[tracing::instrument]
pub async fn get_user(user: uuid::Uuid) -> crate::Result<MinecraftCredentials> {
let state = State::get().await?;
let users = state.users.read().await;
let user = users
.users
.get(&user)
.ok_or_else(|| anyhow::anyhow!("failed to get nonexistent user with uuid {user}"))?
.clone();
Ok(user)
}

View file

@ -1,31 +0,0 @@
//! **`OneLauncher` API**
//!
//! API for interacting with our Rust core.
pub mod cluster;
pub mod handler;
pub mod ingress;
pub mod java;
pub mod metadata;
pub mod minecraft;
pub mod package;
pub mod processor;
pub mod proxy;
pub mod settings;
pub mod data {
pub use crate::store::{
ClusterMeta, Credentials, Directories, InitHooks, JavaOptions, Loader, ManagedDependency,
ManagedPackage, ManagedUser, ManagedVersion, Memory, MinecraftCredentials, PackageData,
PackageType, Resolution, Settings,
};
}
pub mod prelude {
pub use crate::cluster::{self, create, Cluster};
pub use crate::data::*;
pub use crate::proxy::InternetPayload;
pub use crate::store::{ClusterPath, JavaVersions, PackageDependency, PackagePath};
pub use crate::utils::java::JavaVersion;
pub use crate::{java, metadata, minecraft, package, processor, settings, State};
}

View file

@ -1,711 +0,0 @@
use std::collections::HashMap;
use bytes::Bytes;
use chrono::{DateTime, Utc};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_repr::{Deserialize_repr, Serialize_repr};
use url::Url;
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
use crate::store::{ManagedVersionFile, ManagedVersionReleaseType, ProviderSearchResults, SearchResult};
use crate::utils::{http, pagination::Pagination};
use crate::{Result, State};
/// Mapping of Curseforge "classes" to their respective category ids
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
#[repr(u32)]
pub enum CFPackageType {
#[default]
Mods = 6,
ModPack = 4471,
DataPack = 6945,
ResourcePack = 12,
ShaderPack = 6552,
}
impl From<CFPackageType> for PackageType {
fn from(package_type: CFPackageType) -> Self {
match package_type {
CFPackageType::Mods => PackageType::Mod,
CFPackageType::ModPack => PackageType::ModPack,
CFPackageType::DataPack => PackageType::DataPack,
CFPackageType::ResourcePack => PackageType::ResourcePack,
CFPackageType::ShaderPack => PackageType::ShaderPack,
}
}
}
impl From<PackageType> for CFPackageType {
fn from(package_type: PackageType) -> Self {
match package_type {
PackageType::Mod => CFPackageType::Mods,
PackageType::ModPack => CFPackageType::ModPack,
PackageType::DataPack => CFPackageType::DataPack,
PackageType::ResourcePack => CFPackageType::ResourcePack,
PackageType::ShaderPack => CFPackageType::ShaderPack,
}
}
}
/// Mapping of Curseforge supported loaders to their respective id
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum CurseforgeLoader {
#[default]
Any = 0,
Forge = 1,
Fabric = 4,
Quilt = 5,
NeoForge = 6,
}
impl From<String> for CurseforgeLoader {
fn from(loader: String) -> Self {
match loader.to_lowercase().as_str() {
"forge" => CurseforgeLoader::Forge,
"fabric" => CurseforgeLoader::Fabric,
"quilt" => CurseforgeLoader::Quilt,
"neoforge" => CurseforgeLoader::NeoForge,
_ => CurseforgeLoader::Any,
}
}
}
impl From<Loader> for CurseforgeLoader {
fn from(loader: Loader) -> Self {
match loader {
Loader::Forge => CurseforgeLoader::Forge,
Loader::Fabric => CurseforgeLoader::Fabric,
Loader::Quilt => CurseforgeLoader::Quilt,
Loader::NeoForge => CurseforgeLoader::NeoForge,
_ => CurseforgeLoader::Any,
}
}
}
impl Into<Loader> for CurseforgeLoader {
fn into(self) -> Loader {
match self {
CurseforgeLoader::Forge => Loader::Forge,
CurseforgeLoader::Fabric => Loader::Fabric,
CurseforgeLoader::Quilt => Loader::Quilt,
CurseforgeLoader::NeoForge => Loader::NeoForge,
_ => Loader::Vanilla,
}
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CurseforgePackage {
pub id: u32,
pub game_id: u32,
#[serde(alias = "classId")]
pub package_type: CFPackageType,
pub name: String,
pub slug: String,
pub links: Links,
pub summary: String,
// pub status: u32,
pub download_count: u64,
#[serde(default)]
pub is_featured: bool,
// pub primary_category_id: u32,
pub categories: Vec<Category>,
pub authors: Vec<Author>,
pub logo: Option<Logo>,
// pub screenshots: Vec<Screenshot>,
// pub main_file_id: u32,
// pub latest_files: Vec<LatestFile>,
// pub latest_files_indexes: Vec<LatestFilesIndex>,
// pub latest_early_access_files_indexes: Vec<LatestEarlyAccessFilesIndex>,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub date_released: DateTime<Utc>,
// #[serde(default)]
// pub allow_mod_distribution: bool,
// pub game_popularity_rank: u32,
// pub is_available: bool,
pub thumbs_up_count: u32,
pub rating: Option<f32>,
}
impl From<CurseforgePackage> for ManagedPackage {
fn from(package: CurseforgePackage) -> Self {
ManagedPackage {
id: package.id.to_string(),
title: package.name,
provider: super::Providers::Curseforge,
package_type: package.package_type.into(),
description: package.summary,
body: crate::store::PackageBody::Url(format!("/v1/mods/{}/description", package.id)),
main: package.slug,
versions: vec![],
game_versions: vec![],
loaders: vec![],
icon_url: package.logo.and_then(|l| Some(l.url)),
created: Some(package.date_created),
updated: Some(package.date_modified),
client: crate::store::PackageSide::Unknown,
server: crate::store::PackageSide::Unknown,
downloads: package.download_count,
followers: package.rating.unwrap_or(0.0) as u32,
categories: vec![], // TODO
optional_categories: None,
license: None,
author: crate::store::Author::Users(
package
.authors
.into_iter()
.map(|a| ManagedUser {
id: a.id.to_string(),
username: a.name,
url: Some(a.url),
is_organization_user: false,
avatar_url: None,
bio: None,
role: None,
})
.collect(),
),
is_archived: false,
}
}
}
impl Into<SearchResult> for CurseforgePackage {
fn into(self) -> SearchResult {
SearchResult {
title: self.name,
slug: self.slug,
project_id: self.id.to_string(),
author: self
.authors
.first()
.and_then(|a| Some(a.name.clone()))
.unwrap_or(String::from("Unknown")),
description: self.summary,
client_side: crate::store::PackageSide::Unknown,
server_side: crate::store::PackageSide::Unknown,
project_type: self.package_type.into(),
downloads: self.download_count,
icon_url: self.logo.map_or(String::new(), |l| l.url),
categories: vec![], // TODO
versions: vec![],
follows: self.thumbs_up_count,
date_created: Some(self.date_created),
date_modified: Some(self.date_modified),
}
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Links {
pub website_url: Option<String>,
pub wiki_url: Option<String>,
pub issues_url: Option<String>,
pub source_url: Option<String>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Category {
pub id: u32,
pub game_id: u32,
pub name: String,
pub slug: String,
pub url: String,
pub icon_url: String,
pub date_modified: String,
// pub parent_category_id: u32,
// pub display_index: u32,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Author {
pub id: u32,
pub name: String,
pub url: String,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Logo {
pub id: u32,
pub mod_id: u32,
pub title: String,
pub description: String,
pub thumbnail_url: String,
pub url: String,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Screenshot {
pub id: u32,
pub mod_id: u32,
pub title: String,
pub description: String,
pub thumbnail_url: String,
pub url: String,
}
#[derive(Default, Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u8)]
pub enum ModFileReleaseType {
#[default]
Release = 1,
Beta = 2,
Alpha = 3,
}
impl Into<ManagedVersionReleaseType> for ModFileReleaseType {
fn into(self) -> ManagedVersionReleaseType {
match self {
ModFileReleaseType::Release => ManagedVersionReleaseType::Release,
ModFileReleaseType::Beta => ManagedVersionReleaseType::Beta,
ModFileReleaseType::Alpha => ManagedVersionReleaseType::Alpha,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModFile {
pub id: u32,
pub game_id: u32,
pub mod_id: u32,
#[serde(default)]
pub is_available: bool,
pub display_name: String,
pub file_name: String,
pub release_type: ModFileReleaseType,
// pub file_status: u32,
pub hashes: Vec<Hash>,
pub file_date: DateTime<Utc>,
pub file_length: u64,
pub download_count: u32,
pub file_size_on_disk: Option<u64>,
pub download_url: Option<String>,
pub game_versions: Vec<String>,
pub sortable_game_versions: Vec<SortableGameVersion>,
pub dependencies: Vec<Dependency>,
// pub expose_as_alternative: bool,
// pub parent_project_file_id: u32,
// pub alternate_file_id: u32,
// pub is_server_pack: bool,
// pub server_pack_file_id: u32,
// pub is_early_access_content: bool,
// pub early_access_end_date: String,
pub file_fingerprint: u32,
// pub modules: Vec<Module>,
}
impl Into<ManagedVersion> for ModFile {
fn into(self) -> ManagedVersion {
let mut hashes = HashMap::new();
for hash in self.hashes {
hashes.insert(hash.algo.name(), hash.value);
}
let mut game_versions = Vec::new();
let mut loader = CurseforgeLoader::Any;
for version in self.sortable_game_versions {
if version.game_version.is_empty() {
// This is most likely a loader
loader = CurseforgeLoader::from(version.game_version_name);
continue;
}
game_versions.push(version.game_version);
}
let mut files = Vec::new();
if let Some(download_url) = self.download_url {
files.push(ManagedVersionFile {
url: download_url,
file_name: self.file_name,
primary: true,
size: self.file_size_on_disk.unwrap_or(self.file_length),
file_type: Some(crate::store::PackageFile::RequiredPack),
hashes,
});
}
ManagedVersion {
package_id: self.mod_id.to_string(),
id: self.id.to_string(),
name: self.display_name.clone(),
author: String::new(),
loaders: vec![Into::<Loader>::into(loader)],
changelog: String::new(),
changelog_url: None,
deps: vec![],
downloads: self.download_count,
featured: false,
is_available: self.is_available && files.len() > 0,
files,
game_versions,
published: Some(self.file_date),
version_display: self.display_name,
version_type: self.release_type.into(),
}
}
}
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum HashAlgorithm {
Sha1 = 1,
MD5 = 2,
}
impl HashAlgorithm {
pub fn name(&self) -> String {
match self {
Self::Sha1 => "sha1",
Self::MD5 => "md5",
}.to_string()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hash {
pub value: String,
pub algo: HashAlgorithm,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SortableGameVersion {
pub game_version_name: String,
pub game_version_padded: String,
pub game_version: String,
pub game_version_release_date: String,
pub game_version_type_id: u32,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Dependency {
pub mod_id: u32,
pub relation_type: u32,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Module {
pub name: String,
pub fingerprint: u32,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModFilesIndex {
pub game_version: String,
pub file_id: u32,
pub filename: String,
pub release_type: u32,
pub game_version_type_id: u32,
#[serde(default)]
pub mod_loader: CurseforgeLoader,
}
// https://docs.curseforge.com/rest-api/?shell#tocS_ModsSearchSortField
#[allow(dead_code)]
enum SearchSort {
Featured = 1,
Popularity = 2,
LastUpdated = 3,
Name = 4,
Author = 5,
TotalDownloads = 6,
Rating = 12,
}
#[derive(Default, Debug, Serialize, Deserialize)]
struct CFData<T> {
data: T,
}
#[derive(Default, Debug, Serialize, Deserialize)]
struct CFPaginatedData<T> {
pagination: Pagination,
data: T,
}
pub async fn fetch_advanced<F: FnOnce(&mut Url)>(
method: Method,
url: &str,
url_builder: Option<F>,
headers: Option<HashMap<&str, &str>>,
json_body: Option<serde_json::Value>,
) -> Result<Bytes> {
// TODO: Get the API key from settings, fallback to constant, and error if missing
let key = crate::constants::CURSEFORGE_API_KEY;
let mut headers = headers.unwrap_or(HashMap::<&str, &str>::new());
headers.insert("x-api-key", key);
let url =
&mut Url::parse(format!("{}{}", crate::constants::CURSEFORGE_API_URL, url).as_str())?;
if let Some(url_builder) = url_builder {
url_builder(url);
}
http::fetch_advanced(
method,
url.as_str(),
None,
json_body,
Some(headers),
None,
&State::get().await?.fetch_semaphore,
)
.await
}
pub async fn fetch_url_builder<F: FnOnce(&mut Url)>(
url: &str,
url_builder: Option<F>,
) -> Result<Bytes> {
fetch_advanced(Method::GET, url, url_builder, None, None).await
}
async fn fetch(url: &str) -> Result<Bytes> {
fetch_url_builder(url, None::<fn(&mut Url)>).await
}
#[tracing::instrument(skip_all)]
pub async fn search(
query: Option<String>,
limit: Option<u8>,
offset: Option<u32>,
game_versions: Option<Vec<String>>,
_categories: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
package_types: Option<Vec<PackageType>>,
) -> Result<ProviderSearchResults> {
type CFSearchResults = CFPaginatedData<Vec<CurseforgePackage>>;
let results: CFSearchResults = serde_json::from_slice(
&fetch_url_builder(
"/v1/mods/search",
Some(Box::new(move |url: &mut Url| {
let params = &mut url.query_pairs_mut();
params.append_pair(
"gameId",
crate::constants::CURSEFORGE_GAME_ID.to_string().as_str(),
);
params.append_pair("pageSize", limit.unwrap_or(10).to_string().as_str());
params.append_pair("index", offset.unwrap_or(0).to_string().as_str());
params.append_pair(
"sortField",
(SearchSort::Popularity as u8).to_string().as_str(),
);
params.append_pair("sortOrder", "desc");
if let Some(query) = query {
params.append_pair("searchFilter", query.as_str());
}
if let Some(game_versions) = game_versions {
// Need to create a list e.g. ["1.16.5", "1.17.1"]
params.append_pair(
"gameVersions",
format!(
"[{}]",
game_versions
.into_iter()
.map(|v| format!("\"{}\"", v))
.collect::<Vec<String>>()
.join(",")
)
.as_str(),
);
}
if let Some(loaders) = loaders {
params.append_pair(
"modLoaderTypes",
format!(
"[{}]",
loaders
.into_iter()
.map(|l| (CurseforgeLoader::from(l) as u32).to_string())
.collect::<Vec<String>>()
.join(",")
)
.as_str(),
);
}
if let Some(package_types) = package_types {
if package_types.len() == 0 {
return;
}
if package_types.len() > 1 {
tracing::warn!(
"curseforge provider does not support multiple package types"
);
}
let package_type = package_types.first().unwrap();
params.append_pair(
"classId",
(CFPackageType::from(*package_type) as u32)
.to_string()
.as_str(),
);
}
})),
)
.await?,
)?;
Ok(ProviderSearchResults {
total: results.pagination.total_count,
results: results
.data
.into_iter()
.map(Into::into)
.collect::<Vec<SearchResult>>(),
provider: crate::package::content::Providers::Curseforge,
})
}
pub async fn get(id: u32) -> Result<CurseforgePackage> {
Ok(
serde_json::from_slice::<CFData<_>>(&fetch(format!("/v1/mods/{}", id).as_str()).await?)?
.data,
)
}
pub async fn get_multiple(ids: &[u32]) -> Result<Vec<CurseforgePackage>> {
let mut map = serde_json::Map::new();
map.insert("modIds".to_string(), ids.to_vec().into());
let json_body = serde_json::Value::Object(map);
Ok(
serde_json::from_slice::<CFData<_>>(
&fetch_advanced(
Method::POST,
"/v1/mods",
None::<fn(&mut Url)>,
None,
Some(json_body),
).await?)?
.data,
)
}
pub async fn get_package_body(url: String) -> Result<String> {
Ok(serde_json::from_slice::<CFData<_>>(&fetch(&url).await?)?.data)
}
pub async fn get_all_versions(
project_id: &str,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
page: Option<u32>,
page_size: Option<u16>,
) -> Result<(Vec<ModFile>, Pagination)> {
let data: CFPaginatedData<Vec<ModFile>> = serde_json::from_slice::<CFPaginatedData<_>>(
&fetch_url_builder(
format!("/v1/mods/{project_id}/files").as_str(),
Some(Box::new(move |url: &mut Url| {
if let Some(game_versions) = game_versions {
if let Some(version) = game_versions.first() {
url.query_pairs_mut().append_pair("gameVersion", version);
}
}
if let Some(loader) = loaders {
if let Some(loader) = loader.first() {
url.query_pairs_mut().append_pair(
"modLoaderType",
(CurseforgeLoader::from(*loader) as u8).to_string().as_str(),
);
}
}
let page = page.unwrap_or(0);
url.query_pairs_mut().append_pair("index", page.to_string().as_str());
let page_size = page_size.unwrap_or(10);
url.query_pairs_mut().append_pair("pageSize", page_size.to_string().as_str());
})),
)
.await?,
)?;
Ok((data.data, data.pagination))
}
pub async fn get_versions(
versions: Vec<String>,
) -> Result<Vec<ModFile>> {
let json_body = json!({
"fileIds": versions.iter().map(|v| v.parse::<u32>().unwrap()).collect::<Vec<u32>>()
});
Ok(serde_json::from_slice::<CFData<_>>(
&fetch_advanced(
Method::POST,
format!("/v1/mods/files").as_str(),
None::<fn(&mut Url)>,
None,
Some(json_body)
)
.await?,
)?
.data)
}
pub async fn get_versions_by_hashes(
hashes: Vec<String>,
) -> Result<HashMap<String, ModFile>> {
let body = json!({
"fingerprints": hashes.into_iter().map(|hash| hash.parse::<u32>().unwrap_or_default()).collect::<Vec<u32>>()
});
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
exact_matches: Vec<FingerprintMatch>,
exact_fingerprints: Vec<u32>
}
#[derive(Debug, Serialize, Deserialize)]
struct FingerprintMatch {
id: u32,
file: ModFile
}
let files = serde_json::from_slice::<CFData<Response>>(
&fetch_advanced(
Method::POST,
format!("/v1/fingerprints/{}", crate::constants::CURSEFORGE_GAME_ID).as_str(),
None::<fn(&mut Url)>,
None,
Some(body),
)
.await?,
)?;
Ok(files.data.exact_matches.into_iter().map(|m| (m.file.file_fingerprint.to_string(), m.file)).collect())
}

View file

@ -1,337 +0,0 @@
//! **`OneLauncher` Content Package**
//!
//! Utilities for searching and downloading content packages to `OneLauncher`.
use std::collections::HashMap;
use std::path::PathBuf;
use modrinth::{Facet, FacetOperation};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
use crate::package::content::modrinth::FacetBuilder;
use crate::store::{Author, PackageBody, ProviderSearchResults};
use crate::utils::crypto;
use crate::utils::http::fetch_json;
use crate::utils::pagination::Pagination;
use crate::{Result, State};
mod curseforge;
mod modrinth;
mod skyclient;
/// Providers for content packages
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
pub enum Providers {
Modrinth,
Curseforge,
SkyClient,
}
impl std::fmt::Display for Providers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl Providers {
/// Get the name of the provider
#[must_use]
pub const fn name(&self) -> &str {
match self {
Self::Modrinth => "Modrinth",
Self::Curseforge => "Curseforge",
Self::SkyClient => "SkyClient",
}
}
/// Get the URL of the provider
#[must_use]
pub const fn url(&self) -> &str {
match self {
Self::Modrinth => "https://modrinth.com",
Self::Curseforge => "https://curseforge.com",
Self::SkyClient => "https://skyclient.co",
}
}
#[must_use]
pub const fn get_providers() -> &'static [Providers] {
&[Self::Modrinth, Self::Curseforge, Self::SkyClient]
}
#[allow(clippy::too_many_arguments)]
pub async fn search(
&self,
query: Option<String>,
limit: Option<u8>,
offset: Option<u32>,
game_versions: Option<Vec<String>>,
categories: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
package_types: Option<Vec<PackageType>>,
open_source: Option<bool>,
) -> Result<ProviderSearchResults> {
match self {
Self::Modrinth => {
modrinth::search(
query,
limit,
offset,
Some(|mut builder| {
build_facets(
&mut builder,
categories,
game_versions,
loaders,
package_types,
open_source,
)
}),
)
.await
}
Self::Curseforge => {
curseforge::search(
query,
limit,
offset,
game_versions,
categories,
loaders,
package_types,
)
.await
}
Self::SkyClient => {
skyclient::search(
query,
limit,
offset,
game_versions,
loaders
).await
},
}
}
pub async fn get(&self, slug_or_id: &str) -> Result<ManagedPackage> {
Ok(match self {
Self::Modrinth => modrinth::get(slug_or_id).await?.into(),
Self::Curseforge => curseforge::get(slug_or_id.parse::<u32>().map_err(|err| anyhow::anyhow!(err))?).await?.into(),
Self::SkyClient => skyclient::get(slug_or_id).await?.into(),
})
}
pub async fn get_multiple(&self, slug_or_ids: &[String]) -> Result<Vec<ManagedPackage>> {
if slug_or_ids.is_empty() {
return Ok(vec![]);
}
Ok(match self {
Self::Modrinth => modrinth::get_multiple(slug_or_ids)
.await?
.into_iter()
.map(Into::into)
.collect(),
Self::Curseforge => {
let parsed_ids = slug_or_ids
.iter()
.filter_map(|id| id.parse::<u32>().ok())
.collect::<Vec<u32>>();
if parsed_ids.is_empty() {
return Ok(vec![]);
}
let parsed_ids = parsed_ids.as_slice();
curseforge::get_multiple(parsed_ids)
.await?
.into_iter()
.map(Into::into)
.collect()
},
Self::SkyClient => skyclient::get_multiple(slug_or_ids)
.await?
.into_iter()
.map(Into::into)
.collect(),
})
}
pub async fn get_all_versions(
&self,
project_id: &str,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
page: Option<u32>,
page_size: Option<u16>,
) -> Result<(Vec<ManagedVersion>, Pagination)> {
Ok(match self {
Self::Modrinth => {
let data = modrinth::get_all_versions(project_id, game_versions, loaders, page, page_size).await?;
(data.0.into_iter().map(Into::into).collect(), data.1)
}
Self::Curseforge => {
let data = curseforge::get_all_versions(project_id, game_versions, loaders, page, page_size).await?;
(data.0.into_iter().map(Into::into).collect(), data.1)
}
Self::SkyClient => {
let data = skyclient::get_all_versions(project_id, game_versions, loaders, page, page_size).await?;
(data.0, data.1)
},
})
}
pub async fn get_versions(&self, versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
Ok(match self {
Self::Modrinth => modrinth::get_versions(versions)
.await?
.into_iter()
.map(Into::into)
.collect(),
Self::Curseforge => curseforge::get_versions(versions)
.await?
.into_iter()
.map(Into::into)
.collect(),
Self::SkyClient => skyclient::get_versions(versions).await?,
})
}
pub async fn get_authors(&self, author: &Author) -> Result<Vec<ManagedUser>> {
Ok(match author {
Author::Team { .. } => match self {
Self::Modrinth => modrinth::get_authors(author).await?,
_ => return Err(anyhow::anyhow!("{} does not support teams", self).into()),
},
Author::Users(users) => users.to_owned(),
})
}
pub async fn get_package_body(&self, body: &PackageBody) -> Result<String> {
Ok(match body {
PackageBody::Url(url) => match self {
Self::Curseforge => curseforge::get_package_body(url.to_owned()).await?,
_ => return Err(anyhow::anyhow!("{} does not support direct URLs", self).into()),
},
PackageBody::Markdown(markdown) => markdown.to_owned(),
})
}
pub async fn get_versions_by_hashes(&self, hashes: Vec<String>) -> Result<HashMap<String, ManagedVersion>> {
Ok(match self {
Self::Modrinth => modrinth::get_versions_by_hashes(hashes)
.await?
.into_iter()
.map(|(hash, version)| (hash, version.into()))
.collect(),
Self::Curseforge => curseforge::get_versions_by_hashes(hashes)
.await?
.into_iter()
.map(|(hash, version)| (hash, version.into()))
.collect(),
Self::SkyClient => skyclient::get_versions_by_hashes(hashes).await?,
})
}
pub fn hash_file(&self, path: &PathBuf) -> Result<String> {
match self {
Providers::Modrinth => crypto::sha1_file(path),
Providers::SkyClient => crypto::md5_file(path),
Providers::Curseforge => {
let hash = crypto::murmur2_file(path)?;
Ok(hash.to_string())
},
}
}
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, Serialize, Deserialize)]
pub struct FeaturedPackage {
pub package_type: PackageType,
pub provider: Providers,
pub id: String,
pub title: String,
pub description: String,
pub thumbnail: String,
pub oneconfig: bool,
}
pub async fn get_featured_packages() -> Result<Vec<FeaturedPackage>> {
let state = State::get().await?;
fetch_json(
Method::GET,
crate::constants::FEATURED_PACKAGES_URL,
None,
None,
&state.fetch_semaphore
).await
}
fn build_facets(
builder: &mut FacetBuilder,
categories: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
package_types: Option<Vec<PackageType>>,
open_source: Option<bool>,
) -> String {
if let Some(categories) = categories {
for category in categories {
builder.and(Facet(
"categories".to_string(),
FacetOperation::Eq,
category,
));
}
}
if let Some(game_versions) = game_versions {
for version in game_versions {
builder.and(Facet("versions".to_string(), FacetOperation::Eq, version));
}
}
if let Some(package_types) = package_types {
if package_types.contains(&PackageType::Mod)
|| package_types.contains(&PackageType::ModPack)
{
if let Some(loaders) = loaders {
for loader in loaders {
builder.and(Facet(
"categories".to_string(),
FacetOperation::Eq,
loader.to_string(),
));
}
}
}
for package_type in package_types {
builder.and(Facet(
"project_types".to_string(),
FacetOperation::Eq,
package_type.get_name().to_string(),
));
}
}
if let Some(open_source) = open_source {
builder.and(Facet(
"open_source".to_string(),
FacetOperation::Eq,
open_source.to_string(),
));
}
// builder.and(Facet("client_side".to_string(), FacetOperation::Eq, "")) // TODO: Possibly make this client_side = required
builder.build()
}

View file

@ -1,535 +0,0 @@
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
use crate::store::{
Author, License, ManagedVersionFile, ManagedVersionReleaseType, PackageFile, PackageSide, ProviderSearchResults, SearchResult
};
use crate::utils::http::{fetch, fetch_advanced};
use crate::utils::pagination::Pagination;
use crate::{Result, State};
use chrono::{DateTime, Utc};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::Providers;
#[serde_with::serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModrinthPackage {
pub slug: String,
pub title: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub categories: Vec<String>,
pub client_side: PackageSide,
pub server_side: PackageSide,
#[serde(default)]
pub body: String,
#[serde(default)]
#[serde(alias = "display_categories")]
pub additional_categories: Vec<String>,
// pub issues_url: String,
// pub source_url: String,
// pub wiki_url: String,
// pub discord_url: String,
// pub donation_urls: Vec<DonationUrl>,
pub project_type: PackageType,
pub downloads: u32,
pub icon_url: Option<String>,
#[serde(alias = "project_id")]
pub id: String,
pub team: String,
#[serde(default)]
pub organization: Option<String>,
#[serde(alias = "date_created")]
pub published: DateTime<Utc>,
#[serde(alias = "date_modified")]
pub updated: DateTime<Utc>,
pub followers: u32,
#[serde(default)]
pub versions: Vec<String>,
pub game_versions: Vec<String>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub loaders: Vec<Loader>,
#[serde(default)]
pub license: Option<License>,
// #[serde(default)]
// pub gallery: Vec<Gallery>,
#[serde(default)]
pub status: String,
}
impl From<ModrinthPackage> for ManagedPackage {
fn from(value: ModrinthPackage) -> Self {
Self {
provider: Providers::Modrinth,
id: value.id,
title: value.title,
description: value.description,
body: crate::store::PackageBody::Markdown(value.body),
main: value.slug,
versions: value.versions,
game_versions: value.game_versions,
loaders: value.loaders,
icon_url: value.icon_url,
created: Some(value.published),
updated: Some(value.updated),
client: value.client_side,
server: value.server_side,
downloads: value.downloads as u64,
followers: value.followers,
categories: value.categories,
optional_categories: Some(value.additional_categories),
package_type: PackageType::Mod,
license: value.license,
author: Author::Team {
team: value.team,
organization: value.organization,
},
is_archived: value.status == "archived",
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DonationUrl {
pub id: String,
pub platform: String,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Gallery {
pub url: String,
pub featured: bool,
#[serde(default)]
pub title: String,
#[serde(default)]
pub description: String,
pub created: DateTime<Utc>,
#[serde(default)]
pub ordering: i64,
}
#[derive(Deserialize)]
struct SearchResults {
hits: Vec<SearchResult>,
total_hits: u32,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModrinthVersion {
pub game_versions: Vec<String>,
pub loaders: Vec<String>,
pub id: String,
pub project_id: String,
pub author_id: String,
pub featured: bool,
pub name: String,
pub version_number: String,
pub changelog: String,
pub changelog_url: Option<String>,
pub date_published: DateTime<Utc>,
pub downloads: u32,
pub version_type: String,
pub status: String,
pub requested_status: Value,
pub files: Vec<File>,
pub dependencies: Vec<Value>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct File {
pub url: String,
pub filename: String,
pub primary: bool,
pub size: u64,
pub file_type: Option<PackageFile>,
pub hashes: HashMap<String, String>
}
impl From<File> for ManagedVersionFile {
fn from(value: File) -> Self {
Self {
url: value.url,
file_name: value.filename,
primary: value.primary,
size: value.size,
file_type: value.file_type,
hashes: value.hashes
}
}
}
impl From<ModrinthVersion> for ManagedVersion {
fn from(value: ModrinthVersion) -> Self {
Self {
id: value.id,
package_id: value.project_id,
author: value.author_id,
name: value.name,
featured: value.featured,
version_display: value.version_number,
changelog: value.changelog,
changelog_url: value.changelog_url,
published: Some(value.date_published),
downloads: value.downloads,
version_type: ManagedVersionReleaseType::from(value.version_type),
files: value.files.into_iter().map(Into::into).collect(),
is_available: true,
deps: vec![], // TODO [`ManagedDependency`]?
game_versions: value.game_versions,
loaders: value
.loaders
.into_iter()
.filter_map(|loader| Loader::try_from(loader).ok())
.collect(),
}
}
}
#[allow(dead_code)]
pub enum FacetOperation {
NotEq,
LargeEq,
Large,
SmallEq,
Small,
Eq,
}
impl Display for FacetOperation {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::NotEq => "!=",
Self::LargeEq => ">=",
Self::Large => ">",
Self::SmallEq => "<=",
Self::Small => "<",
Self::Eq => "=",
})
}
}
pub struct Facet(pub String, pub FacetOperation, pub String);
impl Display for Facet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(format!("{}{}{}", self.0, self.1, self.2).as_str())
}
}
pub struct FacetBuilder {
pub facets: Vec<Vec<Facet>>,
}
impl FacetBuilder {
pub const fn builder() -> Self {
Self { facets: vec![] }
}
pub fn and(&mut self, facet: Facet) -> &Self {
self.facets.push(vec![facet]);
self
}
#[allow(dead_code)]
pub fn or(&mut self, facet: Facet) -> &Self {
let mut last_facet = self.facets.pop().unwrap_or_default();
last_facet.push(facet);
self.facets.push(last_facet);
self
}
pub fn build(&self) -> String {
let mut builder: Vec<String> = vec![];
for facet in &self.facets {
let mut stringified = String::new();
stringified.push('[');
for (i, f) in facet.iter().enumerate() {
if i != 0 {
stringified.push(',');
}
stringified.push_str(format!("\"{f}\"").as_str());
}
stringified.push(']');
builder.push(stringified);
}
builder.join(",")
}
}
macro_rules! format_url {
($($arg:tt)*) => {{
format!("{}{}", crate::constants::MODRINTH_API_URL, format!($($arg)*))
}};
}
macro_rules! format_url_v3 {
($($arg:tt)*) => {{
format!("{}{}", crate::constants::MODRINTH_V3_API_URL, format!($($arg)*))
}};
}
pub async fn get(id: &str) -> Result<ModrinthPackage> {
Ok(serde_json::from_slice(
&fetch(
format_url!("/project/{}", id).as_str(),
None,
&State::get().await?.fetch_semaphore,
)
.await?,
)?)
}
pub async fn get_multiple(ids: &[String]) -> Result<Vec<ModrinthPackage>> {
Ok(serde_json::from_slice(
&fetch(
format_url!(
"/projects?ids=[{}]",
ids.iter()
.map(|id| format!("\"{id}\""))
.collect::<Vec<String>>()
.join(",")
)
.as_str(),
None,
&State::get().await?.fetch_semaphore,
)
.await?,
)?)
}
pub async fn search<F>(
query: Option<String>,
limit: Option<u8>,
offset: Option<u32>,
facets: Option<F>,
) -> Result<ProviderSearchResults>
where
F: FnOnce(FacetBuilder) -> String,
{
let facets = facets.map_or(String::new(), |func| func(FacetBuilder::builder()));
let results: SearchResults = serde_json::from_slice(
&fetch(
format_url!(
"/search?query={}&limit={}&offset={}{}",
query.unwrap_or_default(),
limit.unwrap_or(10),
offset.unwrap_or(0),
if facets.is_empty() {
String::new()
} else {
format!("&facets=[{facets}]")
}
)
.as_str(),
None,
&State::get().await?.fetch_semaphore,
)
.await?,
)?;
Ok(ProviderSearchResults {
provider: Providers::Modrinth,
results: results.hits,
total: results.total_hits,
})
}
#[derive(Deserialize)]
struct TeamMember {
#[serde(default)]
pub role: Option<String>,
pub user: ManagedUser,
}
/// Get the authors of a project
pub async fn get_authors(author: &Author) -> Result<Vec<ManagedUser>> {
match author {
Author::Users(users) => Ok(users.clone()),
Author::Team { team, organization } => {
if let Some(organization) = organization {
#[derive(Deserialize)]
struct Organization {
pub id: String,
pub name: String,
pub icon_url: String,
pub description: String,
pub members: Vec<TeamMember>,
}
let organization: Organization = serde_json::from_slice(
&fetch(
format_url_v3!("/organization/{}", organization).as_str(),
None,
&State::get().await?.fetch_semaphore,
)
.await?,
)?;
let mut org_user = ManagedUser {
id: organization.id,
username: organization.name,
avatar_url: Some(organization.icon_url),
bio: Some(organization.description),
url: None,
is_organization_user: true,
role: None,
};
org_user.url = Some(format!("https://modrinth.com/organization/{}", org_user.id));
let members = get_team(team.clone()).await;
let members = match members {
Ok(members) => members,
Err(err) => {
tracing::error!("Failed to get team members: {}", err);
organization
.members
.into_iter()
.map(|member| member.user)
.collect::<Vec<ManagedUser>>()
}
};
let mut authors = vec![org_user];
authors.extend(members);
Ok(authors)
} else {
get_team(team.clone()).await
}
}
}
}
async fn get_team(team_id: String) -> Result<Vec<ManagedUser>> {
let authors: Vec<TeamMember> = serde_json::from_slice(
&fetch(
format_url!("/team/{}/members", team_id).as_str(),
None,
&State::get().await?.fetch_semaphore,
)
.await?,
)?;
Ok(authors
.into_iter()
.map(|member| {
let mut user = member.user;
user.url = Some(format!("https://modrinth.com/user/{}", user.id));
user.role = member.role;
user
})
.collect())
}
// TODO: modrinth api v3
// pub async fn get_org_projects(organization: &str) -> Result<>
pub async fn get_all_versions(
project_id: &str,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
page: Option<u32>,
page_size: Option<u16>,
) -> Result<(Vec<ModrinthVersion>, Pagination)> {
let mut url = url::Url::parse(format_url!("/project/{}/version", project_id).as_str())?;
if let Some(game_versions) = game_versions {
url.query_pairs_mut().append_pair(
"game_versions",
&game_versions
.iter()
.map(|v| format!("\"{v}\""))
.collect::<Vec<String>>()
.join(","),
);
}
if let Some(loaders) = loaders {
url.query_pairs_mut().append_pair(
"loaders",
format!(
"[{}]",
&loaders
.iter()
.map(|l| format!("\"{l}\""))
.collect::<Vec<String>>()
.join(",")
)
.as_str(),
);
}
let data = serde_json::from_slice::<Vec<ModrinthVersion>>(
&fetch(
format_url!("/project/{}/version", project_id).as_str(),
None,
&State::get().await?.fetch_semaphore,
)
.await?,
)?;
let data_len = data.len() as u32;
Ok((
data,
Pagination {
index: page.unwrap_or(1),
page_size: page_size.unwrap_or(10),
result_count: data_len,
total_count: data_len,
},
))
}
pub async fn get_versions(versions: Vec<String>) -> Result<Vec<ModrinthVersion>> {
Ok(serde_json::from_slice(
&fetch(
format_url!(
"/versions?ids=[{}]",
versions
.iter()
.map(|v| format!("\"{v}\""))
.collect::<Vec<String>>()
.join(",")
)
.as_str(),
None,
&State::get().await?.fetch_semaphore,
)
.await?,
)?)
}
pub async fn get_versions_by_hashes(hashes: Vec<String>) -> Result<HashMap<String, ModrinthVersion>> {
let body = serde_json::json!({
"hashes": hashes,
"algorithm": "sha1"
});
Ok(serde_json::from_slice(
&fetch_advanced(
Method::POST,
format_url!("/version_files").as_str(),
None,
Some(body),
None,
None,
&State::get().await?.fetch_semaphore,
)
.await?
)?)
}

View file

@ -1,363 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use chrono::{DateTime, Utc};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::sync::{OnceCell, RwLock};
use crate::{data::{Loader, ManagedPackage, ManagedUser, ManagedVersion}, store::{ManagedVersionFile, ProviderSearchResults, SearchResult}, utils::{http, pagination::Pagination}, Result, State};
async fn fetch<T: DeserializeOwned>(url: &str) -> Result<T> {
let state = State::get().await?;
Ok(serde_json::from_slice(
&http::fetch(
format!("{}/{}", crate::constants::SKYCLIENT_BASE_URL, url).as_str(),
None,
&state.fetch_semaphore
).await?
)?)
}
static SKYCLIENTSTORE_STATIC: OnceCell<RwLock<SkyClientStore>> = OnceCell::const_new();
struct SkyClientStore {
pub mods: Option<Vec<SkyClientMod>>
}
impl SkyClientStore {
pub async fn get() -> Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
Ok(Arc::new(
SKYCLIENTSTORE_STATIC
.get_or_try_init(Self::initialize)
.await?
.read()
.await,
))
}
#[tracing::instrument]
#[onelauncher_macros::memory]
async fn initialize() -> Result<RwLock<Self>> {
let mods: Vec<SkyClientMod> = fetch("/mods/mods.json").await?;
Ok(RwLock::new(SkyClientStore {
mods: Some(mods)
}))
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct SkyClientMod {
pub id: String,
#[serde(default)]
pub nicknames: Vec<String>,
pub display: String,
pub creator: String,
pub discord_code: Option<String>,
pub description: Option<String>,
pub icon: Option<String>,
#[serde(default)]
pub links: Vec<SkyClientModLink>,
pub versions: Vec<SkyClientModVersion>,
#[serde(default)]
pub commands: Vec<String>,
pub icon_scaling: Option<String>,
pub oneconfig: Option<bool>,
}
impl Into<SearchResult> for SkyClientMod {
fn into(self) -> SearchResult {
SearchResult {
slug: self.id.clone(),
project_id: self.id,
author: self.creator,
categories: vec![],
title: self.display,
client_side: crate::store::PackageSide::Required,
server_side: crate::store::PackageSide::Unknown,
date_created: None,
date_modified: None,
description: self.description.unwrap_or_default(),
project_type: crate::data::PackageType::Mod,
downloads: 0,
icon_url: self.icon.map(|i| format!("{}/icons/{}", crate::constants::SKYCLIENT_BASE_URL, i)).unwrap_or_default(),
versions: self.versions.iter().map(|v| v.version.clone()).collect(),
follows: 0,
}
}
}
impl Into<ManagedPackage> for SkyClientMod {
fn into(self) -> ManagedPackage {
ManagedPackage {
id: self.id.clone(),
main: self.id,
title: self.display,
body: crate::store::PackageBody::Markdown(self.description.clone().unwrap_or_default()),
categories: vec![],
client: crate::store::PackageSide::Required,
server: crate::store::PackageSide::Unknown,
created: None, // TODO: Get the oldest date from versions
updated: None, // TODO: Get the newest date from versions
description: self.description.unwrap_or_default(),
downloads: 0,
followers: 0,
game_versions: self.versions.iter().flat_map(|v| v.game_versions.clone()).collect(),
icon_url: self.icon.map(|i| format!("{}/icons/{}", crate::constants::SKYCLIENT_BASE_URL, i)),
is_archived: false,
license: None,
loaders: self.versions.iter().flat_map(|v| v.loaders.clone().into_iter().map(Loader::from_string)).collect(),
versions: self.versions.iter().map(|v| v.version.clone()).collect(),
optional_categories: None,
author: crate::store::Author::Users(vec![
ManagedUser {
id: self.creator.clone(),
username: self.creator,
is_organization_user: false,
avatar_url: None,
bio: None,
role: None,
url: self.discord_code.map(|code| format!("https://discord.gg/{}", code)),
}
]),
package_type: crate::data::PackageType::Mod,
provider: super::Providers::SkyClient,
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct SkyClientModLink {
pub icon: String,
pub text: String,
pub link: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct SkyClientModVersion {
pub version: String,
pub game_versions: Vec<String>,
pub loaders: Vec<String>,
pub file: String,
pub url: String,
pub hash: String,
pub date_published: DateTime<Utc>,
pub mod_id: Option<String>,
}
fn get_managed_version(m: &SkyClientMod, v: &SkyClientModVersion) -> ManagedVersion {
let mut map = HashMap::new();
map.insert(String::from("md5"), v.hash.clone());
ManagedVersion {
id: v.version.clone(),
package_id: m.id.clone(),
author: m.creator.clone(),
changelog_url: None,
changelog: String::new(),
deps: vec![],
downloads: 0,
featured: false,
files: vec![ManagedVersionFile {
file_name: v.file.clone(),
url: v.url.clone(),
file_type: None,
hashes: map,
primary: true,
size: 0,
}],
game_versions: v.game_versions.clone(),
is_available: true,
loaders: v.loaders.iter().map(|l| Loader::from_string(l.to_owned())).collect(),
name: format!("{} v{}", m.display, v.version),
published: Some(v.date_published),
version_display: v.version.clone(),
version_type: crate::store::ManagedVersionReleaseType::Release,
}
}
pub async fn get(id: &str) -> Result<SkyClientMod> {
let store = SkyClientStore::get().await?;
let Some(mods) = &store.mods else {
return Err(anyhow::anyhow!("mod not found").into())
};
for m in mods {
if m.id == id {
return Ok(m.clone());
}
}
Err(anyhow::anyhow!("mod not found").into())
}
pub async fn get_multiple(slug_or_ids: &[String]) -> Result<Vec<SkyClientMod>> {
let store = SkyClientStore::get().await?;
let mods = match &store.mods {
Some(mods) => mods,
None => return Ok(vec![])
};
let mut results = vec![];
for id in slug_or_ids {
for m in mods {
if m.id == *id {
results.push(m.clone());
}
}
}
Ok(results)
}
pub async fn get_all_versions(
project_id: &str,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
page: Option<u32>,
page_size: Option<u16>,
) -> Result<(Vec<ManagedVersion>, Pagination)> {
let mut pagination = Pagination::default();
let mut versions = vec![];
if let Ok(m) = &get(project_id).await {
for v in &m.versions {
let mut can_add = true;
if let Some(game_versions) = &game_versions {
if !game_versions.is_empty() {
can_add = v.game_versions.iter().any(|gv| game_versions.contains(gv))
}
}
if can_add {
if let Some(loaders) = &loaders {
if !loaders.is_empty() {
can_add = v.loaders.iter().any(|l| loaders.contains(&Loader::from_string(l.to_owned())))
}
}
}
if can_add {
if page_size.map(|ps| versions.len() < ps as usize).unwrap_or(true) && page.map(|p| p <= pagination.index).unwrap_or(true) {
versions.push(get_managed_version(m, v));
}
pagination.total_count += 1;
}
}
}
Ok((versions, pagination))
}
pub async fn get_versions(versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
let store = SkyClientStore::get().await?;
let mods = match &store.mods {
Some(mods) => mods,
None => return Ok(vec![])
};
let mut results = vec![];
for m in mods {
for v in &m.versions {
if versions.contains(&v.version) {
results.push(get_managed_version(m, v));
}
}
}
Ok(results)
}
pub async fn get_versions_by_hashes(hashes: Vec<String>) -> Result<HashMap<String, ManagedVersion>> {
let store = SkyClientStore::get().await?;
let mods = match &store.mods {
Some(mods) => mods,
None => return Ok(HashMap::new())
};
let mut results = HashMap::new();
for m in mods {
for v in &m.versions {
if hashes.contains(&v.hash) {
results.insert(v.hash.clone(), get_managed_version(m, v));
}
}
}
Ok(results)
}
pub async fn search(
query: Option<String>,
limit: Option<u8>,
offset: Option<u32>,
game_versions: Option<Vec<String>>,
// package_types: Option<Vec<PackageType>>,
loaders: Option<Vec<Loader>>,
) -> Result<ProviderSearchResults> {
let store = SkyClientStore::get().await?;
let mut results = ProviderSearchResults {
provider: super::Providers::SkyClient,
results: vec![],
total: 0,
};
let mods = match &store.mods {
Some(mods) => mods,
None => return Ok(results),
};
let query = query.map(|q| q.trim().to_lowercase());
let limit = limit.map(|l| l as usize);
let offset = offset.unwrap_or(0);
for m in mods {
let mut can_add = true;
if let Some(query) = &query {
if !query.is_empty() {
let display = m.display.to_lowercase();
can_add = display.contains(query) || m.id.contains(query) || m.nicknames.contains(query)
}
}
if can_add {
if let Some(game_versions) = &game_versions {
if !game_versions.is_empty() {
can_add = m.versions.iter().any(|v| v.game_versions.iter().any(|gv| game_versions.contains(&gv)))
}
}
}
if can_add {
if let Some(loaders) = &loaders {
if !loaders.is_empty() {
can_add = m.versions.iter().any(|v| v.loaders.iter().any(|l| loaders.contains(&Loader::from_string(l.to_owned()))))
}
}
}
if can_add {
if limit.map(|l| results.results.len() < l).unwrap_or(true) && offset <= results.total {
let result: SearchResult = m.clone().into();
results.results.push(result);
}
results.total += 1;
}
}
Ok(results)
}

View file

@ -1,409 +0,0 @@
//! Install modpacks from different sources.
use crate::data::{Loader, ManagedPackage, ManagedVersion, PackageData};
use crate::prelude::ClusterPath;
use crate::proxy::send::{init_ingress, send_ingress};
use crate::proxy::IngressId;
use crate::store::{ClusterStage, PackageSide};
use crate::utils::http::{fetch, fetch_advanced, fetch_json, write_icon};
use crate::{IngressType, InnerPathLinux, State};
use onelauncher_utils::io;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PackFormat {
pub game: String,
pub format_version: i32,
pub version_id: String,
pub name: String,
pub summary: Option<String>,
pub files: Vec<PackFile>,
pub dependencies: HashMap<PackDependency, String>,
}
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PackFile {
pub path: InnerPathLinux,
pub hashes: HashMap<PackFileHash, String>,
pub env: Option<HashMap<EnvType, PackageSide>>,
pub downloads: Vec<String>,
pub file_size: u32,
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase", from = "String")]
pub enum PackFileHash {
Sha1,
Sha512,
Unknown(String),
}
impl From<String> for PackFileHash {
fn from(s: String) -> Self {
match s.as_str() {
"sha1" => Self::Sha1,
"sha512" => Self::Sha512,
_ => Self::Unknown(s),
}
}
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase")]
pub enum EnvType {
Client,
Server,
}
#[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)]
pub enum PackDependency {
#[serde(rename = "forge")]
Forge,
#[serde(rename = "neoforge")]
#[serde(alias = "neo-forge")]
NeoForge,
#[serde(rename = "fabric-loader")]
FabricLoader,
#[serde(rename = "quilt-loader")]
QuiltLoader,
#[serde(rename = "minecraft")]
Minecraft,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CreatePackLocation {
/// Create a pack from a Modrinth modpack.
FromModrinth {
package_id: String,
version_id: String,
title: String,
icon_url: Option<String>,
},
/// Create a pack from a file (importing, importing an mrpack or zip-style modpack)
FromFile { path: PathBuf },
}
/// Modpack associated [`Cluster`] wrapper.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePackCluster {
/// The name of the cluster, and relative path.
pub name: String,
/// The game version (MC) of the cluster.
pub mc_version: String,
/// The mod [`Loader`] to use (if applicable).
pub mod_loader: Loader,
/// The mod [`Loader`] version to use, set to `latest` or `stable` or the ID of the loader.
/// The default value is `latest` for the latest version of the mod loader.
pub loader_version: Option<String>,
/// The icon of the cluster.
pub icon: Option<PathBuf>,
/// The URL of an icon for a cluster. (ONLY USE THIS FOR A TEMPORARY CLUSTER)
pub icon_url: Option<String>,
/// The linked package data (mainly used for modpacks and updating files).
pub package_data: Option<PackageData>,
/// Whether or not the cluster should skip being installed.
pub skip: Option<bool>,
/// Whether or not the cluster should skip file watching.
pub skip_watch: Option<bool>,
}
impl Default for CreatePackCluster {
fn default() -> Self {
Self {
name: "Untitled".to_string(),
mc_version: "1.20.4".to_string(),
mod_loader: Loader::Vanilla,
loader_version: None,
icon: None,
icon_url: None,
package_data: None,
skip: Some(true),
skip_watch: Some(false),
}
}
}
#[derive(Clone)]
pub struct CreatePack {
pub file: bytes::Bytes,
pub description: CreatePackDescription,
}
#[derive(Clone, Debug)]
pub struct CreatePackDescription {
pub icon: Option<PathBuf>,
pub override_title: Option<String>,
pub package_id: Option<String>,
pub version_id: Option<String>,
pub existing_ingress: Option<IngressId>,
pub cluster_path: ClusterPath,
}
#[must_use]
pub fn get_cluster_from_pack(location: CreatePackLocation) -> CreatePackCluster {
match location {
CreatePackLocation::FromModrinth {
package_id,
version_id,
title,
icon_url,
} => CreatePackCluster {
name: title,
icon_url,
package_data: Some(PackageData {
package_id: Some(package_id),
version_id: Some(version_id),
locked: Some(true),
}),
..Default::default()
},
CreatePackLocation::FromFile { path } => {
let file_name = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
CreatePackCluster {
name: file_name,
..Default::default()
}
}
}
}
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn generate_pack_from_version_id(
package_id: String,
version_id: String,
title: String,
icon_url: Option<String>,
cluster_path: ClusterPath,
existing_ingress: Option<IngressId>,
) -> crate::Result<CreatePack> {
let state = State::get().await?;
let ingress = if let Some(ing) = existing_ingress {
send_ingress(&ing, 0.0, Some("downloading pack file")).await?;
ing
} else {
init_ingress(
IngressType::DownloadPack {
cluster_path: cluster_path.full_path().await?,
package_name: title,
icon: icon_url,
package_version: version_id.clone(),
},
100.0,
"downloading pack file",
)
.await?
};
send_ingress(&ingress, 0.0, Some("fetching version")).await?;
let version: ManagedVersion = fetch_json(
Method::GET,
&format!(
"{}version/{}",
crate::constants::MODRINTH_API_URL,
version_id
),
None,
None,
&state.fetch_semaphore,
)
.await?;
send_ingress(&ingress, 10.0, None).await?;
let (url, hash) = if let Some(file) = version.files.iter().find(|x| x.primary) {
Some((file.url.clone(), file.hashes.get("sha1")))
} else {
version
.files
.first()
.map(|file| (file.url.clone(), file.hashes.get("sha1")))
}
.ok_or_else(|| anyhow::anyhow!("specified version has no files"))?;
let file = fetch_advanced(
Method::GET,
&url,
hash.map(|x| &**x),
None,
None,
Some((&ingress, 70.0)),
&state.fetch_semaphore,
)
.await?;
send_ingress(&ingress, 0.0, Some("fetching project metadata")).await?;
let pkg: ManagedPackage = fetch_json(
Method::GET,
&format!(
"{}project/{}",
crate::constants::MODRINTH_API_URL,
version.package_id
),
None,
None,
&state.fetch_semaphore,
)
.await?;
send_ingress(&ingress, 10.0, Some("Retrieving icon")).await?;
let icon = if let Some(icon_url) = pkg.icon_url {
let state = State::get().await?;
let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?;
let filename = icon_url.rsplit('/').next();
if let Some(filename) = filename {
Some(
write_icon(
filename,
&state.directories.caches_dir().await,
icon_bytes,
&state.io_semaphore,
)
.await?,
)
} else {
None
}
} else {
None
};
send_ingress(&ingress, 10.0, None).await?;
Ok(CreatePack {
file,
description: CreatePackDescription {
icon,
override_title: None,
package_id: Some(package_id),
version_id: Some(version_id),
existing_ingress: Some(ingress),
cluster_path,
},
})
}
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn generate_pack_from_file(
path: PathBuf,
cluster_path: ClusterPath,
) -> crate::Result<CreatePack> {
let file = io::read(&path).await?;
Ok(CreatePack {
file: bytes::Bytes::from(file),
description: CreatePackDescription {
icon: None,
override_title: None,
package_id: None,
version_id: None,
existing_ingress: None,
cluster_path,
},
})
}
/// Sets generated cluster attributes to the pack attributes (using `cluster::edit`).
/// This includes the pack name, icon, game version, loader version, and loader.
#[onelauncher_macros::memory]
pub async fn set_cluster_information<S: ::std::hash::BuildHasher>(
cluster_path: ClusterPath,
description: &CreatePackDescription,
backup_name: &str,
deps: &HashMap<PackDependency, String, S>,
ignore_lock: bool,
) -> crate::Result<()> {
let mut game_version: Option<&String> = None;
let mut mod_loader = None;
let mut loader_version = None;
for (key, value) in deps {
match key {
PackDependency::Forge => {
mod_loader = Some(Loader::Forge);
loader_version = Some(value);
}
PackDependency::NeoForge => {
mod_loader = Some(Loader::NeoForge);
loader_version = Some(value);
}
PackDependency::FabricLoader => {
mod_loader = Some(Loader::Fabric);
loader_version = Some(value);
}
PackDependency::QuiltLoader => {
mod_loader = Some(Loader::Quilt);
loader_version = Some(value);
}
PackDependency::Minecraft => game_version = Some(value),
}
}
let Some(game_version) = game_version else {
return Err(anyhow::anyhow!("pack did not specify Minecraft version").into());
};
let mod_loader = mod_loader.unwrap_or(Loader::Vanilla);
let loader_version = if mod_loader == Loader::Vanilla {
None
} else {
crate::cluster::create::get_loader_version(
game_version.clone(),
mod_loader,
loader_version.cloned(),
)
.await?
};
// crate::api::cluster::create::create_cluster(
// description.override_title.clone().unwrap_or_else(|| backup_name.to_string()),
// mc_version, mod_loader, loader_version, icon, icon_url, package_data, skip, skip_watch)
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = description
.override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
let package_id = description.package_id.clone();
let version_id = description.version_id.clone();
cl.meta.package_data = if package_id.is_some() && version_id.is_some() {
Some(PackageData {
package_id,
version_id,
locked: if ignore_lock {
cl.meta.package_data.as_ref().and_then(|x| x.locked)
} else {
Some(true)
},
})
} else {
None
};
cl.meta.icon.clone_from(&description.icon);
cl.meta.mc_version.clone_from(game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
Ok(())
}

View file

@ -1,240 +0,0 @@
//! Launcher Import: `ATLauncher`
//! Source Code available at <https://github.com/ATLauncher/ATLauncher>
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::package::from::CreatePackDescription;
use crate::package::import::{self, copy_minecraft};
use crate::package::{self};
use crate::prelude::{Cluster, ClusterPath, Loader};
use crate::store::{ClusterStage, PackageData, State};
use onelauncher_utils::io;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ATInstance {
pub id: String, // minecraft version id ie: 1.12.1, not a name
pub launcher: ATLauncher,
pub java_version: ATJavaVersion,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncher {
pub name: String,
pub pack: String,
pub version: String,
pub loader_version: ATLauncherLoaderVersion,
pub managed_project: Option<ATLauncherManagedProject>,
pub managed_version: Option<ATLauncherManagedVersion>,
pub managed_manifest: Option<package::from::PackFormat>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ATJavaVersion {
pub major_version: u8,
pub component: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherLoaderVersion {
pub r#type: String,
pub version: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ATLauncherManagedProject {
pub id: String,
pub slug: String,
pub project_type: String,
pub team: String,
pub client_side: Option<String>,
pub server_side: Option<String>,
pub categories: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ATLauncherManagedVersion {
pub id: String,
pub project_id: String,
pub name: String,
pub version_number: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherManagedVersionFile {
pub hashes: HashMap<String, String>,
pub url: String,
pub filename: String,
pub primary: bool,
pub size: u64,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherManagedVersionDependency {
pub project_id: Option<String>,
pub version_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherMod {
pub name: String,
pub version: String,
pub file: String,
pub managed_project: Option<ATLauncherManagedProject>,
pub managed_version: Option<ATLauncherManagedVersion>,
}
pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
let instance: String = io::read_to_string(&instance_folder.join("instance.json"))
.await
.unwrap_or(String::new());
let instance: Result<ATInstance, serde_json::Error> =
serde_json::from_str::<ATInstance>(&instance);
if let Err(e) = instance {
tracing::warn!(
"failed to parse instance.json at {}: {}",
instance_folder.display(),
e
);
false
} else {
true
}
}
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn import_atlauncher(
// The path to the base ATLauncher folder.
atlauncher_base_path: PathBuf,
// The instance folder in atlauncher_base_path
instance_folder: String,
// The path to the importing Cluster
cluster_path: ClusterPath,
) -> crate::Result<()> {
let atlauncher_instance_path = atlauncher_base_path
.join("instances")
.join(instance_folder.clone());
let atinstance: String =
io::read_to_string(&atlauncher_instance_path.join("instance.json")).await?;
let atinstance: ATInstance = serde_json::from_str::<ATInstance>(&atinstance)?;
// Icon path should be {instance_folder}/instance.png if it exists,
// Another possibility is ATLauncher/configs/images/{safe_pack_name}.png (safe pack name is alphanumeric lowercase)
let icon_path_primary = atlauncher_instance_path.join("instance.png");
let safe_pack_name = atinstance
.launcher
.pack
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase();
let icon_path_secondary = atlauncher_base_path
.join("configs")
.join("images")
.join(safe_pack_name + ".png");
let icon = match (icon_path_primary.exists(), icon_path_secondary.exists()) {
(true, _) => import::cache_icon(icon_path_primary).await?,
(_, true) => import::cache_icon(icon_path_secondary).await?,
_ => None,
};
// Create description from instance.cfg
let description = CreatePackDescription {
icon,
override_title: Some(atinstance.launcher.name.clone()),
package_id: None,
version_id: None,
existing_ingress: None,
cluster_path: cluster_path.clone(),
};
let backup_name = format!("ATLauncher-{instance_folder}");
let minecraft_folder = atlauncher_instance_path;
import_atlauncher_unmanaged(
cluster_path,
minecraft_folder,
backup_name,
description,
atinstance,
)
.await?;
Ok(())
}
async fn import_atlauncher_unmanaged(
cluster_path: ClusterPath,
minecraft_folder: PathBuf,
backup_name: String,
description: CreatePackDescription,
atinstance: ATInstance,
) -> crate::Result<()> {
let mod_loader = format!(
"\"{}\"",
atinstance.launcher.loader_version.r#type.to_lowercase()
);
let mod_loader: Loader = serde_json::from_str::<Loader>(&mod_loader)?;
let game_version = atinstance.id;
let loader_version = if mod_loader == Loader::Vanilla {
None
} else {
crate::cluster::create::get_loader_version(
game_version.clone(),
mod_loader,
Some(atinstance.launcher.loader_version.version.clone()),
)
.await?
};
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = description
.override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
cl.meta.package_data = Some(PackageData {
package_id: description.package_id.clone(),
version_id: description.version_id.clone(),
locked: Some(description.package_id.is_some() && description.version_id.is_some()),
});
cl.meta.icon.clone_from(&description.icon);
cl.meta.mc_version.clone_from(&game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
// moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -1,168 +0,0 @@
//! Launcher Import: `CurseForge`
//! Closed Source, all this information is datamined unfortunately.
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::{cache_icon, copy_minecraft};
use crate::prelude::{Cluster, ClusterPath, Loader};
use crate::store::{ClusterStage, State};
use crate::utils::http::{fetch, write_icon};
use onelauncher_utils::io;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftInstance {
pub name: Option<String>,
pub base_mod_loader: Option<MinecraftInstanceModLoader>,
pub profile_image_path: Option<PathBuf>,
pub installed_modpack: Option<InstalledModpack>,
/// Minecraft game version. Non-prioritized, use this if Vanilla
pub game_version: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftInstanceModLoader {
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InstalledModpack {
pub thumbnail_url: Option<String>,
}
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
let minecraftinstance: String =
io::read_to_string(&instance_folder.join("minecraftinstance.json"))
.await
.unwrap_or(String::new());
let minecraftinstance: Result<MinecraftInstance, serde_json::Error> =
serde_json::from_str::<MinecraftInstance>(&minecraftinstance);
minecraftinstance.is_ok()
}
#[allow(clippy::too_many_lines)]
pub async fn import_curseforge(
// The path to the CurseForge instance
curseforge_instance_folder: PathBuf,
// The path to the cluster
cluster_path: ClusterPath,
) -> crate::Result<()> {
let minecraft_instance: String =
io::read_to_string(&curseforge_instance_folder.join("minecraftinstance.json")).await?;
let minecraft_instance: MinecraftInstance =
serde_json::from_str::<MinecraftInstance>(&minecraft_instance)?;
let override_title: Option<String> = minecraft_instance.name.clone();
let backup_name = format!(
"Curseforge-{}",
curseforge_instance_folder
.file_name()
.map_or("Unknown".to_string(), |a| a.to_string_lossy().to_string())
);
let state = State::get().await?;
let mut icon = None;
if let Some(icon_path) = minecraft_instance.profile_image_path.clone() {
icon = cache_icon(icon_path).await?;
} else if let Some(InstalledModpack {
thumbnail_url: Some(thumbnail_url),
}) = minecraft_instance.installed_modpack.clone()
{
let icon_bytes = fetch(&thumbnail_url, None, &state.fetch_semaphore).await?;
let filename = thumbnail_url.rsplit('/').last();
if let Some(filename) = filename {
icon = Some(
write_icon(
filename,
&state.directories.caches_dir().await,
icon_bytes,
&state.io_semaphore,
)
.await?,
);
}
}
if let Some(instance_mod_loader) = minecraft_instance.base_mod_loader {
let game_version = minecraft_instance.game_version;
let mut mod_loader = None;
let mut loader_version = None;
match instance_mod_loader.name.split('-').collect::<Vec<&str>>()[..] {
["forge", version] => {
mod_loader = Some(Loader::Forge);
loader_version = Some(version.to_string());
}
["fabric", version, _game_version] => {
mod_loader = Some(Loader::Fabric);
loader_version = Some(version.to_string());
}
_ => {}
}
let mod_loader = mod_loader.unwrap_or(Loader::Vanilla);
let loader_version = if mod_loader == Loader::Vanilla {
None
} else {
crate::cluster::create::get_loader_version(
game_version.clone(),
mod_loader,
loader_version,
)
.await?
};
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
cl.meta.icon.clone_from(&icon);
cl.meta.mc_version.clone_from(&game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
} else {
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.meta.icon.clone_from(&icon);
cl.meta
.mc_version
.clone_from(&minecraft_instance.game_version);
cl.meta.loader_version = None;
cl.meta.loader = Loader::Vanilla;
async { Ok(()) }
})
.await?;
}
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
curseforge_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -1,2 +0,0 @@
//! Launcher Import: FTB App
//! Source Code available at <https://github.com/FTBTeam/FTB-App>

View file

@ -1,109 +0,0 @@
//! Launcher Import: `GDLauncher` (legacy)
//! Source Code available at <https://github.com/gorilla-devs/GDLauncher>
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::{cache_icon, copy_minecraft};
use crate::prelude::{Cluster, ClusterPath, Loader};
use crate::store::{ClusterStage, State};
use onelauncher_utils::io;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GDLauncherConfig {
pub background: Option<String>,
pub loader: GDLauncherLoader,
// pub mods: Vec<GDLauncherMod>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GDLauncherLoader {
pub loader_type: Loader,
pub loader_version: Option<String>,
pub mc_version: String,
pub source: Option<String>,
pub source_name: Option<String>,
}
pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
let config: String = io::read_to_string(&instance_folder.join("config.json"))
.await
.unwrap_or(String::new());
let config: Result<GDLauncherConfig, serde_json::Error> =
serde_json::from_str::<GDLauncherConfig>(&config);
config.is_ok()
}
pub async fn import_gdlauncher(
gdlauncher_instance_folder: PathBuf,
cluster_path: ClusterPath,
) -> crate::Result<()> {
let config: String =
io::read_to_string(&gdlauncher_instance_folder.join("config.json")).await?;
let config: GDLauncherConfig = serde_json::from_str::<GDLauncherConfig>(&config)?;
let override_title: Option<String> = config.loader.source_name.clone();
let backup_name = format!(
"GDLauncher-{}",
gdlauncher_instance_folder
.file_name()
.map_or("Unknown".to_string(), |a| a.to_string_lossy().to_string())
);
let icon = config
.background
.clone()
.map(|b| gdlauncher_instance_folder.join(b));
let icon = if let Some(icon) = icon {
cache_icon(icon).await?
} else {
None
};
let game_version = config.loader.mc_version;
let mod_loader = config.loader.loader_type;
let loader_version = config.loader.loader_version;
let loader_version = if mod_loader == Loader::Vanilla {
None
} else {
crate::cluster::create::get_loader_version(game_version.clone(), mod_loader, loader_version)
.await?
};
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
cl.meta.icon.clone_from(&icon);
cl.meta.mc_version.clone_from(&game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
gdlauncher_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -1,318 +0,0 @@
//! Manages importing data from other launchers.
use crate::prelude::ClusterPath;
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
use crate::proxy::IngressId;
use crate::store::Clusters;
use crate::utils::http::{self, IoSemaphore};
use onelauncher_utils::io::{self, IOError};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::{Path, PathBuf};
pub mod atlauncher;
pub mod curseforge;
pub mod ftb;
pub mod gdlauncher;
pub mod modrinth;
pub mod multibased;
pub mod technic;
pub mod tlauncher;
/// List of launcher types we support importing from.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ImportType {
/// `MultiMC` based launchers
MultiMC,
/// Prism Launcher has its own category because it has different logic (also is objectively better than mmc)
PrismLauncher,
/// `GDLauncher`
GDLauncher,
/// Curseforge's launcher
Curseforge,
/// `ATLauncher`
ATLauncher,
/// Modrinth app.
Modrinth,
/// `TLauncher`
TLauncher,
/// FTB Launcher
FTBLauncher,
/// Technic
Technic,
/// Unknown import option (not widely adopted -> probably a custom launcher with a similar file structure to the above)
#[serde(other)]
Unknown,
}
impl fmt::Display for ImportType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MultiMC => write!(f, "MultiMC"),
Self::ATLauncher => write!(f, "ATLauncher"),
Self::Curseforge => write!(f, "Curseforge"),
Self::GDLauncher => write!(f, "GDLauncher"),
Self::Modrinth => write!(f, "Modrinth App"),
Self::PrismLauncher => write!(f, "PrismLauncher"),
Self::TLauncher => write!(f, "TLauncher"),
Self::FTBLauncher => write!(f, "Feed The Beast"),
Self::Technic => write!(f, "Technic"),
Self::Unknown => write!(f, "Unknown"),
}
}
}
impl ImportType {
pub async fn get_instances_subpath(&self, path: &Path) -> crate::Result<PathBuf> {
Ok(path.join(
&(match self {
Self::GDLauncher | Self::ATLauncher => "instances".to_string(),
Self::Curseforge => "Instances".to_string(),
Self::MultiMC => {
multibased::get_instances_subpath(path.to_path_buf().join("multimc.cfg"))
.await
.unwrap_or_else(|| "instances".to_string())
}
Self::PrismLauncher => {
multibased::get_instances_subpath(path.to_path_buf().join("prismlauncher.cfg"))
.await
.unwrap_or_else(|| "instances".to_string())
}
Self::Modrinth => "profiles".to_string(),
Self::TLauncher => "tinstances".to_string(),
Self::FTBLauncher => "ftbinstances".to_string(),
Self::Technic => "tecinstances".to_string(),
Self::Unknown => {
return Err(anyhow::anyhow!("launcher type unknown, cant import").into())
}
}),
))
}
}
pub async fn get_launcher_instances(import: ImportType, path: Option<PathBuf>) -> crate::Result<(PathBuf, Vec<String>)> {
let base_path = match &path {
Some(path) => path,
None => &default_launcher_path(import).ok_or_else(|| anyhow::anyhow!("could not get launcher base path for {import}"))?
};
let instances_dir = import.get_instances_subpath(base_path).await?;
let mut instances = Vec::new();
let mut dir = io::read_dir(&instances_dir)
.await
.map_err(|_| anyhow::anyhow!("invalid {import} launcher path, failed to import."))?;
while let Some(e) = dir
.next_entry()
.await
.map_err(|e| IOError::with_path(e, &instances_dir))?
{
let path = e.path();
if path.is_dir() && is_valid_instance(path.clone(), import).await {
let name = path.file_name();
if let Some(name) = name {
instances.push(name.to_string_lossy().to_string());
}
}
}
Ok((base_path.to_owned(), instances))
}
#[tracing::instrument]
pub async fn import_instances(
import: ImportType,
base_path: PathBuf,
instances: Vec<String>,
) -> crate::Result<()> {
for instance in instances {
import_instance(import, base_path.clone(), instance).await?;
}
Ok(())
}
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn import_instance(
import: ImportType,
base_path: PathBuf,
instance_path: String,
) -> crate::Result<()> {
tracing::debug!("importing instance from {instance_path}");
let cluster_path = crate::cluster::create::create_unfinished_cluster(instance_path.clone()).await?;
let result = match import {
ImportType::MultiMC | ImportType::PrismLauncher => {
multibased::import_mmc(base_path, instance_path, cluster_path.clone()).await
}
ImportType::ATLauncher => {
atlauncher::import_atlauncher(base_path, instance_path, cluster_path.clone()).await
}
ImportType::GDLauncher => {
gdlauncher::import_gdlauncher(
base_path.join("instances").join(instance_path),
cluster_path.clone(),
)
.await
}
ImportType::Curseforge => {
curseforge::import_curseforge(
base_path.join("Instances").join(instance_path),
cluster_path.clone(),
)
.await
}
ImportType::Modrinth => {
modrinth::import_modrinth(
base_path.join("profiles").join(instance_path),
cluster_path.clone(),
)
.await
}
// ImportType::TLauncher => tlauncher::import_tlauncher(path.join("instances").join(instance_path), cluster_path.clone()).await,
// ImportType::Technic => technic::import_technic(path.join("instances").join(instance_path), cluster_path.clone()).await,
// ImportType::FTBLauncher => ftb::import_ftb(path.join("instances").join(instance_path), cluster_path.clone()).await,
ImportType::Unknown => Err(anyhow::anyhow!("unknown launcher type").into()),
_ => todo!(),
};
match result {
Ok(()) => {}
Err(e) => {
tracing::warn!("failed to import modpack: {:?}", e);
let _ = crate::api::cluster::remove(&cluster_path).await;
return Err(e);
}
}
tokio::task::spawn(Clusters::update_versions());
tracing::debug!("completed import of instance.");
Ok(())
}
/// returns the default path for a given [`ImportType`].
#[must_use]
pub fn default_launcher_path(r#type: ImportType) -> Option<PathBuf> {
let path = match r#type {
ImportType::PrismLauncher => Some(dirs::data_dir()?.join("PrismLauncher")),
ImportType::ATLauncher => Some(dirs::data_dir()?.join("ATLauncher")),
ImportType::GDLauncher => Some(dirs::data_dir()?.join("gdlauncher_next")),
ImportType::Curseforge => Some(dirs::home_dir()?.join("curseforge").join("minecraft")),
ImportType::Modrinth => Some(dirs::data_dir()?.join("theseus")),
ImportType::FTBLauncher => Some(dirs::data_dir()?.join("FTB")),
ImportType::Technic => Some(dirs::data_dir()?.join("Technic")),
ImportType::TLauncher => Some(dirs::data_dir()?.join("TLauncher")),
ImportType::Unknown | ImportType::MultiMC => None, // MultiMC data is in it's application directory
};
let path = path?;
if path.exists() {
Some(path)
} else {
None
}
}
/// Checks if a [`PathBuf`] is a valid instance for a given [`ImportType`]
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn is_valid_instance(instance_path: PathBuf, r#type: ImportType) -> bool {
match r#type {
ImportType::MultiMC | ImportType::PrismLauncher => {
multibased::is_valid_multibased(instance_path).await
}
ImportType::ATLauncher => atlauncher::is_valid_atlauncher(instance_path).await,
ImportType::GDLauncher => gdlauncher::is_valid_gdlauncher(instance_path).await,
ImportType::Curseforge => curseforge::is_valid_curseforge(instance_path).await,
// ImportType::Modrinth => modrinth::is_valid_modrinth(instance_path).await,
// ImportType::TLauncher => tlauncher::is_valid_tlauncher(instance_path).await,
// ImportType::FTBLauncher => ftb::is_valid_ftb(instance_path).await,
// ImportType::Technic => technic::is_valid_technic(instance_path).await,
ImportType::Unknown => false,
_ => todo!(),
}
}
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn cache_icon(icon_path: PathBuf) -> crate::Result<Option<PathBuf>> {
let state = crate::State::get().await?;
let bytes = tokio::fs::read(&icon_path).await;
if let Ok(bytes) = bytes {
let bytes = bytes::Bytes::from(bytes);
let cache_dir = &state.directories.caches_dir().await;
let semaphore = &state.io_semaphore;
Ok(Some(
http::write_icon(&icon_path.to_string_lossy(), cache_dir, bytes, semaphore).await?,
))
} else {
Ok(None)
}
}
pub async fn copy_minecraft(
cluster_path: ClusterPath,
minecraft_path: PathBuf,
io_semaphore: &IoSemaphore,
old_ingress: Option<IngressId>,
) -> crate::Result<IngressId> {
let cluster_path_full = cluster_path.full_path().await?;
let subfiles = sub(&minecraft_path, false).await?;
let total_subfiles = subfiles.len() as u64;
let ingress = init_or_edit_ingress(
old_ingress,
crate::IngressType::CopyCluster {
import: minecraft_path.clone(),
cluster_name: cluster_path.to_string(),
},
total_subfiles as f64,
"copying files in preexisting cluster",
)
.await?;
for sub in subfiles {
let child = sub
.strip_prefix(&minecraft_path)
.map_err(|_| anyhow::anyhow!("invalid .minecraft file {}", &sub.display()))?;
let child = cluster_path_full.join(child);
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
http::copy(&sub, &child, io_semaphore).await?;
send_ingress(&ingress, 1.0, None).await?;
}
Ok(ingress)
}
/// recursively get a [`Vec<PathBuf>`] of all subfiles.
#[onelauncher_macros::memory]
#[async_recursion::async_recursion]
#[tracing::instrument]
pub async fn sub(path: &Path, include_empty: bool) -> crate::Result<Vec<PathBuf>> {
if !path.is_dir() {
return Ok(vec![path.to_path_buf()]);
}
let mut files = Vec::new();
let mut dir = io::read_dir(&path).await?;
let mut has_files = false;
while let Some(child) = dir
.next_entry()
.await
.map_err(|e| IOError::with_path(e, path))?
{
has_files = true;
let path_child = child.path();
files.append(&mut sub(&path_child, include_empty).await?);
}
if !has_files && include_empty {
files.push(path.to_path_buf());
}
Ok(files)
}

View file

@ -1,27 +0,0 @@
//! Launcher Import: Modrinth App
//! Source Code available at <https://github.com/modrinth/code>
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::prelude::ClusterPath;
use onelauncher_utils::io;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ModrinthProfile {}
pub async fn is_valid_modrinth(instance_folder: PathBuf) -> bool {
let config: String = io::read_to_string(&instance_folder.join("profile.json"))
.await
.unwrap_or(String::new());
let config: Result<ModrinthProfile, serde_json::Error> =
serde_json::from_str::<ModrinthProfile>(&config);
config.is_ok()
}
pub async fn import_modrinth(
_modrinth_instance_folder: PathBuf,
_cluster_path: ClusterPath,
) -> crate::Result<()> {
Ok(())
}

View file

@ -1,307 +0,0 @@
//! Launcher Import: `MultiMC` & Prism Launcher
//! Source Code available at <https://github.com/PrismLauncher/PrismLauncher>
//! Source Code available at <https://github.com/MultiMC/Launcher>
use serde::{de, Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::package::from::{self, CreatePackDescription, PackDependency};
use crate::package::import::{self, copy_minecraft};
use crate::prelude::{Cluster, ClusterPath, State};
use onelauncher_utils::io;
// instance.cfg: https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/MinecraftInstance.cpp
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[serde(untagged)]
enum MMCInstanceEnum {
General(MMCInstanceGeneral),
Instance(MMCInstance),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct MMCInstanceGeneral {
pub general: MMCInstance,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MMCInstance {
pub java_path: Option<String>,
pub jvm_args: Option<String>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_optional_bool")]
pub managed_pack: Option<bool>,
#[serde(rename = "ManagedPackID")]
pub managed_pack_id: Option<String>,
pub managed_pack_type: Option<MMCManagedPackType>,
#[serde(rename = "ManagedPackVersionID")]
pub managed_pack_version_id: Option<String>,
pub managed_pack_version_name: Option<String>,
#[serde(rename = "iconKey")]
pub icon_key: Option<String>,
#[serde(rename = "name")]
pub name: Option<String>,
}
/// Deserializes INI String Boolean values into Rust Boolean values for [`serde-ini`].
fn deserialize_optional_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: de::Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?.map_or_else(
|| Ok(None),
|string| match string.as_str() {
"true" => Ok(Some(true)),
"false" => Ok(Some(false)),
_ => Err(de::Error::custom("expected 'true' or 'false'")),
},
)
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum MMCManagedPackType {
Modrinth,
Flame,
ATLauncher,
#[serde(other)]
Unknown,
}
// mmc-pack.json
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MMCPack {
components: Vec<MMCComponent>,
format_version: u32,
}
// https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/Component.h
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MMCComponent {
pub uid: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub dependency_only: bool,
#[serde(default)]
pub important: bool,
#[serde(default)]
pub disabled: bool,
pub cached_name: Option<String>,
pub cached_version: Option<String>,
#[serde(default)]
pub cached_requires: Vec<MMCComponentRequirement>,
#[serde(default)]
pub cached_conflicts: Vec<MMCComponentRequirement>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MMCComponentRequirement {
pub uid: String,
pub equals_version: Option<String>,
pub suggests: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[serde(untagged)]
enum MMCLauncherEnum {
General(MMCLauncherGeneral),
Instance(MMCLauncher),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct MMCLauncherGeneral {
pub general: MMCLauncher,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MMCLauncher {
instance_dir: String,
}
#[tracing::instrument]
pub async fn is_valid_multibased(instance_folder: PathBuf) -> bool {
let instance_cfg = instance_folder.join("instance.cfg");
let mmc_pack = instance_folder.join("mmc-pack.json");
let Ok(mmc_pack) = io::read_to_string(&mmc_pack).await else {
return false;
};
load_instance_cfg(&instance_cfg).await.is_ok()
&& serde_json::from_str::<MMCPack>(&mmc_pack).is_ok()
}
#[tracing::instrument]
pub async fn get_instances_subpath(config: PathBuf) -> Option<String> {
let launcher = io::read_to_string(&config).await.ok()?;
let launcher: MMCLauncherEnum = serde_ini::from_str(&launcher).ok()?;
match launcher {
MMCLauncherEnum::General(p) => Some(p.general.instance_dir),
MMCLauncherEnum::Instance(p) => Some(p.instance_dir),
}
}
async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
let instance_cfg: String = io::read_to_string(file_path).await?;
let instance_cfg_enum: MMCInstanceEnum = serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?;
match instance_cfg_enum {
MMCInstanceEnum::General(instance_cfg) => Ok(instance_cfg.general),
MMCInstanceEnum::Instance(instance_cfg) => Ok(instance_cfg),
}
}
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn import_mmc(
mmc_base_path: PathBuf,
instance_folder: String,
cluster_path: ClusterPath,
) -> crate::Result<()> {
let mmc_instance_path = mmc_base_path
.join("instances")
.join(instance_folder.clone());
let mmc_pack = io::read_to_string(&mmc_instance_path.join("mmc-pack.json")).await?;
let mmc_pack: MMCPack = serde_json::from_str::<MMCPack>(&mmc_pack)?;
let instance_cfg = load_instance_cfg(&mmc_instance_path.join("instance.cfg")).await?;
let icon = if let Some(icon_key) = instance_cfg.icon_key {
let icon_path = mmc_base_path.join("icons").join(icon_key);
import::cache_icon(icon_path).await?
} else {
None
};
let description = CreatePackDescription {
icon,
override_title: instance_cfg.name,
package_id: instance_cfg.managed_pack_id,
version_id: instance_cfg.managed_pack_version_id,
existing_ingress: None,
cluster_path: cluster_path.clone(),
};
let mut minecraft_folder = mmc_base_path
.join("instances")
.join(instance_folder);
if minecraft_folder.join("minecraft").exists() {
minecraft_folder = minecraft_folder.join("minecraft");
} else if minecraft_folder.join(".minecraft").exists() {
minecraft_folder = minecraft_folder.join(".minecraft");
}
let backup_name = if instance_cfg.managed_pack.unwrap_or(false) {
match instance_cfg.managed_pack_type {
Some(MMCManagedPackType::Modrinth) => "Imported Modrinth Modpack".to_string(),
Some(MMCManagedPackType::Flame) => "Imported CurseForge Modpack".to_string(),
Some(MMCManagedPackType::ATLauncher) => "Imported ATLauncher Modpack".to_string(),
Some(_) => "ImportedModpack".to_string(),
_ => {
return Err(anyhow::anyhow!(
"Instance is managed, but managed pack type not specified in instance.cfg"
)
.into())
}
}
} else {
"Imported Modpack".to_string()
};
import_mmc_unmanaged(
cluster_path,
minecraft_folder,
backup_name,
description,
mmc_pack,
)
.await?;
Ok(())
}
async fn import_mmc_unmanaged(
cluster_path: ClusterPath,
minecraft_folder: PathBuf,
backup_name: String,
description: CreatePackDescription,
mmc_pack: MMCPack,
) -> crate::Result<()> {
let dependencies = mmc_pack
.components
.iter()
.filter_map(|component| {
if component.uid.starts_with("net.fabricmc.fabric-loader") {
return Some((
PackDependency::FabricLoader,
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("net.minecraftforge") {
return Some((
PackDependency::Forge,
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("org.quiltmc.quilt-loader") {
return Some((
PackDependency::QuiltLoader,
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("net.minecraft") {
return Some((
PackDependency::Minecraft,
component.version.clone().unwrap_or_default(),
));
}
None
})
.collect();
from::set_cluster_information::<std::hash::RandomState>(
cluster_path.clone(),
&description,
&backup_name,
&dependencies,
false,
)
.await?;
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -1,2 +0,0 @@
//! Launcher Import: Technic Launcher
//! Source Code available at <https://github.com/TechnicPack/LauncherV3>

View file

@ -1,2 +0,0 @@
//! Launcher Import: `TLauncher`
//! Closed source (technically) ;3

View file

@ -1,254 +0,0 @@
// //! **OneLauncher Inferral**
// //!
// //! Infers package metadata beyond just looking up the file hashes.
// use std::collections::HashMap;
// use std::future::Future;
// use std::path::PathBuf;
// use std::pin::Pin;
// use async_zip::tokio::read::fs::ZipFileReader;
// use serde::Deserialize;
// use crate::prelude::PackageType;
// use crate::store::{read_icon, Package, PackageMetadata};
// use crate::utils::http::{FetchSemaphore, IoSemaphore};
// /// A structure representing a package file that hasn't been analyzed.
// pub struct UnanalyzedFile {
// path: String,
// file_name: String,
// package_type: PackageType,
// size: u64,
// cache: String,
// }
// #[derive(Deserialize)]
// #[serde(rename_all = "camelCase")]
// /// A structure representing a `jar/META-INF/mods.toml` Forge metadata file.
// pub struct ForgeModsInfo {
// pub mods: Vec<ForgeMods>,
// }
// #[derive(Deserialize)]
// #[serde(rename_all = "camelCase")]
// /// A structure representing `jar/META-INF/mods.toml` Forge mod metadata.
// pub struct ForgeMods {
// mod_id: String,
// version: Option<String>,
// display_name: Option<String>,
// description: Option<String>,
// logo_file: Option<String>,
// authors: Option<String>,
// }
// #[derive(Deserialize)]
// #[serde(rename_all = "camelCase")]
// /// A structure representing a `jar/mcmod.info` Forge metadata file.
// pub struct ForgeMcMod {
// modid: String,
// name: String,
// description: Option<String>,
// version: Option<String>,
// author_list: Option<Vec<String>>,
// logo_file: Option<String>,
// }
// #[derive(Deserialize)]
// #[serde(untagged)]
// /// A structure representing `jar/fabric.mod.json` Fabric author metadata.
// pub enum FabricAuthor {
// String(String),
// Object { name: String },
// }
// #[derive(Deserialize)]
// #[serde(rename_all = "camelCase")]
// /// A structure representing a `jar/fabric.mod.json` Fabric metadata file.
// pub struct FabricMod {
// id: String,
// version: String,
// name: Option<String>,
// description: Option<String>,
// authors: Vec<FabricAuthor>,
// icon: Option<String>,
// }
// #[derive(Deserialize)]
// /// A structure representing `jar/quilt.mod.json` Quilt metadata.
// pub struct QuiltMetadata {
// name: Option<String>,
// description: Option<String>,
// contributors: Option<HashMap<String, String>>,
// icon: Option<String>,
// }
// #[derive(Deserialize)]
// /// A structure representing a `jar/quilt.mod.json` Quilt metadata file.
// pub struct QuiltMod {
// id: String,
// version: String,
// metadata: Option<QuiltMetadata>,
// }
// #[derive(Deserialize)]
// /// A structure representing a `zip/pack.mcmeta` package metadata file.
// pub struct Pack {
// description: Option<String>,
// }
// async fn process<F, M>(
// zipfr: &ZipFileReader,
// disabled: bool,
// file_name: String,
// sha512: String,
// index: usize,
// reader_fn: F,
// meta_fn: M,
// ) -> crate::Result<Package>
// where
// F: Fn(&ZipFileReader, usize) -> Pin<Box<dyn Future<Output = crate::Result<String>>>>,
// M: Fn(String) -> Option<PackageMetadata>,
// {
// let filestr = reader_fn(zipfr, index).await?;
// if let Some(meta) = meta_fn(filestr) {
// Ok(Package {
// sha512,
// disabled,
// file_name,
// meta,
// })
// } else {
// Ok(Package {
// sha512,
// disabled,
// file_name,
// meta: PackageMetadata::Unknown,
// })
// }
// }
// fn forge_metadata(filestr: String) -> Option<PackageMetadata> {
// if let Ok(pkg) = toml::from_str::<ForgeModsInfo>(&filestr) {
// if let Some(pkg) = pkg.mods.first() {
// return Some(PackageMetadata::Mapped {
// title: Some(
// pkg.display_name
// .clone()
// .unwrap_or_else(|| pkg.mod_id.clone()),
// ),
// description: pkg.description.clone(),
// authors: pkg.authors.clone().map(|x| vec![x]).unwrap_or_default(),
// version: pkg.version.clone(),
// icon: pkg.logo_file.clone(),
// package_type: Some(PackageType::Mod),
// });
// }
// }
// None
// }
// pub async fn infer(
// hash: String,
// path: PathBuf,
// cache_dir: PathBuf,
// io_semaphore: &IoSemaphore,
// fetch_semaphore: &FetchSemaphore,
// ) -> crate::Result<Package> {
// let file_name = path
// .file_name()
// .unwrap_or_default()
// .to_string_lossy()
// .to_string();
// let disabled = file_name.ends_with(".disabled");
// let zipfr = match ZipFileReader::new(path.clone()).await {
// Ok(zipfr) => zipfr,
// Err(_) => {
// return Ok(Package {
// sha512: hash,
// disabled,
// meta: PackageMetadata::Unknown,
// file_name,
// })
// }
// };
// let zip_idx = zipfr
// .file()
// .entries()
// .iter()
// .position(|f| f.filename().as_str().unwrap_or_default() == "META-INF/mods.toml");
// if let Some(idx) = zip_idx {
// let mut filestr = String::new();
// if zipfr
// .reader_with_entry(idx)
// .await?
// .read_to_string_checked(&mut filestr)
// .await
// .is_ok()
// {
// if let Ok(pkg) = toml::from_str::<ForgeModsInfo>(&filestr) {
// if let Some(pkg) = pkg.mods.first() {
// return Ok(Package {
// sha512: hash,
// disabled,
// file_name,
// meta: PackageMetadata::Mapped {
// title: Some(
// pkg.display_name
// .clone()
// .unwrap_or_else(|| pkg.mod_id.clone()),
// ),
// description: pkg.description.clone(),
// authors: pkg.authors.clone().map(|x| vec![x]).unwrap_or_default(),
// version: pkg.version.clone(),
// icon: read_icon(pkg.logo_file.clone(), &cache_dir, &path, io_semaphore)
// .await?,
// package_type: Some(PackageType::Mod),
// },
// });
// }
// }
// }
// }
// let zip_idx = zipfr
// .file()
// .entries()
// .iter()
// .position(|f| f.filename().as_str().unwrap_or_default() == "mcmod.info");
// if let Some(idx) = zip_idx {
// let mut filestr = String::new();
// if zipfr
// .reader_with_entry(idx)
// .await?
// .read_to_string_checked(&mut filestr)
// .await
// .is_ok()
// {
// if let Ok(pkg) = serde_json::from_str::<ForgeMcMod>(&filestr) {
// let icon = read_icon(pkg.logo_file, &cache_dir, &path, io_semaphore).await?;
// return Ok(Package {
// sha512: hash,
// disabled,
// file_name,
// meta: PackageMetadata::Mapped {
// title: Some(if pkg.name.is_empty() {
// pkg.modid
// } else {
// pkg.name
// }),
// description: pkg.description,
// authors: pkg.author_list.unwrap_or_default(),
// version: pkg.version,
// icon,
// package_type: Some(PackageType::Mod),
// },
// });
// }
// }
// }
// }

View file

@ -1,10 +0,0 @@
//! **`OneLauncher` Package**
//!
//! Utilities for importing packages from and to `OneLauncher`.
pub mod content;
pub mod from;
pub mod import;
pub mod infer;
pub mod mrpack;
pub mod zip;

View file

@ -1,338 +0,0 @@
//! Import and Export `.mrpack` mod packs.
use crate::package::from::{set_cluster_information, EnvType, PackFile, PackFileHash};
use crate::prelude::ClusterPath;
use crate::proxy::ingress_try_for_each;
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
use crate::store::{ClusterStage, Clusters, PackageSide};
use crate::utils::http::{fetch_from_mirrors, write};
use crate::{cluster, IngressType, State};
use async_zip::base::read::seek::ZipFileReader;
use onelauncher_utils::io;
use futures::StreamExt;
use std::io::Cursor;
use std::path::{Component, PathBuf};
use super::from::{
generate_pack_from_file, generate_pack_from_version_id, CreatePack, CreatePackLocation,
PackFormat,
};
/// Install a pack
/// Wrapper around [`install_pack_files`] that generates a pack creation description, and
/// attempts to install the pack files. If it fails, it will remove the cluster (fail safely)
/// Install a modpack from a mrpack file (a modrinth .zip format)
#[onelauncher_macros::memory]
pub async fn install_zipped_mrpack(
location: CreatePackLocation,
cluster_path: ClusterPath,
) -> crate::Result<ClusterPath> {
let create_pack: CreatePack = match location {
CreatePackLocation::FromModrinth {
package_id,
version_id,
title,
icon_url,
} => {
generate_pack_from_version_id(
package_id,
version_id,
title,
icon_url,
cluster_path.clone(),
None,
)
.await?
}
CreatePackLocation::FromFile { path } => {
generate_pack_from_file(path, cluster_path.clone()).await?
}
};
let result = install_zipped_mrpack_files(create_pack, false).await;
tokio::task::spawn(Clusters::update_versions());
match result {
Ok(cluster) => Ok(cluster),
Err(err) => {
let _ = cluster::remove(&cluster_path).await;
Err(err)
}
}
}
/// Install all pack files from a description. Does not remove the cluster if it fails
#[onelauncher_macros::memory]
pub async fn install_zipped_mrpack_files(
create_pack: CreatePack,
ignore_lock: bool,
) -> crate::Result<ClusterPath> {
let state = &State::get().await?;
let file = create_pack.file;
let description = create_pack.description.clone(); // make a copy for cluster::edit
let icon = create_pack.description.icon;
let package_id = create_pack.description.package_id;
let version_id = create_pack.description.version_id;
let existing_ingress = create_pack.description.existing_ingress;
let cluster_path = create_pack.description.cluster_path;
let icon_exists = icon.is_some();
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
let mut zip_reader = ZipFileReader::with_tokio(reader).await?;
let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.filename().as_str().unwrap_or_default() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(anyhow::anyhow!("pack doesn't support Minecraft").into());
}
set_cluster_information(
cluster_path.clone(),
&description,
&pack.name,
&pack.dependencies,
ignore_lock,
)
.await?;
let cluster_path = cluster_path.clone();
let ingress = init_or_edit_ingress(
existing_ingress,
IngressType::DownloadPackage {
cluster_path: cluster_path.full_path().await?.clone(),
package_name: pack.name.clone(),
icon,
package_id,
package_version: version_id,
},
100.0,
"downloading modpack",
)
.await?;
let num_files = pack.files.len();
ingress_try_for_each(
futures::stream::iter(pack.files.into_iter()).map(Ok::<PackFile, crate::Error>),
None,
Some(&ingress),
70.0,
num_files,
None,
|pack| {
let cluster_path = cluster_path.clone();
async move {
// TODO: prompt user for optional files in a modpack
if let Some(env) = pack.env {
if env
.get(&EnvType::Client)
.is_some_and(|x| x == &PackageSide::Unsupported)
{
return Ok(());
}
}
let file = fetch_from_mirrors(
&pack.downloads.iter().map(|x| &**x).collect::<Vec<&str>>(),
pack.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
&state.fetch_semaphore,
)
.await?;
let package_path = pack.path.to_string();
let path = std::path::Path::new(&package_path).components().next();
if let Some(Component::CurDir | Component::Normal(_)) = path {
let path = cluster_path.full_path().await?.join(&package_path);
write(&path, &file, &state.io_semaphore).await?;
};
Ok(())
}
},
)
.await?;
send_ingress(&ingress, 0.0, Some("extracting overrides")).await?;
let mut total_len = 0;
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
total_len += 1;
}
}
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
let file_path = PathBuf::from(filename);
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
let mut content = Vec::new();
let mut reader = zip_reader.reader_with_entry(index).await?;
reader.read_to_end_checked(&mut content).await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
if new_path.file_name().is_some() {
write(
&cluster_path.full_path().await?.join(new_path),
&content,
&state.io_semaphore,
)
.await?;
}
send_ingress(
&ingress,
30.0 / f64::from(total_len),
Some(&format!("extracting override {index}/{total_len}")),
)
.await?;
}
}
let potential_icon = cluster_path.full_path().await?.join("icon.png");
if !icon_exists && potential_icon.exists() {
cluster::edit_icon(&cluster_path, Some(&potential_icon)).await?;
}
if let Some(cluster) = cluster::get(&cluster_path).await? {
crate::game::install_minecraft(&cluster, Some(ingress), false).await?;
State::sync().await?;
}
Ok::<ClusterPath, crate::Error>(cluster_path.clone())
} else {
Err(anyhow::anyhow!("no pack manifest found in mrpack").into())
}
}
#[tracing::instrument(skip(mrpack_file))]
#[onelauncher_macros::memory]
pub async fn remove_all_related_files(
cluster_path: ClusterPath,
mrpack_file: bytes::Bytes,
) -> crate::Result<()> {
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
let mut zip_reader = ZipFileReader::with_tokio(reader).await?;
let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.filename().as_str().unwrap_or_default() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(anyhow::anyhow!("pack doesn't support Minecraft").into());
}
cluster::edit(&cluster_path, |cl| {
cl.stage = ClusterStage::PackDownloading;
async { Ok(()) }
})
.await?;
// let state = State::get().await?;
// TODO: Packages
// let all_hashes = pack
// .files
// .iter()
// .filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
// .collect::<Vec<_>>();
// let files_url = format!("{}version_files", crate::constants::MODRINTH_API_URL);
// let hash_packages = fetch_json::<HashMap<String, ManagedVersion>>(
// Method::POST,
// &files_url,
// None,
// Some(json!({
// "hashes": all_hashes,
// "algorithm": "sha512",
// })),
// &state.fetch_semaphore,
// )
// .await?;
// let to_remove = hash_packages
// .into_values()
// .map(|p| p.package_id)
// .collect::<Vec<_>>();
// let cluster = cluster::get(&cluster_path).await?.ok_or_else(|| {
// anyhow::anyhow!("{} is an unmanaged cluster!", cluster_path.to_string())
// })?;
// for (package_path, package) in &cluster.packages {
// if let PackageMetadata::Managed { package_id, .. } = &package.meta {
// if to_remove.contains(package_id) {
// let path = cluster.get_full_path().await?.join(package_path.0.clone());
// if path.exists() {
// io::remove_file(&path).await?;
// }
// }
// }
// }
for file in pack.files {
let path: PathBuf = cluster_path.full_path().await?.join(file.path.to_string());
if path.exists() {
io::remove_file(&path).await?;
}
}
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
let file_path = PathBuf::from(filename);
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
let existing_file = cluster_path.full_path().await?.join(&new_path);
if existing_file.exists() {
io::remove_file(&existing_file).await?;
}
}
}
Ok(())
} else {
Err(anyhow::anyhow!("no pack manifest found in mrpack").into())
}
}

View file

@ -1 +0,0 @@
//! Import and Export `.zip` mod packs

View file

@ -1,232 +0,0 @@
//! Process management interface
pub use crate::store::{Cluster, InitHooks, JavaOptions, Memory, Resolution, Settings, State};
use crate::store::{ClusterPath, ProcessorChild};
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(serde::Serialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct DetailedProcess {
pub uuid: Uuid,
pub user: Option<Uuid>,
pub started_at: DateTime<Utc>,
pub pid: u32,
}
impl DetailedProcess {
pub async fn from_processor_child(process: &ProcessorChild) -> Self {
let pid = process.current_child.read().await.id().unwrap_or(0);
Self {
uuid: process.uuid,
user: process.user,
started_at: process.started_at,
pid,
}
}
}
/// get detailed processes by a [`ClusterPath`].
#[tracing::instrument]
pub async fn get_process_detailed_by_id(uuid: Uuid) -> crate::Result<DetailedProcess> {
let state = State::get().await?;
let processor = state.processor.read().await;
let child = processor
.get(uuid)
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
let child = child.read().await;
let pid = child
.current_child
.read()
.await
.id()
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
Ok(DetailedProcess {
uuid,
user: child.user,
started_at: child.started_at,
pid,
})
}
/// get detailed processes by a [`ClusterPath`].
#[tracing::instrument]
pub async fn get_processes_detailed_by_path(
path: ClusterPath,
) -> crate::Result<Vec<DetailedProcess>> {
let state = State::get().await?;
let processor = state.processor.read().await;
let uuids = processor.running_cluster(path).await?;
let mut processes = Vec::new();
for uuid in uuids {
let child = processor
.get(uuid)
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
let child = child.read().await;
let pid = child
.current_child
.read()
.await
.id()
.ok_or_else(|| anyhow::anyhow!("process not found"))?;
processes.push(DetailedProcess {
uuid,
user: child.user,
started_at: child.started_at,
pid,
});
}
Ok(processes)
}
/// check whether or not a process has completed by its [`Uuid`].
#[tracing::instrument]
pub async fn uuid_is_finished(uuid: Uuid) -> crate::Result<bool> {
Ok(uuid_exit_status(uuid).await?.is_some())
}
/// get the user from a processor by its [`Uuid`].
#[tracing::instrument]
pub async fn get_user_by_process(uuid: Uuid) -> crate::Result<Option<Uuid>> {
let state = State::get().await?;
let processor = state.processor.read().await;
if let Some(processor) = processor.get(uuid) {
Ok(processor.read().await.user)
} else {
Err(anyhow::anyhow!("process not found").into())
}
}
/// get the user from a processor by its [`Uuid`].
#[tracing::instrument]
pub async fn get_process_started_at(uuid: Uuid) -> crate::Result<DateTime<Utc>> {
let state = State::get().await?;
let processor = state.processor.read().await;
if let Some(processor) = processor.get(uuid) {
Ok(processor.read().await.started_at)
} else {
Err(anyhow::anyhow!("process not found").into())
}
}
/// check the exit status of a process by its [`Uuid`].
#[tracing::instrument]
pub async fn uuid_exit_status(uuid: Uuid) -> crate::Result<Option<i32>> {
let state = State::get().await?;
let processor = state.processor.read().await;
processor.exit_status(uuid).await
}
/// get all process [`Uuid`]s.
#[tracing::instrument]
pub async fn get_uuids() -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let processor = state.processor.read().await;
Ok(processor.keys())
}
/// get all running process [`Uuid`]s.
#[tracing::instrument]
pub async fn get_running() -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let processor = state.processor.read().await;
processor.running().await
}
/// get all running process' [`ClusterPath`]s.
#[tracing::instrument]
pub async fn get_running_cluster_paths() -> crate::Result<Vec<ClusterPath>> {
let state = State::get().await?;
let processor = state.processor.read().await;
processor.running_cluster_paths().await
}
/// get all running process' [`Cluster`]s.
#[tracing::instrument]
pub async fn get_running_clusters() -> crate::Result<Vec<Cluster>> {
let state = State::get().await?;
let processor = state.processor.read().await;
processor.running_clusters().await
}
/// check if a cluster is running by its [`Uuid`].
#[tracing::instrument]
pub async fn is_cluster_running(uuid: Uuid) -> crate::Result<bool> {
let clusters = get_running_clusters().await?;
Ok(clusters.iter().any(|cluster| cluster.uuid == uuid))
}
/// get all processes by a [`ClusterPath`].
#[tracing::instrument]
pub async fn get_uuids_by_cluster_path(cluster_path: ClusterPath) -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let processor = state.processor.read().await;
processor.running_cluster(cluster_path).await
}
/// kill an existing and running process by its [`Uuid`]
#[tracing::instrument]
pub async fn kill_by_uuid(uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let processor = state.processor.read().await;
if let Some(process) = processor.get(uuid) {
let mut process = process.write().await;
kill(&mut process).await
} else {
Ok(())
}
}
/// wait for a child process to finish running its manager by its [`Uuid`].
#[tracing::instrument]
pub async fn wait_for_by_uuid(uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let processor = state.processor.read().await;
if let Some(process) = processor.get(uuid) {
let mut process = process.write().await;
wait_for(&mut process).await
} else {
Ok(())
}
}
/// kill an existing and running process
#[tracing::instrument(skip(process))]
pub async fn kill(process: &mut ProcessorChild) -> crate::Result<()> {
process.current_child.write().await.kill().await?;
Ok(())
}
/// wait for a child process to finish running its manager
#[tracing::instrument(skip(process))]
pub async fn wait_for(process: &mut ProcessorChild) -> crate::Result<()> {
process
.manager
.take()
.ok_or_else(|| anyhow::anyhow!("manager already completed for process {}", process.uuid))?
.await?
.map_err(|err| anyhow::anyhow!("failed to run minecraft: {err}"))?;
Ok(())
}
/// get process pid by its [`Uuid`].
#[tracing::instrument]
pub async fn get_pid_by_uuid(uuid: Uuid) -> crate::Result<u32> {
let state = State::get().await?;
let processor = state.processor.read().await;
Ok(processor
.get(uuid)
.ok_or_else(|| anyhow::anyhow!("process not found"))?
.read()
.await
.current_child
.read()
.await
.id()
.ok_or_else(|| anyhow::anyhow!("process not found"))?)
}

View file

@ -1,277 +0,0 @@
//! Proxy for Tauri/CLI specific features and communication
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{OnceCell, RwLock};
use uuid::Uuid;
use crate::store::{ClusterPath, IngressProcessor};
pub mod send;
pub mod utils;
pub use utils::*;
static PROXY_STATE: OnceCell<Arc<ProxyState>> = OnceCell::const_new();
pub struct ProxyState {
#[cfg(feature = "tauri")]
pub app: tauri::AppHandle<tauri::Wry>,
pub ingress_feeds: RwLock<HashMap<Uuid, Ingress>>,
}
#[derive(Serialize, Debug, Clone)]
pub struct Ingress {
pub ingress_uuid: Uuid,
pub message: String,
pub ingress_type: IngressType,
pub total: f64,
pub current: f64,
#[serde(skip)]
pub last_sent: f64,
#[cfg(feature = "cli")]
#[serde(skip)]
pub cli: indicatif::ProgressBar,
}
#[derive(Serialize, Debug, Clone)]
pub struct IngressId(Uuid);
impl Drop for IngressId {
#[clippy::has_significant_drop]
fn drop(&mut self) {
let ingress_uuid = self.0;
tokio::spawn(async move {
if let Ok(proxy_state) = ProxyState::get().await {
let mut ingress_feeds = proxy_state.ingress_feeds.write().await;
#[cfg(any(feature = "tauri", feature = "cli"))]
if let Some(ingress) = ingress_feeds.remove(&ingress_uuid) {
#[cfg(feature = "cli")]
{
let cli = ingress.cli;
cli.finish();
}
#[cfg(feature = "tauri")]
{
use tauri::Emitter;
let event = ingress.ingress_type.clone();
let fraction = ingress.current / ingress.total;
let _ = proxy_state.app.emit(
"ingress",
IngressPayload {
fraction: None,
message: "Complete".to_string(),
event,
ingress_uuid,
},
);
tracing::trace!("exited at {fraction} for ingress: {:?}", ingress_uuid);
}
}
#[cfg(not(any(feature = "tauri", feature = "cli")))]
ingress_feeds.remove(&ingress_uuid);
}
if crate::State::initalized() {
let _ = IngressProcessor::finish(
crate::store::IngressProcessType::IngressFeed,
ingress_uuid,
)
.await;
}
});
}
}
impl ProxyState {
#[cfg(feature = "tauri")]
pub async fn initialize(handle: &tauri::AppHandle<tauri::Wry>) -> crate::Result<Arc<Self>> {
PROXY_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
app: handle.clone(),
ingress_feeds: RwLock::new(HashMap::new()),
}))
})
.await
.cloned()
}
#[cfg(not(feature = "tauri"))]
pub async fn initialize() -> crate::Result<Arc<Self>> {
PROXY_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
ingress_feeds: RwLock::new(HashMap::new()),
}))
})
.await
.cloned()
}
#[cfg(feature = "tauri")]
pub async fn get_main_window() -> crate::Result<tauri::WebviewWindow> {
use tauri::Manager;
let value = Self::get().await?;
Ok(value.app.get_webview_window("main").unwrap())
}
#[allow(clippy::unused_async)]
#[cfg(feature = "tauri")]
pub async fn get() -> crate::Result<Arc<Self>> {
Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
}
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::initialize().await
}
pub async fn list_ingress_feeds() -> crate::Result<HashMap<Uuid, Ingress>> {
let value = Self::get().await?;
let read = value.ingress_feeds.read().await;
let mut display_list: HashMap<Uuid, Ingress> = HashMap::new();
for (uuid, ingress) in read.iter() {
display_list.insert(*uuid, ingress.clone());
}
Ok(display_list)
}
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum IngressType {
Initialize,
DownloadJava {
version: u32,
},
DownloadLoader {
cluster_path: PathBuf,
cluster_name: String,
},
SyncCluster {
cluster_path: PathBuf,
cluster_name: String,
},
InferPackages,
CopyCluster {
import: PathBuf,
cluster_name: String,
},
SyncConfig {
new_path: PathBuf,
},
Archival {
cluster_path: PathBuf,
cluster_name: String,
},
DownloadPackage {
cluster_path: PathBuf,
package_name: String,
icon: Option<PathBuf>,
package_id: Option<String>,
package_version: Option<String>,
},
DownloadPack {
cluster_path: PathBuf,
package_name: String,
icon: Option<String>,
package_version: String,
},
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct IngressPayload {
pub event: IngressType,
pub ingress_uuid: Uuid,
pub fraction: Option<f64>,
pub message: String,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct OfflinePayload {
pub offline: bool,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct MessagePayload {
pub message: String,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
#[serde(tag = "event")]
pub enum InternetPayload {
InstallPackage { id: String },
InstallPack { id: String },
InstallPath { path: PathBuf },
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct ProcessPayload {
pub uuid: Uuid,
pub pid: u32,
pub event: ProcessPayloadType,
pub message: String,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPayloadType {
Started,
Modified,
Finished,
Logging,
}
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
pub struct ClusterPayload {
pub uuid: Uuid,
pub cluster_path: ClusterPath,
pub path: PathBuf,
pub name: String,
pub event: ClusterPayloadType,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ClusterPayloadType {
Created,
Inserted,
Synced,
Edited,
Deleted,
}
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
#[error("event state was not initialized!")]
NotInitialized,
#[error("non-existant ingress of key: {0}")]
NoIngressFound(Uuid),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
}

View file

@ -1,255 +0,0 @@
use super::IngressId;
use crate::api::proxy::{
ClusterPayloadType, Ingress, IngressType, InternetPayload, ProcessPayloadType, ProxyError,
};
use crate::proxy::OfflinePayload;
use crate::store::{ClusterPath, IngressProcessType, IngressProcessor};
use uuid::Uuid;
#[cfg(feature = "cli")]
use crate::constants::CLI_TOTAL_INGRESS;
#[cfg(feature = "tauri")]
use tauri_specta::Event;
#[cfg(feature = "tauri")]
use crate::api::proxy::{ClusterPayload, IngressPayload, MessagePayload, ProcessPayload};
#[cfg(feature = "tauri")]
use tauri::Emitter;
#[onelauncher_macros::memory]
pub async fn init_ingress(
ingress_type: IngressType,
total: f64,
title: &str,
) -> crate::Result<IngressId> {
let key = init_ingress_internal(ingress_type, total, title).await?;
IngressProcessor::add_ingress(IngressProcessType::IngressFeed, key.0).await?;
Ok(key)
}
#[onelauncher_macros::memory]
pub async fn init_ingress_internal(
ingress_type: IngressType,
total: f64,
title: &str,
) -> crate::Result<IngressId> {
let proxy_state = crate::ProxyState::get().await?;
let key = IngressId(Uuid::new_v4());
proxy_state.ingress_feeds.write().await.insert(
key.0,
Ingress {
ingress_uuid: key.0,
message: title.to_string(),
total,
current: 0.0,
last_sent: 0.0,
ingress_type,
#[cfg(feature = "cli")]
cli: {
let pb = indicatif::ProgressBar::new(CLI_TOTAL_INGRESS);
pb.set_position(0);
pb.set_style(indicatif::ProgressStyle::default_bar().template(
"{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}"
).unwrap().progress_chars("#>-"));
pb
},
},
);
send_ingress(&key, 0.0, None).await?;
Ok(key)
}
pub async fn init_or_edit_ingress(
id: Option<IngressId>,
ingress_type: IngressType,
total: f64,
title: &str,
) -> crate::Result<IngressId> {
if let Some(id) = id {
edit_ingress(&id, ingress_type, total, title).await?;
Ok(id)
} else {
init_ingress(ingress_type, total, title).await
}
}
pub async fn edit_ingress(
id: &IngressId,
ingress_type: IngressType,
total: f64,
title: &str,
) -> crate::Result<()> {
let proxy_state = crate::ProxyState::get().await?;
let ingress_feeds = proxy_state.ingress_feeds.write();
if let Some(ingress) = ingress_feeds.await.get_mut(&id.0) {
ingress.ingress_type = ingress_type;
ingress.total = total;
ingress.message = title.to_string();
ingress.current = 0.0;
ingress.last_sent = 0.0;
#[cfg(feature = "cli")]
{
ingress.cli.reset();
}
};
send_ingress(id, 0.0, None).await?;
Ok(())
}
#[allow(unused_variables)]
#[tracing::instrument(level = "debug")]
#[onelauncher_macros::memory]
pub async fn send_ingress(
key: &IngressId,
increment: f64,
message: Option<&str>,
) -> crate::Result<()> {
let proxy_state = crate::ProxyState::get().await?;
let mut ingress = proxy_state.ingress_feeds.write().await;
let Some(ingress) = ingress.get_mut(&key.0) else {
return Err(ProxyError::NoIngressFound(key.0).into());
};
ingress.current += increment;
let display = ingress.current / ingress.total;
let display_conv = if display >= 1.0 { None } else { Some(display) };
if f64::abs(display - ingress.last_sent) > 0.005 {
#[cfg(feature = "cli")]
#[allow(clippy::cast_possible_truncation)]
{
ingress
.cli
.set_message(message.map_or_else(|| ingress.message.clone(), ToString::to_string));
ingress
.cli
.set_position((display * CLI_TOTAL_INGRESS as f64).round() as u64);
}
#[cfg(feature = "tauri")]
proxy_state
.app
.emit(
IngressPayload::NAME,
IngressPayload {
fraction: display_conv,
message: message.unwrap_or(&ingress.message).to_string(),
event: ingress.ingress_type.clone(),
ingress_uuid: ingress.ingress_uuid,
},
)
.map_err(ProxyError::from)?;
ingress.last_sent = display;
}
Ok(())
}
pub async fn send_message(message: &str) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let proxy_state = crate::ProxyState::get().await?;
proxy_state
.app
.emit(
MessagePayload::NAME,
MessagePayload {
message: message.to_string(),
},
)
.map_err(ProxyError::from)?;
}
tracing::warn!("{}", message);
Ok(())
}
pub async fn send_offline(offline: bool) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let proxy_state = crate::ProxyState::get().await?;
proxy_state
.app
.emit(OfflinePayload::NAME, OfflinePayload { offline })
.map_err(ProxyError::from)?;
}
Ok(())
}
pub async fn send_internet(internet: InternetPayload) -> crate::Result<()> {
tracing::debug!("operation {}", serde_json::to_string(&internet)?);
#[cfg(feature = "tauri")]
{
let proxy_state = crate::ProxyState::get().await?;
proxy_state
.app
.emit(InternetPayload::NAME, internet)
.map_err(ProxyError::from)?;
}
Ok(())
}
pub async fn send_process(
uuid: Uuid,
pid: u32,
event: ProcessPayloadType,
message: &str,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let proxy_state = crate::ProxyState::get().await?;
proxy_state
.app
.emit(
ProcessPayload::NAME,
ProcessPayload {
uuid,
pid,
event,
message: message.to_string(),
},
)
.map_err(ProxyError::from)?;
}
Ok(())
}
pub async fn send_cluster(
uuid: Uuid,
cluster_path: &ClusterPath,
name: &str,
event: ClusterPayloadType,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let path = cluster_path.full_path().await?;
let proxy_state = crate::ProxyState::get().await?;
proxy_state
.app
.emit(
ClusterPayload::NAME,
ClusterPayload {
uuid,
cluster_path: cluster_path.clone(),
path,
name: name.to_string(),
event,
},
)
.map_err(ProxyError::from)?;
}
Ok(())
}

View file

@ -1,78 +0,0 @@
use futures::prelude::*;
use crate::proxy::send::send_ingress;
use crate::proxy::IngressId;
#[macro_export]
macro_rules! count {
() => (0usize);
( $x:tt $($xs:tt)* ) => (1usize + $crate::count!($($xs)*));
}
#[macro_export]
macro_rules! ingress_join {
($key:expr, $total:expr, $msg:expr; $($task:expr $(,)?)+) => {
{
let key = $key;
let msg: Option<&str> = $msg;
let num_futures = $crate::count!($($task)*);
let i = $total / num_futures as f64;
paste::paste! {
$( let [ <unique_name $task> ] = {
{
let key = key.clone();
let msg = msg.clone();
async move {
let res = $task.await;
if let Some(key) = key {
$crate::api::proxy::send::send_ingress(key, i, msg).await?;
}
res
}
}
};)+
}
paste::paste! {
tokio::try_join! (
$( [ <unique_name $task>] ),+
)
}
}
};
}
/// TryStreamExt concurrently for Ingress feeds
#[tracing::instrument(skip(stream, f))]
#[onelauncher_macros::memory]
pub async fn ingress_try_for_each<I, F, Fut, T>(
stream: I,
limit: Option<usize>,
key: Option<&IngressId>,
total: f64,
num_futs: usize,
message: Option<&str>,
f: F,
) -> crate::Result<()>
where
I: futures::TryStreamExt<Error = crate::Error> + TryStream<Ok = T>,
F: FnMut(T) -> Fut + Send,
Fut: Future<Output = crate::Result<()>> + Send,
T: Send,
{
let mut f = f;
stream
.try_for_each_concurrent(limit, |item| {
let f = f(item);
async move {
f.await?;
if let Some(key) = key {
send_ingress(key, total / (num_futs as f64), message).await?;
}
Ok(())
}
})
.await
}

View file

@ -1,205 +0,0 @@
//! Settings Management
use crate::proxy::send::{init_ingress, send_ingress};
use crate::store::{Clusters, Directories, Settings};
use crate::State;
use onelauncher_utils::io;
use std::path::{Path, PathBuf};
use tokio::sync::RwLock;
/// gets a [`Settings`] object state.
#[tracing::instrument]
pub async fn get() -> crate::Result<Settings> {
let state = State::get().await?;
let settings = state.settings.read().await;
Ok(settings.clone())
}
/// sets a [`Settings`] object state.
#[tracing::instrument]
pub async fn set(settings: Settings) -> crate::Result<()> {
let state = State::get().await?;
if settings.config_dir != state.settings.read().await.config_dir {
return Err(anyhow::anyhow!("cannot change config directory as a setting").into());
}
let (io, fetch) = async {
let read = state.settings.read().await;
(
settings.max_async_io_operations != read.max_async_io_operations,
settings.max_async_fetches != read.max_async_fetches,
)
}
.await;
let discord_rpc = {
let read = state.settings.read().await;
settings.disable_discord != read.disable_discord
};
{
*state.settings.write().await = settings;
}
if discord_rpc {
state.discord_rpc.clear(true).await?;
}
if io {
state.reset_io_semaphore().await;
}
if fetch {
state.reset_fetch_semaphore().await;
}
State::sync().await?;
Ok(())
}
/// sets the config and caches directory, this can have side effects.
#[allow(clippy::too_many_lines)]
pub async fn set_directory(new: PathBuf) -> crate::Result<()> {
tracing::trace!("changing config directory to {}", new.display());
if !new.is_dir() {
return Err(
anyhow::anyhow!("new config directory {} is not a folder!", new.display()).into(),
);
}
if !is_writable(new.clone()).await? {
return Err(
anyhow::anyhow!("new config directory {} is not writeable", new.display()).into(),
);
}
let ingress = init_ingress(
crate::IngressType::SyncConfig {
new_path: new.clone(),
},
100.0,
"syncing and changing configuration directory",
)
.await?;
tracing::trace!("changing config directory, overriding state write access");
let mut state_write = State::get_and_write().await?;
let old = state_write.directories.config_dir.read().await.clone();
tracing::trace!("resetting file watcher after changing config directory");
let fs_watcher = crate::utils::watcher::initialize_watcher()?;
state_write.watcher = RwLock::new(fs_watcher);
tracing::trace!("collecting all config files to be transfered");
let mut cfg_entries = io::read_dir(&old).await?;
let across_filesystems = is_different_fs(&old, &new);
let mut entries = vec![];
let mut cleanable = vec![];
while let Some(e) = cfg_entries
.next_entry()
.await
.map_err(|err| io::IOError::with_path(err, &old))?
{
let epath = e.path();
if let Some(file_name) = epath.file_name() {
if file_name == crate::constants::CLUSTERS_FOLDER
|| file_name == crate::constants::METADATA_FOLDER
{
if across_filesystems {
entries.extend(crate::package::import::sub(&epath, false).await?);
cleanable.push(epath.clone());
} else {
entries.push(epath.clone());
}
}
}
}
tracing::trace!("transferring configuration files");
let io_semaphore = &state_write.io_semaphore;
let nentries = entries.len() as f64;
for epath in entries {
let relpath = epath
.strip_prefix(&old)
.map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?;
let newpath = new.join(relpath);
if across_filesystems {
crate::utils::http::copy(&epath, &newpath, io_semaphore).await?;
} else {
io::rename(epath.clone(), newpath.clone()).await?;
}
send_ingress(&ingress, 80.0 * (1.0 / nentries), None).await?;
}
tracing::trace!("re-setting configuration settings");
let settings = {
let mut settings = state_write.settings.write().await;
settings.config_dir = Some(new.clone());
tracing::trace!("updating java paths and keys");
for j in settings.java_versions.keys() {
if let Some(java) = settings.java_versions.get_mut(&j) {
if let Ok(relpath) = PathBuf::from(java.path.clone()).strip_prefix(&old) {
java.path = new.join(relpath).to_string_lossy().to_string();
}
}
}
tracing::trace!("re-syncing settings file");
settings
.sync(&state_write.directories.settings_file())
.await?;
settings.clone()
};
tracing::trace!("re-initializing the config directory");
state_write.directories = Directories::initalize(&settings)?;
let cleanable_nentries = cleanable.len();
if cleanable_nentries > 0 {
tracing::trace!("deleting old config files");
}
for ce in cleanable {
io::remove_dir_all(ce).await?;
send_ingress(&ingress, 10.0 * (1.0 / cleanable_nentries as f64), None).await?;
}
tracing::trace!("re-resetting file watching system");
let mut file_watcher = crate::utils::watcher::initialize_watcher()?;
state_write.clusters =
RwLock::new(Clusters::initialize(&state_write.directories, &mut file_watcher).await?);
state_write.watcher = RwLock::new(file_watcher);
send_ingress(&ingress, 10.0, None).await?;
tracing::info!(
"successfully switched configuration directory to {}",
new.display()
);
Ok(())
}
/// checks if two paths are on different data storage devices
fn is_different_fs(first: &Path, second: &Path) -> bool {
let first_root = first.components().next();
let second_root = second.components().next();
first_root != second_root
}
/// checks if a [`PathBuf`] is writable or locked.
pub async fn is_writable(path: PathBuf) -> crate::Result<bool> {
let tmp = path.join(".tmp");
match io::write(tmp.clone(), "test").await {
Ok(()) => {
io::remove_file(tmp).await?;
Ok(true)
}
Err(e) => {
tracing::error!("failed to write to config directory: {}", e);
Ok(false)
}
}
}

View file

@ -1,118 +0,0 @@
//! **`OneLauncher` Constants**
//!
//! Public constant variables and strings that are used throughout the launcher.
//! Can be shared publically, any secrets should be stored in [`crate::store::credentials`].
// =========== Core Metadata ===========
/// The name of the launcher.
pub const NAME: &str = "OneLauncher";
/// The version of the launcher (from `../Cargo.toml` env).
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
// =========== Default Ingress ===========
/// The amount of attempts to fetch a file before a request fails.
pub const FETCH_ATTEMPTS: usize = 3;
/// The total ingress capacity of the CLI, provided to [`indicatif`].
#[cfg(feature = "cli")]
pub const CLI_TOTAL_INGRESS: u64 = 1000;
/// The newline which we replace in metadata files.
pub const DUMMY_REPLACE_NEWLINE: &str = "\n";
// =========== Authentication ===========
/// Our Discord RPC client ID.
pub const DISCORD_RPC_CLIENT_ID: &str = "1274084193193955408";
/// Our Microsoft client ID.
pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03";
/// Mojang/Microsoft client ID.
pub const MINECRAFT_CLIENT_ID: &str = "00000000402b5328";
/// Mojang/Microsoft login redirect URI.
pub const MINECRAFT_REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
/// Mojang/Microsoft login xboxlive scopes to get tokens.
pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
// =========== API ===========
// !!! URLS must NOT have a trailing slash. !!!
/// The Modrinth API base url.
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2";
/// The Modrinth V3 API base url. Used for things like fetching information about organizations.
pub const MODRINTH_V3_API_URL: &str = "https://api.modrinth.com/v3";
/// The Curseforge API base url.
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com";
/// The Curseforge API key. Reserved for use in OneLauncher projects only. Do not use in other projects without permission from the OneLauncher team.
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
/// The Minecraft game ID on Curseforge.
pub const CURSEFORGE_GAME_ID: u32 = 432;
/// Our metadata API base url.
pub const METADATA_API_URL: &str = "https://meta.polyfrost.org";
/// Featured packages API url.
pub const FEATURED_PACKAGES_URL: &str = "https://polyfrost.org/meta/onelauncher/featured.json";
/// <https://mclo.gs>/ API base url.
pub const MCLOGS_API_URL: &str = "https://api.mclo.gs/1";
/// https://skyclient.co/ metadata base url.
pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1";
// =========== Paths ===========
/// The public `settings.json` file used to store the global [`Settings`] state.
///
/// [`Settings`]: crate::store::Settings
pub const SETTINGS_FILE: &str = "settings.json";
/// The public `cluster.json` file used to store the global [`Clusters`] state.
///
/// [`Clusters`]: crate::store::Clusters
pub const CLUSTER_FILE: &str = "cluster.json";
/// The public `authentication.json` file used to store the global [`MinecraftAuth`] state.
///
/// [`MinecraftAuth`]: crate::store::MinecraftAuth
pub const AUTH_FILE: &str = "authentication.json";
/// The public `processor.json` file used to store the global [`Processor`] state.
///
/// [`Processor`]: crate::store::Processor
pub const PROCESSOR_FILE: &str = "processor.json";
/// The current [`Settings`] format version, bumped for breaking changes.
/// If updated, a config file migration logic **NEEDS** to be implemented.
///
/// [`Settings`]: crate::store::Settings
pub const CURRENT_SETTINGS_FORMAT_VERSION: u32 = 1;
/// The constant core caches folder.
pub const CACHES_FOLDER: &str = "caches";
/// The constant core crash reports folder.
pub const CRASH_PATH: &str = "crash-reports";
/// The constant core clusters folder.
pub const CLUSTERS_FOLDER: &str = "clusters";
/// The constant core metadata folder.
pub const METADATA_FOLDER: &str = "metadata";
// =========== Hacky Mojang-spec OS constants ===========
#[cfg(target_os = "windows")]
pub const TARGET_OS: &str = "windows";
#[cfg(target_os = "macos")]
pub const TARGET_OS: &str = "osx";
#[cfg(target_os = "linux")]
pub const TARGET_OS: &str = "linux";
#[cfg(target_arch = "x86")]
pub const NATIVE_ARCH: &str = "32";
#[cfg(target_arch = "x86_64")]
pub const NATIVE_ARCH: &str = "64";
#[cfg(all(not(target_arch = "x86_64"), not(target_arch = "x86")))]
pub const NATIVE_ARCH: &str = "64";
// =========== Platform ===========
/// The default java executable on Windows or anything else.
pub const JAVA_BIN: &str = if cfg!(windows) { "javaw.exe" } else { "java" };
/// The bit width on x64 or x32 architectures.
pub const ARCH_WIDTH: &str = if cfg!(target_pointer_width = "64") {
"64"
} else {
"32"
};
/// Evaluates to `\r\n` on windows, and `\n` on everything else.
pub const LINE_ENDING: &str = if cfg!(windows) { "\r\n" } else { "\n" };

View file

@ -1,180 +0,0 @@
//! **`OneLauncher` Errors**
//!
//! Public wrappers around common `OneLauncher` errors.
use tracing_error::InstrumentError;
/// A standardized [`thiserror::Error`] type that is used across the launcher
#[derive(thiserror::Error, Debug)]
pub enum ErrorKind {
/// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors.
#[error("failed to acquire a tokio semaphore: {0}")]
AcquireError(#[from] tokio::sync::AcquireError),
/// Wrapper around [`tokio::sync::oneshot::error::RecvError`] to handle recieve errors.
#[error("failed to handle async reciever: {0}")]
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
/// Wrapper around [`tokio::task::JoinError`] to handle tokio join handle errors.
#[error("failed to handle an async task: {0}")]
JoinError(#[from] tokio::task::JoinError),
/// Wrapper around [`flate2::CompressError`] to handle flate compression errors.
#[error("failed to compress a file with flate: {0}")]
FlateError(#[from] flate2::CompressError),
/// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors.
#[error("failed to decompress a file with flate: {0}")]
DeflateError(#[from] flate2::DecompressError),
/// Wrapper around [`uuid::Error`] to handle UUID parsing errors.
#[error("failed to parse and handle uuids: {0}")]
UUIDError(#[from] uuid::Error),
/// Wrapper around [`interpulse::Error`] to handle interpulse errors.
#[error("failed to parse and handle metadata: {0}")]
MetadataError(#[from] interpulse::Error),
/// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors.
#[error("failed to manage serialization of json: {0}")]
SerdeError(#[from] serde_json::Error),
/// Wrapper around [`serde_ini::de::Error`] to handle Serde INI parsing errors.
#[error("failed to manage deserialization of ini: {0}")]
INIError(#[from] serde_ini::de::Error),
/// Wrapper around [`reqwest::Error`] to handle HTTP errors.
#[error("failed to establish an http connection: {0}")]
HTTPError(#[from] reqwest::Error),
/// Wrapper around [`zip::result::ZipError`] to handle zip errors.
#[error("failed to manage zip files: {0}")]
ZipError(#[from] zip::result::ZipError),
/// Wrapper around [`async_zip::error::ZipError`] to handle async zip errors.
#[error("failed to manage zip files asynchronously: {0}")]
AsyncZipError(#[from] async_zip::error::ZipError),
/// Wrapper around [`anyhow::Error`] to handle generic errors.
#[error(transparent)]
AnyhowError(#[from] anyhow::Error),
/// Wrapper around [`chrono::ParseError`] to handle date parsing errors.
#[error("failed to parse and handle datetime: {0}")]
ChronoError(#[from] chrono::ParseError),
/// Wrapper around [`crate::state::DirectoryError`] to handle directory errors.
#[error("failed to get directory: {0}")]
DirectoryError(#[from] crate::store::DirectoryError),
/// Wrapper around [`crate::api::proxy::ProxyError`] to handle ingress errors.
#[error("failed to handle api communication: {0}")]
ProxyError(#[from] crate::api::proxy::ProxyError),
/// Wrapper around [`crate::store::MinecraftAuthError`] to handle Minecraft auth errors.
#[error("failed to authenticate with microsoft: {0}")]
AuthError(#[from] crate::store::MinecraftAuthError),
/// Wrapper around [`crate::api::cluster::create::CreateClusterError`] to handle Cluster creation errors.
#[error("failed to create clusters: {0}")]
CreateClusterError(#[from] crate::api::cluster::create::CreateClusterError),
/// Wrapper around [`notify:Error`] to handle file watching errors.
#[error("failed to watch file {0}")]
NotifyError(#[from] notify::Error),
/// Wrapper around [`regex::Error`] to handle `RegExp` errors.
#[error("string verification with regex failed: {0}")]
RegexError(#[from] regex::Error),
/// Wrapper around [`reqwest::header::ToStrError`] to handle header conversion errors.
#[error("failed to convert header to string: {0}")]
HeaderToStrError(#[from] reqwest::header::ToStrError),
/// Wrapper around [`async_tungstenite::tungstenite::Error`] to handle WebSocket errors.
#[error("failed to handle websocket connection: {0}")]
WebSocketError(#[from] async_tungstenite::tungstenite::Error),
/// Indicates an error with SHA checksum.
#[error("checksum {0} did not match {1}!")]
HashError(String, String),
/// Wrapper around [`onelauncher_utils::io::IOError`] to handle wrapped `std::io` errors.
#[error("error handling IO operations: {0}")]
IOError(#[from] onelauncher_utils::io::IOError),
/// Wrapper around [`std::io::Error`] to handle non-wrapped `std::io` errors.
#[error("error handling standard IO operations: {0}")]
StdIOError(#[from] std::io::Error),
/// Wrapper around [`crate::utils::java::JavaError`] to handle java errors.
#[error("failed to manage java installations: {0}")]
JavaError(#[from] crate::utils::java::JavaError),
/// Wrapper around [`crate::store::StrongholdError`] to handle stronghold errors.
#[error("failed to manage credentials with IOTA stronghold: {0}")]
StrongholdError(#[from] crate::store::StrongholdError),
/// Wrapper around [`url::ParseError`] to handle URL parsing errors
#[error("failed to parse and handle URLs: {0}")]
URLError(#[from] url::ParseError),
/// Wrapper around [`tauri::Error`] to handle Tauri errors when the feature flag is enabled
#[cfg(feature = "tauri")]
#[error("error handling tauri: {0}")]
TauriError(#[from] tauri::Error),
}
/// A safe, accessible error structure wrapper around [`ErrorKind`] for use with [`tracing_error`].
#[derive(Debug)]
pub struct Error {
/// The raw, inner [`ErrorKind`], wrapped in an [`Arc`].
pub raw: std::sync::Arc<ErrorKind>,
/// The traced [`ErrorKind`] ([`tracing_error`]) to be parsed and logged.
pub source: tracing_error::TracedError<std::sync::Arc<ErrorKind>>,
}
impl From<Error> for String {
/// Converts an [`Error`] into a [`String`] value.
fn from(value: Error) -> Self {
value.to_string()
}
}
impl std::error::Error for Error {
/// Get the source for the inner traced [`Error`].
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.source()
}
}
impl std::fmt::Display for Error {
/// Format the [`Error`] and output it into the formatter buffer.
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(fmt, "{}", self.source)
}
}
impl<E: Into<ErrorKind>> From<E> for Error {
/// Converts an [`ErrorKind`] error into a traced [`Error`].
/// Useful for reducing error mapping and standardizing error types.
fn from(source: E) -> Self {
let error = Into::<ErrorKind>::into(source);
let boxed_error = std::sync::Arc::new(error);
Self {
raw: boxed_error.clone(),
source: boxed_error.in_current_span(),
}
}
}
impl ErrorKind {
/// Converts the current [`ErrorKind`] into a traced [`Error`].
pub fn as_error(self) -> Error {
self.into()
}
}
/// Alias for a [`core::result::Result`] with the error type [`Error`].
pub type Result<T> = anyhow::Result<T, Error>;

View file

@ -1,361 +0,0 @@
//! Programatic Minecraft argument parser which replaces variables in arguments
use std::collections::{HashMap, HashSet};
use std::io::prelude::BufRead;
use std::io::BufReader;
use std::path::Path;
use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
use interpulse::api::modded::SidedDataEntry;
use interpulse::utils::get_path_from_artifact;
use crate::constants::DUMMY_REPLACE_NEWLINE;
use crate::store::{Memory, MinecraftCredentials, Resolution};
use onelauncher_utils::io::IOError;
use onelauncher_utils::platform::classpath_separator;
#[allow(clippy::too_many_arguments)]
pub fn java_arguments(
arguments: Option<&[Argument]>,
natives_path: &Path,
libraries_path: &Path,
classpaths: &str,
version: &str,
memory: Memory,
custom_args: Vec<String>,
java_arch: &str,
) -> crate::Result<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,
)
},
java_arch,
)?;
} else {
parsed.push(format!(
"-Djava.library.path={}",
onelauncher_utils::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{}M", memory.maximum));
parsed.push(format!("-Xms{}M", memory.minimum));
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,
) -> crate::Result<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>>(
libraries_path: &Path,
args: &[T],
data: &HashMap<String, SidedDataEntry>,
) -> crate::Result<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) -> crate::Result<Option<String>> {
let main_class = tokio::task::spawn_blocking(move || {
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 zip_reader = BufReader::new(file);
for line in zip_reader.lines() {
let mut line = line.map_err(IOError::from)?;
line.retain(|c| !c.is_whitespace());
if line.starts_with("Main-Class:") {
if let Some(class) = line.split(':').nth(1) {
return Ok(Some(class.to_string()));
}
}
}
Ok::<Option<String>, crate::Error>(None)
})
.await??;
Ok(main_class)
}
#[allow(clippy::too_many_arguments)]
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: Resolution,
) -> crate::Result<String> {
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}", crate::constants::MICROSOFT_CLIENT_ID)
.replace("${user_properties}", "{}")
.replace("${user_type}", "msa")
.replace("${version_name}", version)
.replace("${assets_index_name}", asset_index)
.replace(
"${game_directory}",
&onelauncher_utils::io::canonicalize(game_directory)?.to_string_lossy(),
)
.replace(
"${assets_root}",
&onelauncher_utils::io::canonicalize(assets_directory)?.to_string_lossy(),
)
.replace(
"${game_assets}",
&onelauncher_utils::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,
java_arch: &str,
) -> crate::Result<String> {
argument.retain(|c| !c.is_whitespace());
Ok(argument
.replace(
"${natives_directory}",
&onelauncher_utils::io::canonicalize(natives_path)?.to_string_lossy(),
)
.replace(
"${library_directory}",
&onelauncher_utils::io::canonicalize(libraries_path)?.to_string_lossy(),
)
.replace("${classpath_separator}", classpath_separator(java_arch))
.replace("${launcher_name}", crate::constants::NAME)
.replace("${launcher_version}", crate::constants::VERSION)
.replace("${version_name}", version)
.replace("${classpath}", classpaths))
}
pub fn classpaths(
libraries_path: &Path,
libraries: &[Library],
client_path: &Path,
java_arch: &str,
updated: bool,
) -> crate::Result<String> {
let mut classpaths = libraries
.iter()
.filter_map(|lib| {
if let Some(rules) = &lib.rules {
if !crate::game::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(
onelauncher_utils::io::canonicalize(client_path)?
.to_string_lossy()
.to_string(),
);
Ok(classpaths
.into_iter()
.collect::<Vec<_>>()
.join(classpath_separator(java_arch)))
}
pub fn get_classpath_library<T: AsRef<str>>(
libraries_path: &Path,
libraries: &[T],
java_arch: &str,
) -> crate::Result<String> {
let classpaths = libraries
.iter()
.map(|lib| get_library(libraries_path, lib.as_ref(), false))
.collect::<Result<Vec<_>, _>>()?;
Ok(classpaths.join(classpath_separator(java_arch)))
}
pub fn get_library(
libraries_path: &Path,
library: &str,
error_exist: bool,
) -> crate::Result<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 = &onelauncher_utils::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,
) -> crate::Result<()>
where
ParseFn: Fn(&str) -> crate::Result<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(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

@ -1,349 +0,0 @@
//! Downloads Minecraft metadata to be launched
use futures::prelude::*;
use interpulse as ip;
use tokio::sync::OnceCell;
use interpulse::api::minecraft::{
Asset, AssetsIndex, Library, Os, Version as GameVersion, VersionInfo as GameVersionInfo,
};
use interpulse::api::modded::LoaderVersion;
use crate::proxy::send::send_ingress;
use crate::proxy::utils::ingress_try_for_each;
use crate::proxy::IngressId;
use crate::store::State;
use crate::utils::http::{fetch, fetch_json, write};
use onelauncher_utils::io;
use onelauncher_utils::platform::OsExt;
#[tracing::instrument(skip(st, version))]
pub async fn download_minecraft(
st: &State,
version: &GameVersionInfo,
ingress: &IngressId,
java_arch: &str,
force: bool,
updated: bool,
) -> crate::Result<()> {
tracing::info!("downloading minecraft version {}", version.id);
let assets_index = download_assets_index(st, version, Some(ingress), force).await?;
let processors = if version.processors.as_ref().is_some_and(|p| !p.is_empty()) {
25.0
} else {
40.0
};
tokio::try_join! {
download_client(st, version, Some(ingress), force),
download_assets(st, version.assets == "legacy", &assets_index, Some(ingress), processors, force),
download_libraries(st, version.libraries.as_slice(), &version.id, Some(ingress), processors, java_arch, force, updated)
}?;
tracing::info!("downloaded minecraft version {}", version.id);
Ok(())
}
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
#[onelauncher_macros::memory]
pub async fn download_version_info(
st: &State,
version: &GameVersion,
loader: Option<&LoaderVersion>,
force: Option<bool>,
ingress: Option<&IngressId>,
) -> crate::Result<GameVersionInfo> {
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
tracing::debug!("loading minecraft version info for minecraft version {version_id}");
let path = st
.directories
.version_dir(&version_id)
.await
.join(format!("{version_id}.json"));
let result = if path.exists() && !force.unwrap_or(false) {
io::read(path)
.err_into::<crate::Error>()
.await
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
} else {
tracing::info!(
"downloading minecraft version info for minecraft version {}",
&version_id
);
let mut info = fetch_json(
reqwest::Method::GET,
&version.url,
None,
None,
&st.fetch_semaphore,
)
.await?;
if let Some(loader) = loader {
let partial: ip::api::modded::PartialVersionInfo = fetch_json(
reqwest::Method::GET,
&loader.url,
None,
None,
&st.fetch_semaphore,
)
.await?;
info = ip::api::modded::merge_partial_version(partial, info);
}
info.id.clone_from(&version_id);
write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?;
Ok(info)
}?;
if let Some(ingress) = ingress {
send_ingress(ingress, 5.0, None).await?;
}
tracing::debug!("loaded minecraft version info for minecraft version {version_id}");
Ok(result)
}
#[tracing::instrument(skip_all)]
#[onelauncher_macros::memory]
pub async fn download_assets_index(
st: &State,
version: &GameVersionInfo,
ingress: Option<&IngressId>,
force: bool,
) -> crate::Result<AssetsIndex> {
tracing::debug!("loading assets index");
let path = st
.directories
.index_dir()
.await
.join(format!("{}.json", &version.asset_index.id));
let result = if path.exists() && !force {
io::read(path)
.err_into::<crate::Error>()
.await
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
} else {
let index = fetch_json(
reqwest::Method::GET,
&version.asset_index.url,
None,
None,
&st.fetch_semaphore,
)
.await?;
write(&path, &serde_json::to_vec(&index)?, &st.io_semaphore).await?;
tracing::info!("downloaded assets index");
Ok(index)
}?;
if let Some(ingress) = ingress {
send_ingress(ingress, 5.0, None).await?;
}
tracing::debug!("loaded assets index");
Ok(result)
}
#[tracing::instrument(skip(st, index))]
#[onelauncher_macros::memory]
pub async fn download_assets(
st: &State,
legacy: bool,
index: &AssetsIndex,
ingress: Option<&IngressId>,
ingress_amount: f64,
force: bool,
) -> crate::Result<()> {
tracing::debug!("loading minecraft assets");
let num_futs = index.objects.len();
let assets = stream::iter(index.objects.iter()).map(Ok::<(&String, &Asset), crate::Error>);
ingress_try_for_each(
assets,
None,
ingress,
ingress_amount,
num_futs,
None,
|(name, asset)| async move {
let hash = &asset.hash;
let resources = st.directories.object_dir(hash).await;
let url = format!(
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
sub_hash = &hash[..2]
);
let fetch_cell = OnceCell::<bytes::Bytes>::new();
tokio::try_join! {
async {
if !resources.exists() || force {
let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
write(&resources, resource, &st.io_semaphore).await?;
tracing::trace!("fetched asset resource with hash {hash}");
}
Ok::<_, crate::Error>(())
},
async {
let resources = st.directories.legacy_assets_dir().await.join(
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
);
if legacy && !resources.exists() || force {
let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
write(&resources, resource, &st.io_semaphore).await?;
tracing::trace!("fetched legacy asset resource with hash {hash}");
}
Ok::<_, crate::Error>(())
},
}?;
tracing::trace!("loaded asset resource with hash {hash}");
Ok(())
},
)
.await?;
tracing::debug!("loaded minecraft assets");
Ok(())
}
#[tracing::instrument(skip(st, libraries))]
#[onelauncher_macros::memory]
#[allow(clippy::too_many_arguments)]
pub async fn download_libraries(
st: &State,
libraries: &[Library],
version: &str,
ingress: Option<&IngressId>,
ingress_amount: f64,
java_arch: &str,
force: bool,
updated: bool,
) -> crate::Result<()> {
tracing::debug!("loading minecraft libraries");
tokio::try_join! {
io::create_dir_all(st.directories.libraries_dir().await),
io::create_dir_all(st.directories.version_natives_dir(version).await)
}?;
let num_files = libraries.len();
// TODO: figure out why rustfmt won't format this (it's not a macro...)
ingress_try_for_each(
stream::iter(libraries.iter())
.map(Ok::<&Library, crate::Error>),
None,
ingress,
ingress_amount,
num_files,
None,
|lib| async move {
if let Some(rules) = &lib.rules {
if !super::rules(rules, java_arch, updated) {
tracing::trace!("skipped library {} due to rule", &lib.name);
return Ok(());
}
}
if !lib.downloadable {
tracing::trace!("skipped non-downloadable library {}", &lib.name);
return Ok(());
}
tokio::try_join! {
async {
let artifact_path = ip::utils::get_path_from_artifact(&lib.name)?;
let path = st.directories.libraries_dir().await.join(&artifact_path);
if path.exists() && !force {
return Ok(());
}
if let Some(ip::api::minecraft::LibraryDownloads {
artifact: Some(ref artifact), ..
}) = lib.downloads {
if !artifact.url.is_empty() {
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore).await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
return Ok::<_, crate::Error>(());
}
}
let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
write(&path, &bytes, &st.io_semaphore).await?;
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
Ok::<_, crate::Error>(())
},
async {
if let Some((os_key, classifiers)) = None.or_else(|| Some((
lib.natives.as_ref()?.get(&Os::native_arch(java_arch))?,
lib.downloads.as_ref()?.classifiers.as_ref()?
))) {
let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH);
if let Some(native) = classifiers.get(&parsed) {
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?;
let reader = std::io::Cursor::new(&data);
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
match archive.extract(st.directories.version_natives_dir(version).await) {
Ok(()) => tracing::debug!("downloaded native {}", &lib.name),
Err(err) => tracing::error!("failed to download native {}: {}", &lib.name, err)
}
} else {
tracing::error!("failed to extract native {}", &lib.name);
}
}
}
Ok(())
}
}?;
tracing::debug!("loaded minecraft library {}", lib.name);
Ok(())
}
).await?;
tracing::debug!("downloaded all minecraft libraries");
Ok(())
}
#[tracing::instrument(skip_all)]
#[onelauncher_macros::memory]
pub async fn download_client(
st: &State,
version: &GameVersionInfo,
ingress: Option<&IngressId>,
force: bool,
) -> crate::Result<()> {
let version_id = &version.id;
tracing::debug!("loading minecraft client for minecraft version {version_id}");
let client = version
.downloads
.get(&ip::api::minecraft::DownloadType::Client)
.ok_or_else(|| anyhow::anyhow!("no client downloads exist for {version_id}"))?;
let path = st
.directories
.version_dir(version_id)
.await
.join(format!("{version_id}.jar"));
if !path.exists() || force {
let result = fetch(&client.url, Some(&client.sha1), &st.fetch_semaphore).await?;
write(&path, &result, &st.io_semaphore).await?;
tracing::trace!("fetched minecraft client version {version_id}");
}
if let Some(ingress) = ingress {
send_ingress(ingress, 9.0, None).await?;
}
tracing::debug!("downloaded client for minecraft version {version_id}");
Ok(())
}

View file

@ -1,544 +0,0 @@
//! **`OneLauncher` Game**
//!
//! Manages, installs, and launches the core Minecraft game.
use crate::prelude::JavaVersion;
use crate::processor;
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
use crate::proxy::{IngressId, IngressType};
use crate::store::{
self as st, Cluster, ClusterStage, MinecraftCredentials, ProcessorChild, State,
};
use onelauncher_utils::io::{self, IOError};
use chrono::Utc;
use interpulse as ip;
use interpulse::api::minecraft::{RuleAction, VersionInfo};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::process::Command;
use uuid::Uuid;
mod arguments;
pub mod metadata;
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),
},
);)+
}
}
#[tracing::instrument(skip(cluster))]
#[onelauncher_macros::memory]
pub async fn install_minecraft(
cluster: &Cluster,
old_ingress: Option<IngressId>,
repair: bool,
) -> crate::Result<()> {
// let should_sync = old_ingress.is_some();
let ingress = init_or_edit_ingress(
old_ingress,
IngressType::DownloadLoader {
cluster_name: cluster.meta.name.clone(),
cluster_path: cluster.get_full_path().await?,
},
100.0,
"downloading minecraft loader",
)
.await?;
crate::api::cluster::edit(&cluster.cluster_path(), |clus| {
clus.stage = ClusterStage::Downloading;
async { Ok(()) }
})
.await?;
State::sync().await?;
// if should_sync {
// let path = cluster.cluster_path();
// tokio::task::spawn(async move {
// Cluster::sync_packages(&path).await
// });
// }
let state = State::get().await?;
let instance_path = &io::canonicalize(cluster.get_full_path().await?)?;
let metadata = state.metadata.read().await;
let version_manifest = metadata.minecraft.clone();
let versions = version_manifest
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft manifest"))?
.versions;
let version_idx = versions
.iter()
.position(|g| g.id == cluster.meta.mc_version)
.ok_or_else(|| {
anyhow::anyhow!("invalid minecraft game version {}", cluster.meta.mc_version)
})?;
let version = &versions[version_idx];
let updated = version_idx <= versions.iter().position(|g| g.id == "22w16a").unwrap_or(0); // LWJGL patching
let version_jar = cluster
.meta
.loader_version
.as_ref()
.map_or(version.id.clone(), |g| {
format!("{}-{}", version.id.clone(), g.id.clone())
});
let mut version_info = metadata::download_version_info(
&state,
version,
cluster.meta.loader_version.as_ref(),
Some(repair),
Some(&ingress),
)
.await?;
// TODO: java checking and installation
let key = version_info
.java_version
.as_ref()
.map_or(8, |it| it.major_version);
let (java_version, set_java) =
if let Some(java_version) = java_version_from_cluster(cluster, &version_info).await? {
(std::path::PathBuf::from(java_version.path), false)
} else {
(crate::api::java::install_java_from_major(key).await?, true)
};
let java_version = crate::api::java::check_java(java_version.clone())
.await?
.ok_or_else(|| anyhow::anyhow!("java path validation failed: {:?}", java_version))?;
if set_java {
{
let mut settings = state.settings.write().await;
settings
.java_versions
.insert(key.to_string(), java_version.clone());
}
State::sync().await?;
}
metadata::download_minecraft(
&state,
&version_info,
&ingress,
&java_version.arch,
repair,
updated,
)
.await?;
if let Some(processors) = &version_info.processors {
let client = state
.directories
.version_dir(&version_jar)
.await
.join(format!("{version_jar}.jar"));
let libraries = state.directories.libraries_dir().await;
if let Some(ref mut data) = version_info.data {
data_entry! {
data;
"SIDE":
client => "client",
server => "";
"MINECRAFT_JAR":
client => client.to_string_lossy(),
server => "";
"MINECRAFT_VERSION":
client => cluster.meta.mc_version.clone(),
server => "";
"ROOT":
client => instance_path.to_string_lossy(),
server => "";
"LIBRARY_DIR":
client => libraries.to_string_lossy(),
server => "";
}
send_ingress(&ingress, 0.0, Some("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 cp = onelauncher_utils::ref_owned!(cp = processor.classpath.clone() => {
cp.push(processor.jar.clone());
});
let child = Command::new(&java_version.path)
.arg("-cp")
.arg(arguments::get_classpath_library(
&libraries,
&cp,
&java_version.arch,
)?)
.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(|e| IOError::with_path(e, &java_version.path))
.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());
}
send_ingress(
&ingress,
30.0 / total_length as f64,
Some(&format!("running forge processor {index}/{total_length}")),
)
.await?;
}
}
}
crate::api::cluster::edit(&cluster.cluster_path(), |clus| {
clus.stage = ClusterStage::Installed;
async { Ok(()) }
})
.await?;
State::sync().await?;
send_ingress(&ingress, 100.0, Some("installed minecraft successfully")).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
#[onelauncher_macros::memory]
#[allow(clippy::too_many_arguments)]
pub async fn launch_minecraft(
cluster: &Cluster,
java_args: &[String],
env_args: &[(String, String)],
mc_options: &[(String, String)],
post_hook: Option<String>,
credentials: &MinecraftCredentials,
resolution: &st::Resolution,
memory: &st::Memory,
wrapper: &Option<String>,
) -> crate::Result<Arc<tokio::sync::RwLock<ProcessorChild>>> {
if cluster.stage == ClusterStage::PackDownloading || cluster.stage == ClusterStage::Downloading
{
return Err(anyhow::anyhow!("cluster is still downloading").into());
}
if cluster.stage != ClusterStage::Installed {
install_minecraft(cluster, None, false).await?;
}
let state = State::get().await?;
let metadata = state.metadata.read().await;
let instance_path = cluster.get_full_path().await?;
let instance_path = &io::canonicalize(instance_path)?;
let version_manifest = metadata.minecraft.clone();
let versions = version_manifest
.ok_or_else(|| anyhow::anyhow!("couldn't get minecraft manifest"))?
.versions;
let version_index = versions
.iter()
.position(|it| it.id == cluster.meta.mc_version)
.ok_or_else(|| anyhow::anyhow!("invalid game version {}", cluster.meta.mc_version))?;
let version = &versions[version_index];
let updated = version_index <= versions.iter().position(|x| x.id == "22w16a").unwrap_or(0);
let version_jar = cluster
.meta
.loader_version
.as_ref()
.map_or(version.id.clone(), |it| {
format!("{}-{}", version.id.clone(), it.id.clone())
});
let version_info = metadata::download_version_info(
&state,
version,
cluster.meta.loader_version.as_ref(),
None,
None,
)
.await?;
let java_version = java_version_from_cluster(cluster, &version_info)
.await?
.ok_or_else(|| anyhow::anyhow!("missing java installation"))?;
let java_version = crate::api::java::check_java(java_version.path.clone().into())
.await?
.ok_or_else(|| anyhow::anyhow!("java path invalid: {}", java_version.path))?;
let client_path = state
.directories
.version_dir(&version_jar)
.await
.join(format!("{version_jar}.jar"));
let args = version_info.arguments.clone().unwrap_or_default();
let mut command = match wrapper {
Some(hook) => {
onelauncher_utils::ref_owned!(it = Command::new(hook) => {it.arg(&java_version.path)})
}
None => Command::new(&java_version.path),
};
let env_args = Vec::from(env_args);
if !state.settings.read().await.allow_parallel_running_clusters {
let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?;
if let Some(uuid) = existing.first() {
return Err(anyhow::anyhow!(
"cluster {} is already running ({uuid})",
cluster.cluster_path()
)
.into());
}
}
command
.args(
arguments::java_arguments(
args.get(&ip::api::minecraft::ArgumentType::Jvm)
.map(Vec::as_slice),
&state.directories.version_natives_dir(&version_jar).await,
&state.directories.libraries_dir().await,
&arguments::classpaths(
&state.directories.libraries_dir().await,
version_info.libraries.as_slice(),
&client_path,
&java_version.arch,
updated,
)?,
&version_jar,
*memory,
Vec::from(java_args),
&java_version.arch,
)?
.into_iter(),
)
.arg(version_info.main_class.clone())
.args(
arguments::minecraft_arguments(
args.get(&ip::api::minecraft::ArgumentType::Game)
.map(Vec::as_slice),
version_info.minecraft_arguments.as_deref(),
credentials,
&version.id,
&version_info.asset_index.id,
instance_path,
&state.directories.assets_dir().await,
version.type_,
*resolution,
&java_version.arch,
)?
.into_iter(),
)
.current_dir(instance_path.clone());
// when cargo sets the `DYLD_LIBRARY_PATH`` it breaks Minecraft
#[cfg(target_os = "macos")]
if std::env::var("CARGO").is_ok() {
command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
}
// remove pre-existing java options, as they should be set in the cluster settings.
command.env_remove("_JAVA_OPTIONS");
command.envs(env_args);
// overrides `options.txt` with our settings: i can't believe it's not yaml
if !mc_options.is_empty() {
let options_path = instance_path.join("options.txt");
let mut options_string = if options_path.exists() {
io::read_to_string(&options_path).await?
} else {
String::new()
};
for (key, value) in mc_options {
let re = regex::Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
if re.is_match(&options_string) {
let replaced_string = re
.replace_all(&options_string, &format!("{key}:{value}"))
.to_string();
options_string = replaced_string;
} else {
options_string.push_str(&format!("\n{key}:{value}"));
}
}
io::write(&options_path, options_string).await?;
}
crate::api::cluster::edit(&cluster.cluster_path(), |clust| {
clust.meta.played_at = Some(Utc::now());
async { Ok(()) }
})
.await?;
State::sync().await?;
let mut censors = HashMap::new();
let username = whoami::username();
let realname = whoami::realname();
censors.insert(format!("/{username}/"), "/{ENV_USERNAME}/".to_string());
censors.insert(format!("\\{username}\\"), "\\{ENV_USERNAME}\\".to_string());
censors.insert(format!("/{realname}/"), "/{ENV_REALNAME}/".to_string());
censors.insert(format!("\\{realname}\\"), "\\{ENV_REALNAME}\\".to_string());
censors.insert(
credentials.access_token.clone(),
"{MC_ACCESS_TOKEN}".to_string(),
);
censors.insert(credentials.username.clone(), "{MC_USERNAME}".to_string());
censors.insert(
credentials.id.as_simple().to_string(),
"{MC_UUID}".to_string(),
);
censors.insert(
credentials.id.as_hyphenated().to_string(),
"{MC_UUID}".to_string(),
);
#[cfg(feature = "tauri")]
{
use crate::ProxyState;
let window = ProxyState::get_main_window().await?;
let settings = state.settings.read().await;
if settings.hide_on_launch {
window.minimize()?;
}
}
if !*state.offline.read().await {
let _ = state
.discord_rpc
.set_activity(&format!("Playing {}", cluster.meta.name), true)
.await;
}
let enable_gamemode = state.settings.read().await.enable_gamemode;
let mut state_processor = state.processor.write().await;
state_processor
.insert_process(
Uuid::new_v4(),
cluster.cluster_path(),
command,
post_hook,
censors,
Some(credentials.id),
Some(enable_gamemode),
)
.await
}
/// Parses an array of Minecraft library feature or OS rules.
#[tracing::instrument]
pub fn rules(rules: &[ip::api::minecraft::Rule], java_version: &str, updated: bool) -> bool {
let mut rule = rules
.iter()
.map(|r| rule(r, java_version, 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]
pub fn rule(rule: &ip::api::minecraft::Rule, java_version: &str, updated: bool) -> Option<bool> {
use ip::api::minecraft::{Rule, RuleAction};
let result = match rule {
Rule {
os: Some(ref os), ..
} => onelauncher_utils::platform::os_rule(os, java_version, updated),
Rule {
features: Some(ref 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
}
}
}
}
pub async fn java_version_from_cluster(
cluster: &Cluster,
version_info: &VersionInfo,
) -> crate::Result<Option<JavaVersion>> {
if let Some(java) = cluster.java.clone().and_then(|x| x.custom_version) {
Ok(Some(java))
} else {
let key = version_info
.java_version
.as_ref()
.map_or(8, |x| x.major_version);
let state = State::get().await?;
let settings = state.settings.read().await;
if let Some(j) = settings.java_versions.get(&key.to_string()) {
return Ok(Some(j.clone()));
}
Ok(None)
}
}

View file

@ -1,20 +0,0 @@
#![allow(clippy::all)]
//! # `OneLauncher`
//!
//! A library used as a core for our launcher and Rust APIs.
#[macro_use]
pub mod utils;
pub mod api;
pub mod constants;
pub mod error;
pub mod game;
pub mod logger;
pub mod store;
pub use api::proxy::{Ingress, IngressType, ProxyState};
pub use api::*;
pub use error::*;
pub use logger::start_logger;
pub use store::{InnerPathLinux, State};

View file

@ -1,74 +0,0 @@
//! **`OneLauncher` Logger**
//!
//! Public utilities for [`tracing`] logging in `OneLauncher`.
//!
//! - `RUST_LOG=xxx` is a variable which defines tracing configuration (`ERROR > WARN > INFO > DEBUG > TRACE`)
//! - `RUST_LOG="onelauncher=trace"` will show **all** mesages from `OneLauncher` and not other dependencies.
//! - `RUST_LOG="info"` will show info, warn, and error logs.
//!
//! Errors returned to [`tauri`] are displayed as tracing error logs.
//! These errors also include an attached span trace if the error is from a [`tracing_error`] and the level is info, debug, or trace.
//! Tracing errors also provide Sentry metadata to be handled.
//! To set the environment variable on `unix` you can use the following command: `RUST_LOG="xxx" {your command}`
//!
//! The default `RUST_LOG` is `onelauncher=show`, meaning only logs from `OneLauncher` will be displayed at the info, warn, and error levels.
use tracing_appender::non_blocking::WorkerGuard;
// TODO: refactor logging and add more env filters.
/// Initialize the global [`tracing`] logger in development.
#[cfg(debug_assertions)]
#[must_use]
pub fn start_logger() -> Option<WorkerGuard> {
use tracing_subscriber::prelude::*;
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("onelauncher=info,onelauncher_gui=info")
});
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
None
}
/// Initialize the global [`tracing`] logger in production.
#[cfg(not(debug_assertions))]
pub fn start_logger() -> Option<WorkerGuard> {
use crate::store::Directories;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::fmt::time::ChronoLocal;
use tracing_subscriber::prelude::*;
let logs_dir = if let Some(directory) = Directories::logs_dir() {
directory
} else {
eprintln!("failed to start logger");
return None;
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("onelauncher=info"));
let file_appender = RollingFileAppender::new(Rotation::DAILY, logs_dir, "onelauncher.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let subscriber = tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_timer(ChronoLocal::rfc_3339()),
)
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
Some(guard)
}

View file

@ -1,802 +0,0 @@
//! Core cluster state management
use chrono::{DateTime, Utc};
use futures::prelude::*;
use interpulse::api::modded::LoaderVersion;
use notify::RecommendedWatcher;
use notify_debouncer_mini::Debouncer;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use uuid::Uuid;
use crate::constants::CLUSTER_FILE;
use crate::proxy::send::{send_cluster, send_message};
use crate::proxy::ClusterPayloadType;
use crate::utils::http::{write_icon, IoSemaphore};
use crate::utils::java::JavaVersion;
use crate::State;
use onelauncher_utils::io::{self, IOError};
use super::{Directories, InitHooks, Memory, PackageType, Resolution};
/// Core Cluster state manager with a [`HashMap<ClusterPath, Cluster>`].
pub struct Clusters(pub HashMap<ClusterPath, Cluster>);
/// Core Cluster stages used in package logic.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ClusterStage {
/// Installed with no downloads left.
Installed,
/// Downloading any sort of core metadata or mod file.
Downloading,
/// Downloading a full pack file, which is granted lower priority due to the high network demand.
PackDownloading,
/// Not installed at all.
#[default]
NotInstalled,
}
impl ClusterStage {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match *self {
Self::Installed => "installed",
Self::Downloading => "downloading",
Self::PackDownloading => "pack_downloading",
Self::NotInstalled => "not_installed",
}
}
}
impl FromStr for ClusterStage {
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"installed" => Self::Installed,
"downloading" => Self::Downloading,
"pack_downloading" => Self::PackDownloading,
_ => Self::NotInstalled,
})
}
type Err = crate::Error;
}
/// Relative Path wrapper to be used as an identifer for a cluster path.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(transparent)]
pub struct ClusterPath(pub PathBuf);
impl ClusterPath {
/// Get a new [`ClusterPath`] instance from an [`Into<PathBuf>`].
pub fn new(path: impl Into<PathBuf>) -> Self {
Self(path.into())
}
pub async fn find_by_uuid(uuid: Uuid) -> crate::Result<Self> {
Ok(crate::cluster::get_by_uuid(uuid)
.await?
.ok_or_else(|| anyhow::anyhow!("Cluster does not exist"))?
.cluster_path())
}
/// Get the full [`PathBuf`] of the current cluster path.
pub async fn full_path(&self) -> crate::Result<PathBuf> {
let state: std::sync::Arc<tokio::sync::RwLockReadGuard<'_, State>> = State::get().await?;
let clusters_dir = state.directories.clusters_dir().await;
Ok(clusters_dir.join(&self.0))
}
/// Get the full [`PathBuf`] of the current cluster path.
pub async fn full_path_dirs(&self, dirs: &Directories) -> crate::Result<PathBuf> {
let clusters_dir = dirs.clusters_dir().await;
Ok(clusters_dir.join(&self.0))
}
/// Validate the UTF of a cluster path.
pub fn validate(&self) -> crate::Result<&Self> {
self.0.to_str().ok_or_else(|| {
anyhow::anyhow!(
"invalid file path string {}!",
self.0.clone().to_string_lossy()
)
})?;
Ok(self)
}
/// Validate the cluster and clone the current [`ClusterPath`].
pub async fn cluster_path(&self) -> crate::Result<Self> {
(crate::cluster::get(self).await?).map_or_else(
|| {
Err(anyhow::anyhow!(
"failed to get path of unmanaged or corrupted cluster {}",
self.to_string()
)
.into())
},
|c| Ok(c.cluster_path()),
)
}
/// Create a [`ClusterPath`] from a full [`PathBuf`].
pub async fn from_path(path: PathBuf) -> crate::Result<Self> {
let path: PathBuf = io::canonicalize(path)?;
let clusters_dir = io::canonicalize(State::get().await?.directories.clusters_dir().await)?;
path.strip_prefix(clusters_dir)
.ok()
.and_then(|f| f.file_name())
.ok_or_else(|| {
anyhow::anyhow!("path {} is not a cluster path", path.to_string_lossy())
})?;
Ok(Self(path))
}
}
impl std::fmt::Display for ClusterPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.display().fmt(f)
}
}
/// Used for backwards compatibility for modpacks which handle windows paths strangely.
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(into = "RawPackagePath", from = "RawPackagePath")]
pub struct InnerPathLinux(pub String);
impl InnerPathLinux {
/// Get the first 2 components of the inner path.
#[must_use]
pub fn get_components(&self) -> String {
self.to_string()
.split('/')
.take(2)
.collect::<Vec<_>>()
.join("/")
}
}
impl std::fmt::Display for InnerPathLinux {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<RawPackagePath> for InnerPathLinux {
fn from(value: RawPackagePath) -> Self {
Self(value.0.replace('\\', "/"))
}
}
/// Used for backwards compatibility for modpacks which handle windows paths strangely.
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
struct RawPackagePath(pub String);
impl From<InnerPathLinux> for RawPackagePath {
fn from(value: InnerPathLinux) -> Self {
Self(value.0)
}
}
// TODO: make it not a pathbuf, just get rid of all packagepath and clusterpath into str.
/// Relative [`PathBuf`] for a specific [`Package`] of a [`Cluster`].
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(transparent)]
pub struct PackagePath(PathBuf);
impl PackagePath {
/// Transform a full [`PathBuf`] into a relative [`PackagePath`].
pub async fn from_path(path: &PathBuf) -> crate::Result<Self> {
let clusters_dir: PathBuf =
std::fs::canonicalize(State::get().await?.directories.clusters_dir().await)?;
let path: PathBuf = std::fs::canonicalize(path)?;
let path = path
.strip_prefix(clusters_dir)
.ok()
.map(|p| p.components().skip(1).collect::<PathBuf>())
.ok_or_else(|| {
anyhow::anyhow!("path {path:?} does not exist in a cluster!", path = path)
})?;
Ok(Self(path))
}
pub fn new(package_type: PackageType, file_name: String) -> crate::Result<Self> {
Ok(Self(
PathBuf::from_str(format!("{}/{}", package_type.get_folder(), file_name).as_str())
.map_err(|e| anyhow::anyhow!("couldn't create package path from '{package_type}' '{file_name}': {e:?}"))?
))
}
/// Get the full [`PathBuf`] of the current package path.
pub async fn full_path(&self, cluster: &ClusterPath) -> crate::Result<PathBuf> {
let cluster_dir = cluster.full_path().await?;
Ok(cluster_dir.join(&self.0))
}
pub fn package_type(&self) -> crate::Result<PackageType> {
PackageType::from_parent(&self.0).ok_or_else(|| anyhow::anyhow!("couldn't get package type of package path: {:?}", self.0).into())
}
pub fn file_name(&self) -> crate::Result<String> {
self.0
.file_name()
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| anyhow::anyhow!("couldn't get file name of package path: {:?}", self.0).into())
}
pub fn relative_path(&self) -> PathBuf {
self.0.clone()
}
}
/// Represents a single Instance and installation of Minecraft
/// Contains settings and identifiers on a per-Cluster basis, falling back to default settings for Options<>
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Cluster {
/// The Uuid of a specific cluster.
pub uuid: Uuid,
/// The download stage that of a cluster.
#[serde(default)]
pub stage: ClusterStage,
/// The core path of the cluster.
#[serde(default)]
pub path: PathBuf,
/// The associated cluster metadata.
pub meta: ClusterMeta,
/// The per-cluster JVM memory allocation options.
#[serde(skip_serializing_if = "Option::is_none")]
pub memory: Option<Memory>,
/// The per-cluster JVM options.
#[serde(skip_serializing_if = "Option::is_none")]
pub java: Option<JavaOptions>,
/// The per-cluster Minecraft window default resolution.
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<Resolution>,
/// The per-cluster Minecraft window fullscreen status.
pub force_fullscreen: Option<bool>,
// The per-cluster initialization hooks.
#[serde(skip_serializing_if = "Option::is_none")]
pub init_hooks: Option<InitHooks>,
}
/// Represents core Cluster metadata ([`Cluster#meta`]).
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ClusterMeta {
/// The user-facing name of the cluster stored as a string.
pub name: String,
/// The user-facing group of the cluster stored as a string.
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
/// The associated Minecraft version of the cluster as last updated.
pub mc_version: String,
/// The associated mod [`Loader`] as specified in the cluster.
#[serde(default)]
pub loader: Loader,
/// The associated mod [`LoaderVersion`] if available.
#[serde(skip_serializing_if = "Option::is_none")]
pub loader_version: Option<LoaderVersion>,
/// The time that the cluster was created in [`DateTime<Utc>`].
#[serde(default)]
pub created_at: DateTime<Utc>,
/// The time the cluster was last modified in [`DateTime<Utc>`].
#[serde(default)]
pub modified_at: DateTime<Utc>,
/// The last time the cluster was played in [`DateTime<Utc>`].
/// (Defaults to None if the cluster has never been played)
#[serde(skip_serializing_if = "Option::is_none")]
pub played_at: Option<DateTime<Utc>>,
/// The overall time played in seconds stored as a [`u64`].
#[serde(default)]
pub overall_played: u64,
/// The recent time played stored as a [`u64`].
#[serde(default)]
pub recently_played: u64,
/// The associated [`PackageData`] and modpack data for the cluster.
#[serde(skip_serializing_if = "Option::is_none")]
pub package_data: Option<PackageData>,
/// The user-facing cluster icon as a [`PathBuf`].
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<PathBuf>,
/// The user-facing cluster icon as a URL string.
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
}
/// Optional data used to link a specific cluster to a package project.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackageData {
/// The package ID as a String.
pub package_id: Option<String>,
/// The version of the package as a String.
pub version_id: Option<String>,
/// Whether or not the current package is locked (for legacy modpack support).
#[serde(default = "default_locked")]
pub locked: Option<bool>,
}
#[must_use]
pub const fn default_locked() -> Option<bool> {
Some(true)
}
/// Available mod loaders to be used for a cluster.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Loader {
/// The default vanilla loader, no modding supported.
#[default]
Vanilla,
/// The `MinecraftForge` Minecraft mod loader.
Forge,
/// The `FabircMC` Minecraft mod loader.
Fabric,
/// The `NeoForge` Minecraft mod loader.
NeoForge,
/// The Quilt Minecraft mod loader.
Quilt,
/// The Legacy Fabric port mod loader.
LegacyFabric,
}
impl Loader {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match *self {
Self::Vanilla => "vanilla",
Self::Forge => "forge",
Self::Fabric => "fabric",
Self::Quilt => "quilt",
Self::NeoForge => "neoforge",
Self::LegacyFabric => "legacyfabric",
}
}
#[must_use]
pub const fn as_meta_str(&self) -> &'static str {
match *self {
Self::Vanilla => "vanilla",
Self::Forge => "forge",
Self::Fabric => "fabric",
Self::Quilt => "quilt",
Self::NeoForge => "neo",
Self::LegacyFabric => "legacyfabric",
}
}
#[must_use]
pub fn from_string(val: String) -> Self {
Self::from_str(val.as_str())
}
#[must_use]
pub fn from_str(val: &str) -> Self {
match val {
"forge" => Self::Forge,
"fabric" => Self::Fabric,
"quilt" => Self::Quilt,
"neoforge" => Self::NeoForge,
_ => Self::Vanilla,
}
}
#[must_use]
pub const fn supports_mods(&self) -> bool {
!matches!(self, Self::Vanilla)
}
}
impl TryFrom<String> for Loader {
type Error = crate::ErrorKind;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(match value.to_lowercase().as_str() {
"vanilla" => Self::Vanilla,
"forge" => Self::Forge,
"fabric" => Self::Fabric,
"neoforge" => Self::NeoForge,
"quilt" => Self::Quilt,
"legacyfabric" | "legacy_fabric" => Self::LegacyFabric,
_ => return Err(anyhow::anyhow!("invalid loader type").into()),
})
}
}
impl std::fmt::Display for Loader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match *self {
Self::Vanilla => "Vanilla",
Self::Forge => "Forge",
Self::Fabric => "Fabric",
Self::NeoForge => "NeoForge",
Self::Quilt => "Quilt",
Self::LegacyFabric => "LegacyFabric",
})
}
}
/// Custom Java arguments on a per-cluster basis.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct JavaOptions {
/// A custom java version from the global java store, if specified.
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_version: Option<JavaVersion>,
/// Custom runtime arguments when running the cluster.
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_arguments: Option<Vec<String>>,
/// Custom environment variables when running the cluster.
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_env_arguments: Option<Vec<(String, String)>>,
}
impl Cluster {
/// Create a new Cluster
#[tracing::instrument]
pub async fn new(uuid: Uuid, name: String, version: String) -> crate::Result<Self> {
if name.trim().is_empty() {
return Err(anyhow::anyhow!("invalid instance name (empty)").into());
}
Ok(Self {
uuid,
stage: ClusterStage::NotInstalled,
path: PathBuf::new().join(&name),
meta: ClusterMeta {
name,
group: None,
mc_version: version,
loader: Loader::Vanilla,
loader_version: None,
created_at: Utc::now(),
modified_at: Utc::now(),
package_data: None,
played_at: None,
overall_played: 0,
recently_played: 0,
icon: None,
icon_url: None,
},
java: None,
memory: None,
resolution: None,
force_fullscreen: None,
init_hooks: None,
})
}
/// Get the [`ClusterPath`] of the specified [`Cluster`].
#[inline]
#[must_use]
pub fn cluster_path(&self) -> ClusterPath {
ClusterPath::new(&self.path)
}
/// Set the icon [`bytes::Bytes`] for this cluster.
#[tracing::instrument(skip(self, io_semaphore, icon))]
pub async fn set_icon_path<'a>(
&'a mut self,
cache_path: &Path,
io_semaphore: &IoSemaphore,
icon: &Path,
file_name: &str,
) -> crate::Result<()> {
let icon = io::read(icon).await?;
self.set_icon_bytes(cache_path, io_semaphore, icon.into(), file_name)
.await?;
Ok(())
}
/// Set the icon [`bytes::Bytes`] for this cluster.
#[tracing::instrument(skip(self, io_semaphore, icon))]
pub async fn set_icon_bytes<'a>(
&'a mut self,
cache_path: &Path,
io_semaphore: &IoSemaphore,
icon: bytes::Bytes,
file_name: &str,
) -> crate::Result<()> {
let file = write_icon(file_name, cache_path, icon, io_semaphore).await?;
self.meta.icon = Some(file);
self.meta.modified_at = Utc::now();
Ok(())
}
/// Handle a cluster crash.
pub fn handle_crash(path: ClusterPath) {
tokio::task::spawn(async move {
let res = async {
let cluster = crate::api::cluster::get(&path).await?;
if let Some(cluster) = cluster {
if cluster.stage == ClusterStage::Installed {
send_message(&format!(
"cluster {} has crashed! visit the logs page for a crash report.",
cluster.meta.name
))
.await?;
}
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => tracing::warn!("failed to send crash report to frontend {err}"),
};
});
}
/// get the full path to the current cluster.
pub async fn get_full_path(&self) -> crate::Result<PathBuf> {
let state = State::get().await?;
let clusters_dir = state.directories.clusters_dir().await;
Ok(clusters_dir.join(&self.path))
}
/// get full paths and subpaths
pub async fn get_full_subs(&self) -> crate::Result<Vec<PathBuf>> {
let mut files = Vec::new();
let cluster_path = self.get_full_path().await?;
let mut paths = |path: &str| {
let new = cluster_path.join(path);
if new.exists() {
for sub in std::fs::read_dir(&new).map_err(|e| IOError::with_path(e, &new))? {
let sub = sub.map_err(IOError::from)?.path();
if sub.is_file() {
files.push(sub);
}
}
}
Ok::<(), crate::Error>(())
};
paths(PackageType::Mod.get_folder())?;
paths(PackageType::ShaderPack.get_folder())?;
paths(PackageType::ResourcePack.get_folder())?;
paths(PackageType::DataPack.get_folder())?;
Ok(files)
}
/// watch the filesystem for changes with [`notify`].
#[tracing::instrument(skip(watcher))]
#[onelauncher_macros::memory]
pub async fn watch(
cluster_path: &Path,
watcher: &mut Debouncer<RecommendedWatcher>,
) -> crate::Result<()> {
async fn watch_path(
cluster_path: &Path,
watcher: &mut Debouncer<RecommendedWatcher>,
path: &str,
) -> crate::Result<()> {
let path = cluster_path.join(path);
io::create_dir_all(&path).await?;
watcher
.watcher()
.watch(&cluster_path.join(path), notify::RecursiveMode::Recursive)?;
Ok(())
}
watch_path(cluster_path, watcher, PackageType::Mod.get_folder()).await?;
watch_path(cluster_path, watcher, PackageType::ShaderPack.get_folder()).await?;
watch_path(
cluster_path,
watcher,
PackageType::ResourcePack.get_folder(),
)
.await?;
watch_path(cluster_path, watcher, PackageType::DataPack.get_folder()).await?;
watch_path(cluster_path, watcher, "crash-reports").await?;
Ok(())
}
}
impl Clusters {
/// Initialize the cluster manager and HashMap.
#[tracing::instrument(skip(watcher))]
#[onelauncher_macros::memory]
pub async fn initialize(
dirs: &Directories,
watcher: &mut Debouncer<RecommendedWatcher>,
) -> crate::Result<Self> {
let mut clusters = HashMap::new();
let clusters_dir = dirs.clusters_dir().await;
io::create_dir_all(&&clusters_dir).await?;
watcher
.watcher()
.watch(&clusters_dir, notify::RecursiveMode::NonRecursive)?;
let mut files = io::read_dir(&dirs.clusters_dir().await).await?;
while let Some(file) = files.next_entry().await.map_err(IOError::from)? {
let path = file.path();
if path.is_dir() {
let cluster = match Self::from_dir(&path, dirs).await {
Ok(cluster) => Some(cluster),
Err(err) => {
tracing::warn!("failed to load cluster {err}. skipping");
None
}
};
if let Some(cluster_) = cluster {
let path = io::canonicalize(path)?;
Cluster::watch(&path, watcher).await?;
clusters.insert(cluster_.cluster_path(), cluster_);
}
}
}
Ok(Self(clusters))
}
/// update registered packages
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn update_packages() {
// TODO: Pacakges
}
/// update all available package versions
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn update_versions() {
let res = async {
let state = State::get().await?;
let mut updateable: Vec<(ClusterPath, String)> = Vec::new();
{
let clusters = state.clusters.read().await;
for (cluster_path, cluster) in &clusters.0 {
if let Some(package_data) = &cluster.meta.package_data {
if let Some(linked_package) = &package_data.package_id {
updateable.push((cluster_path.clone(), linked_package.clone()));
}
}
}
}
// TODO
{
let clusters = state.clusters.read().await;
clusters.sync().await?;
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => tracing::warn!("failed to update managed packages: {err}"),
};
}
/// insert a cluster into the HashMap
#[tracing::instrument(skip(self, cluster))]
#[onelauncher_macros::memory]
pub async fn insert(&mut self, cluster: Cluster, dont_watch: bool) -> crate::Result<&Self> {
send_cluster(
cluster.uuid,
&cluster.cluster_path(),
&cluster.meta.name,
ClusterPayloadType::Inserted,
)
.await?;
if !dont_watch {
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?;
}
let cluster_name = cluster.cluster_path();
cluster_name.validate()?;
self.0.insert(cluster_name, cluster);
Ok(self)
}
/// remove a cluster from the HashMap
#[tracing::instrument(skip(self))]
pub async fn remove(&mut self, cluster_path: &ClusterPath) -> crate::Result<Option<Cluster>> {
let cluster = self.0.remove(cluster_path);
let path = cluster_path.full_path().await?;
if path.exists() {
io::remove_dir_all(&path).await?;
}
Ok(cluster)
}
/// sync all available clusters
#[tracing::instrument(skip_all)]
pub async fn sync(&self) -> crate::Result<&Self> {
let _state = State::get().await?;
stream::iter(self.0.iter())
.map(Ok::<_, crate::Error>)
.try_for_each_concurrent(None, |(_, cluster)| async move {
let json = serde_json::to_vec(&cluster)?;
let json_path = cluster.get_full_path().await?.join(CLUSTER_FILE);
io::write(&json_path, &json).await?;
Ok::<_, crate::Error>(())
})
.await?;
Ok(self)
}
/// read a cluster from a directory
async fn from_dir(path: &Path, dirs: &Directories) -> crate::Result<Cluster> {
let json = io::read(&path.join(CLUSTER_FILE)).await?;
let mut cluster = serde_json::from_slice::<Cluster>(&json)?;
cluster.path = PathBuf::from(
path.strip_prefix(dirs.clusters_dir().await)
.map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?,
);
Ok(cluster)
}
/// sync a cluster
pub fn sync_cluster(cluster_path: ClusterPath) {
tokio::task::spawn(async move {
let span = tracing::span!(tracing::Level::INFO, "sync_cluster");
let res = span.in_scope(|| async {
let state = State::get().await?;
let dirs = &state.directories;
let mut clusters = state.clusters.write().await;
if let Some(cluster) = clusters.0.get_mut(&cluster_path) {
if !cluster.get_full_path().await?.exists() {
send_cluster(
cluster.uuid,
&cluster_path,
&cluster.meta.name,
ClusterPayloadType::Deleted,
)
.await?;
tracing::debug!("removed non-existant fs cluster!");
clusters.0.remove(&cluster_path);
}
} else if cluster_path.full_path().await?.exists() {
clusters
.insert(
Self::from_dir(&cluster_path.full_path().await?, dirs).await?,
false,
)
.await?;
tokio::task::spawn(async move {
// Cluster::sync_packages(&cluster_path).await;
});
}
Ok::<(), crate::Error>(())
})
.await;
match res {
Ok(()) => {}
Err(err) => tracing::warn!("failed to fetch a cluster: {err}"),
};
});
}
}

View file

@ -1,80 +0,0 @@
//! Core credential management and storage with IOTA Stronghold.
use super::Directories;
use crate::utils::http::IoSemaphore;
use iota_stronghold::{KeyProvider, SnapshotPath, Stronghold};
use serde::{Serialize, Serializer};
use std::ops::Deref;
use std::path::Path;
use zeroize::Zeroizing;
/// A K/V encrypted store to handle digital secrets using IOTA Stronghold.
#[derive(Debug)]
pub struct Credentials {
inner: Stronghold,
path: SnapshotPath,
keyprovider: KeyProvider,
}
impl Credentials {
/// Initialize the K/V encrypted stronghold.
pub fn new<P: AsRef<Path>>(
path: P,
password: Zeroizing<Vec<u8>>,
_dirs: &Directories,
_io_semaphore: &IoSemaphore,
) -> crate::Result<Self> {
let path = SnapshotPath::from_path(path);
let stronghold = Stronghold::default();
let keyprovider = KeyProvider::try_from(password).map_err(StrongholdError::MemoryError)?;
if path.exists() {
stronghold
.load_snapshot(&keyprovider, &path)
.map_err(StrongholdError::ClientError)?;
}
Ok(Self {
inner: stronghold,
path,
keyprovider,
})
}
pub fn save(&self) -> crate::Result<()> {
self.inner
.commit_with_keyprovider(&self.path, &self.keyprovider)
.map_err(StrongholdError::ClientError)?;
Ok(())
}
pub const fn inner(&self) -> &Stronghold {
&self.inner
}
}
impl Deref for Credentials {
type Target = Stronghold;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[derive(thiserror::Error, Debug)]
pub enum StrongholdError {
#[error("stronghold not initialized")]
StrongholdNotInitialized,
#[error(transparent)]
ClientError(#[from] iota_stronghold::ClientError),
#[error(transparent)]
MemoryError(#[from] iota_stronghold::MemoryError),
#[error(transparent)]
ProcedureError(#[from] iota_stronghold::procedures::ProcedureError),
}
impl Serialize for StrongholdError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}

View file

@ -1,191 +0,0 @@
//! Handles core directory management
use std::path::PathBuf;
use tokio::sync::RwLock;
use super::Settings;
use crate::constants;
/// Directory management and utilities.
#[derive(Debug)]
pub struct Directories {
/// Settings: All small caches and the core settings.json
pub settings_dir: PathBuf,
/// Base Configuration: Clusters and all Assets (should be changable as a setting that can go across file systems)
/// By default this should be the same as the `settings_dir`
pub config_dir: RwLock<PathBuf>,
/// The current working directory of the directory manager.
pub cwd: PathBuf,
}
impl Directories {
/// Bootstrap the initial settings directory on first startup.
#[must_use]
pub fn init_settings_dir() -> Option<PathBuf> {
Self::env_path("ONELAUNCHER_CONFIG")
.or_else(|| Some(dirs::data_dir()?.join(constants::NAME)))
}
/// Bootstrap the initial settings directory on first startup.
#[inline]
pub fn init_settings_file() -> crate::Result<PathBuf> {
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
Ok(settings_dir.join("settings.json"))
}
/// Initialize the core directory manager
#[tracing::instrument]
pub fn initalize(settings: &Settings) -> crate::Result<Self> {
let cwd = std::env::current_dir().map_err(DirectoryError::WorkingDir)?;
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
let config_dir = settings
.config_dir
.clone()
.ok_or(DirectoryError::ConfigDir)?;
Ok(Self {
settings_dir,
config_dir: RwLock::new(config_dir),
cwd,
})
}
/// Get the `config_dir/metadata` folder within the core config directory.
#[inline]
pub async fn metadata_dir(&self) -> PathBuf {
self.config_dir
.read()
.await
.join(constants::METADATA_FOLDER)
}
/// Get the `config_dir/metadata/libraries` folder for Minecraft libraries.
#[inline]
pub async fn libraries_dir(&self) -> PathBuf {
self.metadata_dir().await.join("libraries")
}
/// Get the `config_dir/metadata/java` directory.
#[inline]
pub async fn java_dir(&self) -> PathBuf {
self.metadata_dir().await.join("java_versions")
}
/// Get the `config_dir/metadata/caches` directory.
#[inline]
pub async fn caches_dir(&self) -> PathBuf {
self.metadata_dir().await.join("caches")
}
/// Bootstrap the core logs directory and get it as a [`PathBuf`].
#[inline]
#[must_use]
pub fn logs_dir() -> Option<PathBuf> {
Self::init_settings_dir().map(|d| d.join("logs"))
}
/// Get the `settings_dir/settings.json` file as a [`PathBuf`].
#[inline]
pub fn settings_file(&self) -> PathBuf {
self.settings_dir.join(constants::SETTINGS_FILE)
}
/// Get a [`PathBuf`] from a provided environment variable.
#[inline]
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name).map(PathBuf::from)
}
/// Get the `config_dir/metadata/assets` directory.
#[inline]
pub async fn assets_dir(&self) -> PathBuf {
self.metadata_dir().await.join("assets")
}
/// Get the `config_dir/metadata/assets/indexes` directory.
#[inline]
pub async fn index_dir(&self) -> PathBuf {
self.assets_dir().await.join("indexes")
}
/// Get the `config_dir/metadata/assets/objects` directory.
#[inline]
pub async fn objects_dir(&self) -> PathBuf {
self.assets_dir().await.join("objects")
}
/// Get the `config_dir/metadata/assets/objects/{hash}` directory.
#[inline]
pub async fn object_dir(&self, hash: &str) -> PathBuf {
self.objects_dir().await.join(&hash[..2]).join(hash)
}
/// Get the `config_dir/clusters` directory.
#[inline]
pub async fn clusters_dir(&self) -> PathBuf {
self.config_dir.read().await.join("clusters")
}
/// Get the `config_dir/clusters/{uuid}` directory.
#[inline]
pub async fn cluster_dir(&self, uuid: uuid::Uuid) -> PathBuf {
self.clusters_dir().await.join(uuid.to_string())
}
/// Get the `{cluster_path}/logs` directory.
#[inline]
pub async fn cluster_logs_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
Ok(cluster_path.full_path().await?.join("logs"))
}
/// Get the `{cluster_path}/crash_reports` directory.
#[inline]
pub async fn crash_reports_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
Ok(cluster_path.full_path().await?.join("crash-reports"))
}
/// Get the `config_dir/metadata/resources` directory.
#[inline]
pub async fn legacy_assets_dir(&self) -> PathBuf {
self.metadata_dir().await.join("resources")
}
/// Get the `config_dir/metadata/natives` directory.
#[inline]
pub async fn natives_dir(&self) -> PathBuf {
self.metadata_dir().await.join("natives")
}
/// Get the `config_dir/metadata/natives/{version}` directory.
#[inline]
pub async fn version_natives_dir(&self, version: &str) -> PathBuf {
self.natives_dir().await.join(version)
}
/// Get the Minecraft `config_dir/metadata/versions` directory.
#[inline]
pub async fn versions_dir(&self) -> PathBuf {
self.metadata_dir().await.join("versions")
}
/// Get the Minecraft `config_dir/metadata/versions/{version}` directory.
#[inline]
pub async fn version_dir(&self, version: &str) -> PathBuf {
self.versions_dir().await.join(version)
}
/// Get the `config_dir/icons` directory.
#[inline]
pub async fn icons_dir(&self) -> PathBuf {
self.config_dir.read().await.join("icons")
}
}
/// Represents a core directory management error.
#[derive(Debug, thiserror::Error)]
pub enum DirectoryError {
/// An error for when a config directory cannot be initialized.
#[error("failed to get the user's config directory")]
ConfigDir,
/// A wrapper over [`std::io::Error`] for cwd fetching errors.
#[error("could not open working directory {0}")]
WorkingDir(std::io::Error),
}

View file

@ -1,162 +0,0 @@
//! Handles Discord rich prescence integration
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use discord_rich_presence::activity::{Activity, Assets, Button};
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use tokio::sync::RwLock;
use crate::constants::DISCORD_RPC_CLIENT_ID;
use crate::State;
/// A structure used to manage a Discord IPC client.
pub struct DiscordRPC {
/// The managed [`DiscordIpcClient`] used for this IPC client.
client: Arc<RwLock<DiscordIpcClient>>,
/// Whether or not we are connected to the IPC server and internet.
connected: Arc<AtomicBool>,
}
impl DiscordRPC {
/// Initialize the Discord IPC client and attempt to connect to the server.
/// If an error is returned, it will be ignored since Discord RPC isnt essential for the launcher to load.
pub fn initialize(is_offline: bool) -> crate::Result<Self> {
let mut discord_ipc = DiscordIpcClient::new(DISCORD_RPC_CLIENT_ID)
.map_err(|e| anyhow::anyhow!("failed to create discord client {}", e))?;
let connected = if is_offline {
Arc::new(AtomicBool::new(false))
} else {
let result = discord_ipc.connect();
if result.is_ok() {
Arc::new(AtomicBool::new(true))
} else {
Arc::new(AtomicBool::new(false))
}
};
let client = Arc::new(RwLock::new(discord_ipc));
Ok(Self { client, connected })
}
/// Set the Discord IPC activity with a message and try to reconnect if the initial connection fails.
pub async fn set_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> {
if !self.retry_online().await {
return Ok(());
}
let state = State::get().await?;
let settings = state.settings.read().await;
if settings.disable_discord {
Ok(self.clear_activity(true).await?)
} else {
Ok(self.apply_activity(msg, reconnect_if_fail).await?)
}
}
/// Do not use this function, use [`DiscordRPC#set_activity`]
pub async fn apply_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> {
if !self.retry_connection().await {
return Ok(());
}
let activity = Activity::new()
.state(msg)
.buttons(vec![Button::new("Download", "https://polyfrost.org/")])
.assets(
Assets::new()
.large_image("onelauncher_logo_512")
.large_text("OneLauncher"),
);
let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> =
self.client.write().await;
let result = client.set_activity(activity.clone());
let could_not_set = |e: Box<dyn serde::ser::StdError>| {
anyhow::anyhow!("failed to apply discord activity {}", e)
};
if reconnect_if_fail {
if let Err(_e) = result {
client
.reconnect()
.map_err(|e| anyhow::anyhow!("failed to reconnect to Discord RPC {}", e))?;
return Ok(client.set_activity(activity).map_err(could_not_set)?);
}
} else {
result.map_err(could_not_set)?;
}
Ok(())
}
/// Attempt to check if we are still in an offline environment.
pub async fn retry_online(&self) -> bool {
let Ok(state) = State::get().await else {
return false;
};
let offline = state.offline.read().await;
if *offline {
return false;
}
true
}
/// Retry the Discord IPC connection.
pub async fn retry_connection(&self) -> bool {
let mut client = self.client.write().await;
if !self.connected.load(std::sync::atomic::Ordering::Relaxed) {
if client.connect().is_ok() {
self.connected
.store(true, std::sync::atomic::Ordering::Relaxed);
return true;
}
return false;
}
true
}
/// Clear the current Discord IPC activity, reconnecting if requested.
pub async fn clear_activity(&self, reconnect_if_fail: bool) -> crate::Result<()> {
if !self.retry_online().await || !self.retry_connection().await {
return Ok(());
}
let mut client = self.client.write().await;
let result = client.clear_activity();
let could_not_clear = |e: Box<dyn serde::ser::StdError>| {
anyhow::anyhow!("failed to clear discord activity {}", e)
};
if reconnect_if_fail {
if result.is_err() {
client
.reconnect()
.map_err(|e| anyhow::anyhow!("failed to reconnect to Discord RPC {}", e))?;
return Ok(client.clear_activity().map_err(could_not_clear)?);
}
} else {
result.map_err(could_not_clear)?;
}
Ok(())
}
/// Clear and disable the Discord IPC, reconnecting if requested.
pub async fn clear(&self, reconnect_if_fail: bool) -> crate::Result<()> {
let state: Arc<tokio::sync::RwLockReadGuard<'_, State>> = State::get().await?;
{
let settings = state.settings.read().await;
if settings.disable_discord {
println!("discord is disabled.. clearing");
return self.clear_activity(true).await;
}
}
self.set_activity("Idling...", reconnect_if_fail).await?;
Ok(())
}
}

View file

@ -1,76 +0,0 @@
//! Handles all available Java installations
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::utils::java;
use crate::utils::java::JavaVersion;
/// A `HashMap` of all located and installed available Java versions
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JavaVersions(HashMap<String, JavaVersion>);
impl Default for JavaVersions {
fn default() -> Self {
Self::new()
}
}
impl JavaVersions {
/// Create an empty `JavaVersions` `HashMap`.
#[must_use]
pub fn new() -> Self {
Self(HashMap::new())
}
/// Inserts a key-value pair into the map.
pub fn insert(&mut self, key: String, java: JavaVersion) {
self.0.insert(key, java);
}
/// Removes a key from the map, returning the value at the key if the key was previously in the map.
pub fn remove(&mut self, key: &String) {
self.0.remove(key);
}
/// Returns a reference to the value corresponding to the key.
#[must_use]
pub fn get(&self, key: &String) -> Option<&JavaVersion> {
self.0.get(key)
}
/// Returns a mutable reference to the value corresponding to the key.
pub fn get_mut(&mut self, key: &String) -> Option<&mut JavaVersion> {
self.0.get_mut(key)
}
/// Returns the number of elements in the map.
#[must_use]
pub fn count(&self) -> usize {
self.0.len()
}
/// A collection visiting all keys in arbitrary order.
#[must_use]
pub fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
/// Validates all stored java versions.
pub async fn validate(&self) -> bool {
for java in self.0.values() {
let runtime = java::check_java_instance(PathBuf::from(&java.path).as_path()).await;
if let Some(runtime) = runtime {
if runtime.version != java.version {
return false;
}
} else {
return false;
}
}
true
}
}

View file

@ -1,263 +0,0 @@
//! Handles fetching metadata via interpulse.
use interpulse::api::minecraft::VersionManifest as MinecraftManifest;
use interpulse::api::modded::Manifest as ModdedManifest;
use serde::{Deserialize, Serialize};
use crate::constants::METADATA_API_URL;
use crate::utils::http::{fetch, read_json, write, FetchSemaphore, IoSemaphore};
use crate::State;
use super::Directories;
/// A structure of manifests and metadata fetching utilities.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Metadata {
/// The [`MinecraftManifest`] associated with core Minecraft versions.
pub minecraft: Option<MinecraftManifest>,
/// The [`ModdedManifest`] associated with core Fabric versions.
pub fabric: Option<ModdedManifest>,
/// The [`ModdedManifest`] associated with core Quilt versions.
pub quilt: Option<ModdedManifest>,
/// The [`ModdedManifest`] associated with core `NeoForge` versions.
pub neoforge: Option<ModdedManifest>,
/// The [`ModdedManifest`] associated with core Forge versions.
pub forge: Option<ModdedManifest>,
// /// The [`ModdedManifest`] associated with core Legacy Fabric versions.
// pub legacy_fabric: Option<ModdedManifest>,
}
impl Metadata {
/// Get the formatted manifest for a specific [`Metadata`] type.
fn get_manifest(name: &str, version: usize) -> String {
format!("{METADATA_API_URL}/{name}/v{version}/manifest.json")
}
/// Fetch all available metadata types that are currently [`None`] and get a new [`Metadata`] structure.
/// Useful for when reading from cache and a certain object not existing
pub async fn fetch_errored(&mut self, semaphore: &FetchSemaphore) -> crate::Result<bool> {
let mut did_change = false;
if self.minecraft.is_none() {
self.minecraft = Self::fetch_minecraft(semaphore).await;
if self.minecraft.is_some() {
did_change = true;
}
}
if self.fabric.is_none() {
self.fabric = Self::fetch_fabric(semaphore).await;
if self.fabric.is_some() {
did_change = true;
}
}
if self.quilt.is_none() {
self.quilt = Self::fetch_quilt(semaphore).await;
if self.quilt.is_some() {
did_change = true;
}
}
if self.neoforge.is_none() {
self.neoforge = Self::fetch_neoforge(semaphore).await;
if self.neoforge.is_some() {
did_change = true;
}
}
if self.forge.is_none() {
self.forge = Self::fetch_forge(semaphore).await;
if self.forge.is_some() {
did_change = true;
}
}
Ok(did_change)
}
/// Fetch all available metadata types and get a new [`Metadata`] structure.
pub async fn fetch(semaphore: &FetchSemaphore) -> crate::Result<Self> {
let (minecraft, fabric, quilt, neoforge, forge) = tokio::join! {
Self::fetch_minecraft(semaphore),
Self::fetch_fabric(semaphore),
Self::fetch_quilt(semaphore),
Self::fetch_neoforge(semaphore),
Self::fetch_forge(semaphore)
};
Ok(Self {
minecraft,
fabric,
quilt,
neoforge,
forge,
})
}
async fn fetch_minecraft(semaphore: &FetchSemaphore) -> Option<MinecraftManifest> {
let url = Self::get_manifest(
"minecraft",
interpulse::api::minecraft::CURRENT_FORMAT_VERSION,
);
fetch_version_manifest(Some(&url), semaphore).await.ok()
}
async fn fetch_fabric(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
let url = Self::get_manifest(
"fabric",
interpulse::api::modded::CURRENT_FABRIC_FORMAT_VERSION,
);
fetch_modded_manifest(&url, semaphore).await.ok()
}
async fn fetch_quilt(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
let url = Self::get_manifest(
"quilt",
interpulse::api::modded::CURRENT_QUILT_FORMAT_VERSION,
);
fetch_modded_manifest(&url, semaphore).await.ok()
}
async fn fetch_neoforge(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
let url = Self::get_manifest(
"neoforge",
interpulse::api::modded::CURRENT_NEOFORGE_FORMAT_VERSION,
);
fetch_modded_manifest(&url, semaphore).await.ok()
}
async fn fetch_forge(semaphore: &FetchSemaphore) -> Option<ModdedManifest> {
let url = Self::get_manifest(
"forge",
interpulse::api::modded::CURRENT_FORGE_FORMAT_VERSION,
);
fetch_modded_manifest(&url, semaphore).await.ok()
}
/// Initialize the core Metadata manager.
#[tracing::instrument(skip(io_semaphore, fetch_semaphore))]
#[onelauncher_macros::memory]
pub async fn initialize(
dirs: &Directories,
online: bool,
io_semaphore: &IoSemaphore,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Self> {
let mut metadata = None;
let path = dirs.caches_dir().await.join("metadata.json");
let backup = dirs.caches_dir().await.join("metadata.json.bak");
let mut should_write = false;
if let Ok(mut metadata_json) = read_json::<Self>(&path, io_semaphore).await {
should_write = metadata_json.fetch_errored(fetch_semaphore).await?;
metadata = Some(metadata_json);
} else if online {
let res = async {
let fetch_data = Self::fetch(fetch_semaphore).await?;
should_write = true;
metadata = Some(fetch_data);
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("failed to fetch metadata: {err}");
}
}
} else if let Ok(metadata_json) = read_json::<Self>(&backup, io_semaphore).await {
metadata = Some(metadata_json);
onelauncher_utils::io::copy(&backup, &path).await?;
}
if should_write {
write(
&path,
&serde_json::to_vec(&metadata).unwrap_or_default(),
io_semaphore,
)
.await?;
write(
&backup,
&serde_json::to_vec(&metadata).unwrap_or_default(),
io_semaphore,
)
.await?;
}
metadata.map_or_else(
|| Err(anyhow::anyhow!("failed to fetch launcher metadata").into()),
Ok,
)
}
/// Update and backup all available metadata.
pub async fn update() {
let res = async {
let state = State::get().await?;
let fetch_data = Self::fetch(&state.fetch_semaphore).await?;
let path = state.directories.caches_dir().await.join("metadata.json");
let backup = state
.directories
.caches_dir()
.await
.join("metadata.json.bak");
if path.exists() {
onelauncher_utils::io::copy(&path, &backup).await?;
}
write(
&path,
&serde_json::to_vec(&fetch_data)?,
&state.io_semaphore,
)
.await?;
let mut old_metadata = state.metadata.write().await;
*old_metadata = fetch_data;
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("failed to update launcher metadata: {err}");
}
};
}
}
async fn fetch_version_manifest(
url: Option<&str>,
semaphore: &FetchSemaphore,
) -> crate::Result<MinecraftManifest> {
let url = url.unwrap_or(interpulse::api::minecraft::VERSION_MANIFEST_URL);
Ok(serde_json::from_slice(
&fetch(url, None, semaphore).await.inspect_err(|_| {
tracing::error!("couldn't fetch version manifest at '{}'", url);
})?,
)?)
}
async fn fetch_modded_manifest(
url: &str,
semaphore: &FetchSemaphore,
) -> crate::Result<ModdedManifest> {
Ok(serde_json::from_slice(
&fetch(url, None, semaphore).await.inspect_err(|_| {
tracing::error!("couldn't fetch modded manifest at '{}'", url);
})?,
)?)
}

File diff suppressed because it is too large Load diff

View file

@ -1,289 +0,0 @@
//! **`OneLauncher` Store**
//!
//! Core state and storage management for the launcher, managing all states.
use std::sync::Arc;
use std::time::Duration;
use notify::RecommendedWatcher;
use notify_debouncer_mini::Debouncer;
use tokio::sync::{OnceCell, RwLock, Semaphore};
use crate::proxy::send::{init_ingress_internal, send_ingress, send_offline};
use crate::utils::http;
use crate::utils::http::{FetchSemaphore, IoSemaphore};
mod dirs;
pub use self::dirs::*;
mod minecraft;
pub use self::minecraft::*;
mod credentials;
pub use self::credentials::*;
mod discord;
pub use self::discord::*;
mod metadata;
pub use self::metadata::*;
mod settings;
pub use self::settings::*;
mod java_versions;
pub use self::java_versions::*;
mod processor;
pub use self::processor::*;
mod clusters;
pub use self::clusters::*;
mod package;
pub use self::package::*;
mod oneconfig;
pub use self::oneconfig::*;
/// The static [`OnceCell<RwLock<State>>`] for storing the global runtime launcher state.
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
/// The public `OneLauncher` state structure.
pub struct State {
/// Handles a boolean to check if the launcher is connected to the internet
pub offline: RwLock<bool>,
/// Handles core launcher directory and files
pub directories: Directories,
/// Semaphore used to handle concurrent network requests
pub fetch_semaphore: FetchSemaphore,
/// Maximum number of semaphore permits to limit the amount of network requests at one time
pub fetch_semaphore_max: RwLock<u32>,
/// Semaphore used to handle concurrent IO requests
pub io_semaphore: IoSemaphore,
/// Maximum number of semaphore permits to limit the amount of IO requests at one time
pub io_semaphore_max: RwLock<u32>,
/// Handles launcher metadata via [`interpulse`]
pub metadata: RwLock<Metadata>,
/// Handles launcher configuration and settings
pub settings: RwLock<Settings>,
/// Handles minecraft auth flows and users
pub users: RwLock<MinecraftState>,
/// Handles processes
pub processor: RwLock<Processor>,
/// Handles `OneConfig` integration with [`async-tungstenite`]
pub oneconfig: RwLock<Option<OneConfig>>,
/// Handles secure credential management with [`iota_stronghold`].
pub credentials: RwLock<Option<Credentials>>,
/// Handles clusters
pub(crate) clusters: RwLock<Clusters>,
/// Handles packages
pub(crate) packages: RwLock<Packages>,
/// Handles internal ingress processes
pub(crate) ingress_processor: RwLock<IngressProcessor>,
/// Handles file system watching for cluster manager
pub(crate) watcher: RwLock<Debouncer<RecommendedWatcher>>,
/// Handles Discord rich prescence
pub discord_rpc: DiscordRPC,
}
impl State {
/// Get the current global launcher state (or initialize it)
pub async fn get() -> crate::Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
Ok(Arc::new(
LAUNCHER_STATE
.get_or_try_init(Self::initialize)
.await?
.read()
.await,
))
}
/// Get and lock writing to the current global launcher state (or initalize it)
pub async fn get_and_write() -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
Ok(LAUNCHER_STATE
.get_or_try_init(Self::initialize)
.await?
.write()
.await)
}
/// Initializes the core OneLauncher state fully.
#[tracing::instrument]
#[onelauncher_macros::memory]
async fn initialize() -> crate::Result<RwLock<Self>> {
let ingress =
init_ingress_internal(crate::IngressType::Initialize, 100.0, "initializing state")
.await?;
let settings = Settings::initialize(&Directories::init_settings_file()?).await?;
let directories = Directories::initalize(&settings)?;
send_ingress(&ingress, 10.0, None).await?;
let mut watcher = crate::utils::watcher::initialize_watcher()?;
let fetch_semaphore =
FetchSemaphore(RwLock::new(Semaphore::new(settings.max_async_fetches)));
let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new(
settings.max_async_io_operations,
)));
send_ingress(&ingress, 10.0, None).await?;
let is_offline = !http::check_internet_connection(3).await;
// TODO: Make this run in the background, this delays launcher startup by a couple seconds
let metadata_in =
Metadata::initialize(&directories, !is_offline, &io_semaphore, &fetch_semaphore);
let clusters_in = Clusters::initialize(&directories, &mut watcher);
let users_in = MinecraftState::initialize(&directories, &io_semaphore);
let (metadata, clusters, users) = crate::ingress_join! {
Some(&ingress), 70.0, Some("loading core");
metadata_in,
clusters_in,
users_in,
}?;
send_ingress(&ingress, 5.0, Some("loading packages")).await?;
let packages = Packages::initialize(&clusters);
let ingress_processor = IngressProcessor::new();
let discord_rpc = DiscordRPC::initialize(is_offline || settings.disable_discord)?;
if !settings.disable_discord && !is_offline {
let _ = discord_rpc.apply_activity("Idling...", true).await;
}
let processor = Processor::new();
Self::status_loop();
send_ingress(&ingress, 10.0, None).await?;
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
offline: RwLock::new(is_offline),
directories,
fetch_semaphore,
fetch_semaphore_max: RwLock::new(settings.max_async_fetches as u32),
io_semaphore,
io_semaphore_max: RwLock::new(settings.max_async_io_operations as u32),
metadata: RwLock::new(metadata),
settings: RwLock::new(settings),
users: RwLock::new(users),
processor: RwLock::new(processor),
oneconfig: RwLock::new(None),
credentials: RwLock::new(None),
clusters: RwLock::new(clusters),
packages: RwLock::new(packages),
ingress_processor: RwLock::new(ingress_processor),
watcher: RwLock::new(watcher),
discord_rpc,
}))
}
/// Initalizes a "game loop" which checks for updates and if we are online.
pub fn status_loop() {
tokio::task::spawn(async {
loop {
let state = Self::get().await;
if let Ok(state) = state {
let _ = state.check_status().await;
}
tokio::time::sleep(Duration::from_secs(10)).await;
}
});
}
/// Updates all data if we are connected to the internet.
pub fn update() {
tokio::task::spawn(async {
let state = Self::get();
if let Ok(state) = state.await {
if !*state.offline.read().await {
let version_up = Clusters::update_versions();
let meta_up = Metadata::update();
let package_up = Clusters::update_packages();
let _ = tokio::join!(version_up, meta_up, package_up);
}
}
});
}
/// Synchronizes data that can change outside of our control.
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn sync() -> crate::Result<()> {
let state = Self::get().await?;
let sync_settings = async {
let state = Arc::clone(&state);
tokio::spawn(async move {
let reader = state.settings.read().await;
reader.sync(&state.directories.settings_file()).await?;
Ok::<_, crate::Error>(())
})
.await?
};
let sync_clusters = async {
let state = Arc::clone(&state);
tokio::spawn(async move {
let clusters = state.clusters.read().await;
clusters.sync().await?;
Ok::<_, crate::Error>(())
})
.await?
};
tokio::try_join!(sync_settings, sync_clusters)?;
Ok(())
}
/// Resets the IO [`Semaphore`] to clear all tasks.
/// # This should ONLY be called when we can ensure
/// # that there are no current tasks running!!
pub async fn reset_io_semaphore(&self) {
let settings = self.settings.read().await;
let mut io_semaphore = self.io_semaphore.0.write().await;
let mut total_permits = self.io_semaphore_max.write().await;
// block ALL semaphore permits - this can be destructive
let _ = io_semaphore.acquire_many(*total_permits).await;
io_semaphore.close();
*total_permits = settings.max_async_io_operations as u32;
*io_semaphore = Semaphore::new(settings.max_async_io_operations);
}
/// Resets the Fetch [`Semaphore`] to clear all tasks.
/// # This should ONLY be called when we can ensure
/// # that there are no current tasks running!!
pub async fn reset_fetch_semaphore(&self) {
let settings = self.settings.read().await;
let mut fetch_semaphore = self.fetch_semaphore.0.write().await;
let mut total_permits = self.fetch_semaphore_max.write().await;
// block ALL semaphore permits - this can be destructive
let _ = fetch_semaphore.acquire_many(*total_permits).await;
fetch_semaphore.close();
*total_permits = settings.max_async_io_operations as u32;
*fetch_semaphore = Semaphore::new(settings.max_async_fetches);
}
/// Checks if we are online used in a loop.
pub async fn check_status(&self) -> crate::Result<()> {
let is_online = http::check_internet_connection(3).await;
let mut offline = self.offline.write().await;
if *offline != is_online {
return Ok(());
}
send_offline(!is_online).await?;
*offline = !is_online;
Ok(())
}
/// Determine if the global state is initalized or not (wrapper)
pub fn initalized() -> bool {
LAUNCHER_STATE.initialized()
}
}

View file

@ -1,39 +0,0 @@
//! `OneConfig` state using [`async_tungstenite`] to communicate to the `OneConfig` server
use super::ClusterPath;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Wrapper over an [`async_tungstenite::WebSocketStream`] to connect to the `OneConfig` server.
pub struct OneConfig {
/// The public tokio socket connection.
pub socket: async_tungstenite::WebSocketStream<async_tungstenite::tokio::ConnectStream>,
}
/// A WebSocket packet while updating a mod to display a GUI in game.
#[derive(Serialize, Deserialize, Debug)]
pub struct ModUpdatePacket {
/// The ID of the packet.
id: Uuid,
/// The ID of the mod being updated.
mod_id: Uuid,
/// The [`ClusterPath`] associated with this event.
cluster_path: ClusterPath,
}
impl OneConfig {
/// Initializes a `OneConfig` socket connection on localhost port `4023`.
pub async fn initialize(cluster: ClusterPath) -> crate::Result<Self> {
let (socket, _) = async_tungstenite::tokio::connect_async(format!(
"wss://localhost:4023/oneconfig/ws?cluster={cluster}"
))
.await?;
Ok(Self { socket })
}
pub async fn close(&mut self) -> crate::Result<()> {
self.socket.close(None).await?;
Ok(())
}
}

View file

@ -1,969 +0,0 @@
//! Handlers for Mod metadata that can be displayed in a GUI mod list or exported as a mod pack
use crate::package::content::Providers;
use crate::proxy::send::{init_ingress, send_ingress};
use crate::proxy::IngressId;
use crate::store::Loader;
use crate::utils::crypto;
use crate::utils::http::{write_icon, IoSemaphore};
use crate::State;
use async_zip::tokio::read::fs::ZipFileReader;
use chrono::{DateTime, Utc};
use onelauncher_utils::io;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::vec;
use tokio::fs::DirEntry;
use super::{ClusterPath, Clusters, Directories, PackagePath};
/// Represents types of packages handled by the launcher.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum PackageType {
/// Represents a mod jarfile
Mod,
/// Represents a datapack file
DataPack,
/// Represents a resourcepack file
ResourcePack,
/// Represents a shaderpack file
#[serde(alias = "shader")]
ShaderPack,
/// Represents a modpack file
ModPack,
}
impl PackageType {
/// attempt to get a [`PackageType`] from a loader string.
#[must_use]
pub fn from_loader(loaders: &[String]) -> Option<Self> {
if loaders
.iter()
.any(|x| ["fabric", "forge", "quilt"].contains(&&**x))
{
Some(Self::Mod)
} else if loaders.iter().any(|x| x == "datapack") {
Some(Self::DataPack)
} else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) {
Some(Self::ShaderPack)
} else if loaders
.iter()
.any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x))
{
Some(Self::ResourcePack)
} else {
None
}
}
#[must_use]
pub fn from_parent(path: &Path) -> Option<Self> {
let path = path.parent()?.file_name()?;
match path.to_str()? {
"mods" => Some(Self::Mod),
"datapacks" => Some(Self::DataPack),
"resourcepacks" => Some(Self::ResourcePack),
"shaderpacks" => Some(Self::ShaderPack),
"modpack" => Some(Self::ModPack),
_ => None,
}
}
#[must_use]
pub const fn get_name(&self) -> &'static str {
match self {
Self::Mod => "mod",
Self::DataPack => "datapack",
Self::ResourcePack => "resourcepack",
Self::ShaderPack => "shader",
Self::ModPack => "modpack",
}
}
#[must_use]
pub const fn get_folder(&self) -> &'static str {
match self {
Self::Mod => "mods",
Self::DataPack => "datapacks",
Self::ResourcePack => "resourcepacks",
Self::ShaderPack => "shaderpacks",
Self::ModPack => "modpack",
}
}
#[must_use]
pub fn get_meta(&self) -> PathBuf {
PathBuf::from(format!(
"{}/{}",
self.get_folder(),
self.get_meta_file_name()
))
}
#[must_use]
pub fn get_meta_file_name(&self) -> String {
String::from(".packages.json")
}
pub async fn file_matches(&self, entry: &DirEntry) -> crate::Result<bool> {
if !(entry.path().try_exists()?) {
return Ok(false);
}
Ok(match self {
Self::Mod => {
entry.file_type().await?.is_file()
&& entry.path().extension() == Some("jar".as_ref())
}
_ => false,
})
}
pub fn iterator() -> impl Iterator<Item = Self> {
[
Self::Mod,
Self::DataPack,
Self::ResourcePack,
Self::ShaderPack,
Self::ModPack,
]
.iter()
.copied()
}
}
impl Display for PackageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.get_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct SearchResult {
pub slug: String,
pub title: String,
pub description: String,
#[serde(default)]
pub categories: Vec<String>,
pub client_side: PackageSide,
pub server_side: PackageSide,
pub project_type: PackageType,
pub downloads: u64,
#[serde(default)]
pub icon_url: String,
pub project_id: String,
pub author: String,
pub versions: Vec<String>,
pub follows: u32,
pub date_created: Option<DateTime<Utc>>,
pub date_modified: Option<DateTime<Utc>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct ProviderSearchResults {
pub provider: Providers,
pub results: Vec<SearchResult>,
pub total: u32,
}
/// A struct that represents a Package.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct Package {
pub sha1: String,
pub meta: PackageMetadata,
pub file_name: String,
pub disabled: bool,
}
impl Package {
pub fn new(path: &PathBuf, meta: PackageMetadata) -> crate::Result<Self> {
let file_name = path.file_name()
.ok_or_else(|| anyhow::anyhow!("couldn't get filename of path {path:?}"))?
.to_string_lossy()
.to_string();
let sha1 = crypto::sha1_file(path)?;
Ok(Self {
sha1,
meta,
file_name,
disabled: false,
})
}
}
pub type PackagesMap = HashMap<PackagePath, Package>;
pub struct Packages {
managers: HashMap<ClusterPath, PackageManager>,
}
impl Packages {
#[must_use]
pub fn initialize(clusters: &Clusters) -> Self {
let mut this = Self { managers: HashMap::new() };
// TODO: This should probably not clone and store the cluster path in like 2 areas
for cluster_path in clusters.0.keys() {
this.add_cluster(cluster_path.clone());
}
this
}
pub fn add_cluster(&mut self, cluster_path: ClusterPath) {
let mgr = PackageManager::new(cluster_path.clone());
self.managers.insert(cluster_path, mgr);
}
#[must_use]
pub fn get(&self, cluster_path: &ClusterPath) -> Option<&PackageManager> {
self.managers.get(cluster_path)
}
pub fn get_mut(&mut self, cluster_path: &ClusterPath) -> Option<&mut PackageManager> {
self.managers.get_mut(cluster_path)
}
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackagesMeta {
pub packages: PackagesMap,
pub package_type: PackageType,
}
impl PackagesMeta {
#[must_use]
pub fn new(package_type: PackageType) -> Self {
Self {
packages: PackagesMap::new(),
package_type,
}
}
}
#[derive(Debug)]
pub struct PackageManager {
pub cluster_path: ClusterPath,
synced: bool,
modpacks: PackagesMeta,
mods: PackagesMeta,
datapacks: PackagesMeta,
resourcepacks: PackagesMeta,
shaderpacks: PackagesMeta,
}
impl PackageManager {
#[must_use]
pub fn new(cluster_path: ClusterPath) -> Self {
Self {
cluster_path,
modpacks: PackagesMeta::new(PackageType::ModPack),
synced: false,
mods: PackagesMeta::new(PackageType::Mod),
datapacks: PackagesMeta::new(PackageType::DataPack),
resourcepacks: PackagesMeta::new(PackageType::ResourcePack),
shaderpacks: PackagesMeta::new(PackageType::ShaderPack),
}
}
pub async fn get_meta_file(
&self,
dirs: &Directories,
package_type: PackageType,
) -> crate::Result<PathBuf> {
Ok(self
.cluster_path
.full_path_dirs(dirs)
.await?
.join(package_type.get_meta()))
}
#[async_recursion::async_recursion]
async fn get_and_sync(&mut self, dirs: Option<&Directories>) -> &mut Self {
if !self.synced {
self.synced = true; // early set to prevent infinite loop
let state = State::get().await.expect("couldn't get state when syncing packages");
let dirs = dirs.map_or_else(|| &state.directories, |dirs| dirs);
self.sync_packages(dirs).await;
}
self
}
/// Get the `PackagesMeta` for a specific package type. Syncs.
#[must_use]
pub async fn get(&mut self, package_type: PackageType) -> &PackagesMeta {
self.get_and_sync(None).await;
match package_type {
PackageType::Mod => &self.mods,
PackageType::DataPack => &self.datapacks,
PackageType::ResourcePack => &self.resourcepacks,
PackageType::ShaderPack => &self.shaderpacks,
PackageType::ModPack => &self.modpacks,
}
}
/// Get the `PackagesMeta` for a specific package type. Does not sync.
fn get_mut(&mut self, package_type: PackageType) -> &mut PackagesMeta {
match package_type {
PackageType::Mod => &mut self.mods,
PackageType::ModPack => &mut self.modpacks,
PackageType::DataPack => &mut self.datapacks,
PackageType::ResourcePack => &mut self.resourcepacks,
PackageType::ShaderPack => &mut self.shaderpacks,
}
}
/// Get the PackagesMeta for a specific package type. Does not sync.
#[tracing::instrument]
async fn get_from_file(
&self,
dirs: &Directories,
package_type: PackageType,
) -> crate::Result<PackagesMeta> {
let packages_meta = &self.get_meta_file(dirs, package_type).await?;
let meta: PackagesMeta = if packages_meta.exists() && packages_meta.is_file() {
if let Ok(meta) = serde_json::from_slice(&io::read(packages_meta).await?) {
meta
} else {
io::copy(packages_meta, packages_meta.with_extension(".json.bak")).await?;
io::write(packages_meta, "{}").await?;
PackagesMeta::new(package_type)
}
} else {
io::write(packages_meta, "{}").await?;
PackagesMeta::new(package_type)
};
Ok(meta)
}
// add a package to the manager
#[tracing::instrument(skip(self, package))]
pub async fn add_package(
&mut self,
package_path: PackagePath,
package: Package,
) -> crate::Result<()> {
let package_type = package_path.package_type()?;
let packages = &mut self.get_mut(package_type).packages;
insert_package_by_path(packages, package_path, package);
Ok(())
}
// remove a package to the manager
#[tracing::instrument(skip(self))]
pub async fn remove_package(
&mut self,
package_path: &PackagePath,
) -> crate::Result<()> {
let package_type = package_path.package_type()?;
let full_path = package_path.full_path(&self.cluster_path).await?;
io::remove_file(full_path).await?;
let packages = &mut self.get_mut(package_type).packages;
packages.remove(package_path);
Ok(())
}
// disable a package in the manager
#[tracing::instrument(skip(self))]
pub async fn set_package_enabled(
&mut self,
package_path: &PackagePath,
enabled: bool,
) -> crate::Result<()> {
let state = State::get().await?;
let package_type = package_path.package_type()?;
let full_path = package_path.full_path(&self.cluster_path).await?;
let packages = &mut self.get_mut(package_type).packages;
let was_disabled = full_path.extension().map_or(false, |e| e == "disabled");
let mut new_path = full_path.clone();
let changed = if enabled && was_disabled {
new_path.set_extension("") // removes .disabled
} else if !enabled && !was_disabled {
new_path.set_extension("jar.disabled") // adds .disabled
} else {
false
};
if changed {
io::rename(full_path, new_path.clone()).await?;
if let Some(mut package) = packages.remove(package_path) {
let file_name = new_path.file_name().ok_or_else(|| anyhow::anyhow!("couldn't get file name"))?.to_string_lossy().to_string();
let package_path = PackagePath::new(package_type.clone(), file_name.clone())?;
package.disabled = !enabled;
package.file_name = file_name;
packages.insert(package_path, package);
}
self.sync_to_file_by_type(&state.directories, package_type).await?;
}
Ok(())
}
/// sync all packages
#[tracing::instrument(skip(self, dirs))]
pub async fn sync_packages(&mut self, dirs: &Directories) {
tracing::info!("syncing packages for cluster {}", self.cluster_path);
let package_types = vec![
PackageType::Mod,
PackageType::DataPack,
PackageType::ResourcePack,
PackageType::ShaderPack,
];
for package_type in package_types {
if let Err(err) = &self.sync_packages_by_type(dirs, package_type, None).await {
tracing::error!("failed to sync package {}: {}", package_type.get_name(), err);
}
}
}
/// sync packages from the metafile
#[tracing::instrument(skip(self, dirs))]
async fn sync_from_file_by_type(
&mut self,
dirs: &Directories,
package_type: PackageType,
) -> crate::Result<()> {
let packages_meta = self.get_from_file(dirs, package_type).await?;
self.get_mut(package_type).packages = packages_meta.packages;
Ok(())
}
/// sync packages from the manager to the metafile
#[tracing::instrument(skip(self, dirs))]
async fn sync_to_file_by_type(
&mut self,
dirs: &Directories,
package_type: PackageType,
) -> crate::Result<()> {
let packages_meta = self.get(package_type).await;
let packages_meta = serde_json::to_string(&packages_meta)?;
io::write(self.get_meta_file(dirs, package_type).await?, packages_meta).await?;
Ok(())
}
/// returns a list of packages that have a file but are not synced in the manager
#[tracing::instrument(skip(self, dirs, packages))]
async fn sync_packages_from_local(
&self,
dirs: &Directories,
packages: &mut PackagesMap,
package_type: PackageType,
) -> crate::Result<()> {
let mut files = io::read_dir(
self.cluster_path
.full_path_dirs(dirs)
.await?
.join(package_type.get_folder()),
)
.await?;
let mut packages_to_keep = HashSet::<PackagePath>::new();
let mut packages_to_infer = HashSet::<PathBuf>::new();
let full_cluster_path = self.cluster_path.full_path().await?;
while let Some(file) = files.next_entry().await? {
// Skip .packages.json meta file
if file.file_name().to_string_lossy().starts_with(&".") || file.file_type().await?.is_dir()
{
continue;
}
let package_path = PackagePath::from_path(&file.path()).await?;
// Check if the file is in the packages list already
if packages.get(&package_path).is_none() {
// Package path is not in the packages list but exists on file system, lets try to infer it later on
packages_to_infer.insert(full_cluster_path.join(package_path.relative_path()));
}
// Add the package to the set to not remove it from the manager
packages_to_keep.insert(package_path);
}
// Infer any packages
if !packages_to_infer.is_empty() {
let ingress = init_ingress(crate::IngressType::InferPackages, 300.0, format!("inferring {package_type}s").as_str()).await?;
if let Some(not_found) = infer_provider(&ingress, Providers::Modrinth, package_type, packages, packages_to_infer, crypto::sha1_file).await {
if let Some(not_found) = infer_provider(&ingress, Providers::SkyClient, package_type, packages, not_found, crypto::md5_file).await {
if let Some(not_found) = infer_provider(&ingress, Providers::Curseforge, package_type, packages, not_found, |path| {
let hash = crypto::murmur2_file(path)?;
Ok(hash.to_string())
}).await {
for path in not_found {
tracing::warn!("failed to infer package: {:?}", path);
let meta = PackageMetadata::Unknown;
let package = Package::new(&path, meta)?;
let package_path = PackagePath::from_path(&path).await?;
insert_package_by_path(packages, package_path, package);
}
}
}
}
let _ = send_ingress(&ingress, 300.0, Some(format!("finished inferring {package_type}s").as_str())).await;
}
if packages.len() != packages_to_keep.len() {
packages.retain(|pkg_path, _| packages_to_keep.contains(pkg_path));
}
Ok(())
}
// sync packages in a cluster
#[tracing::instrument(skip(self, dirs))]
pub async fn sync_packages_by_type(
&mut self,
dirs: &Directories,
package_type: PackageType,
clear: Option<bool>,
) -> crate::Result<()> {
if clear.unwrap_or(false) {
self.get_mut(package_type).packages.clear();
}
// Clone the current packages and merge them with the local package list
let mut packages = self.get(package_type).await.packages.clone();
let mut new_packages = self.get_from_file(dirs, package_type).await?.packages;
packages.extend(new_packages.drain());
// Sync the packages from FS
self.sync_packages_from_local(dirs, &mut packages, package_type).await?;
// Finally store the new list in memory and on disk
let stored_packages = &self.get_mut(package_type).packages;
if stored_packages.len() != packages.len() {
self.get_mut(package_type).packages = packages;
self.sync_to_file_by_type(dirs, package_type).await?;
}
Ok(())
}
}
fn insert_package_by_path(packages: &mut PackagesMap, path: PackagePath, mut package: Package) {
if let Ok(file_name) = path.file_name() {
let is_disabled = file_name.ends_with(".disabled");
if is_disabled {
package.disabled = true;
}
}
packages.insert(path, package);
}
#[tracing::instrument(skip(packages, to_infer))]
async fn infer_provider(
ingress: &IngressId,
provider: Providers,
package_type: PackageType,
packages: &mut PackagesMap,
to_infer: HashSet<PathBuf>,
hash_fn: fn(&PathBuf) -> crate::Result<String>,
) -> Option<HashSet<PathBuf>> {
let _ = send_ingress(ingress, 5.0, Some(format!("inferring {package_type}s from {provider}").as_str())).await;
let mut hash_map = HashMap::<String, PathBuf>::new();
for path in &to_infer {
if let Ok(hash) = hash_fn(path) {
hash_map.insert(hash, path.clone());
}
}
let hash_len = hash_map.len();
let _ = send_ingress(ingress, 25.0, None).await;
match provider.get_versions_by_hashes(hash_map.iter().map(|hash| hash.0.clone()).collect()).await {
Ok(versions) => {
let found_all = (hash_len - versions.len()) == 0;
let havent_found = if found_all {
None
} else {
Some(hash_map.iter().filter_map(|(hash, path)| {
if versions.contains_key(hash) {
None
} else {
Some(path.clone())
}
}).collect::<HashSet<PathBuf>>())
};
let package_ids = versions.iter().map(|(hash, version)| (version.package_id.clone(), hash.clone())).collect::<HashMap<String, String>>();
let _ = send_ingress(ingress, 25.0, None).await;
match provider.get_multiple(package_ids.clone().into_keys().collect::<Vec<String>>().as_slice()).await {
Ok(managed_packages) => {
for managed in managed_packages {
let Some(hash) = package_ids.get(&managed.id) else { continue };
let Some(version) = versions.get(hash) else { continue; };
if let Some(path) = hash_map.get(hash) {
let meta = PackageMetadata::Managed {
package_id: managed.id,
provider: provider.clone(),
package_type,
title: managed.title,
version_id: version.id.clone(),
version_formatted: version.version_display.clone(),
mc_versions: Some(version.game_versions.clone()),
icon_url: managed.icon_url
};
let Ok(package_path) = PackagePath::from_path(path).await else {
tracing::error!("failed to create package path for {:?}", path);
continue;
};
match Package::new(path, meta) {
Ok(package) => {
insert_package_by_path(packages, package_path, package);
},
Err(err) => {
tracing::error!("failed to create package from {}: {}", provider, err);
},
};
}
}
},
Err(err) => {
tracing::error!("failed to get versions from {}: {}", provider, err);
}
}
let _ = send_ingress(ingress, 45.0, Some(format!("infered {package_type}s from {provider}").as_str())).await;
return havent_found;
},
Err(err) => {
tracing::error!("failed to get versions hashes from {}: {}", provider, err);
}
};
let _ = send_ingress(ingress, 70.0, Some(format!("failed to infer {package_type}s from {provider}").as_str())).await;
Some(to_infer)
}
/// Metadata that represents a [`Package`].
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PackageMetadata {
Managed {
package_id: String,
provider: Providers,
package_type: PackageType,
title: String,
version_id: String,
version_formatted: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
mc_versions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
icon_url: Option<String>,
},
Unknown,
}
impl PackageMetadata {
#[must_use]
pub fn from_managed_package(package: ManagedPackage, version: ManagedVersion) -> Self {
Self::Managed {
package_id: package.id,
version_id: version.id,
version_formatted: version.version_display,
title: package.title,
provider: package.provider,
package_type: package.package_type,
mc_versions: Some(version.game_versions),
icon_url: package.icon_url,
}
}
#[must_use]
pub const fn get_package_type(&self) -> Option<PackageType> {
match self {
Self::Managed { package_type, .. } => Some(*package_type),
Self::Unknown => None,
}
}
}
/// Universal metadata for any managed user from a Mod distribution platform.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ManagedUser {
pub id: String,
pub username: String,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub avatar_url: Option<String>,
#[serde(default)]
pub bio: Option<String>,
#[serde(default = "default_is_organization_user")]
pub is_organization_user: bool,
#[serde(default)]
pub role: Option<String>,
}
const fn default_is_organization_user() -> bool {
false
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum Author {
Team {
team: String,
organization: Option<String>,
},
Users(Vec<ManagedUser>),
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum PackageBody {
Url(String),
Markdown(String),
}
/// Universal metadata for any managed package from a Mod distribution platform.
#[serde_with::serde_as]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ManagedPackage {
// Core Metadata
pub provider: Providers,
pub id: String,
pub package_type: PackageType,
pub title: String,
pub description: String,
pub body: PackageBody,
pub main: String,
pub versions: Vec<String>,
pub game_versions: Vec<String>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub loaders: Vec<Loader>,
pub icon_url: Option<String>,
pub created: Option<DateTime<Utc>>,
pub updated: Option<DateTime<Utc>>,
pub client: PackageSide,
pub server: PackageSide,
pub downloads: u64,
pub followers: u32,
pub categories: Vec<String>,
pub optional_categories: Option<Vec<String>>,
pub license: Option<License>,
pub author: Author,
pub is_archived: bool,
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct License {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub url: Option<String>,
}
/// Universal managed package version of a package.
#[serde_with::serde_as]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ManagedVersion {
pub id: String,
pub package_id: String,
pub author: String,
pub name: String,
pub featured: bool,
pub version_display: String,
pub changelog: String,
pub changelog_url: Option<String>,
pub published: Option<DateTime<Utc>>,
pub downloads: u32,
pub version_type: ManagedVersionReleaseType,
pub files: Vec<ManagedVersionFile>,
pub is_available: bool,
pub deps: Vec<ManagedDependency>,
pub game_versions: Vec<String>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub loaders: Vec<Loader>,
}
impl ManagedVersion {
#[must_use]
pub fn get_primary_file(&self) -> Option<&ManagedVersionFile> {
self.files.iter().find(|f| f.primary)
}
}
/// Universal version release type
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub enum ManagedVersionReleaseType {
#[default]
Release,
Alpha,
Beta,
Snapshot,
}
impl From<String> for ManagedVersionReleaseType {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"alpha" => Self::Alpha,
"beta" => Self::Beta,
"snapshot" => Self::Snapshot,
_ => Self::Release,
}
}
}
impl Display for ManagedVersionReleaseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Release => "Release",
Self::Alpha => "Alpha",
Self::Beta => "Beta",
Self::Snapshot => "Snapshot",
})
}
}
/// Universal interface for managed package files.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct ManagedVersionFile {
pub url: String,
pub file_name: String,
pub primary: bool,
pub size: u64,
pub file_type: Option<PackageFile>,
pub hashes: HashMap<String, String>,
}
/// Universal interface for managed package dependencies.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ManagedDependency {
pub version_id: Option<String>,
pub package_id: Option<String>,
pub file_name: Option<String>,
pub dependency_type: PackageDependency,
}
/// The type of a [`ManagedDependency`].
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum PackageDependency {
Required,
Optional,
Incompatible,
Embedded,
}
/// The Client/Server side type of a [`Package`].
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub enum PackageSide {
Required,
Optional,
Unsupported,
Unknown,
}
/// The file type of a [`Package`].
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PackageFile {
RequiredPack,
OptionalPack,
Unknown,
}
#[tracing::instrument(skip(io_semaphore))]
#[onelauncher_macros::memory]
pub async fn read_icon(
icon_path: Option<String>,
cache_dir: &Path,
path: &PathBuf,
io_semaphore: &IoSemaphore,
) -> crate::Result<Option<PathBuf>> {
if let Some(icon_path) = icon_path {
let zip_reader = ZipFileReader::new(path).await;
if let Ok(zip_reader) = zip_reader {
let zip_idx_res = zip_reader
.file()
.entries()
.iter()
.position(|f| f.filename().as_str().unwrap_or_default() == icon_path);
if let Some(zip_idx) = zip_idx_res {
let mut bytes = Vec::new();
if zip_reader
.reader_with_entry(zip_idx)
.await?
.read_to_end_checked(&mut bytes)
.await
.is_ok()
{
let bytes = bytes::Bytes::from(bytes);
let path = write_icon(&icon_path, cache_dir, bytes, io_semaphore).await?;
return Ok(Some(path));
}
}
}
}
Ok(None)
}

View file

@ -1,762 +0,0 @@
//! Handles child processes in a unified API
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::process::Stdio;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use uuid::Uuid;
use crate::api::cluster;
use crate::constants::PROCESSOR_FILE;
use crate::proxy::send::{send_message, send_process};
use crate::proxy::ProcessPayloadType;
use crate::utils::http::read_json;
use crate::State;
use onelauncher_utils::io::IOError;
use super::{Cluster, ClusterPath};
/// Wrapper over a `HashMap` of PIDs to `ProcessorChildren` and unified apis
pub struct Processor(HashMap<Uuid, Arc<RwLock<ProcessorChild>>>);
/// Manager for safe processes like ingress feeds which need to be safely shut down.
pub struct IngressProcessor {
/// A vector of ingress feed uuids.
pub ingress_feeds: Vec<Uuid>,
}
/// A type of Ingress safety process.
#[derive(Debug, Copy, Clone)]
pub enum IngressProcessType {
/// A single Ingress feed.
IngressFeed,
// Writes,
// Reads,
// Fetch,
}
impl Default for IngressProcessor {
fn default() -> Self {
Self::new()
}
}
impl IngressProcessor {
/// Initialize a new ingresss process manager.
#[must_use]
pub const fn new() -> Self {
Self {
ingress_feeds: Vec::new(),
}
}
/// Add an ingress safety process to the `HashMap`.
pub async fn add_ingress(r#type: IngressProcessType, uuid: Uuid) -> crate::Result<Uuid> {
let state = State::get().await?;
let mut ingress_processor = state.ingress_processor.write().await;
match r#type {
IngressProcessType::IngressFeed => {
ingress_processor.ingress_feeds.push(uuid);
}
}
Ok(uuid)
}
/// Complete and retain an ingress safety process from the `HashMap` of ingress feeds.
pub async fn finish(r#type: IngressProcessType, uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let mut ingress_processor = state.ingress_processor.write().await;
match r#type {
IngressProcessType::IngressFeed => {
ingress_processor.ingress_feeds.retain(|f| *f != uuid);
}
}
Ok(())
}
/// Finish and check if an ingress safety processes ingress feed is empty and finished.
pub async fn finished(r#type: IngressProcessType) -> crate::Result<bool> {
let state = State::get().await?;
let ingress_processor = state.ingress_processor.read().await;
match r#type {
IngressProcessType::IngressFeed => {
if ingress_processor.ingress_feeds.is_empty() {
return Ok(true);
}
}
}
Ok(false)
}
}
/// A child process type.
#[derive(Debug)]
pub enum ChildType {
/// A tokio-managed child process.
ChildProcess(Child),
/// A restored child from a process cache that needs to be re-registered.
RescuedChild(u32),
}
/// A structure representing a cache of a running process.
#[derive(Serialize, Deserialize, Debug)]
pub struct ProcessorCache {
/// The PID of a process.
pub pid: u32,
/// The stored UUID of a process.
pub uuid: Uuid,
/// The time a process started.
pub start_time: u64,
/// The name of a process.
pub name: String,
/// The executable file running the process.
pub exe: String,
/// The associated [`ClusterPath`] to a process.
pub cluster_path: ClusterPath,
/// A post hook to be run once a process has completed.
pub post: Option<String>,
/// The [`Uuid`] of the Minecraft account it was started by.
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<Uuid>,
}
impl ChildType {
/// Get the PID of a specific `ChildType` process.
#[must_use]
pub fn id(&self) -> Option<u32> {
match self {
Self::ChildProcess(child) => child.id(),
Self::RescuedChild(pid) => Some(*pid),
}
}
/// Store a cache of a specific `ChildType`.
pub async fn cache(
&self,
uuid: Uuid,
cluster_path: ClusterPath,
post: Option<String>,
user: Option<Uuid>,
) -> crate::Result<()> {
let pid = match self {
Self::ChildProcess(child) => child.id().unwrap_or(0),
Self::RescuedChild(pid) => *pid,
};
let state = State::get().await?;
let mut system = sysinfo::System::new();
system.refresh_processes(sysinfo::ProcessesToUpdate::All);
let process = system
.process(sysinfo::Pid::from_u32(pid))
.ok_or_else(|| anyhow::anyhow!("could not find process {}", pid))?;
let start_time = process.start_time();
let name = process.name().to_string_lossy().to_string();
let Some(path) = process.exe() else {
return Err(anyhow::anyhow!("cached process {} has no path", pid).into());
};
let exe = path.to_string_lossy().to_string();
let cached_proccess = ProcessorCache {
pid,
uuid,
start_time,
name,
exe,
cluster_path,
post,
user,
};
let path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
let mut caches = (read_json::<HashMap<Uuid, ProcessorCache>>(&path, &state.io_semaphore)
.await)
.unwrap_or_default();
caches.insert(uuid, cached_proccess);
crate::utils::http::write(&path, &serde_json::to_vec(&caches)?, &state.io_semaphore)
.await?;
Ok(())
}
/// Remove a child from the global store of processes.
pub async fn remove(&self, uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
let mut caches = (read_json::<HashMap<Uuid, ProcessorCache>>(&path, &state.io_semaphore)
.await)
.unwrap_or_default();
caches.remove(&uuid);
crate::utils::http::write(&path, &serde_json::to_vec(&caches)?, &state.io_semaphore)
.await?;
Ok(())
}
/// Kill a process in the global store of processes.
pub async fn kill(&mut self) -> crate::Result<()> {
match self {
Self::ChildProcess(child) => Ok(child.kill().await.map_err(IOError::from)?),
Self::RescuedChild(pid) => {
let mut system = sysinfo::System::new();
if system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[
sysinfo::Pid::from_u32(*pid),
])) != 0
{
let process = system.process(sysinfo::Pid::from_u32(*pid));
if let Some(process) = process {
process.kill();
}
}
Ok(())
}
}
}
/// Wait for a process to complete.
pub fn try_wait(&mut self) -> crate::Result<Option<i32>> {
match self {
Self::ChildProcess(child) => Ok(child
.try_wait()
.map_err(IOError::from)?
.map(|x| x.code().unwrap_or(0))),
Self::RescuedChild(pid) => {
let mut system = sysinfo::System::new();
if system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[
sysinfo::Pid::from_u32(*pid),
])) == 0
{
return Ok(Some(0));
}
system.process(sysinfo::Pid::from_u32(*pid)).map_or_else(
|| Ok(Some(0)),
|process| {
if process.status() == sysinfo::ProcessStatus::Run {
Ok(None)
} else {
Ok(Some(0))
}
},
)
}
}
}
}
/// A [`Processor`] child process.
#[derive(Debug)]
pub struct ProcessorChild {
/// The uuid of a process.
pub uuid: Uuid,
/// The associated [`ClusterPath`] to a process.
pub cluster_path: ClusterPath,
/// The process manager managing this process.
pub manager: Option<JoinHandle<crate::Result<i32>>>,
/// The [`ChildType`] of this process.
pub current_child: Arc<RwLock<ChildType>>,
/// When this process was last updated in [`Utc`].
pub last_updated: DateTime<Utc>,
/// When this process was last updated in [`Utc`].
pub started_at: DateTime<Utc>,
/// What [`Uuid`] this process is running under.
pub user: Option<Uuid>,
}
impl Processor {
/// Get a new [`Processor`] instance.
#[must_use]
pub fn new() -> Self {
Self(HashMap::new())
}
/// Returns a reference to the value corresponding to the key.
#[must_use]
pub fn get(&self, uuid: Uuid) -> Option<Arc<RwLock<ProcessorChild>>> {
self.0.get(&uuid).cloned()
}
/// An collection visiting all keys in arbitrary order.
#[must_use]
pub fn keys(&self) -> Vec<Uuid> {
self.0.keys().copied().collect()
}
/// Get all running processes.
pub async fn running(&self) -> crate::Result<Vec<Uuid>> {
let mut keys = Vec::new();
for key in self.keys() {
if let Some(process) = self.get(key) {
let process = process.clone();
let process = process.write().await;
if process.current_child.write().await.try_wait()?.is_none() {
keys.push(key);
}
}
}
Ok(keys)
}
/// Get all running cluster processes.
pub async fn running_cluster(&self, cluster_path: ClusterPath) -> crate::Result<Vec<Uuid>> {
let running = self.running().await?;
let mut keys = Vec::new();
for key in running {
if let Some(process) = self.get(key) {
let process = process.clone();
let process = process.read().await;
if process.cluster_path == cluster_path {
keys.push(key);
}
}
}
Ok(keys)
}
/// get all running cluster paths.
pub async fn running_cluster_paths(&self) -> crate::Result<Vec<ClusterPath>> {
let mut clusters = Vec::new();
for key in self.keys() {
if let Some(process) = self.get(key) {
let process = process.clone();
let process = process.write().await;
if process.current_child.write().await.try_wait()?.is_none() {
clusters.push(process.cluster_path.clone());
}
}
}
Ok(clusters)
}
/// get all running clusters.
pub async fn running_clusters(&self) -> crate::Result<Vec<Cluster>> {
let mut clusters = Vec::new();
for key in self.keys() {
if let Some(process) = self.get(key) {
let process = process.clone();
let process = process.write().await;
if process.current_child.write().await.try_wait()?.is_none() {
if let Some(cluster) = cluster::get(&process.cluster_path.clone()).await? {
clusters.push(cluster);
}
}
}
}
Ok(clusters)
}
/// get the exit status of a process by its [`Uuid`].
pub async fn exit_status(&self, uuid: Uuid) -> crate::Result<Option<i32>> {
if let Some(process) = self.get(uuid) {
let process = process.write().await;
let status = process.current_child.write().await.try_wait()?;
Ok(status)
} else {
Ok(None)
}
}
/// restore processes from the cache.
pub async fn restore(&mut self) -> crate::Result<()> {
let state = State::get().await?;
let processor_path = state.directories.caches_dir().await.join(PROCESSOR_FILE);
let mut processor_caches = if let Ok(processes_json) =
read_json::<HashMap<Uuid, ProcessorCache>>(&processor_path, &state.io_semaphore).await
{
let zeros = HashMap::<Uuid, ProcessorCache>::new();
crate::utils::http::write(
&processor_path,
&serde_json::to_vec(&zeros)?,
&state.io_semaphore,
)
.await?;
processes_json
} else {
HashMap::new()
};
for (_, cache) in processor_caches.drain() {
let uuid = cache.uuid;
match self.insert_restored(cache).await {
Ok(process) => {
self.0.insert(uuid, process);
}
Err(e) => tracing::warn!("failed to restore process {}: {}", uuid, e),
}
}
Ok(())
}
/// insert a process with a command and [`ClusterPath`].
#[tracing::instrument(skip(self, uuid, command, post, censors))]
#[tracing::instrument(level = "trace", skip(self))]
#[onelauncher_macros::memory]
#[allow(clippy::too_many_arguments)]
pub async fn insert_process(
&mut self,
uuid: Uuid,
cluster_path: ClusterPath,
mut command: Command,
post: Option<String>,
censors: HashMap<String, String>,
user: Option<Uuid>,
enable_gamemode: Option<bool>,
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
let mut proc = command.spawn().map_err(IOError::from)?;
let pid = proc
.id()
.ok_or_else(|| anyhow::anyhow!("process failed, couldn't get pid"))?;
let stdout = proc.stdout.take().unwrap();
let stderr = proc.stderr.take().unwrap();
#[cfg(target_os = "linux")]
{
if enable_gamemode.is_some_and(|x| x) {
if let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid) {
tracing::warn!("failed to enable gamemode, continuing: {}", err);
};
}
}
tokio::spawn(async move {
let mut stdout = BufReader::new(stdout).lines();
let mut stderr = BufReader::new(stderr).lines();
while let Ok(Some(line)) = tokio::select! {
line = stdout.next_line() => line,
line = stderr.next_line() => line,
} {
let mut censored = line.clone();
for (key, value) in &censors {
censored = censored.replace(key, value);
}
if let Err(err) =
send_process(uuid, pid, ProcessPayloadType::Logging, &censored).await
{
tracing::warn!("failed to send process log: {}", err);
};
}
});
let process = ChildType::ChildProcess(proc);
process
.cache(uuid, cluster_path.clone(), post.clone(), user)
.await?;
let current_child = Arc::new(RwLock::new(process));
let manager = Some(tokio::spawn(Self::manager(
uuid,
post,
pid,
current_child.clone(),
cluster_path.clone(),
)));
send_process(uuid, pid, ProcessPayloadType::Started, "started process").await?;
let last_updated = Utc::now();
let child = ProcessorChild {
uuid,
cluster_path,
current_child,
manager,
last_updated,
started_at: Utc::now(),
user,
};
let child = Arc::new(RwLock::new(child));
self.0.insert(uuid, child.clone());
Ok(child)
}
/// Insert a cached process with a [`ProcessorCache`].
#[tracing::instrument(skip(self, cache))]
#[tracing::instrument(level = "trace", skip(self))]
#[onelauncher_macros::memory]
pub async fn insert_restored(
&mut self,
cache: ProcessorCache,
) -> crate::Result<Arc<RwLock<ProcessorChild>>> {
let _state = State::get().await?;
// ensure located stray process matches with our pid recorded process
{
let mut system = sysinfo::System::new();
system.refresh_processes(sysinfo::ProcessesToUpdate::All);
let process = system
.process(sysinfo::Pid::from_u32(cache.pid))
.ok_or_else(|| anyhow::anyhow!("could not find pid {}", cache.pid))?;
if cache.start_time != process.start_time() {
return Err(anyhow::anyhow!(
"restored process {} has a mismatched start time {}",
cache.pid,
process.start_time()
)
.into());
}
if cache.name != process.name().to_string_lossy() {
return Err(anyhow::anyhow!(
"restored process {} has a mismatched name {}",
cache.pid,
process.name().to_string_lossy().to_string()
)
.into());
}
if let Some(path) = process.exe() {
if cache.exe != path.to_string_lossy() {
return Err(anyhow::anyhow!(
"restored process {} has a mismatched exe {}",
cache.pid,
path.to_string_lossy()
)
.into());
}
} else {
return Err(
anyhow::anyhow!("restored process {} has no exe path", cache.pid).into(),
);
}
}
let process = ChildType::RescuedChild(cache.pid);
let pid = process
.id()
.ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?;
process
.cache(
cache.uuid,
cache.cluster_path.clone(),
cache.post.clone(),
cache.user,
)
.await?;
let current_child = Arc::new(RwLock::new(process));
let manager = Some(tokio::spawn(Self::manager(
cache.uuid,
cache.post,
pid,
current_child.clone(),
cache.cluster_path.clone(),
)));
send_process(
cache.uuid,
pid,
ProcessPayloadType::Started,
"started process",
)
.await?;
let last_updated = Utc::now();
let child = ProcessorChild {
uuid: cache.uuid,
cluster_path: cache.cluster_path,
started_at: Utc
.timestamp_opt(cache.start_time as i64, 0)
.single()
.ok_or_else(|| {
anyhow::anyhow!("couldn't convert processor cache timestamp to Utc")
})?, // TODO: Look into this
current_child,
manager,
last_updated,
user: cache.user,
};
let child = Arc::new(RwLock::new(child));
self.0.insert(cache.uuid, child.clone());
Ok(child)
}
/// Get a process manager and runner awaiting for the exit status.
#[tracing::instrument(skip(current_child))]
#[onelauncher_macros::memory]
async fn manager(
uuid: Uuid,
post: Option<String>,
mut current_pid: u32,
current_child: Arc<RwLock<ChildType>>,
cluster_path: ClusterPath,
) -> crate::Result<i32> {
let current_child = current_child.clone();
let mut exit_status;
let mut last_updated = Utc::now();
// core main process loop, managed by tokio
loop {
let value = current_child.write().await.try_wait();
if let Some(stat) = value? {
exit_status = stat;
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let update = Utc::now().signed_duration_since(last_updated).num_seconds();
if update >= 60 {
if let Err(err) = cluster::edit(&cluster_path, |cluster| {
cluster.meta.recently_played += update as u64;
async { Ok(()) }
})
.await
{
tracing::warn!(
"failed to update playtime for cluster {}: {}",
&cluster_path,
err
);
}
last_updated = Utc::now();
}
}
let update = Utc::now().signed_duration_since(last_updated).num_seconds();
if let Err(err) = cluster::edit(&cluster_path, |cluster| {
cluster.meta.recently_played += update as u64;
async { Ok(()) }
})
.await
{
tracing::warn!(
"failed to update playtime for cluster {}: {}",
&cluster_path,
err
);
}
let cluster_path_in = cluster_path.clone();
tokio::spawn(async move {
if let Err(err) = cluster::update_playtime(&cluster_path_in).await {
tracing::warn!(
"failed to update playtime for cluster {}: {}",
&cluster_path_in,
err
);
}
});
tokio::spawn(async {
let state = State::get().await;
let state = match state {
Ok(state) => state,
Err(err) => {
tracing::warn!("failed to get state: {}", err);
return;
}
};
let _ = state.discord_rpc.clear(true).await;
});
#[cfg(feature = "tauri")]
{
let window = crate::ProxyState::get_main_window().await?;
window.unminimize()?;
}
{
let current_child = current_child.write().await;
current_child.remove(uuid).await?;
}
if !exit_status == 0 {
send_process(
uuid,
current_pid,
ProcessPayloadType::Finished,
"exited process",
)
.await?;
return Ok(exit_status);
}
let post = if let Some(hook) = post {
if hook.is_empty() {
None
} else {
let mut cmd = hook.split(' ');
if let Some(c) = cmd.next() {
let mut c = Command::new(c);
c.args(cmd).current_dir(cluster_path.full_path().await?);
Some(c)
} else {
None
}
}
} else {
None
};
if let Some(mut m_c) = post {
let result = {
{
let mut current_child: tokio::sync::RwLockWriteGuard<'_, ChildType> =
current_child.write().await;
let new_child = m_c.spawn().map_err(IOError::from)?;
current_pid = new_child
.id()
.ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?;
*current_child = ChildType::ChildProcess(new_child);
};
send_process(
uuid,
current_pid,
ProcessPayloadType::Modified,
"running post hook",
)
.await?;
let value = current_child.write().await.try_wait();
if let Ok(Some(stat)) = value {
exit_status = stat;
}
Ok::<(), crate::Error>(())
};
if let Err(err) = result {
tracing::error!("failed to run post hook: {err}");
let _ = send_message(format!("failed to run post hook: {err}").as_str()).await;
}
};
send_process(
uuid,
current_pid,
ProcessPayloadType::Finished,
"exited process",
)
.await?;
Ok(exit_status)
}
}
impl Default for Processor {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,209 +0,0 @@
//! Handles user-inputted settings and global values
use super::{Directories, JavaVersions};
use crate::constants::CURRENT_SETTINGS_FORMAT_VERSION;
use onelauncher_utils::io;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
/// A global settings state for the launcher.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Settings {
/// A `OneLauncher` theme managed by the `OneLauncher` GUI.
#[serde(default = "theme_default")]
pub theme: String,
/// A global browser list view for the `OneLauncher` GUI.
#[serde(default)]
pub browser_list_view: BrowserListView,
/// Does not ask for confirmation when closing the `OneLauncher` GUI
#[serde(default)]
pub hide_close_prompt: bool,
/// Disables animations in the `OneLauncher` GUI
#[serde(default)]
pub disable_animations: bool,
/// A global fullscreen Minecraft state.
#[serde(default)]
pub force_fullscreen: bool,
/// Whether to allow launching the same cluster under the same account.
#[serde(default)]
pub allow_parallel_running_clusters: bool,
/// Whether to launch Feral Gamemode on Linux systems.
#[serde(default)]
pub enable_gamemode: bool,
/// A global default [`Resolution`] for Minecraft.
pub resolution: Resolution,
/// A global [`JavaVersions`] list and default version.
pub java_versions: JavaVersions,
/// A global [`Memory`] settings store for Java memory settings.
pub memory: Memory,
/// Global and default initialization hooks .
pub init_hooks: InitHooks,
/// Global and default custom Java arguments.
pub custom_java_args: Vec<String>,
/// Global and default custom environment variables.
pub custom_env_args: Vec<(String, String)>,
/// Global and default maximum [`Semaphore`] I/O operations.
pub max_async_io_operations: usize,
/// Global and default maximum [`Semaphore`] HTTP operations.
pub max_async_fetches: usize,
/// The [`CURRENT_FORMAT_VERSION`] of this settings file.
pub version: u32,
/// Whether or not to disable Discord IPC.
#[serde(default)]
pub disable_discord: bool,
/// Whether or not to enable a debug mode in the launcher.
#[serde(default)]
pub debug_mode: bool,
/// Whether or not to disable Plausible and Sentry analytics.
#[serde(default)]
pub disable_analytics: bool,
/// The core global config directory stored as a [`PathBuf`].
#[serde(default = "Directories::init_settings_dir")]
pub config_dir: Option<PathBuf>,
/// Whether or not to minimize the launcher upon a game launch.
#[serde(default)]
pub hide_on_launch: bool,
/// Enable/disable custom window decorations.
#[serde(default)]
pub custom_frame: bool,
/// Completed onboarding.
#[serde(default)]
pub onboarding_completed: bool,
}
fn theme_default() -> String {
"dark".to_string()
}
impl Settings {
/// Initializes the global settings state.
#[tracing::instrument]
pub async fn initialize(file: &Path) -> crate::Result<Self> {
let mut recovered_corruption = false;
let settings = if file.exists() {
let read_settings = io::read(&file)
.await
.map_err(|err| anyhow::anyhow!("error reading settings file: {0}", err).into())
.and_then(|it| serde_json::from_slice::<Self>(&it).map_err(crate::Error::from));
if let Err(ref err) = read_settings {
tracing::error!("failed to load settings file: {err}.");
let backup = file.with_extension("json.bak");
tracing::error!(
"corrupted settings will be backed up as {}, and a new one will be created",
backup.display()
);
let _ = io::rename(file, backup).await;
recovered_corruption = true;
}
read_settings.ok()
} else {
None
};
if let Some(settings) = settings {
Ok(settings)
} else {
let settings = Self {
theme: "dark".to_string(),
browser_list_view: BrowserListView::Grid,
hide_close_prompt: true,
disable_animations: false,
force_fullscreen: false,
allow_parallel_running_clusters: false,
#[cfg(target_os = "linux")]
enable_gamemode: true,
#[cfg(not(target_os = "linux"))]
enable_gamemode: false,
resolution: Resolution::default(),
java_versions: JavaVersions::new(),
memory: Memory::default(),
init_hooks: InitHooks::default(),
custom_env_args: Vec::new(),
custom_java_args: Vec::new(),
max_async_fetches: 10,
max_async_io_operations: 10,
version: CURRENT_SETTINGS_FORMAT_VERSION,
disable_analytics: false,
disable_discord: false,
debug_mode: false,
config_dir: Directories::init_settings_dir(),
hide_on_launch: false,
custom_frame: true,
onboarding_completed: false,
};
if recovered_corruption {
settings.sync(file).await?;
}
Ok(settings)
}
}
/// Synchronizes the current settings from a file.
#[tracing::instrument(skip(self))]
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
io::write(to, serde_json::to_vec(self)?).await?;
Ok(())
}
}
/// `OneLauncher` browser list type.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BrowserListView {
#[default]
Grid,
List,
}
/// Global memory settings across all clusters.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct Memory {
/// Maximum amount of Java memory available globally.
pub maximum: u32,
/// Minimum amount of Java memory available globally.
pub minimum: u32,
}
impl Default for Memory {
fn default() -> Self {
Self {
maximum: 2048,
minimum: 256,
}
}
}
/// Global Minecraft resolution.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct Resolution(pub u16, pub u16);
impl Default for Resolution {
fn default() -> Self {
Self(854, 480)
}
}
/// Global initialization hooks for all Minecraft clusters.
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub struct InitHooks {
/// Pre-launch hook.
#[serde(skip_serializing_if = "Option::is_none")]
pub pre: Option<String>,
/// Wrapper hook for the runtime of the game instance.
#[serde(skip_serializing_if = "Option::is_none")]
pub wrapper: Option<String>,
/// Post-launch hook.
#[serde(skip_serializing_if = "Option::is_none")]
pub post: Option<String>,
}

View file

@ -1,68 +0,0 @@
use std::path::PathBuf;
use crate::Result;
pub fn sha1(bytes: &[u8]) -> String {
sha1_smol::Sha1::from(bytes).digest().to_string()
}
pub fn sha1_file(file: &PathBuf) -> Result<String> {
Ok(sha1(&std::fs::read(file)?))
}
pub fn sha1_files(files: Vec<&PathBuf>) -> Vec<Option<String>> {
let mut hashes = Vec::new();
for file in files {
hashes.push(sha1_file(file).ok());
}
hashes
}
pub fn md5(bytes: &[u8]) -> String {
let digest = md5::compute(bytes);
format!("{:x}", digest)
}
pub fn md5_file(file: &PathBuf) -> Result<String> {
Ok(md5(&std::fs::read(file)?))
}
pub fn md5_files(files: Vec<&PathBuf>) -> Vec<Option<String>> {
let mut hashes = Vec::new();
for file in files {
hashes.push(md5_file(file).ok());
}
hashes
}
const CURSEFORGE_FINGERPRINT_SEED: u32 = 1;
pub fn murmur2(bytes: &[u8]) -> u32 {
let normalized = normalize_byte_array(bytes);
murmur2::murmur2(normalized.as_slice(), CURSEFORGE_FINGERPRINT_SEED)
}
pub fn murmur2_file(file: &PathBuf) -> Result<u32> {
Ok(murmur2(&std::fs::read(file)?))
}
pub fn murmur2_files(files: Vec<&PathBuf>) -> Vec<Option<u32>> {
let mut hashes = Vec::new();
for file in files {
hashes.push(murmur2_file(file).ok());
}
hashes
}
// Curseforge and their documented code :D (it wasn't documented)
// https://github.com/CurseForgeCommunity/.NET-APIClient/blob/2c4f5f613d20025f9286fdd53592f8519022918f/Murmur2.cs#L8-L21
// https://github.com/comp500/infra.link/blob/a298502fdb15b8939fe0effe71ed42ad06f3946f/murmur2/go/main.go#L10-L40
fn normalize_byte_array(bytes: &[u8]) -> Vec<u8> {
let normalized = bytes
.iter()
.filter(|byte| !matches!(byte, 9 | 10 | 13 | 32))
.copied()
.collect::<Vec<u8>>();
// println!("Old len {}", bytes.len());
// println!("New len {}", normalized.len());
normalized
}

View file

@ -1,303 +0,0 @@
//! **HTTP Utilities**
//!
//! Async extensions and wrappers around [`reqwest`] functions.
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use bytes::Bytes;
use reqwest::Method;
use serde::de::DeserializeOwned;
use tokio::io::AsyncWriteExt;
use tokio::sync::{RwLock, Semaphore};
use crate::constants::FETCH_ATTEMPTS;
use crate::proxy::send::send_ingress;
use crate::proxy::IngressId;
use onelauncher_utils::io;
use onelauncher_utils::io::IOError;
use super::crypto;
/// A [`Semaphore`] used for all I/O operations.
#[derive(Debug)]
pub struct IoSemaphore(pub RwLock<Semaphore>);
/// A [`Semaphore`] used for all HTTP operations.
#[derive(Debug)]
pub struct FetchSemaphore(pub RwLock<Semaphore>);
lazy_static::lazy_static! {
/// A public reqwest client with configured headers.
pub static ref REQWEST_CLIENT: reqwest::Client = {
let mut headers = reqwest::header::HeaderMap::new();
let header = reqwest::header::HeaderValue::from_str(&format!(
"{}/{} (https://polyfrost.org)",
crate::constants::NAME,
crate::constants::VERSION,
)).expect("failed to build reqwest headers!");
headers.insert(reqwest::header::USER_AGENT, header);
reqwest::Client::builder()
.tcp_keepalive(Some(std::time::Duration::from_secs(15)))
.default_headers(headers)
.build()
.expect("failed to build reqwest client!")
};
}
/// Basic HTTP fetch interface.
#[tracing::instrument(skip(semaphore))]
pub async fn fetch(
url: &str,
sha1: Option<&str>,
semaphore: &FetchSemaphore,
) -> crate::Result<Bytes> {
fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore).await
}
/// Basic JSON fetch interface.
#[tracing::instrument(skip(json_body, semaphore))]
pub async fn fetch_json<T>(
method: Method,
url: &str,
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
semaphore: &FetchSemaphore,
) -> crate::Result<T>
where
T: DeserializeOwned,
{
let result = fetch_advanced(method, url, sha1, json_body, None, None, semaphore).await?;
let value = serde_json::from_slice(&result)?;
Ok(value)
}
/// Advanced HTTP fetch interface with json, sha1 validation, and ingress support.
#[tracing::instrument(skip(json_body, semaphore))]
#[onelauncher_macros::memory]
pub async fn fetch_advanced(
method: Method,
url: &str,
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
headers: Option<HashMap<&str, &str>>,
ingress: Option<(&IngressId, f64)>,
semaphore: &FetchSemaphore,
) -> crate::Result<Bytes> {
let io_semaphore = semaphore.0.read().await;
let _permit = io_semaphore.acquire().await?;
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
let mut req = REQWEST_CLIENT.request(method.clone(), url);
if let Some(body) = json_body.clone() {
req = req.json(&body);
}
if let Some(headers) = &headers {
for (key, value) in headers {
req = req.header(*key, *value);
}
}
let result = req.send().await;
match result {
Ok(x) => {
let bytes = if let Some((feed, total)) = &ingress {
let length = x.content_length();
if let Some(total_size) = length {
use futures::StreamExt;
let mut stream = x.bytes_stream();
let mut bytes = Vec::new();
while let Some(item) = stream.next().await {
let chunk =
item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?;
bytes.append(&mut chunk.to_vec());
send_ingress(
feed,
(chunk.len() as f64 / total_size as f64) * total,
None,
)
.await?;
}
Ok(Bytes::from(bytes))
} else {
x.bytes().await
}
} else {
x.bytes().await
};
if let Ok(bytes) = bytes {
if let Some(sha1) = sha1 {
let hash = crypto::sha1(&bytes);
if &*hash != sha1 {
if attempt <= 3 {
continue;
}
return Err(anyhow::anyhow!(
"hash {0} does not match {1}",
sha1.to_string(),
hash
)
.into());
}
}
tracing::trace!("finished downloading {url}");
return Ok(bytes);
} else if attempt <= 3 {
continue;
} else if let Err(err) = bytes {
return Err(err.into());
}
}
Err(_) if attempt <= 3 => continue,
Err(err) => {
return Err(err.into());
}
}
}
unreachable!()
}
/// A utility to fetch from multiple mirrored sources.
#[tracing::instrument(skip(semaphore))]
#[onelauncher_macros::memory]
pub async fn fetch_from_mirrors(
mirrors: &[&str],
sha1: Option<&str>,
semaphore: &FetchSemaphore,
) -> crate::Result<Bytes> {
if mirrors.is_empty() {
return Err(anyhow::anyhow!("no mirrors provided!").into());
}
for (index, mirror) in mirrors.iter().enumerate() {
let result = fetch(mirror, sha1, semaphore).await;
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
return result;
}
}
unreachable!()
}
/// Checks if we are playing offline by contacting a reliable server.
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn check_internet_connection(timeout: u64) -> bool {
REQWEST_CLIENT
.get("https://api.polyfrost.org/")
.timeout(std::time::Duration::from_secs(timeout))
.send()
.await
.is_ok()
}
/// Post JSON to a specified URL (helpful for Microsoft auth and Metadata)
#[tracing::instrument(skip(json_body, semaphore))]
#[onelauncher_macros::memory]
pub async fn post_json<T>(
url: &str,
json_body: serde_json::Value,
semaphore: &FetchSemaphore,
) -> crate::Result<T>
where
T: DeserializeOwned,
{
let io_semaphore = semaphore.0.read().await;
let _permit = io_semaphore.acquire().await?;
let req = REQWEST_CLIENT.post(url).json(&json_body);
let result = req.send().await?.error_for_status()?;
let value = result.json().await?;
Ok(value)
}
/// Read JSON from a specified [`Path`].
pub async fn read_json<T>(path: &Path, semaphore: &IoSemaphore) -> crate::Result<T>
where
T: DeserializeOwned,
{
let io_semaphore = semaphore.0.read().await;
let _permit = io_semaphore.acquire().await?;
let json = io::read(path).await?;
let json = serde_json::from_slice::<T>(&json)?;
Ok(json)
}
/// Write to a file at a specified [`Path`].
#[tracing::instrument(skip(bytes, semaphore))]
pub async fn write<'a>(path: &Path, bytes: &[u8], semaphore: &IoSemaphore) -> crate::Result<()> {
let io_semaphore = semaphore.0.read().await;
let _permit = io_semaphore.acquire().await?;
if let Some(parent) = path.parent() {
io::create_dir_all(parent).await?;
}
let mut file = tokio::fs::File::create(path)
.await
.map_err(|e| IOError::with_path(e, path))?;
file.write_all(bytes)
.await
.map_err(|e| IOError::with_path(e, path))?;
tracing::trace!("done writing to file {}", path.display());
Ok(())
}
/// Copy a file from one [`AsRef<Path>`] to another.
pub async fn copy(
src: impl AsRef<Path>,
dest: impl AsRef<Path>,
semaphore: &IoSemaphore,
) -> crate::Result<()> {
let src = src.as_ref();
let dest = dest.as_ref();
let io_semaphore = semaphore.0.read().await;
let _permit = io_semaphore.acquire().await?;
if let Some(parent) = dest.parent() {
io::create_dir_all(parent).await?;
}
io::copy(src, dest).await?;
tracing::trace!(
"done copying file from {} to {}",
src.display(),
dest.display()
);
Ok(())
}
#[tracing::instrument(skip(bytes, io_semaphore))]
pub async fn write_icon(
icon_path: &str,
cache_dir: &Path,
bytes: Bytes,
io_semaphore: &IoSemaphore,
) -> crate::Result<PathBuf> {
let ext = Path::new(&icon_path).extension().and_then(OsStr::to_str);
let hash = crypto::sha1(&bytes);
let path = cache_dir.join("icons").join(if let Some(e) = ext {
format!("{hash}.{e}")
} else {
hash
});
write(&path, &bytes, io_semaphore).await?;
let path = io::canonicalize(path)?;
Ok(path)
}

View file

@ -1,366 +0,0 @@
//! **Java Utilities**
//!
//! Async utilities for managing and downloading Java versions.
use crate::constants::JAVA_BIN;
use crate::State;
use futures::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use tokio::task::JoinError;
#[cfg(target_os = "windows")]
use winreg::{
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY},
RegKey,
};
use onelauncher_utils::io;
/// A struct representing a single version of the Java Runtime Environment.
/// Use [`locate_java`] to get an array of all located java instances on the machine.
///
/// common java paths from <https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/java/JavaUtils.cpp> under GPL 3.0
///
/// java check functionality from <https://github.com/modrinth/theseus/blob/master/theseus/src/util/jre.rs> under MIT
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)]
pub struct JavaVersion {
pub version: String,
pub arch: String,
pub path: String,
}
/// Java entrypoint: Returns all possibly existing java paths on a Linux machine and validates them,
/// returning a [`Vec<JavaVersion>`] with all valid java installations, their paths, and associated metadata.
#[cfg(target_os = "windows")]
#[tracing::instrument]
pub async fn locate_java() -> Result<Vec<JavaVersion>, JavaError> {
let mut paths = HashSet::new();
paths.extend(locate_java_path().await);
paths.extend(locate_installed_java().await?);
if let Ok(java_home) = env::var("JAVA_HOME") {
paths.insert(PathBuf::from(java_home));
}
let common_java_paths = [
r"C:/Program Files/Java",
r"C:\Program Files\Eclipse Adoptium",
r"C:/Program Files (x86)/Java",
r"C:\Program Files (x86)/Eclipse Adoptium",
];
for path in common_java_paths {
let Ok(java_subs) = std::fs::read_dir(path) else {
continue;
};
for java_sub in java_subs.flatten() {
let full_path = java_sub.path();
paths.insert(full_path.join("bin"));
}
}
let common_key_paths = [
r"SOFTWARE\\JavaSoft\\Java Runtime Environment",
r"SOFTWARE\\JavaSoft\\Java Development Kit",
r"SOFTWARE\\JavaSoft\\JRE",
r"SOFTWARE\\JavaSoft\\JDK",
r"SOFTWARE\\AdoptOpenJDK\\JRE",
r"SOFTWARE\\AdoptOpenJDK\\JDK",
r"SOFTWARE\\Eclipse Foundation\\JDK",
r"SOFTWARE\\Eclipse Foundation\\JRE",
r"SOFTWARE\\Eclipse Adoptium\\JRE",
r"SOFTWARE\\Eclipse Adoptium\\JDK",
r"SOFTWARE\\Microsoft\\JDK",
r"SOFTWARE\\Azul Systems\\Zulu",
r"SOFTWARE\\BellSoft\\Liberica",
];
for key in common_key_paths {
if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE)
.open_subkey_with_flags(key, KEY_READ | KEY_WOW64_32KEY)
{
paths.extend(get_java_paths_from_windows(java_key));
}
if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE)
.open_subkey_with_flags(key, KEY_READ | KEY_WOW64_64KEY)
{
paths.extend(get_java_paths_from_windows(java_key));
}
}
let java = check_java(paths).await.into_iter().collect();
Ok(java)
}
/// Locates possibly existing java paths on Windows from Registry keys, returning as a [`HashSet<PathBuf>`]
#[cfg(target_os = "windows")]
#[tracing::instrument]
pub fn get_java_paths_from_windows(java_key: RegKey) -> HashSet<PathBuf> {
let mut paths = HashSet::new();
for subkey in java_key.enum_keys().flatten() {
if let Ok(subkey) = java_key.open_subkey(subkey) {
let common_subkey_value_names = [r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"];
for subkey_value in common_subkey_value_names {
let path: Result<String, std::io::Error> = subkey.get_value(subkey_value);
let Ok(path) = path else { continue };
paths.insert(PathBuf::from(path).join("bin"));
}
}
}
paths
}
/// Java entrypoint: Returns all possibly existing java paths on a macOS machine and validates them,
/// returning a [`Vec<JavaVersion>`] with all valid java installations, their paths, and associated metadata.
#[cfg(target_os = "macos")]
#[tracing::instrument]
pub async fn locate_java() -> Result<Vec<JavaVersion>, JavaError> {
let mut paths = HashSet::new();
paths.extend(locate_java_path().await);
paths.extend(locate_installed_java().await?);
let jvm_base = PathBuf::from("/Library/Java/JavaVirtualMachines/");
if let Ok(dir) = std::fs::read_dir(jvm_base) {
for entry in dir.flatten() {
let entry = entry.path().join("Contents/Home/bin");
paths.insert(entry);
}
}
let common_java_paths = [
r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands",
r"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java",
r"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home",
];
for path in common_java_paths {
paths.insert(PathBuf::from(path));
}
let java = check_java(paths).await.into_iter().collect();
Ok(java)
}
/// Java entrypoint: Returns all possibly existing java paths on a Linux machine and validates them,
/// returning a [`Vec<JavaVersion>`] with all valid java installations, their paths, and associated metadata.
#[cfg(target_os = "linux")]
#[tracing::instrument]
pub async fn locate_java() -> Result<Vec<JavaVersion>, JavaError> {
let mut paths = HashSet::new();
paths.extend(locate_java_path().await);
paths.extend(locate_installed_java().await?);
let common_java_paths = [
r"/usr",
r"/usr/java",
r"/usr/lib/jvm",
r"/usr/lib64/jvm",
r"/usr/lib32/jvm",
r"/opt/jdk",
r"/opt/jdks",
r"/app/jdk",
];
for path in common_java_paths {
let path = PathBuf::from(path);
paths.insert(PathBuf::from(&path).join("jre").join("bin"));
paths.insert(PathBuf::from(&path).join("bin"));
if let Ok(dir) = std::fs::read_dir(path) {
for entry in dir.flatten() {
let entry_path = entry.path();
paths.insert(entry_path.join("jre").join("bin"));
paths.insert(entry_path.join("bin"));
}
}
}
let java = check_java(paths).await.into_iter().collect();
Ok(java)
}
/// Locate Java paths automatically added by the launcher instance.
#[tracing::instrument]
#[onelauncher_macros::memory]
async fn locate_installed_java() -> Result<HashSet<PathBuf>, JavaError> {
Box::pin(async move {
let mut paths = HashSet::new();
let java_base_path = State::get()
.await
.map_err(|_| JavaError::MutexError)?
.directories
.java_dir()
.await;
if java_base_path.is_dir() {
if let Ok(dir) = std::fs::read_dir(java_base_path) {
for entry in dir.flatten() {
let path = entry.path().join("bin");
if let Ok(contents) = io::read_to_string(path.clone()).await {
let entry = entry.path().join(contents);
paths.insert(entry);
} else {
#[cfg(not(target_os = "macos"))]
{
let path = path.join(JAVA_BIN);
paths.insert(path);
}
}
}
}
}
Ok(paths)
})
.await
}
/// Locates all installed Java instances from the `PATH` env variable, returning a
/// [`HashSet<PathBuf>`] containing all paths in the `PATH` variable to be sorted into java versions.
#[tracing::instrument]
async fn locate_java_path() -> HashSet<PathBuf> {
let paths = env::var("PATH").map(|p| env::split_paths(&p).collect::<HashSet<_>>());
paths.unwrap_or_else(|_| HashSet::new())
}
/// Verifies that each Java instance is valid from a [`HashSet<PathBuf>`], returning a
/// [`HashSet<JavaVersion>`] for each validated path containing the installation's metadata.
#[tracing::instrument]
pub async fn check_java(paths: HashSet<PathBuf>) -> HashSet<JavaVersion> {
let java_instances = stream::iter(paths.into_iter())
.map(|p: PathBuf| tokio::task::spawn(async move { check_java_instance(&p).await }))
.buffer_unordered(64)
.collect::<Vec<_>>()
.await;
java_instances
.into_iter()
.filter_map(Result::ok)
.flatten()
.collect()
}
/// Verifies that a java instance [`Path`] is valid, returning a [`JavaVersion`] if the java path
/// is valid, containing the installation's metadata.
/// java_check from https://github.com/modrinth/theseus/blob/master/theseus/library/JavaInfo.class under MIT
#[tracing::instrument]
#[onelauncher_macros::memory]
pub async fn check_java_instance(path: &Path) -> Option<JavaVersion> {
let Ok(path) = io::canonicalize(path) else {
return None;
};
let java = if path.file_name()?.to_str()? == JAVA_BIN {
path
} else {
path.join(JAVA_BIN)
};
if !java.exists() {
return None;
}
let bytes = include_bytes!("../../wrapper/java/JavaInfo.class");
let tempdir: PathBuf = tempfile::tempdir().ok()?.into_path();
if !tempdir.exists() {
return None;
}
let file_path = tempdir.join("JavaInfo.class");
tracing::info!(
"checking java with {}",
file_path.to_string_lossy().to_string()
);
io::write(&file_path, bytes).await.ok()?;
let output = Command::new(&java)
.arg("-cp")
.arg(file_path.parent().expect("failed to get java parent"))
.arg("JavaInfo")
.env_remove("_JAVA_OPTIONS")
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
tracing::debug!("{stdout}");
let mut java_version = None;
let mut java_arch = None;
for line in stdout.lines() {
let mut parts = line.split('=');
let key = parts.next().unwrap_or_default();
let value = parts.next().unwrap_or_default();
if key == "os.arch" {
java_arch = Some(value);
} else if key == "java.version" {
java_version = Some(value);
}
}
if let Some(arch) = java_arch {
if let Some(version) = java_version {
let path = java.to_string_lossy().to_string();
return Some(JavaVersion {
path,
version: version.to_string(),
arch: arch.to_string(),
});
}
}
None
}
/// Get a formatted java version as `(major: u32, minor: u32)`.
///
/// This means 1.8 becomes `(1, 8)`, and 21.0.3 becomes `(21, 0)`.
pub fn get_java_version(version: &str) -> Result<(u32, u32), JavaError> {
let mut split = version.split('.');
let major_opt = split.next();
let mut major;
let mut minor = if let Some(minor) = split.next() {
major = major_opt.unwrap_or("1").parse::<u32>()?;
minor.parse::<u32>()?
} else {
major = 1;
major_opt
.ok_or_else(|| JavaError::InvalidVersion(version.to_string()))?
.parse::<u32>()?
};
if major > 1 {
minor = major;
major = 1;
}
Ok((major, minor))
}
/// An error wrapper for everything that can go wrong while managing Java installations.
#[derive(thiserror::Error, Debug)]
pub enum JavaError {
#[error("failed to fetch environment variable: {0}")]
EnvError(#[from] env::VarError),
#[error("error running java command: {0}")]
IOError(#[from] std::io::Error),
#[error("failed to parse integer: {0}")]
ParseError(#[from] std::num::ParseIntError),
#[error("tokio error: {0}")]
JoinError(#[from] JoinError),
#[error("couldn't get a valid java version from {0}")]
InvalidVersion(String),
#[error("failed to get launcher state mutex!")]
MutexError,
}

View file

@ -1,15 +0,0 @@
//! **`OneLauncher` Core Utilities**
//! Standard asynchronous utilities and wrappers for use within the launcher core.
//!
//! - [`http`]: Async extensions and wrappers around [`reqwest`] functions.
//! - [`java`]: Async utilities for managing and downloading Java versions.
//! - [`watcher`]: Async utilities for watching files with [`notify`].
pub mod http;
pub mod java;
pub mod pagination;
pub mod watcher;
pub mod crypto;
#[cfg(feature = "tauri")]
pub mod window;

View file

@ -1,23 +0,0 @@
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct Pagination {
pub index: u32,
pub page_size: u16,
pub result_count: u32,
pub total_count: u32,
}
pub fn get_page_offset(page: Option<u32>, page_size: Option<u16>) -> (u32, u16) {
let page = page.unwrap_or(0);
let page_size = page_size.unwrap_or(10);
let offset = (page - 1) * page_size as u32;
(offset, page_size)
}
pub fn get_page_count(total: u32, page_size: u16) -> u32 {
(total as f64 / page_size as f64).ceil() as u32
}

View file

@ -1,86 +0,0 @@
//! **Watcher Utilities**
//!
//! Async utilities for watching files with [`notify`].
use std::path::PathBuf;
use std::time::Duration;
use crate::store::{Cluster, ClusterPath, Clusters};
use futures::channel::mpsc::channel;
use futures::{SinkExt, StreamExt};
use notify::RecommendedWatcher;
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
/// Creates and initializes an FS watcher for the `clusters` directory, returning
/// the watcher as a [`Debouncer<RecommendedWatcher>`].
pub fn initialize_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
let (mut sender, mut rscv) = channel(1);
let watcher = new_debouncer(
Duration::from_secs_f32(2.0),
move |result: DebounceEventResult| {
futures::executor::block_on(async {
sender
.send(result)
.await
.expect("failed to send debouse event");
});
},
)?;
tokio::task::spawn(async move {
let span = tracing::span!(tracing::Level::INFO, "initialize_watcher");
tracing::info!(parent: &span, "initializing fs watcher...");
while let Some(result) = rscv.next().await {
let _span = span.enter();
match result {
Ok(mut events) => {
let mut paths = Vec::new();
events.sort_by(|a, b| a.path.cmp(&b.path));
for a in &events {
let mut formatted = PathBuf::new();
let mut components = a.path.components();
let mut matched = false;
for cmp in components.by_ref() {
formatted.push(cmp);
if matched {
break;
}
if cmp.as_os_str() == "clusters" {
matched = true;
}
}
let sub = components.next().is_none();
let cluster_path = ClusterPath::new(PathBuf::from(
formatted.file_name().unwrap_or_default(),
));
if a.path
.components()
.any(|c| c.as_os_str() == crate::constants::CRASH_PATH)
&& a.path.extension().is_some_and(|e| e == "txt")
{
Cluster::handle_crash(cluster_path);
} else if !paths.contains(&formatted) {
if sub {
// TODO: Packages Figure this out
// let cluster_path = cluster_path.clone();
// tokio::task::spawn(async move {
// Cluster::sync_packages(&cluster_path, true).await
// });
paths.push(formatted);
} else {
Clusters::sync_cluster(cluster_path);
}
}
}
}
Err(err) => tracing::warn!("fs watching error: {err}"),
}
}
});
Ok(watcher)
}

View file

@ -1,19 +0,0 @@
use tauri::WebviewWindow;
pub fn set_window_styling(
win: &WebviewWindow,
custom: bool,
) -> Result<(), Box<dyn std::error::Error>> {
win.set_decorations(!custom)?;
#[cfg(target_os = "macos")]
{
if custom {
win.set_title_bar_style(tauri::TitleBarStyle::Overlay)?;
} else {
win.set_title_bar_style(tauri::TitleBarStyle::Visible)?;
}
}
Ok(())
}

View file

@ -1,15 +0,0 @@
# Library Wrappers
TODO: Don't pre-build binaries. That is a huge security issue.
## `JavaCheck`
A simple prebuilt Java class that prints the architecture and version of a JVM, [GitHub repository](https://github.com/PrismLauncher/PrismLauncher/tree/develop/libraries/javacheck) (Available under the Unlicense, sublicensed from ["Do what you want with it. It is so trivial that noone cares."](https://github.com/PrismLauncher/PrismLauncher/blob/develop/libraries/README.md#javacheck))
## `Gamemmode`
A performance optimization daemon, [GitHub repository](https://github.com/FeralInteractive/gamemode). (Available under `BSD-3-Clause` on their GitHub repository)
## `ForgeWrapper`
An interface for the launching systems of modern Minecraft Forge, [GitHub repository](https://github.com/PrismLauncher/ForgeWrapper). (Available under `MIT` on their GitHub repository)

View file

@ -1,19 +0,0 @@
public final class JavaInfo {
private static final String[] CHECKED_PROPERTIES = new String[] {"os.arch", "java.version", "java.vendor"};
public static void main(String[] args) {
int returnCode = 0;
for (String key : CHECKED_PROPERTIES) {
String property = System.getProperty(key);
if (property != null) {
System.out.println(key + "=" + property);
} else {
returnCode = 1;
}
}
System.exit(returnCode);
}
}

View file

@ -3,7 +3,8 @@ use std::{fmt::Display, ops::Deref, str::FromStr};
use sea_orm::{sea_query::Nullable, DeriveValueType};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
// #[onelauncher_macro::specta]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType, specta::Type)]
pub struct Icon(String);
impl Nullable for Icon {
@ -73,7 +74,7 @@ pub enum IconType {
impl Display for IconType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
write!(f, "{self:?}")
}
}

View file

@ -1,6 +1,7 @@
use sea_orm::{sea_query::{Nullable, ValueType}, TryGetableFromJson};
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Resolution {
pub width: u32,

View file

@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
cfg_match! {
target_os = "windows" => {
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsOsExtra {
@ -17,6 +18,7 @@ cfg_match! {
}
}
target_os = "macos" => {
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsOsExtra {
@ -31,6 +33,7 @@ cfg_match! {
}
}
not(any(target_os = "windows", target_os = "macos")) => {
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsOsExtra {
pub enable_gamemode: Option<bool>,

View file

@ -2,6 +2,7 @@ use std::ops::Deref;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(transparent)]
pub struct DbVec<T>(pub Vec<T>);

Some files were not shown because too many files have changed in this diff Show more