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}