scl_core/version/
mods.rs

1//! 模组文件的管理
2use std::{
3    collections::HashMap,
4    io::Read,
5    path::{Path, PathBuf},
6};
7
8use anyhow::Context;
9use image::DynamicImage;
10
11use crate::prelude::*;
12
13/// 模组文件数据
14/// 可通过 [`crate::version::structs::VersionInfo::get_mods`] 获取
15#[derive(Debug, Clone, Default)]
16pub struct Mod {
17    file_name: String,
18    path: PathBuf,
19    enabled: bool,
20}
21
22/// 一个 Fabric 模组的图标集,其内的成员都是文件路径
23///
24/// 图标可以是单个也可以是多个
25///
26/// 在 SCL 里,如果提供多个则默认展示第一个
27#[derive(Debug, Clone, Deserialize)]
28#[serde(untagged)]
29pub enum FabricModIcon {
30    /// 多个图标
31    Multiply(HashMap<String, String>),
32    /// 单个图标
33    Single(String),
34}
35
36/// 一个 Fabric 模组的元数据信息
37#[derive(Debug, Clone, Default, Deserialize)]
38#[serde(default)]
39pub struct FabricModMeta {
40    /// 模组的名称
41    pub name: String,
42    /// 模组的介绍
43    pub description: String,
44    /// 模组的版本号
45    pub version: String,
46    /// 模组的图标集
47    pub icon: Option<FabricModIcon>,
48}
49
50/// 一个 Forge 模组的元数据信息
51///
52/// 注:在对老版本 1.12.2 以前的模组需要使用 [`Vec<ForgeModMeta>`] 来反序列化模组信息
53#[derive(Debug, Clone, Default, Deserialize)]
54#[serde(default)]
55pub struct ForgeModMeta {
56    /// 模组的名称
57    pub name: String,
58    /// 模组的介绍
59    pub description: String,
60    /// 模组的版本号
61    pub version: String,
62    /// 模组的图标文件路径
63    #[serde(rename = "logoFile")]
64    pub logo_file: String,
65}
66
67/// 一个模组的元数据信息
68///
69/// 有可能是 Forge 模组或者 Fabric 模组
70#[derive(Debug, Clone)]
71pub enum ModMeta {
72    /// 一个 Fabric 模组的元数据
73    Fabric(FabricModMeta),
74    /// 一个 Forge 模组的元数据
75    Forge(ForgeModMeta),
76}
77
78impl ModMeta {
79    /// 获取模组的名字
80    pub fn name(&self) -> &str {
81        match &self {
82            ModMeta::Fabric(m) => m.name.as_str(),
83            ModMeta::Forge(m) => m.name.as_str(),
84        }
85    }
86
87    /// 获取模组的版本号
88    pub fn version(&self) -> &str {
89        match &self {
90            ModMeta::Fabric(m) => m.version.as_str(),
91            ModMeta::Forge(m) => m.version.as_str(),
92        }
93    }
94}
95
96/// 新版 Forge 模组的元数据信息
97///
98/// 新版采用了 TOML 文件记录元数据,且支持单包多模组,故此处的 [`NewForgeModMeta::mods`] 是一个数组
99#[derive(Debug, Clone, Deserialize)]
100pub struct NewForgeModMeta {
101    /// 当前模组包包含的所有模组
102    pub mods: Vec<ForgeModMeta>,
103}
104
105impl Mod {
106    /// 通过模组文件路径获取当前模组的信息
107    pub async fn from_path(path: impl AsRef<Path>) -> DynResult<Self> {
108        let path = path.as_ref();
109        let file_name = path.file_name().unwrap().to_string_lossy().to_string();
110        let enabled = !file_name.ends_with(".disabled");
111        Ok(Self {
112            file_name,
113            path: path.to_path_buf(),
114            enabled,
115        })
116    }
117
118    /// 模组是否已经启用(既是否以 `.jar.disabled` 结尾)
119    pub fn is_enabled(&self) -> bool {
120        self.enabled
121    }
122
123    /// 模组的文件名
124    pub fn file_name(&self) -> &str {
125        &self.file_name
126    }
127
128    /// 尝试获取一个可供显示的模组名称
129    pub async fn display_name(&self) -> String {
130        self.try_get_mod_name().await.unwrap_or_else(|_| {
131            self.file_name()
132                .trim_end_matches(".jar.disabled")
133                .trim_end_matches(".jar")
134                .to_owned()
135        })
136    }
137
138    /// 模组的文件所在路径
139    pub fn path(&self) -> &PathBuf {
140        &self.path
141    }
142
143    /// 尝试获取模组的元数据信息
144    pub async fn try_get_mod_meta(&self) -> DynResult<ModMeta> {
145        let path = self.path.to_owned();
146        inner_future::unblock(move || -> DynResult<ModMeta> {
147            let mut z = zip::ZipArchive::new(
148                std::fs::OpenOptions::new()
149                    .read(true)
150                    .open(&path)
151                    .with_context(|| format!("无法打开模组文件 {}", path.to_string_lossy()))
152                    .with_context(|| format!("无法读取模组文件 {}", path.to_string_lossy()))?,
153            )?;
154            if let Ok(meta) = z.by_name("fabric.mod.json").and_then(|r| {
155                serde_json::from_reader::<_, FabricModMeta>(r)
156                    .map_err(|_| zip::result::ZipError::InvalidArchive("fabric.mod.json"))
157            }) {
158                // fabric.mod.json https://github.com/FabricMC/fabric-example-mod/blob/1.19/src/main/resources/fabric.mod.json
159                Ok(ModMeta::Fabric(meta))
160            } else if let Ok(meta) = z.by_name("mods.toml").and_then(|mut r| {
161                // mods.toml https://docs.minecraftforge.net/en/1.19.x/gettingstarted/structuring/
162                let mut buf = String::with_capacity(r.size() as _);
163                r.read_to_string(&mut buf).unwrap_or_default();
164                toml::from_str::<NewForgeModMeta>(&buf)
165                    .map_err(|_| zip::result::ZipError::InvalidArchive("mods.toml"))
166            }) {
167                Ok(ModMeta::Forge(meta.mods.first().cloned().ok_or_else(
168                    || anyhow::anyhow!("Can't get mod info from mcmod.info"),
169                )?))
170            } else if let Ok(meta) = z.by_name("mcmod.info").and_then(|r| {
171                // mcmod.info https://docs.minecraftforge.net/en/1.12.x/gettingstarted/structuring/
172                serde_json::from_reader::<_, Vec<ForgeModMeta>>(r)
173                    .map_err(|_| zip::result::ZipError::InvalidArchive("mcmod.info"))
174            }) {
175                Ok(ModMeta::Forge(meta.first().cloned().ok_or_else(|| {
176                    anyhow::anyhow!("Can't get mod info from mcmod.info")
177                })?))
178            } else {
179                anyhow::bail!("Mod name not found")
180            }
181        })
182        .await
183    }
184
185    /// [`Mod::try_get_mod_meta`] 的语法糖,尝试根据模组元数据获取模组名称
186    pub async fn try_get_mod_name(&self) -> DynResult<String> {
187        Ok(self.try_get_mod_meta().await?.name().to_owned())
188    }
189
190    /// [`Mod::try_get_mod_meta`] 的语法糖,尝试根据模组元数据获取模组的其中一个图标
191    pub async fn try_get_mod_icon(&self) -> DynResult<DynamicImage> {
192        let icon_path = match self.try_get_mod_meta().await? {
193            ModMeta::Fabric(meta) => match meta.icon {
194                Some(FabricModIcon::Multiply(icon)) => {
195                    icon.iter().next().map(|x| x.0.to_owned()).ok_or_else(|| {
196                        anyhow::anyhow!("Can't find icon from such multiply fabric mod icons")
197                    })?
198                }
199                Some(FabricModIcon::Single(icon)) => icon,
200                None => anyhow::bail!("Can't find icon from such multiply fabric mod icons"),
201            },
202            ModMeta::Forge(meta) => meta.logo_file,
203        };
204        let path = self.path.to_owned();
205        inner_future::unblock(move || -> DynResult<DynamicImage> {
206            let mut z = zip::ZipArchive::new(std::fs::OpenOptions::new().read(true).open(path)?)?;
207            let mut r = z.by_name(icon_path.trim_start_matches(['/', '\\']))?;
208            let mut buf = Vec::with_capacity(r.size() as _);
209            r.read_to_end(&mut buf)?;
210            Ok(image::load_from_memory(&buf)?)
211        })
212        .await
213    }
214
215    /// 如果模组是禁用的(既以 `.jar.disabled` 结尾),启用该模组(既还原成 `.jar` 结尾)
216    ///
217    /// 如果重命名失败则会返回错误
218    pub async fn enable(&mut self) -> DynResult {
219        if self.file_name.ends_with(".disabled") && !self.enabled {
220            let mut path = self.path.clone();
221            path.set_file_name(&self.file_name[..self.file_name.len() - 9]);
222            inner_future::fs::rename(&self.path, &path).await?;
223            self.file_name = path.file_name().unwrap().to_string_lossy().to_string();
224            self.path = path;
225            self.enabled = true;
226        }
227        Ok(())
228    }
229
230    /// 如果模组是启用的(既以 `.jar` 结尾),禁用该模组(既重命名为以 `.jar.disabled` 结尾)
231    ///
232    /// 如果重命名失败则会返回错误
233    pub async fn disable(&mut self) -> DynResult {
234        if !self.file_name.ends_with(".disabled") && self.enabled {
235            let mut path = self.path.clone();
236            path.set_file_name(&self.file_name);
237            path.set_extension(format!(
238                "{}.disabled",
239                path.extension()
240                    .map(|x| x.to_string_lossy())
241                    .unwrap_or_default()
242            ));
243            inner_future::fs::rename(&self.path, &path).await?;
244            self.file_name = path.file_name().unwrap().to_string_lossy().to_string();
245            self.path = path;
246            self.enabled = false;
247        }
248        Ok(())
249    }
250
251    /// 删除该模组,**此操作不可撤销**
252    pub async fn remove(self) -> DynResult {
253        inner_future::fs::remove_file(self.path).await?;
254        Ok(())
255    }
256}