mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 08:07:53 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5221cc92d | ||
|
|
a7841ccb18 | ||
|
|
5091896e2a |
No files matched your search
@@ -0,0 +1,322 @@
|
||||
//! The command that starts the real Minecraft: Java Edition, read from its version JSON as the
|
||||
//! official launcher reads it: the main class, the classpath of this system's libraries and the
|
||||
//! client jar, the JVM and game arguments with their rules and `${…}` placeholders (or the old
|
||||
//! `minecraftArguments` line), an old version's natives unpacked into a folder of their own, and
|
||||
//! the logging setup Mojang gives (for 1.7 to 1.18, the one that keeps Log4Shell out).
|
||||
//!
|
||||
//! The files are where `mojang::download` puts them: `versions/`, `libraries/` and `assets/` in
|
||||
//! one game folder. Nothing here reaches the network.
|
||||
|
||||
use crate::mojang;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
/// Who plays: the account's name, UUID, Minecraft access token and Xbox user id.
|
||||
pub struct Player {
|
||||
pub name: String,
|
||||
pub uuid: String,
|
||||
pub token: String,
|
||||
pub xuid: String,
|
||||
}
|
||||
|
||||
/// Everything the command needs besides the version JSON.
|
||||
pub struct Launch<'a> {
|
||||
/// Holds `versions/`, `libraries/`, `assets/`.
|
||||
pub minecraft: &'a Path,
|
||||
/// The instance's game folder (saves, options.txt, mods).
|
||||
pub game_dir: &'a Path,
|
||||
/// Where an old version's natives are unpacked.
|
||||
pub natives: &'a Path,
|
||||
/// The `java` program.
|
||||
pub java: &'a Path,
|
||||
pub memory_mb: u32,
|
||||
/// The player's own JVM arguments.
|
||||
pub jvm_args: &'a [String],
|
||||
pub player: &'a Player,
|
||||
/// `osx`, `linux` or `windows`, as version files name systems.
|
||||
pub os: &'a str,
|
||||
}
|
||||
|
||||
/// This system's name in version files.
|
||||
pub fn os_name() -> &'static str {
|
||||
match crate::sources::Os::current() {
|
||||
crate::sources::Os::Linux => "linux",
|
||||
crate::sources::Os::MacOs => "osx",
|
||||
crate::sources::Os::Windows => "windows",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a rule list lets this system in: the last matching rule decides; no rules allow.
|
||||
/// Every optional feature (demo, custom resolution, quick play) is off.
|
||||
fn allowed(rules: &Value, os: &str) -> bool {
|
||||
let Some(rules) = rules.as_array() else {
|
||||
return true;
|
||||
};
|
||||
let arch_matches = |arch: &str| match arch {
|
||||
"x86" => cfg!(target_arch = "x86"),
|
||||
"arm64" | "aarch64" => cfg!(target_arch = "aarch64"),
|
||||
"x86_64" | "amd64" => cfg!(target_arch = "x86_64"),
|
||||
_ => false,
|
||||
};
|
||||
let mut allow = false;
|
||||
for rule in rules {
|
||||
let system = &rule["os"];
|
||||
let matches = system["name"].as_str().is_none_or(|n| n == os)
|
||||
&& system["arch"].as_str().is_none_or(arch_matches)
|
||||
&& rule["features"]
|
||||
.as_object()
|
||||
.is_none_or(|f| f.values().all(|v| v == false));
|
||||
if matches {
|
||||
allow = rule["action"] == "allow";
|
||||
}
|
||||
}
|
||||
allow
|
||||
}
|
||||
|
||||
/// The libraries this system uses, then the client jar.
|
||||
pub fn classpath(version: &Value, minecraft: &Path, os: &str) -> Result<Vec<PathBuf>, String> {
|
||||
let id = version["id"].as_str().ok_or("The version file has no id")?;
|
||||
let mut paths: Vec<PathBuf> = Vec::new();
|
||||
for library in version["libraries"].as_array().into_iter().flatten() {
|
||||
if !allowed(&library["rules"], os) {
|
||||
continue;
|
||||
}
|
||||
if let Some(path) = library["downloads"]["artifact"]["path"].as_str() {
|
||||
let path = minecraft.join("libraries").join(mojang::relative(path)?);
|
||||
if !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
paths.push(
|
||||
minecraft
|
||||
.join("versions")
|
||||
.join(mojang::relative(id)?)
|
||||
.join(format!("{id}.jar")),
|
||||
);
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
/// Unpacks this system's natives of an old version (a library's classifier) into `to`, without
|
||||
/// what the library's `extract.exclude` names (META-INF/). Versions since 1.19 have none.
|
||||
pub fn unpack_natives(
|
||||
version: &Value,
|
||||
minecraft: &Path,
|
||||
os: &str,
|
||||
to: &Path,
|
||||
) -> Result<(), String> {
|
||||
fs::create_dir_all(to).map_err(|e| format!("{}: {e}", to.display()))?;
|
||||
for library in version["libraries"].as_array().into_iter().flatten() {
|
||||
if !allowed(&library["rules"], os) {
|
||||
continue;
|
||||
}
|
||||
let Some(key) = mojang::native_classifier(library, os) else {
|
||||
continue;
|
||||
};
|
||||
let Some(path) = library["downloads"]["classifiers"][key.as_str()]["path"].as_str() else {
|
||||
continue;
|
||||
};
|
||||
let jar = minecraft.join("libraries").join(mojang::relative(path)?);
|
||||
let exclude: Vec<&str> = library["extract"]["exclude"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_str)
|
||||
.collect();
|
||||
let file = fs::File::open(&jar).map_err(|e| format!("{}: {e}", jar.display()))?;
|
||||
let mut zip = zip::ZipArchive::new(file).map_err(|e| format!("{}: {e}", jar.display()))?;
|
||||
for i in 0..zip.len() {
|
||||
let mut entry = zip.by_index(i).map_err(|e| e.to_string())?;
|
||||
let Some(name) = entry.enclosed_name() else {
|
||||
continue;
|
||||
};
|
||||
let text = name.to_string_lossy().replace('\\', "/");
|
||||
if entry.is_dir() || exclude.iter().any(|x| text.starts_with(x)) {
|
||||
continue;
|
||||
}
|
||||
let out = to.join(&name);
|
||||
fs::create_dir_all(out.parent().unwrap()).map_err(|e| e.to_string())?;
|
||||
let mut file = fs::File::create(&out).map_err(|e| format!("{}: {e}", out.display()))?;
|
||||
std::io::copy(&mut entry, &mut file).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Where an old version reads its assets by name (`virtual` indexes, `map_to_resources`), with
|
||||
/// the objects copied there once; the asset store itself for every other version.
|
||||
pub fn game_assets(version: &Value, minecraft: &Path, game_dir: &Path) -> Result<PathBuf, String> {
|
||||
let id = version["assetIndex"]["id"].as_str().unwrap_or("legacy");
|
||||
let assets = minecraft.join("assets");
|
||||
let index = assets
|
||||
.join("indexes")
|
||||
.join(mojang::relative(&format!("{id}.json"))?);
|
||||
let Ok(text) = fs::read_to_string(&index) else {
|
||||
return Ok(assets);
|
||||
};
|
||||
let index: Value =
|
||||
serde_json::from_str(&text).map_err(|e| format!("{}: {e}", index.display()))?;
|
||||
let to = if index["map_to_resources"] == true {
|
||||
game_dir.join("resources")
|
||||
} else if index["virtual"] == true {
|
||||
assets.join("virtual").join(mojang::relative(id)?)
|
||||
} else {
|
||||
return Ok(assets);
|
||||
};
|
||||
for (name, object) in index["objects"].as_object().into_iter().flatten() {
|
||||
let Some(hash) = object["hash"].as_str().filter(|h| h.len() == 40) else {
|
||||
continue;
|
||||
};
|
||||
let out = to.join(mojang::relative(name)?);
|
||||
if !out.is_file() {
|
||||
let from = assets.join("objects").join(&hash[..2]).join(hash);
|
||||
fs::create_dir_all(out.parent().unwrap())
|
||||
.and_then(|_| fs::copy(&from, &out))
|
||||
.map_err(|e| format!("{}: {e}", from.display()))?;
|
||||
}
|
||||
}
|
||||
Ok(to)
|
||||
}
|
||||
|
||||
/// The arguments of one list (`arguments.jvm` or `arguments.game`), their rules applied.
|
||||
fn list(args: &Value, os: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for arg in args.as_array().into_iter().flatten() {
|
||||
match arg {
|
||||
Value::String(s) => out.push(s.clone()),
|
||||
Value::Object(o) if allowed(&o["rules"], os) => match &o["value"] {
|
||||
Value::String(s) => out.push(s.clone()),
|
||||
Value::Array(values) => {
|
||||
out.extend(values.iter().filter_map(Value::as_str).map(String::from))
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `${name}` replaced by its value; an unknown one stays as written.
|
||||
fn fill(arg: &str, vars: &HashMap<&str, String>) -> String {
|
||||
let mut out = String::new();
|
||||
let mut rest = arg;
|
||||
while let Some(start) = rest.find("${") {
|
||||
let Some(len) = rest[start..].find('}') else {
|
||||
break;
|
||||
};
|
||||
out.push_str(&rest[..start]);
|
||||
let key = &rest[start + 2..start + len];
|
||||
match vars.get(key) {
|
||||
Some(value) => out.push_str(value),
|
||||
None => out.push_str(&rest[start..start + len + 1]),
|
||||
}
|
||||
rest = &rest[start + len + 1..];
|
||||
}
|
||||
out.push_str(rest);
|
||||
out
|
||||
}
|
||||
|
||||
/// The command that starts `version` (its JSON) as `launch` says, run in the game folder.
|
||||
/// Unpacks natives and lays out old assets first.
|
||||
pub fn command(version: &Value, launch: &Launch) -> Result<Command, String> {
|
||||
let id = version["id"].as_str().ok_or("The version file has no id")?;
|
||||
let main = version["mainClass"]
|
||||
.as_str()
|
||||
.ok_or("The version file has no mainClass")?;
|
||||
let separator = if cfg!(windows) { ";" } else { ":" };
|
||||
let classpath = classpath(version, launch.minecraft, launch.os)?;
|
||||
unpack_natives(version, launch.minecraft, launch.os, launch.natives)?;
|
||||
let assets = launch.minecraft.join("assets");
|
||||
let game_assets = game_assets(version, launch.minecraft, launch.game_dir)?;
|
||||
let path = |p: &Path| p.to_string_lossy().into_owned();
|
||||
let p = launch.player;
|
||||
let vars: HashMap<&str, String> = HashMap::from([
|
||||
("auth_player_name", p.name.clone()),
|
||||
("auth_uuid", p.uuid.clone()),
|
||||
("auth_access_token", p.token.clone()),
|
||||
("auth_session", format!("token:{}:{}", p.token, p.uuid)),
|
||||
("auth_xuid", p.xuid.clone()),
|
||||
("clientid", String::new()),
|
||||
("user_type", "msa".into()),
|
||||
("user_properties", "{}".into()),
|
||||
("version_name", id.to_string()),
|
||||
(
|
||||
"version_type",
|
||||
version["type"].as_str().unwrap_or("release").into(),
|
||||
),
|
||||
("game_directory", path(launch.game_dir)),
|
||||
("assets_root", path(&assets)),
|
||||
("game_assets", path(&game_assets)),
|
||||
(
|
||||
"assets_index_name",
|
||||
version["assetIndex"]["id"]
|
||||
.as_str()
|
||||
.unwrap_or("legacy")
|
||||
.into(),
|
||||
),
|
||||
("natives_directory", path(launch.natives)),
|
||||
(
|
||||
"library_directory",
|
||||
path(&launch.minecraft.join("libraries")),
|
||||
),
|
||||
("classpath_separator", separator.into()),
|
||||
(
|
||||
"classpath",
|
||||
classpath
|
||||
.iter()
|
||||
.map(|c| path(c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator),
|
||||
),
|
||||
("launcher_name", "horizon-launcher".into()),
|
||||
("launcher_version", env!("CARGO_PKG_VERSION").into()),
|
||||
]);
|
||||
let mut jvm = vec![
|
||||
format!("-Xmx{}M", launch.memory_mb),
|
||||
// Whatever the logging setup, never look names up in log messages.
|
||||
"-Dlog4j2.formatMsgNoLookups=true".to_string(),
|
||||
];
|
||||
let logging = &version["logging"]["client"];
|
||||
if let (Some(arg), Some(file)) = (logging["argument"].as_str(), logging["file"]["id"].as_str())
|
||||
{
|
||||
let config = launch
|
||||
.minecraft
|
||||
.join("assets")
|
||||
.join("log_configs")
|
||||
.join(mojang::relative(file)?);
|
||||
jvm.push(arg.replace("${path}", &path(&config)));
|
||||
}
|
||||
let (jvm_list, game) = match version["arguments"].as_object() {
|
||||
Some(a) => (list(&a["jvm"], launch.os), list(&a["game"], launch.os)),
|
||||
// Before 1.13: one line of game arguments, and the JVM's natives and classpath.
|
||||
None => (
|
||||
vec![
|
||||
"-Djava.library.path=${natives_directory}".into(),
|
||||
"-cp".into(),
|
||||
"${classpath}".into(),
|
||||
],
|
||||
version["minecraftArguments"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.split_whitespace()
|
||||
.map(String::from)
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
jvm.extend(jvm_list.iter().map(|a| fill(a, &vars)));
|
||||
jvm.extend(launch.jvm_args.iter().cloned());
|
||||
let mut cmd = Command::new(launch.java);
|
||||
cmd.current_dir(launch.game_dir)
|
||||
.args(&jvm)
|
||||
.arg(main)
|
||||
.args(game.iter().map(|a| fill(a, &vars)));
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "java_launch/tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,212 @@
|
||||
//! Version files shaped like Mojang's (a 1.21 and a 1.8.9), with the files they name made up in
|
||||
//! a scratch folder.
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use std::io::Write;
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("horizon-java-launch-{name}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
dir
|
||||
}
|
||||
|
||||
/// `rel` (`a/b/c`) under `base`, one part at a time as the code joins them (Windows paths).
|
||||
fn under(base: &Path, rel: &str) -> PathBuf {
|
||||
rel.split('/')
|
||||
.fold(base.to_path_buf(), |path, part| path.join(part))
|
||||
}
|
||||
|
||||
fn player() -> Player {
|
||||
Player {
|
||||
name: "Notch".into(),
|
||||
uuid: "069a79f444e94726a5befca90e38aaf5".into(),
|
||||
token: "mc-token".into(),
|
||||
xuid: "2535400000000000".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn args(cmd: &Command) -> Vec<String> {
|
||||
cmd.get_args()
|
||||
.map(|a| a.to_string_lossy().into_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn modern() -> Value {
|
||||
json!({
|
||||
"id": "1.21.4", "type": "release", "mainClass": "net.minecraft.client.main.Main",
|
||||
"assetIndex": {"id": "19"},
|
||||
"arguments": {
|
||||
"game": ["--username", "${auth_player_name}", "--version", "${version_name}",
|
||||
"--gameDir", "${game_directory}", "--assetsDir", "${assets_root}",
|
||||
"--assetIndex", "${assets_index_name}", "--uuid", "${auth_uuid}",
|
||||
"--accessToken", "${auth_access_token}", "--xuid", "${auth_xuid}",
|
||||
"--userType", "${user_type}",
|
||||
{"rules": [{"action": "allow", "features": {"is_demo_user": true}}], "value": "--demo"},
|
||||
{"rules": [{"action": "allow", "features": {"has_custom_resolution": true}}],
|
||||
"value": ["--width", "${resolution_width}"]}],
|
||||
"jvm": [{"rules": [{"action": "allow", "os": {"name": "osx"}}], "value": ["-XstartOnFirstThread"]},
|
||||
{"rules": [{"action": "allow", "os": {"name": "windows", "arch": "x86"}}], "value": "-Xss1M"},
|
||||
"-Djava.library.path=${natives_directory}", "-Dminecraft.launcher.brand=${launcher_name}",
|
||||
"-cp", "${classpath}"]
|
||||
},
|
||||
"logging": {"client": {"argument": "-Dlog4j.configurationFile=${path}",
|
||||
"file": {"id": "client-1.21.2.xml"}}},
|
||||
"libraries": [
|
||||
{"name": "a:common:1", "downloads": {"artifact": {"path": "a/common/1/common-1.jar"}}},
|
||||
{"name": "a:mac:1", "downloads": {"artifact": {"path": "a/mac/1/mac-1.jar"}},
|
||||
"rules": [{"action": "allow", "os": {"name": "osx"}}]},
|
||||
{"name": "a:not-mac:1", "downloads": {"artifact": {"path": "a/not-mac/1/not-mac-1.jar"}},
|
||||
"rules": [{"action": "allow"}, {"action": "disallow", "os": {"name": "osx"}}]},
|
||||
{"name": "a:common:1-again", "downloads": {"artifact": {"path": "a/common/1/common-1.jar"}}}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_modern_version_gets_its_rules_placeholders_and_classpath() {
|
||||
let dir = scratch("modern");
|
||||
let (mc, game, natives) = (dir.join("mc"), dir.join("game"), dir.join("natives"));
|
||||
let java = dir.join("java");
|
||||
let p = player();
|
||||
let extra = vec!["-XX:+UseZGC".to_string()];
|
||||
let launch = |os| Launch {
|
||||
minecraft: &mc,
|
||||
game_dir: &game,
|
||||
natives: &natives,
|
||||
java: &java,
|
||||
memory_mb: 6144,
|
||||
jvm_args: &extra,
|
||||
player: &p,
|
||||
os,
|
||||
};
|
||||
let cmd = command(&modern(), &launch("osx")).unwrap();
|
||||
let a = args(&cmd);
|
||||
let at = |s: &str| {
|
||||
a.iter()
|
||||
.position(|x| x == s)
|
||||
.unwrap_or_else(|| panic!("no {s} in {a:?}"))
|
||||
};
|
||||
assert_eq!(a[0], "-Xmx6144M");
|
||||
assert!(a.contains(&"-Dlog4j2.formatMsgNoLookups=true".into()));
|
||||
let config = under(&mc, "assets/log_configs/client-1.21.2.xml");
|
||||
assert!(a.contains(&format!("-Dlog4j.configurationFile={}", config.display())));
|
||||
assert!(at("-XstartOnFirstThread") < at("net.minecraft.client.main.Main"));
|
||||
assert!(at("-XX:+UseZGC") < at("net.minecraft.client.main.Main"));
|
||||
assert!(a.contains(&"-Dminecraft.launcher.brand=horizon-launcher".into()));
|
||||
assert_eq!(a[at("--username") + 1], "Notch");
|
||||
assert_eq!(a[at("--accessToken") + 1], "mc-token");
|
||||
assert_eq!(a[at("--assetIndex") + 1], "19");
|
||||
assert_eq!(a[at("--gameDir") + 1], game.to_string_lossy());
|
||||
assert_eq!(a[at("--userType") + 1], "msa");
|
||||
// Features are off: no demo, no custom size.
|
||||
assert!(!a.iter().any(|x| x == "--demo" || x == "--width"));
|
||||
let sep = if cfg!(windows) { ";" } else { ":" };
|
||||
let cp: Vec<String> = a[at("-cp") + 1].split(sep).map(String::from).collect();
|
||||
// Library paths are Mojang's, `/` and all (`mojang::relative`): Windows takes them.
|
||||
let lib = |p: &str| mc.join("libraries").join(p).to_string_lossy().into_owned();
|
||||
assert_eq!(
|
||||
cp,
|
||||
[
|
||||
lib("a/common/1/common-1.jar"),
|
||||
lib("a/mac/1/mac-1.jar"),
|
||||
under(&mc, "versions/1.21.4/1.21.4.jar")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
]
|
||||
);
|
||||
assert_eq!(cmd.get_current_dir(), Some(game.as_path()));
|
||||
|
||||
let linux = args(&command(&modern(), &launch("linux")).unwrap());
|
||||
assert!(!linux.contains(&"-XstartOnFirstThread".into()));
|
||||
assert!(linux.iter().any(|x| x.contains("not-mac-1.jar")));
|
||||
// The x86-only rule holds on 32-bit Windows alone.
|
||||
let windows = args(&command(&modern(), &launch("windows")).unwrap());
|
||||
assert_eq!(
|
||||
windows.contains(&"-Xss1M".into()),
|
||||
cfg!(target_arch = "x86")
|
||||
);
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A 1.8.9-shaped version: `minecraftArguments`, natives unpacked without META-INF, and a
|
||||
/// virtual asset index laid out by name.
|
||||
#[test]
|
||||
fn an_old_version_gets_its_natives_and_assets_by_name() {
|
||||
let dir = scratch("old");
|
||||
let (mc, game, natives) = (dir.join("mc"), dir.join("game"), dir.join("natives"));
|
||||
let jar = mc.join("libraries/org/lwjgl/lwjgl-platform-2.9.4-natives-osx.jar");
|
||||
fs::create_dir_all(jar.parent().unwrap()).unwrap();
|
||||
let mut zip = zip::ZipWriter::new(fs::File::create(&jar).unwrap());
|
||||
let opts = zip::write::SimpleFileOptions::default();
|
||||
for (name, body) in [
|
||||
("liblwjgl.dylib", b"lib".as_slice()),
|
||||
("META-INF/MANIFEST.MF", b"m"),
|
||||
] {
|
||||
zip.start_file(name, opts).unwrap();
|
||||
zip.write_all(body).unwrap();
|
||||
}
|
||||
zip.finish().unwrap();
|
||||
let hash = "0123456789abcdef0123456789abcdef01234567";
|
||||
let object = mc.join("assets/objects/01").join(hash);
|
||||
fs::create_dir_all(object.parent().unwrap()).unwrap();
|
||||
fs::write(&object, b"ogg").unwrap();
|
||||
fs::create_dir_all(mc.join("assets/indexes")).unwrap();
|
||||
fs::write(
|
||||
mc.join("assets/indexes/1.8.json"),
|
||||
json!({"virtual": true, "objects": {"sounds/step/grass1.ogg": {"hash": hash, "size": 3}}})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
let version = json!({
|
||||
"id": "1.8.9", "type": "release", "mainClass": "net.minecraft.client.main.Main",
|
||||
"assetIndex": {"id": "1.8"},
|
||||
"minecraftArguments": "--username ${auth_player_name} --session ${auth_session} --assetsDir ${game_assets} --userProperties ${user_properties} --unknown ${nothing}",
|
||||
"libraries": [{
|
||||
"name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4",
|
||||
"natives": {"osx": "natives-osx"},
|
||||
"extract": {"exclude": ["META-INF/"]},
|
||||
"downloads": {"classifiers": {"natives-osx": {"path": "org/lwjgl/lwjgl-platform-2.9.4-natives-osx.jar"}}}
|
||||
}]
|
||||
});
|
||||
let (java, p) = (dir.join("java"), player());
|
||||
let launch = Launch {
|
||||
minecraft: &mc,
|
||||
game_dir: &game,
|
||||
natives: &natives,
|
||||
java: &java,
|
||||
memory_mb: 2048,
|
||||
jvm_args: &[],
|
||||
player: &p,
|
||||
os: "osx",
|
||||
};
|
||||
let a = args(&command(&version, &launch).unwrap());
|
||||
assert_eq!(fs::read(natives.join("liblwjgl.dylib")).unwrap(), b"lib");
|
||||
assert!(!natives.join("META-INF").exists());
|
||||
let virtual_dir = under(&mc, "assets/virtual/1.8");
|
||||
assert_eq!(
|
||||
fs::read(virtual_dir.join("sounds/step/grass1.ogg")).unwrap(),
|
||||
b"ogg"
|
||||
);
|
||||
let at = |s: &str| a.iter().position(|x| x == s).unwrap();
|
||||
assert_eq!(a[at("--assetsDir") + 1], virtual_dir.to_string_lossy());
|
||||
assert_eq!(
|
||||
a[at("--session") + 1],
|
||||
"token:mc-token:069a79f444e94726a5befca90e38aaf5"
|
||||
);
|
||||
assert_eq!(a[at("--userProperties") + 1], "{}");
|
||||
assert_eq!(a[at("--unknown") + 1], "${nothing}");
|
||||
assert_eq!(a[0], "-Xmx2048M");
|
||||
assert!(a.contains(&format!("-Djava.library.path={}", natives.display())));
|
||||
// Natives are not on the classpath; the client jar is.
|
||||
let cp = &a[at("-cp") + 1];
|
||||
assert!(cp.ends_with("1.8.9.jar") && !cp.contains("natives"), "{cp}");
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholders_fill_in_place() {
|
||||
let vars = HashMap::from([("a", "1".to_string()), ("b", "two".to_string())]);
|
||||
assert_eq!(fill("x${a}y${b}z", &vars), "x1ytwoz");
|
||||
assert_eq!(fill("${c} ${a", &vars), "${c} ${a");
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub mod i18n;
|
||||
pub mod import;
|
||||
pub mod instances;
|
||||
pub mod java;
|
||||
pub mod java_launch;
|
||||
pub mod launch;
|
||||
pub mod minecraft_versions;
|
||||
pub mod modpacks;
|
||||
|
||||
@@ -268,7 +268,7 @@ fn text(v: &Value, key: &str) -> Result<String, String> {
|
||||
}
|
||||
|
||||
/// A path from the version file, refused if it could leave the folder.
|
||||
fn relative(path: &str) -> Result<PathBuf, String> {
|
||||
pub(crate) fn relative(path: &str) -> Result<PathBuf, String> {
|
||||
let safe = !path.contains('\\')
|
||||
&& Path::new(path)
|
||||
.components()
|
||||
|
||||
Reference in new issue
Block a user