mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: Implement End City structures (#2761)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user