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