use std::path::{Path, PathBuf};
const LOG4J_FIXED_BASE: &str = "https://files.prismlauncher.org/maven/org/apache/logging/log4j";
pub struct LwjglifyPatches {
pub jvm_args: Vec<String>,
pub main_class: String,
pub extra_args: Vec<String>,
}
pub async fn apply(
minecraft_dir: &Path,
lib_dir: &Path,
classpath: &mut Vec<PathBuf>,
) -> Option<LwjglifyPatches> {
let mods_dir = minecraft_dir.join("mods");
let lwjgl3ify_jar = find_lwjgl3ify_jar(&mods_dir)?;
let patches_dest = minecraft_dir.join(".forge-patches.jar");
if let Err(e) = extract_forge_patches(&lwjgl3ify_jar, &patches_dest) {
tracing::warn!("Failed to extract lwjgl3ify forge patches: {e}");
return None;
}
classpath.insert(0, patches_dest.clone());
let mut jvm_args = parse_add_opens(&patches_dest).unwrap_or_default();
jvm_args.extend([
"-Djava.system.class.loader=com.gtnewhorizons.retrofuturabootstrap.RfbSystemClassLoader"
.to_string(),
"-Dfile.encoding=UTF-8".to_string(),
]);
strip_replaced_libs(classpath);
add_lwjgl3(lib_dir, classpath);
replace_log4j_fixed(lib_dir, classpath).await;
write_log4j_config(minecraft_dir, &mut jvm_args);
let shim_path = deploy_shim(minecraft_dir);
classpath.insert(0, shim_path);
Some(LwjglifyPatches {
jvm_args,
main_class: "RmclShim".to_string(),
extra_args: vec!["com.gtnewhorizons.retrofuturabootstrap.Main".to_string()],
})
}
const SHIM_JAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/rmcl-shim.jar"));
fn deploy_shim(minecraft_dir: &Path) -> PathBuf {
let dest = minecraft_dir.join(".rmcl-shim.jar");
if let Err(e) = std::fs::write(&dest, SHIM_JAR) {
tracing::warn!("Failed to write rmcl-shim.jar: {e}");
}
dest
}
fn find_lwjgl3ify_jar(mods_dir: &Path) -> Option<PathBuf> {
let entries = std::fs::read_dir(mods_dir).ok()?;
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("lwjgl3ify") && name.ends_with(".jar") {
return Some(entry.path());
}
}
None
}
fn extract_forge_patches(lwjgl3ify_jar: &Path, dest: &Path) -> Result<(), std::io::Error> {
use std::io::Read;
let file = std::fs::File::open(lwjgl3ify_jar)?;
let mut archive = zip::ZipArchive::new(file)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut entry = archive
.by_name("me/eigenraven/lwjgl3ify/relauncher/forgePatches.zip")
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
std::fs::write(dest, &buf)
}
fn parse_add_opens(patches_archive: &Path) -> Option<Vec<String>> {
use std::io::Read;
let file = std::fs::File::open(patches_archive).ok()?;
let mut archive = zip::ZipArchive::new(file).ok()?;
let mut entry = archive.by_name("META-INF/MANIFEST.MF").ok()?;
let mut manifest = String::new();
entry.read_to_string(&mut manifest).ok()?;
let manifest = manifest.replace("\r\n ", "").replace("\n ", "");
let mut args = Vec::new();
for line in manifest.lines() {
if let Some(value) = line.strip_prefix("Add-Opens: ") {
for module_package in value.split_whitespace() {
args.push("--add-opens".to_string());
args.push(format!("{module_package}=ALL-UNNAMED"));
}
}
}
Some(args)
}
fn strip_replaced_libs(classpath: &mut Vec<PathBuf>) {
let replaced = [
"launchwrapper-",
"asm-all-",
"lwjgl-2.",
"lwjgl_util-",
"commons-compress-",
"commons-io-",
"guava-15.",
];
classpath.retain(|entry| {
let name = entry
.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default();
let dominated = replaced.iter().any(|prefix| name.starts_with(prefix));
if dominated {
tracing::info!("Stripping {name} from classpath (replaced by forge-patches)");
}
!dominated
});
}
fn add_lwjgl3(lib_dir: &Path, classpath: &mut Vec<PathBuf>) {
let lwjgl3_modules = [
"lwjgl",
"lwjgl-freetype",
"lwjgl-glfw",
"lwjgl-jemalloc",
"lwjgl-openal",
"lwjgl-opengl",
"lwjgl-stb",
"lwjgl-tinyfd",
];
let os_classifier = match std::env::consts::OS {
"macos" => "natives-macos",
"windows" => "natives-windows",
_ => "natives-linux",
};
let mut insert_pos = 1.min(classpath.len());
for module in &lwjgl3_modules {
let base = if *module == "lwjgl" {
lib_dir.join("org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3.jar")
} else {
lib_dir.join(format!("org/lwjgl/{0}/3.3.3/{0}-3.3.3.jar", module))
};
if base.exists() {
classpath.insert(insert_pos, base);
insert_pos += 1;
}
let natives = if *module == "lwjgl" {
lib_dir.join(format!(
"org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-{os_classifier}.jar"
))
} else {
lib_dir.join(format!(
"org/lwjgl/{0}/3.3.3/{0}-3.3.3-{1}.jar",
module, os_classifier
))
};
if natives.exists() {
classpath.insert(insert_pos, natives);
insert_pos += 1;
}
}
tracing::info!("Added {} LWJGL 3.3.3 jars to classpath", insert_pos - 1);
}
fn write_log4j_config(minecraft_dir: &Path, jvm_args: &mut Vec<String>) {
let config_path = minecraft_dir.join(".rmcl-log4j2.xml");
let config = r#"<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n"/>
</Console>
<Queue name="ServerGuiConsole">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log"
filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="SysOut"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
"#;
if let Err(e) = std::fs::write(&config_path, config) {
tracing::warn!("Failed to write log4j2 config: {e}");
return;
}
jvm_args.push(format!(
"-Dlog4j.configurationFile={}",
config_path.display()
));
}
async fn replace_log4j_fixed(lib_dir: &Path, classpath: &mut [PathBuf]) {
let replacements = [
(
"log4j-api-2.0-beta9.jar",
"org/apache/logging/log4j/log4j-api/2.0-beta9-fixed/log4j-api-2.0-beta9-fixed.jar",
format!("{LOG4J_FIXED_BASE}/log4j-api/2.0-beta9-fixed/log4j-api-2.0-beta9-fixed.jar"),
),
(
"log4j-core-2.0-beta9.jar",
"org/apache/logging/log4j/log4j-core/2.0-beta9-fixed/log4j-core-2.0-beta9-fixed.jar",
format!("{LOG4J_FIXED_BASE}/log4j-core/2.0-beta9-fixed/log4j-core-2.0-beta9-fixed.jar"),
),
];
for (old_name, fixed_rel, url) in &replacements {
let fixed_path = lib_dir.join(fixed_rel);
if !fixed_path.exists() {
tracing::info!("Downloading patched {old_name}...");
if let Some(parent) = fixed_path.parent() {
let _ = tokio::fs::create_dir_all(parent).await;
}
let client = crate::net::HttpClient::new();
if let Err(e) = crate::net::download_file(&client, url, &fixed_path, |_, _| {}).await {
tracing::error!(
"Failed to download patched {old_name}: {e}, continuing with unpatched version"
);
continue;
}
}
for entry in classpath.iter_mut() {
if entry
.file_name()
.is_some_and(|n| n.to_string_lossy() == *old_name)
{
tracing::info!("Replacing {old_name} with patched version");
*entry = fixed_path.clone();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn make_zip(tmp: &Path, name: &str, entries: &[(&str, &[u8])]) -> PathBuf {
let path = tmp.join(name);
let file = std::fs::File::create(&path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let opts: zip::write::SimpleFileOptions = Default::default();
for (filename, bytes) in entries {
zip.start_file(*filename, opts).unwrap();
zip.write_all(bytes).unwrap();
}
zip.finish().unwrap();
path
}
#[test]
fn strip_replaced_libs_removes_dominated_prefixes() {
let mut classpath = vec![
PathBuf::from("/libs/launchwrapper-1.12.jar"),
PathBuf::from("/libs/asm-all-5.0.3.jar"),
PathBuf::from("/libs/lwjgl-2.9.4.jar"),
PathBuf::from("/libs/lwjgl_util-2.9.4.jar"),
PathBuf::from("/libs/commons-compress-1.4.1.jar"),
PathBuf::from("/libs/commons-io-2.4.jar"),
PathBuf::from("/libs/guava-15.0.jar"),
PathBuf::from("/libs/log4j-core-2.0.jar"),
PathBuf::from("/libs/guava-21.0.jar"),
];
strip_replaced_libs(&mut classpath);
assert_eq!(
classpath,
vec![
PathBuf::from("/libs/log4j-core-2.0.jar"),
PathBuf::from("/libs/guava-21.0.jar"),
]
);
}
#[test]
fn strip_replaced_libs_keeps_unrelated_entries() {
let mut classpath = vec![
PathBuf::from("/libs/log4j-core-2.0.jar"),
PathBuf::from("/libs/mixin-0.8.5.jar"),
];
let original = classpath.clone();
strip_replaced_libs(&mut classpath);
assert_eq!(classpath, original);
}
#[test]
fn parse_add_opens_extracts_module_args() {
let tmp = tempfile::tempdir().unwrap();
let manifest = b"Manifest-Version: 1.0\nAdd-Opens: java.base/java.lang java.base/java.util\n";
let zip_path = make_zip(tmp.path(), "patches.zip", &[("META-INF/MANIFEST.MF", manifest)]);
let args = parse_add_opens(&zip_path).expect("parsed");
assert_eq!(
args,
vec![
"--add-opens",
"java.base/java.lang=ALL-UNNAMED",
"--add-opens",
"java.base/java.util=ALL-UNNAMED",
]
);
}
#[test]
fn parse_add_opens_handles_continuation_lines() {
let tmp = tempfile::tempdir().unwrap();
let manifest =
b"Manifest-Version: 1.0\nAdd-Opens: java.base/java.lang \n java.base/sun.security.util\n";
let zip_path = make_zip(
tmp.path(),
"patches-continuation.zip",
&[("META-INF/MANIFEST.MF", manifest)],
);
let args = parse_add_opens(&zip_path).expect("parsed");
assert_eq!(
args,
vec![
"--add-opens",
"java.base/java.lang=ALL-UNNAMED",
"--add-opens",
"java.base/sun.security.util=ALL-UNNAMED",
]
);
}
#[test]
fn parse_add_opens_returns_none_when_manifest_missing() {
let tmp = tempfile::tempdir().unwrap();
let zip_path = make_zip(tmp.path(), "no-manifest.zip", &[("other.txt", b"x")]);
assert!(parse_add_opens(&zip_path).is_none());
}
#[test]
fn parse_add_opens_returns_none_for_missing_file() {
assert!(parse_add_opens(Path::new("/nonexistent/x.zip")).is_none());
}
#[test]
fn add_lwjgl3_inserts_only_jars_that_exist() {
let tmp = tempfile::tempdir().unwrap();
let lib_dir = tmp.path();
let core_jar = lib_dir.join("org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3.jar");
std::fs::create_dir_all(core_jar.parent().unwrap()).unwrap();
std::fs::write(&core_jar, b"jar").unwrap();
let mut classpath = vec![PathBuf::from("/leading/forge-patches.jar")];
add_lwjgl3(lib_dir, &mut classpath);
assert_eq!(classpath[0], PathBuf::from("/leading/forge-patches.jar"));
assert_eq!(classpath[1], core_jar);
assert_eq!(classpath.len(), 2);
}
}