hexomc_lib/mods/
detector.rs1use serde::{Deserialize, Serialize};
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub enum ModLoader {
6 Fabric,
7 Forge,
8 NeoForge,
9 Quilt,
10 Unknown,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ModInfo {
15 pub file_name: String,
16 pub file_path: PathBuf,
17 pub mod_id: String,
18 pub name: String,
19 pub version: String,
20 pub loader: ModLoader,
21 pub description: Option<String>,
22}
23
24pub fn detect_mods(mods_dir: &Path) -> Vec<ModInfo> {
26 let mut mods = Vec::new();
27
28 let Ok(entries) = std::fs::read_dir(mods_dir) else {
29 return mods;
30 };
31
32 for entry in entries.flatten() {
33 let path = entry.path();
34 if path.extension().and_then(|e| e.to_str()) != Some("jar") {
35 continue;
36 }
37
38 if let Some(info) = read_mod_info(&path) {
39 mods.push(info);
40 }
41 }
42
43 mods
44}
45
46fn read_mod_info(jar_path: &Path) -> Option<ModInfo> {
47 let file = std::fs::File::open(jar_path).ok()?;
48 let mut archive = zip::ZipArchive::new(file).ok()?;
49
50 let file_name = jar_path.file_name()?.to_string_lossy().to_string();
51
52 if let Some(info) = read_fabric_mod(&mut archive, jar_path, &file_name) {
54 return Some(info);
55 }
56
57 if let Some(info) = read_forge_mod(&mut archive, jar_path, &file_name) {
59 return Some(info);
60 }
61
62 if let Some(info) = read_mcmod_info(&mut archive, jar_path, &file_name) {
64 return Some(info);
65 }
66
67 Some(ModInfo {
69 file_name: file_name.clone(),
70 file_path: jar_path.to_path_buf(),
71 mod_id: file_name.clone(),
72 name: file_name,
73 version: "unknown".to_string(),
74 loader: ModLoader::Unknown,
75 description: None,
76 })
77}
78
79#[derive(Deserialize)]
80struct FabricModJson {
81 id: String,
82 version: String,
83 name: Option<String>,
84 description: Option<String>,
85}
86
87fn read_fabric_mod(
88 archive: &mut zip::ZipArchive<std::fs::File>,
89 jar_path: &Path,
90 file_name: &str,
91) -> Option<ModInfo> {
92 let entry = archive.by_name("fabric.mod.json").ok()?;
93 let fabric: FabricModJson = serde_json::from_reader(entry).ok()?;
94
95 Some(ModInfo {
96 file_name: file_name.to_string(),
97 file_path: jar_path.to_path_buf(),
98 mod_id: fabric.id.clone(),
99 name: fabric.name.unwrap_or(fabric.id),
100 version: fabric.version,
101 loader: ModLoader::Fabric,
102 description: fabric.description,
103 })
104}
105
106fn read_forge_mod(
107 archive: &mut zip::ZipArchive<std::fs::File>,
108 jar_path: &Path,
109 file_name: &str,
110) -> Option<ModInfo> {
111 use std::io::Read;
112 let mut entry = archive.by_name("META-INF/mods.toml").ok()?;
113 let mut content = String::new();
114 entry.read_to_string(&mut content).ok()?;
115
116 let parsed: toml::Value = toml::from_str(&content).ok()?;
117 let mods_arr = parsed.get("mods")?.as_array()?;
118 let first = mods_arr.first()?;
119
120 let mod_id = first.get("modId")?.as_str()?.to_string();
121 let version = first
122 .get("version")
123 .and_then(|v| v.as_str())
124 .unwrap_or("unknown")
125 .to_string();
126 let name = first
127 .get("displayName")
128 .and_then(|v| v.as_str())
129 .unwrap_or(&mod_id)
130 .to_string();
131 let description = first
132 .get("description")
133 .and_then(|v| v.as_str())
134 .map(|s| s.to_string());
135
136 let loader = if content.contains("neoforge") {
138 ModLoader::NeoForge
139 } else {
140 ModLoader::Forge
141 };
142
143 Some(ModInfo {
144 file_name: file_name.to_string(),
145 file_path: jar_path.to_path_buf(),
146 mod_id,
147 name,
148 version,
149 loader,
150 description,
151 })
152}
153
154#[derive(Deserialize)]
155struct McModInfo {
156 modid: String,
157 name: Option<String>,
158 version: Option<String>,
159 description: Option<String>,
160}
161
162fn read_mcmod_info(
163 archive: &mut zip::ZipArchive<std::fs::File>,
164 jar_path: &Path,
165 file_name: &str,
166) -> Option<ModInfo> {
167 let entry = archive.by_name("mcmod.info").ok()?;
168 let val: serde_json::Value = serde_json::from_reader(entry).ok()?;
170
171 let info: McModInfo = if val.is_array() {
172 serde_json::from_value(val.as_array()?.first()?.clone()).ok()?
173 } else {
174 let list = val.get("modList")?.as_array()?.first()?.clone();
175 serde_json::from_value(list).ok()?
176 };
177
178 Some(ModInfo {
179 file_name: file_name.to_string(),
180 file_path: jar_path.to_path_buf(),
181 mod_id: info.modid.clone(),
182 name: info.name.unwrap_or(info.modid),
183 version: info.version.unwrap_or_else(|| "unknown".to_string()),
184 loader: ModLoader::Forge,
185 description: info.description,
186 })
187}