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 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 Ok(img.into_raw())
51 }
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 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 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 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}