use std::path::{Path, PathBuf};
use super::LaunchError;
use crate::launch_profile::{model::LaunchProfile, system::JavaPlatform};
const LOG4J_FIXED_BASE: &str = "https://files.prismlauncher.org/maven/org/apache/logging/log4j";
#[derive(Debug)]
pub struct LwjglifyPatches {
pub profile: LaunchProfile,
patches_path: PathBuf,
}
pub fn load(
minecraft_dir: &Path,
platform: &JavaPlatform,
) -> Result<Option<LwjglifyPatches>, LaunchError> {
use sha1::Digest;
use std::io::Read;
let mods_dir = minecraft_dir.join("mods");
let Some(lwjgl3ify_jar) = find_lwjgl3ify_jar(&mods_dir)? else {
return Ok(None);
};
let file = std::fs::File::open(&lwjgl3ify_jar)?;
let mut archive = zip::ZipArchive::new(file).map_err(|error| {
LaunchError::Parse(format!(
"Invalid lwjgl3ify jar {}: {error}",
lwjgl3ify_jar.display()
))
})?;
let entry = archive
.by_name("me/eigenraven/lwjgl3ify/relauncher/version.json")
.map_err(|error| {
LaunchError::Parse(format!(
"lwjgl3ify bundled client profile is missing in {}: {error}",
lwjgl3ify_jar.display()
))
})?;
let mut profile: LaunchProfile = serde_json::from_reader(entry)?;
if profile
.main_class
.as_deref()
.is_none_or(|main| main.trim().is_empty())
|| profile.arguments.is_none()
|| profile
.java_version
.as_ref()
.is_none_or(|version| version.major_version == 0)
{
return Err(LaunchError::Parse(
"lwjgl3ify bundled client profile is missing mainClass, arguments or javaVersion"
.into(),
));
}
for lib in &profile.libraries {
if lib
.downloads
.as_ref()
.is_none_or(|downloads| downloads.artifact.is_none() && lib.natives.is_none())
{
return Err(LaunchError::Parse(format!(
"lwjgl3ify bundled library {} is missing download metadata",
lib.name
)));
}
}
select_lwjgl_natives(&mut profile, platform)?;
let patch_artifact = profile
.libraries
.iter()
.find(|lib| lib.name.ends_with(":forgePatches"))
.and_then(|lib| lib.downloads.as_ref())
.and_then(|downloads| downloads.artifact.as_ref())
.ok_or_else(|| {
LaunchError::Parse(
"lwjgl3ify bundled profile is missing the forgePatches artifact".into(),
)
})?;
let mut payload = Vec::new();
archive
.by_name("me/eigenraven/lwjgl3ify/relauncher/forgePatches.zip")
.map_err(|error| {
LaunchError::Parse(format!(
"lwjgl3ify forgePatches payload is missing: {error}"
))
})?
.read_to_end(&mut payload)?;
if payload.len() as u64 != patch_artifact.size
|| !format!("{:x}", sha1::Sha1::digest(&payload)).eq_ignore_ascii_case(&patch_artifact.sha1)
{
return Err(LaunchError::Parse(
"lwjgl3ify embedded forgePatches does not match its bundled profile".into(),
));
}
profile
.libraries
.retain(|lib| !lib.name.ends_with(":forgePatches"));
let patches_path = minecraft_dir.join(".forge-patches.jar");
crate::storage::write_atomic(&patches_path, &payload)?;
Ok(Some(LwjglifyPatches {
profile,
patches_path,
}))
}
pub async fn apply(
patches: LwjglifyPatches,
minecraft_dir: &Path,
lib_dir: &Path,
classpath: &mut Vec<PathBuf>,
) -> Result<(String, Vec<String>, Vec<String>), LaunchError> {
classpath.insert(0, patches.patches_path);
replace_log4j_fixed(lib_dir, classpath).await?;
let mut jvm_args = Vec::new();
write_log4j_config(minecraft_dir, &mut jvm_args)?;
let shim_path = deploy_shim(minecraft_dir)?;
classpath.insert(0, shim_path);
Ok((
"RmclShim".to_owned(),
vec![
patches
.profile
.main_class
.expect("validated client main class"),
],
jvm_args,
))
}
const SHIM_JAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/rmcl-shim.jar"));
fn deploy_shim(minecraft_dir: &Path) -> std::io::Result<PathBuf> {
let dest = minecraft_dir.join(".rmcl-shim.jar");
crate::storage::write_atomic(&dest, SHIM_JAR)?;
Ok(dest)
}
fn find_lwjgl3ify_jar(mods_dir: &Path) -> std::io::Result<Option<PathBuf>> {
let entries = match std::fs::read_dir(mods_dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
let mut found = None;
for entry in entries {
let entry = entry?;
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("lwjgl3ify") && name.ends_with(".jar") {
if found.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Multiple lwjgl3ify jars found in mods",
));
}
found = Some(entry.path());
}
}
Ok(found)
}
pub fn strip_replaced_libs(profile: &mut LaunchProfile) {
let replaced = [
"launchwrapper-",
"asm-all-",
"lwjgl-2.",
"lwjgl_util-",
"commons-compress-",
"commons-io-",
"guava-15.",
];
profile.libraries.retain(|lib| {
if lib.name.starts_with("org.lwjgl.lwjgl:") {
return false;
}
let name = lib.name.split(':').collect::<Vec<_>>();
let filename = match name.as_slice() {
[_, artifact, version, ..] => format!("{artifact}-{version}"),
_ => return true,
};
let dominated = replaced.iter().any(|prefix| filename.starts_with(prefix));
if dominated {
tracing::info!("Stripping {} (replaced by forge-patches)", lib.name);
}
!dominated
});
}
fn select_lwjgl_natives(
profile: &mut LaunchProfile,
platform: &JavaPlatform,
) -> Result<(), LaunchError> {
let classifier = match (platform.os_name, platform.arch.as_str(), platform.bitness) {
("osx", "arm64", 64) => "natives-macos-arm64",
("osx", "x86_64", 64) => "natives-macos",
("windows", "arm64", 64) => "natives-windows-arm64",
("windows", "x86", 32) => "natives-windows-x86",
("windows", "x86_64", 64) => "natives-windows",
("linux", "arm64", 64) => "natives-linux-arm64",
("linux", "arm", 32) => "natives-linux-arm32",
("linux", "ppc64le", 64) => "natives-linux-ppc64le",
("linux", "x86_64", 64) => "natives-linux",
_ => {
return Err(LaunchError::Parse(format!(
"lwjgl3ify does not support Java {} {} ({}-bit)",
platform.os_name, platform.arch, platform.bitness
)));
}
};
let native = |name: &str| -> Option<(String, String)> {
let parts: Vec<_> = name.split(':').collect();
match parts.as_slice() {
["org.lwjgl", artifact, _, variant] if variant.starts_with("natives-") => {
Some(((*artifact).to_owned(), (*variant).to_owned()))
}
["org.lwjgl", artifact, _] => artifact
.split_once("-natives-")
.map(|(module, variant)| (module.to_owned(), format!("natives-{variant}"))),
_ => None,
}
};
let mut native_modules = std::collections::HashSet::new();
let mut selected = std::collections::HashSet::new();
profile.libraries.retain(|lib| {
let Some((module, variant)) = native(&lib.name) else {
return true;
};
native_modules.insert(module.clone());
if variant == classifier {
selected.insert(module);
true
} else {
false
}
});
if let Some(missing) = native_modules.difference(&selected).next() {
return Err(LaunchError::Parse(format!(
"lwjgl3ify bundled profile has no {classifier} for {missing}"
)));
}
Ok(())
}
fn write_log4j_config(minecraft_dir: &Path, jvm_args: &mut Vec<String>) -> std::io::Result<()> {
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>
"#;
crate::storage::write_atomic(&config_path, config.as_bytes())?;
jvm_args.push(format!(
"-Dlog4j.configurationFile={}",
config_path.display()
));
Ok(())
}
async fn replace_log4j_fixed(lib_dir: &Path, classpath: &mut [PathBuf]) -> Result<(), LaunchError> {
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 {
if !classpath
.iter()
.any(|entry| entry.file_name().is_some_and(|name| name == *old_name))
{
continue;
}
let fixed_path = lib_dir.join(fixed_rel);
if !fixed_path.exists() {
tracing::info!("Downloading patched {old_name}...");
let client = crate::net::HttpClient::new();
crate::net::download_file(&client, url, &fixed_path, |_, _| {}).await?;
}
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();
}
}
}
Ok(())
}
#[cfg(test)]
#[path = "../tests/launch/patches.rs"]
mod tests;