fastnbt_tools/
lib.rs

1use fastanvil::{
2    tex::{Blockstate, Model, Render, Renderer, Texture},
3    Rgba,
4};
5use flate2::write::GzEncoder;
6use std::error::Error;
7use std::path::Path;
8use std::{collections::HashMap, fmt::Display};
9
10use regex::Regex;
11
12type Result<T> = std::result::Result<T, Box<dyn Error>>;
13
14#[derive(Debug)]
15struct ErrorMessage(&'static str);
16impl std::error::Error for ErrorMessage {}
17
18impl Display for ErrorMessage {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "{}", self.0)
21    }
22}
23
24fn avg_colour(rgba_data: &[u8]) -> Rgba {
25    let mut avg = [0f64; 4];
26    let mut count = 0;
27
28    for p in rgba_data.chunks(4) {
29        // alpha is reasonable.
30        avg[0] += ((p[0] as u64) * (p[0] as u64)) as f64;
31        avg[1] += ((p[1] as u64) * (p[1] as u64)) as f64;
32        avg[2] += ((p[2] as u64) * (p[2] as u64)) as f64;
33        avg[3] += ((p[3] as u64) * (p[3] as u64)) as f64;
34        count += 1;
35    }
36
37    [
38        (avg[0] / count as f64).sqrt() as u8,
39        (avg[1] / count as f64).sqrt() as u8,
40        (avg[2] / count as f64).sqrt() as u8,
41        (avg[3] / count as f64).sqrt() as u8,
42    ]
43}
44
45fn load_texture(path: &Path) -> Result<Texture> {
46    let img = image::open(path)?;
47    let img = img.to_rgba8();
48
49    //if img.dimensions() == (16, 16) {
50    Ok(img.into_raw())
51    // } else {
52    //     Err(Box::new(ErrorMessage("texture was not 16 by 16")))
53    // }
54}
55
56fn load_blockstates(blockstates_path: &Path) -> Result<HashMap<String, Blockstate>> {
57    let mut blockstates = HashMap::<String, Blockstate>::new();
58
59    for entry in std::fs::read_dir(blockstates_path)? {
60        let entry = entry?;
61        let path = entry.path();
62
63        if path.is_file() {
64            let json = std::fs::read_to_string(&path)?;
65
66            let json: Blockstate = serde_json::from_str(&json)?;
67            blockstates.insert(
68                "minecraft:".to_owned()
69                    + path
70                        .file_stem()
71                        .ok_or(format!("invalid file name: {}", path.display()))?
72                        .to_str()
73                        .ok_or(format!("nonunicode file name: {}", path.display()))?,
74                json,
75            );
76        }
77    }
78
79    Ok(blockstates)
80}
81
82fn load_models(path: &Path) -> Result<HashMap<String, Model>> {
83    let mut models = HashMap::<String, Model>::new();
84
85    for entry in std::fs::read_dir(path)? {
86        let entry = entry?;
87        let path = entry.path();
88
89        if path.is_file() {
90            let json = std::fs::read_to_string(&path)?;
91
92            let json: Model = serde_json::from_str(&json)?;
93            models.insert(
94                "minecraft:block/".to_owned()
95                    + path
96                        .file_stem()
97                        .ok_or(format!("invalid file name: {}", path.display()))?
98                        .to_str()
99                        .ok_or(format!("nonunicode file name: {}", path.display()))?,
100                json,
101            );
102        }
103    }
104
105    Ok(models)
106}
107
108fn load_textures(path: &Path) -> Result<HashMap<String, Texture>> {
109    let mut tex = HashMap::new();
110
111    for entry in std::fs::read_dir(path)? {
112        let entry = entry?;
113        let path = entry.path();
114
115        if path.is_file() && path.extension().ok_or("invalid ext")?.to_string_lossy() == "png" {
116            let texture = load_texture(&path);
117
118            match texture {
119                Err(_) => continue,
120                Ok(texture) => tex.insert(
121                    "minecraft:block/".to_owned()
122                        + path
123                            .file_stem()
124                            .ok_or(format!("invalid file name: {}", path.display()))?
125                            .to_str()
126                            .ok_or(format!("nonunicode file name: {}", path.display()))?,
127                    texture,
128                ),
129            };
130        }
131    }
132
133    Ok(tex)
134}
135
136#[derive(Debug)]
137struct RegexMapping {
138    blockstate: Regex,
139    texture_template: &'static str,
140}
141
142impl RegexMapping {
143    fn apply(&self, blockstate: &str) -> Option<String> {
144        let caps = self.blockstate.captures(blockstate)?;
145
146        let mut i = 1;
147        let mut tex = self.texture_template.to_string();
148
149        for cap in caps.iter().skip(1) {
150            let cap = match cap {
151                Some(cap) => cap,
152                None => continue,
153            };
154
155            tex = tex.replace(&format!("${}", i), cap.into());
156            i += 1;
157        }
158
159        Some(tex)
160    }
161}
162
163pub fn make_palette(mc_jar_path: &Path) -> Result<()> {
164    let assets = mc_jar_path.to_owned().join("assets").join("minecraft");
165
166    let textures = load_textures(&assets.join("textures").join("block"))?;
167    let blockstates = load_blockstates(&assets.join("blockstates"))?;
168    let models = load_models(&assets.join("models").join("block"))?;
169
170    let mut renderer = Renderer::new(blockstates.clone(), models, textures.clone());
171    let mut failed = 0;
172    let mut mapped = 0;
173    let mut success = 0;
174
175    let mappings = vec![
176        RegexMapping {
177            blockstate: Regex::new(r"minecraft:(.+)_fence").unwrap(),
178            texture_template: "minecraft:block/$1_planks",
179        },
180        RegexMapping {
181            blockstate: Regex::new(r"minecraft:(.+)_wall(_sign)?").unwrap(),
182            texture_template: "minecraft:block/$1_planks",
183        },
184        RegexMapping {
185            blockstate: Regex::new(r"minecraft:(.+)_wall(_sign)?").unwrap(),
186            texture_template: "minecraft:block/$1",
187        },
188        RegexMapping {
189            blockstate: Regex::new(r"minecraft:(.+)_glazed_terracotta").unwrap(),
190            texture_template: "minecraft:block/$1_glazed_terracotta",
191        },
192        RegexMapping {
193            blockstate: Regex::new(r"minecraft:(.+)_mushroom_block").unwrap(),
194            texture_template: "minecraft:block/$1_mushroom_block",
195        },
196        RegexMapping {
197            blockstate: Regex::new(r"minecraft:wheat").unwrap(),
198            texture_template: "minecraft:block/wheat_stage7",
199        },
200        RegexMapping {
201            blockstate: Regex::new(r"minecraft:carrots").unwrap(),
202            texture_template: "minecraft:block/carrots_stage3",
203        },
204        RegexMapping {
205            blockstate: Regex::new(r"minecraft:poppy").unwrap(),
206            texture_template: "minecraft:block/poppy",
207        },
208        RegexMapping {
209            blockstate: Regex::new(r"minecraft:daisy").unwrap(),
210            texture_template: "minecraft:block/daisy",
211        },
212        RegexMapping {
213            blockstate: Regex::new(r"minecraft:dandelion").unwrap(),
214            texture_template: "minecraft:block/dandelion",
215        },
216        RegexMapping {
217            blockstate: Regex::new(r"minecraft:oxeye_daisy").unwrap(),
218            texture_template: "minecraft:block/oxeye_daisy",
219        },
220        RegexMapping {
221            blockstate: Regex::new(r"minecraft:azure_bluet").unwrap(),
222            texture_template: "minecraft:block/azure_bluet",
223        },
224        RegexMapping {
225            blockstate: Regex::new(r"minecraft:lava").unwrap(),
226            texture_template: "minecraft:block/lava_still",
227        },
228        RegexMapping {
229            blockstate: Regex::new(r"minecraft:dead_bush").unwrap(),
230            texture_template: "minecraft:block/dead_bush",
231        },
232        RegexMapping {
233            blockstate: Regex::new(r"minecraft:(.+)_tulip").unwrap(),
234            texture_template: "minecraft:block/$1_tulip",
235        },
236        RegexMapping {
237            blockstate: Regex::new(r"minecraft:allium").unwrap(),
238            texture_template: "minecraft:block/allium",
239        },
240        RegexMapping {
241            blockstate: Regex::new(r"minecraft:cornflower").unwrap(),
242            texture_template: "minecraft:block/cornflower",
243        },
244        RegexMapping {
245            blockstate: Regex::new(r"minecraft:lily_of_the_valley").unwrap(),
246            texture_template: "minecraft:block/lily_of_the_valley",
247        },
248        RegexMapping {
249            blockstate: Regex::new(r"minecraft:sugar_cane").unwrap(),
250            texture_template: "minecraft:block/sugar_cane",
251        },
252        RegexMapping {
253            blockstate: Regex::new(r"minecraft:sunflower").unwrap(),
254            texture_template: "minecraft:block/sunflower_front",
255        },
256        RegexMapping {
257            blockstate: Regex::new(r"minecraft:peony").unwrap(),
258            texture_template: "minecraft:block/peony_top",
259        },
260        RegexMapping {
261            blockstate: Regex::new(r"minecraft:rose_bush").unwrap(),
262            texture_template: "minecraft:block/rose_bush_top",
263        },
264        RegexMapping {
265            blockstate: Regex::new(r"minecraft:lilac").unwrap(),
266            texture_template: "minecraft:block/lilac_top",
267        },
268        RegexMapping {
269            blockstate: Regex::new(r"minecraft:(.+)_orchid").unwrap(),
270            texture_template: "minecraft:block/$1_orchid",
271        },
272        RegexMapping {
273            blockstate: Regex::new(r"minecraft:sweet_berry_bush").unwrap(),
274            texture_template: "minecraft:block/sweet_berry_bush_stage3",
275        },
276        RegexMapping {
277            blockstate: Regex::new(r"minecraft:(.+)_mushroom").unwrap(),
278            texture_template: "minecraft:block/$1_mushroom",
279        },
280        RegexMapping {
281            blockstate: Regex::new(r"minecraft:potatoes").unwrap(),
282            texture_template: "minecraft:block/potatoes_stage3",
283        },
284        RegexMapping {
285            blockstate: Regex::new(r"minecraft:(\w+)_sapling").unwrap(),
286            texture_template: "minecraft:block/$1_sapling",
287        },
288        RegexMapping {
289            blockstate: Regex::new(r"minecraft:tripwire").unwrap(),
290            texture_template: "minecraft:block/tripwire",
291        },
292        RegexMapping {
293            blockstate: Regex::new(r"minecraft:bamboo").unwrap(),
294            texture_template: "minecraft:block/bamboo_stalk",
295        },
296        RegexMapping {
297            blockstate: Regex::new(r"minecraft:beetroots").unwrap(),
298            texture_template: "minecraft:block/beetroots_stage3",
299        },
300        RegexMapping {
301            blockstate: Regex::new(r"minecraft:fire").unwrap(),
302            texture_template: "minecraft:block/fire_0",
303        },
304    ];
305
306    let mut palette = HashMap::new();
307
308    let mut try_mapping = |mapping: &RegexMapping, blockstate: String| {
309        if let Some(tex) = mapping.apply(&blockstate) {
310            let texture = textures.get(&tex);
311            println!("map: {:?} to {}, {:?}", mapping, blockstate, tex);
312
313            if let Some(texture) = texture {
314                println!("mapped {} to {}", blockstate, tex);
315                mapped += 1;
316                let col = avg_colour(texture.as_slice());
317                return Some(col);
318            }
319        }
320
321        None
322    };
323
324    let mut try_mappings = |blockstate: String| {
325        let c = mappings
326            .iter()
327            .map(|mapping| try_mapping(mapping, blockstate.clone()))
328            .find_map(|col| col);
329
330        if c.is_none() {
331            println!("did not understand: {:?}", blockstate);
332            failed += 1;
333        }
334
335        c
336    };
337
338    for name in blockstates.keys() {
339        let bs = &blockstates[name];
340
341        match bs {
342            Blockstate::Variants(vars) => {
343                for props in vars.keys() {
344                    let res = renderer.get_top(name, props);
345                    match res {
346                        Ok(texture) => {
347                            let col = avg_colour(texture.as_slice());
348
349                            // We want to add the pipe if the props are anything
350                            // but empty.
351                            let description =
352                                (*name).clone() + if props.is_empty() { "" } else { "|" } + props;
353
354                            palette.insert(description, col);
355                            success += 1;
356                        }
357                        Err(_) => {
358                            if let Some(c) = try_mappings((*name).clone()) {
359                                palette.insert((*name).clone(), c);
360                                eprintln!("mapped {}", *name);
361                            }
362                        }
363                    };
364                }
365            }
366            Blockstate::Multipart(_) => {
367                if let Some(c) = try_mappings((*name).clone()) {
368                    palette.insert((*name).clone(), c);
369                }
370            }
371        }
372    }
373
374    // 1.17 renamed grass_path to dirt_path. This hacks it back in for old
375    // region files to still render them.
376    if let Some(path) = palette.get("minecraft:dirt_path").cloned() {
377        palette.insert("minecraft:grass_path".into(), path);
378    }
379
380    let f = std::fs::File::create("palette.tar.gz")?;
381    let f = GzEncoder::new(f, Default::default());
382
383    let mut ar = tar::Builder::new(f);
384
385    let grass_colourmap = &assets.join("textures").join("colormap").join("grass.png");
386    ar.append_file(
387        "grass-colourmap.png",
388        &mut std::fs::File::open(grass_colourmap)?,
389    )?;
390
391    let foliage_colourmap = &assets.join("textures").join("colormap").join("foliage.png");
392    ar.append_file(
393        "foliage-colourmap.png",
394        &mut std::fs::File::open(foliage_colourmap)?,
395    )?;
396
397    let palette_data = serde_json::to_vec(&palette)?;
398    let mut header = tar::Header::new_gnu();
399    header.set_size(palette_data.len() as u64);
400    header.set_cksum();
401    header.set_mode(0o666);
402    ar.append_data(&mut header, "blockstates.json", palette_data.as_slice())?;
403
404    // finishes the archive.
405    let f = ar.into_inner()?;
406    f.finish()?;
407
408    println!(
409        "succeeded in understanding {} of {} possible blocks (mapped {}, failed on {})",
410        success,
411        success + failed,
412        mapped,
413        failed,
414    );
415
416    Ok(())
417}