hexomc_lib/assets/
detector.rs1use 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 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 pub icon: Option<Vec<u8>>,
26}
27
28pub 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
57pub 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
91pub 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
270fn 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}