Skip to main content

hexomc_lib/mods/
detector.rs

1use 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
24/// Scan a mods directory, reading each .jar's metadata.
25pub 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    // Fabric: fabric.mod.json
53    if let Some(info) = read_fabric_mod(&mut archive, jar_path, &file_name) {
54        return Some(info);
55    }
56
57    // Forge/NeoForge: META-INF/mods.toml
58    if let Some(info) = read_forge_mod(&mut archive, jar_path, &file_name) {
59        return Some(info);
60    }
61
62    // Old Forge: mcmod.info
63    if let Some(info) = read_mcmod_info(&mut archive, jar_path, &file_name) {
64        return Some(info);
65    }
66
67    // Unrecognized: return basic info.
68    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    // NeoForge if mods.toml mentions neoforge.
137    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    // mcmod.info may be an array or an object with a "modList" key.
169    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}