feat: Implement End City structures (#2761)

This commit is contained in:
ZlordHUN
2026-08-05 08:45:34 +02:00
committed by GitHub
parent 060c10dfa2
commit 150eff5897
5 changed files with 1183 additions and 184 deletions

View File

@@ -257,6 +257,59 @@ mod tests {
assert_eq!(jigsaw_blocks, 0, "jigsaw blocks were not replaced");
}
#[test]
fn fixed_seed_generates_vanilla_end_ship_chunk() {
// Vanilla 26.2 places this seed's ship in chunk (-306, -275).
let dimension = Dimension::THE_END;
let seed = Seed(12_345);
let block_registry = Arc::new(BlockRegistry);
let world_gen = get_world_gen(seed, dimension.clone(), false, Vec::new(), String::new());
let biome_mixer_seed = hash_seed(world_gen.seed());
let chunk = generate_single_chunk(
&dimension,
biome_mixer_seed,
&world_gen,
block_registry.as_ref(),
-306,
-275,
StagedChunkEnum::Features,
);
let Chunk::Proto(chunk) = chunk else {
panic!("features stage should return a proto chunk");
};
let mut hash = 0xcbf29ce484222325u64;
let mut non_air = 0;
for y in 123..=146 {
for x in -4896..=-4881 {
for z in -4405..=-4393 {
let state =
chunk.get_block_state(&pumpkin_util::math::vector3::Vector3::new(x, y, z));
hash ^= u64::from(state.as_u16());
hash = hash.wrapping_mul(0x100000001b3);
non_air += usize::from(!state.to_state().is_air());
}
}
}
assert_eq!(non_air, 59);
assert_eq!(hash, 0x7db9_af53_56af_6917);
assert!(chunk.pending_block_entities.iter().any(|nbt| {
nbt.get_string("id") == Some("minecraft:skull")
&& nbt.get_int("x") == Some(-4888)
&& nbt.get_int("y") == Some(131)
&& nbt.get_int("z") == Some(-4399)
}));
assert_eq!(
chunk
.pending_block_entities
.iter()
.filter(
|nbt| nbt.get_string("LootTable") == Some("minecraft:chests/end_city_treasure")
)
.count(),
2
);
}
#[test]
fn pillager_outpost_features_shape_ground_at_vanilla_height() {
let dimension = Dimension::OVERWORLD;

View File

@@ -1,184 +0,0 @@
use std::sync::Arc;
use pumpkin_data::block_rotation::Rotation;
use pumpkin_util::{
math::{block_box::BlockBox, position::BlockPos, vector3::Vector3},
random::{RandomGenerator, RandomImpl},
};
use crate::{
ProtoChunk,
generation::{
positions::chunk_pos::{get_center_x, get_center_z},
structure::{
piece::StructurePieceType,
structures::{
StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase,
StructurePiecesCollector, StructurePosition, WorldPortalExt,
},
template::{StructureTemplate, get_template, place_template},
},
},
};
pub struct EndCityGenerator;
impl StructureGenerator for EndCityGenerator {
fn get_structure_position(
&self,
mut context: StructureGeneratorContext<'_>,
) -> Option<StructurePosition> {
let chunk_center_x = get_center_x(context.chunk_x);
let chunk_center_z = get_center_z(context.chunk_z);
let rotation_idx = context.random.next_bounded_i32(4) as u8;
let rotation = Rotation::from_index(rotation_idx);
let base_floor = get_template("end_city/base_floor")?;
let tower_base = get_template("end_city/tower_base")?;
let tower_piece = get_template("end_city/tower_piece")?;
let tower_top = get_template("end_city/tower_top")?;
let ship = get_template("end_city/ship")?;
let bounding_box = BlockBox::new(
chunk_center_x - 30,
context.min_y,
chunk_center_z - 30,
chunk_center_x + 30,
256,
chunk_center_z + 30,
);
let mut collector = StructurePiecesCollector::default();
collector.add_piece(Box::new(EndCityPiece {
piece: StructurePiece::new(StructurePieceType::EndCity, bounding_box, 0),
base_floor,
tower_base,
tower_piece,
tower_top,
ship,
rotation,
has_ship: context.random.next_f32() < 0.5,
}));
Some(StructurePosition {
start_pos: BlockPos::new(chunk_center_x, 64, chunk_center_z),
collector: Arc::new(collector.into()),
})
}
}
pub struct EndCityPiece {
piece: StructurePiece,
base_floor: Arc<StructureTemplate>,
tower_base: Arc<StructureTemplate>,
tower_piece: Arc<StructureTemplate>,
tower_top: Arc<StructureTemplate>,
ship: Arc<StructureTemplate>,
rotation: Rotation,
has_ship: bool,
}
impl StructurePieceBase for EndCityPiece {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn get_structure_piece(&self) -> &StructurePiece {
&self.piece
}
fn get_structure_piece_mut(&mut self) -> &mut StructurePiece {
&mut self.piece
}
fn place(
&mut self,
chunk: &mut ProtoChunk,
_block_registry: &dyn WorldPortalExt,
_random: &mut RandomGenerator,
_seed: i64,
chunk_box: &BlockBox,
) {
let origin = self.piece.bounding_box.min;
let sample_y =
chunk.get_top_y(&pumpkin_util::HeightMap::WorldSurfaceWg, origin.x, origin.z);
let start_y = if sample_y <= 0 { 60 } else { sample_y };
// 1. Place Base Floor
let mut pos = Vector3::new(origin.x, start_y, origin.z);
place_template(
chunk,
&self.base_floor,
pos,
(0, 0),
self.rotation,
true,
false,
&[],
Some(chunk_box),
);
// 2. Place Tower Base
pos.y += self.base_floor.size.y;
place_template(
chunk,
&self.tower_base,
pos,
(0, 0),
self.rotation,
true,
false,
&[],
Some(chunk_box),
);
// 3. Place Tower Piece
pos.y += self.tower_base.size.y;
place_template(
chunk,
&self.tower_piece,
pos,
(0, 0),
self.rotation,
true,
false,
&[],
Some(chunk_box),
);
// 4. Place Tower Top
pos.y += self.tower_piece.size.y;
place_template(
chunk,
&self.tower_top,
pos,
(0, 0),
self.rotation,
true,
false,
&[],
Some(chunk_box),
);
// 5. Place End Ship
if self.has_ship {
let ship_pos = Vector3::new(origin.x + 16, start_y + 20, origin.z + 16);
place_template(
chunk,
&self.ship,
ship_pos,
(0, 0),
self.rotation,
true,
false,
&[],
Some(chunk_box),
);
crate::generation::structure::template::place_template_entities(
chunk,
&self.ship,
ship_pos,
self.rotation,
chunk_box,
);
}
}
}

View File

@@ -0,0 +1,702 @@
use std::sync::Arc;
use pumpkin_data::Rotation;
use pumpkin_util::{
math::{block_box::BlockBox, vector3::Vector3},
random::{RandomGenerator, RandomImpl},
};
use crate::generation::structure::template::{StructureTemplate, get_template};
const MAX_GENERATION_DEPTH: i32 = 8;
const TOWER_BRIDGES: [(Rotation, Vector3<i32>); 4] = [
(Rotation::None, Vector3::new(1, -1, 0)),
(Rotation::Clockwise90, Vector3::new(6, -1, 1)),
(Rotation::CounterClockwise90, Vector3::new(0, -1, 5)),
(Rotation::Rotate180, Vector3::new(5, -1, 6)),
];
const FAT_TOWER_BRIDGES: [(Rotation, Vector3<i32>); 4] = [
(Rotation::None, Vector3::new(4, -1, 0)),
(Rotation::Clockwise90, Vector3::new(12, -1, 4)),
(Rotation::CounterClockwise90, Vector3::new(0, -1, 8)),
(Rotation::Rotate180, Vector3::new(8, -1, 12)),
];
#[derive(Clone)]
pub(super) struct PieceDescriptor {
pub template: Arc<StructureTemplate>,
pub template_position: Vector3<i32>,
pub rotation: Rotation,
pub overwrite: bool,
pub bounding_box: BlockBox,
generation_group: i32,
#[cfg(test)]
pub template_name: &'static str,
}
impl PieceDescriptor {
pub(super) fn new(
template_name: &'static str,
template_position: Vector3<i32>,
rotation: Rotation,
overwrite: bool,
) -> Option<Self> {
let template = get_template(&format!("end_city/{template_name}"))?;
let max = Vector3::new(
template.size.x - 1,
template.size.y - 1,
template.size.z - 1,
);
let (max_x, max_z) = rotation.rotate_offset(max.x, max.z);
let bounding_box = BlockBox::new(
template_position.x.min(template_position.x + max_x),
template_position.y,
template_position.z.min(template_position.z + max_z),
template_position.x.max(template_position.x + max_x),
template_position.y + max.y,
template_position.z.max(template_position.z + max_z),
);
Some(Self {
template,
template_position,
rotation,
overwrite,
bounding_box,
generation_group: 0,
#[cfg(test)]
template_name,
})
}
fn connected(
parent: &Self,
offset: Vector3<i32>,
template_name: &'static str,
rotation: Rotation,
overwrite: bool,
) -> Option<Self> {
// Vanilla's calculateConnectedPosition rotates both connector positions
// around the template origin. End City children always use ZERO for the
// child connector, leaving only the parent's rotated offset.
let (x, z) = parent.rotation.rotate_offset(offset.x, offset.z);
Self::new(
template_name,
Vector3::new(
parent.template_position.x + x,
parent.template_position.y + offset.y,
parent.template_position.z + z,
),
rotation,
overwrite,
)
}
}
#[derive(Clone, Copy)]
enum Section {
HouseTower,
Tower,
TowerBridge,
FatTower,
}
pub(super) struct EndCityLayout {
ship_created: bool,
}
impl EndCityLayout {
pub fn create(
origin: Vector3<i32>,
rotation: Rotation,
random: &mut RandomGenerator,
) -> Option<Vec<PieceDescriptor>> {
let mut layout = Self {
ship_created: false,
};
let mut pieces = Vec::new();
let mut last = PieceDescriptor::new("base_floor", origin, rotation, true)?;
pieces.push(last.clone());
last = Self::push_connected(
&mut pieces,
&last,
Vector3::new(-1, 0, -1),
"second_floor_1",
rotation,
false,
)?;
last = Self::push_connected(
&mut pieces,
&last,
Vector3::new(-1, 4, -1),
"third_floor_1",
rotation,
false,
)?;
last = Self::push_connected(
&mut pieces,
&last,
Vector3::new(-1, 8, -1),
"third_roof",
rotation,
true,
)?;
let _ = layout.recursive_children(Section::Tower, 1, &last, None, &mut pieces, random)?;
Some(pieces)
}
fn push_connected(
pieces: &mut Vec<PieceDescriptor>,
parent: &PieceDescriptor,
offset: Vector3<i32>,
template_name: &'static str,
rotation: Rotation,
overwrite: bool,
) -> Option<PieceDescriptor> {
let piece = PieceDescriptor::connected(parent, offset, template_name, rotation, overwrite)?;
pieces.push(piece.clone());
Some(piece)
}
fn recursive_children(
&mut self,
section: Section,
depth: i32,
parent: &PieceDescriptor,
offset: Option<Vector3<i32>>,
pieces: &mut Vec<PieceDescriptor>,
random: &mut RandomGenerator,
) -> Option<bool> {
if depth > MAX_GENERATION_DEPTH {
return Some(false);
}
let mut children = Vec::new();
if !self.generate_section(section, depth, parent, offset, &mut children, random)? {
return Some(false);
}
let generation_group = random.next_i32();
for child in &mut children {
child.generation_group = generation_group;
if let Some(collision) = pieces
.iter()
.find(|piece| piece.bounding_box.intersects(&child.bounding_box))
&& collision.generation_group != parent.generation_group
{
return Some(false);
}
}
pieces.extend(children);
Some(true)
}
fn generate_section(
&mut self,
section: Section,
depth: i32,
parent: &PieceDescriptor,
offset: Option<Vector3<i32>>,
pieces: &mut Vec<PieceDescriptor>,
random: &mut RandomGenerator,
) -> Option<bool> {
match section {
Section::HouseTower => {
self.generate_house_tower(depth, parent, offset?, pieces, random)
}
Section::Tower => self.generate_tower(depth, parent, pieces, random),
Section::TowerBridge => self.generate_tower_bridge(depth, parent, pieces, random),
Section::FatTower => self.generate_fat_tower(depth, parent, pieces, random),
}
}
fn generate_house_tower(
&mut self,
depth: i32,
parent: &PieceDescriptor,
offset: Vector3<i32>,
pieces: &mut Vec<PieceDescriptor>,
random: &mut RandomGenerator,
) -> Option<bool> {
if depth > MAX_GENERATION_DEPTH {
return Some(false);
}
let rotation = parent.rotation;
let mut last = Self::push_connected(pieces, parent, offset, "base_floor", rotation, true)?;
match random.next_bounded_i32(3) {
0 => {
Self::push_connected(
pieces,
&last,
Vector3::new(-1, 4, -1),
"base_roof",
rotation,
true,
)?;
}
1 => {
last = Self::push_connected(
pieces,
&last,
Vector3::new(-1, 0, -1),
"second_floor_2",
rotation,
false,
)?;
last = Self::push_connected(
pieces,
&last,
Vector3::new(-1, 8, -1),
"second_roof",
rotation,
false,
)?;
let _ = self.recursive_children(
Section::Tower,
depth + 1,
&last,
None,
pieces,
random,
)?;
}
_ => {
last = Self::push_connected(
pieces,
&last,
Vector3::new(-1, 0, -1),
"second_floor_2",
rotation,
false,
)?;
last = Self::push_connected(
pieces,
&last,
Vector3::new(-1, 4, -1),
"third_floor_2",
rotation,
false,
)?;
last = Self::push_connected(
pieces,
&last,
Vector3::new(-1, 8, -1),
"third_roof",
rotation,
true,
)?;
let _ = self.recursive_children(
Section::Tower,
depth + 1,
&last,
None,
pieces,
random,
)?;
}
}
Some(true)
}
fn generate_tower(
&mut self,
depth: i32,
parent: &PieceDescriptor,
pieces: &mut Vec<PieceDescriptor>,
random: &mut RandomGenerator,
) -> Option<bool> {
let rotation = parent.rotation;
let mut last = Self::push_connected(
pieces,
parent,
Vector3::new(
3 + random.next_bounded_i32(2),
-3,
3 + random.next_bounded_i32(2),
),
"tower_base",
rotation,
true,
)?;
last = Self::push_connected(
pieces,
&last,
Vector3::new(0, 7, 0),
"tower_piece",
rotation,
true,
)?;
let mut bridge_piece = (random.next_bounded_i32(3) == 0).then(|| last.clone());
let tower_height = 1 + random.next_bounded_i32(3);
for index in 0..tower_height {
last = Self::push_connected(
pieces,
&last,
Vector3::new(0, 4, 0),
"tower_piece",
rotation,
true,
)?;
if index < tower_height - 1 && random.next_bool() {
bridge_piece = Some(last.clone());
}
}
if let Some(bridge_piece) = bridge_piece {
for (bridge_rotation, offset) in TOWER_BRIDGES {
if random.next_bool() {
let bridge_start = Self::push_connected(
pieces,
&bridge_piece,
offset,
"bridge_end",
rotation.then(bridge_rotation),
true,
)?;
let _ = self.recursive_children(
Section::TowerBridge,
depth + 1,
&bridge_start,
None,
pieces,
random,
)?;
}
}
Self::push_connected(
pieces,
&last,
Vector3::new(-1, 4, -1),
"tower_top",
rotation,
true,
)?;
return Some(true);
}
if depth != 7 {
return self.recursive_children(
Section::FatTower,
depth + 1,
&last,
None,
pieces,
random,
);
}
Self::push_connected(
pieces,
&last,
Vector3::new(-1, 4, -1),
"tower_top",
rotation,
true,
)?;
Some(true)
}
fn generate_tower_bridge(
&mut self,
depth: i32,
parent: &PieceDescriptor,
pieces: &mut Vec<PieceDescriptor>,
random: &mut RandomGenerator,
) -> Option<bool> {
let rotation = parent.rotation;
let bridge_length = random.next_bounded_i32(4) + 1;
let mut last = Self::push_connected(
pieces,
parent,
Vector3::new(0, 0, -4),
"bridge_piece",
rotation,
true,
)?;
last.generation_group = -1;
if let Some(piece) = pieces.last_mut() {
piece.generation_group = -1;
}
let mut next_y = 0;
for _ in 0..bridge_length {
let offset_y = next_y;
let (template, z) = if random.next_bool() {
next_y = 0;
("bridge_piece", -4)
} else {
let template = if random.next_bool() {
"bridge_steep_stairs"
} else {
"bridge_gentle_stairs"
};
let z = if template == "bridge_steep_stairs" {
-4
} else {
-8
};
next_y = 4;
(template, z)
};
last = Self::push_connected(
pieces,
&last,
Vector3::new(0, offset_y, z),
template,
rotation,
true,
)?;
}
if !self.ship_created && random.next_bounded_i32(10 - depth) == 0 {
Self::push_connected(
pieces,
&last,
Vector3::new(
-8 + random.next_bounded_i32(8),
next_y,
-70 + random.next_bounded_i32(10),
),
"ship",
rotation,
true,
)?;
self.ship_created = true;
} else if !self.recursive_children(
Section::HouseTower,
depth + 1,
&last,
Some(Vector3::new(-3, next_y + 1, -11)),
pieces,
random,
)? {
return Some(false);
}
Self::push_connected(
pieces,
&last,
Vector3::new(4, next_y, 0),
"bridge_end",
rotation.then(Rotation::Rotate180),
true,
)?;
if let Some(piece) = pieces.last_mut() {
piece.generation_group = -1;
}
Some(true)
}
fn generate_fat_tower(
&mut self,
depth: i32,
parent: &PieceDescriptor,
pieces: &mut Vec<PieceDescriptor>,
random: &mut RandomGenerator,
) -> Option<bool> {
let rotation = parent.rotation;
let mut last = Self::push_connected(
pieces,
parent,
Vector3::new(-3, 4, -3),
"fat_tower_base",
rotation,
true,
)?;
last = Self::push_connected(
pieces,
&last,
Vector3::new(0, 4, 0),
"fat_tower_middle",
rotation,
true,
)?;
for _ in 0..2 {
if random.next_bounded_i32(3) == 0 {
break;
}
last = Self::push_connected(
pieces,
&last,
Vector3::new(0, 8, 0),
"fat_tower_middle",
rotation,
true,
)?;
for (bridge_rotation, offset) in FAT_TOWER_BRIDGES {
if random.next_bool() {
let bridge_start = Self::push_connected(
pieces,
&last,
offset,
"bridge_end",
rotation.then(bridge_rotation),
true,
)?;
let _ = self.recursive_children(
Section::TowerBridge,
depth + 1,
&bridge_start,
None,
pieces,
random,
)?;
}
}
}
Self::push_connected(
pieces,
&last,
Vector3::new(-2, 8, -2),
"fat_tower_top",
rotation,
true,
)?;
Some(true)
}
}
#[cfg(test)]
mod tests {
use pumpkin_data::Mirror;
use pumpkin_util::random::legacy_rand::LegacyRand;
use crate::generation::structure::{
structures::create_chunk_random, template::BlockStateResolver,
};
use super::*;
#[test]
fn fixed_seed_matches_vanilla_ship_city() {
let mut random = create_chunk_random(12_345, -298, -275);
let rotation = Rotation::from_index(random.next_bounded_i32(4) as u8);
let pieces =
EndCityLayout::create(Vector3::new(-4761, 60, -4393), rotation, &mut random).unwrap();
assert_eq!(rotation, Rotation::Rotate180);
assert_eq!(pieces.len(), 78);
let ship = &pieces[43];
assert_eq!(ship.template_name, "ship");
assert_eq!(ship.rotation, Rotation::CounterClockwise90);
assert_eq!(ship.template_position, Vector3::new(-4888, 123, -4393));
assert_eq!(ship.generation_group, -340_338_317);
assert_eq!(
(ship.bounding_box.min, ship.bounding_box.max),
(
Vector3::new(-4888, 123, -4405),
Vector3::new(-4860, 146, -4393),
)
);
}
#[test]
fn fixed_seed_matches_vanilla_fat_tower_city() {
let mut random = create_chunk_random(12_345, 62, 24);
let rotation = Rotation::from_index(random.next_bounded_i32(4) as u8);
let pieces =
EndCityLayout::create(Vector3::new(999, 61, 391), rotation, &mut random).unwrap();
assert_eq!(
pieces
.iter()
.map(|piece| piece.template_name)
.collect::<Vec<_>>(),
[
"base_floor",
"second_floor_1",
"third_floor_1",
"third_roof",
"tower_base",
"tower_piece",
"tower_piece",
"tower_piece",
"fat_tower_base",
"fat_tower_middle",
"fat_tower_top",
]
);
let top = pieces.last().unwrap();
assert_eq!(top.template_position, Vector3::new(994, 101, 386));
assert_eq!(top.generation_group, 1_141_071_963);
}
#[test]
fn connected_pieces_rotate_around_the_parent_origin() {
let parent = PieceDescriptor::new(
"base_floor",
Vector3::new(100, 70, 200),
Rotation::Clockwise90,
true,
)
.unwrap();
let child = PieceDescriptor::connected(
&parent,
Vector3::new(-1, 4, -3),
"base_roof",
Rotation::Clockwise90,
true,
)
.unwrap();
assert_eq!(child.template_position, Vector3::new(103, 74, 199));
}
#[test]
fn vanilla_templates_are_available() {
for name in [
"base_floor",
"base_roof",
"bridge_end",
"bridge_gentle_stairs",
"bridge_piece",
"bridge_steep_stairs",
"fat_tower_base",
"fat_tower_middle",
"fat_tower_top",
"second_floor_1",
"second_floor_2",
"second_roof",
"ship",
"third_floor_1",
"third_floor_2",
"third_roof",
"tower_base",
"tower_piece",
"tower_top",
] {
let template = get_template(&format!("end_city/{name}")).unwrap();
for palette in &template.palette {
for rotation in [
Rotation::None,
Rotation::Clockwise90,
Rotation::Rotate180,
Rotation::CounterClockwise90,
] {
assert!(
BlockStateResolver::resolve(palette, rotation, Mirror::None).is_some(),
"failed to resolve {} in {name} with {rotation:?}",
palette.name
);
}
}
}
}
#[test]
fn recursive_generation_never_creates_multiple_ships() {
for seed in 0..256 {
let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(seed));
let pieces = EndCityLayout::create(
Vector3::new(0, 70, 0),
Rotation::from_index(random.next_bounded_i32(4) as u8),
&mut random,
)
.unwrap();
assert!(
pieces
.iter()
.filter(|piece| piece.template_name == "ship")
.count()
<= 1
);
}
}
}

View File

@@ -0,0 +1,427 @@
mod layout;
use std::sync::{Arc, Mutex};
use pumpkin_data::{BlockDirection, Mirror, Rotation, item::Item, item_stack::ItemStack};
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
use pumpkin_util::{
math::{block_box::BlockBox, position::BlockPos, vector3::Vector3},
random::{RandomGenerator, RandomImpl},
};
use crate::{
ProtoChunk,
generation::structure::{
piece::StructurePieceType,
structures::{
StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase,
StructurePiecesCollector, StructurePosition,
},
template::{BlockStateResolver, PaletteEntry, get_block_entity_id},
},
world::WorldPortalExt,
};
use self::layout::{EndCityLayout, PieceDescriptor};
const LOOT_TABLE: &str = "minecraft:chests/end_city_treasure";
pub struct EndCityGenerator;
impl StructureGenerator for EndCityGenerator {
fn get_structure_position(
&self,
mut context: StructureGeneratorContext<'_>,
) -> Option<StructurePosition> {
let rotation = Rotation::from_index(context.random.next_bounded_i32(4) as u8);
let x = context.chunk_x * 16 + 7;
let z = context.chunk_z * 16 + 7;
let (offset_x, offset_z) = match rotation {
Rotation::None => (5, 5),
Rotation::Clockwise90 => (-5, 5),
Rotation::Rotate180 => (-5, -5),
Rotation::CounterClockwise90 => (5, -5),
};
let y = {
let sampler = context.height_sampler.as_deref_mut()?;
[
sampler.estimate_height(x, z) - 1,
sampler.estimate_height(x, z + offset_z) - 1,
sampler.estimate_height(x + offset_x, z) - 1,
sampler.estimate_height(x + offset_x, z + offset_z) - 1,
]
.into_iter()
.min()?
};
if y < 60 {
return None;
}
let descriptors =
EndCityLayout::create(Vector3::new(x, y, z), rotation, &mut context.random)?;
let mut collector = StructurePiecesCollector::default();
for descriptor in descriptors {
collector.add_piece(Box::new(EndCityTemplatePiece::new(descriptor)));
}
Some(StructurePosition {
start_pos: BlockPos::new(x, y, z),
collector: Arc::new(Mutex::new(collector)),
})
}
}
struct EndCityTemplatePiece {
piece: StructurePiece,
descriptor: PieceDescriptor,
}
impl EndCityTemplatePiece {
const fn new(descriptor: PieceDescriptor) -> Self {
Self {
piece: StructurePiece::new(StructurePieceType::EndCity, descriptor.bounding_box, 0),
descriptor,
}
}
const fn world_position(&self, local: Vector3<i32>) -> Vector3<i32> {
let (x, z) = self.descriptor.rotation.rotate_offset(local.x, local.z);
Vector3::new(
self.descriptor.template_position.x + x,
self.descriptor.template_position.y + local.y,
self.descriptor.template_position.z + z,
)
}
fn place_blocks(
&self,
chunk: &mut ProtoChunk,
random: &mut RandomGenerator,
chunk_box: &BlockBox,
) {
for block in &self.descriptor.template.blocks {
let palette = &self.descriptor.template.palette[block.state as usize];
if matches!(
palette.name.as_str(),
"minecraft:structure_void" | "minecraft:structure_block"
) || (!self.descriptor.overwrite && palette.name == "minecraft:air")
{
continue;
}
let position = self.world_position(block.pos);
if !chunk_box.contains_pos(&position) {
continue;
}
let Some(state) =
BlockStateResolver::resolve(palette, self.descriptor.rotation, Mirror::None)
else {
continue;
};
chunk.set_block_state(position.x, position.y, position.z, state);
Self::place_block_entity(chunk, palette, block.nbt.as_ref(), position, random);
}
for block in &self.descriptor.template.blocks {
let palette = &self.descriptor.template.palette[block.state as usize];
if palette.name != "minecraft:structure_block" {
continue;
}
let Some(marker) = block
.nbt
.as_ref()
.and_then(|nbt| nbt.get_string("metadata"))
else {
continue;
};
self.handle_marker(
chunk,
random,
marker,
self.world_position(block.pos),
chunk_box,
);
}
}
fn place_block_entity(
chunk: &mut ProtoChunk,
palette: &PaletteEntry,
template_nbt: Option<&NbtCompound>,
position: Vector3<i32>,
random: &mut RandomGenerator,
) {
let block_entity_id = get_block_entity_id(&palette.name);
if template_nbt.is_none() && block_entity_id.is_none() {
return;
}
let mut nbt = NbtCompound::new();
nbt.put_string("id", block_entity_id.unwrap_or(&palette.name).to_string());
nbt.put_int("x", position.x);
nbt.put_int("y", position.y);
nbt.put_int("z", position.z);
if let Some(template_nbt) = template_nbt {
for (key, value) in &template_nbt.child_tags {
if !matches!(key.as_ref(), "x" | "y" | "z" | "id") {
nbt.child_tags.insert(key.clone(), value.clone());
}
}
}
if nbt.get_string("LootTable").is_some() && nbt.get_long("LootTableSeed").is_none() {
nbt.put_long("LootTableSeed", random.next_i64());
}
chunk.add_block_entity(nbt);
}
fn handle_marker(
&self,
chunk: &mut ProtoChunk,
random: &mut RandomGenerator,
marker: &str,
position: Vector3<i32>,
chunk_box: &BlockBox,
) {
if marker.starts_with("Chest") {
let chest_position = Vector3::new(position.x, position.y - 1, position.z);
if chunk_box.contains_pos(&chest_position) {
let mut nbt = NbtCompound::new();
nbt.put_string("id", "minecraft:chest".to_string());
nbt.put_int("x", chest_position.x);
nbt.put_int("y", chest_position.y);
nbt.put_int("z", chest_position.z);
nbt.put_string("LootTable", LOOT_TABLE.to_string());
nbt.put_long("LootTableSeed", random.next_i64());
chunk.add_block_entity(nbt);
}
return;
}
if !chunk_box.contains_pos(&position) {
return;
}
if marker.starts_with("Sentry") {
chunk.add_structure_entity(entity_nbt(
"minecraft:shulker",
Vector3::new(
f64::from(position.x) + 0.5,
f64::from(position.y),
f64::from(position.z) + 0.5,
),
));
} else if marker.starts_with("Elytra") {
chunk.add_structure_entity(self.item_frame_nbt(position));
}
}
fn item_frame_nbt(&self, position: Vector3<i32>) -> NbtCompound {
let facing = match self.descriptor.rotation {
Rotation::None => BlockDirection::South,
Rotation::Clockwise90 => BlockDirection::West,
Rotation::Rotate180 => BlockDirection::North,
Rotation::CounterClockwise90 => BlockDirection::East,
};
let offset = facing.to_offset();
let mut nbt = entity_nbt(
"minecraft:item_frame",
Vector3::new(
f64::from(position.x) + 0.5 - f64::from(offset.x) * 0.46875,
f64::from(position.y) + 0.5,
f64::from(position.z) + 0.5 - f64::from(offset.z) * 0.46875,
),
);
nbt.put_byte("Facing", facing.to_index() as i8);
nbt.put(
"block_pos",
NbtTag::IntArray(vec![position.x, position.y, position.z]),
);
nbt.put_byte("ItemRotation", 0);
nbt.child_tags.insert(
"Rotation".into(),
NbtTag::List(vec![
match facing {
BlockDirection::South => 0.0f32,
BlockDirection::West => 90.0,
BlockDirection::North => 180.0,
BlockDirection::East => 270.0,
BlockDirection::Down | BlockDirection::Up => unreachable!(),
}
.into(),
0.0f32.into(),
]),
);
let stack = ItemStack::new(1, &Item::ELYTRA);
let mut item = NbtCompound::new();
stack.write_item_stack(&mut item);
nbt.put_compound("Item", item);
nbt
}
}
impl StructurePieceBase for EndCityTemplatePiece {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn get_structure_piece(&self) -> &StructurePiece {
&self.piece
}
fn get_structure_piece_mut(&mut self) -> &mut StructurePiece {
&mut self.piece
}
fn place(
&mut self,
chunk: &mut ProtoChunk,
_block_registry: &dyn WorldPortalExt,
random: &mut RandomGenerator,
_seed: i64,
chunk_box: &BlockBox,
) {
self.place_blocks(chunk, random, chunk_box);
}
}
fn entity_nbt(id: &str, position: Vector3<f64>) -> NbtCompound {
let mut nbt = NbtCompound::new();
nbt.put_string("id", id.to_string());
nbt.put(
"Pos",
NbtTag::List(vec![
position.x.into(),
position.y.into(),
position.z.into(),
]),
);
nbt.put(
"Motion",
NbtTag::List(vec![0.0.into(), 0.0.into(), 0.0.into()]),
);
nbt.put("Rotation", NbtTag::List(vec![0.0f32.into(), 0.0f32.into()]));
nbt
}
#[cfg(test)]
mod tests {
use pumpkin_util::random::legacy_rand::LegacyRand;
use crate::generation::structure::{structures::HeightSampler, template::get_template};
use super::*;
struct RecordingHeightSampler {
calls: Vec<(i32, i32)>,
heights: std::vec::IntoIter<i32>,
}
impl HeightSampler for RecordingHeightSampler {
fn estimate_height(&mut self, block_x: i32, block_z: i32) -> i32 {
self.calls.push((block_x, block_z));
self.heights.next().unwrap()
}
}
#[test]
fn start_position_uses_java_five_by_five_corner_sampling() {
let seed = 42;
let mut expected_random = RandomGenerator::Legacy(LegacyRand::from_seed(seed));
let rotation = Rotation::from_index(expected_random.next_bounded_i32(4) as u8);
let (offset_x, offset_z) = match rotation {
Rotation::None => (5, 5),
Rotation::Clockwise90 => (-5, 5),
Rotation::Rotate180 => (-5, -5),
Rotation::CounterClockwise90 => (5, -5),
};
let mut sampler = RecordingHeightSampler {
calls: Vec::new(),
heights: vec![74, 70, 72, 73].into_iter(),
};
let position = EndCityGenerator
.get_structure_position(StructureGeneratorContext {
seed: seed as i64,
chunk_x: 3,
chunk_z: -2,
random: RandomGenerator::Legacy(LegacyRand::from_seed(seed)),
sea_level: 63,
min_y: 0,
height_sampler: Some(&mut sampler),
structure_key: None,
})
.unwrap();
let x = 3 * 16 + 7;
let z = -2 * 16 + 7;
assert_eq!(
sampler.calls,
vec![
(x, z),
(x, z + offset_z),
(x + offset_x, z),
(x + offset_x, z + offset_z),
]
);
assert_eq!(position.start_pos, BlockPos::new(x, 69, z));
}
#[test]
fn terrain_below_sixty_rejects_the_city() {
let mut sampler = RecordingHeightSampler {
calls: Vec::new(),
heights: vec![70, 59, 70, 70].into_iter(),
};
assert!(
EndCityGenerator
.get_structure_position(StructureGeneratorContext {
seed: 0,
chunk_x: 0,
chunk_z: 0,
random: RandomGenerator::Legacy(LegacyRand::from_seed(0)),
sea_level: 63,
min_y: 0,
height_sampler: Some(&mut sampler),
structure_key: None,
})
.is_none()
);
}
#[test]
fn elytra_frame_matches_vanilla_ship_marker() {
let descriptor = PieceDescriptor::new(
"ship",
Vector3::new(-4888, 123, -4393),
Rotation::CounterClockwise90,
true,
)
.unwrap();
let nbt =
EndCityTemplatePiece::new(descriptor).item_frame_nbt(Vector3::new(-4881, 128, -4399));
assert_eq!(
nbt.get_byte("Facing"),
Some(BlockDirection::East.to_index() as i8)
);
assert_eq!(
nbt.get_compound("Item")
.and_then(|item| item.get_string("id")),
Some("minecraft:elytra")
);
assert_eq!(
nbt.get_int_array("block_pos"),
Some(&[-4881, 128, -4399][..])
);
assert_eq!(
nbt.get_list("Pos"),
Some(&[(-4880.96875).into(), 128.5.into(), (-4398.5).into(),][..])
);
assert_eq!(
nbt.get_list("Rotation")
.and_then(|rotation| rotation[0].extract_float()),
Some(270.0)
);
}
#[test]
fn ship_dragon_head_uses_the_skull_block_entity() {
let ship = get_template("end_city/ship").unwrap();
assert!(ship.palette.iter().any(|palette| {
palette.name == "minecraft:dragon_wall_head"
&& get_block_entity_id(&palette.name) == Some("minecraft:skull")
}));
}
}

View File

@@ -283,6 +283,7 @@ pub(crate) fn get_block_entity_id(block_name: &str) -> Option<&'static str> {
"minecraft:smoker" => Some("minecraft:smoker"),
"minecraft:shulker_box" => Some("minecraft:shulker_box"),
"minecraft:bed" => Some("minecraft:bed"),
"minecraft:dragon_wall_head" => Some("minecraft:skull"),
"minecraft:sign"
| "minecraft:oak_sign"
| "minecraft:spruce_sign"