1use 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#[derive(Debug, Clone, Default)]
16pub struct Mod {
17 file_name: String,
18 path: PathBuf,
19 enabled: bool,
20}
21
22#[derive(Debug, Clone, Deserialize)]
28#[serde(untagged)]
29pub enum FabricModIcon {
30 Multiply(HashMap<String, String>),
32 Single(String),
34}
35
36#[derive(Debug, Clone, Default, Deserialize)]
38#[serde(default)]
39pub struct FabricModMeta {
40 pub name: String,
42 pub description: String,
44 pub version: String,
46 pub icon: Option<FabricModIcon>,
48}
49
50#[derive(Debug, Clone, Default, Deserialize)]
54#[serde(default)]
55pub struct ForgeModMeta {
56 pub name: String,
58 pub description: String,
60 pub version: String,
62 #[serde(rename = "logoFile")]
64 pub logo_file: String,
65}
66
67#[derive(Debug, Clone)]
71pub enum ModMeta {
72 Fabric(FabricModMeta),
74 Forge(ForgeModMeta),
76}
77
78impl ModMeta {
79 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 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#[derive(Debug, Clone, Deserialize)]
100pub struct NewForgeModMeta {
101 pub mods: Vec<ForgeModMeta>,
103}
104
105impl Mod {
106 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 pub fn is_enabled(&self) -> bool {
120 self.enabled
121 }
122
123 pub fn file_name(&self) -> &str {
125 &self.file_name
126 }
127
128 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 pub fn path(&self) -> &PathBuf {
140 &self.path
141 }
142
143 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 Ok(ModMeta::Fabric(meta))
160 } else if let Ok(meta) = z.by_name("mods.toml").and_then(|mut r| {
161 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 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 pub async fn try_get_mod_name(&self) -> DynResult<String> {
187 Ok(self.try_get_mod_meta().await?.name().to_owned())
188 }
189
190 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 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 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 pub async fn remove(self) -> DynResult {
253 inner_future::fs::remove_file(self.path).await?;
254 Ok(())
255 }
256}