schematic_mesher/resource_pack/
loader.rs

1//! Resource pack loading from ZIP files and directories.
2
3use super::{BlockModel, BlockstateDefinition, ResourcePack, TextureData};
4use crate::error::{MesherError, Result};
5use crate::resource_pack::texture::load_texture_from_bytes;
6use std::io::Read;
7use std::path::Path;
8
9/// Load a resource pack from a file path.
10///
11/// Supports both ZIP files and directories.
12pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<ResourcePack> {
13    let path = path.as_ref();
14
15    if path.is_dir() {
16        load_from_directory(path)
17    } else {
18        let data = std::fs::read(path)?;
19        load_from_bytes(&data)
20    }
21}
22
23/// Load a resource pack from bytes (ZIP data).
24pub fn load_from_bytes(data: &[u8]) -> Result<ResourcePack> {
25    let cursor = std::io::Cursor::new(data);
26    let mut archive = zip::ZipArchive::new(cursor)?;
27
28    let mut pack = ResourcePack::new();
29
30    // Iterate through all files in the archive
31    for i in 0..archive.len() {
32        let mut file = archive.by_index(i)?;
33        let file_path = file.name().to_string();
34
35        // Skip directories
36        if file.is_dir() {
37            continue;
38        }
39
40        // Parse the path to determine namespace and type
41        if let Some((namespace, asset_type, asset_path)) = parse_asset_path(&file_path) {
42            match asset_type {
43                "blockstates" => {
44                    if asset_path.ends_with(".json") {
45                        let mut contents = String::new();
46                        file.read_to_string(&mut contents)?;
47
48                        let block_id = asset_path.trim_end_matches(".json");
49                        match serde_json::from_str::<BlockstateDefinition>(&contents) {
50                            Ok(def) => {
51                                pack.add_blockstate(namespace, block_id, def);
52                            }
53                            Err(e) => {
54                                // Log warning but continue
55                                eprintln!(
56                                    "Warning: Failed to parse blockstate {}/{}: {}",
57                                    namespace, block_id, e
58                                );
59                            }
60                        }
61                    }
62                }
63                "models" => {
64                    if asset_path.ends_with(".json") {
65                        let mut contents = String::new();
66                        file.read_to_string(&mut contents)?;
67
68                        let model_path = asset_path.trim_end_matches(".json");
69                        match serde_json::from_str::<BlockModel>(&contents) {
70                            Ok(model) => {
71                                pack.add_model(namespace, model_path, model);
72                            }
73                            Err(e) => {
74                                eprintln!(
75                                    "Warning: Failed to parse model {}/{}: {}",
76                                    namespace, model_path, e
77                                );
78                            }
79                        }
80                    }
81                }
82                "textures" => {
83                    if asset_path.ends_with(".png") {
84                        let mut data = Vec::new();
85                        file.read_to_end(&mut data)?;
86
87                        let texture_path = asset_path.trim_end_matches(".png");
88                        match load_texture_from_bytes(&data) {
89                            Ok(texture) => {
90                                pack.add_texture(namespace, texture_path, texture);
91                            }
92                            Err(e) => {
93                                eprintln!(
94                                    "Warning: Failed to load texture {}/{}: {}",
95                                    namespace, texture_path, e
96                                );
97                            }
98                        }
99                    }
100                }
101                _ => {}
102            }
103        }
104    }
105
106    Ok(pack)
107}
108
109/// Load a resource pack from a directory.
110fn load_from_directory(path: &Path) -> Result<ResourcePack> {
111    let mut pack = ResourcePack::new();
112
113    // Look for assets directory
114    let assets_path = path.join("assets");
115    if !assets_path.exists() {
116        return Err(MesherError::InvalidResourcePack(
117            "No assets directory found".to_string(),
118        ));
119    }
120
121    // Iterate through namespaces
122    for namespace_entry in std::fs::read_dir(&assets_path)? {
123        let namespace_entry = namespace_entry?;
124        if !namespace_entry.file_type()?.is_dir() {
125            continue;
126        }
127
128        let namespace = namespace_entry
129            .file_name()
130            .to_string_lossy()
131            .to_string();
132        let namespace_path = namespace_entry.path();
133
134        // Load blockstates
135        let blockstates_path = namespace_path.join("blockstates");
136        if blockstates_path.exists() {
137            load_json_files(&blockstates_path, &namespace, |block_id, contents| {
138                if let Ok(def) = serde_json::from_str::<BlockstateDefinition>(contents) {
139                    pack.add_blockstate(&namespace, block_id, def);
140                }
141            })?;
142        }
143
144        // Load models
145        let models_path = namespace_path.join("models");
146        if models_path.exists() {
147            load_json_files_recursive(&models_path, &models_path, &namespace, &mut |model_path, contents| {
148                if let Ok(model) = serde_json::from_str::<BlockModel>(contents) {
149                    pack.add_model(&namespace, model_path, model);
150                }
151            })?;
152        }
153
154        // Load textures
155        let textures_path = namespace_path.join("textures");
156        if textures_path.exists() {
157            load_texture_files_recursive(&textures_path, &textures_path, &namespace, &mut |texture_path, data| {
158                if let Ok(texture) = load_texture_from_bytes(data) {
159                    pack.add_texture(&namespace, texture_path, texture);
160                }
161            })?;
162        }
163    }
164
165    Ok(pack)
166}
167
168/// Parse an asset path from a ZIP file.
169/// Returns (namespace, asset_type, asset_path) if valid.
170fn parse_asset_path(file_path: &str) -> Option<(&str, &str, &str)> {
171    // Expected format: assets/{namespace}/{type}/{path}
172    let parts: Vec<&str> = file_path.splitn(4, '/').collect();
173
174    if parts.len() >= 4 && parts[0] == "assets" {
175        Some((parts[1], parts[2], parts[3]))
176    } else {
177        None
178    }
179}
180
181/// Load JSON files from a directory.
182fn load_json_files<F>(dir: &Path, namespace: &str, mut handler: F) -> Result<()>
183where
184    F: FnMut(&str, &str),
185{
186    for entry in std::fs::read_dir(dir)? {
187        let entry = entry?;
188        let path = entry.path();
189
190        if path.extension().map(|e| e == "json").unwrap_or(false) {
191            let file_name = path
192                .file_stem()
193                .unwrap()
194                .to_string_lossy()
195                .to_string();
196
197            let contents = std::fs::read_to_string(&path)?;
198            handler(&file_name, &contents);
199        }
200    }
201    Ok(())
202}
203
204/// Load JSON files recursively from a directory.
205fn load_json_files_recursive<F>(
206    base: &Path,
207    dir: &Path,
208    namespace: &str,
209    handler: &mut F,
210) -> Result<()>
211where
212    F: FnMut(&str, &str),
213{
214    for entry in std::fs::read_dir(dir)? {
215        let entry = entry?;
216        let path = entry.path();
217
218        if path.is_dir() {
219            load_json_files_recursive(base, &path, namespace, handler)?;
220        } else if path.extension().map(|e| e == "json").unwrap_or(false) {
221            let relative = path
222                .strip_prefix(base)
223                .unwrap()
224                .with_extension("")
225                .to_string_lossy()
226                .replace('\\', "/");
227
228            let contents = std::fs::read_to_string(&path)?;
229            handler(&relative, &contents);
230        }
231    }
232    Ok(())
233}
234
235/// Load texture files recursively from a directory.
236fn load_texture_files_recursive<F>(
237    base: &Path,
238    dir: &Path,
239    namespace: &str,
240    handler: &mut F,
241) -> Result<()>
242where
243    F: FnMut(&str, &[u8]),
244{
245    for entry in std::fs::read_dir(dir)? {
246        let entry = entry?;
247        let path = entry.path();
248
249        if path.is_dir() {
250            load_texture_files_recursive(base, &path, namespace, handler)?;
251        } else if path.extension().map(|e| e == "png").unwrap_or(false) {
252            let relative = path
253                .strip_prefix(base)
254                .unwrap()
255                .with_extension("")
256                .to_string_lossy()
257                .replace('\\', "/");
258
259            let data = std::fs::read(&path)?;
260            handler(&relative, &data);
261        }
262    }
263    Ok(())
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_parse_asset_path() {
272        assert_eq!(
273            parse_asset_path("assets/minecraft/blockstates/stone.json"),
274            Some(("minecraft", "blockstates", "stone.json"))
275        );
276        assert_eq!(
277            parse_asset_path("assets/minecraft/models/block/stone.json"),
278            Some(("minecraft", "models", "block/stone.json"))
279        );
280        assert_eq!(
281            parse_asset_path("assets/mymod/textures/block/custom.png"),
282            Some(("mymod", "textures", "block/custom.png"))
283        );
284        assert_eq!(parse_asset_path("pack.mcmeta"), None);
285        assert_eq!(parse_asset_path("data/minecraft/recipes/test.json"), None);
286    }
287}