feat: aquifer sample and top material in cave carver (#2242)

* sin/cos util

* 4 nieghbor check

* formating

* air helper

* carver wrapper

* fmt

* clippy

* clipp2

* remove local y

* out of bound guard

* fluid tick

* surface rule

* wire up

* math correct

* start fix

* bring back xoroshiro oops

* scheduler guard

* simplify test

* clean test

* comment

* crash fix

* clippy

* clippy

* i hate you codex

* sin cos

* im stupid f32
This commit is contained in:
ChocoDev
2026-07-05 20:55:43 +07:00
committed by GitHub
parent 3aea5ee720
commit da2401d07d
17 changed files with 1246 additions and 368 deletions

View File

@@ -29,7 +29,6 @@ use std::error::Error;
use std::fmt::Debug;
use std::str::FromStr;
use std::{
collections::HashMap,
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
@@ -2347,8 +2346,9 @@ impl DataComponentImpl for ContainerImpl {
default_impl!(Container);
}
#[derive(Clone, Debug)]
#[allow(clippy::disallowed_types)]
pub struct BlockStateImpl {
pub properties: HashMap<String, String>,
pub properties: std::collections::HashMap<String, String>,
}
impl PartialEq for BlockStateImpl {
fn eq(&self, other: &Self) -> bool {
@@ -2367,9 +2367,10 @@ impl std::hash::Hash for BlockStateImpl {
}
}
impl BlockStateImpl {
#[allow(clippy::disallowed_types)]
fn read_data(data: &NbtTag) -> Option<Self> {
let compound = data.extract_compound()?;
let mut properties = HashMap::new();
let mut properties = std::collections::HashMap::new();
for (key, val) in compound.child_tags.iter() {
if let Some(s) = val.extract_string() {
properties.insert(key.to_string(), s.to_string());

View File

@@ -1,4 +1,5 @@
use num_traits::{Float, One, PrimInt, Zero};
use std::sync::LazyLock;
pub mod atomic_f32;
pub mod bit_storage;
@@ -15,6 +16,25 @@ pub mod vector2;
pub mod vector3;
pub mod vertical_surface_type;
const SIN_SCALE: f32 = 10430.378; // 65536 / 2P
const SIN_MASK: i32 = 65535;
static SIN: LazyLock<[f32; 65536]> = LazyLock::new(|| {
std::array::from_fn(|i| (f64::from(i as u32) / 10430.378350470453).sin() as f32)
});
/// Returns the vanilla sine table value for an angle in radians.
#[must_use]
pub fn sin(value: f32) -> f32 {
SIN[((value * SIN_SCALE) as i32 & SIN_MASK) as usize]
}
/// Returns the vanilla cosine table value for an angle in radians.
#[must_use]
pub fn cos(value: f32) -> f32 {
SIN[((value * SIN_SCALE + 16384.0) as i32 & SIN_MASK) as usize]
}
/// Wraps an angle in degrees to the range [-180, 180).
///
/// # Arguments

View File

@@ -16,7 +16,7 @@ fn main() {
);
let mut pool_code = String::from(
"
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
#[must_use]
pub fn get_pool_elements(pool_id: &str) -> Option<&'static [&'static str]> {\n match pool_id {\n",

View File

@@ -309,7 +309,7 @@ impl Chunk {
z: proto_chunk.z,
dirty: AtomicBool::new(true),
block_ticks: ChunkTickScheduler::default(),
fluid_ticks: ChunkTickScheduler::default(),
fluid_ticks: ChunkTickScheduler::from_iter(proto_chunk.fluid_ticks),
pending_block_entities: Mutex::new(pending_block_entities),
status: proto_chunk.stage.into(),
blending_data: proto_chunk.blending_data,

View File

@@ -65,8 +65,14 @@ impl GenerationCache for Cache {
let dx = chunk_x - self.x;
let dz = chunk_z - self.z;
(dx >= 0 && dx < self.size && dz >= 0 && dz < self.size)
.then(|| self.chunks[(dx * self.size + dz) as usize].get_proto_chunk_mut())
if dx < 0 || dx >= self.size || dz < 0 || dz >= self.size {
return None;
}
match &mut self.chunks[(dx * self.size + dz) as usize] {
Chunk::Proto(chunk) => Some(chunk),
Chunk::Level(_) => None,
}
}
fn get_chunk(&self, chunk_x: i32, chunk_z: i32) -> Option<&ProtoChunk> {
@@ -349,6 +355,11 @@ impl Cache {
lighting_config: &LightingEngineConfig,
) {
let mid = ((self.size * self.size) >> 1) as usize;
match &self.chunks[mid] {
Chunk::Level(_) => return,
Chunk::Proto(chunk) if chunk.stage >= stage => return,
Chunk::Proto(_) => {}
}
match stage {
StagedChunkEnum::Empty => panic!("empty stage"),
StagedChunkEnum::StructureStart => {

View File

@@ -244,6 +244,49 @@ impl GenerationSchedule {
self.queue = BinaryHeap::from(tasks);
}
/// TODO: will remove at some point
pub(crate) fn restore_ready_tasks(
graph: &mut DAG,
queue: &mut BinaryHeap<TaskHeapNode>,
chunk_map: &HashMap<ChunkPos, ChunkHolder>,
last_level: &ChunkLevel,
last_high_priority: &[ChunkPos],
waiting_for_chunks: &HashSetType<NodeKey>,
) -> usize {
debug_assert!(queue.is_empty());
let mut ready = Vec::new();
for (key, node) in &mut graph.nodes {
node.in_queue = false;
if node.stage == StagedChunkEnum::None
|| node.in_degree != 0
|| waiting_for_chunks.contains(&key)
{
continue;
}
let Some(holder) = chunk_map.get(&node.pos) else {
continue;
};
if holder.current_stage >= node.stage || holder.tasks[node.stage as usize] != key {
continue;
}
ready.push((key, node.pos, node.stage));
}
for (key, pos, stage) in &ready {
let Some(node) = graph.nodes.get_mut(*key) else {
continue;
};
node.in_queue = true;
queue.push(TaskHeapNode(
Self::calc_priority(last_level, last_high_priority, *pos, *stage),
*key,
));
}
ready.len()
}
/// Ensure that the dependency chain for `req_stage` exists on `holder` (for chunk at
/// `chunk_pos`) and wire it to depend on `dependency_task`.
///
@@ -720,6 +763,16 @@ impl GenerationSchedule {
}
}
fn drop_satisfied_tasks(&mut self, holder: &mut ChunkHolder, stage: StagedChunkEnum) {
for task_idx in (holder.current_stage as usize + 1)..=(stage as usize) {
if !holder.tasks[task_idx].is_null() {
self.waiting_for_chunks.remove(&holder.tasks[task_idx]);
self.drop_node(holder.tasks[task_idx]);
holder.tasks[task_idx] = NodeKey::null();
}
}
}
#[expect(clippy::too_many_lines)]
fn receive_chunk(&mut self, pos: ChunkPos, data: RecvChunk) {
match data {
@@ -733,11 +786,9 @@ impl GenerationSchedule {
}
debug_assert_eq!(holder.current_stage, StagedChunkEnum::None);
for i in (holder.current_stage as usize + 1)..=(chunk.get_stage_id() as usize) {
self.drop_node(holder.tasks[i]);
holder.tasks[i] = NodeKey::null();
}
holder.current_stage = StagedChunkEnum::from(chunk.get_stage_id());
let stage = StagedChunkEnum::from(chunk.get_stage_id());
self.drop_satisfied_tasks(&mut holder, stage);
holder.current_stage = stage;
debug_assert!(self.graph.nodes.contains_key(holder.occupied));
self.drop_node(holder.occupied);
holder.occupied = NodeKey::null();
@@ -784,6 +835,7 @@ impl GenerationSchedule {
match chunk {
Chunk::Level(chunk) => {
let mut holder = self.chunk_map.remove(&new_pos).unwrap();
let stage = StagedChunkEnum::Full;
if new_pos == pos {
if holder.current_stage != StagedChunkEnum::Spawn {
warn!(
@@ -794,12 +846,11 @@ impl GenerationSchedule {
);
holder.current_stage = StagedChunkEnum::Spawn;
}
self.drop_node(holder.tasks[StagedChunkEnum::Full as usize]);
holder.tasks[StagedChunkEnum::Full as usize] = NodeKey::null();
self.drop_satisfied_tasks(&mut holder, stage);
if self.graph.nodes.contains_key(holder.occupied) {
self.drop_node(holder.occupied);
}
holder.current_stage = StagedChunkEnum::Full;
holder.current_stage = stage;
let was_public = holder.public;
self.apply_lighting_override(&chunk);
@@ -836,7 +887,8 @@ impl GenerationSchedule {
}
}
} else {
holder.current_stage = StagedChunkEnum::Full;
self.drop_satisfied_tasks(&mut holder, stage);
holder.current_stage = stage;
holder.chunk = Some(Chunk::Level(chunk));
}
@@ -861,26 +913,18 @@ impl GenerationSchedule {
Chunk::Proto(chunk) => {
let mut holder = self.chunk_map.remove(&new_pos).unwrap();
let stage = chunk.stage_id();
for task_idx in (holder.current_stage as usize + 1)
..=(stage as usize).min(holder.tasks.len() - 1)
{
if !holder.tasks[task_idx].is_null() {
self.waiting_for_chunks.remove(&holder.tasks[task_idx]);
self.drop_node(holder.tasks[task_idx]);
holder.tasks[task_idx] = NodeKey::null();
}
}
let stage = StagedChunkEnum::from(chunk.stage_id());
self.drop_satisfied_tasks(&mut holder, stage);
if new_pos == pos {
debug_assert_ne!(holder.current_stage, StagedChunkEnum::None);
if self.graph.nodes.contains_key(holder.occupied) {
self.drop_node(holder.occupied);
}
holder.current_stage = StagedChunkEnum::from(stage);
holder.current_stage = stage;
} else {
if holder.current_stage < StagedChunkEnum::from(stage) {
holder.current_stage = StagedChunkEnum::from(stage);
if holder.current_stage < stage {
holder.current_stage = stage;
}
if !holder.occupied.is_null()
&& self.graph.nodes.contains_key(holder.occupied)
@@ -1269,6 +1313,18 @@ impl GenerationSchedule {
}
} else {
// No tasks in flight, wait indefinitely for LevelChannel changes
let restored = Self::restore_ready_tasks(
&mut self.graph,
&mut self.queue,
&self.chunk_map,
&self.last_level,
&self.last_high_priority,
&self.waiting_for_chunks,
);
if restored > 0 {
warn!("Restored {restored} stranded ready chunk tasks to generation queue");
continue;
}
debug_assert!(self.debug_check());
debug_assert_eq!(self.running_task_count, 0);
self.resort_work(self.send_level.wait_and_get(level));

View File

@@ -325,7 +325,7 @@ impl BiomeSupplier for BlenderBiomeSupplier<'_> {
fn biome(&self, x: i32, y: i32, z: i32, sampler: &mut MultiNoiseSampler<'_>) -> &'static Biome {
self.blender
.blend_biome(x, y, z, &self.shift_noise)
.map_or_else(|| self.base.biome(x, y, z, sampler), |blended| blended)
.unwrap_or_else(|| self.base.biome(x, y, z, sampler))
}
}

View File

@@ -1,9 +1,6 @@
use super::Carver;
use super::cave::get_height;
use crate::ProtoChunk;
use pumpkin_data::block_state::BlockState;
use super::{CarveRun, Carver, overworld_carve_state, place_carved_block};
use pumpkin_data::carver::{CarverAdditionalConfig, CarverConfig};
use pumpkin_data::{Block, BlockId};
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::random::{RandomGenerator, RandomImpl};
use std::f32::consts::PI;
@@ -14,7 +11,7 @@ impl Carver for CanyonCarver {
fn carve(
&self,
config: &CarverConfig,
chunk: &mut ProtoChunk,
run: &mut CarveRun,
random: &mut RandomGenerator,
_chunk_pos: &Vector2<i32>,
carver_chunk_pos: &Vector2<i32>,
@@ -24,8 +21,8 @@ impl Carver for CanyonCarver {
return;
};
let min_y = chunk.bottom_y() as i32;
let height = chunk.height();
let min_y = run.chunk.bottom_y() as i32;
let height = run.chunk.height();
let max_distance = (4 * 2 - 1) * 16;
@@ -42,7 +39,7 @@ impl Carver for CanyonCarver {
Self::do_carve(
config,
chunk,
run,
random.next_i64(),
x as f64,
y as f64,
@@ -62,7 +59,7 @@ impl CanyonCarver {
#[allow(clippy::too_many_arguments)]
fn do_carve(
config: &CarverConfig,
chunk: &mut ProtoChunk,
run: &mut CarveRun,
tunnel_seed: i64,
mut x: f64,
mut y: f64,
@@ -75,17 +72,9 @@ impl CanyonCarver {
y_scale: f64,
legacy_random_source: bool,
) {
let mut random = if legacy_random_source {
RandomGenerator::Legacy(pumpkin_util::random::legacy_rand::LegacyRand::from_seed(
tunnel_seed as u64,
))
} else {
RandomGenerator::Xoroshiro(pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(
tunnel_seed as u64,
))
};
let mut random = super::new_carver_random(tunnel_seed as u64, legacy_random_source);
let width_factor_per_height =
Self::init_width_factors(chunk.height() as usize, config, &mut random);
Self::init_width_factors(run.chunk.height() as usize, config, &mut random);
let mut y_rota = 0.0f32;
let mut x_rota = 0.0f32;
@@ -94,8 +83,9 @@ impl CanyonCarver {
};
for current_step in step..distance {
let progress = current_step as f32 * PI / distance as f32;
let mut horizontal_radius =
(1.5 + (current_step as f32 * PI / distance as f32).sin() * thickness) as f64;
1.5 + f64::from(pumpkin_util::math::sin(progress) * thickness);
let mut vertical_radius = horizontal_radius * y_scale;
horizontal_radius *= canyon_config
.shape
@@ -109,11 +99,11 @@ impl CanyonCarver {
current_step as f32,
);
let xc = vertical_rotation.cos();
let xs = vertical_rotation.sin();
x += (horizontal_rotation.cos() * xc) as f64;
let xc = pumpkin_util::math::cos(vertical_rotation);
let xs = pumpkin_util::math::sin(vertical_rotation);
x += f64::from(pumpkin_util::math::cos(horizontal_rotation) * xc);
y += xs as f64;
z += (horizontal_rotation.sin() * xc) as f64;
z += f64::from(pumpkin_util::math::sin(horizontal_rotation) * xc);
vertical_rotation *= 0.7;
vertical_rotation += x_rota * 0.05;
@@ -124,12 +114,20 @@ impl CanyonCarver {
y_rota += (random.next_f32() - random.next_f32()) * random.next_f32() * 4.0;
if random.next_bounded_i32(4) != 0 {
if !Self::can_reach(chunk.x, chunk.z, x, z, current_step, distance, thickness) {
if !Self::can_reach(
run.chunk.x,
run.chunk.z,
x,
z,
current_step,
distance,
thickness,
) {
return;
}
Self::carve_ellipsoid(
chunk,
run,
config,
x,
y,
@@ -199,7 +197,7 @@ impl CanyonCarver {
#[allow(clippy::too_many_arguments)]
fn carve_ellipsoid(
chunk: &mut ProtoChunk,
run: &mut CarveRun,
config: &CarverConfig,
x: f64,
y: f64,
@@ -208,24 +206,25 @@ impl CanyonCarver {
vertical_radius: f64,
width_factor_per_height: &[f32],
) {
let center_x = (chunk.x << 4) as f64 + 8.0;
let center_z = (chunk.z << 4) as f64 + 8.0;
let center_x = (run.chunk.x << 4) as f64 + 8.0;
let center_z = (run.chunk.z << 4) as f64 + 8.0;
let max_delta = 16.0 + horizontal_radius * 2.0;
if (x - center_x).abs() > max_delta || (z - center_z).abs() > max_delta {
return;
}
let chunk_min_x = chunk.x << 4;
let chunk_min_z = chunk.z << 4;
let chunk_min_x = run.chunk.x << 4;
let chunk_min_z = run.chunk.z << 4;
let x_index_min = ((x - horizontal_radius).floor() as i32 - chunk_min_x - 1).max(0);
let x_index_max = ((x + horizontal_radius).floor() as i32 - chunk_min_x).min(15);
let min_y = ((y - vertical_radius).floor() as i32 - 1).max(chunk.bottom_y() as i32 + 1);
let min_y = ((y - vertical_radius).floor() as i32 - 1).max(run.chunk.bottom_y() as i32 + 1);
let protected_blocks_on_top = 7;
let max_y = ((y + vertical_radius).floor() as i32 + 1)
.min(chunk.bottom_y() as i32 + chunk.height() as i32 - 1 - protected_blocks_on_top);
let max_y = ((y + vertical_radius).floor() as i32 + 1).min(
run.chunk.bottom_y() as i32 + run.chunk.height() as i32 - 1 - protected_blocks_on_top,
);
let z_index_min = ((z - horizontal_radius).floor() as i32 - chunk_min_z - 1).max(0);
let z_index_max = ((z + horizontal_radius).floor() as i32 - chunk_min_z).min(15);
@@ -239,6 +238,8 @@ impl CanyonCarver {
let zd = (world_z as f64 + 0.5 - z) / horizontal_radius;
if xd * xd + zd * zd < 1.0 {
let mut has_grass = false;
for world_y in (min_y + 1..=max_y).rev() {
let yd = (world_y as f64 - 0.5 - y) / vertical_radius;
@@ -248,11 +249,18 @@ impl CanyonCarver {
yd,
zd,
world_y,
chunk.bottom_y() as i32,
) && !chunk.carving_mask.get(world_x, world_y, world_z)
run.chunk.bottom_y() as i32,
) && !run.chunk.carving_mask.get(world_x, world_y, world_z)
{
chunk.carving_mask.set(world_x, world_y, world_z);
Self::carve_block(chunk, config, world_x, world_y, world_z);
run.chunk.carving_mask.set(world_x, world_y, world_z);
Self::carve_block(
run,
config,
world_x,
world_y,
world_z,
&mut has_grass,
);
}
}
}
@@ -275,28 +283,39 @@ impl CanyonCarver {
(xd * xd + zd * zd) * width_factor_per_height[y_index - 1] as f64 + yd * yd / 6.0 >= 1.0
}
fn carve_block(chunk: &mut ProtoChunk, config: &CarverConfig, x: i32, y: i32, z: i32) -> bool {
let local_y = y - chunk.bottom_y() as i32;
let state_id = chunk.get_block_state_raw(x & 15, local_y, z & 15);
let block = Block::from_state_id(state_id);
fn carve_block(
run: &mut CarveRun,
config: &CarverConfig,
x: i32,
y: i32,
z: i32,
has_grass: &mut bool,
) -> bool {
let local_y = y - run.chunk.bottom_y() as i32;
let state_id = run.chunk.get_block_state_raw(x & 15, local_y, z & 15);
let block = pumpkin_data::Block::from_state_id(state_id);
if block.id == BlockId::WATER || block.id == BlockId::LAVA {
return false;
if block.id == pumpkin_data::Block::GRASS_BLOCK.id
|| block.id == pumpkin_data::Block::MYCELIUM.id
{
*has_grass = true;
}
if block.id.has_tag(config.replaceable) {
let air = BlockState::from_id(Block::AIR.default_state.id);
let lava = BlockState::from_id(Block::LAVA.default_state.id);
let Some((state, should_schedule_fluid_update)) =
overworld_carve_state(run, config, x, y, z)
else {
return false;
};
let lava_y = config
.lava_level
.get_y(chunk.bottom_y() as i16, chunk.height());
if y <= lava_y {
chunk.set_block_state(x & 15, local_y, z & 15, lava);
} else {
chunk.set_block_state(x & 15, local_y, z & 15, air);
}
place_carved_block(
run,
pumpkin_util::math::vector3::Vector3::new(x, y, z),
state,
should_schedule_fluid_update,
*has_grass,
true,
);
return true;
}

View File

@@ -1,7 +1,5 @@
use super::Carver;
use crate::ProtoChunk;
use super::{CarveRun, Carver, overworld_carve_state, place_carved_block};
use pumpkin_data::carver::{CarverAdditionalConfig, CarverConfig, HeightProvider};
use pumpkin_data::{Block, BlockId};
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::random::{RandomGenerator, RandomImpl};
@@ -13,7 +11,7 @@ impl Carver for CaveCarver {
fn carve(
&self,
config: &CarverConfig,
chunk: &mut ProtoChunk,
run: &mut CarveRun,
random: &mut RandomGenerator,
_chunk_pos: &Vector2<i32>,
carver_chunk_pos: &Vector2<i32>,
@@ -25,8 +23,8 @@ impl Carver for CaveCarver {
CarverAdditionalConfig::Canyon(_) => return,
};
let min_y = chunk.bottom_y() as i32;
let height = chunk.height();
let min_y = run.chunk.bottom_y() as i32;
let height = run.chunk.height();
let max_distance = (4 * 2 - 1) << 4;
@@ -51,7 +49,7 @@ impl Carver for CaveCarver {
let y_scale = config.y_scale.get(random) as f64;
let thickness = 1.0 + random.next_f32() * 6.0;
Self::create_room(
chunk,
run,
x as f64,
y,
z as f64,
@@ -72,7 +70,7 @@ impl Carver for CaveCarver {
Self::create_tunnel(
config,
chunk,
run,
random.next_i64(),
x as f64,
y,
@@ -109,7 +107,7 @@ impl CaveCarver {
#[allow(clippy::too_many_arguments)]
fn create_room(
chunk: &mut ProtoChunk,
run: &mut CarveRun,
x: f64,
y: f64,
z: f64,
@@ -119,15 +117,16 @@ impl CaveCarver {
floor_level: f64,
is_nether: bool,
) {
let horizontal_radius = 1.5 + (PI / 2.0).sin() * thickness;
let vertical_radius = horizontal_radius as f64 * y_scale;
let horizontal_radius =
1.5 + f64::from(pumpkin_util::math::sin(std::f32::consts::FRAC_PI_2) * thickness);
let vertical_radius = horizontal_radius * y_scale;
Self::carve_ellipsoid(
chunk,
run,
config,
x + 1.0,
y,
z,
horizontal_radius as f64,
horizontal_radius,
vertical_radius,
floor_level,
is_nether,
@@ -137,7 +136,7 @@ impl CaveCarver {
#[allow(clippy::too_many_arguments)]
fn create_tunnel(
config: &CarverConfig,
chunk: &mut ProtoChunk,
run: &mut CarveRun,
tunnel_seed: i64,
mut x: f64,
mut y: f64,
@@ -154,28 +153,21 @@ impl CaveCarver {
is_nether: bool,
legacy_random_source: bool,
) {
let mut random = if legacy_random_source {
RandomGenerator::Legacy(pumpkin_util::random::legacy_rand::LegacyRand::from_seed(
tunnel_seed as u64,
))
} else {
RandomGenerator::Xoroshiro(pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(
tunnel_seed as u64,
))
};
let mut random = super::new_carver_random(tunnel_seed as u64, legacy_random_source);
let split_point = random.next_bounded_i32(dist / 2) + dist / 4;
let is_steep = random.next_bounded_i32(6) == 0;
let mut y_rota = 0.0f32;
let mut x_rota = 0.0f32;
for current_step in step..dist {
let progress_arg = PI * current_step as f32 / dist as f32;
let horizontal_radius =
1.5 + (PI * current_step as f32 / dist as f32).sin() * thickness;
let vertical_radius = horizontal_radius as f64 * y_scale;
let cos_x = vertical_rotation.cos();
x += (horizontal_rotation.cos() * cos_x) as f64;
y += vertical_rotation.sin() as f64;
z += (horizontal_rotation.sin() * cos_x) as f64;
1.5 + f64::from(pumpkin_util::math::sin(progress_arg) * thickness);
let vertical_radius = horizontal_radius * y_scale;
let cos_x = pumpkin_util::math::cos(vertical_rotation);
x += f64::from(pumpkin_util::math::cos(horizontal_rotation) * cos_x);
y += f64::from(pumpkin_util::math::sin(vertical_rotation));
z += f64::from(pumpkin_util::math::sin(horizontal_rotation) * cos_x);
vertical_rotation *= if is_steep { 0.92 } else { 0.7 };
vertical_rotation += x_rota * 0.1;
@@ -188,7 +180,7 @@ impl CaveCarver {
if current_step == split_point && thickness > 1.0 {
Self::create_tunnel(
config,
chunk,
run,
random.next_i64(),
x,
y,
@@ -207,7 +199,7 @@ impl CaveCarver {
);
Self::create_tunnel(
config,
chunk,
run,
random.next_i64(),
x,
y,
@@ -228,12 +220,20 @@ impl CaveCarver {
}
if random.next_bounded_i32(4) != 0 {
if !Self::can_reach(chunk.x, chunk.z, x, z, current_step, dist, thickness) {
if !Self::can_reach(
run.chunk.x,
run.chunk.z,
x,
z,
current_step,
dist,
thickness,
) {
return;
}
Self::carve_ellipsoid(
chunk,
run,
config,
x,
y,
@@ -247,7 +247,7 @@ impl CaveCarver {
}
}
#[allow(clippy::too_many_arguments)]
#[must_use]
fn can_reach(
chunk_x: i32,
chunk_z: i32,
@@ -268,7 +268,7 @@ impl CaveCarver {
#[allow(clippy::too_many_arguments)]
fn carve_ellipsoid(
chunk: &mut ProtoChunk,
run: &mut CarveRun,
config: &CarverConfig,
x: f64,
y: f64,
@@ -278,24 +278,25 @@ impl CaveCarver {
floor_level: f64,
is_nether: bool,
) {
let center_x = (chunk.x << 4) as f64 + 8.0;
let center_z = (chunk.z << 4) as f64 + 8.0;
let center_x = (run.chunk.x << 4) as f64 + 8.0;
let center_z = (run.chunk.z << 4) as f64 + 8.0;
let max_delta = 16.0 + horizontal_radius * 2.0;
if (x - center_x).abs() > max_delta || (z - center_z).abs() > max_delta {
return;
}
let chunk_min_x = chunk.x << 4;
let chunk_min_z = chunk.z << 4;
let chunk_min_x = run.chunk.x << 4;
let chunk_min_z = run.chunk.z << 4;
let x_index_min = ((x - horizontal_radius).floor() as i32 - chunk_min_x - 1).max(0);
let x_index_max = ((x + horizontal_radius).floor() as i32 - chunk_min_x).min(15);
let min_y = ((y - vertical_radius).floor() as i32 - 1).max(chunk.bottom_y() as i32 + 1);
let min_y = ((y - vertical_radius).floor() as i32 - 1).max(run.chunk.bottom_y() as i32 + 1);
let protected_blocks_on_top = 7;
let max_y = ((y + vertical_radius).floor() as i32 + 1)
.min(chunk.bottom_y() as i32 + chunk.height() as i32 - 1 - protected_blocks_on_top);
let max_y = ((y + vertical_radius).floor() as i32 + 1).min(
run.chunk.bottom_y() as i32 + run.chunk.height() as i32 - 1 - protected_blocks_on_top,
);
let z_index_min = ((z - horizontal_radius).floor() as i32 - chunk_min_z - 1).max(0);
let z_index_max = ((z + horizontal_radius).floor() as i32 - chunk_min_z).min(15);
@@ -315,11 +316,11 @@ impl CaveCarver {
let yd = (world_y as f64 - 0.5 - y) / vertical_radius;
if !Self::should_skip(xd, yd, zd, floor_level)
&& !chunk.carving_mask.get(world_x, world_y, world_z)
&& !run.chunk.carving_mask.get(world_x, world_y, world_z)
{
chunk.carving_mask.set(world_x, world_y, world_z);
run.chunk.carving_mask.set(world_x, world_y, world_z);
Self::carve_block(
chunk,
run,
config,
world_x,
world_y,
@@ -344,7 +345,7 @@ impl CaveCarver {
#[allow(clippy::too_many_arguments)]
fn carve_block(
chunk: &mut ProtoChunk,
run: &mut CarveRun,
config: &CarverConfig,
x: i32,
y: i32,
@@ -352,65 +353,43 @@ impl CaveCarver {
is_nether: bool,
has_grass: &mut bool,
) -> bool {
let local_y = y - chunk.bottom_y() as i32;
let state = chunk.get_block_state(&Vector3::new(x, y, z));
let block = state.to_block_id();
let state = run.chunk.get_block_state(&Vector3::new(x, y, z));
let block = state.to_block();
if block == BlockId::GRASS_BLOCK || block == BlockId::MYCELIUM {
if block.id == pumpkin_data::Block::GRASS_BLOCK.id
|| block.id == pumpkin_data::Block::MYCELIUM.id
{
*has_grass = true;
}
if !block.has_tag(config.replaceable) {
if !block.id.has_tag(config.replaceable) {
return false;
}
let carve_state = {
let lava_y = if is_nether {
chunk.bottom_y() as i32 + 31
let (state, should_schedule_fluid_update) = if is_nether {
let state = if y <= run.chunk.bottom_y() as i32 + 31 {
run.ids.lava
} else {
config
.lava_level
.get_y(chunk.bottom_y() as i16, chunk.height())
run.ids.cave_air
};
if y <= lava_y {
Some(Block::LAVA.default_state)
} else {
// TODO: Aquifer logic goes here.
// BlockState state = aquifer.computeSubstance(...)
// return state (or debug barrier if null)
if block == BlockId::WATER || block == BlockId::LAVA {
None
} else {
Some(Block::AIR.default_state)
}
}
(state, false)
} else {
let Some(state) = overworld_carve_state(run, config, x, y, z) else {
return false;
};
state
};
if let Some(state) = carve_state {
chunk.set_block_state(x, local_y, z, state);
place_carved_block(
run,
Vector3::new(x, y, z),
state,
should_schedule_fluid_update,
*has_grass,
!is_nether,
);
// TODO: Fluid scheduling
// if aquifer.should_schedule_fluid_update() && !state.fluid_state().is_empty() {
// chunk.mark_pos_for_postprocessing(x, y, z);
// }
// TODO: fix this (grass block survival logic)
// if *has_grass {
// let below_state_id = chunk.get_block_state_raw(x, local_y - 1, z);
// let below_block = pumpkin_data::Block::from_state_id(below_state_id);
// if below_block.id == pumpkin_data::Block::DIRT.id {
// let top_material =
// pumpkin_data::Block::GRASS_BLOCK.default_state;
// chunk.set_block_state(x, local_y - 1, z, top_material);
// }
// }
return true;
}
false
true
}
}
@@ -444,3 +423,102 @@ pub fn get_height(p: &HeightProvider, random: &mut RandomGenerator, min_y: i8, h
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pumpkin_data::carver::CAVE;
use pumpkin_data::{Block, BlockStateId, dimension::Dimension};
type Run<'a, 'b> = super::super::CarveRun<'a, 'b>;
#[test]
fn carves_at_world_y() {
super::super::with_carve_run(Dimension::OVERWORLD, |run| {
let expected = super::super::overworld_carve_state(run, &CAVE, 5, 20, 6)
.expect("test position should carve")
.0
.id;
assert_world_y(run, 5, 20, 6, expected);
assert_world_y(run, 7, -58, 8, Block::LAVA.default_state.id);
});
}
#[test]
fn uses_aquifer_state() {
super::super::with_carve_run(Dimension::OVERWORLD, |run| {
let (x, y, z, expected_state) =
find_aquifer_carve_state(run, |state, _| state.id != Block::AIR.default_state.id)
.expect("expected non-air aquifer carve state in test chunk");
carve_at(run, x, y, z, Block::WATER.default_state, expected_state.id);
assert_ne!(expected_state.id, Block::AIR.default_state.id);
let (x, y, z, expected_state) =
find_aquifer_carve_state(run, |state, schedule| state.is_liquid() && schedule)
.expect("expected scheduled aquifer fluid in test chunk");
let old_tick_count = run.chunk.fluid_ticks.len();
carve_at(run, x, y, z, Block::STONE.default_state, expected_state.id);
assert_eq!(run.chunk.fluid_ticks.len(), old_tick_count + 1);
let pos = run.chunk.fluid_ticks.last().unwrap().position.0;
assert_eq!((pos.x, pos.y, pos.z), (x, y, z));
});
}
fn assert_world_y(run: &mut Run, x: i32, y: i32, z: i32, expected: BlockStateId) {
let old_wrong_y = y - run.chunk.bottom_y() as i32;
let stone = Block::STONE.default_state;
run.chunk.set_block_state(x, y, z, stone);
run.chunk.set_block_state(x, old_wrong_y, z, stone);
carve_at(run, x, y, z, stone, expected);
assert_eq!(block_id(run, x, old_wrong_y, z), stone.id);
}
fn carve_at(
run: &mut Run,
x: i32,
y: i32,
z: i32,
initial_state: &'static pumpkin_data::BlockState,
expected: BlockStateId,
) {
let mut has_grass = false;
run.chunk.set_block_state(x, y, z, initial_state);
let carved = CaveCarver::carve_block(run, &CAVE, x, y, z, false, &mut has_grass);
assert!(carved);
assert_eq!(block_id(run, x, y, z), expected);
}
fn block_id(run: &Run, x: i32, y: i32, z: i32) -> BlockStateId {
run.chunk.get_block_state(&Vector3::new(x, y, z))
}
fn find_aquifer_carve_state(
run: &mut Run,
predicate: impl Fn(&'static pumpkin_data::BlockState, bool) -> bool,
) -> Option<(i32, i32, i32, &'static pumpkin_data::BlockState)> {
let lava_y = CAVE
.lava_level
.get_y(run.chunk.bottom_y() as i16, run.chunk.height());
for y in (lava_y + 1)..=63 {
for x in 0..16 {
for z in 0..16 {
let Some((state, should_schedule)) =
super::super::overworld_carve_state(run, &CAVE, x, y, z)
else {
continue;
};
if predicate(state, should_schedule) {
return Some((x, y, z, state));
}
}
}
}
None
}
}

View File

@@ -3,17 +3,115 @@ pub mod cave;
pub mod mask;
use crate::ProtoChunk;
use crate::generation::GlobalRandomConfig;
use crate::generation::generator::VanillaGenerator;
use crate::generation::noise::aquifer_sampler::CarverAquiferSampler;
use crate::generation::noise::perlin::DoublePerlinNoiseSampler;
use crate::generation::noise::router::surface_height_sampler::{
SurfaceHeightEstimateSampler, SurfaceHeightSamplerBuilderOptions,
};
use crate::generation::surface::rule::try_apply_material_rule;
use crate::generation::surface::terrain::SurfaceTerrainBuilder;
use crate::generation::surface::{MaterialRuleContext, steep_material_condition};
use pumpkin_data::block_state::BlockState;
use pumpkin_data::carver::{CANYON, CAVE, CAVE_EXTRA_UNDERGROUND, NETHER_CAVE};
use pumpkin_data::carver::{CarverAdditionalConfig, CarverConfig};
use pumpkin_data::chunk_gen_settings::MaterialRule;
use pumpkin_data::dimension::Dimension;
use pumpkin_data::fluid::Fluid;
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::random::{RandomGenerator, RandomImpl, get_carver_seed};
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::random::{RandomGenerator, RandomImpl};
const OVERWORLD_CARVERS: [&CarverConfig; 3] = [&CAVE, &CAVE_EXTRA_UNDERGROUND, &CANYON];
const NETHER_CARVERS: [&CarverConfig; 1] = [&NETHER_CAVE];
pub struct CarverBlockIds {
pub air: &'static BlockState,
pub cave_air: &'static BlockState,
pub lava: &'static BlockState,
pub dirt: &'static BlockState,
pub grass_block: &'static BlockState,
pub mycelium: &'static BlockState,
}
impl Default for CarverBlockIds {
fn default() -> Self {
Self::new()
}
}
impl CarverBlockIds {
#[must_use]
pub fn new() -> Self {
Self {
air: pumpkin_data::Block::AIR.default_state,
cave_air: pumpkin_data::Block::CAVE_AIR.default_state,
lava: pumpkin_data::Block::LAVA.default_state,
dirt: pumpkin_data::Block::DIRT.default_state,
grass_block: pumpkin_data::Block::GRASS_BLOCK.default_state,
mycelium: pumpkin_data::Block::MYCELIUM.default_state,
}
}
}
pub struct CarvingContext<'a> {
pub min_y: i8,
pub height: u16,
pub random_config: &'a GlobalRandomConfig,
pub surface_noise: &'a DoublePerlinNoiseSampler,
pub secondary_noise: &'a DoublePerlinNoiseSampler,
pub terrain_builder: &'a SurfaceTerrainBuilder,
pub sea_level: i32,
pub surface_rule: &'a MaterialRule,
pub surface_height_sampler: SurfaceHeightEstimateSampler<'a>,
pub carver_aquifer: Option<CarverAquiferSampler<'a>>,
}
pub struct CarveRun<'a, 'b> {
pub ctx: &'a mut CarvingContext<'b>,
pub chunk: &'a mut ProtoChunk,
pub ids: CarverBlockIds,
}
impl CarvingContext<'_> {
pub fn top_material(
&mut self,
chunk: &mut ProtoChunk,
x: i32,
y: i32,
z: i32,
under_fluid: bool,
steep: bool,
) -> Option<&'static BlockState> {
let mut context = MaterialRuleContext::new(
self.min_y,
self.height,
&self.random_config.base_random_deriver,
self.terrain_builder,
self.surface_noise,
self.secondary_noise,
self.sea_level,
);
context.init_horizontal(x, z);
context.biome = chunk.get_terrain_gen_biome(x, y, z);
context.set_steep_material_condition(steep);
context.init_vertical(1, 1, y, if under_fluid { y + 1 } else { i32::MIN });
try_apply_material_rule(
self.surface_rule,
chunk,
&mut context,
&mut self.surface_height_sampler,
)
}
}
pub trait Carver {
fn carve(
&self,
config: &CarverConfig,
chunk: &mut ProtoChunk,
run: &mut CarveRun,
random: &mut RandomGenerator,
chunk_pos: &Vector2<i32>,
carver_chunk_pos: &Vector2<i32>,
@@ -28,18 +126,58 @@ pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) {
let chunk_z = chunk.z;
let chunk_pos = Vector2::new(chunk_x, chunk_z);
let overworld_carvers = [&CAVE, &CAVE_EXTRA_UNDERGROUND, &CANYON];
let nether_carvers = [&NETHER_CAVE];
let carvers_to_use = carvers_for_dimension(&generator.dimension);
let carvers_to_use = if generator.dimension == pumpkin_data::dimension::Dimension::OVERWORLD {
&overworld_carvers[..]
} else if generator.dimension == pumpkin_data::dimension::Dimension::THE_NETHER {
&nether_carvers[..]
} else {
&[]
let start_x = crate::generation::positions::chunk_pos::start_block_x(chunk_x);
let start_z = crate::generation::positions::chunk_pos::start_block_z(chunk_z);
let generation_shape = &generator.settings.shape;
let horizontal_cell_count = 16 / generation_shape.horizontal_cell_block_count();
let horizontal_biome_end = crate::generation::biome_coords::from_block(
horizontal_cell_count as i32 * generation_shape.horizontal_cell_block_count() as i32,
);
let surface_config = SurfaceHeightSamplerBuilderOptions::new(
crate::generation::biome_coords::from_block(start_x),
crate::generation::biome_coords::from_block(start_z),
horizontal_biome_end as usize,
generation_shape.min_y as i32,
generation_shape.max_y() as i32,
generation_shape.vertical_cell_block_count() as usize,
);
let surface_height_sampler = SurfaceHeightEstimateSampler::generate(
&generator.base_router.surface_estimator,
&surface_config,
);
let carver_aquifer = generator.settings.aquifers_enabled.then(|| {
CarverAquiferSampler::new(
chunk_x,
chunk_z,
&generator.base_router,
&generator.random_config,
generator.settings,
)
});
let mut context = CarvingContext {
min_y: generator.dimension.min_y as i8,
height: generator.dimension.logical_height as u16,
random_config: &generator.random_config,
surface_noise: &generator.terrain_cache.surface_noise,
secondary_noise: &generator.terrain_cache.secondary_noise,
terrain_builder: &generator.terrain_cache.terrain_builder,
sea_level: generator.settings.sea_level,
surface_rule: &generator.settings.surface_rule,
surface_height_sampler,
carver_aquifer,
};
//let _cave_carver = cave::CaveCarver;
let mut run = CarveRun {
ctx: &mut context,
chunk,
ids: CarverBlockIds::new(),
};
let cave_carver = cave::CaveCarver;
let canyon_carver = canyon::CanyonCarver;
for dx in -radius..=radius {
@@ -51,37 +189,30 @@ pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) {
// In vanilla, carvers are per-biome. Here we use the hardcoded list but
// maintain the random seed logic.
for (index, &config) in carvers_to_use.iter().enumerate() {
let seed = get_carver_seed(
let seed = get_large_feature_seed(
generator.random_config.seed + index as u64,
carver_x,
carver_z,
);
let mut carver_random = if generator.settings.legacy_random_source {
RandomGenerator::Legacy(
pumpkin_util::random::legacy_rand::LegacyRand::from_seed(seed),
)
} else {
RandomGenerator::Xoroshiro(
pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(seed),
)
};
let mut carver_random =
new_carver_random(seed, generator.settings.legacy_random_source);
if should_carve(config, &mut carver_random) {
match config.additional {
CarverAdditionalConfig::Cave(_) | CarverAdditionalConfig::NetherCave(_) => {
// cave_carver.carve(
// config,
// chunk,
// &mut carver_random,
// &chunk_pos,
// &carver_chunk_pos,
// generator.settings.legacy_random_source,
// );
cave_carver.carve(
config,
&mut run,
&mut carver_random,
&chunk_pos,
&carver_chunk_pos,
generator.settings.legacy_random_source,
);
}
CarverAdditionalConfig::Canyon(_) => {
canyon_carver.carve(
config,
chunk,
&mut run,
&mut carver_random,
&chunk_pos,
&carver_chunk_pos,
@@ -98,3 +229,343 @@ pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) {
fn should_carve(config: &CarverConfig, random: &mut RandomGenerator) -> bool {
random.next_f32() <= config.probability
}
fn get_large_feature_seed(seed: u64, chunk_x: i32, chunk_z: i32) -> u64 {
let mut random = pumpkin_util::random::legacy_rand::LegacyRand::from_seed(seed);
let x_scale = random.next_i64();
let z_scale = random.next_i64();
let seed = seed as i64;
let result =
(chunk_x as i64).wrapping_mul(x_scale) ^ (chunk_z as i64).wrapping_mul(z_scale) ^ seed;
result as u64
}
const fn new_carver_random(seed: u64, non_vanilla_random: bool) -> RandomGenerator {
if non_vanilla_random {
RandomGenerator::Xoroshiro(pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(
seed,
))
} else {
RandomGenerator::Legacy(pumpkin_util::random::legacy_rand::LegacyRand::from_seed(
seed,
))
}
}
fn carvers_for_dimension(dimension: &Dimension) -> &'static [&'static CarverConfig] {
if dimension == &Dimension::OVERWORLD {
&OVERWORLD_CARVERS
} else if dimension == &Dimension::THE_NETHER {
&NETHER_CARVERS
} else {
&[]
}
}
fn carve_top_material(
run: &mut CarveRun,
x: i32,
carved_y: i32,
z: i32,
carved_state: &'static BlockState,
has_grass: bool,
overworld: bool,
) {
if !overworld || !has_grass {
return;
}
let below_y = carved_y - 1;
let below_state = run.chunk.get_block_state(&Vector3::new(x, below_y, z));
if below_state != run.ids.dirt.id {
return;
}
let steep = steep_material_condition(run.chunk, x, z);
if let Some(top_material) =
run.ctx
.top_material(run.chunk, x, below_y, z, carved_state.is_liquid(), steep)
{
run.chunk.set_block_state(x, below_y, z, top_material);
schedule_fluid_tick_for_state(run.chunk, x, below_y, z, top_material);
}
}
fn overworld_carve_state(
run: &mut CarveRun,
config: &CarverConfig,
x: i32,
y: i32,
z: i32,
) -> Option<(&'static BlockState, bool)> {
let lava_y = config
.lava_level
.get_y(run.chunk.bottom_y() as i16, run.chunk.height());
if y <= lava_y {
return Some((run.ids.lava, false));
}
let Some(aquifer) = run.ctx.carver_aquifer.as_mut() else {
return Some((run.ids.air, false));
};
let result = aquifer.compute(&Vector3::new(x, y, z), 0.0);
result
.state
.map(|state| (state, result.should_schedule_fluid_update))
}
fn place_carved_block(
run: &mut CarveRun,
pos: Vector3<i32>,
state: &'static BlockState,
should_schedule_fluid_update: bool,
has_grass: bool,
overworld: bool,
) {
run.chunk.set_block_state(pos.x, pos.y, pos.z, state);
if overworld && should_schedule_fluid_update && state.is_liquid() {
schedule_fluid_tick_for_state(run.chunk, pos.x, pos.y, pos.z, state);
}
carve_top_material(run, pos.x, pos.y, pos.z, state, has_grass, overworld);
}
fn schedule_fluid_tick_for_state(
chunk: &mut ProtoChunk,
x: i32,
y: i32,
z: i32,
state: &'static BlockState,
) {
if state.id == pumpkin_data::Block::WATER.default_state.id {
chunk.schedule_fluid_tick(x, y, z, &Fluid::WATER);
} else if state.id == pumpkin_data::Block::LAVA.default_state.id {
chunk.schedule_fluid_tick(x, y, z, &Fluid::LAVA);
}
}
#[cfg(test)]
fn with_carve_run<F>(dimension: Dimension, test: F)
where
F: FnOnce(&mut CarveRun<'_, '_>),
{
with_carve_run_options(dimension, None, true, test);
}
#[cfg(test)]
fn with_carve_run_options<F>(
dimension: Dimension,
surface_rule: Option<&MaterialRule>,
use_carver_aquifer: bool,
test: F,
) where
F: FnOnce(&mut CarveRun<'_, '_>),
{
use crate::generation::generator::{GeneratorInit, VanillaGenerator};
use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(42), dimension);
let mut chunk = ProtoChunk::new(0, 0, &generator);
let start_x = crate::generation::positions::chunk_pos::start_block_x(chunk.x);
let start_z = crate::generation::positions::chunk_pos::start_block_z(chunk.z);
let generation_shape = &generator.settings.shape;
let horizontal_cell_count = 16 / generation_shape.horizontal_cell_block_count();
let horizontal_biome_end = crate::generation::biome_coords::from_block(
horizontal_cell_count as i32 * generation_shape.horizontal_cell_block_count() as i32,
);
let surface_config = SurfaceHeightSamplerBuilderOptions::new(
crate::generation::biome_coords::from_block(start_x),
crate::generation::biome_coords::from_block(start_z),
horizontal_biome_end as usize,
generation_shape.min_y as i32,
generation_shape.max_y() as i32,
generation_shape.vertical_cell_block_count() as usize,
);
let surface_height_sampler = SurfaceHeightEstimateSampler::generate(
&generator.base_router.surface_estimator,
&surface_config,
);
let carver_aquifer = use_carver_aquifer.then(|| {
CarverAquiferSampler::new(
chunk.x,
chunk.z,
&generator.base_router,
&generator.random_config,
generator.settings,
)
});
let mut context = CarvingContext {
min_y: generator.dimension.min_y as i8,
height: generator.dimension.logical_height as u16,
random_config: &generator.random_config,
surface_noise: &generator.terrain_cache.surface_noise,
secondary_noise: &generator.terrain_cache.secondary_noise,
terrain_builder: &generator.terrain_cache.terrain_builder,
sea_level: generator.settings.sea_level,
surface_rule: surface_rule.unwrap_or(&generator.settings.surface_rule),
surface_height_sampler,
carver_aquifer,
};
let mut run = CarveRun {
ctx: &mut context,
chunk: &mut chunk,
ids: CarverBlockIds::new(),
};
test(&mut run);
}
#[cfg(test)]
mod tests {
use super::*;
use pumpkin_data::Block;
use pumpkin_data::chunk_gen_settings::{
BlockMaterialRule, ConditionMaterialRule, MaterialCondition, SequenceMaterialRule,
WaterMaterialCondition,
};
static PODZOL_RULE: MaterialRule = MaterialRule::Block(BlockMaterialRule {
result_state: Block::PODZOL.default_state,
});
static GRASS_RULE: MaterialRule = MaterialRule::Block(BlockMaterialRule {
result_state: Block::GRASS_BLOCK.default_state,
});
static WATER_SENSITIVE_RULES: [MaterialRule; 2] = [
MaterialRule::Condition(ConditionMaterialRule {
if_true: MaterialCondition::Water(WaterMaterialCondition {
offset: 0,
surface_depth_multiplier: 0,
add_stone_depth: false,
}),
then_run: &GRASS_RULE,
}),
MaterialRule::Block(BlockMaterialRule {
result_state: Block::DIRT.default_state,
}),
];
static WATER_SENSITIVE_RULE: MaterialRule = MaterialRule::Sequence(SequenceMaterialRule {
sequence: &WATER_SENSITIVE_RULES,
});
#[test]
fn overworld_has_aquifer() {
with_carve_run(Dimension::OVERWORLD, |run| {
assert!(run.ctx.carver_aquifer.is_some());
});
}
#[test]
fn restores_surface() {
with_carve_run_options(Dimension::OVERWORLD, Some(&PODZOL_RULE), false, |run| {
let x = 4;
let y = 70;
let z = 5;
run.chunk
.set_block_state(x, y - 1, z, Block::DIRT.default_state);
carve_top_material(run, x, y, z, Block::AIR.default_state, true, true);
assert_eq!(
run.chunk.get_block_state(&Vector3::new(x, y - 1, z)),
Block::PODZOL.default_state.id,
);
});
}
#[test]
fn skips_surface_restore() {
with_carve_run_options(Dimension::OVERWORLD, Some(&PODZOL_RULE), false, |run| {
let x = 4;
let y = 70;
let z = 5;
run.chunk
.set_block_state(x, y - 1, z, Block::DIRT.default_state);
carve_top_material(run, x, y, z, Block::AIR.default_state, false, true);
assert_eq!(
run.chunk.get_block_state(&Vector3::new(x, y - 1, z)),
Block::DIRT.default_state.id,
);
run.chunk
.set_block_state(x, y - 1, z, Block::STONE.default_state);
carve_top_material(run, x, y, z, Block::AIR.default_state, true, true);
assert_eq!(
run.chunk.get_block_state(&Vector3::new(x, y - 1, z)),
Block::STONE.default_state.id,
);
run.chunk
.set_block_state(x, y - 1, z, Block::DIRT.default_state);
carve_top_material(run, x, y, z, Block::AIR.default_state, true, false);
assert_eq!(
run.chunk.get_block_state(&Vector3::new(x, y - 1, z)),
Block::DIRT.default_state.id,
);
});
}
#[test]
fn passes_fluid_to_rule() {
with_carve_run_options(
Dimension::OVERWORLD,
Some(&WATER_SENSITIVE_RULE),
false,
|run| {
let x = 6;
let y = 70;
let z = 7;
let dry = run
.ctx
.top_material(run.chunk, x, y - 1, z, false, false)
.unwrap();
let under_fluid = run
.ctx
.top_material(run.chunk, x, y - 1, z, true, false)
.unwrap();
assert_eq!(dry.id, Block::GRASS_BLOCK.default_state.id);
assert_eq!(under_fluid.id, Block::DIRT.default_state.id);
},
);
}
#[test]
fn steep_matches_vanilla() {
with_carve_run(Dimension::OVERWORLD, |run| {
let x = 5;
let z = 5;
run.chunk.flat_surface_height_map = [64; crate::chunk::CHUNK_AREA];
set_surface_height(run.chunk, x, z - 1, 60);
set_surface_height(run.chunk, x, z + 1, 64);
assert!(steep_material_condition(run.chunk, x, z));
run.chunk.flat_surface_height_map = [64; crate::chunk::CHUNK_AREA];
set_surface_height(run.chunk, x, z - 1, 64);
set_surface_height(run.chunk, x, z + 1, 60);
assert!(!steep_material_condition(run.chunk, x, z));
run.chunk.flat_surface_height_map = [64; crate::chunk::CHUNK_AREA];
set_surface_height(run.chunk, x - 1, z, 64);
set_surface_height(run.chunk, x + 1, z, 60);
assert!(steep_material_condition(run.chunk, x, z));
run.chunk.flat_surface_height_map = [64; crate::chunk::CHUNK_AREA];
set_surface_height(run.chunk, x - 1, z, 60);
set_surface_height(run.chunk, x + 1, z, 64);
assert!(!steep_material_condition(run.chunk, x, z));
});
}
fn set_surface_height(chunk: &mut ProtoChunk, x: i32, z: i32, height: i16) {
let index = (x & 15) as usize * 16 + (z & 15) as usize;
chunk.flat_surface_height_map[index] = height;
}
}

View File

@@ -33,7 +33,7 @@ pub fn get_world_gen(seed: Seed, dimension: Dimension) -> Box<VanillaGenerator>
pub struct GlobalRandomConfig {
pub seed: u64,
pub legacy_random_source: bool,
base_random_deriver: XoroshiroSplitter,
pub base_random_deriver: XoroshiroSplitter,
aquifer_random_deriver: XoroshiroSplitter,
pub ore_random_deriver: XoroshiroSplitter,
}

View File

@@ -1,16 +1,20 @@
use enum_dispatch::enum_dispatch;
use pumpkin_data::{Block, BlockState};
use pumpkin_data::{Block, BlockState, chunk_gen_settings::GenerationSettings};
use pumpkin_util::{
math::{clamped_map, floor_div, vector3::Vector3},
random::{RandomImpl, xoroshiro128::XoroshiroSplitter},
};
use crate::generation::{
GlobalRandomConfig, biome_coords,
noise::{
LAVA_BLOCK, WATER_BLOCK,
CHUNK_DIM, LAVA_BLOCK, WATER_BLOCK,
router::{
chunk_density_function::ChunkNoiseFunctionSampleOptions,
chunk_density_function::{
ChunkNoiseFunctionBuilderOptions, ChunkNoiseFunctionSampleOptions, SampleAction,
},
chunk_noise_router::ChunkNoiseRouter,
proto_noise_router::ProtoNoiseRouters,
surface_height_sampler::SurfaceHeightEstimateSampler,
},
},
@@ -55,6 +59,107 @@ pub enum AquiferSampler {
Aquifer(WorldAquiferSampler),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CarverAquiferResult {
pub state: Option<&'static BlockState>,
pub should_schedule_fluid_update: bool,
}
pub struct CarverAquiferSampler<'a> {
aquifer: WorldAquiferSampler,
router: ChunkNoiseRouter<'a>,
height_estimator: SurfaceHeightEstimateSampler<'a>,
sample_options: ChunkNoiseFunctionSampleOptions,
}
impl<'a> CarverAquiferSampler<'a> {
#[must_use]
pub fn new(
chunk_x: i32,
chunk_z: i32,
base_router: &'a ProtoNoiseRouters,
random_config: &GlobalRandomConfig,
settings: &GenerationSettings,
) -> Self {
let shape = &settings.shape;
let horizontal_cell_count = CHUNK_DIM / shape.horizontal_cell_block_count();
let start_x = chunk_pos::start_block_x(chunk_x);
let start_z = chunk_pos::start_block_z(chunk_z);
let horizontal_biome_end = biome_coords::from_block(
horizontal_cell_count as i32 * shape.horizontal_cell_block_count() as i32,
);
let builder_options = ChunkNoiseFunctionBuilderOptions::new(
shape.horizontal_cell_block_count() as usize,
shape.vertical_cell_block_count() as usize,
floor_div(
shape.height as usize,
shape.vertical_cell_block_count() as usize,
),
horizontal_cell_count as usize,
biome_coords::from_block(start_x),
biome_coords::from_block(start_z),
horizontal_biome_end as usize,
Vec::new(),
Vec::new(),
None,
);
let surface_config =
super::router::surface_height_sampler::SurfaceHeightSamplerBuilderOptions::new(
biome_coords::from_block(start_x),
biome_coords::from_block(start_z),
horizontal_biome_end as usize,
shape.min_y as i32,
shape.max_y() as i32,
shape.vertical_cell_block_count() as usize,
);
let fluid_level = StandardChunkFluidLevelSampler::new(
FluidLevel::new(
settings.sea_level,
Block::from_state_id(settings.default_fluid.id),
),
FluidLevel::new(-54, &Block::LAVA),
);
Self {
aquifer: WorldAquiferSampler::new(
chunk_x,
chunk_z,
&random_config.aquifer_random_deriver,
shape.min_y,
shape.height,
fluid_level,
),
router: ChunkNoiseRouter::generate(&base_router.noise, &builder_options),
height_estimator: SurfaceHeightEstimateSampler::generate(
&base_router.surface_estimator,
&surface_config,
),
sample_options: ChunkNoiseFunctionSampleOptions::new(
false,
SampleAction::SkipCellCaches,
0,
0,
0,
),
}
}
pub fn compute(&mut self, pos: &Vector3<i32>, density: f64) -> CarverAquiferResult {
let (state, should_schedule_fluid_update) = self.aquifer.apply_internal(
&mut self.router,
pos,
&self.sample_options,
&mut self.height_estimator,
density,
);
CarverAquiferResult {
state,
should_schedule_fluid_update,
}
}
}
macro_rules! packed_position_index {
($local_x:expr,$local_y:expr,$local_z:expr,$dim_y:expr,$dim_z:expr) => {
($local_x * $dim_z + $local_z) * $dim_y + $local_y
@@ -158,28 +263,35 @@ impl WorldAquiferSampler {
}
}
const fn packed_position_index(&self, x: i32, y: i32, z: i32) -> usize {
let local_x = (x - self.start_x) as usize;
let local_y = (y - self.start_y) as usize;
let local_z = (z - self.start_z) as usize;
fn checked_packed_position_index(&self, x: i32, y: i32, z: i32) -> Option<usize> {
let local_x = usize::try_from(x - self.start_x).ok()?;
let local_y = usize::try_from(y - self.start_y).ok()?;
let local_z = usize::try_from(z - self.start_z).ok()?;
packed_position_index!(local_x, local_y, local_z, self.size_y, self.size_z)
if local_y >= self.size_y || local_z >= self.size_z {
return None;
}
let index = packed_position_index!(local_x, local_y, local_z, self.size_y, self.size_z);
(index < self.packed_positions.len()).then_some(index)
}
fn random_positions_for_pos(&self, x: i32, y: i32, z: i32) -> [i64; 12] {
fn random_positions_for_pos(&self, x: i32, y: i32, z: i32) -> Option<[i64; 12]> {
let sy = self.size_y;
let syz = self.size_y * self.size_z;
let i00 = self.packed_position_index(x, y - 1, z);
let i00 = self.checked_packed_position_index(x, y - 1, z)?;
let i01 = i00 + sy;
let i10 = i00 + syz;
let i11 = i10 + sy;
assert!(i11 + 2 < self.packed_positions.len(), "Index out of bounds");
if i11 + 2 >= self.packed_positions.len() {
return None;
}
let p = &self.packed_positions;
[
Some([
p[i11 + 2],
p[i10 + 2],
p[i01 + 2],
@@ -192,7 +304,7 @@ impl WorldAquiferSampler {
p[i10],
p[i01],
p[i00],
]
])
}
#[inline]
@@ -250,7 +362,7 @@ impl WorldAquiferSampler {
router: &mut ChunkNoiseRouter,
height_estimator: &mut SurfaceHeightEstimateSampler,
sample_options: &ChunkNoiseFunctionSampleOptions,
) -> &FluidLevel {
) -> FluidLevel {
let x = block_pos::unpack_x(packed_pos);
let y = block_pos::unpack_y(packed_pos);
let z = block_pos::unpack_z(packed_pos);
@@ -259,9 +371,20 @@ impl WorldAquiferSampler {
let local_y = local_y!(y);
let local_z = local_xz!(z);
let index = self.packed_position_index(local_x, local_y, local_z);
let Some(index) = self.checked_packed_position_index(local_x, local_y, local_z) else {
return Self::get_fluid_level(
&self.fluid_level_sampler,
x,
y,
z,
router,
height_estimator,
sample_options,
);
};
if let Some(ref level) = self.levels[index] {
return level;
return level.clone();
}
let sampled = Self::get_fluid_level(
@@ -274,7 +397,8 @@ impl WorldAquiferSampler {
sample_options,
);
self.levels[index].insert(sampled)
self.levels[index] = Some(sampled.clone());
sampled
}
fn get_fluid_level(
@@ -450,9 +574,9 @@ impl WorldAquiferSampler {
sample_options: &ChunkNoiseFunctionSampleOptions,
height_estimator: &mut SurfaceHeightEstimateSampler,
density: f64,
) -> Option<&'static BlockState> {
) -> (Option<&'static BlockState>, bool) {
if density > 0f64 {
return None;
return (None, false);
}
let sample_x = pos.x;
@@ -463,37 +587,32 @@ impl WorldAquiferSampler {
.fluid_level_sampler
.get_fluid_level(sample_x, sample_y, sample_z);
if fluid_level.get_block(sample_y) == &LAVA_BLOCK {
return Some(LAVA_BLOCK.default_state);
return (Some(LAVA_BLOCK.default_state), false);
}
let scaled_x = local_xz!(sample_x - 5);
let scaled_y = local_y!(sample_y + 1);
let scaled_z = local_xz!(sample_z - 5);
// Inline random_positions_for_pos: read directly from packed_positions with a
// single bounds check instead of stack-allocating and copying a [i64; 12].
let sy = self.size_y;
let syz = sy * self.size_z;
let i00 = self.packed_position_index(scaled_x, scaled_y - 1, scaled_z);
let i01 = i00 + sy;
let i10 = i00 + syz;
let i11 = i10 + sy;
let Some(random_positions) = self.random_positions_for_pos(scaled_x, scaled_y, scaled_z)
else {
return (Some(fluid_level.get_block(sample_y).default_state), false);
};
let p = &self.packed_positions;
// i11 + 2 is the largest index we ever access; all others are strictly smaller.
assert!(i11 + 2 < p.len(), "Index out of bounds");
let mut nearest = [(0i64, i32::MAX); 4];
let mut nearest = [(0i64, i32::MAX); 3];
// SAFETY: every index passed to this macro is <= i11 + 2, checked by the assert above.
macro_rules! process {
($idx:expr) => {{
let packed = unsafe { *p.get_unchecked($idx) };
($packed:expr) => {{
let packed = $packed;
let dx = block_pos::unpack_x(packed) - sample_x;
let dy = block_pos::unpack_y(packed) - sample_y;
let dz = block_pos::unpack_z(packed) - sample_z;
let h = dx * dx + dy * dy + dz * dz;
if nearest[3].1 > h {
nearest[3] = (packed, h);
}
if nearest[2].1 > h {
nearest[3] = nearest[2];
nearest[2] = (packed, h);
}
if nearest[1].1 > h {
@@ -507,34 +626,26 @@ impl WorldAquiferSampler {
}};
}
// Same insertion order as the original array literal sort behaviour is preserved.
process!(i11 + 2);
process!(i10 + 2);
process!(i01 + 2);
process!(i00 + 2);
process!(i11 + 1);
process!(i10 + 1);
process!(i01 + 1);
process!(i00 + 1);
process!(i11);
process!(i10);
process!(i01);
process!(i00);
// Same insertion order as the original array literal; sort behaviour is preserved.
for packed in random_positions {
process!(packed);
}
// Precompute all three pairwise distances before fetching any water levels so we
// can skip the third get_water_level call entirely when neither f nor g_dist > 0.
let d = Self::max_distance(nearest[0].1, nearest[1].1);
let f = Self::max_distance(nearest[0].1, nearest[2].1);
let g_dist = Self::max_distance(nearest[1].1, nearest[2].1);
let fluid_level2 = self
.get_water_level(nearest[0].0, router, height_estimator, sample_options)
.clone();
let fluid_level2 =
self.get_water_level(nearest[0].0, router, height_estimator, sample_options);
let block_state = fluid_level2.get_block(sample_y);
let sim12 = Self::max_distance(nearest[0].1, nearest[1].1);
if d <= 0f64 {
// TODO: Handle fluid tick
return Some(block_state.default_state);
if sim12 <= 0f64 {
let should_schedule = if sim12 >= -0.12f64 {
// FLOWING_UPDATE_SIMILARITY
let fluid_level3 =
self.get_water_level(nearest[1].0, router, height_estimator, sample_options);
fluid_level2.block != fluid_level3.block || fluid_level2.max_y != fluid_level3.max_y
} else {
false
};
return (Some(block_state.default_state), should_schedule);
}
if block_state == &WATER_BLOCK
@@ -544,67 +655,83 @@ impl WorldAquiferSampler {
.get_block(sample_y - 1)
== &LAVA_BLOCK
{
return Some(block_state.default_state);
return (Some(block_state.default_state), true);
}
let mut barrier_sample = None;
let fluid_level3 = self
.get_water_level(nearest[1].0, router, height_estimator, sample_options)
.clone();
let e = d * Self::calculate_density(
&mut barrier_sample,
pos,
router,
sample_options,
&fluid_level2,
&fluid_level3,
);
let fluid_level3 =
self.get_water_level(nearest[1].0, router, height_estimator, sample_options);
let barrier12 = sim12
* Self::calculate_density(
&mut barrier_sample,
pos,
router,
sample_options,
&fluid_level2,
&fluid_level3,
);
if density + e > 0f64 {
return None;
if density + barrier12 > 0f64 {
return (None, false);
}
// Only pay for the cache/noise lookup when at least one distance weight is positive;
// when both are <= 0 the third centre cannot affect the result.
if f > 0f64 || g_dist > 0f64 {
let fluid_level4 =
self.get_water_level(nearest[2].0, router, height_estimator, sample_options);
if f > 0f64 {
let contrib = d
* f
* Self::calculate_density(
&mut barrier_sample,
pos,
router,
sample_options,
&fluid_level2,
fluid_level4,
);
if density + contrib > 0f64 {
return None;
}
}
if g_dist > 0f64 {
let contrib = d
* g_dist
* Self::calculate_density(
&mut barrier_sample,
pos,
router,
sample_options,
&fluid_level3,
fluid_level4,
);
if density + contrib > 0f64 {
return None;
}
let fluid_level4 =
self.get_water_level(nearest[2].0, router, height_estimator, sample_options);
let sim13 = Self::max_distance(nearest[0].1, nearest[2].1);
if sim13 > 0f64 {
let barrier13 = sim12
* sim13
* Self::calculate_density(
&mut barrier_sample,
pos,
router,
sample_options,
&fluid_level2,
&fluid_level4,
);
if density + barrier13 > 0f64 {
return (None, false);
}
}
// TODO: Handle fluid tick
Some(block_state.default_state)
let sim23 = Self::max_distance(nearest[1].1, nearest[2].1);
if sim23 > 0f64 {
let barrier23 = sim12
* sim23
* Self::calculate_density(
&mut barrier_sample,
pos,
router,
sample_options,
&fluid_level3,
&fluid_level4,
);
if density + barrier23 > 0f64 {
return (None, false);
}
}
let may_flow12 =
fluid_level2.block != fluid_level3.block || fluid_level2.max_y != fluid_level3.max_y;
let may_flow23 = sim23 >= -0.12f64
&& (fluid_level3.block != fluid_level4.block
|| fluid_level3.max_y != fluid_level4.max_y);
let may_flow13 = sim13 >= -0.12f64
&& (fluid_level2.block != fluid_level4.block
|| fluid_level2.max_y != fluid_level4.max_y);
let should_schedule = if may_flow12 || may_flow23 || may_flow13 {
true
} else {
let fluid_level5 =
self.get_water_level(nearest[3].0, router, height_estimator, sample_options);
sim13 >= -0.12f64
&& Self::max_distance(nearest[0].1, nearest[3].1) >= -0.12f64
&& (fluid_level2.block != fluid_level5.block
|| fluid_level2.max_y != fluid_level5.max_y)
};
(Some(block_state.default_state), should_schedule)
}
}
@@ -616,7 +743,7 @@ impl AquiferSamplerImpl for WorldAquiferSampler {
pos: &Vector3<i32>,
sample_options: &ChunkNoiseFunctionSampleOptions,
height_estimator: &mut SurfaceHeightEstimateSampler,
) -> Option<&'static BlockState> {
) -> (Option<&'static BlockState>, bool) {
let density = router.final_density(pos, sample_options);
self.apply_internal(router, pos, sample_options, height_estimator, density)
}
@@ -640,17 +767,19 @@ impl AquiferSamplerImpl for SeaLevelAquiferSampler {
pos: &Vector3<i32>,
sample_options: &ChunkNoiseFunctionSampleOptions,
_height_estimator: &mut SurfaceHeightEstimateSampler,
) -> Option<&'static BlockState> {
) -> (Option<&'static BlockState>, bool) {
let sample = router.final_density(pos, sample_options);
//log::debug!("Aquifer sample {:?}: {}", &pos, sample);
if sample > 0f64 {
None
(None, false)
} else {
Some(
self.level_sampler
.get_fluid_level(pos.x, pos.y, pos.z)
.get_block(pos.y)
.default_state,
(
Some(
self.level_sampler
.get_fluid_level(pos.x, pos.y, pos.z)
.get_block(pos.y)
.default_state,
),
false,
)
}
}
@@ -664,7 +793,7 @@ pub trait AquiferSamplerImpl {
pos: &Vector3<i32>,
sample_options: &ChunkNoiseFunctionSampleOptions,
height_estimator: &mut SurfaceHeightEstimateSampler,
) -> Option<&'static BlockState>;
) -> (Option<&'static BlockState>, bool);
}
#[cfg(test)]
@@ -694,7 +823,7 @@ mod random_positions_and_hypot {
proto_chunk::StandardChunkFluidLevelSampler,
};
use super::{AquiferSampler, FluidLevel, WorldAquiferSampler};
use super::{AquiferSampler, CarverAquiferSampler, FluidLevel, WorldAquiferSampler};
const SEED: u64 = 0;
static RANDOM_CONFIG: LazyLock<GlobalRandomConfig> =
@@ -773,6 +902,57 @@ mod random_positions_and_hypot {
(aquifer, noise.router, height_estimator, options)
}
fn create_carver_aquifer() -> CarverAquiferSampler<'static> {
let settings = GenerationSettings::from_dimension(&Dimension::OVERWORLD);
CarverAquiferSampler::new(7, 4, &PROTO_ROUTER, &RANDOM_CONFIG, settings)
}
#[test]
fn carver_aquifer_returns_stable_output() {
let pos = Vector3::new(112, 0, 64);
let mut first = create_carver_aquifer();
let mut second = create_carver_aquifer();
assert_eq!(first.compute(&pos, -1.0), second.compute(&pos, -1.0));
}
#[test]
fn carver_aquifer_handles_chunk_edges() {
let mut aquifer = create_carver_aquifer();
let positions = [
Vector3::new(112, -64, 64),
Vector3::new(127, -64, 79),
Vector3::new(112, 319, 79),
Vector3::new(127, 319, 64),
];
for pos in positions {
let _ = aquifer.compute(&pos, -1.0);
}
}
#[test]
fn carver_aquifer_reports_fluid_schedule_signal() {
let mut aquifer = create_carver_aquifer();
let mut found_schedule = false;
'positions: for y in -64..=63 {
for x in 112..=127 {
for z in 64..=79 {
if aquifer
.compute(&Vector3::new(x, y, z), -1.0)
.should_schedule_fluid_update
{
found_schedule = true;
break 'positions;
}
}
}
}
assert!(found_schedule);
}
#[test]
#[expect(clippy::too_many_lines, clippy::large_stack_arrays)]
fn get_fluid_block_state() {
@@ -2420,7 +2600,9 @@ mod random_positions_and_hypot {
for ((x, y, z, sample), result) in values {
let pos = Vector3::new(x, y, z);
assert_eq!(
aquifer.apply_internal(&mut router, &pos, &env, &mut height_estimator, sample),
aquifer
.apply_internal(&mut router, &pos, &env, &mut height_estimator, sample)
.0,
result.map(pumpkin_data::BlockStateId::to_state)
);
}

View File

@@ -51,7 +51,11 @@ impl BlockStateSampler {
height_estimator: &mut SurfaceHeightEstimateSampler,
) -> Option<&'static BlockState> {
match self {
Self::Aquifer(aquifer) => aquifer.apply(router, pos, sample_options, height_estimator),
Self::Aquifer(aquifer) => {
aquifer
.apply(router, pos, sample_options, height_estimator)
.0
}
Self::Ore(ore) => ore.sample(router, ore_random_deriver, pos, sample_options),
}
}

View File

@@ -57,6 +57,8 @@ use crate::{
use pumpkin_data::tag::get_tag_ids;
use pumpkin_nbt::compound::NbtCompound;
use crate::tick::{ScheduledTick, TickPriority};
enum ActiveSupplier {
Overworld(MultiNoiseBiomeSupplier),
Nether(MultiNoiseBiomeSupplier),
@@ -142,6 +144,7 @@ pub struct ProtoChunk {
pub carving_mask: crate::generation::carver::mask::CarvingMask,
pub blending_data: Option<crate::generation::blender::blending_data::BlendingData>,
pub pending_block_entities: Vec<NbtCompound>,
pub fluid_ticks: Vec<ScheduledTick<&'static Fluid>>,
}
pub struct TerrainCache {
@@ -215,6 +218,7 @@ impl ProtoChunk {
),
blending_data: None,
pending_block_entities: Vec::new(),
fluid_ticks: Vec::new(),
}
}
@@ -328,6 +332,15 @@ impl ProtoChunk {
std::mem::take(&mut self.pending_block_entities)
}
pub fn schedule_fluid_tick(&mut self, x: i32, y: i32, z: i32, fluid: &'static Fluid) {
self.fluid_ticks.push(ScheduledTick {
delay: 0,
priority: TickPriority::Normal,
position: BlockPos::new(x, y, z),
value: fluid,
});
}
fn maybe_update_surface_height_map(&mut self, index: usize, y: i16) {
let current_height = self.flat_surface_height_map[index];
self.flat_surface_height_map[index] = current_height.max(y) as _;
@@ -834,6 +847,9 @@ impl ProtoChunk {
pub fn spawn_mobs<T: GenerationCache>(cache: &mut T, block_registry: &dyn WorldPortalExt) {
let chunk = cache.get_center_chunk();
if chunk.stage >= StagedChunkEnum::Spawn {
return;
}
debug_assert_eq!(chunk.stage, StagedChunkEnum::Lighting);
let biome = chunk.get_terrain_gen_biome(

View File

@@ -52,6 +52,7 @@ pub struct MaterialRuleContext<'a> {
pub stone_depth_above: i32,
pub terrain_builder: &'a SurfaceTerrainBuilder,
pub sea_level: i32,
steep_material_condition: Option<bool>,
}
impl<'a> MaterialRuleContext<'a> {
@@ -88,6 +89,7 @@ impl<'a> MaterialRuleContext<'a> {
stone_depth_below: 0,
stone_depth_above: 0,
sea_level,
steep_material_condition: None,
}
}
@@ -133,9 +135,12 @@ impl<'a> MaterialRuleContext<'a> {
}
self.secondary_depth
}
pub const fn set_steep_material_condition(&mut self, steep: bool) {
self.steep_material_condition = Some(steep);
}
}
#[expect(clippy::similar_names)]
pub fn test_condition(
condition: &MaterialCondition,
chunk: &mut ProtoChunk,
@@ -161,28 +166,9 @@ pub fn test_condition(
);
temperature < 0.15f32
}
MaterialCondition::Steep => {
let local_x = context.block_pos_x & 15;
let local_z = context.block_pos_z & 15;
let local_z_sub = 0.max(local_z - 1);
let local_z_add = 15.min(local_z + 1);
let sub_height = chunk.top_block_height_exclusive(local_x, local_z_sub);
let add_height = chunk.top_block_height_exclusive(local_x, local_z_add);
if add_height >= sub_height + 4 {
true
} else {
let local_x_sub = 0.max(local_x - 1);
let local_x_add = 15.min(local_x + 1);
let sub_height = chunk.top_block_height_exclusive(local_x_sub, local_z);
let add_height = chunk.top_block_height_exclusive(local_x_add, local_z);
sub_height >= add_height + 4
}
}
MaterialCondition::Steep => context.steep_material_condition.unwrap_or_else(|| {
steep_material_condition(chunk, context.block_pos_x, context.block_pos_z)
}),
MaterialCondition::Not(not) => {
test_not_material(not, chunk, context, surface_height_estimate_sampler)
}
@@ -194,6 +180,30 @@ pub fn test_condition(
}
}
#[must_use]
pub fn steep_material_condition(chunk: &ProtoChunk, block_x: i32, block_z: i32) -> bool {
let local_x = block_x & 15;
let local_z = block_z & 15;
let local_z_sub = 0.max(local_z - 1);
let local_z_add = 15.min(local_z + 1);
let sub_height = chunk.top_block_height_exclusive(local_x, local_z_sub);
let add_height = chunk.top_block_height_exclusive(local_x, local_z_add);
if add_height >= sub_height + 4 {
return true;
}
let local_x_sub = 0.max(local_x - 1);
let local_x_add = 15.min(local_x + 1);
let sub_height = chunk.top_block_height_exclusive(local_x_sub, local_z);
let add_height = chunk.top_block_height_exclusive(local_x_add, local_z);
sub_height >= add_height + 4
}
pub struct HoleMaterialCondition;
impl HoleMaterialCondition {

View File

@@ -139,7 +139,6 @@ mod test {
use pumpkin_data::game_rules::GameRuleRegistry;
use pumpkin_nbt::{deserializer::from_bytes, serializer::to_bytes};
use pumpkin_util::{Difficulty, world_seed::Seed};
use std::assert_matches;
use std::{
fs,
io::{Cursor, Read},
@@ -283,6 +282,9 @@ mod test {
.unwrap();
let result = AnvilLevelInfo.read_world_info(temp_dir.path());
assert_matches!(result, Err(WorldInfoError::UnsupportedDataVersion(_)));
assert!(matches!(
result,
Err(WorldInfoError::UnsupportedDataVersion(_))
));
}
}

View File

@@ -2579,11 +2579,6 @@ impl World {
client_suggestions::send_c_commands_packet(player, server, &command_dispatcher).await;
};
// Spawn in initial chunks
// This is made before the player teleport so that the player doesn't glitch out when spawning
chunker::update_position(player).await;
// Teleport
let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) {
let position = player.position();
let yaw = player.get_entity().yaw.load(); //info.spawn_angle;
@@ -2605,13 +2600,26 @@ impl World {
(position, info.spawn_yaw, info.spawn_pitch)
};
let velocity = player.get_entity().velocity.load();
// Load chunks around the real spawn position before teleporting the client there.
player.living_entity.entity.set_pos(position);
player.living_entity.entity.set_rotation(yaw, pitch);
player.living_entity.entity.last_pos.store(position);
chunker::update_position(player).await;
let center_chunk = player.living_entity.entity.chunk_pos.load();
let chunk = self
.level
.get_or_fetch_chunk(center_chunk, std::clone::Clone::clone)
.await;
client.send_packet_now(&CChunkBatchStart).await;
client.send_packet_now(&CChunkData(&chunk)).await;
client.send_packet_now(&CChunkBatchEnd::new(1u16)).await;
let velocity = player.living_entity.entity.velocity.load();
debug!("Sending player teleport to {}", player.gameprofile.name);
player.request_teleport(position, yaw, pitch).await;
player.get_entity().last_pos.store(position);
let gameprofile = &player.gameprofile;
let bedrock_player_list = CPlayerList {
action: CPlayerList::ACTION_ADD,