Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Structure types

A structure type is a mod's code where a data pack names one: a structure JSON whose "type" is "mymod:tower" instead of a vanilla type generates by the mod's StructureType (vanilla's StructureType, whose find_generation_point returns the start and whose pieces are StructurePieces, loaded by the StructurePieceType that load_piece is — NeoForge registers these in the registries STRUCTURE_TYPE and STRUCTURE_PIECE). Register it once, at startup, in Hooks::register; the id is in the mod's namespace, and a duplicate id, an id in another mod's namespace or an empty name stops the game at startup with a message naming the mod:

    use mod_api::{
        BlockKey, BlockMetadata, BlockState, BoundingBox, Compound, FoundStructure,
        GenerationContext, Hooks, PieceContext, Registrar, RegistrationContext, StateConstraint,
        StructureError, StructurePiece, StructureType, Tag,
    };
    use std::sync::Arc;

    const MATERIAL: &str = "minecraft:oak_log";
    struct Tower {
        material: BlockKey,
    }
    #[derive(Debug)]
    struct Pillar {
        min: [i32; 3],
        max: [i32; 3],
        state: BlockState,
        axis: String,
    }

    impl StructureType for Tower {
        fn find_generation_point(
            &self,
            context: &mut dyn GenerationContext,
        ) -> Option<FoundStructure> {
            let state = BlockState::of(
                context.metadata(),
                self.material,
                &[StateConstraint::new("axis", "y")],
            )
            .ok()?;
            let min = [
                context.chunk().x * 16,
                context.sea_level(),
                context.chunk().z * 16,
            ];
            Some(FoundStructure {
                position: min,
                pieces: vec![Arc::new(Pillar {
                    min,
                    max: [min[0], min[1] + 4, min[2]],
                    state,
                    axis: "y".into(),
                })],
            })
        }
        fn load_piece(
            &self,
            metadata: &dyn BlockMetadata,
            tag: &Compound,
        ) -> Result<Arc<dyn StructurePiece>, StructureError> {
            let block = saved_text(tag, "block")?;
            if metadata
                .name(self.material)
                .map_err(|error| StructureError(format!("saved material: {error:?}")))?
                != block
            {
                return Err(StructureError(
                    "saved block does not match this pillar's material".into(),
                ));
            }
            let axis = saved_text(tag, "axis")?;
            let state = BlockState::of(
                metadata,
                self.material,
                &[StateConstraint::new("axis", &axis)],
            )
            .map_err(|error| StructureError(format!("saved block properties: {error:?}")))?;
            Ok(Arc::new(Pillar {
                min: saved_position(tag, "min")?,
                max: saved_position(tag, "max")?,
                state,
                axis,
            }))
        }
    }
    impl StructurePiece for Pillar {
        fn bounding_box(&self) -> BoundingBox {
            BoundingBox::new(self.min, self.max)
        }
        fn post_process(&self, context: &mut dyn PieceContext) -> Result<(), StructureError> {
            for y in self.min[1]..=self.max[1] {
                context.set_block([self.min[0], y, self.min[2]], self.state);
            }
            Ok(())
        }
        fn add_additional_save_data(&self, tag: &mut Compound) {
            tag.insert("min".into(), Tag::IntArray(self.min.to_vec()));
            tag.insert("max".into(), Tag::IntArray(self.max.to_vec()));
            tag.insert("block".into(), Tag::String(MATERIAL.into()));
            tag.insert("axis".into(), Tag::String(self.axis.as_str().into()));
        }
    }
    fn saved_text(tag: &Compound, key: &str) -> Result<String, StructureError> {
        match tag.get(&key.into()) {
            Some(Tag::String(value)) => value
                .to_string()
                .map_err(|error| StructureError(error.to_string())),
            _ => Err(StructureError(format!("missing or invalid saved {key}"))),
        }
    }
    fn saved_position(tag: &Compound, key: &str) -> Result<[i32; 3], StructureError> {
        match tag.get(&key.into()) {
            Some(Tag::IntArray(values)) if values.len() == 3 => {
                Ok([values[0], values[1], values[2]])
            }
            _ => Err(StructureError(format!("missing or invalid saved {key}"))),
        }
    }
    #[derive(Default)]
    pub struct StructureMod;
    #[mod_api::horizon_mod]
    impl Hooks for StructureMod {
        fn register(&self, registrar: &mut dyn Registrar, _: &RegistrationContext) {
            let material = registrar.block_key(MATERIAL);
            registrar.register_structure_type("tower", Tower { material });
        }
    }

There is no Host in world generation: starts build on worker threads and pieces place per chunk, so the type and its pieces see only two small contexts, Send + Sync and deterministic. The same seed gives the same pieces, and a type keeps no mutable state, because two workers can build neighbouring chunks at once.

StructureType::find_generation_point(&self, context: &mut dyn GenerationContext) answers FoundStructure { position, pieces } (each piece an Arc<dyn StructurePiece>) or None for "no start in this chunk". The GenerationContext is vanilla's GenerationContext over one chunk: the seed, the chunk, the random seeded as vanilla seeds it for the chunk (setLargeFeatureSeed), the height and base-column queries, opaque noise-biome identities and the valid-biome test, borrowed immutable metadata(), and the structure's config as a shared Arc<Tag> (context.config()). JSON converts once when the definition loads or reloads, not once per start. Nested nulls refuse at load. Both contexts are traits and the random is a StructureRandom (next_int_bound, next_long, next_float, next_double, next_boolean), so a type built to mod.wasm runs the same code. Generated worker calls take the dependency group's store lock and per-call fuel or epoch budget; a trap stops that group with a typed refusal. Everything around the type stays the game's: the structure set's spacing, the structure's biomes and terrain_adaptation are placed by the engine, exactly as for a fortress.

Each piece is a StructurePiece (Debug + Send + Sync): bounding_box, post_process and add_additional_save_data are required. After all pieces of the start were placed, the type's own after_place (vanilla's Structure.afterPlace) runs once per chunk with every piece's current box; it defaults to doing nothing. The PieceContext of post_process and after_place is the chunk being placed: set_block writes a block and drops a position outside the writable box silently, as vanilla's StructurePiece.placeBlock skips it; block and height read, state/state_with resolve a a registered BlockKey into an API-owned BlockState, random is the piece's RandomSource as a StructureRandom, and metadata().name(state.block()) borrows its canonical block name. solid_render asks whether a state draws as a solid block, and entity queues an entity from its saved NBT — its id, Pos and fields — which the generating chunk holds and the game finishes as a structure mob's (Mob.finalizeSpawn with STRUCTURE when the NBT keeps the "horizon:finalize" byte a structure template's entities carry). block_entity places a block entity from its NBT.

Saving goes through the engine's StructurePiece.createTag — the type id, BB, O, GD — around the piece's add_additional_save_data. When the chunk loads back, the type's load_piece (the StructurePieceType loader) rebuilds the piece exactly as it was saved; there is no regeneration from the seed. load_piece(metadata, tag) returns a typed StructureError for invalid saved block names, properties or piece data. Its metadata is the same retained snapshot the worker contexts borrow. Resolve states once in piece construction or load, as the example does; keep canonical names and properties in saved data, never process-local ordinals. post_process then writes retained states without per-block name lookups or allocation.

Open question — the saved piece's id is the structure type's, not a piece type's. Every piece of a type saves with that type's id, the STRUCTURE_TYPE id; Horizon has no separate STRUCTURE_PIECE registry, so there is no per-piece-kind id in the tag. A mod whose type has several kinds of pieces tells them apart in load_piece by its own saved data (a tag field its add_additional_save_data writes). The O (orientation) and GD (genDepth) fields are written as -1 and 0 — the adapter has no orientation or depth to record — so a piece must not read them back as meaningful.

The pieces ride the one piece mechanism the game has: they are legacy pieces behind an adapter, so the start cache, the cross-chunk placement, the beardifier and /locate structure treat a tower piece like a fortress piece's. StructureRegistry reads the registered types before it falls to Unsupported, so a data pack structure whose "type" no installed mod registers places nothing, exactly as before.