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.