OneLauncher-Cracked/packages/oneclient_mc/src/arguments.rs
Maciek 0ab544f265
feat: custom cluster instances (#859)
* feat: added adding new instances of a cluster with 'start from scratch' option enabling user to create a vanilla/modded cluster; fixed couple pre-existing bugs that wouldn't let other loaders boot up

* feat: fully implemented cluster creation modal design

* chore: refactoring

* feat: expanded the tags section on customization step; found that fabric doesnt work with 1.8.9

* feat: added gallery covers handling for new cluster instances

* feat: added 'redirecting' from fabric to orinthe on versions pre-1.14

* feat: added java-version-override

* feat: added loader types migrations (right now they dont migrate mods because there are no bundles)

* chore: wider loader dropdown for multiloader cluster (if it will be used in the future)

* chore: fixed more issues with isolated and shared dir, with migrations and instances naming

* chore: refactor; taken the loader version dropdown from version picker view back to loader selector

* chore: deleted Quilt from UI

---------

Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
2026-09-29 19:08:21 +02:00

1174 lines
37 KiB
Rust

use std::collections::{HashMap, HashSet};
use std::path::Path;
use interfrost::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
use interfrost::api::modded::SidedDataEntry;
use interfrost::utils::get_path_from_artifact;
use crate::error::McError;
use crate::error::McResult;
use crate::rules::validate_rules;
use oneclient_common::Resolution;
use oneclient_common::constants::{self, DUMMY_REPLACE_NEWLINE};
use oneclient_common::paths;
#[allow(clippy::too_many_arguments)]
pub fn java_arguments(
version_updated: bool,
arguments: Option<&[Argument]>,
natives_path: &Path,
libraries_path: &Path,
classpaths: &str,
version: &str,
mem_max: u32,
custom_args: String,
java_arch: &str,
java_major: u32,
) -> McResult<Vec<String>> {
let mut parsed = Vec::new();
if let Some(args) = arguments {
parse_arguments(
version_updated,
args,
&mut parsed,
|a| {
parse_java_argument(
a.to_string(),
natives_path,
libraries_path,
classpaths,
version,
)
},
java_arch,
)?;
}
if !parsed
.iter()
.any(|arg| arg.starts_with("-Djava.library.path="))
{
parsed.push(format!(
"-Djava.library.path={}",
polyio::canonicalize(natives_path)
.map_err(|_| McError::LibraryPath(natives_path.display().to_string()))?
.display()
));
}
if !parsed.iter().any(|arg| arg == "-cp" || arg == "-classpath") {
parsed.push("-cp".to_string());
parsed.push(classpaths.to_string());
}
let custom = split_custom_args(&custom_args);
parsed.extend(performance_flags(java_major, java_arch, mem_max, &custom));
parsed.push(format!("-Xmx{mem_max}M"));
parsed.extend(custom);
Ok(parsed)
}
const ZGC_MIN_HEAP_MB: u32 = 8192;
const INITIAL_HEAP_MB: u32 = 512;
#[must_use]
pub fn performance_flags(
java_major: u32,
java_arch: &str,
mem_max: u32,
custom_args: &[String],
) -> Vec<String> {
let mut flags = vec![format!("-Xms{}M", mem_max.min(INITIAL_HEAP_MB))];
let collector_chosen = custom_args.iter().any(|arg| is_collector_flag(arg));
let use_zgc =
!collector_chosen && java_major >= 21 && is_64_bit(java_arch) && mem_max >= ZGC_MIN_HEAP_MB;
let use_g1 = !collector_chosen && !use_zgc && java_major >= 8;
if (use_zgc || use_g1) && java_major == 24 {
flags.push("-XX:+UnlockExperimentalVMOptions".to_string());
}
if use_zgc {
flags.push("-XX:+UseZGC".to_string());
if java_major <= 22 {
flags.push("-XX:+ZGenerational".to_string());
}
} else if use_g1 {
flags.push("-XX:+UseG1GC".to_string());
flags.push("-XX:+ParallelRefProcEnabled".to_string());
}
if (use_zgc || use_g1) && java_major >= 24 && is_64_bit(java_arch) {
flags.push("-XX:+UseCompactObjectHeaders".to_string());
}
flags
}
fn is_collector_flag(arg: &str) -> bool {
const COLLECTORS: [&str; 8] = [
"UseG1GC",
"UseZGC",
"UseShenandoahGC",
"UseParallelGC",
"UseParallelOldGC",
"UseConcMarkSweepGC",
"UseSerialGC",
"UseEpsilonGC",
];
arg.starts_with("-XX:") && COLLECTORS.iter().any(|name| arg.contains(name))
}
fn is_64_bit(java_arch: &str) -> bool {
matches!(
java_arch,
"amd64" | "x86_64" | "x64" | "aarch64" | "arm64" | "ppc64le" | "s390x" | "riscv64"
)
}
/// Whitespace-separated double quotes for values containing spaces
/// Must not be passed as one argv entry or the JVM sees one unrecognised option
fn split_custom_args(raw: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut quoted = false;
let mut started = false;
for ch in raw.chars() {
match ch {
'"' => {
quoted = !quoted;
started = true;
}
c if c.is_whitespace() && !quoted => {
if started {
args.push(std::mem::take(&mut current));
started = false;
}
}
c => {
current.push(c);
started = true;
}
}
}
if started {
args.push(current);
}
args
}
#[allow(clippy::too_many_arguments)]
pub fn minecraft_arguments(
version_updated: bool,
args: Option<&[Argument]>,
legacy_args: Option<&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,
java_arch: &str,
) -> McResult<Vec<String>> {
let mut parsed = Vec::new();
// a legacy version states its arguments as a string and a loader merged onto
// one adds a list on top the game needs both, in that order
if let Some(legacy_args) = legacy_args {
for arg in legacy_args.split(' ') {
parsed.push(parse_minecraft_argument(
&arg.replace(' ', DUMMY_REPLACE_NEWLINE),
access_token,
username,
uuid,
version,
asset_index,
game_directory,
assets_directory,
version_type,
resolution,
)?);
}
}
if let Some(args) = args {
parse_arguments(
version_updated,
args,
&mut parsed,
|arg| {
parse_minecraft_argument(
arg,
access_token,
username,
uuid,
version,
asset_index,
game_directory,
assets_directory,
version_type,
resolution,
)
},
java_arch,
)?;
}
drop_repeated_arguments(&mut parsed);
Ok(parsed)
}
fn drop_repeated_arguments(args: &mut Vec<String>) {
let mut seen: HashSet<(String, Option<String>)> = HashSet::new();
let mut kept: Vec<String> = Vec::with_capacity(args.len());
let mut index = 0;
while index < args.len() {
let token = &args[index];
if !token.starts_with("--") {
kept.push(token.clone());
index += 1;
continue;
}
let value = args
.get(index + 1)
.filter(|next| !next.starts_with("--"))
.cloned();
if seen.insert((token.clone(), value.clone())) {
kept.push(token.clone());
if let Some(value) = &value {
kept.push(value.clone());
}
}
index += if value.is_some() { 2 } else { 1 };
}
*args = kept;
}
pub fn append_profile_game_arguments(
args: &mut Vec<String>,
force_fullscreen: Option<bool>,
launch_args: Option<&str>,
) {
if force_fullscreen.unwrap_or(false) {
args.push("--fullscreen".to_string());
}
if let Some(extra) = launch_args.map(str::trim).filter(|s| !s.is_empty()) {
args.push(extra.to_string());
}
}
pub fn processor_arguments<T: AsRef<str>, S: std::hash::BuildHasher>(
libraries_path: &Path,
args: &[T],
data: &HashMap<String, SidedDataEntry, S>,
) -> McResult<Vec<String>> {
let mut parsed = Vec::new();
for arg in args {
let arg = arg.as_ref();
if let Some(coordinate) = arg.strip_prefix('[').and_then(|a| a.strip_suffix(']')) {
parsed.push(get_library(libraries_path, coordinate, true)?);
continue;
}
parsed.push(expand_data_placeholders(libraries_path, arg, data)?);
}
Ok(parsed)
}
fn expand_data_placeholders<S: std::hash::BuildHasher>(
libraries_path: &Path,
arg: &str,
data: &HashMap<String, SidedDataEntry, S>,
) -> McResult<String> {
let mut out = String::with_capacity(arg.len());
let mut rest = arg;
while let Some(open) = rest.find('{') {
let Some(close) = rest[open..].find('}').map(|at| open + at) else {
break;
};
out.push_str(&rest[..open]);
let key = &rest[open + 1..close];
match data.get(key) {
Some(entry) => {
match entry
.client
.strip_prefix('[')
.and_then(|c| c.strip_suffix(']'))
{
Some(coordinate) => {
out.push_str(&get_library(libraries_path, coordinate, true)?);
}
None => out.push_str(&entry.client),
}
}
None => out.push_str(&rest[open..=close]),
}
rest = &rest[close + 1..];
}
out.push_str(rest);
Ok(out)
}
#[tracing::instrument(skip_all, level = "debug")]
pub async fn main_class(path: impl AsRef<std::path::Path>) -> McResult<Option<String>> {
let file = tokio::fs::File::open(path.as_ref()).await?;
let manifest = match polyio::try_read_zip_entry_bytes(
tokio::io::BufReader::new(file),
"META-INF/MANIFEST.MF",
)
.await
{
Err(polyio::IOError::FileNotFoundInZip { .. }) => return Ok(None),
result => result?,
};
Ok(String::from_utf8_lossy(&manifest).lines().find_map(|line| {
line.trim()
.strip_prefix("Main-Class:")
.map(|class| class.trim().to_string())
}))
}
#[allow(clippy::too_many_arguments)]
pub fn parse_minecraft_argument(
argument: &str,
access_token: &str,
username: &str,
uuid: uuid::Uuid,
version: &str,
asset_index: &str,
game_directory: &Path,
assets_directory: &Path,
version_type: VersionType,
resolution: Resolution,
) -> McResult<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")
.replace("${auth_uuid}", &uuid.simple().to_string())
.replace("${uuid}", &uuid.simple().to_string())
.replace(
"${clientid}",
oneclient_common::constants::MICROSOFT_CLIENT_ID,
)
.replace("${user_properties}", "{}")
.replace("${user_type}", "msa")
.replace("${version_name}", version)
.replace("${assets_index_name}", asset_index)
.replace(
"${game_directory}",
&polyio::canonicalize(game_directory)?.display().to_string(),
)
.replace(
"${assets_root}",
&polyio::canonicalize(assets_directory)?
.display()
.to_string(),
)
.replace("${game_assets}", &legacy_assets_path()?)
.replace("${version_type}", version_type.as_str())
.replace("${resolution_width}", &resolution.width.to_string())
.replace("${resolution_height}", &resolution.height.to_string()))
}
fn legacy_assets_path() -> McResult<String> {
let dir = paths::legacy_assets_dir()?;
Ok(polyio::canonicalize(&dir)
.unwrap_or(dir)
.display()
.to_string())
}
fn parse_java_argument(
mut argument: String,
natives_path: &Path,
libraries_path: &Path,
classpaths: &str,
version: &str,
) -> McResult<String> {
argument.retain(|c| !c.is_whitespace());
Ok(argument
.replace(
"${natives_directory}",
&polyio::canonicalize(natives_path)?.display().to_string(),
)
.replace(
"${library_directory}",
&polyio::canonicalize(libraries_path)?.display().to_string(),
)
.replace("${classpath_separator}", constants::CLASSPATH_SEPARATOR)
.replace("${launcher_name}", "OneClient")
.replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
.replace("${version_name}", version)
.replace("${classpath}", classpaths))
}
pub fn classpaths(
libraries_path: &Path,
libraries: &[Library],
client_path: &Path,
java_arch: &str,
updated: bool,
) -> McResult<String> {
let mut chosen: HashMap<String, (ClasspathRank, &str)> = HashMap::new();
for lib in libraries {
if let Some(rules) = &lib.rules
&& !validate_rules(rules, java_arch, updated)
{
continue;
}
if !lib.include_in_classpath {
continue;
}
// natives-only libraries (classifiers, no artifact) have no jar on disk
if !crate::install::has_main_artifact(lib) {
continue;
}
let (artifact, version) = split_artifact_version(&lib.name);
let bundled = is_legacy_asm_bundle(&artifact);
let slot = classpath_slot(artifact);
let rank = (!bundled, version_key(version));
match chosen.get(&slot) {
Some((existing, existing_name)) if *existing >= rank => {
tracing::debug!(
skipped = %lib.name,
kept = %existing_name,
"classpath: dropping superseded library"
);
}
_ => {
chosen.insert(slot, (rank, &lib.name));
}
}
}
let mut classpaths = chosen
.values()
.map(|(_, name)| get_library(libraries_path, name, false))
.collect::<Result<HashSet<_>, _>>()?;
classpaths.insert(polyio::canonicalize(client_path)?.display().to_string());
tracing::debug!(entries = classpaths.len(), "classpath resolved");
Ok(classpaths
.into_iter()
.collect::<Vec<_>>()
.join(constants::CLASSPATH_SEPARATOR))
}
type ClasspathRank = (bool, Vec<u64>);
const ASM_SLOT: &str = "org.ow2.asm:asm";
fn is_legacy_asm_bundle(artifact: &str) -> bool {
matches!(
artifact,
"org.ow2.asm:asm-all" | "org.ow2.asm:asm-debug-all" | "asm:asm-all" | "asm:asm-debug-all"
)
}
fn classpath_slot(artifact: String) -> String {
if is_legacy_asm_bundle(&artifact) {
return ASM_SLOT.to_string();
}
artifact
}
fn split_artifact_version(name: &str) -> (String, &str) {
match name.rsplit_once(':') {
Some((key, version)) if version.starts_with(|c: char| c.is_ascii_digit()) => {
(key.to_string(), version)
}
_ => (name.to_string(), ""),
}
}
fn version_key(version: &str) -> Vec<u64> {
version
.split(['.', '-', '+', '_'])
.map(|seg| seg.parse::<u64>().unwrap_or(0))
.collect()
}
pub fn get_classpath_library<T: AsRef<str>>(
libraries_path: &Path,
libraries: &[T],
) -> McResult<String> {
let classpaths = libraries
.iter()
.map(|lib| get_library(libraries_path, lib.as_ref(), false))
.collect::<Result<Vec<_>, _>>()?;
Ok(classpaths.join(constants::CLASSPATH_SEPARATOR))
}
pub fn get_library(libraries_path: &Path, library: &str, allow_missing: bool) -> McResult<String> {
let mut path = libraries_path.to_path_buf();
path.push(
get_path_from_artifact(library).map_err(|_| McError::LibraryPath(library.to_string()))?,
);
if !path.exists() {
if allow_missing {
return Ok(path.display().to_string());
}
return Err(McError::MissingLibrary {
library: library.to_string(),
path: path.display().to_string(),
});
}
Ok(polyio::canonicalize(&path)?.display().to_string())
}
fn parse_arguments<ParseFn>(
version_updated: bool,
args: &[Argument],
parsed: &mut Vec<String>,
parse_function: ParseFn,
java_arch: &str,
) -> McResult<()>
where
ParseFn: Fn(&str) -> McResult<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 validate_rules(rules, java_arch, version_updated) {
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(())
}
#[cfg(test)]
mod tests {
use super::{
HashMap, Library, Path, SidedDataEntry, ZGC_MIN_HEAP_MB, classpaths,
drop_repeated_arguments, get_library, is_collector_flag, java_arguments,
minecraft_arguments, performance_flags, processor_arguments, split_custom_args,
};
use oneclient_common::Resolution;
#[test]
fn blank_input_contributes_nothing() {
assert!(split_custom_args("").is_empty());
assert!(split_custom_args(" \t ").is_empty());
}
#[test]
fn each_flag_becomes_its_own_argument() {
assert_eq!(
split_custom_args("-XX:+UseG1GC -Xms2G"),
vec!["-XX:+UseG1GC", "-Xms2G"]
);
}
#[test]
fn quotes_hold_a_value_with_spaces_together() {
assert_eq!(
split_custom_args(r#"-Dname="My Server" -Xss1M"#),
vec!["-Dname=My Server", "-Xss1M"]
);
}
#[test]
fn an_empty_quoted_value_survives() {
assert_eq!(split_custom_args(r#"-Dempty="""#), vec!["-Dempty="]);
}
fn flags(java_major: u32, java_arch: &str, mem_max: u32) -> Vec<String> {
performance_flags(java_major, java_arch, mem_max, &[])
}
fn custom(args: &str) -> Vec<String> {
split_custom_args(args)
}
#[test]
fn a_java_8_runtime_gets_g1() {
let j8 = flags(8, "amd64", 4096);
assert_eq!(j8.first().unwrap(), "-Xms512M");
assert!(j8.contains(&"-XX:+UseG1GC".to_string()));
}
#[test]
fn the_versions_told_to_upgrade_still_get_a_tuned_collector() {
for major in [9, 11, 17, 20] {
let flags = flags(major, "amd64", 4096);
assert!(
flags.contains(&"-XX:+UseG1GC".to_string()),
"java {major} should still be tuned, it is what old packs run on"
);
assert!(
!flags.iter().any(|f| f.contains("UseZGC")),
"java {major} predates the generational ZGC we would want"
);
}
}
#[test]
fn a_32_bit_runtime_never_gets_zgc() {
for arch in ["x86", "arm", "i386", "unknown"] {
let flags = flags(25, arch, 16384);
assert!(
!flags.iter().any(|f| f.contains("UseZGC")),
"{arch} must not select a collector it cannot load"
);
assert!(
!flags.contains(&"-XX:+UseCompactObjectHeaders".to_string()),
"{arch} has no compact headers to enable"
);
assert!(flags.contains(&"-XX:+UseG1GC".to_string()));
}
}
#[test]
fn a_small_heap_gets_g1_rather_than_zgc() {
for mem in [2048, 4096, ZGC_MIN_HEAP_MB - 1] {
let flags = flags(21, "amd64", mem);
assert!(
!flags.iter().any(|f| f.contains("UseZGC")),
"{mem}M is not enough heap for ZGC to stay ahead of a modded client"
);
assert!(flags.contains(&"-XX:+UseG1GC".to_string()));
}
}
#[test]
fn a_large_heap_still_gets_zgc() {
for mem in [ZGC_MIN_HEAP_MB, 12288, 16384] {
let flags = flags(21, "amd64", mem);
assert!(flags.contains(&"-XX:+UseZGC".to_string()));
assert!(
!flags.contains(&"-XX:+UseG1GC".to_string()),
"only one collector may be selected"
);
}
}
#[test]
fn the_collector_is_selected_but_not_sized() {
for mem in [2048, 4096, 8192, 16384] {
let flags = flags(17, "amd64", mem);
assert_eq!(
flags,
vec![
"-Xms512M".to_string(),
"-XX:+UseG1GC".to_string(),
"-XX:+ParallelRefProcEnabled".to_string(),
],
"{mem}M should get no hand-picked sizing"
);
}
}
#[test]
fn a_collector_in_the_custom_arguments_replaces_ours() {
for arg in [
"-XX:+UseG1GC",
"-XX:+UseZGC",
"-XX:+UseSerialGC",
"-XX:+UseShenandoahGC",
"-XX:-UseG1GC",
] {
let flags = performance_flags(21, "amd64", 16384, &custom(arg));
assert!(
!flags.iter().any(|f| is_collector_flag(f)),
"{arg} should leave the collector entirely to the user"
);
assert!(
!flags.iter().any(|f| f.starts_with("-XX:G1")),
"{arg} should not be tuned for a collector we did not select"
);
assert_eq!(
flags,
vec!["-Xms512M"],
"{arg} should leave only the collector-independent flags"
);
}
}
#[test]
fn unrelated_custom_arguments_leave_the_collector_alone() {
let flags = performance_flags(21, "amd64", 4096, &custom("-Dfoo=bar -Xss1M"));
assert!(flags.contains(&"-XX:+UseG1GC".to_string()));
}
#[test]
fn zgc_is_asked_for_a_young_generation_only_before_23() {
assert!(flags(21, "amd64", 16384).contains(&"-XX:+ZGenerational".to_string()));
assert!(flags(22, "aarch64", 16384).contains(&"-XX:+ZGenerational".to_string()));
assert!(!flags(23, "amd64", 16384).contains(&"-XX:+ZGenerational".to_string()));
assert!(!flags(25, "amd64", 16384).contains(&"-XX:+ZGenerational".to_string()));
}
#[test]
fn compact_headers_arrive_on_24_whichever_collector_is_chosen() {
for major in [21, 23] {
assert!(
!flags(major, "amd64", 16384).contains(&"-XX:+UseCompactObjectHeaders".to_string()),
"java {major} predates compact object headers"
);
}
for mem in [4096, 16384] {
let j24 = flags(24, "amd64", mem);
let unlock = j24
.iter()
.position(|f| f == "-XX:+UnlockExperimentalVMOptions")
.expect("24 needs the unlock flag");
let compact = j24
.iter()
.position(|f| f == "-XX:+UseCompactObjectHeaders")
.expect("24 supports compact headers");
assert!(
unlock < compact,
"the unlock flag must precede the option it unlocks"
);
}
let j25_zgc = flags(25, "amd64", 16384);
assert!(j25_zgc.contains(&"-XX:+UseCompactObjectHeaders".to_string()));
assert!(!j25_zgc.contains(&"-XX:+UnlockExperimentalVMOptions".to_string()));
assert!(flags(25, "amd64", 4096).contains(&"-XX:+UseCompactObjectHeaders".to_string()));
}
#[test]
fn every_experimental_option_sits_after_the_unlock() {
const EXPERIMENTAL: [&str; 1] = ["-XX:+UseCompactObjectHeaders"];
for major in [8, 17, 21, 24, 25] {
for mem in [4096, 16384] {
let flags = flags(major, "amd64", mem);
let Some(unlock) = flags
.iter()
.position(|f| f == "-XX:+UnlockExperimentalVMOptions")
else {
assert!(
!flags
.iter()
.any(|f| EXPERIMENTAL.iter().any(|e| f.starts_with(e))
&& !(major >= 25 && f == "-XX:+UseCompactObjectHeaders")),
"java {major} at {mem}M emits an experimental option with no unlock"
);
continue;
};
for (i, flag) in flags.iter().enumerate() {
if EXPERIMENTAL.iter().any(|e| flag.starts_with(e)) {
assert!(
unlock < i,
"java {major} at {mem}M puts {flag} before the unlock"
);
}
}
}
}
}
#[test]
fn string_deduplication_is_never_added() {
for major in [7, 8, 17, 21, 24, 25] {
assert!(
!flags(major, "amd64", 4096).contains(&"-XX:+UseStringDeduplication".to_string()),
"java {major} should not be told to deduplicate strings"
);
}
}
#[test]
fn a_modern_runtime_gets_the_full_set() {
assert_eq!(
flags(25, "aarch64", 16384),
vec!["-Xms512M", "-XX:+UseZGC", "-XX:+UseCompactObjectHeaders"]
);
}
#[test]
fn a_tiny_profile_never_starts_above_its_ceiling() {
assert_eq!(
flags(21, "amd64", 256),
vec!["-Xms256M", "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled",],
"a start size above the maximum is refused by the JVM outright"
);
}
#[test]
fn a_java_7_runtime_is_left_untouched() {
assert_eq!(flags(7, "amd64", 4096), vec!["-Xms512M"]);
}
fn install_jars(tag: &str, jars: &[&str]) -> (std::path::PathBuf, std::path::PathBuf) {
let dir = std::env::temp_dir().join(tag);
for jar in jars {
let path = dir.join(jar);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, b"").unwrap();
}
let client = dir.join("client.jar");
std::fs::write(&client, b"").unwrap();
(dir, client)
}
#[test]
fn a_legacy_asm_bundle_loses_to_the_split_modules() {
let libraries: Vec<Library> = serde_json::from_str(
r#"[
{ "name": "org.ow2.asm:asm-all:4.1" },
{ "name": "org.ow2.asm:asm:9.10.1" },
{ "name": "org.ow2.asm:asm-tree:9.10.1" },
{ "name": "net.minecraft:launchwrapper:1.5" }
]"#,
)
.unwrap();
let (dir, client) = install_jars(
"oneclient-classpath-asm-all",
&[
"org/ow2/asm/asm-all/4.1/asm-all-4.1.jar",
"org/ow2/asm/asm/9.10.1/asm-9.10.1.jar",
"org/ow2/asm/asm-tree/9.10.1/asm-tree-9.10.1.jar",
"net/minecraft/launchwrapper/1.5/launchwrapper-1.5.jar",
],
);
let cp = classpaths(&dir, &libraries, &client, "x86", false).unwrap();
assert!(
!cp.contains("asm-all"),
"fabric aborts on two copies of org/objectweb/asm/ClassReader.class: {cp}"
);
assert!(cp.contains("asm-9.10.1.jar"), "{cp}");
assert!(cp.contains("asm-tree-9.10.1.jar"), "{cp}");
assert!(cp.contains("launchwrapper-1.5.jar"), "{cp}");
}
#[test]
fn a_legacy_asm_bundle_stays_when_nothing_supersedes_it() {
let libraries: Vec<Library> = serde_json::from_str(
r#"[
{ "name": "org.ow2.asm:asm-all:4.1" },
{ "name": "net.minecraft:launchwrapper:1.5" }
]"#,
)
.unwrap();
let (dir, client) = install_jars(
"oneclient-classpath-asm-only",
&[
"org/ow2/asm/asm-all/4.1/asm-all-4.1.jar",
"net/minecraft/launchwrapper/1.5/launchwrapper-1.5.jar",
],
);
let cp = classpaths(&dir, &libraries, &client, "x86", false).unwrap();
assert!(cp.contains("asm-all-4.1.jar"), "{cp}");
}
#[test]
fn a_newer_bundle_does_not_outrank_an_older_split_module() {
let libraries: Vec<Library> = serde_json::from_str(
r#"[
{ "name": "org.ow2.asm:asm-debug-all:5.2" },
{ "name": "org.ow2.asm:asm:5.0.3" }
]"#,
)
.unwrap();
let (dir, client) = install_jars(
"oneclient-classpath-asm-debug",
&[
"org/ow2/asm/asm-debug-all/5.2/asm-debug-all-5.2.jar",
"org/ow2/asm/asm/5.0.3/asm-5.0.3.jar",
],
);
let cp = classpaths(&dir, &libraries, &client, "x86", false).unwrap();
assert!(!cp.contains("asm-debug-all"), "{cp}");
assert!(cp.contains("asm-5.0.3.jar"), "{cp}");
}
#[test]
fn a_missing_classpath_jar_names_the_library_it_could_not_find() {
let libraries: Vec<Library> = serde_json::from_str(
r#"[
{ "name": "net.fabricmc:fabric-loader:0.19.5" },
{ "name": "net.fabricmc:intermediary:1.8.9" }
]"#,
)
.unwrap();
let dir = std::env::temp_dir().join("oneclient-classpath-missing");
let present = dir.join("net/fabricmc/fabric-loader/0.19.5/fabric-loader-0.19.5.jar");
std::fs::create_dir_all(present.parent().unwrap()).unwrap();
std::fs::write(&present, b"").unwrap();
let client = dir.join("client.jar");
std::fs::write(&client, b"").unwrap();
let err = classpaths(&dir, &libraries, &client, "x86", false)
.expect_err("a jar that never downloaded must not reach the jvm as a silent gap");
let message = err.to_string();
assert!(
message.contains("net.fabricmc:intermediary:1.8.9"),
"{message}"
);
assert!(message.contains("intermediary-1.8.9.jar"), "{message}");
}
#[test]
fn a_tolerated_missing_library_keeps_its_expected_path() {
let dir = std::env::temp_dir().join("oneclient-library-tolerated");
let path = get_library(&dir, "net.fabricmc:intermediary:1.8.9", true)
.expect("allow_missing hands back the path it would have used");
assert!(path.ends_with("intermediary-1.8.9.jar"), "{path}");
}
#[test]
fn a_natives_only_library_is_not_a_classpath_entry() {
let libraries: Vec<Library> = serde_json::from_str(
r#"[
{ "name": "net.fabricmc:fabric-loader:0.19.5" },
{ "name": "org.lwjgl.lwjgl:lwjgl:2.9.4+legacyfabric.15",
"downloads": { "artifact": { "sha1": "a", "size": 1, "url": "https://maven.legacyfabric.net/lwjgl.jar" } } },
{ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4+legacyfabric.15",
"downloads": { "classifiers": { "natives-osx": { "sha1": "b", "size": 2, "url": "https://maven.legacyfabric.net/natives-osx.jar" } } },
"natives": { "osx": "natives-osx" } }
]"#,
)
.unwrap();
let dir = std::env::temp_dir().join("oneclient-classpath-test");
let client = dir.join("client.jar");
for lib in [
"net/fabricmc/fabric-loader/0.19.5/fabric-loader-0.19.5.jar",
"org/lwjgl/lwjgl/lwjgl/2.9.4+legacyfabric.15/lwjgl-2.9.4+legacyfabric.15.jar",
] {
let path = dir.join(lib);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, b"").unwrap();
}
std::fs::write(&client, b"").unwrap();
let cp = classpaths(&dir, &libraries, &client, "x86", false)
.expect("a jar that was never downloaded must not fail the launch");
assert!(!cp.contains("lwjgl-platform"));
assert!(cp.contains("fabric-loader") && cp.contains("lwjgl-2.9.4"));
}
#[test]
fn a_loader_on_a_legacy_version_still_gets_a_classpath() {
// ornithe's manifest declares two -D flags and nothing else
// 1.8.9 has no `arguments` block to supply -cp or the natives path
let loader_jvm: Vec<interfrost::api::minecraft::Argument> = serde_json::from_str(
r#"["-Dfabric.fixPackageAccess=true", "-Dfabric.gameVersion=1.8.9"]"#,
)
.unwrap();
let natives = std::env::temp_dir();
let args = java_arguments(
false,
Some(&loader_jvm),
&natives,
&natives,
"/libs/fabric-loader.jar",
"1.8.9",
2048,
String::new(),
"arm64",
21,
)
.unwrap();
assert!(args.contains(&"-Dfabric.fixPackageAccess=true".to_string()));
let cp = args.iter().position(|a| a == "-cp").expect("no classpath");
assert_eq!(args[cp + 1], "/libs/fabric-loader.jar");
assert!(args.iter().any(|a| a.starts_with("-Djava.library.path=")));
}
#[test]
fn a_loader_game_argument_does_not_replace_the_legacy_string() {
let loader_game: Vec<interfrost::api::minecraft::Argument> =
serde_json::from_str(r#"["--fabric"]"#).unwrap();
let dir = std::env::temp_dir();
let args = minecraft_arguments(
false,
Some(&loader_game),
Some("--username ${auth_player_name} --gameDir ${game_directory}"),
"token",
"player",
uuid::Uuid::nil(),
"1.8.9",
"1.8",
&dir,
&dir,
interfrost::api::minecraft::VersionType::Release,
Resolution::default(),
"arm64",
)
.unwrap();
assert_eq!(args.first().unwrap(), "--username");
assert_eq!(args[1], "player");
assert_eq!(args.last().unwrap(), "--fabric");
}
#[test]
fn a_loader_repeating_the_legacy_string_is_not_passed_twice() {
let loader_game: Vec<interfrost::api::minecraft::Argument> = serde_json::from_str(
r#"["--username", "${auth_player_name}", "--gameDir", "${game_directory}",
"--tweakClass", "net.minecraftforge.fml.common.launcher.FMLTweaker"]"#,
)
.unwrap();
let dir = std::env::temp_dir();
let args = minecraft_arguments(
false,
Some(&loader_game),
Some(
"--username ${auth_player_name} --gameDir ${game_directory} --tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker",
),
"token",
"player",
uuid::Uuid::nil(),
"1.11",
"1.11",
&dir,
&dir,
interfrost::api::minecraft::VersionType::Release,
Resolution::default(),
"arm64",
)
.unwrap();
assert_eq!(args.iter().filter(|a| *a == "--gameDir").count(), 1);
assert_eq!(args.iter().filter(|a| *a == "--username").count(), 1);
assert_eq!(args.iter().filter(|a| *a == "--tweakClass").count(), 1);
}
#[test]
fn an_embedded_placeholder_keeps_its_surrounding_text() {
let mut data = HashMap::new();
data.insert(
"ROOT".to_string(),
SidedDataEntry {
client: "/meta".to_string(),
server: String::new(),
},
);
data.insert(
"BINPATCH".to_string(),
SidedDataEntry {
client: "/meta/libraries/patch.lzma".to_string(),
server: String::new(),
},
);
let args = [
"--task",
"PROCESS_MINECRAFT_JAR",
"--extract-libraries-to",
"{ROOT}/libraries/",
"--apply-patches",
"{BINPATCH}",
];
let parsed = processor_arguments(Path::new("/libs"), &args, &data).unwrap();
assert_eq!(parsed.len(), args.len());
assert_eq!(parsed[3], "/meta/libraries/");
assert_eq!(parsed[4], "--apply-patches");
assert_eq!(parsed[5], "/meta/libraries/patch.lzma");
}
#[test]
fn an_unknown_placeholder_is_left_alone_rather_than_dropped() {
let data: HashMap<String, SidedDataEntry> = HashMap::new();
let args = ["--flag", "{NOPE}", "--after"];
let parsed = processor_arguments(Path::new("/libs"), &args, &data).unwrap();
assert_eq!(parsed, vec!["--flag", "{NOPE}", "--after"]);
}
#[test]
fn distinct_tweakers_both_survive() {
let mut args = vec![
"--tweakClass".to_string(),
"forge.FMLTweaker".to_string(),
"--tweakClass".to_string(),
"optifine.OptiFineTweaker".to_string(),
"--gameDir".to_string(),
"/games".to_string(),
"--gameDir".to_string(),
"/games".to_string(),
];
drop_repeated_arguments(&mut args);
assert_eq!(args.iter().filter(|a| *a == "--tweakClass").count(), 2);
assert_eq!(args.iter().filter(|a| *a == "--gameDir").count(), 1);
}
}