feat: Add Ocean Monument Structure Generation (#2737)

* feat: Implement ocean monument structures

* Fix monuments being cut in half

* Fix structure generation height handling
This commit is contained in:
ZlordHUN
2026-08-09 11:38:49 +02:00
committed by GitHub
parent 00b921c020
commit 8ea6fe9f9b
11 changed files with 2888 additions and 129 deletions

View File

@@ -360,17 +360,21 @@ mod tests {
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 {
for z in -4400..=-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, 0x5af3_06b3_536d_8053);
assert!(chunk.pending_block_entities.iter().any(|nbt| {
nbt.get_string("id") == Some("minecraft:skull")
&& nbt.get_int("x") == Some(-4888)

View File

@@ -180,6 +180,11 @@ impl TerrainCache {
}
impl ProtoChunk {
#[cfg(test)]
pub(crate) fn has_structure(&self, key: StructureKeys) -> bool {
self.structure_starts.contains_key(&key)
}
#[must_use]
pub fn new(x: i32, z: i32, generator: &super::generator::WorldGenerator) -> Self {
let dimension = generator.dimension();
@@ -348,7 +353,21 @@ impl ProtoChunk {
}
}
proto_chunk.stage = StagedChunkEnum::from(chunk_data.status);
let saved_stage = StagedChunkEnum::from(chunk_data.status);
proto_chunk.stage = saved_stage;
if let super::generator::WorldGenerator::Noise(generator) = generator
&& (StagedChunkEnum::StructureStart..StagedChunkEnum::Features).contains(&saved_stage)
{
// Structure starts and references are currently transient proto-chunk data.
// Rebuild them when resuming a partially generated chunk so structures that
// cross chunk boundaries are not truncated at the unload boundary.
proto_chunk.stage = StagedChunkEnum::Biomes;
proto_chunk.set_structure_starts(generator);
if saved_stage >= StagedChunkEnum::StructureReferences {
proto_chunk.set_structure_references(generator);
}
proto_chunk.stage = saved_stage;
}
proto_chunk
}
@@ -1300,7 +1319,7 @@ impl ProtoChunk {
global_cache,
settings.sea_level,
entry,
random_config,
generator,
&mut height_sampler,
);
}
@@ -1332,7 +1351,7 @@ impl ProtoChunk {
global_cache,
settings.sea_level,
selected_entry,
random_config,
generator,
&mut height_sampler,
) {
break;
@@ -1350,9 +1369,28 @@ impl ProtoChunk {
global_cache: &GlobalStructureCache,
sea_level: i32,
entry: &WeightedEntry,
random_config: &GlobalRandomConfig,
generator: &super::generator::VanillaGenerator,
height_sampler: &mut dyn crate::generation::structure::structures::HeightSampler,
) -> bool {
if entry.structure == StructureKeys::Monument {
let config = MultiNoiseSamplerBuilderOptions::new(0, 0, 0);
let mut sampler =
MultiNoiseSampler::generate(&generator.base_router.multi_noise, &config);
let center_x = chunk_pos::get_center_x(self.x);
let center_z = chunk_pos::get_center_z(self.z);
let start_y = height_sampler.estimate_ocean_floor_height(center_x, center_z);
if !crate::generation::structure::structures::ocean_monument::has_valid_biomes(
&MultiNoiseBiomeSupplier::OVERWORLD,
&mut sampler,
self.x,
self.z,
sea_level,
start_y,
) {
return false;
}
}
let chunk_x = self.x;
let chunk_z = self.z;
let position =
@@ -1361,7 +1399,7 @@ impl ProtoChunk {
try_generate_structure(
&entry.structure,
structure,
random_config.seed as i64,
generator.random_config.seed as i64,
self,
sea_level,
Some(height_sampler),

View File

@@ -6,6 +6,36 @@ mod test {
use pumpkin_data::dimension::Dimension;
use pumpkin_util::world_seed::Seed;
#[test]
fn structure_references_are_rebuilt_when_resuming_generation() {
use crate::chunk_system::chunk_state::Chunk;
use pumpkin_config::lighting::LightingEngineConfig;
use pumpkin_data::structures::StructureKeys;
let seed = Seed(1_782_124_772_053_846_960);
let world_gen = get_world_gen(seed, Dimension::OVERWORLD, false, Vec::new(), String::new());
let WorldGenerator::Noise(generator) = &*world_gen else {
unreachable!()
};
// This chunk contains the far edge of a monument starting at (-553, 173).
let mut proto = ProtoChunk::new(-553, 174, &world_gen);
proto.step_to_biomes(generator);
proto.set_structure_starts(generator);
proto.set_structure_references(generator);
assert!(proto.has_structure(StructureKeys::Monument));
let mut staged = Chunk::Proto(Box::new(proto));
staged.upgrade_to_level_chunk(&Dimension::OVERWORLD, &LightingEngineConfig::Default);
let Chunk::Level(chunk_data) = staged else {
unreachable!()
};
let resumed = ProtoChunk::from_chunk_data(&chunk_data, &world_gen);
assert_eq!(resumed.stage, StagedChunkEnum::StructureReferences);
assert!(resumed.has_structure(StructureKeys::Monument));
}
// Regression test for transposed heightmaps during Noise-stage chunk resume.
// Flat terrain cannot expose this bug, so use a sloped chunk.
#[test]

View File

@@ -1,4 +1,4 @@
use pumpkin_data::Block;
use pumpkin_data::{Block, BlockId, block_properties::blocks_movement};
use pumpkin_util::math::floor_div;
use rustc_hash::FxHashMap;
@@ -21,6 +21,7 @@ pub struct NoiseHeightSampler<'a> {
generator: &'a VanillaGenerator,
preliminary: SurfaceHeightEstimateSampler<'a>,
heights: FxHashMap<(i32, i32), i32>,
ocean_floor_heights: FxHashMap<(i32, i32), i32>,
}
impl<'a> NoiseHeightSampler<'a> {
@@ -42,10 +43,11 @@ impl<'a> NoiseHeightSampler<'a> {
generator,
preliminary,
heights: FxHashMap::default(),
ocean_floor_heights: FxHashMap::default(),
}
}
fn sample_column(&mut self, x: i32, z: i32) -> i32 {
fn sample_column(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32 {
let settings = self.generator.settings;
let shape = &settings.shape;
let horizontal = i32::from(shape.horizontal_cell_block_count());
@@ -100,7 +102,11 @@ impl<'a> NoiseHeightSampler<'a> {
&mut self.preliminary,
)
.unwrap_or(self.generator.default_block);
if !state.is_air() {
if if ocean_floor {
blocks_movement(state, BlockId::from_state_id(state.id))
} else {
!state.is_air()
} {
return y + 1;
}
}
@@ -116,8 +122,20 @@ impl HeightSampler for NoiseHeightSampler<'_> {
if let Some(height) = self.heights.get(&key) {
return *height;
}
let height = self.sample_column(block_x, block_z);
let height = self.sample_column(block_x, block_z, false);
self.heights.insert(key, height);
height
}
fn estimate_ocean_floor_height(&mut self, block_x: i32, block_z: i32) -> i32 {
let key = (block_x, block_z);
if let Some(height) = self.ocean_floor_heights.get(&key) {
return *height;
}
// Vanilla's structure helper asks for the highest occupied block, while
// heightmaps store the first free block above it.
let height = self.sample_column(block_x, block_z, true) - 1;
self.ocean_floor_heights.insert(key, height);
height
}
}

View File

@@ -11,14 +11,23 @@ use crate::{
noise::router::multi_noise_sampler::MultiNoiseSampler,
structure::structures::{
StructureGenerator, StructureGeneratorContext, StructurePosition,
buried_treasure::BuriedTreasureGenerator, create_chunk_random,
desert_pyramid::DesertPyramidGenerator, end_city::EndCityGenerator,
igloo::IglooGenerator, jigsaw::JigsawGenerator, jungle_temple::JungleTempleGenerator,
mansion::MansionGenerator, mineshaft::MineshaftGenerator,
nether_fortress::NetherFortressGenerator, nether_fossil::NetherFossilGenerator,
ocean_monument::OceanMonumentGenerator, ocean_ruin::OceanRuinGenerator,
ruined_portal::RuinedPortalGenerator, shipwreck::ShipwreckGenerator,
stronghold::StrongholdGenerator, swamp_hut::SwampHutGenerator,
buried_treasure::BuriedTreasureGenerator,
create_chunk_random,
desert_pyramid::DesertPyramidGenerator,
end_city::EndCityGenerator,
igloo::IglooGenerator,
jigsaw::JigsawGenerator,
jungle_temple::JungleTempleGenerator,
mansion::MansionGenerator,
mineshaft::MineshaftGenerator,
nether_fortress::NetherFortressGenerator,
nether_fossil::NetherFossilGenerator,
ocean_monument::{OceanMonumentGenerator, has_valid_biomes},
ocean_ruin::OceanRuinGenerator,
ruined_portal::RuinedPortalGenerator,
shipwreck::ShipwreckGenerator,
stronghold::StrongholdGenerator,
swamp_hut::SwampHutGenerator,
},
},
};
@@ -180,10 +189,31 @@ pub fn try_generate_structure(
pub fn lazily_generate_structure(
key: &StructureKeys,
structure: &Structure,
context: StructureGeneratorContext, // Replaces 5 separate arguments!
mut context: StructureGeneratorContext, // Replaces 5 separate arguments!
biome_supplier: &dyn BiomeSupplier,
multi_noise_sampler: &mut MultiNoiseSampler,
) -> Option<StructurePosition> {
if *key == StructureKeys::Monument {
let center_x = crate::generation::positions::chunk_pos::get_center_x(context.chunk_x);
let center_z = crate::generation::positions::chunk_pos::get_center_z(context.chunk_z);
let start_y = context
.height_sampler
.as_deref_mut()
.map_or(context.sea_level, |sampler| {
sampler.estimate_ocean_floor_height(center_x, center_z)
});
if !has_valid_biomes(
biome_supplier,
multi_noise_sampler,
context.chunk_x,
context.chunk_z,
context.sea_level,
start_y,
) {
return None;
}
}
let structure_pos = generate_structure_position(key, structure, context);
if let Some(pos) = structure_pos {

View File

@@ -149,7 +149,7 @@ impl StructurePiece {
}
}
const fn offset_pos(&self, x: i32, y: i32, z: i32) -> Vector3<i32> {
pub(crate) const fn offset_pos(&self, x: i32, y: i32, z: i32) -> Vector3<i32> {
Vector3::new(
self.apply_x_transform(x, z),
self.apply_y_transform(y),
@@ -672,6 +672,10 @@ pub trait StructureGenerator {
pub trait HeightSampler {
fn estimate_height(&mut self, block_x: i32, block_z: i32) -> i32;
fn estimate_ocean_floor_height(&mut self, block_x: i32, block_z: i32) -> i32 {
self.estimate_height(block_x, block_z)
}
}
impl HeightSampler

View File

@@ -1,109 +0,0 @@
use std::sync::Arc;
use pumpkin_data::Block;
use pumpkin_util::{
math::{block_box::BlockBox, position::BlockPos},
random::RandomGenerator,
};
use crate::{
ProtoChunk,
generation::{
positions::chunk_pos::{get_center_x, get_center_z},
structure::{
piece::StructurePieceType,
structures::{
StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase,
StructurePiecesCollector, StructurePosition, WorldPortalExt,
},
},
},
};
pub struct OceanMonumentGenerator;
impl StructureGenerator for OceanMonumentGenerator {
fn get_structure_position(
&self,
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 bounding_box = BlockBox::new(
chunk_center_x - 29,
context.min_y,
chunk_center_z - 29,
chunk_center_x + 29,
256,
chunk_center_z + 29,
);
let mut collector = StructurePiecesCollector::default();
collector.add_piece(Box::new(OceanMonumentPiece {
piece: StructurePiece::new(StructurePieceType::OceanMonumentBase, bounding_box, 0),
}));
Some(StructurePosition {
start_pos: BlockPos::new(chunk_center_x, 64, chunk_center_z),
collector: Arc::new(collector.into()),
})
}
}
pub struct OceanMonumentPiece {
piece: StructurePiece,
}
impl StructurePieceBase for OceanMonumentPiece {
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::OceanFloorWg, origin.x, origin.z);
let start_y = if sample_y <= 0 { 40 } else { sample_y - 4 };
// Draw a giant 58x58 pyramid/stepped prismarine temple!
for dy in 0..15 {
let half_sz = 29 - dy;
if half_sz < 0 {
break;
}
let min_x = origin.x - half_sz;
let max_x = origin.x + half_sz;
let min_z = origin.z - half_sz;
let max_z = origin.z + half_sz;
let y = start_y + dy;
for x in min_x..=max_x {
for z in min_z..=max_z {
if chunk_box.contains(x, y, z) {
let is_border = x == min_x || x == max_x || z == min_z || z == max_z;
let block = if is_border {
Block::DARK_PRISMARINE
} else if dy % 3 == 0 {
Block::PRISMARINE_BRICKS
} else {
Block::PRISMARINE
};
chunk.set_block_state(x, y, z, block.default_state);
}
}
}
}
}
}

View File

@@ -0,0 +1,586 @@
use pumpkin_util::{math::block_box::BlockBox, random::RandomGenerator};
use crate::ProtoChunk;
use super::{BASE_BLACK, BASE_GRAY, BASE_LIGHT, LAMP, MonumentBuilding};
impl MonumentBuilding {
pub(super) fn place_building(
&self,
chunk: &mut ProtoChunk,
random: &mut RandomGenerator,
chunk_box: &BlockBox,
) {
let sea_level = self.sea_level;
let water_height = sea_level.max(64) - self.base.piece.bounding_box.min.y;
self.base
.water(chunk, chunk_box, sea_level, 0, 0, 0, 58, water_height, 58);
self.generate_wing(chunk, chunk_box, false, 0);
self.generate_wing(chunk, chunk_box, true, 33);
self.generate_entrance_arches(chunk, chunk_box);
self.generate_entrance_wall(chunk, chunk_box);
self.generate_roof(chunk, chunk_box);
self.generate_lower_wall(chunk, chunk_box);
self.generate_middle_wall(chunk, chunk_box);
self.generate_upper_wall(chunk, chunk_box);
for pillar_x in 0..7 {
let mut pillar_z = 0;
while pillar_z < 7 {
if pillar_z == 0 && pillar_x == 3 {
pillar_z = 6;
}
let x = pillar_x * 9;
let z = pillar_z * 9;
for dx in 0..4 {
for dz in 0..4 {
self.base
.block(chunk, chunk_box, x + dx, 0, z + dz, BASE_LIGHT);
self.base.piece.fill_downwards(
chunk,
BASE_LIGHT,
x + dx,
-1,
z + dz,
chunk_box,
);
}
}
pillar_z += if pillar_x == 0 || pillar_x == 6 { 1 } else { 6 };
}
}
for offset in 0..5 {
self.base.water(
chunk,
chunk_box,
sea_level,
-1 - offset,
offset * 2,
-1 - offset,
-1 - offset,
23,
58 + offset,
);
self.base.water(
chunk,
chunk_box,
sea_level,
58 + offset,
offset * 2,
-1 - offset,
58 + offset,
23,
58 + offset,
);
self.base.water(
chunk,
chunk_box,
sea_level,
-offset,
offset * 2,
-1 - offset,
57 + offset,
23,
-1 - offset,
);
self.base.water(
chunk,
chunk_box,
sea_level,
-offset,
offset * 2,
58 + offset,
57 + offset,
23,
58 + offset,
);
}
for child in &self.children {
if child.piece.piece.bounding_box.intersects(chunk_box) {
child.place(chunk, random, chunk_box, sea_level, &self.graph);
}
}
}
fn generate_wing(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox, flipped: bool, x: i32) {
let p = &self.base;
if !p.chunk_intersects(chunk_box, x, 0, x + 23, 20) {
return;
}
p.fill(chunk, chunk_box, x, 0, 0, x + 24, 0, 20, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, x, 1, 0, x + 24, 10, 20);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
x + i,
i + 1,
i,
x + i,
i + 1,
20,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
x + i + 7,
i + 5,
i + 7,
x + i + 7,
i + 5,
20,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
x + 17 - i,
i + 5,
i + 7,
x + 17 - i,
i + 5,
20,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
x + 24 - i,
i + 1,
i,
x + 24 - i,
i + 1,
20,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
x + i + 1,
i + 1,
i,
x + 23 - i,
i + 1,
i,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
x + i + 8,
i + 5,
i + 7,
x + 16 - i,
i + 5,
i + 7,
BASE_LIGHT,
);
}
p.fill(chunk, chunk_box, x + 4, 4, 4, x + 6, 4, 20, BASE_GRAY);
p.fill(chunk, chunk_box, x + 7, 4, 4, x + 17, 4, 6, BASE_GRAY);
p.fill(chunk, chunk_box, x + 18, 4, 4, x + 20, 4, 20, BASE_GRAY);
p.fill(chunk, chunk_box, x + 11, 8, 11, x + 13, 8, 20, BASE_GRAY);
for z in [12, 15, 18] {
p.block(chunk, chunk_box, x + 12, 9, z, BASE_LIGHT);
}
let left = x + if flipped { 19 } else { 5 };
let right = x + if flipped { 5 } else { 19 };
for z in (5..=20).rev().step_by(3) {
p.block(chunk, chunk_box, left, 5, z, BASE_LIGHT);
}
for z in (7..=19).rev().step_by(3) {
p.block(chunk, chunk_box, right, 5, z, BASE_LIGHT);
}
for i in 0..4 {
let pos = if flipped {
x + 24 - (17 - i * 3)
} else {
x + 17 - i * 3
};
p.block(chunk, chunk_box, pos, 5, 5, BASE_LIGHT);
}
p.block(chunk, chunk_box, right, 5, 5, BASE_LIGHT);
p.fill(chunk, chunk_box, x + 11, 1, 12, x + 13, 7, 12, BASE_GRAY);
p.fill(chunk, chunk_box, x + 12, 1, 11, x + 12, 7, 13, BASE_GRAY);
}
fn generate_entrance_arches(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) {
let p = &self.base;
if !p.chunk_intersects(chunk_box, 22, 5, 35, 17) {
return;
}
p.water(chunk, chunk_box, self.sea_level, 25, 0, 0, 32, 8, 20);
for i in 0..4 {
let z = 5 + i * 4;
p.fill(chunk, chunk_box, 24, 2, z, 24, 4, z, BASE_LIGHT);
p.fill(chunk, chunk_box, 22, 4, z, 23, 4, z, BASE_LIGHT);
p.block(chunk, chunk_box, 25, 5, z, BASE_LIGHT);
p.block(chunk, chunk_box, 26, 6, z, BASE_LIGHT);
p.block(chunk, chunk_box, 26, 5, z, LAMP);
p.fill(chunk, chunk_box, 33, 2, z, 33, 4, z, BASE_LIGHT);
p.fill(chunk, chunk_box, 34, 4, z, 35, 4, z, BASE_LIGHT);
p.block(chunk, chunk_box, 32, 5, z, BASE_LIGHT);
p.block(chunk, chunk_box, 31, 6, z, BASE_LIGHT);
p.block(chunk, chunk_box, 31, 5, z, LAMP);
p.fill(chunk, chunk_box, 27, 6, z, 30, 6, z, BASE_GRAY);
}
}
fn generate_entrance_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) {
let p = &self.base;
if !p.chunk_intersects(chunk_box, 15, 20, 42, 21) {
return;
}
p.fill(chunk, chunk_box, 15, 0, 21, 42, 0, 21, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 26, 1, 21, 31, 3, 21);
for &(x0, y, x1) in &[
(21, 12, 36),
(17, 11, 40),
(16, 10, 41),
(15, 9, 42),
(15, 8, 42),
(15, 7, 42),
(16, 6, 41),
(17, 5, 40),
(21, 4, 36),
(22, 3, 26),
(31, 3, 35),
(23, 2, 25),
(32, 2, 34),
] {
p.fill(chunk, chunk_box, x0, y, 21, x1, y, 21, BASE_GRAY);
}
p.fill(chunk, chunk_box, 28, 4, 20, 29, 4, 21, BASE_LIGHT);
for &(x, y) in &[(27, 3), (30, 3), (26, 2), (31, 2), (25, 1), (32, 1)] {
p.block(chunk, chunk_box, x, y, 21, BASE_LIGHT);
}
for i in 0..7 {
p.block(chunk, chunk_box, 28 - i, 6 + i, 21, BASE_BLACK);
p.block(chunk, chunk_box, 29 + i, 6 + i, 21, BASE_BLACK);
}
for i in 0..4 {
p.block(chunk, chunk_box, 28 - i, 9 + i, 21, BASE_BLACK);
p.block(chunk, chunk_box, 29 + i, 9 + i, 21, BASE_BLACK);
}
p.block(chunk, chunk_box, 28, 12, 21, BASE_BLACK);
p.block(chunk, chunk_box, 29, 12, 21, BASE_BLACK);
for i in 0..3 {
for y in 8..=9 {
p.block(chunk, chunk_box, 22 - i * 2, y, 21, BASE_BLACK);
p.block(chunk, chunk_box, 35 + i * 2, y, 21, BASE_BLACK);
}
}
for &(x0, y0, x1, y1) in &[
(15, 13, 42, 15),
(15, 1, 15, 6),
(16, 1, 16, 5),
(17, 1, 20, 4),
(21, 1, 21, 3),
(22, 1, 22, 2),
(23, 1, 24, 1),
(42, 1, 42, 6),
(41, 1, 41, 5),
(37, 1, 40, 4),
(36, 1, 36, 3),
(33, 1, 34, 1),
(35, 1, 35, 2),
] {
p.water(chunk, chunk_box, self.sea_level, x0, y0, 21, x1, y1, 21);
}
}
fn generate_roof(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) {
let p = &self.base;
if !p.chunk_intersects(chunk_box, 21, 21, 36, 36) {
return;
}
p.fill(chunk, chunk_box, 21, 0, 22, 36, 0, 36, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 21, 1, 22, 36, 23, 36);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
21 + i,
13 + i,
21 + i,
36 - i,
13 + i,
21 + i,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
21 + i,
13 + i,
36 - i,
36 - i,
13 + i,
36 - i,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
21 + i,
13 + i,
22 + i,
21 + i,
13 + i,
35 - i,
BASE_LIGHT,
);
p.fill(
chunk,
chunk_box,
36 - i,
13 + i,
22 + i,
36 - i,
13 + i,
35 - i,
BASE_LIGHT,
);
}
p.fill(chunk, chunk_box, 25, 16, 25, 32, 16, 32, BASE_GRAY);
for &(x, z) in &[(25, 25), (32, 25), (25, 32), (32, 32)] {
p.fill(chunk, chunk_box, x, 17, z, x, 19, z, BASE_LIGHT);
}
for &(x, y, z, block) in &[
(26, 20, 26, BASE_LIGHT),
(27, 21, 27, BASE_LIGHT),
(27, 20, 27, LAMP),
(26, 20, 31, BASE_LIGHT),
(27, 21, 30, BASE_LIGHT),
(27, 20, 30, LAMP),
(31, 20, 31, BASE_LIGHT),
(30, 21, 30, BASE_LIGHT),
(30, 20, 30, LAMP),
(31, 20, 26, BASE_LIGHT),
(30, 21, 27, BASE_LIGHT),
(30, 20, 27, LAMP),
] {
p.block(chunk, chunk_box, x, y, z, block);
}
p.fill(chunk, chunk_box, 28, 21, 27, 29, 21, 27, BASE_GRAY);
p.fill(chunk, chunk_box, 27, 21, 28, 27, 21, 29, BASE_GRAY);
p.fill(chunk, chunk_box, 28, 21, 30, 29, 21, 30, BASE_GRAY);
p.fill(chunk, chunk_box, 30, 21, 28, 30, 21, 29, BASE_GRAY);
}
fn generate_lower_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) {
let p = &self.base;
if p.chunk_intersects(chunk_box, 0, 21, 6, 58) {
p.fill(chunk, chunk_box, 0, 0, 21, 6, 0, 57, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 0, 1, 21, 6, 7, 57);
p.fill(chunk, chunk_box, 4, 4, 21, 6, 4, 53, BASE_GRAY);
for i in 0..4 {
p.fill(chunk, chunk_box, i, i + 1, 21, i, i + 1, 57 - i, BASE_LIGHT);
}
for z in (23..53).step_by(3) {
p.block(chunk, chunk_box, 5, 5, z, BASE_LIGHT);
}
p.block(chunk, chunk_box, 5, 5, 52, BASE_LIGHT);
p.fill(chunk, chunk_box, 4, 1, 52, 6, 3, 52, BASE_GRAY);
p.fill(chunk, chunk_box, 5, 1, 51, 5, 3, 53, BASE_GRAY);
}
if p.chunk_intersects(chunk_box, 51, 21, 58, 58) {
p.fill(chunk, chunk_box, 51, 0, 21, 57, 0, 57, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 51, 1, 21, 57, 7, 57);
p.fill(chunk, chunk_box, 51, 4, 21, 53, 4, 53, BASE_GRAY);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
57 - i,
i + 1,
21,
57 - i,
i + 1,
57 - i,
BASE_LIGHT,
);
}
for z in (23..53).step_by(3) {
p.block(chunk, chunk_box, 52, 5, z, BASE_LIGHT);
}
p.block(chunk, chunk_box, 52, 5, 52, BASE_LIGHT);
p.fill(chunk, chunk_box, 51, 1, 52, 53, 3, 52, BASE_GRAY);
p.fill(chunk, chunk_box, 52, 1, 51, 52, 3, 53, BASE_GRAY);
}
if p.chunk_intersects(chunk_box, 0, 51, 57, 57) {
p.fill(chunk, chunk_box, 7, 0, 51, 50, 0, 57, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 7, 1, 51, 50, 10, 57);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
i + 1,
i + 1,
57 - i,
56 - i,
i + 1,
57 - i,
BASE_LIGHT,
);
}
}
}
fn generate_middle_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) {
let p = &self.base;
if p.chunk_intersects(chunk_box, 7, 21, 13, 50) {
p.fill(chunk, chunk_box, 7, 0, 21, 13, 0, 50, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 7, 1, 21, 13, 10, 50);
p.fill(chunk, chunk_box, 11, 8, 21, 13, 8, 53, BASE_GRAY);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
i + 7,
i + 5,
21,
i + 7,
i + 5,
54,
BASE_LIGHT,
);
}
for z in (21..=45).step_by(3) {
p.block(chunk, chunk_box, 12, 9, z, BASE_LIGHT);
}
}
if p.chunk_intersects(chunk_box, 44, 21, 50, 54) {
p.fill(chunk, chunk_box, 44, 0, 21, 50, 0, 50, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 44, 1, 21, 50, 10, 50);
p.fill(chunk, chunk_box, 44, 8, 21, 46, 8, 53, BASE_GRAY);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
50 - i,
i + 5,
21,
50 - i,
i + 5,
54,
BASE_LIGHT,
);
}
for z in (21..=45).step_by(3) {
p.block(chunk, chunk_box, 45, 9, z, BASE_LIGHT);
}
}
if p.chunk_intersects(chunk_box, 8, 44, 49, 54) {
p.fill(chunk, chunk_box, 14, 0, 44, 43, 0, 50, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 14, 1, 44, 43, 10, 50);
for x in (12..=45).step_by(3) {
p.block(chunk, chunk_box, x, 9, 45, BASE_LIGHT);
p.block(chunk, chunk_box, x, 9, 52, BASE_LIGHT);
if [12, 18, 24, 33, 39, 45].contains(&x) {
for &(y, z) in &[
(9, 47),
(9, 50),
(10, 45),
(10, 46),
(10, 51),
(10, 52),
(11, 47),
(11, 50),
(12, 48),
(12, 49),
] {
p.block(chunk, chunk_box, x, y, z, BASE_LIGHT);
}
}
}
for i in 0..3 {
p.fill(
chunk,
chunk_box,
8 + i,
5 + i,
54,
49 - i,
5 + i,
54,
BASE_GRAY,
);
}
p.fill(chunk, chunk_box, 11, 8, 54, 46, 8, 54, BASE_LIGHT);
p.fill(chunk, chunk_box, 14, 8, 44, 43, 8, 53, BASE_GRAY);
}
}
fn generate_upper_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) {
let p = &self.base;
if p.chunk_intersects(chunk_box, 14, 21, 20, 43) {
p.fill(chunk, chunk_box, 14, 0, 21, 20, 0, 43, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 14, 1, 22, 20, 14, 43);
p.fill(chunk, chunk_box, 18, 12, 22, 20, 12, 39, BASE_GRAY);
p.fill(chunk, chunk_box, 18, 12, 21, 20, 12, 21, BASE_LIGHT);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
i + 14,
i + 9,
21,
i + 14,
i + 9,
43 - i,
BASE_LIGHT,
);
}
for z in (23..=39).step_by(3) {
p.block(chunk, chunk_box, 19, 13, z, BASE_LIGHT);
}
}
if p.chunk_intersects(chunk_box, 37, 21, 43, 43) {
p.fill(chunk, chunk_box, 37, 0, 21, 43, 0, 43, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 37, 1, 22, 43, 14, 43);
p.fill(chunk, chunk_box, 37, 12, 22, 39, 12, 39, BASE_GRAY);
p.fill(chunk, chunk_box, 37, 12, 21, 39, 12, 21, BASE_LIGHT);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
43 - i,
i + 9,
21,
43 - i,
i + 9,
43 - i,
BASE_LIGHT,
);
}
for z in (23..=39).step_by(3) {
p.block(chunk, chunk_box, 38, 13, z, BASE_LIGHT);
}
}
if p.chunk_intersects(chunk_box, 15, 37, 42, 43) {
p.fill(chunk, chunk_box, 21, 0, 37, 36, 0, 43, BASE_GRAY);
p.water(chunk, chunk_box, self.sea_level, 21, 1, 37, 36, 14, 43);
p.fill(chunk, chunk_box, 21, 12, 37, 36, 12, 39, BASE_GRAY);
for i in 0..4 {
p.fill(
chunk,
chunk_box,
15 + i,
i + 9,
43 - i,
42 - i,
i + 9,
43 - i,
BASE_LIGHT,
);
}
for x in (21..=36).step_by(3) {
p.block(chunk, chunk_box, x, 13, 38, BASE_LIGHT);
}
}
}
}

View File

@@ -0,0 +1,276 @@
use pumpkin_util::{
BlockDirection,
random::{RandomGenerator, RandomImpl},
};
const GRID_SIZE: usize = 75;
const SOURCE_INDEX: i32 = room_index(2, 0, 0);
const TOP_CONNECT_INDEX: i32 = room_index(2, 2, 0);
const LEFT_CONNECT_INDEX: i32 = room_index(0, 1, 0);
const RIGHT_CONNECT_INDEX: i32 = room_index(4, 1, 0);
#[derive(Clone)]
pub(super) struct RoomDefinition {
pub index: i32,
pub connections: [Option<usize>; 6],
pub openings: [bool; 6],
pub claimed: bool,
source: bool,
}
impl RoomDefinition {
const fn new(index: i32) -> Self {
Self {
index,
connections: [None; 6],
openings: [false; 6],
claimed: false,
source: false,
}
}
pub const fn is_special(&self) -> bool {
self.index >= 75
}
pub fn count_openings(&self) -> usize {
self.openings.iter().filter(|opening| **opening).count()
}
}
pub(super) struct RoomGraph {
pub rooms: Vec<RoomDefinition>,
pub source: usize,
pub core: usize,
}
impl RoomGraph {
#[expect(clippy::too_many_lines)]
pub fn generate(random: &mut RandomGenerator) -> (Self, Vec<usize>) {
let mut graph = Self {
rooms: Vec::with_capacity(49),
source: 0,
core: 0,
};
let mut grid = [None; GRID_SIZE];
for y in 0..=1 {
for z in 0..4 {
for x in 0..5 {
let index = room_index(x, y, z);
grid[index as usize] = Some(graph.push(index));
}
}
}
for z in 0..2 {
for x in 1..4 {
let index = room_index(x, 2, z);
grid[index as usize] = Some(graph.push(index));
}
}
graph.source = grid[SOURCE_INDEX as usize].unwrap();
for x in 0..5 {
for z in 0..5 {
for y in 0..3 {
let index = room_index(x, y, z);
let Some(room) = grid[index as usize] else {
continue;
};
for direction in DIRECTIONS {
let step = direction.to_vector();
let nx = x + step.x;
let ny = y + step.y;
let nz = z + step.z;
if !(0..5).contains(&nx) || !(0..3).contains(&ny) || !(0..5).contains(&nz) {
continue;
}
let Some(neighbor) = grid[room_index(nx, ny, nz) as usize] else {
continue;
};
let connection_direction = if nz == z {
direction
} else {
direction.opposite()
};
graph.connect(room, connection_direction, neighbor);
}
}
}
}
let roof = graph.push(1003);
let left_wing = graph.push(1001);
let right_wing = graph.push(1002);
graph.connect(
grid[TOP_CONNECT_INDEX as usize].unwrap(),
BlockDirection::Up,
roof,
);
graph.connect(
grid[LEFT_CONNECT_INDEX as usize].unwrap(),
BlockDirection::South,
left_wing,
);
graph.connect(
grid[RIGHT_CONNECT_INDEX as usize].unwrap(),
BlockDirection::South,
right_wing,
);
graph.rooms[roof].claimed = true;
graph.rooms[left_wing].claimed = true;
graph.rooms[right_wing].claimed = true;
graph.rooms[graph.source].source = true;
graph.core = grid[room_index(random.next_bounded_i32(4), 0, 2) as usize].unwrap();
graph.claim_core();
for room in &mut graph.rooms {
// Vanilla updates every grid room and the roof room here. The two wing
// sentinels deliberately keep their side closed so that edge pruning
// cannot disconnect their grid attachment.
if !room.is_special() || room.index == 1003 {
for direction in DIRECTIONS {
room.openings[direction as usize] =
room.connections[direction as usize].is_some();
}
}
}
let mut order = grid.into_iter().flatten().collect::<Vec<_>>();
shuffle(&mut order, random);
for &room in &order {
let mut closed = 0;
for _ in 0..5 {
if closed == 2 {
break;
}
let direction = random.next_bounded_i32(6) as usize;
if !graph.rooms[room].openings[direction] {
continue;
}
let neighbor = graph.rooms[room].connections[direction].unwrap();
let opposite = opposite_index(direction);
graph.rooms[room].openings[direction] = false;
graph.rooms[neighbor].openings[opposite] = false;
if graph.find_source(room) && graph.find_source(neighbor) {
closed += 1;
} else {
graph.rooms[room].openings[direction] = true;
graph.rooms[neighbor].openings[opposite] = true;
}
}
}
order.extend([roof, left_wing, right_wing]);
(graph, order)
}
fn push(&mut self, index: i32) -> usize {
let room = self.rooms.len();
self.rooms.push(RoomDefinition::new(index));
room
}
fn connect(&mut self, room: usize, direction: BlockDirection, neighbor: usize) {
self.rooms[room].connections[direction as usize] = Some(neighbor);
self.rooms[neighbor].connections[direction.opposite() as usize] = Some(room);
}
fn claim_core(&mut self) {
let east = self.connection(self.core, BlockDirection::East);
let north = self.connection(self.core, BlockDirection::North);
let north_east = self.connection(east, BlockDirection::North);
let up = self.connection(self.core, BlockDirection::Up);
let east_up = self.connection(east, BlockDirection::Up);
let north_up = self.connection(north, BlockDirection::Up);
let north_east_up = self.connection(north_east, BlockDirection::Up);
for room in [
self.core,
east,
north,
north_east,
up,
east_up,
north_up,
north_east_up,
] {
self.rooms[room].claimed = true;
}
}
fn find_source(&self, start: usize) -> bool {
let mut visited = vec![false; self.rooms.len()];
let mut pending = vec![start];
while let Some(room) = pending.pop() {
if std::mem::replace(&mut visited[room], true) {
continue;
}
if self.rooms[room].source {
return true;
}
for direction in 0..6 {
if self.rooms[room].openings[direction]
&& let Some(neighbor) = self.rooms[room].connections[direction]
{
pending.push(neighbor);
}
}
}
false
}
pub fn connection(&self, room: usize, direction: BlockDirection) -> usize {
self.rooms[room].connections[direction as usize].unwrap()
}
}
const DIRECTIONS: [BlockDirection; 6] = [
BlockDirection::Down,
BlockDirection::Up,
BlockDirection::North,
BlockDirection::South,
BlockDirection::West,
BlockDirection::East,
];
const fn room_index(x: i32, y: i32, z: i32) -> i32 {
y * 25 + z * 5 + x
}
const fn opposite_index(direction: usize) -> usize {
[1, 0, 3, 2, 5, 4][direction]
}
fn shuffle(values: &mut [usize], random: &mut RandomGenerator) {
for index in (1..values.len()).rev() {
values.swap(index, random.next_bounded_i32(index as i32 + 1) as usize);
}
}
#[cfg(test)]
mod tests {
use pumpkin_util::random::{RandomGenerator, legacy_rand::LegacyRand};
use super::RoomGraph;
#[test]
fn generated_graph_keeps_every_room_connected() {
for seed in 0..128 {
let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(seed));
let (graph, order) = RoomGraph::generate(&mut random);
assert_eq!(graph.rooms.len(), 49);
assert_eq!(order.len(), 49);
assert_eq!(
graph.rooms.iter().filter(|room| room.is_special()).count(),
3
);
assert!(
graph
.rooms
.iter()
.enumerate()
.filter(|(_, definition)| !definition.is_special())
.all(|(room, _)| graph.find_source(room))
);
}
}
}

View File

@@ -0,0 +1,652 @@
mod building;
mod graph;
mod rooms;
use std::sync::{Arc, Mutex};
use pumpkin_data::{Block, BlockState};
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
use pumpkin_util::{
BlockDirection,
math::{block_box::BlockBox, position::BlockPos, vector3::Vector3},
random::{RandomGenerator, RandomImpl},
};
use crate::{
ProtoChunk,
biome::BiomeSupplier,
generation::{
biome_coords,
noise::router::multi_noise_sampler::MultiNoiseSampler,
positions::chunk_pos::{get_center_x, get_center_z, start_block_x, start_block_z},
structure::{
piece::StructurePieceType,
structures::{
StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase,
StructurePiecesCollector, StructurePosition, WorldPortalExt,
},
},
},
};
use self::{
graph::RoomGraph,
rooms::{RoomKind, RoomPiece},
};
const BASE_GRAY: &BlockState = Block::PRISMARINE.default_state;
const BASE_LIGHT: &BlockState = Block::PRISMARINE_BRICKS.default_state;
const BASE_BLACK: &BlockState = Block::DARK_PRISMARINE.default_state;
const LAMP: &BlockState = Block::SEA_LANTERN.default_state;
const WATER: &BlockState = Block::WATER.default_state;
pub struct OceanMonumentGenerator;
pub(crate) fn has_valid_biomes(
biome_supplier: &dyn BiomeSupplier,
sampler: &mut MultiNoiseSampler,
chunk_x: i32,
chunk_z: i32,
sea_level: i32,
start_y: i32,
) -> bool {
let start_x = get_center_x(chunk_x);
let start_z = get_center_z(chunk_z);
let start_biomes = pumpkin_data::tag::WorldgenBiome::MINECRAFT_HAS_STRUCTURE_OCEAN_MONUMENT.1;
if !start_biomes.contains(
&(biome_supplier
.biome(
biome_coords::from_block(start_x),
biome_coords::from_block(start_y),
biome_coords::from_block(start_z),
sampler,
)
.id as u16),
) {
return false;
}
let center_x = start_block_x(chunk_x) + 9;
let center_z = start_block_z(chunk_z) + 9;
let min_x = biome_coords::from_block(center_x - 29);
let max_x = biome_coords::from_block(center_x + 29);
let min_y = biome_coords::from_block(sea_level - 29);
let max_y = biome_coords::from_block(sea_level + 29);
let min_z = biome_coords::from_block(center_z - 29);
let max_z = biome_coords::from_block(center_z + 29);
let allowed = pumpkin_data::tag::WorldgenBiome::MINECRAFT_REQUIRED_OCEAN_MONUMENT_SURROUNDING.1;
(min_x..=max_x).all(|x| {
(min_z..=max_z).all(|z| {
(min_y..=max_y)
.all(|y| allowed.contains(&(biome_supplier.biome(x, y, z, sampler).id as u16)))
})
})
}
impl StructureGenerator for OceanMonumentGenerator {
fn get_structure_position(
&self,
mut context: StructureGeneratorContext<'_>,
) -> Option<StructurePosition> {
let center_x = get_center_x(context.chunk_x);
let center_z = get_center_z(context.chunk_z);
let start_y = context
.height_sampler
.as_deref_mut()
.map_or(context.sea_level, |sampler| {
sampler.estimate_ocean_floor_height(center_x, center_z)
});
let direction = BlockDirection::get_random_horizontal_direction(&mut context.random);
let building = MonumentBuilding::new(
&mut context.random,
start_block_x(context.chunk_x) - 29,
start_block_z(context.chunk_z) - 29,
direction,
context.sea_level,
);
let mut collector = StructurePiecesCollector::default();
collector.add_piece(Box::new(building));
Some(StructurePosition {
start_pos: BlockPos::new(center_x, start_y, center_z),
collector: Arc::new(Mutex::new(collector)),
})
}
}
struct MonumentBuilding {
base: MonumentPiece,
graph: RoomGraph,
children: Vec<RoomPiece>,
sea_level: i32,
}
impl MonumentBuilding {
fn new(
random: &mut RandomGenerator,
west: i32,
north: i32,
direction: BlockDirection,
sea_level: i32,
) -> Self {
let mut piece = StructurePiece::new(
StructurePieceType::OceanMonumentBase,
BlockBox::new(west, 39, north, west + 57, 61, north + 57),
0,
);
piece.set_facing(Some(direction));
let base = MonumentPiece { piece };
let (mut graph, room_order) = RoomGraph::generate(random);
graph.rooms[graph.source].claimed = true;
let mut children = vec![
RoomPiece::for_room(
RoomKind::Entry,
direction,
graph.source,
graph.rooms[graph.source].index,
),
RoomPiece::for_room(
RoomKind::Core,
direction,
graph.core,
graph.rooms[graph.core].index,
),
];
for room in room_order {
if graph.rooms[room].claimed || graph.rooms[room].is_special() {
continue;
}
children.push(RoomPiece::fit(random, direction, room, &mut graph));
}
let room_offset = base.piece.offset_pos(9, 0, 22);
for child in &mut children {
child
.piece
.piece
.bounding_box
.move_pos(room_offset.x, room_offset.y, room_offset.z);
}
let left_wing = box_from_corners(
base.piece.offset_pos(1, 1, 1),
base.piece.offset_pos(23, 8, 21),
);
let right_wing = box_from_corners(
base.piece.offset_pos(34, 1, 1),
base.piece.offset_pos(56, 8, 21),
);
let penthouse = box_from_corners(
base.piece.offset_pos(22, 13, 22),
base.piece.offset_pos(35, 17, 35),
);
let wing_design = random.next_i32();
children.push(RoomPiece::special(
RoomKind::Wing(wing_design),
direction,
left_wing,
));
children.push(RoomPiece::special(
RoomKind::Wing(wing_design.wrapping_add(1)),
direction,
right_wing,
));
children.push(RoomPiece::special(
RoomKind::Penthouse,
direction,
penthouse,
));
Self {
base,
graph,
children,
sea_level,
}
}
}
impl StructurePieceBase for MonumentBuilding {
fn get_structure_piece(&self) -> &StructurePiece {
&self.base.piece
}
fn get_structure_piece_mut(&mut self) -> &mut StructurePiece {
&mut self.base.piece
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn place(
&mut self,
chunk: &mut ProtoChunk,
_block_registry: &dyn WorldPortalExt,
random: &mut RandomGenerator,
_seed: i64,
chunk_box: &BlockBox,
) {
self.place_building(chunk, random, chunk_box);
}
}
struct MonumentPiece {
piece: StructurePiece,
}
impl MonumentPiece {
const fn for_room(
kind: RoomKind,
direction: BlockDirection,
room_index: i32,
width: i32,
height: i32,
depth: i32,
) -> Self {
let room_x = room_index % 5;
let room_z = room_index / 5 % 5;
let room_y = room_index / 25;
let mut bounding_box = match direction {
BlockDirection::North | BlockDirection::South => {
BlockBox::new(0, 0, 0, width * 8 - 1, height * 4 - 1, depth * 8 - 1)
}
_ => BlockBox::new(0, 0, 0, depth * 8 - 1, height * 4 - 1, width * 8 - 1),
};
match direction {
BlockDirection::North => {
bounding_box.move_pos(room_x * 8, room_y * 4, -(room_z + depth) * 8 + 1);
}
BlockDirection::South => {
bounding_box.move_pos(room_x * 8, room_y * 4, room_z * 8);
}
BlockDirection::West => {
bounding_box.move_pos(-(room_z + depth) * 8 + 1, room_y * 4, room_x * 8);
}
_ => {
bounding_box.move_pos(room_z * 8, room_y * 4, room_x * 8);
}
}
let mut piece = StructurePiece::new(kind.piece_type(), bounding_box, 1);
piece.set_facing(Some(direction));
Self { piece }
}
const fn special(kind: RoomKind, direction: BlockDirection, bounding_box: BlockBox) -> Self {
let mut piece = StructurePiece::new(kind.piece_type(), bounding_box, 1);
piece.set_facing(Some(direction));
Self { piece }
}
#[expect(clippy::too_many_arguments)]
fn fill(
&self,
chunk: &mut ProtoChunk,
chunk_box: &BlockBox,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
block: &BlockState,
) {
self.piece
.fill(chunk, chunk_box, x0, y0, z0, x1, y1, z1, block);
}
fn block(
&self,
chunk: &mut ProtoChunk,
chunk_box: &BlockBox,
x: i32,
y: i32,
z: i32,
block: &BlockState,
) {
self.piece.add_block(chunk, block, x, y, z, chunk_box);
}
#[expect(clippy::too_many_arguments)]
fn water(
&self,
chunk: &mut ProtoChunk,
chunk_box: &BlockBox,
sea_level: i32,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
) {
for y in y0..=y1 {
for x in x0..=x1 {
for z in z0..=z1 {
let state = self.piece.get_block_at(chunk, x, y, z, chunk_box);
let block_id = state.id.to_block_id();
if [
Block::ICE.id,
Block::PACKED_ICE.id,
Block::BLUE_ICE.id,
Block::WATER.id,
]
.contains(&block_id)
{
continue;
}
let state = if self.piece.offset_pos(x, y, z).y >= sea_level {
Block::AIR.default_state
} else {
WATER
};
self.block(chunk, chunk_box, x, y, z, state);
}
}
}
}
fn default_floor(
&self,
chunk: &mut ProtoChunk,
chunk_box: &BlockBox,
x: i32,
z: i32,
down_opening: bool,
) {
if !down_opening {
self.fill(chunk, chunk_box, x, 0, z, x + 7, 0, z + 7, BASE_GRAY);
return;
}
self.fill(chunk, chunk_box, x, 0, z, x + 2, 0, z + 7, BASE_GRAY);
self.fill(chunk, chunk_box, x + 5, 0, z, x + 7, 0, z + 7, BASE_GRAY);
self.fill(chunk, chunk_box, x + 3, 0, z, x + 4, 0, z + 2, BASE_GRAY);
self.fill(
chunk,
chunk_box,
x + 3,
0,
z + 5,
x + 4,
0,
z + 7,
BASE_GRAY,
);
self.fill(
chunk,
chunk_box,
x + 3,
0,
z + 2,
x + 4,
0,
z + 2,
BASE_LIGHT,
);
self.fill(
chunk,
chunk_box,
x + 3,
0,
z + 5,
x + 4,
0,
z + 5,
BASE_LIGHT,
);
self.fill(
chunk,
chunk_box,
x + 2,
0,
z + 3,
x + 2,
0,
z + 4,
BASE_LIGHT,
);
self.fill(
chunk,
chunk_box,
x + 5,
0,
z + 3,
x + 5,
0,
z + 4,
BASE_LIGHT,
);
}
#[expect(clippy::too_many_arguments)]
fn fill_on_water(
&self,
chunk: &mut ProtoChunk,
chunk_box: &BlockBox,
x0: i32,
y0: i32,
z0: i32,
x1: i32,
y1: i32,
z1: i32,
block: &BlockState,
) {
for y in y0..=y1 {
for x in x0..=x1 {
for z in z0..=z1 {
if self
.piece
.get_block_at(chunk, x, y, z, chunk_box)
.id
.to_block_id()
== Block::WATER.id
{
self.block(chunk, chunk_box, x, y, z, block);
}
}
}
}
}
fn chunk_intersects(&self, chunk_box: &BlockBox, x0: i32, z0: i32, x1: i32, z1: i32) -> bool {
let first = self.piece.offset_pos(x0, 0, z0);
let second = self.piece.offset_pos(x1, 0, z1);
chunk_box.intersects_raw_xz(
first.x.min(second.x),
first.z.min(second.z),
first.x.max(second.x),
first.z.max(second.z),
)
}
fn spawn_elder(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox, x: i32, y: i32, z: i32) {
let pos = self.piece.offset_pos(x, y, z);
if !chunk_box.contains_pos(&pos) {
return;
}
chunk.add_structure_entity(elder_guardian_nbt(pos));
}
}
fn elder_guardian_nbt(pos: Vector3<i32>) -> NbtCompound {
let mut nbt = NbtCompound::new();
nbt.put_string("id", "minecraft:elder_guardian".to_string());
nbt.put(
"Pos",
NbtTag::List(vec![
(f64::from(pos.x) + 0.5).into(),
f64::from(pos.y).into(),
(f64::from(pos.z) + 0.5).into(),
]),
);
nbt.put(
"Motion",
NbtTag::List(vec![0.0f64.into(), 0.0f64.into(), 0.0f64.into()]),
);
nbt.put("Rotation", NbtTag::List(vec![0.0f32.into(), 0.0f32.into()]));
nbt.put_float("Health", 80.0);
nbt.put_bool("PersistenceRequired", true);
nbt
}
fn box_from_corners(first: Vector3<i32>, second: Vector3<i32>) -> BlockBox {
BlockBox::new(
first.x.min(second.x),
first.y.min(second.y),
first.z.min(second.z),
first.x.max(second.x),
first.y.max(second.y),
first.z.max(second.z),
)
}
#[cfg(test)]
mod tests {
use pumpkin_data::{Block, dimension::Dimension};
use pumpkin_util::{
BlockDirection,
math::block_box::BlockBox,
random::{RandomGenerator, legacy_rand::LegacyRand},
world_seed::Seed,
};
use crate::{
ProtoChunk,
generation::{
get_world_gen,
positions::chunk_pos::{start_block_x, start_block_z},
structure::structures::{HeightSampler, StructureGenerator, StructureGeneratorContext},
},
};
use super::{MonumentBuilding, OceanMonumentGenerator, elder_guardian_nbt};
struct OceanFloorSampler;
impl HeightSampler for OceanFloorSampler {
fn estimate_height(&mut self, _block_x: i32, _block_z: i32) -> i32 {
panic!("monuments must not use the world-surface heightmap");
}
fn estimate_ocean_floor_height(&mut self, _block_x: i32, _block_z: i32) -> i32 {
37
}
}
#[test]
fn structure_start_uses_the_ocean_floor_heightmap() {
let mut height_sampler = OceanFloorSampler;
let context = StructureGeneratorContext {
seed: 0,
chunk_x: 2,
chunk_z: -3,
random: RandomGenerator::Legacy(LegacyRand::from_seed(0)),
sea_level: 63,
min_y: -64,
height_sampler: Some(&mut height_sampler),
structure_key: None,
};
let position = OceanMonumentGenerator
.get_structure_position(context)
.unwrap();
assert_eq!(position.start_pos.0.y, 37);
}
#[test]
fn orientation_does_not_move_the_monument_origin() {
for direction in [
BlockDirection::North,
BlockDirection::South,
BlockDirection::West,
BlockDirection::East,
] {
let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(0));
let building = MonumentBuilding::new(&mut random, 100, 200, direction, 63);
let bounds = building.base.piece.bounding_box;
assert_eq!((bounds.min.x, bounds.min.y, bounds.min.z), (100, 39, 200));
assert_eq!((bounds.max.x, bounds.max.y, bounds.max.z), (157, 61, 257));
assert!(building.children.iter().all(|child| {
let child = child.piece.piece.bounding_box;
child.min.x >= bounds.min.x
&& child.min.y >= bounds.min.y
&& child.min.z >= bounds.min.z
&& child.max.x <= bounds.max.x
&& child.max.y <= bounds.max.y
&& child.max.z <= bounds.max.z
}));
}
}
#[test]
fn elder_guardian_nbt_is_accepted_by_the_entity_loader() {
let nbt = elder_guardian_nbt(pumpkin_util::math::vector3::Vector3::new(1, 2, 3));
assert_eq!(nbt.get_string("id"), Some("minecraft:elder_guardian"));
assert_eq!(nbt.get_list("Pos").unwrap().len(), 3);
assert_eq!(nbt.get_list("Motion").unwrap().len(), 3);
assert_eq!(nbt.get_list("Rotation").unwrap().len(), 2);
assert_eq!(nbt.get_float("Health"), Some(80.0));
}
#[test]
fn every_orientation_places_the_complete_treasure_core() {
let world_gen = get_world_gen(
Seed(0),
Dimension::OVERWORLD,
false,
Vec::new(),
String::new(),
);
for direction in [
BlockDirection::North,
BlockDirection::South,
BlockDirection::West,
BlockDirection::East,
] {
let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(0));
let building = MonumentBuilding::new(&mut random, 0, 0, direction, 63);
let bounds = building.base.piece.bounding_box;
let mut gold_blocks = 0;
for chunk_x in bounds.min.x.div_euclid(16)..=bounds.max.x.div_euclid(16) {
for chunk_z in bounds.min.z.div_euclid(16)..=bounds.max.z.div_euclid(16) {
let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen);
let chunk_box = BlockBox::new(
start_block_x(chunk_x),
chunk.bottom_y() as i32,
start_block_z(chunk_z),
start_block_x(chunk_x) + 15,
chunk.bottom_y() as i32 + chunk.height() as i32 - 1,
start_block_z(chunk_z) + 15,
);
let mut placement_random = RandomGenerator::Legacy(LegacyRand::from_seed(1));
building.place_building(&mut chunk, &mut placement_random, &chunk_box);
for x in 0..16 {
for y in 0..chunk.height() as i32 {
for z in 0..16 {
if chunk
.get_block_state_raw(x, y, z)
.to_state()
.id
.to_block_id()
== Block::GOLD_BLOCK.id
{
gold_blocks += 1;
}
}
}
}
}
}
assert_eq!(gold_blocks, 8);
}
}
}

File diff suppressed because it is too large Load Diff