feat:移除了弹窗,服务器添加sls
This commit is contained in:
commit
6a295f9a7a
4082 files changed
+1322534
No files matched your search
@@ -0,0 +1,472 @@
|
||||
//! OptiFine loader support.
|
||||
//!
|
||||
//! OptiFine has no official metadata API, so available versions and installer
|
||||
//! downloads are resolved through BMCLAPI. Standalone OptiFine instances are
|
||||
//! launched through LaunchWrapper with `optifine.OptiFineTweaker`, mirroring
|
||||
//! the approach used by HMCL and PCL.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use daedalus::minecraft::{
|
||||
Argument, ArgumentType, Library, VersionInfo as GameVersionInfo,
|
||||
};
|
||||
use daedalus::modded::{LoaderVersion, PartialVersionInfo};
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::State;
|
||||
use crate::util::fetch::{
|
||||
ContentValidation, DownloadRequest, Integrity, ResourceClass,
|
||||
download_to_path, fetch_json,
|
||||
};
|
||||
use crate::util::io;
|
||||
|
||||
const BMCLAPI_OPTIFINE_BASE: &str = "https://bmclapi2.bangbang93.com/optifine";
|
||||
pub const OPTIFINE_LOADER_PREFIX: &str = "OptiFine_";
|
||||
const OPTIFINE_TWEAK_CLASS: &str = "optifine.OptiFineTweaker";
|
||||
const LAUNCH_WRAPPER_MAIN_CLASS: &str = "net.minecraft.launchwrapper.Launch";
|
||||
const FALLBACK_LAUNCH_WRAPPER: &str = "net.minecraft:launchwrapper:1.12";
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct BmclapiOptifineEntry {
|
||||
mcversion: String,
|
||||
#[serde(rename = "type")]
|
||||
type_: String,
|
||||
patch: String,
|
||||
}
|
||||
|
||||
impl BmclapiOptifineEntry {
|
||||
fn version_id(&self) -> String {
|
||||
format!("{}_{}", self.type_, self.patch)
|
||||
}
|
||||
|
||||
fn download_url(&self) -> String {
|
||||
format!(
|
||||
"{BMCLAPI_OPTIFINE_BASE}/{}/{}/{}",
|
||||
self.mcversion, self.type_, self.patch
|
||||
)
|
||||
}
|
||||
|
||||
fn is_stable(&self) -> bool {
|
||||
!self.patch.to_ascii_lowercase().contains("pre")
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_entries(
|
||||
game_version: &str,
|
||||
) -> crate::Result<Vec<BmclapiOptifineEntry>> {
|
||||
let state = State::get().await?;
|
||||
let entries: Vec<BmclapiOptifineEntry> = fetch_json(
|
||||
Method::GET,
|
||||
&format!("{BMCLAPI_OPTIFINE_BASE}/{game_version}"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&state.api_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
Ok(entries
|
||||
.into_iter()
|
||||
.filter(|entry| entry.mcversion == game_version)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Lists installable OptiFine versions for a game version as loader versions,
|
||||
/// ordered oldest to newest as reported by BMCLAPI.
|
||||
pub async fn list_loader_versions(
|
||||
game_version: &str,
|
||||
) -> crate::Result<Vec<LoaderVersion>> {
|
||||
Ok(list_entries(game_version)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|entry| LoaderVersion {
|
||||
id: format!("{OPTIFINE_LOADER_PREFIX}{}", entry.version_id()),
|
||||
url: entry.download_url(),
|
||||
stable: entry.is_stable(),
|
||||
profile_source: Default::default(),
|
||||
fallback_url: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Strips launcher-specific prefixes so pack-provided OptiFine version strings
|
||||
/// like `OptiFine_1.12.2_HD_U_G5`, `1.12.2_HD_U_G5`, or `HD_U_G5` all resolve
|
||||
/// to the same BMCLAPI entry.
|
||||
fn normalize_version_id(game_version: &str, requested: &str) -> String {
|
||||
let mut value = requested.trim();
|
||||
if let Some(stripped) = value.strip_prefix(OPTIFINE_LOADER_PREFIX) {
|
||||
value = stripped;
|
||||
}
|
||||
if let Some(stripped) = value.strip_prefix(game_version)
|
||||
&& let Some(stripped) = stripped.strip_prefix('_')
|
||||
{
|
||||
value = stripped;
|
||||
}
|
||||
value.to_string()
|
||||
}
|
||||
|
||||
pub async fn resolve_loader_version(
|
||||
game_version: &str,
|
||||
requested: Option<&str>,
|
||||
) -> crate::Result<Option<LoaderVersion>> {
|
||||
let resolved = match requested.unwrap_or("latest") {
|
||||
"latest" => list_loader_versions(game_version).await?.pop(),
|
||||
"stable" => {
|
||||
let versions = list_loader_versions(game_version).await?;
|
||||
versions
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|version| version.stable)
|
||||
.or(versions.last())
|
||||
.cloned()
|
||||
}
|
||||
// A pinned version resolves without the network so offline launches of
|
||||
// installed instances keep working; the installer download resolves
|
||||
// the actual BMCLAPI URL itself when needed.
|
||||
id => {
|
||||
let of_id = normalize_version_id(game_version, id);
|
||||
Some(LoaderVersion {
|
||||
id: format!("{OPTIFINE_LOADER_PREFIX}{of_id}"),
|
||||
url: String::new(),
|
||||
stable: !of_id.to_ascii_lowercase().contains("pre"),
|
||||
profile_source: Default::default(),
|
||||
fallback_url: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn optifine_library_name(game_version: &str, of_id: &str) -> String {
|
||||
format!("optifine:OptiFine:{game_version}_{of_id}")
|
||||
}
|
||||
|
||||
fn library(name: String, downloadable: bool) -> Library {
|
||||
Library {
|
||||
downloads: None,
|
||||
extract: None,
|
||||
name,
|
||||
url: None,
|
||||
natives: None,
|
||||
rules: None,
|
||||
checksums: None,
|
||||
include_in_classpath: true,
|
||||
downloadable,
|
||||
}
|
||||
}
|
||||
|
||||
struct InstallerInfo {
|
||||
launchwrapper_library: Library,
|
||||
launchwrapper_entry: Option<String>,
|
||||
needs_patching: bool,
|
||||
}
|
||||
|
||||
fn inspect_installer(path: &Path) -> crate::Result<InstallerInfo> {
|
||||
let file = std::fs::File::open(path)
|
||||
.map_err(|error| io::IOError::with_path(error, path))?;
|
||||
let mut archive = zip::ZipArchive::new(file).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"OptiFine installer archive is invalid: {error}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut launchwrapper_version = None;
|
||||
if let Ok(mut entry) = archive.by_name("launchwrapper-of.txt") {
|
||||
let mut version = String::new();
|
||||
entry.read_to_string(&mut version)?;
|
||||
let version = version.trim().to_string();
|
||||
if !version.is_empty() {
|
||||
launchwrapper_version = Some(version);
|
||||
}
|
||||
}
|
||||
if launchwrapper_version.is_none() {
|
||||
launchwrapper_version = archive.file_names().find_map(|name| {
|
||||
name.strip_prefix("launchwrapper-of-")
|
||||
.and_then(|rest| rest.strip_suffix(".jar"))
|
||||
.map(str::to_string)
|
||||
});
|
||||
}
|
||||
|
||||
let needs_patching = archive.by_name("optifine/Patcher.class").is_ok();
|
||||
|
||||
let (launchwrapper_library, launchwrapper_entry) =
|
||||
match launchwrapper_version {
|
||||
Some(version) => (
|
||||
library(format!("optifine:launchwrapper-of:{version}"), false),
|
||||
Some(format!("launchwrapper-of-{version}.jar")),
|
||||
),
|
||||
// Very old OptiFine builds ship without their own LaunchWrapper;
|
||||
// Mojang's launchwrapper 1.12 from libraries.minecraft.net works.
|
||||
None => (library(FALLBACK_LAUNCH_WRAPPER.to_string(), true), None),
|
||||
};
|
||||
|
||||
Ok(InstallerInfo {
|
||||
launchwrapper_library,
|
||||
launchwrapper_entry,
|
||||
needs_patching,
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_installer(
|
||||
state: &State,
|
||||
game_version: &str,
|
||||
of_id: &str,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let path = state
|
||||
.directories
|
||||
.caches_dir()
|
||||
.join("optifine")
|
||||
.join(game_version)
|
||||
.join(format!("{OPTIFINE_LOADER_PREFIX}{of_id}.jar"));
|
||||
if path.is_file() {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let entries = list_entries(game_version).await?;
|
||||
let entry = entries
|
||||
.iter()
|
||||
.find(|entry| entry.version_id() == of_id)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"OptiFine {of_id} is not available for Minecraft {game_version}"
|
||||
))
|
||||
})?;
|
||||
|
||||
download_to_path(
|
||||
DownloadRequest::new(entry.download_url(), ResourceClass::Loader)
|
||||
.with_integrity(Integrity {
|
||||
content: ContentValidation::Jar,
|
||||
..Integrity::default()
|
||||
}),
|
||||
&path,
|
||||
&state.download_semaphore,
|
||||
&state.pool,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn strip_loader_prefix(loader_version_id: &str) -> &str {
|
||||
loader_version_id
|
||||
.strip_prefix(OPTIFINE_LOADER_PREFIX)
|
||||
.unwrap_or(loader_version_id)
|
||||
}
|
||||
|
||||
/// Builds the loader profile for a standalone OptiFine version locally, taking
|
||||
/// the place of the Daedalus partial version metadata used by other loaders.
|
||||
pub(crate) async fn build_partial_version_info(
|
||||
state: &State,
|
||||
vanilla: &GameVersionInfo,
|
||||
game_version: &str,
|
||||
loader_version_id: &str,
|
||||
) -> crate::Result<PartialVersionInfo> {
|
||||
let of_id = strip_loader_prefix(loader_version_id).to_string();
|
||||
let installer = ensure_installer(state, game_version, &of_id).await?;
|
||||
let installer_for_inspect = installer.clone();
|
||||
let info = tokio::task::spawn_blocking(move || {
|
||||
inspect_installer(&installer_for_inspect)
|
||||
})
|
||||
.await??;
|
||||
|
||||
let libraries = vec![
|
||||
library(optifine_library_name(game_version, &of_id), false),
|
||||
info.launchwrapper_library,
|
||||
];
|
||||
|
||||
let mut minecraft_arguments = None;
|
||||
let mut arguments = None;
|
||||
if let Some(vanilla_arguments) = &vanilla.minecraft_arguments {
|
||||
minecraft_arguments = Some(format!(
|
||||
"{vanilla_arguments} --tweakClass {OPTIFINE_TWEAK_CLASS}"
|
||||
));
|
||||
} else {
|
||||
arguments = Some(HashMap::from([(
|
||||
ArgumentType::Game,
|
||||
vec![
|
||||
Argument::Normal("--tweakClass".to_string()),
|
||||
Argument::Normal(OPTIFINE_TWEAK_CLASS.to_string()),
|
||||
],
|
||||
)]));
|
||||
}
|
||||
|
||||
Ok(PartialVersionInfo {
|
||||
id: format!("{game_version}-{loader_version_id}"),
|
||||
inherits_from: game_version.to_string(),
|
||||
release_time: vanilla.release_time,
|
||||
time: vanilla.time,
|
||||
main_class: Some(LAUNCH_WRAPPER_MAIN_CLASS.to_string()),
|
||||
minecraft_arguments,
|
||||
arguments,
|
||||
libraries,
|
||||
java_version: None,
|
||||
type_: vanilla.type_.clone(),
|
||||
data: None,
|
||||
processors: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_installer_entry(
|
||||
installer: &Path,
|
||||
entry_name: &str,
|
||||
target: &Path,
|
||||
) -> crate::Result<()> {
|
||||
let file = std::fs::File::open(installer)
|
||||
.map_err(|error| io::IOError::with_path(error, installer))?;
|
||||
let mut archive = zip::ZipArchive::new(file).map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"OptiFine installer archive is invalid: {error}"
|
||||
))
|
||||
})?;
|
||||
let mut entry = archive.by_name(entry_name).map_err(|_| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"OptiFine installer is missing {entry_name}"
|
||||
))
|
||||
})?;
|
||||
if let Some(parent) = target.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|error| io::IOError::with_path(error, parent))?;
|
||||
}
|
||||
let mut output = std::fs::File::create(target)
|
||||
.map_err(|error| io::IOError::with_path(error, target))?;
|
||||
std::io::copy(&mut entry, &mut output)
|
||||
.map_err(|error| io::IOError::with_path(error, target))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Materializes the OptiFine libraries after the vanilla client jar has been
|
||||
/// downloaded: extracts the bundled LaunchWrapper and produces the OptiFine
|
||||
/// library jar, running the installer's patcher against the client jar when
|
||||
/// the installer only ships patch data.
|
||||
pub(crate) async fn install_optifine_libraries(
|
||||
state: &State,
|
||||
java_path: &Path,
|
||||
game_version: &str,
|
||||
loader_version_id: &str,
|
||||
client_jar_path: &Path,
|
||||
) -> crate::Result<()> {
|
||||
let of_id = strip_loader_prefix(loader_version_id).to_string();
|
||||
let installer = ensure_installer(state, game_version, &of_id).await?;
|
||||
let installer_for_inspect = installer.clone();
|
||||
let info = tokio::task::spawn_blocking(move || {
|
||||
inspect_installer(&installer_for_inspect)
|
||||
})
|
||||
.await??;
|
||||
let libraries_dir = state.directories.libraries_dir();
|
||||
|
||||
if let Some(entry_name) = info.launchwrapper_entry {
|
||||
let target = libraries_dir.join(daedalus::get_path_from_artifact(
|
||||
&info.launchwrapper_library.name,
|
||||
)?);
|
||||
if !target.exists() {
|
||||
let installer = installer.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
extract_installer_entry(&installer, &entry_name, &target)
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
}
|
||||
|
||||
let optifine_target = libraries_dir.join(daedalus::get_path_from_artifact(
|
||||
&optifine_library_name(game_version, &of_id),
|
||||
)?);
|
||||
if optifine_target.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(parent) = optifine_target.parent() {
|
||||
io::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
if info.needs_patching {
|
||||
let mut command = Command::new(java_path);
|
||||
command
|
||||
.kill_on_drop(true)
|
||||
.arg("-cp")
|
||||
.arg(&installer)
|
||||
.arg("optifine.Patcher")
|
||||
.arg(client_jar_path)
|
||||
.arg(&installer)
|
||||
.arg(&optifine_target);
|
||||
let output = command.output().await.map_err(|error| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running OptiFine patcher: {error}"
|
||||
))
|
||||
})?;
|
||||
if !output.status.success() {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"OptiFine patcher error: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
} else {
|
||||
io::copy(&installer, &optifine_target).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads the OptiFine jar usable as a Forge/NeoForge mod into the given
|
||||
/// directory, patching it against the client jar when required. Returns the
|
||||
/// target file path.
|
||||
pub async fn install_optifine_as_mod(
|
||||
state: &State,
|
||||
instance_id: &str,
|
||||
cancellation: tokio_util::sync::CancellationToken,
|
||||
java_path: &Path,
|
||||
game_version: &str,
|
||||
requested_version: &str,
|
||||
client_jar_path: &Path,
|
||||
mods_dir: &Path,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let of_id = normalize_version_id(game_version, requested_version);
|
||||
let installer = ensure_installer(state, game_version, &of_id).await?;
|
||||
let installer_for_inspect = installer.clone();
|
||||
let info = tokio::task::spawn_blocking(move || {
|
||||
inspect_installer(&installer_for_inspect)
|
||||
})
|
||||
.await??;
|
||||
|
||||
let target = mods_dir.join(format!(
|
||||
"{OPTIFINE_LOADER_PREFIX}{game_version}_{of_id}.jar"
|
||||
));
|
||||
io::create_dir_all(mods_dir).await?;
|
||||
|
||||
if info.needs_patching {
|
||||
let mut command = Command::new(java_path);
|
||||
command
|
||||
.arg("-cp")
|
||||
.arg(&installer)
|
||||
.arg("optifine.Patcher")
|
||||
.arg(client_jar_path)
|
||||
.arg(&installer)
|
||||
.arg(&target);
|
||||
let output = super::run_instance_install_command(
|
||||
instance_id.to_string(),
|
||||
cancellation,
|
||||
command,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running OptiFine patcher: {error}"
|
||||
))
|
||||
})?;
|
||||
if !output.status.success() {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"OptiFine patcher error: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
} else {
|
||||
io::copy(&installer, &target).await?;
|
||||
}
|
||||
|
||||
Ok(target)
|
||||
}
|
||||
Reference in new issue
Block a user