Skip to main content

steel_core/worldgen/template/
loading.rs

1use std::sync::{Arc, LazyLock};
2
3use rustc_hash::FxHashMap;
4use steel_utils::locks::SyncRwLock;
5
6use super::*;
7
8/// Process-wide cache of parsed vanilla structure templates.
9///
10/// Bundled template NBT is immutable, so each template is decompressed and
11/// parsed at most once; every placement afterwards shares the parsed result.
12static VANILLA_TEMPLATE_CACHE: LazyLock<SyncRwLock<FxHashMap<Identifier, Arc<StructureTemplate>>>> =
13    LazyLock::new(|| SyncRwLock::new(FxHashMap::default()));
14
15impl StructureTemplate {
16    pub(crate) fn load_vanilla(registry: &Registry, key: &Identifier) -> Result<Arc<Self>, String> {
17        if let Some(template) = VANILLA_TEMPLATE_CACHE.read().get(key) {
18            return Ok(Arc::clone(template));
19        }
20
21        let mut cache = VANILLA_TEMPLATE_CACHE.write();
22        if let Some(template) = cache.get(key) {
23            return Ok(Arc::clone(template));
24        }
25
26        let bytes = vanilla_template_pools::vanilla_template_nbt_bytes(key)
27            .ok_or_else(|| format!("vanilla structure template {key} is not bundled"))?;
28        let template = Arc::new(Self::load_gzip_nbt(registry, bytes, &key.to_string())?);
29        cache.insert(key.clone(), Arc::clone(&template));
30        Ok(template)
31    }
32
33    pub(super) fn load_gzip_nbt(
34        registry: &Registry,
35        bytes: &[u8],
36        context: &str,
37    ) -> Result<Self, String> {
38        let mut decoder = GzDecoder::new(bytes);
39        let mut data = Vec::new();
40        decoder
41            .read_to_end(&mut data)
42            .map_err(|err| format!("failed to decompress structure template {context}: {err}"))?;
43
44        let nbt = read_nbt(&mut Cursor::new(&data))
45            .map_err(|err| format!("failed to parse structure template {context}: {err}"))?;
46        let root = match nbt {
47            BorrowedNbt::Some(root) => root,
48            BorrowedNbt::None => {
49                return Err(format!("structure template {context} is empty"));
50            }
51        };
52        let compound = root.as_compound();
53
54        let size = Self::read_vec3(compound.list("size"), context, "size")?;
55        let palettes = Self::read_palettes(registry, &compound, context)?;
56        let blocks = compound
57            .list("blocks")
58            .and_then(|list| list.compounds())
59            .ok_or_else(|| format!("structure template {context} has non-compound blocks list"))?;
60
61        let mut loaded_palettes = Vec::with_capacity(palettes.len());
62        for palette in &palettes {
63            loaded_palettes.push(StructureTemplatePalette {
64                blocks: Self::read_blocks(registry, &blocks, palette, context)?,
65            });
66        }
67
68        let entities = Self::read_entities(registry, &compound, context)?;
69
70        Ok(Self {
71            size,
72            palettes: loaded_palettes,
73            entities,
74        })
75    }
76
77    pub(super) fn read_vec3(
78        list: Option<BorrowedNbtList<'_, '_>>,
79        context: &str,
80        field: &str,
81    ) -> Result<IVec3, String> {
82        let ints = list
83            .and_then(|list| list.ints())
84            .ok_or_else(|| format!("structure template {context} has non-int {field} list"))?;
85        if ints.len() < 3 {
86            return Err(format!(
87                "structure template {context} {field} list has fewer than 3 entries"
88            ));
89        }
90        Ok(IVec3::new(ints[0], ints[1], ints[2]))
91    }
92
93    pub(super) fn read_vec3d(
94        list: Option<BorrowedNbtList<'_, '_>>,
95        context: &str,
96        field: &str,
97    ) -> Result<DVec3, String> {
98        let doubles = list
99            .and_then(|list| list.doubles())
100            .ok_or_else(|| format!("structure template {context} has non-double {field} list"))?;
101        if doubles.len() < 3 {
102            return Err(format!(
103                "structure template {context} {field} list has fewer than 3 entries"
104            ));
105        }
106        Ok(DVec3::new(doubles[0], doubles[1], doubles[2]))
107    }
108
109    pub(super) fn read_palettes(
110        registry: &Registry,
111        compound: &BorrowedNbtCompound<'_, '_>,
112        context: &str,
113    ) -> Result<Vec<Vec<BlockStateId>>, String> {
114        if let Some(palette) = compound.list("palette").and_then(|list| list.compounds()) {
115            return Ok(vec![Self::read_palette(registry, &palette, context)?]);
116        }
117
118        let palettes = compound
119            .list("palettes")
120            .and_then(|list| list.lists())
121            .ok_or_else(|| {
122                format!("structure template {context} is missing palette or palettes")
123            })?;
124        if palettes.is_empty() {
125            return Err(format!(
126                "structure template {context} has empty palettes list"
127            ));
128        }
129
130        let mut result = Vec::with_capacity(palettes.len());
131        for palette in palettes {
132            let entries = palette.compounds().ok_or_else(|| {
133                format!("structure template {context} has non-compound palette entry")
134            })?;
135            result.push(Self::read_palette(registry, &entries, context)?);
136        }
137        Ok(result)
138    }
139
140    pub(super) fn read_palette(
141        registry: &Registry,
142        entries: &BorrowedNbtCompoundList<'_, '_>,
143        context: &str,
144    ) -> Result<Vec<BlockStateId>, String> {
145        let mut states = Vec::with_capacity(entries.len());
146        for entry in entries.clone() {
147            let Some(name) = entry.string("Name") else {
148                return Err(format!(
149                    "structure template {context} has palette entry without Name"
150                ));
151            };
152            let name = Identifier::from_str(name.to_str().as_ref()).map_err(|err| {
153                format!("structure template {context} has invalid block identifier: {err}")
154            })?;
155            let mut properties = BTreeMap::new();
156            if let Some(props) = entry.compound("Properties") {
157                for (key, value) in props.iter() {
158                    let Some(value) = value.string() else {
159                        return Err(format!(
160                            "structure template {context} has non-string property {} on {name}",
161                            key.to_str()
162                        ));
163                    };
164                    properties.insert(key.to_str().into_owned(), value.to_str().into_owned());
165                }
166            }
167            states.push(WorldgenStateResolver::block_state_from_data(
168                registry,
169                &BlockStateData { name, properties },
170                "structure template palette",
171            ));
172        }
173        Ok(states)
174    }
175
176    pub(super) fn read_blocks(
177        registry: &Registry,
178        blocks: &BorrowedNbtCompoundList<'_, '_>,
179        palette: &[BlockStateId],
180        context: &str,
181    ) -> Result<Vec<StructureBlockInfo>, String> {
182        let mut full_blocks = Vec::new();
183        let mut other_blocks = Vec::new();
184        let mut block_entities = Vec::new();
185
186        for block in blocks.clone() {
187            let pos = Self::read_vec3(block.list("pos"), context, "block pos")?;
188            let state_index = block
189                .int("state")
190                .ok_or_else(|| format!("structure template {context} block is missing state"))?;
191            if state_index < 0 {
192                return Err(format!(
193                    "structure template {context} has negative palette state {state_index}"
194                ));
195            }
196            let state_index = usize::try_from(state_index).map_err(|_| {
197                format!("structure template {context} state index does not fit usize")
198            })?;
199            let Some(&state) = palette.get(state_index) else {
200                return Err(format!(
201                    "structure template {context} state index {state_index} exceeds palette length {}",
202                    palette.len()
203                ));
204            };
205            let nbt = block.compound("nbt").map(|nbt| nbt.to_owned());
206            let info = StructureBlockInfo {
207                pos: BlockPos::new(pos[0], pos[1], pos[2]),
208                state,
209                nbt,
210            };
211
212            if info.nbt.is_some() {
213                block_entities.push(info);
214            } else if Self::is_static_full_block(registry, state) {
215                full_blocks.push(info);
216            } else {
217                other_blocks.push(info);
218            }
219        }
220
221        Self::sort_block_infos(&mut full_blocks);
222        Self::sort_block_infos(&mut other_blocks);
223        Self::sort_block_infos(&mut block_entities);
224
225        full_blocks.extend(other_blocks);
226        full_blocks.extend(block_entities);
227        Ok(full_blocks)
228    }
229
230    pub(super) fn read_entities(
231        registry: &Registry,
232        compound: &BorrowedNbtCompound<'_, '_>,
233        context: &str,
234    ) -> Result<Vec<StructureEntityInfo>, String> {
235        let Some(entities) = compound.list("entities").and_then(|list| list.compounds()) else {
236            return Ok(Vec::new());
237        };
238
239        let mut result = Vec::with_capacity(entities.len());
240        for entity in entities.clone() {
241            let pos = Self::read_vec3d(entity.list("pos"), context, "entity pos")?;
242            let block_pos = Self::read_vec3(entity.list("blockPos"), context, "entity blockPos")?;
243            let entity_nbt = entity.compound("nbt").ok_or_else(|| {
244                format!("structure template {context} has entity entry without nbt")
245            })?;
246            let id = entity_nbt
247                .string("id")
248                .ok_or_else(|| format!("structure template {context} has entity nbt without id"))?;
249            let id = Identifier::from_str(id.to_str().as_ref()).map_err(|err| {
250                format!("structure template {context} has invalid entity identifier: {err}")
251            })?;
252            let entity_type = registry.entity_types.by_key(&id).ok_or_else(|| {
253                format!("structure template {context} references unknown entity type {id}")
254            })?;
255            let rotation = Self::read_entity_rotation(&entity_nbt);
256            let velocity = Self::read_optional_vec3d(&entity_nbt, "Motion");
257            let fall_distance = entity_nbt.double("fall_distance").unwrap_or(0.0);
258            let fire_freeze = EntityFireFreezeState::from_parts(
259                Self::read_optional_int(&entity_nbt, "Fire").unwrap_or(0),
260                Self::read_optional_int(&entity_nbt, "TicksFrozen").unwrap_or(0),
261                false,
262                false,
263                entity_nbt
264                    .byte("HasVisualFire")
265                    .is_some_and(|value| value != 0),
266            );
267            let on_ground = entity_nbt.byte("OnGround").is_some_and(|value| value != 0);
268            let save_data = EntityBaseSaveData {
269                air_supply: Self::read_optional_int(&entity_nbt, "Air")
270                    .unwrap_or(DEFAULT_MAX_AIR_SUPPLY),
271                portal_cooldown: Self::read_optional_int(&entity_nbt, "PortalCooldown")
272                    .unwrap_or(0),
273                no_gravity: entity_nbt.byte("NoGravity").is_some_and(|value| value != 0),
274                invulnerable: entity_nbt
275                    .byte("Invulnerable")
276                    .is_some_and(|value| value != 0),
277                custom_name: Self::read_custom_name(&entity_nbt),
278                custom_name_visible: entity_nbt
279                    .byte("CustomNameVisible")
280                    .is_some_and(|value| value != 0),
281                silent: entity_nbt.byte("Silent").is_some_and(|value| value != 0),
282                glowing: entity_nbt.byte("Glowing").is_some_and(|value| value != 0),
283                tags: Self::read_entity_tags(&entity_nbt),
284                custom_data: entity_nbt
285                    .compound("data")
286                    .map_or_else(NbtCompound::new, |compound| compound.to_owned()),
287            };
288            let mut nbt = entity_nbt.to_owned();
289            Self::strip_entity_base_fields(&mut nbt);
290
291            result.push(StructureEntityInfo {
292                pos,
293                block_pos: BlockPos::new(block_pos[0], block_pos[1], block_pos[2]),
294                entity_type,
295                rotation,
296                velocity,
297                fall_distance,
298                fire_freeze,
299                on_ground,
300                save_data,
301                nbt,
302            });
303        }
304
305        Ok(result)
306    }
307
308    pub(super) fn read_entity_rotation(nbt: &BorrowedNbtCompound<'_, '_>) -> (f32, f32) {
309        let Some(rotation) = nbt.list("Rotation").and_then(|list| list.floats()) else {
310            return (0.0, 0.0);
311        };
312        if rotation.len() < 2 {
313            return (0.0, 0.0);
314        }
315        (rotation[0], rotation[1])
316    }
317
318    pub(super) fn read_optional_vec3d(nbt: &BorrowedNbtCompound<'_, '_>, field: &str) -> DVec3 {
319        let Some(values) = nbt.list(field).and_then(|list| list.doubles()) else {
320            return DVec3::ZERO;
321        };
322        if values.len() < 3 {
323            return DVec3::ZERO;
324        }
325        DVec3::new(values[0], values[1], values[2])
326    }
327
328    pub(super) fn read_optional_int(nbt: &BorrowedNbtCompound<'_, '_>, field: &str) -> Option<i32> {
329        nbt.int(field)
330            .or_else(|| nbt.short(field).map(i32::from))
331            .or_else(|| nbt.byte(field).map(i32::from))
332    }
333
334    pub(super) fn read_custom_name(nbt: &BorrowedNbtCompound<'_, '_>) -> Option<TextComponent> {
335        let tag = nbt.get("CustomName")?;
336        TextComponent::from_nbt(&tag.to_owned())
337    }
338
339    pub(super) fn read_entity_tags(nbt: &BorrowedNbtCompound<'_, '_>) -> BTreeSet<String> {
340        nbt.list("Tags")
341            .and_then(|list| list.strings())
342            .map(|tags| {
343                tags.iter()
344                    .take(MAX_ENTITY_TAGS)
345                    .map(|tag| tag.to_str().into_owned())
346                    .collect()
347            })
348            .unwrap_or_default()
349    }
350
351    pub(super) fn strip_entity_base_fields(nbt: &mut NbtCompound) {
352        for field in [
353            "id",
354            "Pos",
355            "Motion",
356            "Rotation",
357            "UUID",
358            "fall_distance",
359            "Fire",
360            "Air",
361            "OnGround",
362            "NoGravity",
363            "Invulnerable",
364            "PortalCooldown",
365            "CustomName",
366            "CustomNameVisible",
367            "Silent",
368            "Glowing",
369            "TicksFrozen",
370            "HasVisualFire",
371            "Tags",
372            "data",
373        ] {
374            let _ = nbt.remove(field);
375        }
376    }
377
378    pub(super) fn is_static_full_block(registry: &Registry, state: BlockStateId) -> bool {
379        let Some(block) = registry.blocks.by_state_id(state) else {
380            return false;
381        };
382        !block.config.dynamic_shape
383            && blocks::shapes::is_shape_full_block(
384                registry.blocks.get_static_collision_shape(state),
385            )
386    }
387
388    pub(super) fn sort_block_infos(blocks: &mut [StructureBlockInfo]) {
389        blocks.sort_by(|left, right| {
390            left.pos
391                .y()
392                .cmp(&right.pos.y())
393                .then(left.pos.x().cmp(&right.pos.x()))
394                .then(left.pos.z().cmp(&right.pos.z()))
395        });
396    }
397
398    pub(crate) const fn size(&self, rotation: Rotation) -> IVec3 {
399        rotation.rotate_size(self.size)
400    }
401
402    pub(crate) const fn zero_position_with_transform(
403        &self,
404        zero_pos: BlockPos,
405        rotation: Rotation,
406    ) -> BlockPos {
407        let x = self.size.x - 1;
408        let z = self.size.z - 1;
409        match rotation {
410            Rotation::None => zero_pos,
411            Rotation::Clockwise90 => zero_pos.offset(z, 0, 0),
412            Rotation::Clockwise180 => zero_pos.offset(x, 0, z),
413            Rotation::CounterClockwise90 => zero_pos.offset(0, 0, x),
414        }
415    }
416}