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
169#[allow(clippy::missing_panics_doc, reason = "no need to document mutex panic")]
177#[must_use]
178pub fn convert_block(block: &MinecraftBlockCode, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
179 static SIMPLE_CACHE: LazyLock<Mutex<Map<String, String>>> = LazyLock::new(Default::default);
181
182 let name = block.name.trim_start_matches("minecraft:");
183 trace!("block: {name}");
184 trace!("properties: {:?}", block.properties);
185
186 let empty_properties = block.properties.is_empty();
187 if empty_properties && let Some(result) = SIMPLE_CACHE.lock().expect("lock should not be poisoned").get(name) {
188 return Some((result.clone(), None));
189 }
190
191 let functions = [
193 convert_common,
194 convert_climbable,
195 convert_foliage,
196 convert_decor,
197 convert_lighting,
198 convert_utility,
199 convert_clutter,
200 convert_farming,
201 convert_crops,
202 convert_nether,
203 convert_ore,
204 convert_wood_block,
205 convert_stone_block,
206 convert_fence,
207 convert_glass,
208 convert_metal,
209 convert_stairs,
210 convert_slab,
211 convert_colourful,
212 convert_moss,
213 convert_miscellaneous,
214 ];
215
216 for function in functions {
217 if let Some(result) = function(name, &block.properties, settings) {
218 if empty_properties && result.1.is_none() {
219 SIMPLE_CACHE.lock().expect("lock should not be poisoned").insert(name.to_string(), result.0.clone());
220 }
221 return Some(result);
222 }
223 }
224
225 warn!("unknown block: {name}");
226 None
227}
228
229fn convert_common(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
231 Some(match name {
232 "chest" | "trapped_chest" | "ender_chest" => convert_chest(properties),
233 "sand" => (String::from("game:sand-sandstone"), None),
234 "red_sand" => (String::from("game:sand-bauxite"), None),
235 "cobblestone" => (String::from("game:cobblestone-granite"), None),
236 "dirt" | "packed_mud" => (String::from("game:soil-low-none"), None),
237 "dirt_path" => (String::from("game:packeddirt"), None),
238 "podzol" | "mud" | "muddy_mangrove_roots" => (String::from("game:dirtygravel-wetverydark-plain"), None),
239 "coarse_dirt" => (String::from("game:soil-verylow-none"), None),
240 "grass_block" => (String::from("game:soil-low-normal"), None),
241 "clay" => (String::from("game:rawclay-blue-none"), None),
242 "gravel" => (String::from("game:gravel-granite"), None),
243 "cobweb" => (String::from("game:spiderweb"), None),
244 "ice" => (String::from("game:lakeice"), None),
245 "packed_ice" => (String::from("game:glacierice"), None),
246 "blue_ice" => (String::from("game:packedglacierice"), None),
247
248 "snow" | "powder_snow" | "snow_block" => {
249 let layer = properties.get_u8("layers").unwrap_or(MAX_SNOW);
251 if layer >= MAX_SNOW {
252 (String::from("game:snowblock"), None)
253 } else {
254 (format!("game:snowlayer-{layer}"), None)
255 }
256 }
257
258 "water" | "lava" => {
259 let level = MAX_FLUID - properties.get_u8("level").unwrap_or_default().min(MAX_FLUID);
260
261 (format!("game:{name}-still-{level}"), None)
262 }
263 _ => return None,
264 })
265}
266
267pub fn convert_wood_block(
269 name: &str,
270 properties: &PropertyMap,
271 settings: &Settings,
272) -> Option<(String, Option<EntityMap>)> {
273 let matches = WOOD_PATTERN.captures(name)?;
274 let prefix = matches.name("prefix").map(|m| m.as_str());
275 let wood = matches.name("tree").map_or("", |m| m.as_str());
276 let variant = matches.name("variant").map_or("", |m| m.as_str());
277
278 let wood = if wood == "bamboo" && variant == "planks" {
279 "bamboo"
281 } else {
282 convert_wood(wood, true)?
283 };
284
285 let variant = match variant {
286 "stem" | "wood" => "log",
287 other => other,
288 };
289
290 let orientation = match properties.get("axis").map(String::as_str) {
291 Some("x") => "we",
292 Some("y") => "ud",
293
294 _ if properties.get("type").is_some_and(|t| t == "double") => "ud",
296
297 _ => "ns",
298 };
299
300 let sign_wood = if settings.mods.contains(&Mod::VanillaVariants) {
301 Some(wood)
302 } else {
303 None
304 };
305
306 Some(match variant {
307 "log" if prefix == Some("stripped") => (format!("game:debarkedlog-{wood}-{orientation}"), None),
308 "log" => (format!("game:{variant}-placed-{wood}-{orientation}"), None),
309 "planks" if wood == "bamboo" => (format!("game:stackedbamboo-{orientation}"), None),
310 "planks" => (format!("game:{variant}-{wood}-{orientation}"), None),
311 "door" => convert_door(wood, properties),
312 "trapdoor" => convert_trapdoor(wood, properties),
313 "sign" => convert_wooden_sign(false, sign_wood, properties),
314 "wall_sign" => convert_wooden_sign(true, sign_wood, properties),
315
316 "leaves" if wood == "purpleheart" => (String::from("game:leaves-placed-crimsonkingmaple"), None),
318 "leaves" => (format!("game:{variant}-placed-{wood}"), None),
319
320 "sapling" => (format!("game:{variant}-{wood}-free"), None),
321 "pressure_plate" => (String::from("game:metalsheet-cupronickel-free"), None),
322 _ => return None,
324 })
325}
326
327fn convert_trapdoor(material: &str, properties: &PropertyMap) -> (String, Option<EntityMap>) {
329 let rotation = if properties.get("half").is_some_and(|o| o == "bottom") {
330 180
331 } else {
332 -180
333 };
334 let face = match properties.get_facing_or_north() {
335 "north" => 0,
336 "east" => 1,
337 "south" => 2,
338 _ => 3,
339 };
340
341 let new_properties = Map::from([
342 (String::from("opened"), Value::Boolean(properties.is_true("open"))),
343 (String::from("rotDeg"), Value::Integer(rotation)),
344 (String::from("attachedFace"), Value::Integer(face)),
345 ]);
346
347 let code = if material == "iron" {
348 String::from("game:trapdoor-bars-iron-1")
349 } else {
350 let which = if matches!(material, "walnut" | "baldcypress" | "ebony") {
352 "solid"
353 } else {
354 "window"
355 };
356 format!("game:trapdoor-{which}-{material}-1")
357 };
358
359 (code, Some(new_properties))
360}
361
362fn convert_door(material: &str, properties: &PropertyMap) -> (String, Option<EntityMap>) {
364 let rotation = orientation_to_radians(properties.get_facing_or_north());
366
367 let new_properties = Map::from([
368 (String::from("opened"), Value::Boolean(properties.is_true("open"))),
369 (String::from("invertHandles"), Value::Boolean(properties.get("hinge").is_some_and(|o| o == "right"))),
370 (String::from("rotateYRad"), Value::Float(rotation)),
371 ]);
372
373 let code = if material == "iron" {
374 String::from("game:metaldoor-sleek-windowed-iron")
375 } else {
376 format!("game:door-sleek-windowed-{material}")
377 };
378
379 (code, Some(new_properties))
380}
381
382fn convert_wooden_sign(wall: bool, wood: Option<&str>, properties: &PropertyMap) -> (String, Option<EntityMap>) {
384 let facing = reverse_orientation(properties.get_facing_or_north());
386
387 let mut new_properties = Map::from([(String::from("unique!"), Value::Boolean(true))]);
390
391 if !wall {
393 let rotation = properties.get_u8("rotation").unwrap_or_default();
394 let rotation = stepped_rotation(rotation);
395 new_properties.insert(String::from("meshAngle"), Value::Float(rotation));
396 }
397
398 let location = if wall { "wall" } else { "ground" };
399 let code = wood.filter(|&w| w != "oak").map_or_else(
400 || format!("game:sign-{location}-{facing}"),
401 |wood| format!("vanvar:sign-{wood}-{location}-{facing}"),
402 );
403
404 (code, Some(new_properties))
405}
406
407#[must_use]
409pub fn convert_stone_block(
410 name: &str,
411 _properties: &PropertyMap,
412 settings: &Settings,
413) -> Option<(String, Option<EntityMap>)> {
414 if name == "obsidian" || name == "coal_block" {
415 return Some((String::from("game:rock-obsidian"), None));
416 } else if name == "crying_obsidian" {
417 return Some((String::from("game:crackedrock-obsidian"), None));
418 } else if name == "purpur_block" {
419 return if settings.mods.contains(&Mod::Bricklayers) {
420 Some((String::from("bricklayers:glazedbricks-milky-purple"), None))
421 } else {
422 Some((String::from("game:planks-purpleheart-ud"), None))
423 };
424 }
425
426 let (material, variant) = material::convert_stone(name, false)?;
427
428 let (material, variant) = match (material, variant) {
429 ("stonebrick", Some("chalk")) => ("quartz", None),
430 ("stonebrick", _) => ("stonebricks", variant),
431 ("clayshingle", _) => ("clayshingleblock", variant),
432 ("brick", _) => {
433 let (count, colour) = variant.unwrap_or("four-red").split_once('-').unwrap_or(("four", "red"));
435 return Some((format!("game:brickcourse-{count}-running-{colour}"), None));
436 }
437 _ => (material, variant),
438 };
439
440 let material = format!("game:{material}");
441 Some((variant.map_or_else(|| material.clone(), |variant| format!("{material}-{variant}")), None))
442}
443
444pub fn convert_stairs(
450 name: &str,
451 properties: &PropertyMap,
452 _settings: &Settings,
453) -> Option<(String, Option<EntityMap>)> {
454 let matches = STAIR_PATTERN.captures(name)?;
455 let material = matches.name("material").map_or("", |m| m.as_str());
456
457 let material: Cow<str> = if let Some(tree) = convert_wood(material, false)
458 && !material.starts_with("bamboo")
459 {
460 format!("plankstairs-{tree}").into()
461 } else if material.ends_with("cut_copper") {
462 match material.trim_end_matches("cut_copper").trim_start_matches("waxed_") {
465 "" | "exposed_" => "stonebrickstairs-bauxite",
466 _ => "stonebrickstairs-peridotite",
467 }
468 .into()
469 } else {
470 match material::convert_wood_or_stone(material) {
471 Some((material, Some(variant))) => format!("{material}stairs-{variant}"),
472 Some((stone, None)) => format!("{stone}stairs"),
473 None => return None,
474 }
475 .into()
476 };
477
478 let facing = properties.get_facing_or_north();
479 let half = match properties.get("half").map_or("bottom", String::as_str) {
480 "bottom" => "up",
481 _ => "down",
482 };
483
484 Some((format!("game:{material}-{half}-{facing}-free"), None))
485}
486
487pub fn convert_slab(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
489 let matches = SLAB_PATTERN.captures(name)?;
490 let material = matches.name("material").map_or("", |m| m.as_str());
491 let slab_type = match properties.get("type").map_or("bottom", String::as_str) {
492 "top" => "up",
493 "bottom" => "down",
494 "double" => {
495 if is_tree(material) {
496 return convert_wood_block(&format!("{material}_planks"), properties, settings);
498 } else if is_stone(material) {
499 return convert_stone_block(material, properties, settings);
501 }
502
503 "double"
505 }
506 other => {
507 warn!("unknown slab type: {other}");
508 return None;
509 }
510 };
511
512 Some((
513 match material {
514 "cut_copper" | "waxed_cut_copper" | "exposed_cut_copper" | "waxed_exposed_cut_copper" => {
516 if slab_type == "double" {
517 String::from("game:stonebricks-bauxite")
518 } else {
519 format!("game:stonebrickslab-bauxite-{slab_type}-free")
520 }
521 }
522 "weathered_cut_copper" | "waxed_weathered_cut_copper" | "oxidized_cut_copper" | "waxed_oxidized_cut_copper" => {
523 match (slab_type, settings.mods.contains(&Mod::Bricklayers)) {
524 ("double", true) => String::from("bricklayers:claytiles-polished-malachite"),
525 ("double", false) => String::from("game:stonebricks-kimberlite"),
526 (_, true) => format!("bricklayers:claytilesslab-polished-malachite-{slab_type}-free"),
527 (_, false) => format!("game:stonebrickslab-kimberlite-{slab_type}-free"),
528 }
529 }
530
531 _ => match material::convert_wood_or_stone(material) {
532 Some((material, Some(variant))) if material == "clayshingle" => {
533 format!("game:{material}labs-{variant}-{slab_type}-free")
536 }
537 Some((material, Some(variant))) if material == "brick" => {
538 format!("game:{material}slabs-{variant}-{slab_type}-free")
540 }
541 Some((material, Some(variant))) => format!("game:{material}slab-{variant}-{slab_type}-free"),
542 Some((material, None)) => format!("game:{material}slab-{slab_type}-free"),
543
544 None => return None,
545 },
546 },
547 None,
548 ))
549}
550
551pub fn convert_fence(
553 name: &str,
554 properties: &PropertyMap,
555 _settings: &Settings,
556) -> Option<(String, Option<EntityMap>)> {
557 let matches = FENCE_PATTERN.captures(name)?;
558 let material = matches.name("material").map_or("", |m| m.as_str());
559 let fence = matches.name("fence").map_or("", |f| f.as_str());
560 let gate = matches.name("gate").is_some();
561
562 let directions = if gate {
563 match properties.get_facing_or_north() {
564 "north" | "south" => String::from("n"),
565 _ => String::from("w"),
566 }
567 } else {
568 let mut directions = String::new();
569
570 for direction in ["north", "east", "south", "west"] {
571 if properties.get(direction).is_some_and(|d| d == "low" || d == "tall") {
572 directions.push(direction.chars().next().unwrap_or_default());
573 }
574 }
575
576 if directions.is_empty() {
577 String::from("empty")
578 } else {
579 directions
580 }
581 };
582
583 let code = match (material::convert_wood_or_stone(material), is_tree(material)) {
584 (Some((_, Some(variant))), true) if gate => {
586 let opened = if properties.is_true("open") { "opened" } else { "closed" };
587
588 Some(format!("game:woodenfencegate-{variant}-{directions}-{opened}-left-free"))
591 }
592
593 _ if material == "nether_brick" || material == "red_nether_brick" => {
595 Some(format!("game:drystonefence-chert-{directions}-free"))
596 }
597
598 (Some((_, Some(variant))), false) => Some(format!("game:drystonefence-{variant}-{directions}-free")),
600
601 (Some((_, Some(variant))), true) => Some(format!("game:woodenfence-{variant}-{directions}-free")),
603
604 _ if fence == "bars" => Some(format!("game:ironfence-base-{directions}")),
606
607 _ if is_stone(material) => Some(format!("game:drystonefence-whitemarble-{directions}-free")),
609
610 _ => None,
611 }?;
612
613 Some((code, None))
614}
615
616fn convert_colourful(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
618 let matches = COLOURFUL_PATTERN.captures(name)?;
619 let colour = matches.name("colour").map_or("", |c| c.as_str());
620 let block = matches.name("block").map_or("", |v| v.as_str());
621
622 Some(match block {
623 "stained_glass" | "stained_glass_pane" => convert_colourful_glass(block, Some(colour), properties),
624 "carpet" => {
625 if settings.mods.contains(&Mod::WoolAndMore) {
626 let colour = convert_wool_colour(colour);
627
628 (format!("wool:wool-{colour}"), None)
629 } else {
630 let colour = match colour {
631 "black" | "blue" | "brown" | "red" | "purple" => colour,
632 "orange" => "red",
633 "magenta" | "pink" => "purple",
634 "light_blue" | "cyan" | "green" | "lime" | "yellow" => "turqoise", _ => "black",
636 };
637
638 (format!("game:smallcarpet-{colour}"), None)
639 }
640 }
641 "terracotta" | "glazed_terracotta" | "concrete" | "concrete_powder" => {
642 convert_terracotta_or_concrete(&settings.mods, colour)
643 }
644 "candle" => convert_candle(properties),
645 "bed" => convert_bed(properties),
646 "wool" => return convert_colourful_wool(colour, settings),
647 "banner" | "wall_banner" => {
648 let wall = block == "wall_banner";
649 let colour = match colour {
650 "blue" | "green" | "orange" | "purple" | "red" | "yellow" => colour,
652
653 "light_blue" | "cyan" => "blue",
655 "lime" => "green",
656 "pink" | "brown" | "gray" | "black" => "red",
657 "magenta" => "purple",
658 _ => "yellow",
659 };
660
661 let rotation = if wall {
662 orientation_to_radians(reverse_orientation(properties.get_facing_or_north()))
663 } else {
664 stepped_rotation(properties.get_u8("rotation").unwrap_or_default())
665 };
666
667 clutter("game:clutter", &format!("banner-sunsmall-{colour}-segment"), None, rotation)
668 }
669 _ => return None,
670 })
671}
672
673fn convert_terracotta_or_concrete(mods: &Set<Mod>, colour: &str) -> (String, Option<EntityMap>) {
675 if mods.contains(&Mod::MaterialNeeds) {
676 let colour = match colour {
677 "white" => return (String::from("materialneeds:plaster-plain"), None),
678 "light_gray" => "ashen",
679 "gray" => "charcoal",
680 "lime" => "moss",
681 "cyan" => "turquoise",
682 "light_blue" => "azure",
683 "purple" => "violet",
684 "pink" => "salmon",
685 colour => colour,
686 };
687
688 (format!("materialneeds:plaster-{colour}-plain"), None)
689 } else if mods.contains(&Mod::Bricklayers) {
690 let colour = match colour {
691 "blue" => "blurple",
692 "cyan" | "light_blue" => "malachite",
693 "lime" => "palegreen",
694 "magenta" => "purple",
695 "light_gray" => "light",
696 "gray" => "granite",
697 colour => colour,
698 };
699
700 (format!("bricklayers:claytiles-polished-{colour}"), None)
701 } else {
702 let colour = match colour {
703 "white" | "yellow" => "white",
704 "green" | "lime" => "green",
705 _ => "red",
706 };
707
708 (format!("game:rock-{colour}marble"), None)
709 }
710}
711
712fn convert_glass(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
717 if matches!(name, "glass" | "glass_pane") {
718 Some(convert_colourful_glass(name, None, properties))
719 } else if name == "tinted_glass" {
720 Some(convert_colourful_glass("glass", Some("black"), properties))
721 } else {
722 None
723 }
724}
725
726fn convert_colourful_glass(name: &str, colour: Option<&str>, properties: &PropertyMap) -> (String, Option<EntityMap>) {
730 (
731 if name.ends_with("pane") {
732 if properties.is_true("north") && properties.is_true("south") {
734 String::from("game:glasspane-leaded-pine-ew")
738 } else {
739 String::from("game:glasspane-leaded-pine-ns")
740 }
741 } else {
742 match colour {
744 None => String::from("game:glass-plain"),
745 Some("white") => String::from("game:glass-quartz"),
746 Some("brown" | "orange") => String::from("game:glass-brown"),
747 Some("red") => String::from("game:glass-red"),
748 Some("yellow") => String::from("game:glass-yellow"),
749 Some("green" | "lime") => String::from("game:glass-green"),
750 Some("cyan" | "light_blue" | "blue") => String::from("game:glass-blue"),
751 Some("purple" | "magenta") => String::from("game:glass-violet"),
752 Some("pink") => String::from("game:glass-pink"),
753 _ => String::from("game:glass-smoky"), }
755 },
756 None,
757 )
758}
759
760fn convert_metal(name: &str, _properties: &PropertyMap, _setting: &Settings) -> Option<(String, Option<EntityMap>)> {
762 Some(match name {
763 "gold_block" => (String::from("game:metalblock-new-plain-gold"), None),
764 "iron_block" => (String::from("game:metalblock-new-plain-iron"), None),
765
766 _ => {
767 if let Some(matches) = COPPER_PATTERN.captures(name) {
770 let variant = matches.name("variant").map(|v| v.as_str());
771 match variant {
772 None => (String::from("game:metalblock-new-plain-copper"), None),
773 Some(_) => (String::from("game:shingleblock-copper"), None),
774 }
775 } else {
776 return None;
777 }
778 }
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 let matches = ORE_PATTERN.captures(name)?;
856 let variant = matches.name("variant").map(|v| v.as_str()).unwrap_or_default();
857 let ore = matches.name("ore").map(|m| m.as_str()).unwrap_or_default();
858
859 let stone = match variant {
860 "" => "granite",
861 "deepslate" => "basalt",
862 "nether" => "chert",
863 _ => return None,
864 };
865
866 let block = match ore {
867 "coal" => {
868 let stone = match stone {
869 "granite" => "limestone",
870 "basalt" => "shale",
871 _ => stone,
872 };
873
874 format!("ore-lignite-{stone}")
875 }
876 "copper" => format!("ore-medium-nativecopper-{stone}"),
877 "iron" => {
878 let ore = if stone == "granite" { "hematite" } else { "limonite" };
879 format!("ore-medium-{ore}-{stone}")
880 }
881 "gold" => format!("ore-medium-quartz_nativegold-{stone}"),
882 "redstone" => {
883 let stone = if stone == "chert" { "granite" } else { stone };
884 format!("ore-cinnabar-{stone}")
885 }
886 "lapis" => {
887 let stone = match stone {
888 "granite" => "limestone",
889 "chert" => "bauxite",
890 "basalt" => "redmarble",
891 _ => stone,
892 };
893 format!("ore-lapislazuli-{stone}")
894 }
895 "diamond" => String::from("ore-medium-diamond-kimberlite"),
896 "emerald" => {
897 let stone = match stone {
898 "granite" => "phyllite",
899 "chert" => "slate",
900 _ => stone,
901 };
902 format!("ore-medium-emerald-{stone}")
903 }
904 "quartz" => format!("ore-quartz-{stone}"),
905 _ => return None,
906 };
907
908 Some((block, None))
909}
910
911fn convert_lighting(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
913 Some(match name {
914 "torch" | "copper_torch" | "redstone_torch" | "soul_torch" => {
915 let new_properties = Map::from([
917 (String::from("transitionHoursLeft"), Value::Double(f64::MAX)),
918 (String::from("lastCheckAtTotalDays"), Value::Double(f64::MAX)),
919 ]);
920
921 let lit = properties.get("lit").is_none_or(|l| l == "true");
922 let code = if lit {
923 "torch-basic-lit-up"
924 } else {
925 "torch-basic-extinct-up"
926 };
927
928 (String::from(code), Some(new_properties))
929 }
930
931 "wall_torch" | "copper_wall_torch" | "redstone_wall_torch" | "soul_wall_torch" => {
932 let facing = reverse_orientation(properties.get_facing_or_north());
934
935 (format!("torchholder-aged-filled-{facing}"), None)
936 }
937
938 "lantern" => {
939 let hanging = properties.is_true("hanging");
940 lantern("brass", None, hanging)
941 }
942
943 "soul_lantern" => {
944 let hanging = properties.is_true("hanging");
945 lantern("steel", None, hanging)
946 }
947
948 "sea_lantern" | "redstone_lamp" => (String::from("game:paperlantern-on"), None),
949
950 "campfire" => (String::from("game:firepit-extinct"), None),
951
952 _ if name
953 .rsplit_once('_')
954 .is_some_and(|(copper, lantern)| COPPER_PATTERN.is_match(copper) && lantern == "lantern") =>
955 {
956 let hanging = properties.is_true("hanging");
957 lantern("copper", None, hanging)
958 }
959
960 "candle" => convert_candle(properties),
961
962 _ => return None,
963 })
964}
965
966fn convert_candle(properties: &PropertyMap) -> (String, Option<EntityMap>) {
967 let count = properties.get_u8("candles").unwrap_or(1).min(9);
968
969 (format!("game:bunchocandles-{count}"), None)
970}
971
972fn convert_foliage(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
974 Some(match name {
975 "short_grass" | "short_dry_grass" | "seagrass" | "grass" => (String::from("game:tallgrass-medium-free"), None),
977 "tall_grass" | "tall_dry_grass" | "tall_seagrass" => (String::from("game:tallgrass-verytall-free"), None),
978
979 "bush" | "azalea" | "flowering_azalea" | "dead_bush" | "fern" | "large_fern" | "firefly_bush" => {
980 (String::from("game:leavesbranchy-placed-birch"), None)
981 }
982 "cactus" => (String::from("game:saguarocactus-segment"), None),
983 "sweet_berry_bush" => (String::from("game:smallberrybush-cranberry-ripe"), None),
984 "bamboo" => {
985 let code = match properties.get("leaves").map_or("none", String::as_str) {
986 "large" => String::from("game:bamboo-placed-green-segment3"),
987 "small" => String::from("game:bamboo-placed-green-segment2"),
988 _ => String::from("game:bamboo-placed-green-segment1"),
989 };
990 (code, None)
991 }
992 "bamboo_sapling" => (String::from("game:sapling-greenbamboo-free"), None),
993
994 "lily_pad" => (String::from("game:aquatic-lilygiant-small"), None),
996 "kelp" | "kelp_plant" => (String::from("game:aquatic-kelp-section"), None),
997 "dried_kelp_block" => (String::from("game:stonebricks-peridotite"), None),
998
999 "tube_coral" | "tube_coral_fan" | "tube_coral_wall_fan" => (String::from("game:coral-shrub-blue-1"), None),
1001
1002 "brain_coral"
1003 | "brain_coral_fan"
1004 | "brain_coral_wall_fan"
1005 | "bubble_coral"
1006 | "bubble_coral_fan"
1007 | "bubble_coral_wall_fan" => (String::from("game:coral-shrub-purple-1"), None),
1008
1009 "fire_coral" | "fire_coral_fan" | "fire_coral_wall_fan" => (String::from("game:coral-shrub-red-1"), None),
1010
1011 "horn_coral" | "horn_coral_fan" | "horn_coral_wall_fan" => (String::from("game:coral-shrub-yellow-1"), None),
1012
1013 "dead_tube_coral"
1015 | "dead_tube_coral_fan"
1016 | "dead_tube_coral_wall_fan"
1017 | "dead_brain_coral"
1018 | "dead_brain_coral_fan"
1019 | "dead_brain_coral_wall_fan"
1020 | "dead_bubble_coral"
1021 | "dead_bubble_coral_fan"
1022 | "dead_bubble_coral_wall_fan"
1023 | "dead_fire_coral"
1024 | "dead_fire_coral_fan"
1025 | "dead_fire_coral_wall_fan"
1026 | "dead_horn_coral"
1027 | "dead_horn_coral_fan"
1028 | "dead_horn_coral_wall_fan" => (String::from("game:coral-shrub-dead-1"), None),
1029
1030 _ => {
1032 if let Some(flower) = convert_flower(name) {
1033 (String::from(flower), None)
1035 } else if let Some(flower) = name
1036 .split_once('_')
1037 .and_then(|(potted, flower)| (potted == "potted").then(|| convert_flower(flower)))
1038 .flatten()
1039 {
1040 (
1045 String::from(VS_FLOWER_POT_CODE),
1046 Some(Map::from([(String::from("flower"), Value::String(String::from(flower)))])),
1047 )
1048 } else {
1049 return None;
1050 }
1051 }
1052 })
1053}
1054
1055fn convert_flower(name: &str) -> Option<&'static str> {
1056 Some(match name {
1057 "cornflower" | "blue_orchid" => "game:flower-cornflower-free",
1059 "poppy" | "red_tulip" => "game:flower-lupine-red-free",
1060 "orange_tulip" | "dandelion" => "game:flower-lupine-orange-free",
1061 "white_tulip" => "game:flower-lupine-white-free",
1062 "pink_tulip" | "lilac" | "allium" | "peony" => "game:flower-lupine-purple-free",
1063 "oxeye_daisy" | "azure_bluet" => "game:flower-wilddaisy-free",
1064 "rose_bush" => "game:flower-redtopgrass-free",
1065 "lily_of_the_valley" => "game:flower-lilyofthevalley-free",
1066 _ => return None,
1067 })
1068}
1069
1070fn convert_decor(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1072 Some(match name {
1073 "bookshelf" | "chiseled_bookshelf" => clutter(
1075 "game:clutteredbookshelf",
1076 "bookshelves/bookshelf-full",
1077 Some("full"),
1078 orientation_to_radians(properties.get_facing_or_north()),
1079 ),
1080
1081 "bed" => convert_bed(properties),
1082
1083 "bell" if properties.get("attachment").is_some_and(|a| a == "ceiling") => {
1084 lantern("gold", properties.get("facing").map(String::as_str), true)
1085 }
1086
1087 "decorated_pot" => (String::from("game:storagevessel-red-fired"), None),
1088 "flower_pot" => (String::from("game:flowerpot-red-fired"), None), _ => return None,
1091 })
1092}
1093
1094fn convert_bed(properties: &PropertyMap) -> (String, Option<EntityMap>) {
1095 let facing = reverse_orientation(properties.get_facing_or_north());
1096 let half = match properties.get("part").map_or("foot", String::as_str) {
1097 "head" => "head",
1098 _ => "feet",
1099 };
1100
1101 (format!("game:bed-wood-{half}-{facing}"), None)
1102}
1103
1104fn convert_utility(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1106 Some(match name {
1107 "furnace" | "blast_furnace" | "smoker" => (String::from("game:forge"), None),
1108 "anvil" | "chipped_anvil" | "damaged_anvil" => {
1109 let angle = orientation_to_radians(properties.get_facing_or_north());
1110 let angle = angle + std::f32::consts::FRAC_PI_2;
1112
1113 let new_properties = Map::from([(String::from("meshAngle"), Value::Float(angle))]);
1114
1115 (String::from("game:anvil-iron"), Some(new_properties))
1116 }
1117 "hopper" => {
1118 if settings.mods.contains(&Mod::VanillaVariants) {
1119 (String::from("vanvar:hopper-iron"), None)
1120 } else {
1121 (String::from("game:hopper"), None)
1122 }
1123 }
1124 "stone_pressure_plate" => (String::from("game:metalsheet-steel-down"), None),
1125 "polished_blackstone_pressure_plate" => (String::from("game:metalsheet-lead-down"), None),
1126 "heavy_weighted_pressure_plate" => (String::from("game:metalsheet-nickel-down"), None),
1127 "light_weighted_pressure_plate" => (String::from("game:metalsheet-gold-down"), None),
1128 "lodestone" | "observer" | "stonecutter" | "dropper" | "dispenser" => {
1129 (String::from("game:diamond-stone-light"), None)
1130 }
1131 "cauldron" | "water_cauldron" => (String::from("game:storagevessel-black-fired"), None),
1132 _ => return None,
1133 })
1134}
1135
1136fn convert_chest(properties: &PropertyMap) -> (String, Option<Map<String, Value>>) {
1141 let (chest, slots) = if properties.get("type").is_some_and(|t| t == "single") {
1146 ("chest", VS_CHEST_SLOTS)
1147 } else {
1148 ("trunk", VS_TRUNK_SLOTS)
1149 };
1150
1151 let facing = reverse_orientation(properties.get_facing_or_north());
1152 let rotation = orientation_to_radians(facing);
1153 let code = format!("game:{chest}-{facing}");
1154
1155 let inventory = Map::from([
1157 (String::from("qslots"), Value::Integer(slots)),
1159 (String::from("slots"), Value::Tree(Map::new())),
1161 ]);
1162
1163 let new_properties = Map::from([
1164 (String::from("inventory"), Value::Tree(inventory)),
1165 (String::from("meshAngle"), Value::Float(rotation)),
1166 (String::from("forBlockCode"), Value::String(code.trim_start_matches("game:").to_owned())),
1168 (String::from("type"), Value::String(String::from("normal-generic"))),
1169 ]);
1170
1171 (code, Some(new_properties))
1172}
1173
1174fn convert_clutter(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1176 Some(match name {
1177 "lectern" => {
1178 let which = if properties.is_true("has_book") {
1179 "bookshelves/lecturn-book-open"
1181 } else {
1182 "bookshelves/lecturn-empty"
1183 };
1184
1185 clutter(
1186 "game:clutter",
1187 which,
1188 None,
1189 orientation_to_radians(properties.get("facing").map_or("north", |f| reverse_orientation(f))),
1190 )
1191 }
1192 _ => return None,
1193 })
1194}
1195
1196fn convert_nether(name: &str, _properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1198 Some(match name {
1199 "netherrack" => (String::from("game:polishedrockold-cobbled-chert"), None),
1200 "soul_sand" => (String::from("game:cobbleskull-conglomerate"), None),
1201 "soul_soil" => (String::from("game:dirtygravel-wet-plain"), None),
1202 "glowstone" => (String::from("game:paperlantern-on"), None),
1203 _ => return None,
1204 })
1205}
1206
1207fn lantern(material: &str, facing: Option<&str>, hanging: bool) -> (String, Option<EntityMap>) {
1209 let rotation = orientation_to_radians(facing.unwrap_or("north"));
1210
1211 let properties = Map::from([
1212 (String::from("meshAngle"), Value::Float(rotation)),
1213 (String::from("glass"), Value::String(String::from("quartz"))),
1214 (String::from("lining"), Value::String(String::from("plain"))),
1215 (String::from("material"), Value::String(material.to_owned())),
1216 ]);
1217
1218 let code = if hanging {
1219 "game:lantern-down"
1220 } else {
1221 "game:lantern-up"
1222 };
1223
1224 (String::from(code), Some(properties))
1225}
1226
1227fn clutter(code: &str, clutter_code: &str, variant: Option<&str>, angle: f32) -> (String, Option<EntityMap>) {
1229 let mut properties = Map::from([
1230 (String::from("meshAngle"), Value::Float(angle)),
1231 (String::from("type"), Value::String(clutter_code.to_owned())),
1232 (String::from("collected"), Value::Boolean(false)),
1233 (String::from("repairState"), Value::Float(0.0)),
1234 ]);
1235
1236 if let Some(variant) = variant {
1237 properties.insert(String::from("variant"), Value::String(variant.to_owned()));
1238 }
1239
1240 (String::from(code), Some(properties))
1241}
1242
1243fn convert_colourful_wool(colour: &str, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1245 if !settings.mods.contains(&Mod::WoolAndMore) {
1246 return None;
1247 }
1248
1249 let colour = convert_wool_colour(colour);
1250 Some((format!("wool:clothblock-wool-{colour}"), None))
1251}
1252
1253fn convert_wool_colour(colour: &str) -> &str {
1255 match colour {
1256 "white" => "plain",
1257 "cyan" | "light_blue" => "blue",
1258 "blue" => "darkblue",
1259 "lime" => "green",
1260 "green" => "darkgreen",
1261 "magenta" => "purple",
1262 "light_gray" => "gray",
1263 _ => colour,
1264 }
1265}
1266
1267fn convert_moss(name: &str, _properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1269 Some(match name {
1270 "moss_block" => (String::from("game:daub-moss-normal"), None),
1271 "pale_moss_block" => (String::from("game:soil-verylow-none"), None),
1272 "moss_carpet" => {
1273 if settings.mods.contains(&Mod::WoolAndMore) {
1274 (String::from("wool:wool-green"), None)
1275 } else {
1276 (String::from("game:smallcarpet-turqoise"), None) }
1278 }
1279 "pale_moss_carpet" => {
1280 if settings.mods.contains(&Mod::WoolAndMore) {
1281 (String::from("wool:wool-gray"), None)
1282 } else {
1283 (String::from("game:smallcarpet-purple"), None)
1284 }
1285 }
1286 _ => return None,
1287 })
1288}
1289
1290fn convert_miscellaneous(
1291 name: &str,
1292 properties: &PropertyMap,
1293 settings: &Settings,
1294) -> Option<(String, Option<EntityMap>)> {
1295 Some(match name {
1296 "red_mushroom_block" => (String::from("game:cobblestone-redmarble"), None),
1298 "mushroom_stem" => (String::from("game:log-placed-oak-ud"), None),
1299
1300 "terracotta" => (String::from("game:rock-redmarble"), None),
1302 "quartz_pillar" | "quartz_block" | "chiseled_quartz_block" | "bone_block" => {
1303 (String::from("game:stonebricks-chalk"), None)
1304 }
1305 "wool" if settings.mods.contains(&Mod::VanillaVariants) => (String::from("wool:clothblock-wool-plain"), None),
1306
1307 "iron_door" => convert_door("iron", properties),
1308 "iron_trapdoor" => convert_trapdoor("iron", properties),
1309
1310 "sponge" | "wet_sponge" => {
1311 if settings.mods.contains(&Mod::WoolAndMore) {
1312 (String::from("wool:clothblock-wool-yellow"), None)
1313 } else {
1314 (String::from("game:strawbedding"), None)
1315 }
1316 }
1317
1318 "slime_block" => (String::from("game:glass-green"), None),
1319 "raw_iron_block" => (String::from("game:cobblestone-claystone"), None),
1320
1321 _ => return None,
1322 })
1323}
1324
1325fn stepped_rotation(rotation: u8) -> f32 { (std::f32::consts::PI / 8.0) * f32::from(16 - rotation) }