1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
use std::collections::HashMap;

use serde::Deserialize;

#[derive(Debug, Clone)]
pub struct Block {
    pub(crate) name: String,
    pub(crate) encoded: String,
    pub(crate) archetype: BlockArchetype,
}

#[derive(Debug, PartialEq, Clone)]
pub enum BlockArchetype {
    Normal,
    Airy,
    Watery,
    Snowy,
}

impl Block {
    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn snowy(&self) -> bool {
        self.archetype == BlockArchetype::Snowy
    }

    /// A string of the format "id|prop1=val1,prop2=val2". The properties are
    /// ordered lexigraphically. This somewhat matches the way Minecraft stores
    /// variants in blockstates, but with the block ID/name prepended.
    pub fn encoded_description(&self) -> &str {
        &self.encoded
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct BlockRaw {
    name: String,

    #[serde(default)]
    properties: HashMap<String, String>,
}

impl<'de> Deserialize<'de> for Block {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw: BlockRaw = Deserialize::deserialize(deserializer)?;
        let snowy = raw.properties.get("snowy").map(String::as_str) == Some("true");

        let mut id = raw.name.clone() + "|";
        let mut sep = "";

        let mut props = raw
            .properties
            .iter()
            .filter(|(k, _)| *k != "waterlogged") // TODO: Handle water logging. See note below
            .filter(|(k, _)| *k != "powered") // TODO: Handle power
            .collect::<Vec<_>>();

        // need to sort the properties for a consistent ID
        props.sort_unstable();

        for (k, v) in props {
            id = id + sep + k + "=" + v;
            sep = ",";
        }

        let arch = if snowy {
            BlockArchetype::Snowy
        } else if is_watery(&raw.name) {
            BlockArchetype::Watery
        } else if is_airy(&raw.name) {
            BlockArchetype::Airy
        } else {
            BlockArchetype::Normal
        };

        Ok(Self {
            name: raw.name,
            archetype: arch,
            encoded: id,
        })
    }
}

/// Blocks that are considered as if they are water when determining colour.
fn is_watery(block: &str) -> bool {
    matches!(
        block,
        "minecraft:water"
            | "minecraft:bubble_column"
            | "minecraft:kelp"
            | "minecraft:kelp_plant"
            | "minecraft:sea_grass"
            | "minecraft:tall_seagrass"
    )
}

/// Blocks that are considered as if they are air when determining colour.
fn is_airy(block: &str) -> bool {
    matches!(block, "minecraft:air" | "minecraft:cave_air")
}