use std::{
collections::BTreeMap as Map,
fmt,
marker::PhantomData,
path::{Path, PathBuf},
};
use inner_future::stream::StreamExt;
use serde::{
de::{self, SeqAccess, Visitor},
Deserialize, Deserializer, Serialize,
};
use super::VersionType;
use crate::{
package::PackageName,
prelude::*,
semver::MinecraftVersion,
utils::{get_full_path, NATIVE_ARCH_LAZY},
};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct OSRule {
#[serde(default)]
pub name: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub arch: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct ApplyRule {
pub action: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub os: Option<OSRule>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub features: Option<Map<String, bool>>,
}
pub trait Allowed {
fn is_allowed(&self) -> bool;
}
impl Allowed for [ApplyRule] {
fn is_allowed(&self) -> bool {
if self.is_empty() {
true
} else {
let mut should_push = false;
for rule in self {
if rule.action == "disallow" {
if let Some(os) = &rule.os {
if !os.name.is_empty()
&& os.name != crate::utils::TARGET_OS
&& !os.arch.is_empty()
&& os.arch != NATIVE_ARCH_LAZY.as_ref()
{
continue;
} else {
break;
}
} else {
continue;
}
} else if rule.action == "allow" {
if let Some(os) = &rule.os {
if (!os.name.is_empty() && os.name != crate::utils::TARGET_OS)
|| (!os.arch.is_empty() && os.arch != NATIVE_ARCH_LAZY.as_ref())
{
continue;
} else {
should_push = true;
break;
}
} else {
should_push = true; continue;
}
}
}
should_push
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct SpecificalArgument {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<ApplyRule>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(deserialize_with = "string_or_seq")]
pub value: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum Argument {
Common(String),
Specify(SpecificalArgument),
}
#[test]
fn argument_test() {
let text = serde_json::from_str::<Argument>(r#""test""#).unwrap();
assert_eq!(text, Argument::Common("test".into()));
let specify = serde_json::from_str::<Argument>(r#"{"rules":[],"value":"test"}"#).unwrap();
assert_eq!(
specify,
Argument::Specify(SpecificalArgument {
rules: vec![],
value: vec!["test".into()]
})
);
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(default)]
pub struct Arguments {
pub game: Vec<Argument>,
pub jvm: Vec<Argument>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct AssetIndex {
pub id: String,
pub sha1: String,
pub size: u32,
#[serde(rename = "totalSize")]
pub total_size: u32,
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct DownloadItem {
#[serde(default)]
pub path: String,
pub sha1: String,
pub size: usize,
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LibraryDownload {
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact: Option<DownloadItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub classifiers: Option<Map<String, DownloadItem>>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Library {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<ApplyRule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub downloads: Option<LibraryDownload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub natives: Option<Map<String, String>>,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LoggingFile {
pub id: String,
pub sha1: String,
pub size: u32,
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LoggingConfig {
pub argument: String,
#[serde(rename = "type")]
pub logger_type: String,
pub file: LoggingFile,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(default)]
pub struct SCLLaunchConfig {
pub max_mem: Option<usize>,
pub java_path: String,
pub game_independent: bool,
pub window_title: String,
pub jvm_args: String,
pub game_args: String,
pub wrapper_path: String,
pub wrapper_args: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Logging {
pub client: Option<LoggingConfig>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct JavaVersion {
pub component: String,
#[serde(rename = "majorVersion")]
pub major_version: u8,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct VersionMeta {
#[serde(default)]
#[serde(rename = "inheritsFrom")]
pub inherits_from: String,
#[serde(default)]
#[serde(rename = "clientVersion")]
pub client_version: String,
#[serde(default)]
#[serde(rename = "javaVersion")]
pub java_version: Option<JavaVersion>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Arguments>,
#[serde(default)]
#[serde(rename = "minecraftArguments")]
pub minecraft_arguments: String,
#[serde(rename = "assetIndex")]
#[serde(skip_serializing_if = "Option::is_none")]
pub asset_index: Option<AssetIndex>,
#[serde(skip_serializing_if = "Option::is_none")]
pub downloads: Option<Map<String, DownloadItem>>,
#[serde(default)]
pub libraries: Vec<Library>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<Logging>,
#[serde(rename = "mainClass")]
pub main_class: String,
#[serde(skip)]
pub main_jars: Vec<String>,
}
impl VersionMeta {
pub(crate) fn fix_libraries(&mut self) {
for library in &mut self.libraries {
if library.rules.is_allowed()
&& library.downloads.is_none()
&& library.natives.is_none()
{
if let Ok(p) = library.name.parse::<PackageName>() {
let p = p.to_maven_jar_path("");
library.downloads = Some(LibraryDownload {
artifact: Some(DownloadItem {
path: p,
sha1: "".into(),
size: 0,
url: "".into(),
}),
classifiers: None,
})
}
}
}
}
pub fn required_java_version(&self) -> u8 {
if let Some(java_version) = &self.java_version {
java_version.major_version
} else if let Some(assets) = &self.asset_index {
if let Ok((_, ver)) = crate::semver::parse_version(&assets.id) {
ver.required_java_version()
} else {
8
}
} else if !self.inherits_from.is_empty() {
if let Ok((_, ver)) = crate::semver::parse_version(&self.inherits_from) {
ver.required_java_version()
} else {
8
}
} else {
8
}
}
}
impl std::ops::AddAssign for VersionMeta {
fn add_assign(&mut self, data: VersionMeta) {
self.main_class = data.main_class.to_owned();
self.minecraft_arguments = data.minecraft_arguments;
self.libraries.extend_from_slice(&data.libraries);
self.main_jars.extend_from_slice(&data.main_jars);
if let Some(downloads) = &mut data.downloads.to_owned() {
if let Some(self_downloads) = &mut self.downloads {
self_downloads.append(downloads);
} else {
self.downloads = Some(downloads.to_owned());
}
}
if let Some(arguments) = &data.arguments {
if let Some(self_arguments) = &mut self.arguments {
for a in arguments.jvm.iter() {
self_arguments.jvm.push(a.to_owned());
}
for a in arguments.game.iter() {
self_arguments.game.push(a.to_owned());
}
} else {
self.arguments = Some(arguments.to_owned())
}
}
if let Some(logging) = &data.logging {
self.logging = Some(logging.to_owned());
}
}
}
#[derive(Debug, Clone, Default)]
pub struct VersionInfo {
pub version_base: String,
pub version: String,
pub meta: Option<VersionMeta>,
pub scl_launch_config: Option<SCLLaunchConfig>,
pub version_type: VersionType,
pub minecraft_version: MinecraftVersion,
pub required_java: u8,
}
impl VersionInfo {
pub async fn load(&mut self) -> DynResult {
let version_base_path = Path::new(&self.version_base);
if version_base_path.is_dir() {
let jar_path = version_base_path
.join(&self.version)
.join(format!("{}.jar", &self.version));
let meta_path = version_base_path
.join(&self.version)
.join(format!("{}.json", &self.version));
let scl_config_path = version_base_path.join(&self.version).join(".scl.json");
if !meta_path.is_file() {
anyhow::bail!(
"该版本 {} (游戏文件夹:{}) 缺失元数据文件",
&self.version,
&self.version_base
)
} else {
if scl_config_path.is_file() {
let data = inner_future::fs::read_to_string(scl_config_path).await?;
let scl_config = serde_json::from_str(data.trim_start_matches('\u{feff}'))?; self.scl_launch_config = Some(scl_config);
}
let data = inner_future::fs::read_to_string(meta_path).await?;
let mut meta: VersionMeta =
serde_json::from_str(data.trim_start_matches('\u{feff}'))?; if jar_path.is_file() {
meta.main_jars.push(get_full_path(jar_path));
}
self.required_java = meta.required_java_version();
if let Some(assets) = &meta.asset_index {
if let Ok((_, ver)) = crate::semver::parse_version(&assets.id) {
self.minecraft_version = ver;
}
} else if !meta.inherits_from.is_empty() {
if let Ok((_, ver)) = crate::semver::parse_version(&meta.inherits_from) {
self.minecraft_version = ver;
}
}
self.meta = Some(meta);
self.version_type = self.guess_version_type();
Ok(())
}
} else {
anyhow::bail!("游戏文件夹 {} 不是正确的文件夹", &self.version_base)
}
}
pub async fn delete(self) {
let version_base_path = Path::new(&self.version_base);
if version_base_path.is_dir() {
let version_path = version_base_path.join(&self.version);
let _ = inner_future::fs::remove_dir_all(version_path).await;
}
}
pub async fn rename_version(&mut self, new_version_name: &str) -> DynResult {
let version_base_path = Path::new(&self.version_base);
if version_base_path.is_dir() {
let version_path = version_base_path.join(&self.version);
let version_jar_path = version_path.join(format!("{}.jar", self.version));
let version_json_path = version_path.join(format!("{}.json", self.version));
let new_version_path = version_base_path.join(new_version_name);
let new_version_jar_path = version_path.join(format!("{new_version_name}.jar"));
let new_version_json_path = version_path.join(format!("{new_version_name}.json"));
if new_version_path.is_dir() {
anyhow::bail!("目标版本名称已存在")
} else {
if version_jar_path.is_file() {
inner_future::fs::rename(version_jar_path, new_version_jar_path).await?;
}
if version_json_path.is_file() {
inner_future::fs::rename(version_json_path, new_version_json_path).await?
};
inner_future::fs::rename(version_path, new_version_path).await?;
self.version = new_version_name.to_owned();
Ok(())
}
} else {
anyhow::bail!("文件夹不存在")
}
}
pub async fn save(&self) -> DynResult {
let version_base_path = Path::new(&self.version_base);
if version_base_path.is_dir() {
let meta_path = version_base_path
.join(&self.version)
.join(format!("{}.json", &self.version));
let scl_config_path = version_base_path.join(&self.version).join(".scl.json");
if !meta_path.is_file() {
anyhow::bail!("版本 JSON 元数据文件缺失")
} else {
if let Some(meta) = &self.meta {
let file = std::fs::OpenOptions::new()
.truncate(true)
.write(true)
.open(meta_path);
if let Ok(file) = file {
match serde_json::to_writer(file, meta) {
Ok(_) => {}
Err(err) => {
anyhow::bail!("元数据解析失败:{}", err)
}
}
} else {
anyhow::bail!("无法打开版本元数据文件")
}
}
if let Some(scl_config) = &self.scl_launch_config {
let file = std::fs::OpenOptions::new()
.truncate(true)
.create(true)
.write(true)
.open(scl_config_path);
if let Ok(file) = file {
match serde_json::to_writer(file, scl_config) {
Ok(_) => {}
Err(err) => {
anyhow::bail!("SCL 配置文件写入失败:{}", err)
}
}
} else {
anyhow::bail!("无法打开 SCL 配置文件")
}
} else if scl_config_path.is_file() {
inner_future::fs::remove_file(scl_config_path).await?;
}
Ok(())
}
} else {
anyhow::bail!("版本文件夹未找到")
}
}
pub fn guess_version_type(&self) -> VersionType {
let mut has_optifine = false;
let mut has_fabric = false;
if let Some(meta) = &self.meta {
for lib in &meta.libraries {
if lib.name.starts_with("net.fabricmc:") {
has_fabric = true;
} else if lib.name.starts_with("net.minecraftforge:") {
return VersionType::Forge;
} else if lib.name.starts_with("org.quiltmc:") {
return VersionType::QuiltMC;
} else if lib.name.starts_with("optifine:") {
has_optifine = true;
}
}
if has_fabric {
VersionType::Fabric
} else if has_optifine {
VersionType::Optifine
} else {
VersionType::Vanilla
}
} else {
VersionType::Unknown
}
}
pub fn version_path(&self) -> PathBuf {
if self
.scl_launch_config
.as_ref()
.map(|x| x.game_independent)
.unwrap_or(false)
{
let mut result = PathBuf::new();
result.push(&self.version_base);
result.push(&self.version);
result
} else {
let mut result = PathBuf::new();
result.push(&self.version_base);
result.pop();
result
}
}
pub async fn get_mods(&self) -> DynResult<Vec<super::mods::Mod>> {
let mods_path = self.version_path().join("mods");
if !mods_path.is_dir() {
return Ok(vec![]);
}
let mut files = inner_future::fs::read_dir(mods_path).await?;
let mut results = vec![];
while let Some(file) = files.try_next().await? {
if file.path().is_file()
&& file
.path()
.file_name()
.map(|x| x.to_string_lossy().ends_with(".jar"))
== Some(true)
|| file
.path()
.file_name()
.map(|x| x.to_string_lossy().ends_with(".jar.disabled"))
== Some(true)
{
results.push(super::mods::Mod::from_path(file.path()).await?);
}
}
Ok(results)
}
pub async fn get_automated_maxium_memory(&self) -> u64 {
let mem_status = crate::utils::get_mem_status();
let mut free = mem_status.free as i64;
let mods = self.get_mods().await.unwrap_or_default();
let (mem_min, mem_t1, mem_t2, mem_t3) = if !mods.is_empty() {
(
400 + mods.len() as i64 * 7,
1500 + mods.len() as i64 * 10,
3000 + mods.len() as i64 * 17,
6000 + mods.len() as i64 * 34,
)
} else {
(300, 1500, 2500, 4000)
};
let mut result = 0;
let mem_delta = mem_t1;
free = (free - 100).max(0);
result += free.min(mem_delta);
free -= mem_delta + 100;
if free < 100 {
return result.max(mem_min) as _;
}
let mem_delta = mem_t2 - mem_t1;
free = (free - 100).max(0);
result += ((free as f64 * 0.8) as i64).min(mem_delta);
free -= ((mem_delta as f64 / 0.8) as i64) + 100;
if free < 100 {
return result.max(mem_min) as _;
}
let mem_delta = mem_t2 - mem_t1;
free = (free - 200).max(0);
result += ((free as f64 * 0.6) as i64).min(mem_delta);
free -= ((mem_delta as f64 / 0.6) as i64) + 200;
if free < 100 {
return result.max(mem_min) as _;
}
let mem_delta = mem_t3;
free = (free - 300).max(0);
result += ((free as f64 * 0.4) as i64).min(mem_delta);
free -= ((mem_delta as f64 / 0.4) as i64) + 300;
if free < 100 {
return result.max(mem_min) as _;
}
result.max(mem_min) as _
}
pub async fn get_saves(&self) -> DynResult<Vec<WorldSave>> {
let saves_path = self.version_path().join("saves");
let mut result = Vec::new();
let mut files = inner_future::fs::read_dir(&saves_path).await?;
while let Some(_file) = files.try_next().await? {
result.push(WorldSave {})
}
Ok(result)
}
pub async fn get_resources_packs(&self) -> DynResult<Vec<ResourcesPack>> {
let resourcepacks_path = self.version_path().join("resourcepacks");
let texturepacks_path = self.version_path().join("texturepacks");
let mut result = Vec::new();
if resourcepacks_path.is_dir() {
let mut files = inner_future::fs::read_dir(&resourcepacks_path).await?;
while let Some(_file) = files.try_next().await? {
result.push(ResourcesPack {})
}
}
if texturepacks_path.is_dir() {
let mut files = inner_future::fs::read_dir(&texturepacks_path).await?;
while let Some(_file) = files.try_next().await? {
result.push(ResourcesPack {})
}
}
Ok(result)
}
}
#[derive(Debug)]
pub struct WorldSave {
}
#[derive(Debug)]
pub struct ResourcesPack {
}
fn string_or_seq<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrVec(PhantomData<Vec<String>>);
impl<'de> Visitor<'de> for StringOrVec {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or list of strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![value.into()])
}
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where
S: SeqAccess<'de>,
{
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
}
}
deserializer.deserialize_any(StringOrVec(PhantomData))
}