Skip to main content

hexomc_lib/assets/
detector.rs

1use std::path::{Path, PathBuf};
2use crate::assets::nbt;
3
4
5#[derive(Debug, Clone)]
6pub struct ResourcePack {
7    pub name: String,
8    pub path: PathBuf,
9    /// true = folder, false = .zip
10    pub is_folder: bool,
11}
12
13#[derive(Debug, Clone)]
14pub struct ShaderPack {
15    pub name: String,
16    pub path: PathBuf,
17    pub is_folder: bool,
18}
19
20#[derive(Debug, Clone)]
21pub struct WorldMap {
22    pub name: String,
23    pub path: PathBuf,
24    /// Raw bytes of the world's icon.png, if present.
25    pub icon: Option<Vec<u8>>,
26}
27
28/// Scan `.minecraft/resourcepacks/`. A valid pack has `pack.mcmeta` (folder) or is
29/// a `.zip` with `pack.mcmeta` at its root.
30pub fn detect_resource_packs(minecraft_dir: &Path) -> Vec<ResourcePack> {
31    let dir = minecraft_dir.join("resourcepacks");
32    scan_packs(&dir, is_resource_pack_zip, is_resource_pack_folder)
33        .into_iter()
34        .map(|(name, path, is_folder)| ResourcePack {
35            name,
36            path,
37            is_folder,
38        })
39        .collect()
40}
41
42fn is_resource_pack_zip(zip_path: &Path) -> bool {
43    let Ok(file) = std::fs::File::open(zip_path) else {
44        return false;
45    };
46    let Ok(mut archive) = zip::ZipArchive::new(file) else {
47        return false;
48    };
49    let result = archive.by_name("pack.mcmeta").is_ok();
50    result
51}
52
53fn is_resource_pack_folder(folder: &Path) -> bool {
54    folder.join("pack.mcmeta").exists()
55}
56
57/// Scan `.minecraft/shaderpacks/`. A valid pack has a `shaders/` directory
58/// (folder) or a `shaders/` folder inside the zip.
59pub fn detect_shader_packs(minecraft_dir: &Path) -> Vec<ShaderPack> {
60    let dir = minecraft_dir.join("shaderpacks");
61    scan_packs(&dir, is_shader_zip, is_shader_folder)
62        .into_iter()
63        .map(|(name, path, is_folder)| ShaderPack {
64            name,
65            path,
66            is_folder,
67        })
68        .collect()
69}
70
71fn is_shader_zip(zip_path: &Path) -> bool {
72    let Ok(file) = std::fs::File::open(zip_path) else {
73        return false;
74    };
75    let Ok(mut archive) = zip::ZipArchive::new(file) else {
76        return false;
77    };
78    let result = (0..archive.len()).any(|i| {
79        archive
80            .by_index_raw(i)
81            .map(|e| e.name().starts_with("shaders/"))
82            .unwrap_or(false)
83    });
84    result
85}
86
87fn is_shader_folder(folder: &Path) -> bool {
88    folder.join("shaders").is_dir()
89}
90
91/// Scan `.minecraft/saves/`. A folder containing `level.dat` is a world.
92pub fn detect_maps(minecraft_dir: &Path) -> Vec<WorldMap> {
93    let saves_dir = minecraft_dir.join("saves");
94    let mut maps = Vec::new();
95
96    let Ok(entries) = std::fs::read_dir(&saves_dir) else {
97        return maps;
98    };
99
100    for entry in entries.flatten() {
101        let path = entry.path();
102        if !path.is_dir() {
103            continue;
104        }
105        if !path.join("level.dat").exists() {
106            continue;
107        }
108
109        let name = path
110            .file_name()
111            .unwrap_or_default()
112            .to_string_lossy()
113            .to_string();
114        let icon_path = path.join("icon.png");
115        let icon = std::fs::read(&icon_path).ok();
116
117        maps.push(WorldMap { name, path, icon });
118    }
119
120    maps
121}
122
123pub fn detect_servers(minecraft_dir: &Path) -> Vec<String> {
124    let servers_file = minecraft_dir.join("servers.dat");
125    if !servers_file.exists() {
126        return Vec::new();
127    }
128
129    let bytes = std::fs::read(&servers_file).unwrap_or_default();
130    let nbt = nbt::parse(&bytes);
131    let mut servers = Vec::new();
132
133    if let Some(nbt::Nbt::Compound(map)) = nbt {
134        if let Some(nbt::Nbt::List(list)) = map.get("servers") {
135            for item in list {
136                if let nbt::Nbt::Compound(server_map) = item {
137                    if let Some(nbt::Nbt::String(name)) = server_map.get("name") {
138                        servers.push(name.clone());
139                    }
140                }
141            }
142        }
143    }
144
145    servers
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use std::fs;
152    use std::io::Write;
153    use tempfile::TempDir;
154    use zip::write::{FileOptions, ZipWriter};
155
156    fn make_zip_with_file(dir: &std::path::Path, zip_name: &str, inner_file: &str) -> PathBuf {
157        let zip_path = dir.join(zip_name);
158        let file = fs::File::create(&zip_path).unwrap();
159        let mut zip = ZipWriter::new(file);
160        let opts = FileOptions::<()>::default();
161        zip.start_file(inner_file, opts).unwrap();
162        zip.write_all(b"{}").unwrap();
163        zip.finish().unwrap();
164        zip_path
165    }
166
167    #[test]
168    fn detect_resource_pack_zip() {
169        let tmp = TempDir::new().unwrap();
170        let mc_dir = tmp.path();
171        let rp_dir = mc_dir.join("resourcepacks");
172        fs::create_dir_all(&rp_dir).unwrap();
173        make_zip_with_file(&rp_dir, "MyPack.zip", "pack.mcmeta");
174
175        let packs = detect_resource_packs(mc_dir);
176        assert_eq!(packs.len(), 1);
177        assert_eq!(packs[0].name, "MyPack");
178        assert!(!packs[0].is_folder);
179    }
180
181    #[test]
182    fn detect_resource_pack_folder() {
183        let tmp = TempDir::new().unwrap();
184        let mc_dir = tmp.path();
185        let rp_dir = mc_dir.join("resourcepacks").join("FolderPack");
186        fs::create_dir_all(&rp_dir).unwrap();
187        fs::write(rp_dir.join("pack.mcmeta"), b"{}").unwrap();
188
189        let packs = detect_resource_packs(mc_dir);
190        assert_eq!(packs.len(), 1);
191        assert_eq!(packs[0].name, "FolderPack");
192        assert!(packs[0].is_folder);
193    }
194
195    #[test]
196    fn invalid_zip_ignored() {
197        let tmp = TempDir::new().unwrap();
198        let mc_dir = tmp.path();
199        let rp_dir = mc_dir.join("resourcepacks");
200        fs::create_dir_all(&rp_dir).unwrap();
201        make_zip_with_file(&rp_dir, "NotAPack.zip", "some_other_file.txt");
202
203        let packs = detect_resource_packs(mc_dir);
204        assert!(packs.is_empty());
205    }
206
207    #[test]
208    fn detect_shader_pack_folder() {
209        let tmp = TempDir::new().unwrap();
210        let mc_dir = tmp.path();
211        let sp_dir = mc_dir.join("shaderpacks").join("MyShader");
212        fs::create_dir_all(sp_dir.join("shaders")).unwrap();
213
214        let shaders = detect_shader_packs(mc_dir);
215        assert_eq!(shaders.len(), 1);
216        assert_eq!(shaders[0].name, "MyShader");
217    }
218
219    #[test]
220    fn detect_shader_pack_zip() {
221        let tmp = TempDir::new().unwrap();
222        let mc_dir = tmp.path();
223        let sp_dir = mc_dir.join("shaderpacks");
224        fs::create_dir_all(&sp_dir).unwrap();
225        make_zip_with_file(&sp_dir, "CoolShader.zip", "shaders/final.fsh");
226
227        let shaders = detect_shader_packs(mc_dir);
228        assert_eq!(shaders.len(), 1);
229        assert_eq!(shaders[0].name, "CoolShader");
230    }
231
232    #[test]
233    fn detect_world_map() {
234        let tmp = TempDir::new().unwrap();
235        let mc_dir = tmp.path();
236        let world_dir = mc_dir.join("saves").join("My World");
237        fs::create_dir_all(&world_dir).unwrap();
238        fs::write(world_dir.join("level.dat"), b"\x1f\x8b").unwrap();
239
240        let maps = detect_maps(mc_dir);
241        assert_eq!(maps.len(), 1);
242        assert_eq!(maps[0].name, "My World");
243        assert!(maps[0].icon.is_none());
244    }
245
246    #[test]
247    fn world_with_icon() {
248        let tmp = TempDir::new().unwrap();
249        let mc_dir = tmp.path();
250        let world_dir = mc_dir.join("saves").join("IconWorld");
251        fs::create_dir_all(&world_dir).unwrap();
252        fs::write(world_dir.join("level.dat"), b"data").unwrap();
253        fs::write(world_dir.join("icon.png"), b"\x89PNG").unwrap();
254
255        let maps = detect_maps(mc_dir);
256        assert_eq!(maps.len(), 1);
257        assert!(maps[0].icon.is_some());
258        assert_eq!(maps[0].icon.as_deref(), Some(b"\x89PNG".as_ref()));
259    }
260
261    #[test]
262    fn empty_saves_dir() {
263        let tmp = TempDir::new().unwrap();
264        let mc_dir = tmp.path();
265        fs::create_dir_all(mc_dir.join("saves")).unwrap();
266        assert!(detect_maps(mc_dir).is_empty());
267    }
268}
269
270/// Generic scan returning (name, path, is_folder).
271fn scan_packs(
272    dir: &Path,
273    check_zip: impl Fn(&Path) -> bool,
274    check_folder: impl Fn(&Path) -> bool,
275) -> Vec<(String, PathBuf, bool)> {
276    let mut results = Vec::new();
277
278    let Ok(entries) = std::fs::read_dir(dir) else {
279        return results;
280    };
281
282    for entry in entries.flatten() {
283        let path = entry.path();
284
285        if path.is_file() {
286            if path.extension().and_then(|e| e.to_str()) == Some("zip") {
287                if check_zip(&path) {
288                    let name = path
289                        .file_stem()
290                        .unwrap_or_default()
291                        .to_string_lossy()
292                        .to_string();
293                    results.push((name, path, false));
294                }
295            }
296        } else if path.is_dir() {
297            if check_folder(&path) {
298                let name = path
299                    .file_name()
300                    .unwrap_or_default()
301                    .to_string_lossy()
302                    .to_string();
303                results.push((name, path, true));
304            }
305        }
306    }
307
308    results
309}