1use std::{
4 borrow::Cow,
5 sync::{LazyLock, Mutex},
6};
7
8use tracing::{trace, warn};
9
10use crate::{
11 Map, Regex, Set,
12 convert::{
13 determine_orientation, material,
14 material::{convert_wood, is_stone, is_tree},
15 mods::Mod,
16 orientation_to_radians, reverse_orientation,
17 },
18 entity::Value,
19 formats::{
20 Settings,
21 internal::{EntityMap, MinecraftBlockCode, PropertyMap, PropertyMapExt},
22 },
23};
24
25const MAX_FLUID: u8 = 7;
27
28const MAX_SNOW: u8 = 8;
30
31pub(crate) const VS_CHEST_SLOTS: i32 = 16;
33
34pub(crate) const VS_TRUNK_SLOTS: i32 = 36;
36
37pub(crate) const VS_FLOWER_POT_CODE: &str = "game:flowerpot-red-fired";
38
39struct CropData {
41 name: &'static str,
43
44 mc_max_age: u8,
46
47 vs_max_age: u8,
49}
50
51static CROPS: LazyLock<Map<&'static str, CropData>> = LazyLock::new(|| {
53 Map::from([
56 (
57 "carrots",
58 CropData {
59 name: "carrot",
60 mc_max_age: 7,
61 vs_max_age: 7,
62 },
63 ),
64 (
65 "potatoes",
66 CropData {
67 name: "parsnip",
68 mc_max_age: 7,
69 vs_max_age: 8,
70 },
71 ),
72 (
73 "wheat",
74 CropData {
75 name: "spelt",
76 mc_max_age: 7,
77 vs_max_age: 9,
78 },
79 ),
80 (
81 "beetroots",
82 CropData {
83 name: "onion",
84 mc_max_age: 3,
85 vs_max_age: 7,
86 },
87 ),
88 ])
89});
90
91static WOOD_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
94 Regex::new(
97 r#"(?x)
98 # matches block codes like "oak_planks" and "jungle_slab"
99 # optional prefix: stripped_
100 ^(?:(?P<prefix>stripped)_)?
101 # tree: pale_oak, jungle, etc
102 (?P<tree>(?:pale_|dark_)?oak|spruce|birch|jungle|acacia|mangrove|cherry|bamboo|crimson|warped|(?:flowering_)?azalea)
103 _
104 # block variant: log, planks, etc
105 (?P<variant>log|stem|planks|door|leaves|wood|trapdoor|sign|wall_sign|sapling|pressure_plate)$
106 "#,
107 )
108 .unwrap()
109});
110
111pub(crate) static STAIR_PATTERN: LazyLock<Regex> =
114 LazyLock::new(|| Regex::new(r"^(?:minecraft:)?(?P<material>.+?)_stairs$").unwrap());
115
116static SLAB_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(?P<material>.+?)_slab$").unwrap());
119
120pub(crate) static FENCE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
123 Regex::new(r"^(?:minecraft:)?(?P<material>.+?)_(?<fence>bars|fence|wall)(?P<gate>_gate)?$").unwrap()
124});
125
126pub(crate) static COLOURFUL_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
129 Regex::new(
130 r"^(?x)
131 # colour and separator
132 (?P<colour>
133 white|orange|magenta|light_blue|yellow|lime|pink|gray|light_gray|cyan|purple|blue|brown|green|red|black
134 )_?
135
136 # the block:
137 (?P<block>
138 # - stained glass blocks and panes
139 stained_glass(?:_pane)?
140 |
141 # - terracotta
142 (?:glazed_)?terracotta
143 |
144 # - concrete
145 concrete(?:_powder)?
146 |
147 # - everything else
148 wool|bed|carpet|candle|banner|wall_banner
149 )$",
150 )
151 .unwrap()
152});
153
154pub(crate) static COPPER_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
157 Regex::new(r"^(?:waxed_)?(?P<variant>weathered|exposed|oxidized)?_?(?:cut_)?copper(?:_block)?$").unwrap()
158});
159
160static ORE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
163 Regex::new(
164 r"^(?P<variant>nether|deepslate)?_?(?P<ore>coal|copper|iron|gold|redstone|lapis|diamond|emerald|quartz)_ore$",
165 )
166 .unwrap()
167});
168
169pub(crate) static SIMPLE_CACHE: LazyLock<Mutex<Map<String, String>>> = LazyLock::new(Default::default);
172
173#[allow(clippy::missing_panics_doc, reason = "no need to document mutex panic")]
181#[must_use]
182pub fn convert_block(block: &MinecraftBlockCode, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
183 let name = block.name.strip_prefix("minecraft:").unwrap_or(block.name.as_str());
184 trace!("block: {name}");
185 trace!("properties: {:?}", block.properties);
186
187 let empty_properties = block.properties.is_empty();
188 if empty_properties && let Some(result) = SIMPLE_CACHE.lock().expect("lock should not be poisoned").get(name) {
189 return Some((result.clone(), None));
190 }
191
192 let functions = [
194 convert_common,
195 convert_climbable,
196 convert_foliage,
197 convert_decor,
198 convert_lighting,
199 convert_utility,
200 convert_clutter,
201 convert_farming,
202 convert_crops,
203 convert_nether,
204 convert_ore,
205 convert_wood_block,
206 convert_stone_block,
207 convert_fence,
208 convert_glass,
209 convert_metal,
210 convert_stairs,
211 convert_slab,
212 convert_colourful,
213 convert_moss,
214 convert_miscellaneous,
215 ];
216
217 for function in functions {
218 if let Some(result) = function(name, &block.properties, settings) {
219 if empty_properties && result.1.is_none() {
220 SIMPLE_CACHE.lock().expect("lock should not be poisoned").insert(name.to_string(), result.0.clone());
221 }
222 return Some(result);
223 }
224 }
225
226 warn!("unknown block: {name}");
227 None
228}
229
230fn convert_common(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
232 Some(match name {
233 "chest" | "trapped_chest" | "ender_chest" => convert_chest(properties),
234 "sand" => (String::from("game:sand-sandstone"), None),
235 "red_sand" => (String::from("game:sand-bauxite"), None),
236 "cobblestone" => (String::from("game:cobblestone-granite"), None),
237 "dirt" | "packed_mud" => (String::from("game:soil-low-none"), None),
238 "dirt_path" => (String::from("game:packeddirt"), None),
239 "podzol" | "mud" | "muddy_mangrove_roots" => (String::from("game:dirtygravel-wetverydark-plain"), None),
240 "coarse_dirt" => (String::from("game:soil-verylow-none"), None),
241 "grass_block" => (String::from("game:soil-low-normal"), None),
242 "clay" => (String::from("game:rawclay-blue-none"), None),
243 "gravel" => (String::from("game:gravel-granite"), None),
244 "cobweb" => (String::from("game:spiderweb"), None),
245 "ice" => (String::from("game:lakeice"), None),
246 "packed_ice" => (String::from("game:glacierice"), None),
247 "blue_ice" => (String::from("game:packedglacierice"), None),
248
249 "snow" | "powder_snow" | "snow_block" => {
250 let layer = properties.get_u8("layers").unwrap_or(MAX_SNOW);
252 if layer >= MAX_SNOW {
253 (String::from("game:snowblock"), None)
254 } else {
255 (format!("game:snowlayer-{layer}"), None)
256 }
257 }
258
259 "water" | "lava" => {
260 let level = MAX_FLUID - properties.get_u8("level").unwrap_or_default().min(MAX_FLUID);
261
262 (format!("game:{name}-still-{level}"), None)
263 }
264 _ => return None,
265 })
266}
267
268pub fn convert_wood_block(
270 name: &str,
271 properties: &PropertyMap,
272 settings: &Settings,
273) -> Option<(String, Option<EntityMap>)> {
274 let matches = WOOD_PATTERN.captures(name)?;
275 let prefix = matches.name("prefix").map(|m| m.as_str());
276 let wood = matches.name("tree").map_or("", |m| m.as_str());
277 let variant = matches.name("variant").map_or("", |m| m.as_str());
278
279 let wood = if wood == "bamboo" && variant == "planks" {
280 "bamboo"
282 } else {
283 convert_wood(wood, true)?
284 };
285
286 let variant = match variant {
287 "stem" | "wood" => "log",
288 other => other,
289 };
290
291 let orientation = match properties.get("axis").map(String::as_str) {
292 Some("x") => "we",
293 Some("y") => "ud",
294
295 _ if properties.get("type").is_some_and(|t| t == "double") => "ud",
297
298 _ => "ns",
299 };
300
301 let sign_wood = || settings.mods.contains(&Mod::VanillaVariants).then_some(wood);
302
303 Some(match variant {
304 "log" if prefix == Some("stripped") => (format!("game:debarkedlog-{wood}-{orientation}"), None),
305 "log" => (format!("game:{variant}-placed-{wood}-{orientation}"), None),
306 "planks" if wood == "bamboo" => (format!("game:stackedbamboo-{orientation}"), None),
307 "planks" => (format!("game:{variant}-{wood}-{orientation}"), None),
308 "door" => convert_door(wood, properties),
309 "trapdoor" => convert_trapdoor(wood, properties),
310 "sign" => convert_wooden_sign(false, sign_wood(), properties),
311 "wall_sign" => convert_wooden_sign(true, sign_wood(), properties),
312
313 "leaves" if wood == "purpleheart" => (String::from("game:leaves-placed-crimsonkingmaple"), None),
315 "leaves" => (format!("game:{variant}-placed-{wood}"), None),
316
317 "sapling" => (format!("game:{variant}-{wood}-free"), None),
318 "pressure_plate" => (String::from("game:metalsheet-cupronickel-free"), None),
319 _ => return None,
321 })
322}
323
324fn convert_trapdoor(material: &str, properties: &PropertyMap) -> (String, Option<EntityMap>) {
326 let rotation = if properties.get("half").is_some_and(|o| o == "bottom") {
327 180
328 } else {
329 -180
330 };
331 let face = match properties.get_facing_or_north() {
332 "north" => 0,
333 "east" => 1,
334 "south" => 2,
335 _ => 3,
336 };
337
338 let new_properties = Map::from([
339 (String::from("opened"), Value::Boolean(properties.is_true("open"))),
340 (String::from("rotDeg"), Value::Integer(rotation)),
341 (String::from("attachedFace"), Value::Integer(face)),
342 ]);
343
344 let code = if material == "iron" {
345 String::from("game:trapdoor-bars-iron-1")
346 } else {
347 let which = if matches!(material, "walnut" | "baldcypress" | "ebony") {
349 "solid"
350 } else {
351 "window"
352 };
353 format!("game:trapdoor-{which}-{material}-1")
354 };
355
356 (code, Some(new_properties))
357}
358
359fn convert_door(material: &str, properties: &PropertyMap) -> (String, Option<EntityMap>) {
361 let rotation = orientation_to_radians(properties.get_facing_or_north());
363
364 let new_properties = Map::from([
365 (String::from("opened"), Value::Boolean(properties.is_true("open"))),
366 (String::from("invertHandles"), Value::Boolean(properties.get("hinge").is_some_and(|o| o == "right"))),
367 (String::from("rotateYRad"), Value::Float(rotation)),
368 ]);
369
370 let code = if material == "iron" {
371 String::from("game:metaldoor-sleek-windowed-iron")
372 } else {
373 format!("game:door-sleek-windowed-{material}")
374 };
375
376 (code, Some(new_properties))
377}
378
379fn convert_wooden_sign(wall: bool, wood: Option<&str>, properties: &PropertyMap) -> (String, Option<EntityMap>) {
381 let facing = reverse_orientation(properties.get_facing_or_north());
383
384 let mut new_properties = Map::from([(String::from("unique!"), Value::Boolean(true))]);
387
388 if !wall {
390 let rotation = properties.get_u8("rotation").unwrap_or_default();
391 let rotation = stepped_rotation(rotation);
392 new_properties.insert(String::from("meshAngle"), Value::Float(rotation));
393 }
394
395 let location = if wall { "wall" } else { "ground" };
396 let code = wood.filter(|&w| w != "oak").map_or_else(
397 || format!("game:sign-{location}-{facing}"),
398 |wood| format!("vanvar:sign-{wood}-{location}-{facing}"),
399 );
400
401 (code, Some(new_properties))
402}
403
404#[must_use]
406pub fn convert_stone_block(
407 name: &str,
408 _properties: &PropertyMap,
409 settings: &Settings,
410) -> Option<(String, Option<EntityMap>)> {
411 if name == "obsidian" || name == "coal_block" {
412 return Some((String::from("game:rock-obsidian"), None));
413 } else if name == "crying_obsidian" {
414 return Some((String::from("game:crackedrock-obsidian"), None));
415 } else if name == "purpur_block" {
416 return if settings.mods.contains(&Mod::Bricklayers) {
417 Some((String::from("bricklayers:glazedbricks-milky-purple"), None))
418 } else {
419 Some((String::from("game:planks-purpleheart-ud"), None))
420 };
421 }
422
423 let (material, variant) = material::convert_stone(name, false)?;
424
425 let (material, variant) = match (material, variant) {
426 ("stonebrick", Some("chalk")) => ("quartz", None),
427 ("stonebrick", _) => ("stonebricks", variant),
428 ("clayshingle", _) => ("clayshingleblock", variant),
429 ("brick", _) => {
430 let (count, colour) = variant.unwrap_or("four-red").split_once('-').unwrap_or(("four", "red"));
432 return Some((format!("game:brickcourse-{count}-running-{colour}"), None));
433 }
434 _ => (material, variant),
435 };
436
437 let material = format!("game:{material}");
438 Some((variant.map_or_else(|| material.clone(), |variant| format!("{material}-{variant}")), None))
439}
440
441pub fn convert_stairs(
447 name: &str,
448 properties: &PropertyMap,
449 _settings: &Settings,
450) -> Option<(String, Option<EntityMap>)> {
451 if !name.ends_with("stairs") {
452 return None;
453 }
454
455 let matches = STAIR_PATTERN.captures(name)?;
456 let material = matches.name("material").map_or("", |m| m.as_str());
457
458 let material: Cow<str> = if let Some(tree) = convert_wood(material, false)
459 && !material.starts_with("bamboo")
460 {
461 format!("plankstairs-{tree}").into()
462 } else if material.ends_with("cut_copper") {
463 match material.trim_end_matches("cut_copper").trim_start_matches("waxed_") {
466 "" | "exposed_" => "stonebrickstairs-bauxite",
467 _ => "stonebrickstairs-peridotite",
468 }
469 .into()
470 } else {
471 match material::convert_wood_or_stone(material) {
472 Some((material, Some(variant))) => format!("{material}stairs-{variant}"),
473 Some((stone, None)) => format!("{stone}stairs"),
474 None => return None,
475 }
476 .into()
477 };
478
479 let facing = properties.get_facing_or_north();
480 let half = match properties.get("half").map_or("bottom", String::as_str) {
481 "bottom" => "up",
482 _ => "down",
483 };
484
485 Some((format!("game:{material}-{half}-{facing}-free"), None))
486}
487
488pub fn convert_slab(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
490 if !name.ends_with("slab") {
491 return None;
492 }
493
494 let matches = SLAB_PATTERN.captures(name)?;
495 let material = matches.name("material").map_or("", |m| m.as_str());
496 let slab_type = match properties.get("type").map_or("bottom", String::as_str) {
497 "top" => "up",
498 "bottom" => "down",
499 "double" => {
500 if is_tree(material) {
501 return convert_wood_block(&format!("{material}_planks"), properties, settings);
503 } else if is_stone(material) {
504 return convert_stone_block(material, properties, settings);
506 }
507
508 "double"
510 }
511 other => {
512 warn!("unknown slab type: {other}");
513 return None;
514 }
515 };
516
517 Some((
518 match material {
519 "cut_copper" | "waxed_cut_copper" | "exposed_cut_copper" | "waxed_exposed_cut_copper" => {
521 if slab_type == "double" {
522 String::from("game:stonebricks-bauxite")
523 } else {
524 format!("game:stonebrickslab-bauxite-{slab_type}-free")
525 }
526 }
527 "weathered_cut_copper" | "waxed_weathered_cut_copper" | "oxidized_cut_copper" | "waxed_oxidized_cut_copper" => {
528 match (slab_type, settings.mods.contains(&Mod::Bricklayers)) {
529 ("double", true) => String::from("bricklayers:claytiles-polished-malachite"),
530 ("double", false) => String::from("game:stonebricks-kimberlite"),
531 (_, true) => format!("bricklayers:claytilesslab-polished-malachite-{slab_type}-free"),
532 (_, false) => format!("game:stonebrickslab-kimberlite-{slab_type}-free"),
533 }
534 }
535
536 _ => match material::convert_wood_or_stone(material) {
537 Some((material, Some(variant))) if material == "clayshingle" => {
538 format!("game:{material}labs-{variant}-{slab_type}-free")
541 }
542 Some((material, Some(variant))) if material == "brick" => {
543 format!("game:{material}slabs-{variant}-{slab_type}-free")
545 }
546 Some((material, Some(variant))) => format!("game:{material}slab-{variant}-{slab_type}-free"),
547 Some((material, None)) => format!("game:{material}slab-{slab_type}-free"),
548
549 None => return None,
550 },
551 },
552 None,
553 ))
554}
555
556pub fn convert_fence(
558 name: &str,
559 properties: &PropertyMap,
560 _settings: &Settings,
561) -> Option<(String, Option<EntityMap>)> {
562 let matches = FENCE_PATTERN.captures(name)?;
563 let material = matches.name("material").map_or("", |m| m.as_str());
564 let fence = matches.name("fence").map_or("", |f| f.as_str());
565 let gate = matches.name("gate").is_some();
566
567 let directions = if gate {
568 match properties.get_facing_or_north() {
569 "north" | "south" => String::from("n"),
570 _ => String::from("w"),
571 }
572 } else {
573 let mut directions = String::new();
574
575 for (direction, character) in [("north", 'n'), ("east", 'e'), ("south", 's'), ("west", 'w')] {
576 if properties.get(direction).is_some_and(|d| d == "low" || d == "tall") {
577 directions.push(character);
578 }
579 }
580
581 if directions.is_empty() {
582 String::from("empty")
583 } else {
584 directions
585 }
586 };
587
588 let code = match (material::convert_wood_or_stone(material), is_tree(material)) {
589 (Some((_, Some(variant))), true) if gate => {
591 let opened = if properties.is_true("open") { "opened" } else { "closed" };
592
593 Some(format!("game:woodenfencegate-{variant}-{directions}-{opened}-left-free"))
596 }
597
598 _ if material == "nether_brick" || material == "red_nether_brick" => {
600 Some(format!("game:drystonefence-chert-{directions}-free"))
601 }
602
603 (Some((_, Some(variant))), false) => Some(format!("game:drystonefence-{variant}-{directions}-free")),
605
606 (Some((_, Some(variant))), true) => Some(format!("game:woodenfence-{variant}-{directions}-free")),
608
609 _ if fence == "bars" => Some(format!("game:ironfence-base-{directions}")),
611
612 _ if is_stone(material) => Some(format!("game:drystonefence-whitemarble-{directions}-free")),
614
615 _ => None,
616 }?;
617
618 Some((code, None))
619}
620
621fn convert_colourful(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
623 let matches = COLOURFUL_PATTERN.captures(name)?;
624 let colour = matches.name("colour").map_or("", |c| c.as_str());
625 let block = matches.name("block").map_or("", |v| v.as_str());
626
627 Some(match block {
628 "stained_glass" | "stained_glass_pane" => convert_colourful_glass(block, Some(colour), properties),
629 "carpet" => {
630 if settings.mods.contains(&Mod::WoolAndMore) {
631 let colour = convert_wool_colour(colour);
632
633 (format!("wool:wool-{colour}"), None)
634 } else {
635 let colour = match colour {
636 "black" | "blue" | "brown" | "red" | "purple" => colour,
637 "orange" => "red",
638 "magenta" | "pink" => "purple",
639 "light_blue" | "cyan" | "green" | "lime" | "yellow" => "turqoise", _ => "black",
641 };
642
643 (format!("game:smallcarpet-{colour}"), None)
644 }
645 }
646 "terracotta" | "glazed_terracotta" | "concrete" | "concrete_powder" => {
647 convert_terracotta_or_concrete(&settings.mods, colour)
648 }
649 "candle" => convert_candle(properties),
650 "bed" => convert_bed(properties),
651 "wool" => return convert_colourful_wool(colour, settings),
652 "banner" | "wall_banner" => {
653 let wall = block == "wall_banner";
654 let colour = match colour {
655 "blue" | "green" | "orange" | "purple" | "red" | "yellow" => colour,
657
658 "light_blue" | "cyan" => "blue",
660 "lime" => "green",
661 "pink" | "brown" | "gray" | "black" => "red",
662 "magenta" => "purple",
663 _ => "yellow",
664 };
665
666 let rotation = if wall {
667 orientation_to_radians(reverse_orientation(properties.get_facing_or_north()))
668 } else {
669 stepped_rotation(properties.get_u8("rotation").unwrap_or_default())
670 };
671
672 clutter("game:clutter", &format!("banner-sunsmall-{colour}-segment"), None, rotation)
673 }
674 _ => return None,
675 })
676}
677
678fn convert_terracotta_or_concrete(mods: &Set<Mod>, colour: &str) -> (String, Option<EntityMap>) {
680 if mods.contains(&Mod::MaterialNeeds) {
681 let colour = match colour {
682 "white" => return (String::from("materialneeds:plaster-plain"), None),
683 "light_gray" => "ashen",
684 "gray" => "charcoal",
685 "lime" => "moss",
686 "cyan" => "turquoise",
687 "light_blue" => "azure",
688 "purple" => "violet",
689 "pink" => "salmon",
690 colour => colour,
691 };
692
693 (format!("materialneeds:plaster-{colour}-plain"), None)
694 } else if mods.contains(&Mod::Bricklayers) {
695 let colour = match colour {
696 "blue" => "blurple",
697 "cyan" | "light_blue" => "malachite",
698 "lime" => "palegreen",
699 "magenta" => "purple",
700 "light_gray" => "light",
701 "gray" => "granite",
702 colour => colour,
703 };
704
705 (format!("bricklayers:claytiles-polished-{colour}"), None)
706 } else {
707 let colour = match colour {
708 "white" | "yellow" => "white",
709 "green" | "lime" => "green",
710 _ => "red",
711 };
712
713 (format!("game:rock-{colour}marble"), None)
714 }
715}
716
717fn convert_glass(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
722 if matches!(name, "glass" | "glass_pane") {
723 Some(convert_colourful_glass(name, None, properties))
724 } else if name == "tinted_glass" {
725 Some(convert_colourful_glass("glass", Some("black"), properties))
726 } else {
727 None
728 }
729}
730
731fn convert_colourful_glass(name: &str, colour: Option<&str>, properties: &PropertyMap) -> (String, Option<EntityMap>) {
735 (
736 if name.ends_with("pane") {
737 if properties.is_true("north") && properties.is_true("south") {
739 String::from("game:glasspane-leaded-pine-ew")
743 } else {
744 String::from("game:glasspane-leaded-pine-ns")
745 }
746 } else {
747 match colour {
749 None => String::from("game:glass-plain"),
750 Some("white") => String::from("game:glass-quartz"),
751 Some("brown" | "orange") => String::from("game:glass-brown"),
752 Some("red") => String::from("game:glass-red"),
753 Some("yellow") => String::from("game:glass-yellow"),
754 Some("green" | "lime") => String::from("game:glass-green"),
755 Some("cyan" | "light_blue" | "blue") => String::from("game:glass-blue"),
756 Some("purple" | "magenta") => String::from("game:glass-violet"),
757 Some("pink") => String::from("game:glass-pink"),
758 _ => String::from("game:glass-smoky"), }
760 },
761 None,
762 )
763}
764
765fn convert_metal(name: &str, _properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
767 Some(match name {
768 "gold_block" => (String::from("game:metalblock-new-plain-gold"), None),
769 "iron_block" => (String::from("game:metalblock-new-plain-iron"), None),
770
771 _ if let Some(matches) = COPPER_PATTERN.captures(name) => {
772 if matches.name("variant").map(|v| v.as_str()).is_some() {
773 (String::from("game:shingleblock-copper"), None)
774 } else {
775 (String::from("game:metalblock-new-plain-copper"), None)
776 }
777 }
778 _ => return None,
779 })
780}
781
782fn convert_climbable(
784 name: &str,
785 properties: &PropertyMap,
786 _settings: &Settings,
787) -> Option<(String, Option<EntityMap>)> {
788 Some(match name {
789 "ladder" => {
790 let orientation = reverse_orientation(properties.get_facing_or_north());
791 (format!("game:ladder-wood-{orientation}"), None)
792 }
793 "vine" => {
794 let orientation = reverse_orientation(determine_orientation(properties).unwrap_or("north"));
795 (format!("game:wildvine-section-{orientation}"), None)
796 }
797 _ => return None,
798 })
799}
800
801fn convert_farming(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
807 Some(match name {
808 "farmland" => {
809 let moisture = properties.get_u8("moisture").unwrap_or_default();
810
811 let code = if moisture == 7 {
812 "game:farmland-moist-verylow"
813 } else {
814 "game:farmland-dry-verylow"
815 };
816
817 let vs_moisture = if moisture == 0 { 0.0 } else { f32::from(moisture) / 7.0 };
818
819 (String::from(code), Some(Map::from([(String::from("moisture"), Value::Float(vs_moisture))])))
820 }
821
822 "hay_block" => (
823 String::from(match properties.get("axis").map_or("x", String::as_str) {
824 "x" => "game:hay-normal-we",
825 "y" => "game:hay-normal-ud",
826 _ => "game:hay-normal-ns",
827 }),
828 None,
829 ),
830
831 "composter" | "barrel" => (String::from("game:barrel"), None),
832
833 "pumpkin" | "carved_pumpkin" | "jack_o_lantern" => (String::from("game:pumpkin-fruit-4"), None),
834
835 "sugar_cane" => (String::from("game:tallplant-tule-land-normal-free"), None),
836
837 _ => return None,
838 })
839}
840
841#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
843fn convert_crops(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
844 let crop_data = CROPS.get(name)?;
845
846 let vs_name = crop_data.name;
847 let age = properties.get_u8("age").unwrap_or_default() + 1;
848 let vs_age = ((f32::from(age) / f32::from(crop_data.mc_max_age)) * f32::from(crop_data.vs_max_age)).round() as u8;
849
850 Some((format!("game:crop-{vs_name}-{vs_age}"), None))
851}
852
853fn convert_ore(name: &str, _properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
855 if !name.ends_with("ore") {
856 return None;
857 }
858
859 let matches = ORE_PATTERN.captures(name)?;
860 let variant = matches.name("variant").map(|v| v.as_str()).unwrap_or_default();
861 let ore = matches.name("ore").map(|m| m.as_str()).unwrap_or_default();
862
863 let stone = match variant {
864 "" => "granite",
865 "deepslate" => "basalt",
866 "nether" => "chert",
867 _ => return None,
868 };
869
870 let block = match ore {
871 "coal" => {
872 let stone = match stone {
873 "granite" => "limestone",
874 "basalt" => "shale",
875 _ => stone,
876 };
877
878 format!("ore-lignite-{stone}")
879 }
880 "copper" => format!("ore-medium-nativecopper-{stone}"),
881 "iron" => {
882 let ore = if stone == "granite" { "hematite" } else { "limonite" };
883 format!("ore-medium-{ore}-{stone}")
884 }
885 "gold" => format!("ore-medium-quartz_nativegold-{stone}"),
886 "redstone" => {
887 let stone = if stone == "chert" { "granite" } else { stone };
888 format!("ore-cinnabar-{stone}")
889 }
890 "lapis" => {
891 let stone = match stone {
892 "granite" => "limestone",
893 "chert" => "bauxite",
894 "basalt" => "redmarble",
895 _ => stone,
896 };
897 format!("ore-lapislazuli-{stone}")
898 }
899 "diamond" => String::from("ore-medium-diamond-kimberlite"),
900 "emerald" => {
901 let stone = match stone {
902 "granite" => "phyllite",
903 "chert" => "slate",
904 _ => stone,
905 };
906 format!("ore-medium-emerald-{stone}")
907 }
908 "quartz" => format!("ore-quartz-{stone}"),
909 _ => return None,
910 };
911
912 Some((block, None))
913}
914
915fn convert_lighting(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
917 Some(match name {
918 "torch" | "copper_torch" | "redstone_torch" | "soul_torch" => {
919 let new_properties = Map::from([
921 (String::from("transitionHoursLeft"), Value::Double(f64::MAX)),
922 (String::from("lastCheckAtTotalDays"), Value::Double(f64::MAX)),
923 ]);
924
925 let lit = properties.get("lit").is_none_or(|l| l == "true");
926 let code = if lit {
927 "game:torch-basic-lit-up"
928 } else {
929 "game:torch-basic-extinct-up"
930 };
931
932 (String::from(code), Some(new_properties))
933 }
934
935 "wall_torch" | "copper_wall_torch" | "redstone_wall_torch" | "soul_wall_torch" => {
936 let facing = reverse_orientation(properties.get_facing_or_north());
938
939 (format!("game:torchholder-aged-filled-{facing}"), None)
940 }
941
942 "lantern" => {
943 let hanging = properties.is_true("hanging");
944 lantern("brass", None, hanging)
945 }
946
947 "soul_lantern" => {
948 let hanging = properties.is_true("hanging");
949 lantern("steel", None, hanging)
950 }
951
952 "sea_lantern" | "redstone_lamp" => (String::from("game:paperlantern-on"), None),
953
954 "campfire" => (String::from("game:firepit-extinct"), None),
955
956 _ if name
957 .rsplit_once('_')
958 .is_some_and(|(copper, lantern)| COPPER_PATTERN.is_match(copper) && lantern == "lantern") =>
959 {
960 let hanging = properties.is_true("hanging");
961 lantern("copper", None, hanging)
962 }
963
964 "candle" => convert_candle(properties),
965
966 _ => return None,
967 })
968}
969
970fn convert_candle(properties: &PropertyMap) -> (String, Option<EntityMap>) {
971 let count = properties.get_u8("candles").unwrap_or(1).min(9);
972
973 (format!("game:bunchocandles-{count}"), None)
974}
975
976fn convert_foliage(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
978 Some(match name {
979 "short_grass" | "short_dry_grass" | "seagrass" | "grass" => (String::from("game:tallgrass-medium-free"), None),
981 "tall_grass" | "tall_dry_grass" | "tall_seagrass" => (String::from("game:tallgrass-verytall-free"), None),
982
983 "bush" | "azalea" | "flowering_azalea" | "dead_bush" | "fern" | "large_fern" | "firefly_bush" => {
984 (String::from("game:leavesbranchy-placed-birch"), None)
985 }
986 "cactus" => (String::from("game:saguarocactus-segment"), None),
987 "sweet_berry_bush" => (String::from("game:smallberrybush-cranberry-ripe"), None),
988 "bamboo" => {
989 let code = match properties.get("leaves").map_or("none", String::as_str) {
990 "large" => String::from("game:bamboo-placed-green-segment3"),
991 "small" => String::from("game:bamboo-placed-green-segment2"),
992 _ => String::from("game:bamboo-placed-green-segment1"),
993 };
994 (code, None)
995 }
996 "bamboo_sapling" => (String::from("game:sapling-greenbamboo-free"), None),
997
998 "lily_pad" => (String::from("game:aquatic-lilygiant-small"), None),
1000 "kelp" | "kelp_plant" => (String::from("game:aquatic-kelp-section"), None),
1001 "dried_kelp_block" => (String::from("game:stonebricks-peridotite"), None),
1002
1003 "tube_coral" | "tube_coral_fan" | "tube_coral_wall_fan" => (String::from("game:coral-shrub-blue-1"), None),
1005
1006 "brain_coral"
1007 | "brain_coral_fan"
1008 | "brain_coral_wall_fan"
1009 | "bubble_coral"
1010 | "bubble_coral_fan"
1011 | "bubble_coral_wall_fan" => (String::from("game:coral-shrub-purple-1"), None),
1012
1013 "fire_coral" | "fire_coral_fan" | "fire_coral_wall_fan" => (String::from("game:coral-shrub-red-1"), None),
1014
1015 "horn_coral" | "horn_coral_fan" | "horn_coral_wall_fan" => (String::from("game:coral-shrub-yellow-1"), None),
1016
1017 "dead_tube_coral"
1019 | "dead_tube_coral_fan"
1020 | "dead_tube_coral_wall_fan"
1021 | "dead_brain_coral"
1022 | "dead_brain_coral_fan"
1023 | "dead_brain_coral_wall_fan"
1024 | "dead_bubble_coral"
1025 | "dead_bubble_coral_fan"
1026 | "dead_bubble_coral_wall_fan"
1027 | "dead_fire_coral"
1028 | "dead_fire_coral_fan"
1029 | "dead_fire_coral_wall_fan"
1030 | "dead_horn_coral"
1031 | "dead_horn_coral_fan"
1032 | "dead_horn_coral_wall_fan" => (String::from("game:coral-shrub-dead-1"), None),
1033
1034 _ => {
1036 if let Some(flower) = convert_flower(name) {
1037 (String::from(flower), None)
1039 } else {
1040 let flower = name
1041 .split_once('_')
1042 .and_then(|(potted, flower)| (potted == "potted").then(|| convert_flower(flower)))
1043 .flatten()?;
1044 (
1049 String::from(VS_FLOWER_POT_CODE),
1050 Some(Map::from([(String::from("flower"), Value::String(String::from(flower)))])),
1051 )
1052 }
1053 }
1054 })
1055}
1056
1057fn convert_flower(name: &str) -> Option<&'static str> {
1058 Some(match name {
1059 "cornflower" | "blue_orchid" => "game:flower-cornflower-free",
1061 "poppy" | "red_tulip" => "game:flower-lupine-red-free",
1062 "orange_tulip" | "dandelion" => "game:flower-lupine-orange-free",
1063 "white_tulip" => "game:flower-lupine-white-free",
1064 "pink_tulip" | "lilac" | "allium" | "peony" => "game:flower-lupine-purple-free",
1065 "oxeye_daisy" | "azure_bluet" => "game:flower-wilddaisy-free",
1066 "rose_bush" => "game:flower-redtopgrass-free",
1067 "lily_of_the_valley" => "game:flower-lilyofthevalley-free",
1068 _ => return None,
1069 })
1070}
1071
1072fn convert_decor(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1074 Some(match name {
1075 "bookshelf" | "chiseled_bookshelf" => clutter(
1077 "game:clutteredbookshelf",
1078 "bookshelves/bookshelf-full",
1079 Some("full"),
1080 orientation_to_radians(properties.get_facing_or_north()),
1081 ),
1082
1083 "bed" => convert_bed(properties),
1084
1085 "bell" if properties.get("attachment").is_some_and(|a| a == "ceiling") => {
1086 lantern("gold", properties.get("facing").map(String::as_str), true)
1087 }
1088
1089 "decorated_pot" => (String::from("game:storagevessel-red-fired"), None),
1090 "flower_pot" => (String::from("game:flowerpot-red-fired"), None), _ => return None,
1093 })
1094}
1095
1096fn convert_bed(properties: &PropertyMap) -> (String, Option<EntityMap>) {
1097 let facing = reverse_orientation(properties.get_facing_or_north());
1098 let half = match properties.get("part").map_or("foot", String::as_str) {
1099 "head" => "head",
1100 _ => "feet",
1101 };
1102
1103 (format!("game:bed-wood-{half}-{facing}"), None)
1104}
1105
1106fn convert_utility(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1108 Some(match name {
1109 "furnace" | "blast_furnace" | "smoker" => (String::from("game:forge"), None),
1110 "anvil" | "chipped_anvil" | "damaged_anvil" => {
1111 let angle = orientation_to_radians(properties.get_facing_or_north());
1112 let angle = angle + std::f32::consts::FRAC_PI_2;
1114
1115 let new_properties = Map::from([(String::from("meshAngle"), Value::Float(angle))]);
1116
1117 (String::from("game:anvil-iron"), Some(new_properties))
1118 }
1119 "hopper" => {
1120 if settings.mods.contains(&Mod::VanillaVariants) {
1121 (String::from("vanvar:hopper-iron"), None)
1122 } else {
1123 (String::from("game:hopper"), None)
1124 }
1125 }
1126 "stone_pressure_plate" => (String::from("game:metalsheet-steel-down"), None),
1127 "polished_blackstone_pressure_plate" => (String::from("game:metalsheet-lead-down"), None),
1128 "heavy_weighted_pressure_plate" => (String::from("game:metalsheet-nickel-down"), None),
1129 "light_weighted_pressure_plate" => (String::from("game:metalsheet-gold-down"), None),
1130 "lodestone" | "observer" | "stonecutter" | "dropper" | "dispenser" => {
1131 (String::from("game:diamond-stone-light"), None)
1132 }
1133 "cauldron" | "water_cauldron" => (String::from("game:storagevessel-black-fired"), None),
1134 _ => return None,
1135 })
1136}
1137
1138fn convert_chest(properties: &PropertyMap) -> (String, Option<Map<String, Value>>) {
1143 let (chest, slots) = if properties.get("type").is_some_and(|t| t == "single") {
1148 ("chest", VS_CHEST_SLOTS)
1149 } else {
1150 ("trunk", VS_TRUNK_SLOTS)
1151 };
1152
1153 let facing = reverse_orientation(properties.get_facing_or_north());
1154 let rotation = orientation_to_radians(facing);
1155 let code = format!("game:{chest}-{facing}");
1156
1157 let inventory = Map::from([
1159 (String::from("qslots"), Value::Integer(slots)),
1161 (String::from("slots"), Value::Tree(Map::new())),
1163 ]);
1164
1165 let new_properties = Map::from([
1166 (String::from("inventory"), Value::Tree(inventory)),
1167 (String::from("meshAngle"), Value::Float(rotation)),
1168 (String::from("forBlockCode"), Value::String(code.trim_start_matches("game:").to_owned())),
1170 (String::from("type"), Value::String(String::from("normal-generic"))),
1171 ]);
1172
1173 (code, Some(new_properties))
1174}
1175
1176fn convert_clutter(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1178 Some(match name {
1179 "lectern" => {
1180 let which = if properties.is_true("has_book") {
1181 "bookshelves/lecturn-book-open"
1183 } else {
1184 "bookshelves/lecturn-empty"
1185 };
1186
1187 clutter(
1188 "game:clutter",
1189 which,
1190 None,
1191 orientation_to_radians(properties.get("facing").map_or("north", |f| reverse_orientation(f))),
1192 )
1193 }
1194 _ => return None,
1195 })
1196}
1197
1198fn convert_nether(name: &str, _properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1200 Some(match name {
1201 "netherrack" => (String::from("game:polishedrockold-cobbled-chert"), None),
1202 "soul_sand" => (String::from("game:cobbleskull-conglomerate"), None),
1203 "soul_soil" => (String::from("game:dirtygravel-wet-plain"), None),
1204 "glowstone" => (String::from("game:paperlantern-on"), None),
1205 _ => return None,
1206 })
1207}
1208
1209fn lantern(material: &str, facing: Option<&str>, hanging: bool) -> (String, Option<EntityMap>) {
1211 let rotation = orientation_to_radians(facing.unwrap_or("north"));
1212
1213 let properties = Map::from([
1214 (String::from("meshAngle"), Value::Float(rotation)),
1215 (String::from("glass"), Value::String(String::from("quartz"))),
1216 (String::from("lining"), Value::String(String::from("plain"))),
1217 (String::from("material"), Value::String(material.to_owned())),
1218 ]);
1219
1220 let code = if hanging {
1221 "game:lantern-down"
1222 } else {
1223 "game:lantern-up"
1224 };
1225
1226 (String::from(code), Some(properties))
1227}
1228
1229fn clutter(code: &str, clutter_code: &str, variant: Option<&str>, angle: f32) -> (String, Option<EntityMap>) {
1231 let mut properties = Map::from([
1232 (String::from("meshAngle"), Value::Float(angle)),
1233 (String::from("type"), Value::String(clutter_code.to_owned())),
1234 (String::from("collected"), Value::Boolean(false)),
1235 (String::from("repairState"), Value::Float(0.0)),
1236 ]);
1237
1238 if let Some(variant) = variant {
1239 properties.insert(String::from("variant"), Value::String(variant.to_owned()));
1240 }
1241
1242 (String::from(code), Some(properties))
1243}
1244
1245fn convert_colourful_wool(colour: &str, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1247 if !settings.mods.contains(&Mod::WoolAndMore) {
1248 return None;
1249 }
1250
1251 let colour = convert_wool_colour(colour);
1252 Some((format!("wool:clothblock-wool-{colour}"), None))
1253}
1254
1255fn convert_wool_colour(colour: &str) -> &str {
1257 match colour {
1258 "white" => "plain",
1259 "cyan" | "light_blue" => "blue",
1260 "blue" => "darkblue",
1261 "lime" => "green",
1262 "green" => "darkgreen",
1263 "magenta" => "purple",
1264 "light_gray" => "gray",
1265 _ => colour,
1266 }
1267}
1268
1269fn convert_moss(name: &str, _properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1271 Some(match name {
1272 "moss_block" => (String::from("game:daub-moss-normal"), None),
1273 "pale_moss_block" => (String::from("game:soil-verylow-none"), None),
1274 "moss_carpet" => {
1275 if settings.mods.contains(&Mod::WoolAndMore) {
1276 (String::from("wool:wool-green"), None)
1277 } else {
1278 (String::from("game:smallcarpet-turqoise"), None) }
1280 }
1281 "pale_moss_carpet" => {
1282 if settings.mods.contains(&Mod::WoolAndMore) {
1283 (String::from("wool:wool-gray"), None)
1284 } else {
1285 (String::from("game:smallcarpet-purple"), None)
1286 }
1287 }
1288 _ => return None,
1289 })
1290}
1291
1292fn convert_miscellaneous(
1293 name: &str,
1294 properties: &PropertyMap,
1295 settings: &Settings,
1296) -> Option<(String, Option<EntityMap>)> {
1297 Some(match name {
1298 "red_mushroom_block" => (String::from("game:cobblestone-redmarble"), None),
1300 "mushroom_stem" => (String::from("game:log-placed-oak-ud"), None),
1301
1302 "terracotta" => (String::from("game:rock-redmarble"), None),
1304 "quartz_pillar" | "quartz_block" | "chiseled_quartz_block" | "bone_block" => {
1305 (String::from("game:stonebricks-chalk"), None)
1306 }
1307 "wool" if settings.mods.contains(&Mod::WoolAndMore) => (String::from("wool:clothblock-wool-plain"), None),
1308
1309 "iron_door" => convert_door("iron", properties),
1310 "iron_trapdoor" => convert_trapdoor("iron", properties),
1311
1312 "sponge" | "wet_sponge" => {
1313 if settings.mods.contains(&Mod::WoolAndMore) {
1314 (String::from("wool:clothblock-wool-yellow"), None)
1315 } else {
1316 (String::from("game:strawbedding"), None)
1317 }
1318 }
1319
1320 "slime_block" => (String::from("game:glass-green"), None),
1321 "raw_iron_block" => (String::from("game:cobblestone-claystone"), None),
1322
1323 _ => return None,
1324 })
1325}
1326
1327fn stepped_rotation(rotation: u8) -> f32 { (std::f32::consts::PI / 8.0) * f32::from(16 - rotation) }