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