1use std::{
4 borrow::Cow,
5 sync::{LazyLock, Mutex},
6};
7
8use tracing::{trace, warn};
9
10use crate::{
11 Map, Regex,
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},
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> =
157 LazyLock::new(|| Regex::new(r"^(waxed_)?(?P<variant>weathered|exposed|oxidized)?_?(cut_)?copper(_block)?$").unwrap());
158
159static ORE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
162 Regex::new(
163 r"^(?P<variant>nether|deepslate)?_?(?P<ore>coal|copper|iron|gold|redstone|lapis|diamond|emerald|quartz)_ore$",
164 )
165 .unwrap()
166});
167
168#[allow(clippy::missing_panics_doc, reason = "no need to document mutex panic")]
176#[must_use]
177pub fn convert_block(block: &MinecraftBlockCode, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
178 static SIMPLE_CACHE: LazyLock<Mutex<Map<String, String>>> = LazyLock::new(Default::default);
180
181 let name = block.name.trim_start_matches("minecraft:");
182 trace!("block: {name}");
183 trace!("properties: {:?}", block.properties);
184
185 let empty_properties = block.properties.is_empty();
186 if empty_properties && let Some(result) = SIMPLE_CACHE.lock().expect("lock should not be poisoned").get(name) {
187 return Some((result.clone(), None));
188 }
189
190 let functions = [
192 convert_common,
193 convert_climbable,
194 convert_foliage,
195 convert_decor,
196 convert_lighting,
197 convert_utility,
198 convert_clutter,
199 convert_farming,
200 convert_crops,
201 convert_nether,
202 convert_ore,
203 convert_wood_block,
204 convert_stone_block,
205 convert_fence,
206 convert_glass,
207 convert_metal,
208 convert_stairs,
209 convert_slab,
210 convert_colourful,
211 convert_moss,
212 ];
213
214 for function in functions {
215 if let Some(result) = function(name, &block.properties, settings) {
216 if empty_properties && result.1.is_none() {
217 SIMPLE_CACHE.lock().expect("lock should not be poisoned").insert(name.to_string(), result.0.clone());
218 }
219 return Some(result);
220 }
221 }
222
223 let (code, properties) = match name {
224 "red_mushroom_block" => (String::from("game:cobblestone-redmarble"), None),
226 "mushroom_stem" => (String::from("game:log-placed-oak-ud"), None),
227
228 "terracotta" => (String::from("game:rock-redmarble"), None),
230 "quartz_pillar" | "quartz_block" | "chiseled_quartz_block" | "bone_block" => {
231 (String::from("game:stonebricks-chalk"), None)
232 }
233 "wool" if settings.mods.contains(&Mod::VanillaVariants) => (String::from("wool:clothblock-wool-plain"), None),
234
235 "iron_door" => convert_door("iron", &block.properties),
236 "iron_trapdoor" => convert_trapdoor("iron", &block.properties),
237
238 "sponge" | "wet_sponge" => {
239 if settings.mods.contains(&Mod::WoolAndMore) {
240 (String::from("wool:clothblock-wool-yellow"), None)
241 } else {
242 (String::from("game:strawbedding"), None)
243 }
244 }
245
246 "slime_block" => (String::from("game:glass-green"), None),
247 "raw_iron_block" => (String::from("game:cobblestone-claystone"), None),
248
249 _ => {
250 warn!("unknown block: {name}");
251 return None;
252 }
253 };
254
255 Some((code, properties))
256}
257
258fn convert_common(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
260 Some(match name {
261 "chest" | "trapped_chest" | "ender_chest" => convert_chest(properties),
262 "sand" => (String::from("game:sand-sandstone"), None),
263 "red_sand" => (String::from("game:sand-bauxite"), None),
264 "cobblestone" => (String::from("game:cobblestone-granite"), None),
265 "dirt" | "packed_mud" => (String::from("game:soil-low-none"), None),
266 "dirt_path" => (String::from("game:packeddirt"), None),
267 "podzol" | "mud" | "muddy_mangrove_roots" => (String::from("game:dirtygravel-wetverydark-plain"), None),
268 "coarse_dirt" => (String::from("game:soil-verylow-none"), None),
269 "grass_block" => (String::from("game:soil-low-normal"), None),
270 "clay" => (String::from("game:rawclay-blue-none"), None),
271 "gravel" => (String::from("game:gravel-granite"), None),
272 "cobweb" => (String::from("game:spiderweb"), None),
273 "ice" => (String::from("game:lakeice"), None),
274 "packed_ice" => (String::from("game:glacierice"), None),
275 "blue_ice" => (String::from("game:packedglacierice"), None),
276
277 "snow" | "powder_snow" | "snow_block" => {
278 let layer = properties.get("layers").map_or(MAX_SNOW, |l| l.parse::<u8>().unwrap_or(MAX_SNOW));
280 if layer >= MAX_SNOW {
281 (String::from("game:snowblock"), None)
282 } else {
283 (format!("game:snowlayer-{layer}"), None)
284 }
285 }
286
287 "water" | "lava" => {
288 let level = MAX_FLUID - properties.get("level").map_or(0, |l| l.parse::<u8>().unwrap_or_default()).min(MAX_FLUID);
289
290 (format!("game:{name}-still-{level}"), None)
291 }
292 _ => return None,
293 })
294}
295
296pub fn convert_wood_block(
298 name: &str,
299 properties: &PropertyMap,
300 settings: &Settings,
301) -> Option<(String, Option<EntityMap>)> {
302 let matches = WOOD_PATTERN.captures(name)?;
303 let prefix = matches.name("prefix").map(|m| m.as_str());
304 let wood = matches.name("tree").map_or("", |m| m.as_str());
305 let variant = matches.name("variant").map_or("", |m| m.as_str());
306
307 let wood = if wood == "bamboo" && variant == "planks" {
308 "bamboo"
310 } else {
311 convert_wood(wood, true)?
312 };
313
314 let variant = match variant {
315 "stem" | "wood" => "log",
316 other => other,
317 };
318
319 let orientation = match properties.get("axis").map(String::as_str) {
320 Some("x") => "we",
321 Some("y") => "ud",
322
323 _ if properties.get("type").is_some_and(|t| t == "double") => "ud",
325
326 _ => "ns",
327 };
328
329 let sign_wood = if settings.mods.contains(&Mod::VanillaVariants) {
330 Some(wood)
331 } else {
332 None
333 };
334
335 Some(match variant {
336 "log" if prefix == Some("stripped") => (format!("game:debarkedlog-{wood}-{orientation}"), None),
337 "log" => (format!("game:{variant}-placed-{wood}-{orientation}"), None),
338 "planks" if wood == "bamboo" => (format!("game:stackedbamboo-{orientation}"), None),
339 "planks" => (format!("game:{variant}-{wood}-{orientation}"), None),
340 "door" => convert_door(wood, properties),
341 "trapdoor" => convert_trapdoor(wood, properties),
342 "sign" => convert_wooden_sign(false, sign_wood, properties),
343 "wall_sign" => convert_wooden_sign(true, sign_wood, properties),
344
345 "leaves" if wood == "purpleheart" => (String::from("game:leaves-placed-crimsonkingmaple"), None),
347 "leaves" => (format!("game:{variant}-placed-{wood}"), None),
348
349 "sapling" => (format!("game:{variant}-{wood}-free"), None),
350 "pressure_plate" => (String::from("game:metalsheet-cupronickel-free"), None),
351 _ => return None,
353 })
354}
355
356fn convert_trapdoor(material: &str, properties: &PropertyMap) -> (String, Option<EntityMap>) {
358 let rotation = if properties.get("half").is_some_and(|o| o == "bottom") {
359 180
360 } else {
361 -180
362 };
363 let face = match properties.get("facing").map_or("north", String::as_str) {
364 "north" => 0,
365 "east" => 1,
366 "south" => 2,
367 _ => 3,
368 };
369
370 let new_properties = Map::from([
371 (String::from("opened"), Value::Boolean(properties.get("open").is_some_and(|o| o == "true"))),
372 (String::from("rotDeg"), Value::Integer(rotation)),
373 (String::from("attachedFace"), Value::Integer(face)),
374 ]);
375
376 let code = if material == "iron" {
377 String::from("game:trapdoor-bars-iron-1")
378 } else {
379 let which = if matches!(material, "walnut" | "baldcypress" | "ebony") {
381 "solid"
382 } else {
383 "window"
384 };
385 format!("game:trapdoor-{which}-{material}-1")
386 };
387
388 (code, Some(new_properties))
389}
390
391fn convert_door(material: &str, properties: &PropertyMap) -> (String, Option<EntityMap>) {
393 let rotation = orientation_to_radians(properties.get("facing").map_or("north", String::as_str));
395
396 let new_properties = Map::from([
397 (String::from("opened"), Value::Boolean(properties.get("open").is_some_and(|o| o == "true"))),
398 (String::from("invertHandles"), Value::Boolean(properties.get("hinge").is_some_and(|o| o == "right"))),
399 (String::from("rotateYRad"), Value::Float(rotation)),
400 ]);
401
402 let code = if material == "iron" {
403 String::from("game:metaldoor-sleek-windowed-iron")
404 } else {
405 format!("game:door-sleek-windowed-{material}")
406 };
407
408 (code, Some(new_properties))
409}
410
411fn convert_wooden_sign(wall: bool, wood: Option<&str>, properties: &PropertyMap) -> (String, Option<EntityMap>) {
413 let facing = reverse_orientation(properties.get("facing").map_or("north", String::as_str));
415
416 let mut new_properties = Map::from([(String::from("unique!"), Value::Boolean(true))]);
419
420 if !wall {
422 let rotation = properties.get("rotation").map_or(0, |r| r.parse::<u8>().unwrap_or_default());
423 let rotation = stepped_rotation(rotation);
424 new_properties.insert(String::from("meshAngle"), Value::Float(rotation));
425 }
426
427 let location = if wall { "wall" } else { "ground" };
428 let code = wood.filter(|&w| w != "oak").map_or_else(
429 || format!("game:sign-{location}-{facing}"),
430 |wood| format!("vanvar:sign-{wood}-{location}-{facing}"),
431 );
432
433 (code, Some(new_properties))
434}
435
436#[must_use]
438pub fn convert_stone_block(
439 name: &str,
440 _properties: &PropertyMap,
441 _settings: &Settings,
442) -> Option<(String, Option<EntityMap>)> {
443 if name == "obsidian" || name == "coal_block" {
444 return Some((String::from("game:rock-obsidian"), None));
445 } else if name == "crying_obsidian" {
446 return Some((String::from("game:crackedrock-obsidian"), None));
447 }
448
449 let (material, variant) = material::convert_stone(name, false)?;
450
451 let (material, variant) = match (material, variant) {
452 ("stonebrick", Some("chalk")) => ("quartz", None),
453 ("stonebrick", _) => ("stonebricks", variant),
454 _ => (material, variant),
455 };
456
457 Some((variant.map_or_else(|| material.to_string(), |variant| format!("{material}-{variant}")), None))
458}
459
460pub fn convert_stairs(
466 name: &str,
467 properties: &PropertyMap,
468 _settings: &Settings,
469) -> Option<(String, Option<EntityMap>)> {
470 let matches = STAIR_PATTERN.captures(name)?;
471 let material = matches.name("material").map_or("", |m| m.as_str());
472
473 let material: Cow<str> = if let Some(tree) = convert_wood(material, false)
474 && !material.starts_with("bamboo")
475 {
476 format!("plankstairs-{tree}").into()
477 } else if material.ends_with("cut_copper") {
478 match material.trim_end_matches("cut_copper").trim_start_matches("waxed_") {
481 "" | "exposed_" => "stonebrickstairs-bauxite",
482 _ => "stonebrickstairs-peridotite",
483 }
484 .into()
485 } else {
486 match material::convert_wood_or_stone(material) {
487 Some((material, Some(variant))) => format!("{material}stairs-{variant}"),
488 Some((stone, None)) => format!("{stone}stairs"),
489 None => return None,
490 }
491 .into()
492 };
493
494 let facing = properties.get("facing").map_or("north", String::as_str);
495 let half = match properties.get("half").map_or("bottom", String::as_str) {
496 "bottom" => "up",
497 _ => "down",
498 };
499
500 Some((format!("game:{material}-{half}-{facing}-free"), None))
501}
502
503pub fn convert_slab(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
505 let matches = SLAB_PATTERN.captures(name)?;
506 let material = matches.name("material").map_or("", |m| m.as_str());
507 let slab_type = match properties.get("type").map_or("bottom", String::as_str) {
508 "top" => "up",
509 "bottom" => "down",
510 "double" => {
511 if is_tree(material) {
512 return convert_wood_block(&format!("{material}_planks"), properties, settings);
514 } else if is_stone(material) {
515 return convert_stone_block(material, properties, settings);
517 }
518
519 "double"
521 }
522 other => {
523 warn!("unknown slab type: {other}");
524 return None;
525 }
526 };
527
528 Some((
529 match material {
530 "cut_copper" | "waxed_cut_copper" | "exposed_cut_copper" | "waxed_exposed_cut_copper" => {
532 if slab_type == "double" {
533 String::from("game:stonebricks-bauxite")
534 } else {
535 format!("game:stonebrickslab-bauxite-{slab_type}-free")
536 }
537 }
538 "weathered_cut_copper" | "waxed_weathered_cut_copper" | "oxidized_cut_copper" | "waxed_oxidized_cut_copper" => {
539 match (slab_type, settings.mods.contains(&Mod::Bricklayers)) {
540 ("double", true) => String::from("bricklayers:claytiles-polished-malachite"),
541 ("double", false) => String::from("game:stonebricks-kimberlite"),
542 (_, true) => format!("bricklayers:claytilesslab-polished-malachite-{slab_type}-free"),
543 (_, false) => format!("game:stonebrickslab-kimberlite-{slab_type}-free"),
544 }
545 }
546
547 _ => match material::convert_wood_or_stone(material) {
548 Some((material, Some(variant))) => format!("game:{material}slab-{variant}-{slab_type}-free"),
549 Some((material, None)) => format!("game:{material}slab-{slab_type}-free"),
550
551 None => return None,
552 },
553 },
554 None,
555 ))
556}
557
558pub fn convert_fence(
560 name: &str,
561 properties: &PropertyMap,
562 _settings: &Settings,
563) -> Option<(String, Option<EntityMap>)> {
564 let matches = FENCE_PATTERN.captures(name)?;
565 let material = matches.name("material").map_or("", |m| m.as_str());
566 let fence = matches.name("fence").map_or("", |f| f.as_str());
567 let gate = matches.name("gate").is_some();
568
569 let directions = if gate {
570 match properties.get("facing").map_or("north", String::as_str) {
571 "north" | "south" => String::from("n"),
572 _ => String::from("w"),
573 }
574 } else {
575 let mut directions = String::new();
576
577 for direction in ["north", "east", "south", "west"] {
578 if properties.get(direction).is_some_and(|d| d == "low" || d == "tall") {
579 directions.push(direction.chars().next().unwrap_or_default());
580 }
581 }
582
583 if directions.is_empty() {
584 String::from("empty")
585 } else {
586 directions
587 }
588 };
589
590 let code = match (material::convert_wood_or_stone(material), is_tree(material)) {
591 (Some((_, Some(variant))), true) if gate => {
593 let opened = if properties.get("open").is_some_and(|o| o == "true") {
594 "opened"
595 } else {
596 "closed"
597 };
598
599 Some(format!("game:woodenfencegate-{variant}-{directions}-{opened}-left-free"))
602 }
603
604 (Some((_, Some(variant))), false) => Some(format!("game:drystonefence-{variant}-{directions}-free")),
606
607 (Some((_, Some(variant))), true) => Some(format!("game:woodenfence-{variant}-{directions}-free")),
609
610 _ if fence == "bars" => Some(format!("game:ironfence-base-{directions}")),
612
613 _ if is_stone(material) => Some(format!("game:drystonefence-whitemarble-{directions}-free")),
615
616 _ => None,
617 }?;
618
619 Some((code, None))
620}
621
622fn convert_colourful(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
624 let matches = COLOURFUL_PATTERN.captures(name)?;
625 let colour = matches.name("colour").map_or("", |c| c.as_str());
626 let block = matches.name("block").map_or("", |v| v.as_str());
627
628 Some(match block {
629 "stained_glass" | "stained_glass_pane" => convert_colourful_glass(block, Some(colour), properties),
630 "carpet" => {
631 if settings.mods.contains(&Mod::WoolAndMore) {
632 let colour = convert_wool_colour(colour);
633
634 (format!("wool:wool-{colour}"), None)
635 } else {
636 let colour = match colour {
637 "black" | "blue" | "brown" | "red" | "purple" => colour,
638 "orange" => "red",
639 "magenta" | "pink" => "purple",
640 "light_blue" | "cyan" | "green" | "lime" | "yellow" => "turqoise", _ => "black",
642 };
643
644 (format!("game:smallcarpet-{colour}"), None)
645 }
646 }
647 "terracotta" | "glazed_terracotta" | "concrete" | "concrete_powder" => {
648 if settings.mods.contains(&Mod::Bricklayers) {
649 let colour = match colour {
650 "blue" => "blurple",
651 "cyan" | "light_blue" => "malachite",
652 "lime" => "palegreen",
653 "magenta" => "purple",
654 "light_gray" => "light",
655 "gray" => "granite",
656 colour => colour,
657 };
658
659 (format!("bricklayers:claytiles-polished-{colour}"), None)
660 } else {
661 let colour = match colour {
662 "white" | "yellow" => "white",
663 "green" | "lime" => "green",
664 _ => "red",
665 };
666
667 (format!("game:rock-{colour}marble"), None)
668 }
669 }
670 "candle" => convert_candle(properties),
671 "bed" => convert_bed(properties),
672 "wool" => return convert_colourful_wool(colour, settings),
673 "banner" | "wall_banner" => {
674 let wall = block == "wall_banner";
675 let colour = match colour {
676 "blue" | "green" | "orange" | "purple" | "red" | "yellow" => colour,
678
679 "light_blue" | "cyan" => "blue",
681 "lime" => "green",
682 "pink" | "brown" | "gray" | "black" => "red",
683 "magenta" => "purple",
684 _ => "yellow",
685 };
686
687 let rotation = if wall {
688 orientation_to_radians(reverse_orientation(properties.get("facing").map_or("north", String::as_str)))
689 } else {
690 stepped_rotation(properties.get("rotation").map_or(0, |r| r.parse::<u8>().unwrap_or_default()))
691 };
692
693 clutter("game:clutter", &format!("banner-sunsmall-{colour}-segment"), None, rotation)
694 }
695 _ => return None,
696 })
697}
698
699fn convert_glass(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
704 if matches!(name, "glass" | "glass_pane") {
705 Some(convert_colourful_glass(name, None, properties))
706 } else if name == "tinted_glass" {
707 Some(convert_colourful_glass("glass", Some("black"), properties))
708 } else {
709 None
710 }
711}
712
713fn convert_colourful_glass(name: &str, colour: Option<&str>, properties: &PropertyMap) -> (String, Option<EntityMap>) {
717 (
718 if name.ends_with("pane") {
719 if properties.get("north").is_some_and(|n| n == "true") && properties.get("south").is_some_and(|e| e == "true") {
721 String::from("game:glasspane-leaded-pine-ew")
725 } else {
726 String::from("game:glasspane-leaded-pine-ns")
727 }
728 } else {
729 match colour {
731 None => String::from("game:glass-plain"),
732 Some("white") => String::from("game:glass-quartz"),
733 Some("brown" | "orange") => String::from("game:glass-brown"),
734 Some("red") => String::from("game:glass-red"),
735 Some("yellow") => String::from("game:glass-yellow"),
736 Some("green" | "lime") => String::from("game:glass-green"),
737 Some("cyan" | "light_blue" | "blue") => String::from("game:glass-blue"),
738 Some("purple" | "magenta") => String::from("game:glass-violet"),
739 Some("pink") => String::from("game:glass-pink"),
740 _ => String::from("game:glass-smoky"), }
742 },
743 None,
744 )
745}
746
747fn convert_metal(name: &str, _properties: &PropertyMap, _setting: &Settings) -> Option<(String, Option<EntityMap>)> {
749 Some(match name {
750 "gold_block" => (String::from("game:metalblock-new-plain-gold"), None),
751 "iron_block" => (String::from("game:metalblock-new-plain-iron"), None),
752
753 _ => {
754 if let Some(matches) = COPPER_PATTERN.captures(name) {
757 let variant = matches.name("variant").map(|v| v.as_str());
758 match variant {
759 None => (String::from("game:metalblock-new-plain-copper"), None),
760 Some(_) => (String::from("game:shingleblock-copper"), None),
761 }
762 } else {
763 return None;
764 }
765 }
766 })
767}
768
769fn convert_climbable(
771 name: &str,
772 properties: &PropertyMap,
773 _settings: &Settings,
774) -> Option<(String, Option<EntityMap>)> {
775 Some(match name {
776 "ladder" => {
777 let orientation = reverse_orientation(properties.get("facing").map_or("north", String::as_str));
778 (format!("game:ladder-wood-{orientation}"), None)
779 }
780 "vine" => {
781 let orientation = reverse_orientation(determine_orientation(properties).unwrap_or("north"));
782 (format!("game:wildvine-section-{orientation}"), None)
783 }
784 _ => return None,
785 })
786}
787
788fn convert_farming(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
794 Some(match name {
795 "farmland" => {
796 let moisture = properties.get("moisture").map(|m| m.parse::<u8>().unwrap_or_default()).unwrap_or_default();
797
798 let code = if moisture == 7 {
799 "game:farmland-moist-verylow"
800 } else {
801 "game:farmland-dry-verylow"
802 };
803
804 let vs_moisture = if moisture == 0 { 0.0 } else { f32::from(moisture) / 7.0 };
805
806 (String::from(code), Some(Map::from([(String::from("moisture"), Value::Float(vs_moisture))])))
807 }
808
809 "hay_block" => (
810 String::from(match properties.get("axis").map_or("x", String::as_str) {
811 "x" => "hay-normal-we",
812 "y" => "hay-normal-ud",
813 _ => "hay-normal-ns",
814 }),
815 None,
816 ),
817
818 "composter" | "barrel" => (String::from("game:barrel"), None),
819
820 "pumpkin" | "carved_pumpkin" | "jack_o_lantern" => (String::from("game:pumpkin-fruit-4"), None),
821
822 "sugar_cane" => (String::from("game:tallplant-tule-land-normal-free"), None),
823
824 _ => return None,
825 })
826}
827
828#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
830fn convert_crops(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
831 let crop_data = CROPS.get(name)?;
832
833 let vs_name = crop_data.name;
834 let age = properties.get("age").map(|a| a.parse::<u8>().unwrap_or_default()).unwrap_or_default() + 1;
835 let vs_age = ((f32::from(age) / f32::from(crop_data.mc_max_age)) * f32::from(crop_data.vs_max_age)).round() as u8;
836
837 Some((format!("crop-{vs_name}-{vs_age}"), None))
838}
839
840fn convert_ore(name: &str, _properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
842 let matches = ORE_PATTERN.captures(name)?;
843 let variant = matches.name("variant").map(|v| v.as_str()).unwrap_or_default();
844 let ore = matches.name("ore").map(|m| m.as_str()).unwrap_or_default();
845
846 let stone = match variant {
847 "" => "granite",
848 "deepslate" => "basalt",
849 "nether" => "chert",
850 _ => return None,
851 };
852
853 let block = match ore {
854 "coal" => {
855 let stone = match stone {
856 "granite" => "limestone",
857 "basalt" => "shale",
858 _ => stone,
859 };
860
861 format!("ore-lignite-{stone}")
862 }
863 "copper" => format!("ore-medium-nativecopper-{stone}"),
864 "iron" => {
865 let ore = if stone == "granite" { "hematite" } else { "limonite" };
866 format!("ore-medium-{ore}-{stone}")
867 }
868 "gold" => format!("ore-medium-quartz_nativegold-{stone}"),
869 "redstone" => {
870 let stone = if stone == "chert" { "granite" } else { stone };
871 format!("ore-cinnabar-{stone}")
872 }
873 "lapis" => {
874 let stone = match stone {
875 "granite" => "limestone",
876 "chert" => "bauxite",
877 "basalt" => "redmarble",
878 _ => stone,
879 };
880 format!("ore-lapislazuli-{stone}")
881 }
882 "diamond" => String::from("ore-medium-diamond-kimberlite"),
883 "emerald" => {
884 let stone = match stone {
885 "granite" => "phyllite",
886 "chert" => "slate",
887 _ => stone,
888 };
889 format!("ore-medium-emerald-{stone}")
890 }
891 "quartz" => format!("ore-quartz-{stone}"),
892 _ => return None,
893 };
894
895 Some((block, None))
896}
897
898fn convert_lighting(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
900 Some(match name {
901 "torch" | "copper_torch" | "redstone_torch" | "soul_torch" => {
902 let new_properties = Map::from([
904 (String::from("transitionHoursLeft"), Value::Double(f64::MAX)),
905 (String::from("lastCheckAtTotalDays"), Value::Double(f64::MAX)),
906 ]);
907
908 let lit = properties.get("lit").is_none_or(|l| l == "true");
909 let code = if lit {
910 "torch-basic-lit-up"
911 } else {
912 "torch-basic-extinct-up"
913 };
914
915 (String::from(code), Some(new_properties))
916 }
917
918 "wall_torch" | "copper_wall_torch" | "redstone_wall_torch" | "soul_wall_torch" => {
919 let facing = reverse_orientation(properties.get("facing").map_or("north", String::as_str));
921
922 (format!("torchholder-aged-filled-{facing}"), None)
923 }
924
925 "lantern" => {
926 let hanging = properties.get("hanging").is_some_and(|h| h == "true");
927 lantern("brass", None, hanging)
928 }
929
930 "soul_lantern" => {
931 let hanging = properties.get("hanging").is_some_and(|h| h == "true");
932 lantern("steel", None, hanging)
933 }
934
935 "sea_lantern" | "redstone_lamp" => (String::from("game:paperlantern-on"), None),
936
937 "campfire" => (String::from("game:firepit-extinct"), None),
938
939 _ if name
940 .rsplit_once('_')
941 .is_some_and(|(copper, lantern)| COPPER_PATTERN.is_match(copper) && lantern == "lantern") =>
942 {
943 let hanging = properties.get("hanging").is_some_and(|h| h == "true");
944 lantern("copper", None, hanging)
945 }
946
947 "candle" => convert_candle(properties),
948
949 _ => return None,
950 })
951}
952
953fn convert_candle(properties: &PropertyMap) -> (String, Option<EntityMap>) {
954 let count = properties.get("candles").map_or(1, |c| c.parse::<u8>().unwrap_or(1)).min(9);
955
956 (format!("bunchocandles-{count}"), None)
957}
958
959fn convert_foliage(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
961 Some(match name {
962 "short_grass" | "short_dry_grass" | "seagrass" | "grass" => (String::from("game:tallgrass-medium-free"), None),
964 "tall_grass" | "tall_dry_grass" | "tall_seagrass" => (String::from("game:tallgrass-verytall-free"), None),
965
966 "bush" | "azalea" | "flowering_azalea" | "dead_bush" | "fern" | "large_fern" | "firefly_bush" => {
967 (String::from("game:leavesbranchy-placed-birch"), None)
968 }
969 "cactus" => (String::from("game:saguarocactus-segment"), None),
970 "sweet_berry_bush" => (String::from("game:smallberrybush-cranberry-ripe"), None),
971 "bamboo" => {
972 let code = match properties.get("leaves").map_or("none", String::as_str) {
973 "large" => String::from("game:bamboo-placed-green-segment3"),
974 "small" => String::from("game:bamboo-placed-green-segment2"),
975 _ => String::from("game:bamboo-placed-green-segment1"),
976 };
977 (code, None)
978 }
979 "bamboo_sapling" => (String::from("game:sapling-greenbamboo-free"), None),
980
981 "lily_pad" => (String::from("game:aquatic-lilygiant-small"), None),
983 "kelp" | "kelp_plant" => (String::from("game:aquatic-kelp-section"), None),
984 "dried_kelp_block" => (String::from("game:stonebricks-peridotite"), None),
985
986 "tube_coral" | "tube_coral_fan" | "tube_coral_wall_fan" => (String::from("game:coral-shrub-blue-1"), None),
988
989 "brain_coral"
990 | "brain_coral_fan"
991 | "brain_coral_wall_fan"
992 | "bubble_coral"
993 | "bubble_coral_fan"
994 | "bubble_coral_wall_fan" => (String::from("game:coral-shrub-purple-1"), None),
995
996 "fire_coral" | "fire_coral_fan" | "fire_coral_wall_fan" => (String::from("game:coral-shrub-red-1"), None),
997
998 "horn_coral" | "horn_coral_fan" | "horn_coral_wall_fan" => (String::from("game:coral-shrub-yellow-1"), None),
999
1000 "dead_tube_coral"
1002 | "dead_tube_coral_fan"
1003 | "dead_tube_coral_wall_fan"
1004 | "dead_brain_coral"
1005 | "dead_brain_coral_fan"
1006 | "dead_brain_coral_wall_fan"
1007 | "dead_bubble_coral"
1008 | "dead_bubble_coral_fan"
1009 | "dead_bubble_coral_wall_fan"
1010 | "dead_fire_coral"
1011 | "dead_fire_coral_fan"
1012 | "dead_fire_coral_wall_fan"
1013 | "dead_horn_coral"
1014 | "dead_horn_coral_fan"
1015 | "dead_horn_coral_wall_fan" => (String::from("game:coral-shrub-dead-1"), None),
1016
1017 _ => {
1019 if let Some(flower) = convert_flower(name) {
1020 (String::from(flower), None)
1022 } else if let Some(flower) = name
1023 .split_once('_')
1024 .and_then(|(potted, flower)| (potted == "potted").then(|| convert_flower(flower)))
1025 .flatten()
1026 {
1027 (
1032 String::from(VS_FLOWER_POT_CODE),
1033 Some(Map::from([(String::from("flower"), Value::String(String::from(flower)))])),
1034 )
1035 } else {
1036 return None;
1037 }
1038 }
1039 })
1040}
1041
1042fn convert_flower(name: &str) -> Option<&'static str> {
1043 Some(match name {
1044 "cornflower" | "blue_orchid" => "game:flower-cornflower-free",
1046 "poppy" | "red_tulip" => "game:flower-lupine-red-free",
1047 "orange_tulip" | "dandelion" => "game:flower-lupine-orange-free",
1048 "white_tulip" => "game:flower-lupine-white-free",
1049 "pink_tulip" | "lilac" | "allium" | "peony" => "game:flower-lupine-purple-free",
1050 "oxeye_daisy" | "azure_bluet" => "game:flower-wilddaisy-free",
1051 "rose_bush" => "game:flower-redtopgrass-free",
1052 "lily_of_the_valley" => "game:flower-lilyofthevalley-free",
1053 _ => return None,
1054 })
1055}
1056
1057fn convert_decor(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1059 Some(match name {
1060 "bookshelf" | "chiseled_bookshelf" => clutter(
1062 "game:clutteredbookshelf",
1063 "bookshelves/bookshelf-full",
1064 Some("full"),
1065 orientation_to_radians(properties.get("facing").map_or("north", String::as_str)),
1066 ),
1067
1068 "bed" => convert_bed(properties),
1069
1070 "bell" if properties.get("attachment").is_some_and(|a| a == "ceiling") => {
1071 lantern("gold", properties.get("facing").map(String::as_str), true)
1072 }
1073
1074 "decorated_pot" => (String::from("game:storagevessel-red-fired"), None),
1075 "flower_pot" => (String::from("game:flowerpot-red-fired"), None), _ => return None,
1078 })
1079}
1080
1081fn convert_bed(properties: &PropertyMap) -> (String, Option<EntityMap>) {
1082 let facing = reverse_orientation(properties.get("facing").map_or("north", String::as_str));
1083 let half = match properties.get("part").map_or("foot", String::as_str) {
1084 "head" => "head",
1085 _ => "feet",
1086 };
1087
1088 (format!("game:bed-wood-{half}-{facing}"), None)
1089}
1090
1091fn convert_utility(name: &str, properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1093 Some(match name {
1094 "furnace" | "blast_furnace" | "smoker" => (String::from("game:forge"), None),
1095 "anvil" | "chipped_anvil" | "damaged_anvil" => {
1096 let angle = orientation_to_radians(properties.get("facing").map_or("north", String::as_str));
1097 let angle = angle + std::f32::consts::FRAC_PI_2;
1099
1100 let new_properties = Map::from([(String::from("meshAngle"), Value::Float(angle))]);
1101
1102 (String::from("game:anvil-iron"), Some(new_properties))
1103 }
1104 "hopper" => {
1105 if settings.mods.contains(&Mod::VanillaVariants) {
1106 (String::from("vanvar:hopper-iron"), None)
1107 } else {
1108 (String::from("game:hopper"), None)
1109 }
1110 }
1111 "stone_pressure_plate" => (String::from("game:metalsheet-steel-down"), None),
1112 "polished_blackstone_pressure_plate" => (String::from("game:metalsheet-lead-down"), None),
1113 "heavy_weighted_pressure_plate" => (String::from("game:metalsheet-nickel-down"), None),
1114 "light_weighted_pressure_plate" => (String::from("game:metalsheet-gold-down"), None),
1115 "lodestone" | "observer" | "stonecutter" | "dropper" | "dispenser" => {
1116 (String::from("game:diamond-stone-light"), None)
1117 }
1118 "cauldron" | "water_cauldron" => (String::from("game:storagevessel-black-fired"), None),
1119 _ => return None,
1120 })
1121}
1122
1123fn convert_chest(properties: &PropertyMap) -> (String, Option<Map<String, Value>>) {
1128 let (chest, slots) = if properties.get("type").is_some_and(|t| t == "single") {
1133 ("chest", VS_CHEST_SLOTS)
1134 } else {
1135 ("trunk", VS_TRUNK_SLOTS)
1136 };
1137
1138 let facing = reverse_orientation(properties.get("facing").map_or("north", String::as_str));
1139 let rotation = orientation_to_radians(facing);
1140 let code = format!("game:{chest}-{facing}");
1141
1142 let inventory = Map::from([
1144 (String::from("qslots"), Value::Integer(slots)),
1146 (String::from("slots"), Value::Tree(Map::new())),
1148 ]);
1149
1150 let new_properties = Map::from([
1151 (String::from("inventory"), Value::Tree(inventory)),
1152 (String::from("meshAngle"), Value::Float(rotation)),
1153 (String::from("forBlockCode"), Value::String(code.trim_start_matches("game:").to_owned())),
1155 (String::from("type"), Value::String(String::from("normal-generic"))),
1156 ]);
1157
1158 (code, Some(new_properties))
1159}
1160
1161fn convert_clutter(name: &str, properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1163 Some(match name {
1164 "lectern" => {
1165 let which = if properties.get("has_book").is_some_and(|b| b == "true") {
1166 "bookshelves/lecturn-book-open"
1168 } else {
1169 "bookshelves/lecturn-empty"
1170 };
1171
1172 clutter(
1173 "game:clutter",
1174 which,
1175 None,
1176 orientation_to_radians(properties.get("facing").map_or("north", |f| reverse_orientation(f))),
1177 )
1178 }
1179 _ => return None,
1180 })
1181}
1182
1183fn convert_nether(name: &str, _properties: &PropertyMap, _settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1185 Some(match name {
1186 "netherrack" => (String::from("game:polishedrockold-cobbled-chert"), None),
1187 "soul_sand" => (String::from("game:cobbleskull-conglomerate"), None),
1188 "soul_soil" => (String::from("game:dirtygravel-wet-plain"), None),
1189 "glowstone" => (String::from("game:paperlantern-on"), None),
1190 _ => return None,
1191 })
1192}
1193
1194fn lantern(material: &str, facing: Option<&str>, hanging: bool) -> (String, Option<EntityMap>) {
1196 let rotation = orientation_to_radians(facing.unwrap_or("north"));
1197
1198 let properties = Map::from([
1199 (String::from("meshAngle"), Value::Float(rotation)),
1200 (String::from("glass"), Value::String(String::from("quartz"))),
1201 (String::from("lining"), Value::String(String::from("plain"))),
1202 (String::from("material"), Value::String(material.to_owned())),
1203 ]);
1204
1205 let code = if hanging {
1206 "game:lantern-down"
1207 } else {
1208 "game:lantern-up"
1209 };
1210
1211 (String::from(code), Some(properties))
1212}
1213
1214fn clutter(code: &str, clutter_code: &str, variant: Option<&str>, angle: f32) -> (String, Option<EntityMap>) {
1216 let mut properties = Map::from([
1217 (String::from("meshAngle"), Value::Float(angle)),
1218 (String::from("type"), Value::String(clutter_code.to_owned())),
1219 (String::from("collected"), Value::Boolean(false)),
1220 (String::from("repairState"), Value::Float(0.0)),
1221 ]);
1222
1223 if let Some(variant) = variant {
1224 properties.insert(String::from("variant"), Value::String(variant.to_owned()));
1225 }
1226
1227 (String::from(code), Some(properties))
1228}
1229
1230fn convert_colourful_wool(colour: &str, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1232 if !settings.mods.contains(&Mod::WoolAndMore) {
1233 return None;
1234 }
1235
1236 let colour = convert_wool_colour(colour);
1237 Some((format!("wool:clothblock-wool-{colour}"), None))
1238}
1239
1240fn convert_wool_colour(colour: &str) -> &str {
1242 match colour {
1243 "white" => "plain",
1244 "cyan" | "light_blue" => "blue",
1245 "blue" => "darkblue",
1246 "lime" => "green",
1247 "green" => "darkgreen",
1248 "magenta" => "purple",
1249 "light_gray" => "gray",
1250 _ => colour,
1251 }
1252}
1253
1254fn convert_moss(name: &str, _properties: &PropertyMap, settings: &Settings) -> Option<(String, Option<EntityMap>)> {
1256 Some(match name {
1257 "moss_block" => (String::from("game:daub-moss-normal"), None),
1258 "pale_moss_block" => (String::from("game:soil-verylow-none"), None),
1259 "moss_carpet" => {
1260 if settings.mods.contains(&Mod::WoolAndMore) {
1261 (String::from("wool:wool-green"), None)
1262 } else {
1263 (String::from("game:smallcarpet-turqoise"), None) }
1265 }
1266 "pale_moss_carpet" => {
1267 if settings.mods.contains(&Mod::WoolAndMore) {
1268 (String::from("wool:wool-gray"), None)
1269 } else {
1270 (String::from("game:smallcarpet-purple"), None)
1271 }
1272 }
1273 _ => return None,
1274 })
1275}
1276
1277fn stepped_rotation(rotation: u8) -> f32 { (std::f32::consts::PI / 8.0) * f32::from(16 - rotation) }