Skip to main content

nbt_rust/
experiments.rs

1use indexmap::IndexMap;
2
3use crate::error::{Error, Result};
4use crate::root::RootTag;
5use crate::tag::{CompoundTag, Tag, TagType};
6
7pub const KNOWN_EXPERIMENT_KEYS: &[&str] = &[
8    "caves_and_cliffs",
9    "upcoming_creator_features",
10    "gametest",
11    "holiday_creator_features",
12    "data_driven_items",
13    "custom_biomes",
14    "next_major_update",
15    "molang_features",
16];
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ExperimentKeyKind {
20    Known,
21    Unknown,
22}
23
24pub fn classify_experiment_key(key: &str) -> ExperimentKeyKind {
25    if is_known_experiment_key(key) {
26        ExperimentKeyKind::Known
27    } else {
28        ExperimentKeyKind::Unknown
29    }
30}
31
32pub fn is_known_experiment_key(key: &str) -> bool {
33    KNOWN_EXPERIMENT_KEYS.contains(&key)
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Default)]
37pub struct Experiments {
38    flags: IndexMap<String, i8>,
39}
40
41impl Experiments {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    pub fn from_compound(compound: &CompoundTag) -> Result<Self> {
47        let mut flags = IndexMap::with_capacity(compound.len());
48        for (key, value) in compound {
49            let flag = match value {
50                Tag::Byte(flag) => *flag,
51                other => {
52                    return Err(Error::UnexpectedType {
53                        context: "level_dat_experiments_value_type",
54                        expected_id: TagType::Byte.id(),
55                        actual_id: other.tag_type().id(),
56                    });
57                }
58            };
59            flags.insert(key.clone(), flag);
60        }
61        Ok(Self { flags })
62    }
63
64    pub fn to_compound(&self) -> CompoundTag {
65        let mut out = CompoundTag::with_capacity(self.flags.len());
66        for (key, value) in &self.flags {
67            out.insert(key.clone(), Tag::Byte(*value));
68        }
69        out
70    }
71
72    pub fn get(&self, key: &str) -> Option<i8> {
73        self.flags.get(key).copied()
74    }
75
76    pub fn set(&mut self, key: impl Into<String>, value: i8) -> Option<i8> {
77        self.flags.insert(key.into(), value)
78    }
79
80    pub fn remove(&mut self, key: &str) -> Option<i8> {
81        self.flags.shift_remove(key)
82    }
83
84    pub fn len(&self) -> usize {
85        self.flags.len()
86    }
87
88    pub fn is_empty(&self) -> bool {
89        self.flags.is_empty()
90    }
91
92    pub fn iter(&self) -> impl Iterator<Item = (&str, i8)> {
93        self.flags.iter().map(|(key, value)| (key.as_str(), *value))
94    }
95
96    pub fn iter_known(&self) -> impl Iterator<Item = (&str, i8)> {
97        self.iter()
98            .filter(|(key, _)| classify_experiment_key(key) == ExperimentKeyKind::Known)
99    }
100
101    pub fn iter_unknown(&self) -> impl Iterator<Item = (&str, i8)> {
102        self.iter()
103            .filter(|(key, _)| classify_experiment_key(key) == ExperimentKeyKind::Unknown)
104    }
105}
106
107pub fn read_experiments_from_root(root: &RootTag) -> Result<Experiments> {
108    let top = match &root.payload {
109        Tag::Compound(value) => value,
110        other => {
111            return Err(Error::UnexpectedType {
112                context: "level_dat_root_payload_type",
113                expected_id: TagType::Compound.id(),
114                actual_id: other.tag_type().id(),
115            });
116        }
117    };
118    let experiments_tag = top.get("experiments").ok_or(Error::InvalidStructureShape {
119        detail: "level_dat_experiments_missing",
120    })?;
121    let experiments_compound = match experiments_tag {
122        Tag::Compound(value) => value,
123        other => {
124            return Err(Error::UnexpectedType {
125                context: "level_dat_experiments_type",
126                expected_id: TagType::Compound.id(),
127                actual_id: other.tag_type().id(),
128            });
129        }
130    };
131    Experiments::from_compound(experiments_compound)
132}
133
134pub fn write_experiments_to_root(root: &mut RootTag, experiments: &Experiments) -> Result<()> {
135    let top = match &mut root.payload {
136        Tag::Compound(value) => value,
137        other => {
138            return Err(Error::UnexpectedType {
139                context: "level_dat_root_payload_type",
140                expected_id: TagType::Compound.id(),
141                actual_id: other.tag_type().id(),
142            });
143        }
144    };
145    top.insert(
146        "experiments".to_string(),
147        Tag::Compound(experiments.to_compound()),
148    );
149    Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn registry_classifies_known_and_unknown_keys() {
158        assert!(is_known_experiment_key("caves_and_cliffs"));
159        assert_eq!(
160            classify_experiment_key("caves_and_cliffs"),
161            ExperimentKeyKind::Known
162        );
163
164        assert!(!is_known_experiment_key("my_future_toggle"));
165        assert_eq!(
166            classify_experiment_key("my_future_toggle"),
167            ExperimentKeyKind::Unknown
168        );
169    }
170
171    #[test]
172    fn experiments_roundtrip_preserves_unknown_keys() {
173        let mut map = CompoundTag::new();
174        map.insert("caves_and_cliffs".to_string(), Tag::Byte(1));
175        map.insert("my_future_toggle".to_string(), Tag::Byte(1));
176        map.insert("another_unknown".to_string(), Tag::Byte(0));
177
178        let experiments = Experiments::from_compound(&map).unwrap();
179        let roundtrip = experiments.to_compound();
180        assert_eq!(roundtrip, map);
181
182        let unknown: Vec<_> = experiments.iter_unknown().collect();
183        assert_eq!(unknown.len(), 2);
184    }
185
186    #[test]
187    fn experiments_reject_non_byte_values() {
188        let mut map = CompoundTag::new();
189        map.insert("caves_and_cliffs".to_string(), Tag::Int(1));
190        let err = Experiments::from_compound(&map).unwrap_err();
191        assert!(matches!(
192            err,
193            Error::UnexpectedType {
194                context: "level_dat_experiments_value_type",
195                expected_id,
196                actual_id
197            } if expected_id == TagType::Byte.id() && actual_id == TagType::Int.id()
198        ));
199    }
200}