From a748aae07e750f09fda56e579361c9c68c3f996c Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Wed, 5 Aug 2026 14:31:26 +0200 Subject: [PATCH] Revert "feat: Add Mineshaft Generation (#2740)" This reverts commit 09d23bd15f026cc01dcd2cc9da30e897e5c9616c. --- pumpkin-data/src/block_state.rs | 25 - pumpkin-util/src/random/mod.rs | 4 +- pumpkin-util/src/random/xoroshiro128.rs | 111 +- pumpkin-world/src/generation/proto_chunk.rs | 142 +- pumpkin-world/src/generation/structure/mod.rs | 31 +- .../src/generation/structure/placement.rs | 34 +- .../structure/structures/mineshaft.rs | 1956 +---------------- .../generation/structure/structures/mod.rs | 4 +- pumpkin/src/entity/mod.rs | 22 - pumpkin/src/entity/player.rs | 121 - pumpkin/src/entity/projectile/arrow.rs | 10 +- pumpkin/src/entity/vehicle/minecart.rs | 368 +--- pumpkin/src/entity/vehicle/minecart/chest.rs | 48 - .../src/entity/vehicle/minecart/container.rs | 283 --- .../src/entity/vehicle/minecart/furnace.rs | 160 -- pumpkin/src/entity/vehicle/minecart/hopper.rs | 143 -- .../src/entity/vehicle/minecart/rideable.rs | 29 - pumpkin/src/entity/vehicle/minecart/tnt.rs | 152 -- pumpkin/src/entity/vehicle/vehicle.rs | 107 +- pumpkin/src/item/items/minecart.rs | 1 - pumpkin/src/world/explosion.rs | 53 +- pumpkin/src/world/mod.rs | 33 +- 22 files changed, 240 insertions(+), 3597 deletions(-) delete mode 100644 pumpkin/src/entity/vehicle/minecart/chest.rs delete mode 100644 pumpkin/src/entity/vehicle/minecart/container.rs delete mode 100644 pumpkin/src/entity/vehicle/minecart/furnace.rs delete mode 100644 pumpkin/src/entity/vehicle/minecart/hopper.rs delete mode 100644 pumpkin/src/entity/vehicle/minecart/rideable.rs delete mode 100644 pumpkin/src/entity/vehicle/minecart/tnt.rs diff --git a/pumpkin-data/src/block_state.rs b/pumpkin-data/src/block_state.rs index eb40e7b2d..368bb54a2 100644 --- a/pumpkin-data/src/block_state.rs +++ b/pumpkin-data/src/block_state.rs @@ -122,16 +122,6 @@ impl BlockState { self.state_flags & IS_SOLID_BLOCK != 0 } - /// Returns `isSolidRender()` from Java Edition. - /// - /// The cached opacity matches that predicate for every block state except - /// tinted glass, which deliberately blocks all light without being a solid - /// render block. - #[must_use] - pub const fn is_solid_render(&self) -> bool { - self.opacity == 15 && self.id.to_block().id.as_u16() != Block::TINTED_GLASS.id.as_u16() - } - #[must_use] pub const fn has_random_ticks(&self) -> bool { self.state_flags & HAS_RANDOM_TICKS != 0 @@ -292,18 +282,3 @@ const WEST_SIDE_SOLID: u8 = 1 << 4; const EAST_SIDE_SOLID: u8 = 1 << 5; const DOWN_CENTER_SOLID: u8 = 1 << 6; const UP_CENTER_SOLID: u8 = 1 << 7; - -#[cfg(test)] -mod tests { - use super::Block; - - #[test] - fn solid_render_distinguishes_occlusion_from_collision_and_conduction() { - assert!(Block::STONE.default_state.is_solid_render()); - assert!(Block::GLOWSTONE.default_state.is_solid_render()); - assert!(Block::REDSTONE_BLOCK.default_state.is_solid_render()); - assert!(!Block::GLASS.default_state.is_solid_render()); - assert!(!Block::SLIME_BLOCK.default_state.is_solid_render()); - assert!(!Block::TINTED_GLASS.default_state.is_solid_render()); - } -} diff --git a/pumpkin-util/src/random/mod.rs b/pumpkin-util/src/random/mod.rs index 85b0d33e0..10fd0564f 100644 --- a/pumpkin-util/src/random/mod.rs +++ b/pumpkin-util/src/random/mod.rs @@ -4,7 +4,7 @@ use std::{ }; use legacy_rand::{LegacyRand, LegacySplitter}; -use xoroshiro128::{WorldgenXoroshiro, Xoroshiro, XoroshiroSplitter}; +use xoroshiro128::{Xoroshiro, XoroshiroSplitter}; mod gaussian; pub mod legacy_rand; @@ -35,8 +35,6 @@ pub enum RandomGenerator { Xoroshiro(Xoroshiro), /// Legacy random number generator (compatible with older Minecraft versions). Legacy(LegacyRand), - /// Xoroshiro source accessed through Vanilla's `WorldgenRandom` bit API. - WorldgenXoroshiro(WorldgenXoroshiro), } /// Unified random number deriver enum for creating child RNGs. diff --git a/pumpkin-util/src/random/xoroshiro128.rs b/pumpkin-util/src/random/xoroshiro128.rs index 8ae19358a..46b7690bf 100644 --- a/pumpkin-util/src/random/xoroshiro128.rs +++ b/pumpkin-util/src/random/xoroshiro128.rs @@ -211,102 +211,6 @@ impl RandomImpl for Xoroshiro { } } -/// A Xoroshiro source exposed through Java's `WorldgenRandom`/`BitRandomSource` -/// adapter. -/// -/// World decoration deliberately uses this API, whose integer and bounded-number -/// methods consume different bits than `XoroshiroRandomSource` itself. -pub struct WorldgenXoroshiro { - source: Xoroshiro, - internal_next_gaussian: Option, -} - -impl WorldgenXoroshiro { - population_seed_fn!(); - - #[must_use] - pub const fn from_seed(seed: u64) -> Self { - Self { - source: Xoroshiro::from_seed(seed), - internal_next_gaussian: None, - } - } - - const fn next(&mut self, bits: u64) -> i32 { - (self.source.next_random() >> (64 - bits)) as i32 - } -} - -impl GaussianGenerator for WorldgenXoroshiro { - fn stored_next_gaussian(&self) -> Option { - self.internal_next_gaussian - } - - fn set_stored_next_gaussian(&mut self, value: Option) { - self.internal_next_gaussian = value; - } -} - -impl RandomImpl for WorldgenXoroshiro { - fn split(&mut self) -> Self { - Self { - source: Xoroshiro::new(self.source.next_random(), self.source.next_random()), - internal_next_gaussian: None, - } - } - - fn next_splitter(&mut self) -> RandomDeriver { - RandomDeriver::Xoroshiro(self.source.next_splitter()) - } - - fn next_i32(&mut self) -> i32 { - self.next(32) - } - - fn next_bounded_i32(&mut self, bound: i32) -> i32 { - if (bound & bound.wrapping_sub(1)) == 0 { - (i64::from(bound).wrapping_mul(i64::from(self.next(31))) >> 31) as i32 - } else { - loop { - let value = self.next(31); - let result = value % bound; - if value - .wrapping_sub(result) - .wrapping_add(bound.wrapping_sub(1)) - >= 0 - { - return result; - } - } - } - } - - fn next_i64(&mut self) -> i64 { - let high = self.next_i32(); - let low = self.next_i32(); - (i64::from(high) << 32).wrapping_add(i64::from(low)) - } - - fn next_bool(&mut self) -> bool { - self.next(1) != 0 - } - - fn next_f32(&mut self) -> f32 { - self.next(24) as f32 * 5.960_464_5E-8f32 - } - - fn next_f64(&mut self) -> f64 { - let high = self.next(26); - let low = self.next(27); - let bits = (i64::from(high) << 27).wrapping_add(i64::from(low)); - bits as f64 * 1.110_223_024_625_156_5E-16 - } - - fn next_gaussian(&mut self) -> f64 { - self.calculate_gaussian() - } -} - /// A splitter for creating derived Xoroshiro random generators. /// /// This struct allows for deterministic creation of multiple independent @@ -361,23 +265,10 @@ impl RandomDeriverImpl for XoroshiroSplitter { mod tests { use crate::random::{RandomDeriverImpl, RandomImpl}; - use super::{WorldgenXoroshiro, Xoroshiro, mix_stafford_13}; + use super::{Xoroshiro, mix_stafford_13}; // Values checked against results from the equivalent Java source - #[test] - fn worldgen_adapter_matches_java_bit_random_source() { - assert_eq!( - WorldgenXoroshiro::get_population_seed(123_456_789, -37 * 16, 84 * 16), - 8_245_768_125_123_811_941 - ); - - let mut random = WorldgenXoroshiro::from_seed(8_245_768_125_123_841_941); - assert_eq!(random.next_i32(), -618_189_195); - assert!((random.next_f32() - 0.066_260_1).abs() < f32::EPSILON); - assert_eq!(random.next_i64(), -8_355_384_254_887_254_362); - } - #[test] fn mix_stafford_13_test() { let values: [(u64, i64); 31] = [ diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index 0f1aacd1b..9a23d2618 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -10,12 +10,12 @@ use pumpkin_data::structures::{ use pumpkin_data::tag::RegistryKey; use pumpkin_data::{Block, BlockState, block_properties::blocks_movement, chunk::Biome}; use pumpkin_data::{BlockId, BlockStateId, tag}; -use pumpkin_util::random::RandomImpl; -use pumpkin_util::random::xoroshiro128::{WorldgenXoroshiro, XoroshiroSplitter}; +use pumpkin_util::random::xoroshiro128::XoroshiroSplitter; +use pumpkin_util::random::{RandomImpl, get_carver_seed}; use pumpkin_util::{ HeightMap, math::{block_box::BlockBox, position::BlockPos, vector3::Vector3}, - random::{RandomGenerator, get_decorator_seed}, + random::{RandomGenerator, get_decorator_seed, xoroshiro128::Xoroshiro}, }; use rustc_hash::FxHashMap; @@ -66,18 +66,6 @@ enum ActiveSupplier { End(TheEndBiomeSupplier), } -fn structures_for_step(step: usize) -> Vec { - let mut structures = StructureKeys::all_names() - .iter() - .filter_map(|name| StructureKeys::from_name(name)) - .filter(|key| Structure::get(key).step.ordinal() == step) - .collect::>(); - // The enum discriminants preserve the registry order emitted by the data - // extractor. Vanilla uses that per-step order as the feature-seed index. - structures.sort_unstable_by_key(|key| *key as usize); - structures -} - pub trait GenerationCache: HeightLimitView + BlockAccessor { fn get_center_chunk_mut(&mut self) -> &mut ProtoChunk; fn get_center_chunk(&self) -> &ProtoChunk; @@ -1085,11 +1073,8 @@ impl ProtoChunk { let start_block_z = chunk_pos::start_block_z(center_z); let origin_pos = BlockPos::new(start_block_x, min_y, start_block_z); - let population_seed = WorldgenXoroshiro::get_population_seed( - random_config.seed, - start_block_x, - start_block_z, - ); + let population_seed = + Xoroshiro::get_population_seed(random_config.seed, start_block_x, start_block_z); for step in 0..11 { Self::generate_structure_step( @@ -1117,9 +1102,8 @@ impl ProtoChunk { for (p, feature_enum) in features_to_run.into_iter().enumerate() { if let Some(feature) = PLACED_FEATURES.get(&feature_enum) { let decorator_seed = get_decorator_seed(population_seed, p as u64, step as u64); - let mut random = RandomGenerator::WorldgenXoroshiro( - WorldgenXoroshiro::from_seed(decorator_seed), - ); + let mut random = + RandomGenerator::Xoroshiro(Xoroshiro::from_seed(decorator_seed)); feature.generate( cache, @@ -1144,7 +1128,7 @@ impl ProtoChunk { population_seed: u64, world_seed: i64, ) { - let mut tasks = FxHashMap::>::default(); + let mut tasks = Vec::new(); { let center_chunk = cache.get_center_chunk(); let center_x = center_chunk.x; @@ -1156,16 +1140,14 @@ impl ProtoChunk { continue; } - let collector = match instance { - StructureInstance::Start(pos) => pos.collector.clone(), - StructureInstance::Reference(collector) => collector.clone(), - }; - let structure_tasks = tasks.entry(*id).or_default(); - if !structure_tasks - .iter() - .any(|task| Arc::ptr_eq(task, &collector)) - { - structure_tasks.push(collector); + match instance { + StructureInstance::Start(pos) => tasks.push(pos.collector.clone()), + StructureInstance::Reference(collector) => { + let collector_arc = collector.clone(); + if !tasks.iter().any(|t| Arc::ptr_eq(t, &collector_arc)) { + tasks.push(collector_arc); + } + } } } @@ -1186,26 +1168,28 @@ impl ProtoChunk { continue; } - let collector = match instance { + match instance { StructureInstance::Start(pos) => { let start_x = chunk_pos::start_block_x(center_x); let start_z = chunk_pos::start_block_z(center_z); let end_x = start_x + 15; let end_z = start_z + 15; - pos.get_bounding_box() + if pos + .get_bounding_box() .intersects_raw_xz(start_x, start_z, end_x, end_z) - .then(|| pos.collector.clone()) + { + let collector_arc = pos.collector.clone(); + if !tasks.iter().any(|t| Arc::ptr_eq(t, &collector_arc)) { + tasks.push(collector_arc); + } + } } - StructureInstance::Reference(collector) => Some(collector.clone()), - }; - if let Some(collector) = collector { - let structure_tasks = tasks.entry(*id).or_default(); - if !structure_tasks - .iter() - .any(|task| Arc::ptr_eq(task, &collector)) - { - structure_tasks.push(collector); + StructureInstance::Reference(collector) => { + let collector_arc = collector.clone(); + if !tasks.iter().any(|t| Arc::ptr_eq(t, &collector_arc)) { + tasks.push(collector_arc); + } } } } @@ -1214,18 +1198,13 @@ impl ProtoChunk { } } + let decorator_seed = get_decorator_seed(population_seed, 0, step as u64); + let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(decorator_seed)); + let chunk = cache.get_center_chunk_mut(); - for (index, key) in structures_for_step(step).into_iter().enumerate() { - let Some(structure_tasks) = tasks.remove(&key) else { - continue; - }; - let decorator_seed = get_decorator_seed(population_seed, index as u64, step as u64); - let mut random = - RandomGenerator::WorldgenXoroshiro(WorldgenXoroshiro::from_seed(decorator_seed)); - for collector_arc in structure_tasks { - let mut collector = collector_arc.lock().unwrap(); - collector.generate_in_chunk(chunk, block_registry, &mut random, world_seed); - } + for collector_arc in tasks { + let mut collector = collector_arc.lock().unwrap(); + collector.generate_in_chunk(chunk, block_registry, &mut random, world_seed); } } @@ -1291,7 +1270,9 @@ impl ProtoChunk { } let mut candidates = set.structures.to_vec(); - let mut random = create_chunk_random(seed as i64, self.x, self.z); + let carver_seed = get_carver_seed(seed, self.x, self.z); + let mut random: RandomGenerator = + RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed)); let mut total_weight: u32 = candidates.iter().map(|e| e.weight).sum(); @@ -1626,48 +1607,3 @@ impl GenerationCache for ProtoChunk { None } } - -#[cfg(test)] -mod tests { - use super::structures_for_step; - use pumpkin_data::structures::{GenerationStep, StructureKeys}; - use pumpkin_util::random::{ - RandomGenerator, RandomImpl, get_decorator_seed, xoroshiro128::WorldgenXoroshiro, - }; - - #[test] - fn structure_decoration_order_matches_the_vanilla_registry() { - assert_eq!( - structures_for_step(GenerationStep::UndergroundStructures.ordinal()), - [ - StructureKeys::Mineshaft, - StructureKeys::MineshaftMesa, - StructureKeys::BuriedTreasure, - StructureKeys::TrailRuins, - StructureKeys::TrialChambers, - ] - ); - } - - #[test] - fn mineshaft_decoration_streams_match_vanilla_26_2() { - let population_seed = - WorldgenXoroshiro::get_population_seed(123_456_789, -37 * 16, 84 * 16); - assert_eq!(population_seed, 8_245_768_125_123_811_941); - - for (index, expected_int, expected_float, expected_long) in [ - (0, -618_189_195, 0.066_260_1, -8_355_384_254_887_254_362), - (1, 1_059_888_632, 0.664_247_5, 6_337_689_326_094_498_930), - ] { - let seed = get_decorator_seed( - population_seed, - index, - GenerationStep::UndergroundStructures.ordinal() as u64, - ); - let mut random = RandomGenerator::WorldgenXoroshiro(WorldgenXoroshiro::from_seed(seed)); - assert_eq!(random.next_i32(), expected_int); - assert!((random.next_f32() - expected_float).abs() < f32::EPSILON); - assert_eq!(random.next_i64(), expected_long); - } - } -} diff --git a/pumpkin-world/src/generation/structure/mod.rs b/pumpkin-world/src/generation/structure/mod.rs index 191246bbe..2db2b01d2 100644 --- a/pumpkin-world/src/generation/structure/mod.rs +++ b/pumpkin-world/src/generation/structure/mod.rs @@ -11,23 +11,14 @@ 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, MineshaftType}, - 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, ocean_ruin::OceanRuinGenerator, + ruined_portal::RuinedPortalGenerator, shipwreck::ShipwreckGenerator, + stronghold::StrongholdGenerator, swamp_hut::SwampHutGenerator, }, }, }; @@ -127,11 +118,7 @@ pub fn generate_structure_position( StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context), StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => { let generator = MineshaftGenerator { - mineshaft_type: if *key == StructureKeys::MineshaftMesa { - MineshaftType::Mesa - } else { - MineshaftType::Normal - }, + is_mesa: *key == StructureKeys::MineshaftMesa, }; generator.get_structure_position(context) } diff --git a/pumpkin-world/src/generation/structure/placement.rs b/pumpkin-world/src/generation/structure/placement.rs index b707f1529..8921d583e 100644 --- a/pumpkin-world/src/generation/structure/placement.rs +++ b/pumpkin-world/src/generation/structure/placement.rs @@ -6,7 +6,7 @@ use pumpkin_data::structures::{ use pumpkin_util::{ math::floor_div, random::{ - RandomGenerator, RandomImpl, get_region_seed, legacy_rand::LegacyRand, + RandomGenerator, RandomImpl, get_carver_seed, get_region_seed, legacy_rand::LegacyRand, xoroshiro128::Xoroshiro, }, }; @@ -17,7 +17,7 @@ use crate::ProtoChunk; use dashmap::DashMap; use pumpkin_data::structures::StructureKeys; -use super::structures::{StructurePosition, create_chunk_random}; +use super::structures::StructurePosition; /// A thread-safe global cache for structures that require world-wide placement calculations /// rather than localized chunk-based math (e.g., Strongholds using Concentric Rings). /// @@ -267,7 +267,8 @@ fn should_generate_frequency( random.next_f32() < frequency } FrequencyReductionMethod::LegacyType3 => { - let mut random = create_chunk_random(seed, chunk_x, chunk_z); + let carver_seed = get_carver_seed(seed as u64, chunk_x, chunk_z); + let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed)); random.next_f64() < f64::from(frequency) } } @@ -353,10 +354,7 @@ fn is_start_chunk_random_spread( mod tests { use pumpkin_data::{ dimension::Dimension, - structures::{ - FrequencyReductionMethod, RandomSpreadStructurePlacement, StructurePlacementCalculator, - StructureSet, - }, + structures::{RandomSpreadStructurePlacement, StructurePlacementCalculator, StructureSet}, }; use pumpkin_util::random::{ RandomGenerator, RandomImpl, get_region_seed, legacy_rand::LegacyRand, @@ -369,7 +367,7 @@ mod tests { get_world_gen, structure::placement::{ GlobalStructureCache, apply_frequency_reduction, get_start_chunk_random_spread, - is_start_chunk, should_generate_frequency, should_generate_structure, + is_start_chunk, should_generate_structure, }, }, }; @@ -393,26 +391,6 @@ mod tests { assert_eq!(z, 4); } - #[test] - fn legacy_type_3_matches_vanilla_large_feature_random() { - assert!(!should_generate_frequency( - FrequencyReductionMethod::LegacyType3, - 123_456_789, - -37, - 84, - 0, - 0.507_83, - )); - assert!(should_generate_frequency( - FrequencyReductionMethod::LegacyType3, - 123_456_789, - -37, - 84, - 99, - 0.507_831, - )); - } - #[test] fn pillager_outposts_respect_the_village_exclusion_zone() { let seed = 0; diff --git a/pumpkin-world/src/generation/structure/structures/mineshaft.rs b/pumpkin-world/src/generation/structure/structures/mineshaft.rs index 322808053..b963ddc48 100644 --- a/pumpkin-world/src/generation/structure/structures/mineshaft.rs +++ b/pumpkin-world/src/generation/structure/structures/mineshaft.rs @@ -1,313 +1,65 @@ -//! Mineshaft structure generator (normal + mesa variants). -//! -//! Port of vanilla `MineshaftPieces` / `MineshaftStructure`. -//! Procedural piece-based structure: a room at Y=50 branches into corridors, -//! crossings and stairs. Normal uses oak, mesa uses dark oak. - use std::sync::Arc; -use pumpkin_data::{ - Block, BlockState, - block_properties::blocks_movement, - tag::{Taggable, WorldgenBiome}, -}; -use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; +use pumpkin_data::Block; use pumpkin_util::{ - BlockDirection, HeightMap, - math::{block_box::BlockBox, position::BlockPos, vector3::Vector3}, - random::{RandomGenerator, RandomImpl}, + math::{block_box::BlockBox, position::BlockPos}, + random::RandomGenerator, }; use crate::{ ProtoChunk, - generation::structure::{ - piece::StructurePieceType, - structures::{ - StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase, - StructurePiecesCollector, StructurePosition, WorldPortalExt, + generation::{ + positions::chunk_pos::{get_center_x, get_center_z}, + structure::{ + piece::StructurePieceType, + structures::{ + StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase, + StructurePiecesCollector, StructurePosition, WorldPortalExt, + }, }, }, }; -const MAX_DEPTH: u32 = 8; -const BOUND: i32 = 80; -const MAGIC_START_Y: i32 = 50; - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum MineshaftType { - Normal, - Mesa, -} - -impl MineshaftType { - const fn planks(self) -> &'static BlockState { - match self { - Self::Normal => Block::OAK_PLANKS.default_state, - Self::Mesa => Block::DARK_OAK_PLANKS.default_state, - } - } - const fn log(self) -> &'static BlockState { - match self { - Self::Normal => Block::OAK_LOG.default_state, - Self::Mesa => Block::DARK_OAK_LOG.default_state, - } - } - const fn fence(self) -> &'static BlockState { - match self { - Self::Normal => Block::OAK_FENCE.default_state, - Self::Mesa => Block::DARK_OAK_FENCE.default_state, - } - } -} - -#[derive(Clone, Copy)] -struct Attachment { - x: i32, - y: i32, - z: i32, - facing: BlockDirection, - depth: u32, -} - pub struct MineshaftGenerator { - pub mineshaft_type: MineshaftType, + pub is_mesa: bool, } impl StructureGenerator for MineshaftGenerator { fn get_structure_position( &self, - mut context: StructureGeneratorContext<'_>, + context: StructureGeneratorContext<'_>, ) -> Option { - // Vanilla consumes a double here (leftover from the old probability gate). - let _ = context.random.next_f64(); + 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 - 16, + context.min_y, + chunk_center_z - 16, + chunk_center_x + 16, + 256, + chunk_center_z + 16, + ); - let west = context.chunk_x * 16 + 2; - let north = context.chunk_z * 16 + 2; let mut collector = StructurePiecesCollector::default(); - - // Create the starting room (vanilla: fixed Y=50, random width/depth/height). - let room_box = BlockBox::new( - west, - MAGIC_START_Y, - north, - west + 7 + context.random.next_bounded_i32(6), - 54 + context.random.next_bounded_i32(6), - north + 7 + context.random.next_bounded_i32(6), - ); - let start_min_x = room_box.min.x; - let start_min_z = room_box.min.z; - - let mut room = MineshaftRoomPiece { - piece: StructurePiece::new(StructurePieceType::MineshaftRoom, room_box, 0), - mineshaft_type: self.mineshaft_type, - child_entrance_boxes: Vec::new(), - }; - - // The room must participate in intersection checks while its children are - // assembled. Replace the provisional copy afterwards with its entrances. - collector.add_piece(Box::new(room.clone())); - room.add_children( - start_min_x, - start_min_z, - &mut context.random, - &mut collector, - ); - collector.pieces[0] = Box::new(room); - - if collector.pieces.is_empty() { - return None; - } - - let vertical_offset = match self.mineshaft_type { - MineshaftType::Normal => { - collector.shift_into(context.sea_level, context.min_y, &mut context.random, 10) - } - MineshaftType::Mesa => { - let bounds = collector.get_bounding_box(); - let center_x = block_box_center(bounds.min.x, bounds.max.x); - let center_z = block_box_center(bounds.min.z, bounds.max.z); - let surface_y = context - .height_sampler - .as_deref_mut() - .map_or(context.sea_level, |sampler| { - sampler.estimate_height(center_x, center_z) - }); - let target_y = if surface_y <= context.sea_level { - context.sea_level - } else { - context - .random - .next_inbetween_i32(context.sea_level, surface_y) - }; - let offset = target_y - block_box_center(bounds.min.y, bounds.max.y); - collector.shift(offset); - offset - } - }; + collector.add_piece(Box::new(MineshaftPiece { + piece: StructurePiece::new(StructurePieceType::MineshaftCrossing, bounding_box, 0), + is_mesa: self.is_mesa, + })); Some(StructurePosition { - start_pos: BlockPos::new( - context.chunk_x * 16 + 8, - MAGIC_START_Y + vertical_offset, - context.chunk_z * 16, - ), + start_pos: BlockPos::new(chunk_center_x, 64, chunk_center_z), collector: Arc::new(collector.into()), }) } } -fn generate_and_add_piece( - attachment: Attachment, - start_min_x: i32, - start_min_z: i32, - mineshaft_type: MineshaftType, - random: &mut RandomGenerator, - collector: &mut StructurePiecesCollector, -) -> Option { - if attachment.depth > MAX_DEPTH - || (attachment.x - start_min_x).abs() > BOUND - || (attachment.z - start_min_z).abs() > BOUND - { - return None; - } - - let roll = random.next_bounded_i32(100); - if roll >= 80 { - let piece = MineshaftCrossingPiece::create(&attachment, mineshaft_type, random, collector)?; - let bounding_box = piece.piece.bounding_box; - collector.add_piece(Box::new(piece.clone())); - piece.add_children(start_min_x, start_min_z, random, collector); - Some(bounding_box) - } else if roll >= 70 { - let piece = MineshaftStairsPiece::create(&attachment, mineshaft_type, collector)?; - let bounding_box = piece.piece.bounding_box; - collector.add_piece(Box::new(piece.clone())); - piece.add_children(start_min_x, start_min_z, random, collector); - Some(bounding_box) - } else { - let piece = MineshaftCorridorPiece::create(&attachment, mineshaft_type, random, collector)?; - let bounding_box = piece.piece.bounding_box; - collector.add_piece(Box::new(piece.clone())); - piece.add_children(start_min_x, start_min_z, random, collector); - Some(bounding_box) - } -} - -// =========================================================================== -// Room -// =========================================================================== - -#[derive(Clone)] -struct MineshaftRoomPiece { +pub struct MineshaftPiece { piece: StructurePiece, - mineshaft_type: MineshaftType, - child_entrance_boxes: Vec, + is_mesa: bool, } -impl MineshaftRoomPiece { - fn add_children( - &mut self, - start_min_x: i32, - start_min_z: i32, - random: &mut RandomGenerator, - collector: &mut StructurePiecesCollector, - ) { - let room = self.piece.bounding_box; - let x_span = room.max.x - room.min.x + 1; - let z_span = room.max.z - room.min.z + 1; - let height_space = (room.max.y - room.min.y + 1 - 4).max(1); - - for direction in [BlockDirection::North, BlockDirection::South] { - let mut offset = 0; - while offset < x_span { - offset += random.next_bounded_i32(x_span); - if offset + 3 > x_span { - break; - } - let z = if direction == BlockDirection::North { - room.min.z - 1 - } else { - room.max.z + 1 - }; - if let Some(child) = generate_and_add_piece( - Attachment { - x: room.min.x + offset, - y: room.min.y + random.next_bounded_i32(height_space) + 1, - z, - facing: direction, - depth: self.piece.chain_length, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ) { - let (min_z, max_z) = if direction == BlockDirection::North { - (room.min.z, room.min.z + 1) - } else { - (room.max.z - 1, room.max.z) - }; - self.child_entrance_boxes.push(BlockBox::new( - child.min.x, - child.min.y, - min_z, - child.max.x, - child.max.y, - max_z, - )); - } - offset += 4; - } - } - - for direction in [BlockDirection::West, BlockDirection::East] { - let mut offset = 0; - while offset < z_span { - offset += random.next_bounded_i32(z_span); - if offset + 3 > z_span { - break; - } - let x = if direction == BlockDirection::West { - room.min.x - 1 - } else { - room.max.x + 1 - }; - if let Some(child) = generate_and_add_piece( - Attachment { - x, - y: room.min.y + random.next_bounded_i32(height_space) + 1, - z: room.min.z + offset, - facing: direction, - depth: self.piece.chain_length, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ) { - let (min_x, max_x) = if direction == BlockDirection::West { - (room.min.x, room.min.x + 1) - } else { - (room.max.x - 1, room.max.x) - }; - self.child_entrance_boxes.push(BlockBox::new( - min_x, - child.min.y, - child.min.z, - max_x, - child.max.y, - child.max.z, - )); - } - offset += 4; - } - } - } -} - -impl StructurePieceBase for MineshaftRoomPiece { +impl StructurePieceBase for MineshaftPiece { fn as_any(&self) -> &dyn std::any::Any { self } @@ -317,12 +69,6 @@ impl StructurePieceBase for MineshaftRoomPiece { fn get_structure_piece_mut(&mut self) -> &mut StructurePiece { &mut self.piece } - fn translate(&mut self, x: i32, y: i32, z: i32) { - self.piece.translate(x, y, z); - for entrance in &mut self.child_entrance_boxes { - entrance.move_pos(x, y, z); - } - } fn place( &mut self, chunk: &mut ProtoChunk, @@ -331,1616 +77,64 @@ impl StructurePieceBase for MineshaftRoomPiece { _seed: i64, chunk_box: &BlockBox, ) { - if is_in_invalid_location(chunk, &self.piece.bounding_box, chunk_box) { - return; - } - let bb = self.piece.bounding_box; - let cave_air = Block::CAVE_AIR.default_state; - fill_world_box( - chunk, - chunk_box, - &BlockBox::new( - bb.min.x, - bb.min.y + 1, - bb.min.z, - bb.max.x, - (bb.min.y + 3).min(bb.max.y), - bb.max.z, - ), - cave_air, - self.mineshaft_type, - ); - for entrance in &self.child_entrance_boxes { - fill_world_box( - chunk, - chunk_box, - &BlockBox::new( - entrance.min.x, - entrance.max.y - 2, - entrance.min.z, - entrance.max.x, - entrance.max.y, - entrance.max.z, - ), - cave_air, - self.mineshaft_type, - ); - } - fill_upper_half_sphere( - chunk, - chunk_box, - &BlockBox::new( - bb.min.x, - bb.min.y + 4, - bb.min.z, - bb.max.x, - bb.max.y, - bb.max.z, - ), - cave_air, - self.mineshaft_type, - ); - } -} + let origin = self.piece.bounding_box.min; + let start_y = 15; -// =========================================================================== -// Corridor -// =========================================================================== + let wood_planks = if self.is_mesa { + Block::DARK_OAK_PLANKS + } else { + Block::OAK_PLANKS + }; + let wood_fence = if self.is_mesa { + Block::DARK_OAK_FENCE + } else { + Block::OAK_FENCE + }; -#[derive(Clone)] -struct MineshaftCorridorPiece { - piece: StructurePiece, - mineshaft_type: MineshaftType, - num_sections: i32, - has_rails: bool, - spider_corridor: bool, - has_placed_spider: bool, -} + // Draw underground corridors + for y in start_y..(start_y + 3) { + for x in (origin.x - 12)..=(origin.x + 12) { + for z in (origin.z - 12)..=(origin.z + 12) { + let in_center = (x - origin.x).abs() <= 2 && (z - origin.z).abs() <= 2; + let in_ns_corridor = (x - origin.x).abs() <= 1; + let in_ew_corridor = (z - origin.z).abs() <= 1; -impl MineshaftCorridorPiece { - fn create( - att: &Attachment, - mineshaft_type: MineshaftType, - random: &mut RandomGenerator, - collector: &StructurePiecesCollector, - ) -> Option { - let mut corridor_length = random.next_bounded_i32(3) + 2; - while corridor_length > 0 { - let block_length = corridor_length * 5; - let bbox = BlockBox::rotated( - att.x, - att.y, - att.z, - 0, - 0, - 0, - 3, - 3, - block_length, - &att.facing, - ); - if collector.get_intersecting(&bbox).is_none() { - let mut piece = - StructurePiece::new(StructurePieceType::MineshaftCorridor, bbox, att.depth + 1); - piece.set_facing(Some(att.facing)); - let has_rails = random.next_bounded_i32(3) == 0; - let spider_corridor = !has_rails && random.next_bounded_i32(23) == 0; - return Some(Self { - piece, - mineshaft_type, - num_sections: corridor_length, - has_rails, - spider_corridor, - has_placed_spider: false, - }); - } - corridor_length -= 1; - } - None - } -} + if (in_center || in_ns_corridor || in_ew_corridor) + && chunk_box.contains(x, y, z) + { + chunk.set_block_state(x, y, z, Block::AIR.default_state); -impl StructurePieceBase for MineshaftCorridorPiece { - 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 - } - #[expect(clippy::too_many_lines)] - fn place( - &mut self, - chunk: &mut ProtoChunk, - block_registry: &dyn WorldPortalExt, - random: &mut RandomGenerator, - _seed: i64, - chunk_box: &BlockBox, - ) { - if is_in_invalid_location(chunk, &self.piece.bounding_box, chunk_box) { - return; - } - let planks = self.mineshaft_type.planks(); - let fence = self.mineshaft_type.fence(); - let air = Block::CAVE_AIR.default_state; - let cobweb = Block::COBWEB.default_state; - let length = self.num_sections * 5 - 1; - - // Clear the tunnel and irregular ceiling. - for z in 0..=length { - for x in 0..=2 { - add_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - air, - x, - 0, - z, - chunk_box, - ); - add_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - air, - x, - 1, - z, - chunk_box, - ); - } - } - for x in 0..=2 { - for z in 0..=length { - if random.next_f32() <= 0.8 { - add_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - air, - x, - 2, - z, - chunk_box, - ); - } - } - } - - if self.spider_corridor { - for y in 0..=1 { - for x in 0..=2 { - for z in 0..=length { - if random.next_f32() <= 0.6 - && is_interior(chunk, &self.piece, x, y, z, chunk_box) - { - add_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - cobweb, - x, - y, - z, - chunk_box, - ); + if (x - origin.x).abs() == 2 && y == start_y { + chunk.set_block_state(x, y, z, wood_fence.default_state); + } + if (z - origin.z).abs() == 2 && y == start_y { + chunk.set_block_state(x, y, z, wood_fence.default_state); } } } } } - let left_fence_properties = - Block::from_state_id(fence.id).from_properties(&[("west", "true")]); - let left_fence = - BlockState::from_id(left_fence_properties.to_state_id(Block::from_state_id(fence.id))); - let right_fence_properties = - Block::from_state_id(fence.id).from_properties(&[("east", "true")]); - let right_fence = - BlockState::from_id(right_fence_properties.to_state_id(Block::from_state_id(fence.id))); - - for section in 0..self.num_sections { - let z = 2 + section * 5; - if is_supporting_box(chunk, &self.piece, 0, 2, 2, z, chunk_box) { - for y in 0..=1 { - place_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - block_registry, - left_fence, - 0, - y, - z, - chunk_box, - ); - place_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - block_registry, - right_fence, - 2, - y, - z, - chunk_box, - ); - } - if random.next_bounded_i32(4) == 0 { - add_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - planks, - 0, - 2, - z, - chunk_box, - ); - add_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - planks, - 2, - 2, - z, - chunk_box, - ); - } else { - for x in 0..=2 { - add_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - planks, - x, - 2, - z, - chunk_box, - ); + // Place rails and supports + for x in (origin.x - 12)..=(origin.x + 12) { + let y = start_y; + let z = origin.z; + if chunk_box.contains(x, y, z) { + chunk.set_block_state(x, y, z, Block::RAIL.default_state); + } + if x % 5 == 0 { + for sy in start_y..=(start_y + 2) { + if chunk_box.contains(x, sy, z - 1) { + chunk.set_block_state(x, sy, z - 1, wood_fence.default_state); } - maybe_place_wall_torch( - chunk, - &self.piece, - block_registry, - random, - 1, - 2, - z - 1, - "south", - chunk_box, - self.mineshaft_type, - ); - maybe_place_wall_torch( - chunk, - &self.piece, - block_registry, - random, - 1, - 2, - z + 1, - "north", - chunk_box, - self.mineshaft_type, - ); - } - } - - for &(chance, dz) in &[(0.1, -1), (0.1, 1), (0.05, -2), (0.05, 2)] { - for x in [0, 2] { - maybe_place_cobweb( - chunk, - &self.piece, - self.mineshaft_type, - random, - chance, - x, - 2, - z + dz, - chunk_box, - ); - } - } - // Chest minecarts on a randomly oriented rail. - if random.next_bounded_i32(100) == 0 { - place_loot_minecart(chunk, &self.piece, 2, 0, z - 1, chunk_box, random); - } - if random.next_bounded_i32(100) == 0 { - place_loot_minecart(chunk, &self.piece, 0, 0, z + 1, chunk_box, random); - } - // Cave-spider spawner. - if self.spider_corridor && !self.has_placed_spider { - let spawner_z = z - 1 + random.next_bounded_i32(3); - if (0..=length).contains(&spawner_z) - && is_interior(chunk, &self.piece, 1, 0, spawner_z, chunk_box) - { - place_spawner( - chunk, - &self.piece, - 1, - 0, - spawner_z, - chunk_box, - "minecraft:cave_spider", - ); - self.has_placed_spider = true; - } - } - } - - for z in 0..=length { - for x in 0..=2 { - set_planks_block(chunk, &self.piece, planks, x, -1, z, chunk_box); - } - } - - let log_state = self.mineshaft_type.log(); - place_double_lower_or_upper_support( - chunk, - &self.piece, - self.mineshaft_type, - log_state, - -1, - 2, - chunk_box, - ); - if self.num_sections > 1 { - place_double_lower_or_upper_support( - chunk, - &self.piece, - self.mineshaft_type, - log_state, - -1, - length - 2, - chunk_box, - ); - } - - if self.has_rails { - for z in 0..=length { - let below = self.piece.offset_pos(1, -1, z); - let below_state = chunk.get_block_state(&below).to_state(); - if chunk_box.contains_pos(&below) - && !below_state.is_air() - && below_state.is_solid_render() - && random.next_f32() - < if is_interior(chunk, &self.piece, 1, 0, z, chunk_box) { - 0.7 - } else { - 0.9 - } - { - place_mineshaft_block( - chunk, - &self.piece, - self.mineshaft_type, - block_registry, - Block::RAIL.default_state, - 1, - 0, - z, - chunk_box, - ); - } - } - } - } -} - -impl MineshaftCorridorPiece { - #[expect(clippy::too_many_lines)] - fn add_children( - &self, - start_min_x: i32, - start_min_z: i32, - random: &mut RandomGenerator, - collector: &mut StructurePiecesCollector, - ) { - let bb = self.piece.bounding_box; - let selection = random.next_bounded_i32(4); - let (x, z, facing) = match self.piece.facing { - Some(BlockDirection::North) => { - if selection <= 1 { - (bb.min.x, bb.min.z - 1, BlockDirection::North) - } else if selection == 2 { - (bb.min.x - 1, bb.min.z, BlockDirection::West) - } else { - (bb.max.x + 1, bb.min.z, BlockDirection::East) - } - } - Some(BlockDirection::South) => { - if selection <= 1 { - (bb.min.x, bb.max.z + 1, BlockDirection::South) - } else if selection == 2 { - (bb.min.x - 1, bb.max.z - 3, BlockDirection::West) - } else { - (bb.max.x + 1, bb.max.z - 3, BlockDirection::East) - } - } - Some(BlockDirection::West) => { - if selection <= 1 { - (bb.min.x - 1, bb.min.z, BlockDirection::West) - } else if selection == 2 { - (bb.min.x, bb.min.z - 1, BlockDirection::North) - } else { - (bb.min.x, bb.max.z + 1, BlockDirection::South) - } - } - _ => { - if selection <= 1 { - (bb.max.x + 1, bb.min.z, BlockDirection::East) - } else if selection == 2 { - (bb.max.x - 3, bb.min.z - 1, BlockDirection::North) - } else { - (bb.max.x - 3, bb.max.z + 1, BlockDirection::South) - } - } - }; - let _ = generate_and_add_piece( - Attachment { - x, - y: bb.min.y - 1 + random.next_bounded_i32(3), - z, - facing, - depth: self.piece.chain_length, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); - - // Side branches deliberately skip another generation depth in vanilla. - if self.piece.chain_length < MAX_DEPTH { - let is_ns = matches!( - self.piece.facing, - Some(BlockDirection::North | BlockDirection::South) - ); - if is_ns { - let mut z = bb.min.z + 3; - while z + 3 <= bb.max.z { - let sel = random.next_bounded_i32(5); - if sel == 0 { - let _ = generate_and_add_piece( - Attachment { - x: bb.min.x - 1, - y: bb.min.y, - z, - facing: BlockDirection::West, - depth: self.piece.chain_length + 1, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); - } else if sel == 1 { - let _ = generate_and_add_piece( - Attachment { - x: bb.max.x + 1, - y: bb.min.y, - z, - facing: BlockDirection::East, - depth: self.piece.chain_length + 1, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); + if chunk_box.contains(x, sy, z + 1) { + chunk.set_block_state(x, sy, z + 1, wood_fence.default_state); } - z += 5; } - } else { - let mut x = bb.min.x + 3; - while x + 3 <= bb.max.x { - let sel = random.next_bounded_i32(5); - if sel == 0 { - let _ = generate_and_add_piece( - Attachment { - x, - y: bb.min.y, - z: bb.min.z - 1, - facing: BlockDirection::North, - depth: self.piece.chain_length + 1, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); - } else if sel == 1 { - let _ = generate_and_add_piece( - Attachment { - x, - y: bb.min.y, - z: bb.max.z + 1, - facing: BlockDirection::South, - depth: self.piece.chain_length + 1, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); - } - x += 5; + if chunk_box.contains(x, start_y + 2, z) { + chunk.set_block_state(x, start_y + 2, z, wood_planks.default_state); } } } } } - -// =========================================================================== -// Crossing -// =========================================================================== - -#[derive(Clone)] -struct MineshaftCrossingPiece { - piece: StructurePiece, - mineshaft_type: MineshaftType, - is_two_floored: bool, -} - -impl MineshaftCrossingPiece { - fn create( - att: &Attachment, - mineshaft_type: MineshaftType, - random: &mut RandomGenerator, - collector: &StructurePiecesCollector, - ) -> Option { - let is_two_floored = random.next_bounded_i32(4) == 0; - let height = if is_two_floored { 7 } else { 3 }; - let bbox = BlockBox::rotated(att.x, att.y, att.z, -1, 0, 0, 5, height, 5, &att.facing); - if collector.get_intersecting(&bbox).is_some() { - return None; - } - let mut piece = - StructurePiece::new(StructurePieceType::MineshaftCrossing, bbox, att.depth + 1); - piece.set_facing(Some(att.facing)); - Some(Self { - piece, - mineshaft_type, - is_two_floored, - }) - } -} - -impl StructurePieceBase for MineshaftCrossingPiece { - 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, - _br: &dyn WorldPortalExt, - _r: &mut RandomGenerator, - _s: i64, - chunk_box: &BlockBox, - ) { - if is_in_invalid_location(chunk, &self.piece.bounding_box, chunk_box) { - return; - } - let planks = self.mineshaft_type.planks(); - let air = Block::CAVE_AIR.default_state; - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 1, - 0, - 0, - 3, - 2, - 4, - air, - ); - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 0, - 0, - 1, - 4, - 2, - 3, - air, - ); - if self.is_two_floored { - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 1, - 4, - 0, - 3, - 6, - 4, - air, - ); - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 0, - 4, - 1, - 4, - 6, - 3, - air, - ); - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 1, - 3, - 1, - 3, - 3, - 3, - air, - ); - } - - let top = if self.is_two_floored { 6 } else { 2 }; - for &(x, z) in &[(1, 1), (1, 3), (3, 1), (3, 3)] { - let above = self.piece.offset_pos(x, top + 1, z); - if chunk_box.contains_pos(&above) && !chunk.get_block_state(&above).to_state().is_air() - { - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - x, - 0, - z, - x, - top, - z, - planks, - ); - } - } - - for x in 0..=4 { - for z in 0..=4 { - set_planks_block(chunk, &self.piece, planks, x, -1, z, chunk_box); - } - } - } -} - -impl MineshaftCrossingPiece { - fn add_children( - &self, - start_min_x: i32, - start_min_z: i32, - random: &mut RandomGenerator, - collector: &mut StructurePiecesCollector, - ) { - let bb = self.piece.bounding_box; - let depth = self.piece.chain_length; - let direction = self.piece.facing.unwrap_or(BlockDirection::North); - let exits: &[(BlockDirection, i32, i32)] = match direction { - BlockDirection::North => &[ - (BlockDirection::North, bb.min.x + 1, bb.min.z - 1), - (BlockDirection::West, bb.min.x - 1, bb.min.z + 1), - (BlockDirection::East, bb.max.x + 1, bb.min.z + 1), - ], - BlockDirection::South => &[ - (BlockDirection::South, bb.min.x + 1, bb.max.z + 1), - (BlockDirection::West, bb.min.x - 1, bb.min.z + 1), - (BlockDirection::East, bb.max.x + 1, bb.min.z + 1), - ], - BlockDirection::West => &[ - (BlockDirection::North, bb.min.x + 1, bb.min.z - 1), - (BlockDirection::South, bb.min.x + 1, bb.max.z + 1), - (BlockDirection::West, bb.min.x - 1, bb.min.z + 1), - ], - _ => &[ - (BlockDirection::North, bb.min.x + 1, bb.min.z - 1), - (BlockDirection::South, bb.min.x + 1, bb.max.z + 1), - (BlockDirection::East, bb.max.x + 1, bb.min.z + 1), - ], - }; - for &(facing, x, z) in exits { - let _ = generate_and_add_piece( - Attachment { - x, - y: bb.min.y, - z, - facing, - depth, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); - } - - if self.is_two_floored { - for (facing, x, z) in [ - (BlockDirection::North, bb.min.x + 1, bb.min.z - 1), - (BlockDirection::West, bb.min.x - 1, bb.min.z + 1), - (BlockDirection::East, bb.max.x + 1, bb.min.z + 1), - (BlockDirection::South, bb.min.x + 1, bb.max.z + 1), - ] { - if random.next_bool() { - let _ = generate_and_add_piece( - Attachment { - x, - y: bb.min.y + 4, - z, - facing, - depth, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); - } - } - } - } -} - -// =========================================================================== -// Stairs -// =========================================================================== - -#[derive(Clone)] -struct MineshaftStairsPiece { - piece: StructurePiece, - mineshaft_type: MineshaftType, -} - -impl MineshaftStairsPiece { - fn create( - att: &Attachment, - mineshaft_type: MineshaftType, - collector: &StructurePiecesCollector, - ) -> Option { - let bbox = BlockBox::rotated(att.x, att.y, att.z, 0, -5, 0, 3, 8, 9, &att.facing); - if collector.get_intersecting(&bbox).is_some() { - return None; - } - let mut piece = - StructurePiece::new(StructurePieceType::MineshaftStairs, bbox, att.depth + 1); - piece.set_facing(Some(att.facing)); - Some(Self { - piece, - mineshaft_type, - }) - } -} - -impl StructurePieceBase for MineshaftStairsPiece { - 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, - _br: &dyn WorldPortalExt, - _r: &mut RandomGenerator, - _s: i64, - chunk_box: &BlockBox, - ) { - if is_in_invalid_location(chunk, &self.piece.bounding_box, chunk_box) { - return; - } - let air = Block::CAVE_AIR.default_state; - // Carve upper landing. - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 0, - 5, - 0, - 2, - 7, - 1, - air, - ); - // Carve lower landing. - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 0, - 0, - 7, - 2, - 2, - 8, - air, - ); - // Descending steps. - for i in 0..5 { - let y = 5 - i - i32::from(i < 4); - fill_mineshaft( - chunk, - &self.piece, - self.mineshaft_type, - chunk_box, - 0, - y, - 2 + i, - 2, - 7 - i, - 2 + i, - air, - ); - } - } -} - -impl MineshaftStairsPiece { - fn add_children( - &self, - start_min_x: i32, - start_min_z: i32, - random: &mut RandomGenerator, - collector: &mut StructurePiecesCollector, - ) { - let bb = self.piece.bounding_box; - let facing = self.piece.facing.unwrap_or(BlockDirection::North); - let (x, z) = match facing { - BlockDirection::North => (bb.min.x, bb.min.z - 1), - BlockDirection::South => (bb.min.x, bb.max.z + 1), - BlockDirection::West => (bb.min.x - 1, bb.min.z), - _ => (bb.max.x + 1, bb.min.z), - }; - let _ = generate_and_add_piece( - Attachment { - x, - y: bb.min.y, - z, - facing, - depth: self.piece.chain_length, - }, - start_min_x, - start_min_z, - self.mineshaft_type, - random, - collector, - ); - } -} - -// =========================================================================== -// Helpers -// =========================================================================== - -const fn block_box_center(min: i32, max: i32) -> i32 { - min + (max - min + 1) / 2 -} - -#[expect( - clippy::manual_midpoint, - reason = "Java integer division truncates negative sums toward zero" -)] -fn java_average(first: i32, second: i32) -> i32 { - ((i64::from(first) + i64::from(second)) / 2) as i32 -} - -fn is_in_invalid_location(chunk: &ProtoChunk, bb: &BlockBox, chunk_box: &BlockBox) -> bool { - let bounds = BlockBox::new( - (bb.min.x - 1).max(chunk_box.min.x), - (bb.min.y - 1).max(chunk_box.min.y), - (bb.min.z - 1).max(chunk_box.min.z), - (bb.max.x + 1).min(chunk_box.max.x), - (bb.max.y + 1).min(chunk_box.max.y), - (bb.max.z + 1).min(chunk_box.max.z), - ); - let center = Vector3::new( - java_average(bounds.min.x, bounds.max.x), - java_average(bounds.min.y, bounds.max.y), - java_average(bounds.min.z, bounds.max.z), - ); - if chunk - .get_terrain_gen_biome(center.x, center.y, center.z) - .has_tag(&WorldgenBiome::MINECRAFT_MINESHAFT_BLOCKING) - { - return true; - } - - for x in bounds.min.x..=bounds.max.x { - for z in bounds.min.z..=bounds.max.z { - if is_liquid(chunk, x, bounds.min.y, z) || is_liquid(chunk, x, bounds.max.y, z) { - return true; - } - } - } - for x in bounds.min.x..=bounds.max.x { - for y in bounds.min.y..=bounds.max.y { - if is_liquid(chunk, x, y, bounds.min.z) || is_liquid(chunk, x, y, bounds.max.z) { - return true; - } - } - } - for z in bounds.min.z..=bounds.max.z { - for y in bounds.min.y..=bounds.max.y { - if is_liquid(chunk, bounds.min.x, y, z) || is_liquid(chunk, bounds.max.x, y, z) { - return true; - } - } - } - false -} - -fn is_liquid(chunk: &ProtoChunk, x: i32, y: i32, z: i32) -> bool { - chunk - .get_block_state(&Vector3::new(x, y, z)) - .to_state() - .is_liquid() -} - -fn fill_world_box( - chunk: &mut ProtoChunk, - chunk_box: &BlockBox, - bounds: &BlockBox, - state: &BlockState, - mineshaft_type: MineshaftType, -) { - for x in bounds.min.x..=bounds.max.x { - for y in bounds.min.y..=bounds.max.y { - for z in bounds.min.z..=bounds.max.z { - let pos = Vector3::new(x, y, z); - if chunk_box.contains_pos(&pos) - && can_replace_mineshaft(chunk, &pos, mineshaft_type) - { - chunk.set_block_state(x, y, z, state); - } - } - } - } -} - -fn fill_upper_half_sphere( - chunk: &mut ProtoChunk, - chunk_box: &BlockBox, - bounds: &BlockBox, - state: &BlockState, - mineshaft_type: MineshaftType, -) { - let x_span = (bounds.max.x - bounds.min.x + 1) as f32; - let y_span = (bounds.max.y - bounds.min.y + 1) as f32; - let z_span = (bounds.max.z - bounds.min.z + 1) as f32; - let center_x = bounds.min.x as f32 + x_span / 2.0; - let center_z = bounds.min.z as f32 + z_span / 2.0; - for y in bounds.min.y..=bounds.max.y { - let dy = (y - bounds.min.y) as f32 / y_span; - for x in bounds.min.x..=bounds.max.x { - let dx = (x as f32 - center_x) / (x_span * 0.5); - for z in bounds.min.z..=bounds.max.z { - let dz = (z as f32 - center_z) / (z_span * 0.5); - let pos = Vector3::new(x, y, z); - if dx * dx + dy * dy + dz * dz <= 1.05 - && chunk_box.contains_pos(&pos) - && can_replace_mineshaft(chunk, &pos, mineshaft_type) - { - chunk.set_block_state(x, y, z, state); - } - } - } - } -} - -fn can_replace_mineshaft( - chunk: &ProtoChunk, - pos: &Vector3, - mineshaft_type: MineshaftType, -) -> bool { - let block = chunk.get_block_state(pos).to_block(); - block != Block::from_state_id(mineshaft_type.planks().id) - && block != Block::from_state_id(mineshaft_type.log().id) - && block != Block::from_state_id(mineshaft_type.fence().id) - && block != &Block::IRON_CHAIN -} - -#[expect(clippy::too_many_arguments)] -fn add_mineshaft_block( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - mineshaft_type: MineshaftType, - state: &'static BlockState, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, -) { - let pos = piece.offset_pos(x, y, z); - if chunk_box.contains_pos(&pos) && can_replace_mineshaft(chunk, &pos, mineshaft_type) { - piece.add_block(chunk, state, x, y, z, chunk_box); - } -} - -#[expect(clippy::too_many_arguments)] -fn place_mineshaft_block( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - mineshaft_type: MineshaftType, - block_registry: &dyn WorldPortalExt, - state: &'static BlockState, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, -) { - let pos = piece.offset_pos(x, y, z); - if chunk_box.contains_pos(&pos) && can_replace_mineshaft(chunk, &pos, mineshaft_type) { - piece.place_block(chunk, block_registry, state, x, y, z, chunk_box); - } -} - -#[expect(clippy::too_many_arguments)] -fn fill_mineshaft( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - mineshaft_type: MineshaftType, - chunk_box: &BlockBox, - min_x: i32, - min_y: i32, - min_z: i32, - max_x: i32, - max_y: i32, - max_z: i32, - state: &'static BlockState, -) { - for y in min_y..=max_y { - for x in min_x..=max_x { - for z in min_z..=max_z { - add_mineshaft_block(chunk, piece, mineshaft_type, state, x, y, z, chunk_box); - } - } - } -} - -fn is_interior( - chunk: &ProtoChunk, - piece: &StructurePiece, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, -) -> bool { - let pos = piece.offset_pos(x, y + 1, z); - chunk_box.contains_pos(&pos) - && pos.y < current_ocean_floor_height_exclusive(chunk, pos.x, pos.z) -} - -fn current_ocean_floor_height_exclusive(chunk: &ProtoChunk, x: i32, z: i32) -> i32 { - let bottom = i32::from(chunk.bottom_y()); - let stored_top = chunk - .get_top_y(&HeightMap::OceanFloorWg, x, z) - .min(bottom + i32::from(chunk.height())); - - // ProtoChunk's compact generation heightmap is monotonic. Structures can - // carve its previous top block, so validate downward from that cached bound - // to retain Vanilla's live OCEAN_FLOOR_WG semantics. - for y in (bottom..stored_top).rev() { - let state = chunk.get_block_state(&Vector3::new(x, y, z)).to_state(); - if blocks_movement(state, state.id.to_block_id()) { - return y + 1; - } - } - bottom -} - -fn set_planks_block( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - planks: &BlockState, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, -) { - if !is_interior(chunk, piece, x, y, z, chunk_box) { - return; - } - let pos = piece.offset_pos(x, y, z); - if !chunk - .get_block_state(&pos) - .to_state() - .is_side_solid(pumpkin_data::BlockDirection::Up) - { - chunk.set_block_state(pos.x, pos.y, pos.z, planks); - } -} - -fn is_supporting_box( - chunk: &ProtoChunk, - piece: &StructurePiece, - min_x: i32, - max_x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, -) -> bool { - (min_x..=max_x).all(|x| { - let pos = piece.offset_pos(x, y + 1, z); - chunk_box.contains_pos(&pos) && !chunk.get_block_state(&pos).to_state().is_air() - }) -} - -#[expect(clippy::too_many_arguments)] -fn maybe_place_wall_torch( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - block_registry: &dyn WorldPortalExt, - random: &mut RandomGenerator, - x: i32, - y: i32, - z: i32, - facing: &str, - chunk_box: &BlockBox, - mineshaft_type: MineshaftType, -) { - if random.next_f32() < 0.05 { - let properties = Block::WALL_TORCH.from_properties(&[("facing", facing)]); - place_mineshaft_block( - chunk, - piece, - mineshaft_type, - block_registry, - BlockState::from_id(properties.to_state_id(&Block::WALL_TORCH)), - x, - y, - z, - chunk_box, - ); - } -} - -#[expect(clippy::too_many_arguments)] -fn maybe_place_cobweb( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - mineshaft_type: MineshaftType, - random: &mut RandomGenerator, - chance: f32, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, -) { - if !is_interior(chunk, piece, x, y, z, chunk_box) || random.next_f32() >= chance { - return; - } - let pos = piece.offset_pos(x, y, z); - let neighbours = [ - (1, 0, 0, pumpkin_data::BlockDirection::West), - (-1, 0, 0, pumpkin_data::BlockDirection::East), - (0, 1, 0, pumpkin_data::BlockDirection::Down), - (0, -1, 0, pumpkin_data::BlockDirection::Up), - (0, 0, 1, pumpkin_data::BlockDirection::North), - (0, 0, -1, pumpkin_data::BlockDirection::South), - ] - .into_iter() - .filter(|&(dx, dy, dz, face)| { - let neighbour = Vector3::new(pos.x + dx, pos.y + dy, pos.z + dz); - chunk_box.contains_pos(&neighbour) - && chunk - .get_block_state(&neighbour) - .to_state() - .is_side_solid(face) - }) - .count(); - if neighbours >= 2 && can_replace_mineshaft(chunk, &pos, mineshaft_type) { - chunk.set_block_state(pos.x, pos.y, pos.z, Block::COBWEB.default_state); - } -} - -fn place_double_lower_or_upper_support( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - mineshaft_type: MineshaftType, - log: &BlockState, - y: i32, - z: i32, - chunk_box: &BlockBox, -) { - for x in [0, 2] { - let pos = piece.offset_pos(x, y, z); - if chunk_box.contains_pos(&pos) - && chunk.get_block_state(&pos).to_block() - == Block::from_state_id(mineshaft_type.planks().id) - { - fill_pillar_down_or_chain_up(chunk, piece, mineshaft_type, log, x, y, z, chunk_box); - } - } -} - -#[expect(clippy::too_many_arguments)] -fn fill_pillar_down_or_chain_up( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - mineshaft_type: MineshaftType, - log: &BlockState, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, -) { - let base = piece.offset_pos(x, y, z); - if !chunk_box.contains_pos(&base) { - return; - } - let mut distance = 1; - let mut search_down = true; - let mut search_up = true; - while search_down || search_up { - if search_down { - let pos = Vector3::new(base.x, base.y - distance, base.z); - let state = chunk.get_block_state(&pos).to_state(); - let replaceable = - is_structure_replaceable(state) && state.id.to_block() != &Block::LAVA; - if !replaceable && state.is_side_solid(pumpkin_data::BlockDirection::Up) { - for fill_y in (pos.y + 1)..base.y { - chunk.set_block_state(base.x, fill_y, base.z, log); - } - return; - } - search_down = distance <= 20 && replaceable && pos.y > chunk.bottom_y() as i32 + 1; - } - - if search_up { - let pos = Vector3::new(base.x, base.y + distance, base.z); - let state = chunk.get_block_state(&pos).to_state(); - let replaceable = is_structure_replaceable(state); - if !replaceable - && state.is_center_solid(pumpkin_data::BlockDirection::Down) - && !is_falling_block(state.id.to_block()) - { - chunk.set_block_state(base.x, base.y + 1, base.z, mineshaft_type.fence()); - for fill_y in (base.y + 2)..pos.y { - chunk.set_block_state(base.x, fill_y, base.z, Block::IRON_CHAIN.default_state); - } - return; - } - search_up = distance <= 50 - && replaceable - && pos.y < chunk.bottom_y() as i32 + chunk.height() as i32 - 1; - } - distance += 1; - } -} - -fn is_falling_block(block: &Block) -> bool { - block == &Block::SAND - || block == &Block::RED_SAND - || block == &Block::GRAVEL - || block == &Block::DRAGON_EGG - || block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_ANVIL) - || block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_CONCRETE_POWDERS) -} - -fn is_structure_replaceable(state: &BlockState) -> bool { - let block = state.id.to_block(); - state.is_air() - || state.is_liquid() - || block == &Block::GLOW_LICHEN - || block == &Block::SEAGRASS - || block == &Block::TALL_SEAGRASS -} - -fn place_spawner( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, - entity_id: &str, -) { - let pos = piece.offset_pos(x, y, z); - if !chunk_box.contains(pos.x, pos.y, pos.z) { - return; - } - chunk.set_block_state(pos.x, pos.y, pos.z, Block::SPAWNER.default_state); - let mut nbt = NbtCompound::new(); - nbt.put_string("id", "minecraft:mob_spawner".to_string()); - nbt.put_int("x", pos.x); - nbt.put_int("y", pos.y); - nbt.put_int("z", pos.z); - let mut spawn_data = NbtCompound::new(); - let mut entity = NbtCompound::new(); - entity.put_string("id", entity_id.to_string()); - spawn_data.put_compound("entity", entity); - nbt.put_compound("SpawnData", spawn_data); - chunk.add_block_entity(nbt); -} - -fn place_loot_minecart( - chunk: &mut ProtoChunk, - piece: &StructurePiece, - x: i32, - y: i32, - z: i32, - chunk_box: &BlockBox, - random: &mut RandomGenerator, -) -> bool { - let pos = piece.offset_pos(x, y, z); - if !chunk_box.contains(pos.x, pos.y, pos.z) - || !chunk.get_block_state(&pos).to_state().is_air() - || chunk - .get_block_state(&Vector3::new(pos.x, pos.y - 1, pos.z)) - .to_state() - .is_air() - { - return false; - } - - let local_north_south = random.next_bool(); - let piece_north_south = matches!( - piece.facing, - Some(BlockDirection::North | BlockDirection::South) - ); - let rail = if local_north_south == piece_north_south { - Block::RAIL.default_state - } else { - let properties = Block::RAIL.from_properties(&[("shape", "east_west")]); - BlockState::from_id(properties.to_state_id(&Block::RAIL)) - }; - chunk.set_block_state(pos.x, pos.y, pos.z, rail); - - let mut nbt = NbtCompound::new(); - nbt.put_string("id", "minecraft:chest_minecart".to_string()); - nbt.put_list( - "Pos", - vec![ - (f64::from(pos.x) + 0.5).into(), - (f64::from(pos.y) + 0.5).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_string( - "LootTable", - "minecraft:chests/abandoned_mineshaft".to_string(), - ); - nbt.put_long("LootTableSeed", random.next_i64()); - chunk.add_structure_entity(nbt); - true -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::generation::structure::structures::{ - HeightSampler, StructureGeneratorContext, create_chunk_random, - }; - use pumpkin_data::{dimension::Dimension, structures::StructureKeys}; - use pumpkin_util::world_seed::Seed; - - struct FixedHeight(i32); - - impl HeightSampler for FixedHeight { - fn estimate_height(&mut self, _block_x: i32, _block_z: i32) -> i32 { - self.0 - } - } - - const fn direction_id(direction: Option) -> i32 { - match direction { - None => -1, - Some(BlockDirection::North) => 0, - Some(BlockDirection::South) => 1, - Some(BlockDirection::West) => 2, - Some(BlockDirection::East) => 3, - Some(BlockDirection::Up | BlockDirection::Down) => unreachable!(), - } - } - - fn hash_i32(hash: &mut u64, value: i32) { - for byte in value.to_le_bytes() { - *hash ^= u64::from(byte); - *hash = hash.wrapping_mul(0x100_0000_01b3); - } - } - - fn hash_box(hash: &mut u64, bounds: BlockBox) { - for value in [ - bounds.min.x, - bounds.min.y, - bounds.min.z, - bounds.max.x, - bounds.max.y, - bounds.max.z, - ] { - hash_i32(hash, value); - } - } - - fn generation_signature( - seed: i64, - chunk_x: i32, - chunk_z: i32, - mineshaft_type: MineshaftType, - surface_y: i32, - ) -> u64 { - let generator = MineshaftGenerator { mineshaft_type }; - let mut height = FixedHeight(surface_y); - let position = generator - .get_structure_position(StructureGeneratorContext { - seed, - chunk_x, - chunk_z, - random: create_chunk_random(seed, chunk_x, chunk_z), - sea_level: 63, - min_y: -64, - height_sampler: Some(&mut height), - structure_key: Some(match mineshaft_type { - MineshaftType::Normal => StructureKeys::Mineshaft, - MineshaftType::Mesa => StructureKeys::MineshaftMesa, - }), - }) - .expect("mineshaft should generate"); - let collector = position.collector.lock().unwrap(); - let room = collector.pieces[0] - .as_any() - .downcast_ref::() - .expect("first mineshaft piece should be its room"); - let mut hash = 0xcbf2_9ce4_8422_2325; - hash_i32(&mut hash, room.piece.bounding_box.min.y - MAGIC_START_Y); - hash_i32(&mut hash, collector.pieces.len() as i32); - - for piece in &collector.pieces { - let base = piece.get_structure_piece(); - if let Some(room) = piece.as_any().downcast_ref::() { - hash_i32(&mut hash, 0); - hash_i32(&mut hash, base.chain_length as i32); - hash_box(&mut hash, base.bounding_box); - hash_i32(&mut hash, -1); - hash_i32(&mut hash, room.child_entrance_boxes.len() as i32); - for entrance in &room.child_entrance_boxes { - hash_box(&mut hash, *entrance); - } - } else if let Some(corridor) = piece.as_any().downcast_ref::() { - hash_i32(&mut hash, 1); - hash_i32(&mut hash, base.chain_length as i32); - hash_box(&mut hash, base.bounding_box); - hash_i32(&mut hash, direction_id(base.facing)); - hash_i32(&mut hash, i32::from(corridor.has_rails)); - hash_i32(&mut hash, i32::from(corridor.spider_corridor)); - hash_i32(&mut hash, corridor.num_sections); - } else if let Some(crossing) = piece.as_any().downcast_ref::() { - hash_i32(&mut hash, 2); - hash_i32(&mut hash, base.chain_length as i32); - hash_box(&mut hash, base.bounding_box); - hash_i32(&mut hash, -1); - hash_i32(&mut hash, i32::from(crossing.is_two_floored)); - hash_i32(&mut hash, direction_id(base.facing)); - } else { - assert!(piece.as_any().is::()); - hash_i32(&mut hash, 3); - hash_i32(&mut hash, base.chain_length as i32); - hash_box(&mut hash, base.bounding_box); - hash_i32(&mut hash, direction_id(base.facing)); - } - } - hash - } - - #[test] - fn vanilla_center_rounding_is_preserved() { - assert_eq!(block_box_center(0, 5), 3); - assert_eq!(block_box_center(-6, -1), -3); - assert_eq!(java_average(-6, -1), -3); - assert_eq!(java_average(-5, 0), -2); - } - - #[test] - fn live_ocean_floor_scan_accounts_for_structure_carving() { - let generator = crate::generation::get_world_gen( - Seed(0), - Dimension::OVERWORLD, - false, - Vec::new(), - String::new(), - ); - let mut chunk = ProtoChunk::new(0, 0, &generator); - chunk.set_block_state(0, 60, 0, Block::STONE.default_state); - chunk.set_block_state(0, 70, 0, Block::STONE.default_state); - chunk.set_block_state(0, 70, 0, Block::CAVE_AIR.default_state); - - assert_eq!(chunk.get_top_y(&HeightMap::OceanFloorWg, 0, 0), 71); - assert_eq!(current_ocean_floor_height_exclusive(&chunk, 0, 0), 61); - } - - #[test] - fn mineshaft_assembles_multiple_pieces() { - let generator = MineshaftGenerator { - mineshaft_type: MineshaftType::Normal, - }; - let context = StructureGeneratorContext { - seed: 42, - chunk_x: 0, - chunk_z: 0, - random: create_chunk_random(42, 0, 0), - sea_level: 63, - min_y: -64, - height_sampler: None, - structure_key: Some(StructureKeys::Mineshaft), - }; - let position = generator - .get_structure_position(context) - .expect("mineshaft should generate"); - let count = position.collector.lock().unwrap().pieces.len(); - assert!(count > 2, "mineshaft generated only {count} pieces"); - } - - #[test] - fn piece_graphs_match_vanilla_26_2() { - for (seed, chunk_x, chunk_z, kind, surface_y, expected) in [ - (42, 0, 0, MineshaftType::Normal, 90, 0xb8e0_88f3_ab03_af36), - ( - 123_456_789, - -37, - 84, - MineshaftType::Normal, - 90, - 0x74d3_de7e_6584_ba3f, - ), - (0, 100, -200, MineshaftType::Mesa, 90, 0xb7c8_124b_8c6f_1fe4), - ( - -987_654_321, - -12, - -34, - MineshaftType::Mesa, - 120, - 0xfb37_31c2_ce84_f691, - ), - ] { - assert_eq!( - generation_signature(seed, chunk_x, chunk_z, kind, surface_y), - expected, - "piece graph differed for seed {seed} at {chunk_x}, {chunk_z}" - ); - } - } -} diff --git a/pumpkin-world/src/generation/structure/structures/mod.rs b/pumpkin-world/src/generation/structure/structures/mod.rs index d27baca92..4c8c692c8 100644 --- a/pumpkin-world/src/generation/structure/structures/mod.rs +++ b/pumpkin-world/src/generation/structure/structures/mod.rs @@ -579,9 +579,7 @@ impl StructurePiecesCollector { let chunk_z = start_block_z(chunk.z); let chunk_box = BlockBox::new( chunk_x, - // Vanilla's writable structure area deliberately excludes the - // dimension's bottommost block layer. - chunk.bottom_y() as i32 + 1, + chunk.bottom_y() as i32, chunk_z, chunk_x + 15, chunk.bottom_y() as i32 + chunk.height() as i32 - 1, diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index a90822c11..c872e9f3d 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -3192,28 +3192,6 @@ impl Entity { ); } - pub(crate) async fn remove_passenger_on_disconnect(&self, passenger_id: i32) { - let mut passengers = self.passengers.lock().await; - if let Some(index) = passengers - .iter() - .position(|passenger| passenger.get_entity().entity_id == passenger_id) - { - let passenger = passengers.remove(index); - *passenger.get_entity().vehicle.lock().await = None; - } - - let passenger_ids: Vec = passengers - .iter() - .map(|passenger| VarInt(passenger.get_entity().entity_id)) - .collect(); - drop(passengers); - - self.world.load().broadcast_to_chunk( - self.chunk_pos.load(), - &CSetPassengers::new(VarInt(self.entity_id), &passenger_ids), - ); - } - #[allow(clippy::too_many_lines)] pub async fn remove_passenger(&self, passenger_id: i32) { let mut passengers = self.passengers.lock().await; diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index a885bb2b5..cfbea8256 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -120,45 +120,6 @@ use pumpkin_world::chunk_system::ChunkLoading; const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128 const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20 -fn write_root_vehicle(nbt: &mut NbtCompound, uuid: Uuid) { - let value = uuid.as_u128(); - let mut root_vehicle = NbtCompound::new(); - root_vehicle.put( - "Attach", - NbtTag::IntArray(vec![ - (value >> 96) as i32, - (value >> 64) as i32, - (value >> 32) as i32, - value as i32, - ]), - ); - nbt.put("RootVehicle", NbtTag::Compound(root_vehicle)); -} - -fn read_root_vehicle(nbt: &NbtCompound) -> Option { - let root_vehicle = nbt.get_compound("RootVehicle")?; - let uuid = if let Some([most, more, less, least]) = root_vehicle.get_int_array("Attach") { - [*most, *more, *less, *least] - } else { - let [most, more, less, least] = root_vehicle.get_list("Attach")? else { - return None; - }; - [ - most.extract_int()?, - more.extract_int()?, - less.extract_int()?, - least.extract_int()?, - ] - }; - - Some(Uuid::from_u128( - (uuid[0] as u32 as u128) << 96 - | (uuid[1] as u32 as u128) << 64 - | (uuid[2] as u32 as u128) << 32 - | uuid[3] as u32 as u128, - )) -} - pub const DATA_VERSION: i32 = 4903; // 26.2 /// Food exhaustion applied for every block a player mines. @@ -551,7 +512,6 @@ pub struct Player { pub experience_pick_up_delay: Mutex, pub chunk_manager: Mutex, pub has_played_before: AtomicBool, - root_vehicle_uuid: AtomicCell>, pub chat_session: Arc>, pub signature_cache: Mutex, pub player_screen_handler: Arc>, @@ -790,7 +750,6 @@ impl Player { last_food_saturation: AtomicBool::new(true), subscribed_debug_sample: AtomicBool::new(false), has_played_before: AtomicBool::new(false), - root_vehicle_uuid: AtomicCell::new(None), chat_session: Arc::new(Mutex::new(ChatSession::default())), // Placeholder value until the player actually sets their session id signature_cache: Mutex::new(MessageCache::default()), player_screen_handler: player_screen_handler.clone(), @@ -959,16 +918,6 @@ impl Player { /// Removes the [`Player`] out of the current [`World`]. pub async fn remove(self: &Arc) { - let vehicle = self.living_entity.entity.vehicle.lock().await.clone(); - if let Some(vehicle) = vehicle { - self.root_vehicle_uuid - .store(Some(vehicle.get_entity().entity_uuid)); - vehicle - .get_entity() - .remove_passenger_on_disconnect(self.entity_id()) - .await; - } - self.stats .lock() .await @@ -1015,21 +964,6 @@ impl Player { //self.world().level.list_cached(); } - pub(crate) async fn try_restore_vehicle(self: &Arc, vehicle: &Arc) { - let Some(expected_uuid) = self.root_vehicle_uuid.swap(None) else { - return; - }; - if vehicle.get_entity().entity_uuid != expected_uuid { - self.root_vehicle_uuid.store(Some(expected_uuid)); - return; - } - - vehicle - .get_entity() - .add_passenger(vehicle.clone(), self.clone()) - .await; - } - #[expect(clippy::too_many_lines)] pub async fn attack(&self, victim: Arc) { let world = self.world(); @@ -4733,18 +4667,6 @@ impl NBTStorage for Player { nbt.put_bool("SpawnForced", respawn.force); } nbt.put_int("XpSeed", self.enchantment_seed.load(Ordering::Relaxed)); - let vehicle_uuid = self - .living_entity - .entity - .vehicle - .lock() - .await - .as_ref() - .map(|vehicle| vehicle.get_entity().entity_uuid) - .or_else(|| self.root_vehicle_uuid.load()); - if let Some(vehicle_uuid) = vehicle_uuid { - write_root_vehicle(nbt, vehicle_uuid); - } self.stats.lock().await.write_nbt(nbt); }) } @@ -4815,7 +4737,6 @@ impl NBTStorage for Player { nbt.get_int("XpSeed").unwrap_or(rand::random()), Ordering::Relaxed, ); - self.root_vehicle_uuid.store(read_root_vehicle(nbt)); self.stats.lock().await.read_nbt(nbt); }) } @@ -5742,45 +5663,3 @@ impl InventoryPlayer for Player { }) } } - -#[cfg(test)] -mod tests { - use super::{read_root_vehicle, write_root_vehicle}; - use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; - use uuid::Uuid; - - #[test] - fn root_vehicle_uuid_round_trips_with_vanilla_shape() { - let expected = Uuid::from_u128(0xFEDC_BA98_7654_3210_89AB_CDEF_0123_4567); - let mut nbt = NbtCompound::new(); - - write_root_vehicle(&mut nbt, expected); - - assert_eq!(read_root_vehicle(&nbt), Some(expected)); - assert!( - nbt.get_compound("RootVehicle") - .and_then(|root| root.get_int_array("Attach")) - .is_some() - ); - } - - #[test] - fn root_vehicle_uuid_accepts_integer_lists() { - let expected = Uuid::from_u128(0xFEDC_BA98_7654_3210_89AB_CDEF_0123_4567); - let value = expected.as_u128(); - let mut root_vehicle = NbtCompound::new(); - root_vehicle.put( - "Attach", - NbtTag::List(vec![ - NbtTag::Int((value >> 96) as i32), - NbtTag::Int((value >> 64) as i32), - NbtTag::Int((value >> 32) as i32), - NbtTag::Int(value as i32), - ]), - ); - let mut nbt = NbtCompound::new(); - nbt.put("RootVehicle", NbtTag::Compound(root_vehicle)); - - assert_eq!(read_root_vehicle(&nbt), Some(expected)); - } -} diff --git a/pumpkin/src/entity/projectile/arrow.rs b/pumpkin/src/entity/projectile/arrow.rs index 4b69b62c8..0f9fbf8ed 100644 --- a/pumpkin/src/entity/projectile/arrow.rs +++ b/pumpkin/src/entity/projectile/arrow.rs @@ -440,7 +440,6 @@ impl EntityBase for ArrowEntity { }) } - #[expect(clippy::too_many_lines)] fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> { Box::pin(async move { let entity = self.get_entity(); @@ -505,14 +504,7 @@ impl EntityBase for ArrowEntity { } let damage_succeeded = target - .damage_with_context( - &*target, - damage as f32, - DamageType::ARROW, - Some(hit_pos), - None, - Some(self), - ) + .damage(&*target, damage as f32, DamageType::ARROW) .await; if let Some(living) = target.get_living_entity() { diff --git a/pumpkin/src/entity/vehicle/minecart.rs b/pumpkin/src/entity/vehicle/minecart.rs index 4f16982b8..e65f9a023 100644 --- a/pumpkin/src/entity/vehicle/minecart.rs +++ b/pumpkin/src/entity/vehicle/minecart.rs @@ -1,43 +1,22 @@ -mod chest; -mod container; -mod furnace; -mod hopper; -mod rideable; -mod tnt; - use std::sync::Arc; -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use pumpkin_protocol::java::server::play::SPlayerInput; -use rand::RngExt; use crate::{ entity::{ - Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity, - player::Player, + Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player, }, server::Server, }; use pumpkin_data::Block; use pumpkin_data::block_properties::{BlockProperties, PoweredRailLikeProperties}; -use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; -use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; -use pumpkin_data::tag::{self, Taggable}; -use pumpkin_nbt::compound::NbtCompound; -use pumpkin_util::GameMode; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; -use pumpkin_world::inventory::Inventory; use crate::entity::vehicle::vehicle::VehicleEntity; -use chest::ChestMinecart; -use container::MinecartInventory; -use furnace::FurnaceMinecart; -use hopper::HopperMinecart; -use rideable::RideableMinecart; -use tnt::TntMinecart; const fn get_exits( shape: pumpkin_data::block_properties::RailShape, @@ -59,96 +38,21 @@ const fn get_exits( pub struct MinecartEntity { pub vehicle: VehicleEntity, - kind: MinecartKind, -} - -enum MinecartKind { - Rideable(RideableMinecart), - Chest(ChestMinecart), - Furnace(FurnaceMinecart), - Hopper(HopperMinecart), - Tnt(TntMinecart), - Other, + pub active: AtomicBool, + pub tnt_fuse: AtomicI32, } impl MinecartEntity { - const DEFAULT_GRAVITY: f64 = 0.04; - const WATER_GRAVITY: f64 = 0.005; - - pub fn new(entity: Entity) -> Self { - let kind = match entity.entity_type.id { - id if id == EntityType::MINECART.id => MinecartKind::Rideable(RideableMinecart), - id if id == EntityType::CHEST_MINECART.id => MinecartKind::Chest(ChestMinecart::new()), - id if id == EntityType::FURNACE_MINECART.id => { - MinecartKind::Furnace(FurnaceMinecart::new()) - } - id if id == EntityType::HOPPER_MINECART.id => { - MinecartKind::Hopper(HopperMinecart::new()) - } - id if id == EntityType::TNT_MINECART.id => MinecartKind::Tnt(TntMinecart::new()), - _ => MinecartKind::Other, - }; + pub const fn new(entity: Entity) -> Self { Self { vehicle: VehicleEntity::new(entity), - kind, + active: AtomicBool::new(false), + tnt_fuse: AtomicI32::new(-1), } } - - const fn container(&self) -> Option<&Arc> { - match &self.kind { - MinecartKind::Chest(minecart) => Some(minecart.inventory()), - MinecartKind::Hopper(minecart) => Some(minecart.inventory()), - _ => None, - } - } - - const fn drop_item(&self) -> Option<&'static Item> { - match &self.kind { - MinecartKind::Chest(_) => Some(&Item::CHEST_MINECART), - MinecartKind::Furnace(_) => Some(&Item::FURNACE_MINECART), - MinecartKind::Hopper(_) => Some(&Item::HOPPER_MINECART), - MinecartKind::Tnt(_) => Some(&Item::TNT_MINECART), - _ => None, - } - } - - const fn apply_gravity(mut velocity: Vector3, touching_water: bool) -> Vector3 { - velocity.y -= if touching_water { - Self::WATER_GRAVITY - } else { - Self::DEFAULT_GRAVITY - }; - velocity - } } -impl NBTStorage for MinecartEntity { - fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { - Box::pin(async move { - self.vehicle.entity.write_nbt(nbt).await; - match &self.kind { - MinecartKind::Chest(minecart) => minecart.write_nbt(nbt).await, - MinecartKind::Furnace(minecart) => minecart.write_nbt(nbt), - MinecartKind::Hopper(minecart) => minecart.write_nbt(nbt).await, - MinecartKind::Tnt(minecart) => minecart.write_nbt(nbt), - MinecartKind::Rideable(_) | MinecartKind::Other => {} - } - }) - } - - fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { - Box::pin(async move { - self.vehicle.entity.read_nbt_non_mut(nbt).await; - match &self.kind { - MinecartKind::Chest(minecart) => minecart.read_nbt(nbt), - MinecartKind::Furnace(minecart) => minecart.read_nbt(nbt), - MinecartKind::Hopper(minecart) => minecart.read_nbt(nbt), - MinecartKind::Tnt(minecart) => minecart.read_nbt(nbt), - MinecartKind::Rideable(_) | MinecartKind::Other => {} - } - }) - } -} +impl NBTStorage for MinecartEntity {} impl EntityBase for MinecartEntity { #[allow(clippy::too_many_lines)] @@ -159,9 +63,6 @@ impl EntityBase for MinecartEntity { ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.vehicle.tick(); - if let MinecartKind::Furnace(minecart) = &self.kind { - minecart.tick(&self.vehicle.entity); - } let world = self.vehicle.entity.world.load(); let pos = self.vehicle.entity.pos.load(); @@ -203,21 +104,11 @@ impl EntityBase for MinecartEntity { } } - if !is_on_rails { - let velocity = Self::apply_gravity( - self.vehicle.entity.velocity.load(), - self.vehicle.entity.touching_water.load(Ordering::Relaxed), - ); - self.vehicle.entity.velocity.store(velocity); - } - if is_powered_rail || is_activator_rail { let props = PoweredRailLikeProperties::from_state_id(state_id, block); let powered = props.powered; - if is_activator_rail && let MinecartKind::Hopper(minecart) = &self.kind { - minecart.set_enabled(!powered); - } + self.active.store(powered, Ordering::Relaxed); if powered { if is_powered_rail { @@ -243,27 +134,27 @@ impl EntityBase for MinecartEntity { } self.vehicle.entity.send_velocity(); } else if is_activator_rail { - match &self.kind { - MinecartKind::Tnt(minecart) => { - minecart.prime(&self.vehicle.entity, 80); + let passengers = self.vehicle.entity.passengers.lock().await.clone(); + for passenger in passengers { + let passenger_id = passenger.get_entity().entity_id; + self.vehicle.entity.remove_passenger(passenger_id).await; + } + + if self.vehicle.entity.entity_type.id == EntityType::TNT_MINECART.id { + let fuse = self.tnt_fuse.load(Ordering::Relaxed); + if fuse == -1 { + self.tnt_fuse.store(80, Ordering::Relaxed); + world.play_sound( + pumpkin_data::sound::Sound::EntityTntPrimed, + pumpkin_data::sound::SoundCategory::Blocks, + &pos, + ); } - MinecartKind::Rideable(_) => { - let passengers = - self.vehicle.entity.passengers.lock().await.clone(); - for passenger in passengers { - self.vehicle - .entity - .remove_passenger(passenger.get_entity().entity_id) - .await; - } - if self.vehicle.get_hurt_time() == 0 { - self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir()); - self.vehicle.set_hurt_time(10); - self.vehicle.set_damage(50.0); - self.vehicle.send_wobble_metadata(); - } - } - _ => {} + } else if self.vehicle.get_hurt_time() == 0 { + self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir()); + self.vehicle.set_hurt_time(10); + self.vehicle.set_damage(50.0); + // TODO: Send entity status } } } else if is_powered_rail { @@ -277,10 +168,27 @@ impl EntityBase for MinecartEntity { } } - if let MinecartKind::Tnt(minecart) = &self.kind - && minecart.tick(&self.vehicle.entity).await - { - return; + if self.vehicle.entity.entity_type.id == EntityType::TNT_MINECART.id { + let fuse = self.tnt_fuse.load(Ordering::Relaxed); + if fuse > 0 { + let new_fuse = fuse - 1; + self.tnt_fuse.store(new_fuse, Ordering::Relaxed); + + if new_fuse % 2 == 0 { + world.spawn_particle( + pos, + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 1, + pumpkin_data::particle::Particle::Smoke, + ); + } + + if new_fuse == 0 { + self.vehicle.entity.remove().await; + world.explode(pos, 4.0).await; + } + } } let mut velocity = self.vehicle.entity.velocity.load(); @@ -431,24 +339,6 @@ impl EntityBase for MinecartEntity { if velocity.length() > 0.001 { self.move_entity(caller, velocity).await; - - if let MinecartKind::Tnt(minecart) = &self.kind - && self - .vehicle - .entity - .horizontal_collision - .load(Ordering::Relaxed) - && velocity.x.mul_add(velocity.x, velocity.z * velocity.z) >= 0.01 - { - minecart - .explode( - &self.vehicle.entity, - velocity.x.mul_add(velocity.x, velocity.z * velocity.z), - ) - .await; - return; - } - let new_pos = self.vehicle.entity.pos.load(); let passengers = self.vehicle.entity.passengers.lock().await; @@ -460,7 +350,7 @@ impl EntityBase for MinecartEntity { self.vehicle.entity.send_pos_rot(); #[allow(clippy::useless_let_if_seq)] - let mut friction = 0.95; // Vanilla minecart air drag + let mut friction = 0.98; // Default off-rail air drag if is_on_rails { let passengers = self.vehicle.entity.passengers.lock().await; @@ -489,23 +379,12 @@ impl EntityBase for MinecartEntity { } } - let mut next_vel = - if is_on_rails && let MinecartKind::Furnace(minecart) = &self.kind { - minecart.velocity(&self.vehicle.entity, velocity) - } else if is_on_rails && let Some(inventory) = self.container() { - container::velocity(&self.vehicle.entity, inventory, velocity).await - } else { - velocity.multiply(friction, friction, friction) - }; + let mut next_vel = velocity.multiply(friction, friction, friction); if next_vel.length() < 0.005 { next_vel = Vector3::new(0.0, 0.0, 0.0); } self.vehicle.entity.velocity.store(next_vel); } - - if let MinecartKind::Hopper(minecart) = &self.kind { - minecart.tick(&self.vehicle.entity).await; - } }) } @@ -669,9 +548,6 @@ impl EntityBase for MinecartEntity { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { self.vehicle.send_wobble_metadata(); - if let MinecartKind::Furnace(minecart) = &self.kind { - minecart.init_data_tracker(&self.vehicle.entity); - } }) } @@ -683,108 +559,47 @@ impl EntityBase for MinecartEntity { &'a self, _caller: &'a dyn EntityBase, amount: f32, - damage_type: DamageType, + _damage_type: pumpkin_data::damage::DamageType, _position: Option>, source: Option<&'a dyn EntityBase>, - cause: Option<&'a dyn EntityBase>, + _cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { - Box::pin(async move { - let creative = source - .and_then(EntityBase::get_player) - .is_some_and(|player| player.gamemode.load() == GameMode::Creative); - - if let MinecartKind::Tnt(minecart) = &self.kind - && damage_type == DamageType::ARROW - && self.vehicle.entity.fire_ticks.load(Ordering::Relaxed) > 0 - { - let projectile_speed_squared = cause - .map(|entity| entity.get_entity().velocity.load().length_squared()) - .unwrap_or_default(); - minecart - .explode(&self.vehicle.entity, projectile_speed_squared) - .await; - if self.vehicle.entity.is_removed() { - return true; - } - } - - let will_break = self.vehicle.entity.is_alive() - && (creative || self.vehicle.get_damage() + amount * 10.0 > 40.0); - - if let MinecartKind::Tnt(minecart) = &self.kind - && will_break - && !creative - { - let velocity = self.vehicle.entity.velocity.load(); - let speed_squared = velocity.x.mul_add(velocity.x, velocity.z * velocity.z); - let ignites = damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FIRE) - || damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_EXPLOSION) - || self.vehicle.entity.fire_ticks.load(Ordering::Relaxed) > 0; - if ignites || speed_squared >= 0.01 { - self.vehicle.apply_damage_wobble(amount); - let fuse = rand::rng().random_range(0..20) + rand::rng().random_range(0..20); - if self - .vehicle - .entity - .world - .load() - .level_info - .load() - .game_rules - .tnt_explodes - { - minecart.prime(&self.vehicle.entity, fuse); - } else { - minecart.set_fuse(fuse); - } - return true; - } - } - - let damaged = self.vehicle.damage_with_context(amount, source).await; - - if will_break && !creative && self.vehicle.entity.is_removed() { - let world = self.vehicle.entity.world.load(); - if world.level_info.load().game_rules.entity_drops { - let position = self.vehicle.entity.block_pos.load(); - if let Some(container) = self.container() - && container.claim_drops() - { - container.unpack_loot().await; - let inventory: Arc = container.clone(); - world.scatter_inventory(&position, &inventory).await; - } - if let Some(item) = self.drop_item() { - world.drop_stack(&position, ItemStack::new(1, item)).await; - } - } - } - - damaged - }) + Box::pin(async move { self.vehicle.damage_with_context(amount, source).await }) } fn interact<'a>( &'a self, player: &'a Arc, - item_stack: &'a mut ItemStack, + _item_stack: &'a mut ItemStack, ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { - match &self.kind { - MinecartKind::Chest(minecart) => { - minecart.interact(&self.vehicle.entity, player).await - } - MinecartKind::Furnace(minecart) => { - minecart.interact(&self.vehicle.entity, player, item_stack) - } - MinecartKind::Hopper(minecart) => { - minecart.interact(&self.vehicle.entity, player).await - } - MinecartKind::Rideable(minecart) => { - minecart.interact(&self.vehicle.entity, player).await - } - MinecartKind::Tnt(_) | MinecartKind::Other => false, + if player.get_entity().is_sneaking() { + return false; } + + if !self.vehicle.entity.passengers.lock().await.is_empty() { + return false; + } + + if player.get_entity().has_vehicle().await { + return false; + } + + let world = self.vehicle.entity.world.load(); + let Some(vehicle) = world.get_entity_by_id(self.vehicle.entity.entity_id) else { + return false; + }; + + let Some(passenger) = world.get_player_by_id(player.entity_id()) else { + return false; + }; + + self.vehicle + .entity + .add_passenger(vehicle, passenger as Arc) + .await; + + true }) } @@ -860,22 +675,3 @@ impl EntityBase for MinecartEntity { self } } - -#[cfg(test)] -mod tests { - use pumpkin_util::math::vector3::Vector3; - - use super::MinecartEntity; - - #[test] - fn minecart_gravity_matches_vanilla() { - assert_eq!( - MinecartEntity::apply_gravity(Vector3::new(0.4, 0.0, 0.0), false), - Vector3::new(0.4, -0.04, 0.0) - ); - assert_eq!( - MinecartEntity::apply_gravity(Vector3::new(0.4, 0.0, 0.0), true), - Vector3::new(0.4, -0.005, 0.0) - ); - } -} diff --git a/pumpkin/src/entity/vehicle/minecart/chest.rs b/pumpkin/src/entity/vehicle/minecart/chest.rs deleted file mode 100644 index 20e7b75cf..000000000 --- a/pumpkin/src/entity/vehicle/minecart/chest.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::sync::Arc; - -use pumpkin_data::translation; -use pumpkin_nbt::compound::NbtCompound; -use pumpkin_util::text::TextComponent; - -use crate::entity::{Entity, player::Player}; - -use super::container::{self, MinecartInventory}; - -pub(super) struct ChestMinecart { - inventory: Arc, -} - -impl ChestMinecart { - pub(super) fn new() -> Self { - Self { - inventory: Arc::new(MinecartInventory::new(27)), - } - } - - pub(super) const fn inventory(&self) -> &Arc { - &self.inventory - } - - pub(super) async fn interact(&self, entity: &Entity, player: &Arc) -> bool { - container::open( - entity, - player, - &self.inventory, - TextComponent::translate_cross( - translation::java::ENTITY_MINECRAFT_CHEST_MINECART, - translation::bedrock::ENTITY_CHEST_MINECART_NAME, - [], - ), - false, - ) - .await - } - - pub(super) async fn write_nbt(&self, nbt: &mut NbtCompound) { - self.inventory.write_nbt(nbt).await; - } - - pub(super) fn read_nbt(&self, nbt: &NbtCompound) { - self.inventory.read_nbt(nbt); - } -} diff --git a/pumpkin/src/entity/vehicle/minecart/container.rs b/pumpkin/src/entity/vehicle/minecart/container.rs deleted file mode 100644 index 2033d714e..000000000 --- a/pumpkin/src/entity/vehicle/minecart/container.rs +++ /dev/null @@ -1,283 +0,0 @@ -use std::any::Any; -use std::pin::Pin; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex as StdMutex}; - -use pumpkin_data::item_stack::ItemStack; -use pumpkin_inventory::generic_container_screen_handler::{create_generic_9x3, create_hopper}; -use pumpkin_inventory::player::player_inventory::PlayerInventory; -use pumpkin_inventory::screen_handler::{ - BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler, -}; -use pumpkin_nbt::compound::NbtCompound; -use pumpkin_util::math::vector3::Vector3; -use pumpkin_util::text::TextComponent; -use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture, split_stack}; -use tokio::sync::Mutex; - -use crate::entity::{Entity, EntityBase, player::Player}; -use crate::world::loot::fill_chest_inventory; -use pumpkin_data::chest_loot_table::get_chest_loot_table; - -pub(super) struct MinecartInventory { - items: Box<[Arc>]>, - loot_table: StdMutex>, - drops_claimed: AtomicBool, -} - -impl MinecartInventory { - pub(super) fn new(size: usize) -> Self { - Self { - items: (0..size) - .map(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone()))) - .collect(), - loot_table: StdMutex::new(None), - drops_claimed: AtomicBool::new(false), - } - } - - pub(super) fn claim_drops(&self) -> bool { - !self.drops_claimed.swap(true, Ordering::AcqRel) - } - - pub(super) fn read_nbt(&self, nbt: &NbtCompound) { - let loot_table = nbt.get_string("LootTable").map(|loot_table| { - ( - loot_table.to_owned(), - nbt.get_long("LootTableSeed").unwrap_or(0), - ) - }); - let has_loot_table = loot_table.is_some(); - *self.loot_table.lock().expect("Loot table mutex poisoned") = loot_table; - - if !has_loot_table { - self.read_data(nbt, &self.items); - } - } - - pub(super) async fn write_nbt(&self, nbt: &mut NbtCompound) { - let loot_table = self - .loot_table - .lock() - .expect("Loot table mutex poisoned") - .clone(); - if let Some((loot_table, seed)) = loot_table { - nbt.put_string("LootTable", loot_table); - if seed != 0 { - nbt.put_long("LootTableSeed", seed); - } - } else { - self.write_inventory_nbt(nbt, true).await; - } - } - - pub(super) fn has_loot_table(&self) -> bool { - self.loot_table - .lock() - .expect("Loot table mutex poisoned") - .is_some() - } - - pub(super) async fn unpack_loot(self: &Arc) { - let loot_table = self - .loot_table - .lock() - .expect("Loot table mutex poisoned") - .take(); - let Some((loot_table, seed)) = loot_table else { - return; - }; - let Some(table) = get_chest_loot_table(&loot_table) else { - *self.loot_table.lock().expect("Loot table mutex poisoned") = Some((loot_table, seed)); - return; - }; - - let inventory: Arc = self.clone(); - fill_chest_inventory(&inventory, table, seed).await; - } -} - -impl Inventory for MinecartInventory { - fn size(&self) -> usize { - self.items.len() - } - - fn is_empty(&self) -> InventoryFuture<'_, bool> { - Box::pin(async move { - for slot in &self.items { - if !slot.lock().await.is_empty() { - return false; - } - } - true - }) - } - - fn get_stack(&self, slot: usize) -> InventoryFuture<'_, Arc>> { - Box::pin(async move { self.items[slot].clone() }) - } - - fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> { - Box::pin(async move { - let mut removed = ItemStack::EMPTY.clone(); - std::mem::swap(&mut removed, &mut *self.items[slot].lock().await); - removed - }) - } - - fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> { - Box::pin(async move { split_stack(&self.items, slot, amount).await }) - } - - fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> { - Box::pin(async move { - *self.items[slot].lock().await = stack; - }) - } - - fn as_any(&self) -> &dyn Any { - self - } -} - -impl Clearable for MinecartInventory { - fn clear(&self) -> Pin + Send + '_>> { - Box::pin(async move { - for slot in &self.items { - *slot.lock().await = ItemStack::EMPTY.clone(); - } - }) - } -} - -struct MinecartScreenFactory { - inventory: Arc, - title: TextComponent, - hopper: bool, -} - -impl ScreenHandlerFactory for MinecartScreenFactory { - fn create_screen_handler<'a>( - &'a self, - sync_id: u8, - player_inventory: &'a Arc, - _player: &'a dyn InventoryPlayer, - ) -> BoxFuture<'a, Option> { - Box::pin(async move { - let inventory: Arc = self.inventory.clone(); - let handler = if self.hopper { - create_hopper(sync_id, player_inventory, inventory).await - } else { - create_generic_9x3(sync_id, player_inventory, inventory).await - }; - Some(Arc::new(Mutex::new(handler)) as SharedScreenHandler) - }) - } - - fn get_display_name(&self) -> TextComponent { - self.title.clone() - } -} - -pub(super) async fn open( - entity: &Entity, - player: &Arc, - inventory: &Arc, - title: TextComponent, - hopper: bool, -) -> bool { - if player.is_spectator() && inventory.has_loot_table() { - return false; - } - if !player.is_spectator() { - inventory.unpack_loot().await; - } - - player - .open_handled_screen( - &MinecartScreenFactory { - inventory: inventory.clone(), - title: entity.custom_name.load().as_ref().clone().unwrap_or(title), - hopper, - }, - None, - ) - .await - .is_some() -} - -pub(super) async fn velocity( - entity: &Entity, - inventory: &MinecartInventory, - velocity: Vector3, -) -> Vector3 { - let signal = crate::block::calculate_comparator_output(inventory).await; - let mut friction = if inventory.has_loot_table() { - 0.98 - } else { - 0.98 + f64::from(15 - signal) * 0.001 - }; - if entity - .touching_water - .load(std::sync::atomic::Ordering::Relaxed) - { - friction *= 0.95; - } - velocity.multiply(friction, 0.0, friction) -} - -#[cfg(test)] -mod tests { - use super::MinecartInventory; - use pumpkin_data::item::Item; - use pumpkin_data::item_stack::ItemStack; - use pumpkin_nbt::compound::NbtCompound; - use pumpkin_world::inventory::Inventory; - - #[tokio::test] - async fn deferred_mineshaft_loot_is_preserved_until_unpacked() { - let inventory = std::sync::Arc::new(MinecartInventory::new(27)); - let mut source = NbtCompound::new(); - source.put_string( - "LootTable", - "minecraft:chests/abandoned_mineshaft".to_string(), - ); - source.put_long("LootTableSeed", 1234); - inventory.read_nbt(&source); - - let mut deferred = NbtCompound::new(); - inventory.write_nbt(&mut deferred).await; - assert_eq!( - deferred.get_string("LootTable"), - Some("minecraft:chests/abandoned_mineshaft") - ); - assert_eq!(deferred.get_long("LootTableSeed"), Some(1234)); - assert!(deferred.get_list("Items").is_none()); - - inventory.unpack_loot().await; - assert!(!inventory.is_empty().await); - - let mut unpacked = NbtCompound::new(); - inventory.write_nbt(&mut unpacked).await; - assert!(unpacked.get_string("LootTable").is_none()); - assert!(unpacked.get_list("Items").is_some()); - } - - #[tokio::test] - async fn chest_minecart_items_round_trip_through_nbt() { - let inventory = MinecartInventory::new(27); - inventory - .set_stack(8, ItemStack::new(3, &Item::POWERED_RAIL)) - .await; - - let mut nbt = NbtCompound::new(); - inventory.write_nbt(&mut nbt).await; - - let restored = MinecartInventory::new(27); - restored.read_nbt(&nbt); - let stack = restored.get_stack(8).await; - let stack = stack.lock().await; - assert_eq!(stack.get_item().id, Item::POWERED_RAIL.id); - assert_eq!(stack.item_count, 3); - } -} diff --git a/pumpkin/src/entity/vehicle/minecart/furnace.rs b/pumpkin/src/entity/vehicle/minecart/furnace.rs deleted file mode 100644 index b8e13ba7b..000000000 --- a/pumpkin/src/entity/vehicle/minecart/furnace.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::sync::atomic::{AtomicI32, Ordering}; - -use crossbeam::atomic::AtomicCell; -use pumpkin_data::item_stack::ItemStack; -use pumpkin_data::meta_data_type::MetaDataType; -use pumpkin_data::particle::Particle; -use pumpkin_data::tag::{self, Taggable}; -use pumpkin_data::tracked_data::TrackedData; -use pumpkin_nbt::compound::NbtCompound; -use pumpkin_protocol::java::client::play::Metadata; -use pumpkin_util::math::vector3::Vector3; -use rand::RngExt; - -use crate::entity::{Entity, EntityBase, player::Player}; - -pub(super) struct FurnaceMinecart { - fuel: AtomicI32, - push: AtomicCell>, -} - -impl FurnaceMinecart { - const FUEL_PER_ITEM: i32 = 3_600; - const MAX_FUEL: i32 = 32_000; - - pub(super) const fn new() -> Self { - Self { - fuel: AtomicI32::new(0), - push: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), - } - } - - fn set_fueled(entity: &Entity, fueled: bool) { - entity.send_meta_data( - &[Metadata::new( - TrackedData::ID_FUEL, - MetaDataType::BOOLEAN, - fueled, - )], - None, - ); - } - - pub(super) fn tick(&self, entity: &Entity) { - let fuel = self.fuel.load(Ordering::Relaxed); - if fuel <= 0 { - self.push.store(Vector3::new(0.0, 0.0, 0.0)); - return; - } - - let remaining = fuel - 1; - self.fuel.store(remaining, Ordering::Relaxed); - if remaining == 0 { - self.push.store(Vector3::new(0.0, 0.0, 0.0)); - Self::set_fueled(entity, false); - } else if rand::rng().random_range(0..4) == 0 { - let mut pos = entity.pos.load(); - pos.y += 0.8; - entity.world.load().spawn_particle( - pos, - Vector3::new(0.0, 0.0, 0.0), - 0.0, - 1, - Particle::LargeSmoke, - ); - } - } - - pub(super) fn velocity(&self, entity: &Entity, velocity: Vector3) -> Vector3 { - let mut push = self.push.load(); - let push_length_squared = push.x.mul_add(push.x, push.z * push.z); - let velocity_length_squared = velocity.x.mul_add(velocity.x, velocity.z * velocity.z); - - let mut next = if push_length_squared > 1.0e-7 { - if push_length_squared > 1.0e-4 && velocity_length_squared > 0.001 { - let velocity_direction = Vector3::new(velocity.x, 0.0, velocity.z).normalize(); - let push_length = push.length(); - let push_scale = if velocity_direction.dot(&push) < 0.0 { - -push_length - } else { - push_length - }; - push = velocity_direction.multiply(push_scale, 0.0, push_scale); - self.push.store(push); - } - velocity.multiply(0.8, 0.0, 0.8).add(&push) - } else { - velocity.multiply(0.98, 0.0, 0.98) - }; - - let in_water = entity.touching_water.load(Ordering::Relaxed); - if in_water { - next = next.multiply(0.1, 0.0, 0.1); - } - let slowdown = 0.96 * if in_water { 0.95 } else { 1.0 }; - next = next.multiply(slowdown, 0.0, slowdown); - - let max_speed = if in_water { 0.3 } else { 0.2 }; - let speed = next.x.hypot(next.z); - if speed > max_speed { - next.x = next.x / speed * max_speed; - next.z = next.z / speed * max_speed; - } - next - } - - pub(super) fn interact( - &self, - entity: &Entity, - player: &Player, - item_stack: &mut ItemStack, - ) -> bool { - let fuel = self.fuel.load(Ordering::Relaxed); - if item_stack - .get_item() - .has_tag(&tag::Item::MINECRAFT_FURNACE_MINECART_FUEL) - && fuel <= Self::MAX_FUEL - Self::FUEL_PER_ITEM - { - self.fuel - .store(fuel + Self::FUEL_PER_ITEM, Ordering::Relaxed); - item_stack.decrement_unless_creative(player.gamemode.load(), 1); - if fuel <= 0 { - Self::set_fueled(entity, true); - } - } - - if self.fuel.load(Ordering::Relaxed) > 0 { - let cart_pos = entity.pos.load(); - let player_pos = player.get_entity().pos.load(); - self.push.store(Vector3::new( - cart_pos.x - player_pos.x, - 0.0, - cart_pos.z - player_pos.z, - )); - } - true - } - - pub(super) fn init_data_tracker(&self, entity: &Entity) { - Self::set_fueled(entity, self.fuel.load(Ordering::Relaxed) > 0); - } - - pub(super) fn write_nbt(&self, nbt: &mut NbtCompound) { - let push = self.push.load(); - nbt.put_double("PushX", push.x); - nbt.put_double("PushZ", push.z); - nbt.put_short("Fuel", self.fuel.load(Ordering::Relaxed) as i16); - } - - pub(super) fn read_nbt(&self, nbt: &NbtCompound) { - self.push.store(Vector3::new( - nbt.get_double("PushX").unwrap_or(0.0), - 0.0, - nbt.get_double("PushZ").unwrap_or(0.0), - )); - self.fuel.store( - i32::from(nbt.get_short("Fuel").unwrap_or(0)), - Ordering::Relaxed, - ); - } -} diff --git a/pumpkin/src/entity/vehicle/minecart/hopper.rs b/pumpkin/src/entity/vehicle/minecart/hopper.rs deleted file mode 100644 index 147834232..000000000 --- a/pumpkin/src/entity/vehicle/minecart/hopper.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; - -use crate::block::entities::hopper::HopperBlockEntity; -use crate::entity::{Entity, player::Player}; -use pumpkin_data::translation; -use pumpkin_nbt::compound::NbtCompound; -use pumpkin_util::math::boundingbox::BoundingBox; -use pumpkin_util::math::position::BlockPos; -use pumpkin_util::math::vector3::Vector3; -use pumpkin_util::text::TextComponent; - -use super::container::{self, MinecartInventory}; - -pub(super) struct HopperMinecart { - enabled: AtomicBool, - inventory: Arc, -} - -impl HopperMinecart { - pub(super) fn new() -> Self { - Self { - enabled: AtomicBool::new(true), - inventory: Arc::new(MinecartInventory::new(5)), - } - } - - pub(super) const fn inventory(&self) -> &Arc { - &self.inventory - } - - pub(super) fn set_enabled(&self, enabled: bool) { - self.enabled.store(enabled, Ordering::Relaxed); - } - - pub(super) async fn tick(&self, entity: &Entity) { - if !self.enabled.load(Ordering::Relaxed) { - return; - } - - let world = entity.world.load(); - let pos = entity.pos.load(); - let source_pos = BlockPos::floored(pos.x, pos.y + 1.5, pos.z); - if let Some(block_entity) = world.get_block_entity(&source_pos) - && let Some(source) = block_entity.get_inventory() - { - for slot in 0..source.size() { - let stack = source.get_stack(slot).await; - let mut stack = stack.lock().await; - if stack.is_empty() - || !source.can_transfer_to(self.inventory.as_ref(), slot, &stack) - { - continue; - } - let backup = stack.clone(); - let one = stack.split(1); - if HopperBlockEntity::add_one_item(source.as_ref(), self.inventory.as_ref(), one) - .await - { - return; - } - *stack = backup; - } - return; - } - - let suction_box = BoundingBox::new( - Vector3::new(pos.x - 0.5, pos.y + 0.6875, pos.z - 0.5), - Vector3::new(pos.x + 0.5, pos.y + 2.0, pos.z + 0.5), - ); - if self.pick_up_item(entity, &suction_box).await { - return; - } - let cart_box = entity.bounding_box.load().expand(0.25, 0.0, 0.25); - self.pick_up_item(entity, &cart_box).await; - } - - async fn pick_up_item(&self, entity: &Entity, search_box: &BoundingBox) -> bool { - let world = entity.world.load(); - for entity in world.get_entities_at_box(search_box) { - let Some(item) = entity.get_item_entity() else { - continue; - }; - let mut stack = item.get_item_stack().lock().await; - if stack.is_empty() { - continue; - } - let backup = stack.clone(); - let one = stack.split(1); - if HopperBlockEntity::add_one_item( - self.inventory.as_ref(), - self.inventory.as_ref(), - one, - ) - .await - { - if stack.is_empty() { - item.get_entity().remove().await; - } - return true; - } - *stack = backup; - } - false - } - - pub(super) async fn interact(&self, entity: &Entity, player: &Arc) -> bool { - container::open( - entity, - player, - &self.inventory, - TextComponent::translate_cross( - translation::java::ENTITY_MINECRAFT_HOPPER_MINECART, - translation::bedrock::ENTITY_HOPPER_MINECART_NAME, - [], - ), - true, - ) - .await - } - - pub(super) async fn write_nbt(&self, nbt: &mut NbtCompound) { - self.inventory.write_nbt(nbt).await; - nbt.put_bool("Enabled", self.enabled.load(Ordering::Relaxed)); - } - - pub(super) fn read_nbt(&self, nbt: &NbtCompound) { - self.inventory.read_nbt(nbt); - self.enabled - .store(nbt.get_bool("Enabled").unwrap_or(true), Ordering::Relaxed); - } -} - -#[cfg(test)] -mod tests { - use super::HopperMinecart; - use pumpkin_world::inventory::Inventory; - - #[test] - fn hopper_minecart_inventory_has_five_slots() { - assert_eq!(HopperMinecart::new().inventory.size(), 5); - } -} diff --git a/pumpkin/src/entity/vehicle/minecart/rideable.rs b/pumpkin/src/entity/vehicle/minecart/rideable.rs deleted file mode 100644 index d1a78a544..000000000 --- a/pumpkin/src/entity/vehicle/minecart/rideable.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::sync::Arc; - -use crate::entity::{Entity, EntityBase, player::Player}; - -pub(super) struct RideableMinecart; - -impl RideableMinecart { - pub(super) async fn interact(&self, entity: &Entity, player: &Arc) -> bool { - if player.get_entity().is_sneaking() - || !entity.passengers.lock().await.is_empty() - || player.get_entity().has_vehicle().await - { - return false; - } - - let world = entity.world.load(); - let Some(vehicle) = world.get_entity_by_id(entity.entity_id) else { - return false; - }; - let Some(passenger) = world.get_player_by_id(player.entity_id()) else { - return false; - }; - - entity - .add_passenger(vehicle, passenger as Arc) - .await; - true - } -} diff --git a/pumpkin/src/entity/vehicle/minecart/tnt.rs b/pumpkin/src/entity/vehicle/minecart/tnt.rs deleted file mode 100644 index 6a3d80325..000000000 --- a/pumpkin/src/entity/vehicle/minecart/tnt.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::sync::atomic::{AtomicI32, Ordering}; - -use crossbeam::atomic::AtomicCell; -use pumpkin_data::entity::EntityStatus; -use pumpkin_data::particle::Particle; -use pumpkin_data::sound::{Sound, SoundCategory}; -use pumpkin_nbt::compound::NbtCompound; -use pumpkin_util::math::vector3::Vector3; -use rand::RngExt; - -use crate::entity::Entity; - -pub(super) struct TntMinecart { - fuse: AtomicI32, - explosion_power: AtomicCell, - explosion_speed_factor: AtomicCell, -} - -impl TntMinecart { - pub(super) const fn new() -> Self { - Self { - fuse: AtomicI32::new(-1), - explosion_power: AtomicCell::new(4.0), - explosion_speed_factor: AtomicCell::new(1.0), - } - } - - pub(super) fn prime(&self, entity: &Entity, fuse: i32) { - if self.fuse.load(Ordering::Relaxed) >= 0 - || !entity - .world - .load() - .level_info - .load() - .game_rules - .tnt_explodes - { - return; - } - - self.fuse.store(fuse, Ordering::Relaxed); - let world = entity.world.load(); - world.send_entity_status(entity, EntityStatus::TntPrime); - world.play_sound( - Sound::EntityTntPrimed, - SoundCategory::Blocks, - &entity.pos.load(), - ); - } - - pub(super) async fn tick(&self, entity: &Entity) -> bool { - let fuse = self.fuse.load(Ordering::Relaxed); - if fuse > 0 { - self.fuse.store(fuse - 1, Ordering::Relaxed); - let mut smoke_pos = entity.pos.load(); - smoke_pos.y += 0.5; - entity.world.load().spawn_particle( - smoke_pos, - Vector3::new(0.0, 0.0, 0.0), - 0.0, - 1, - Particle::Smoke, - ); - } else if fuse == 0 { - let velocity = entity.velocity.load(); - self.explode( - entity, - velocity.x.mul_add(velocity.x, velocity.z * velocity.z), - ) - .await; - return true; - } - false - } - - pub(super) async fn explode(&self, entity: &Entity, horizontal_speed_squared: f64) { - let world = entity.world.load(); - if !world.level_info.load().game_rules.tnt_explodes { - if self.fuse.load(Ordering::Relaxed) > -1 { - entity.remove().await; - } - return; - } - - let power = Self::explosion_strength( - self.explosion_power.load(), - self.explosion_speed_factor.load(), - horizontal_speed_squared, - rand::rng().random_range(0.0..1.0), - ); - let pos = entity.pos.load(); - let primed = self.fuse.load(Ordering::Relaxed) > -1; - entity.remove().await; - if primed { - world.explode_tnt_minecart(pos, power).await; - } else { - world.explode(pos, power).await; - } - } - - pub(super) fn set_fuse(&self, fuse: i32) { - self.fuse.store(fuse, Ordering::Relaxed); - } - - pub(super) fn write_nbt(&self, nbt: &mut NbtCompound) { - nbt.put_int("fuse", self.fuse.load(Ordering::Relaxed)); - let power = self.explosion_power.load(); - if power != 4.0 { - nbt.put_float("explosion_power", power); - } - let speed_factor = self.explosion_speed_factor.load(); - if speed_factor != 1.0 { - nbt.put_float("explosion_speed_factor", speed_factor); - } - } - - pub(super) fn read_nbt(&self, nbt: &NbtCompound) { - self.fuse - .store(nbt.get_int("fuse").unwrap_or(-1), Ordering::Relaxed); - self.explosion_power.store( - nbt.get_float("explosion_power") - .unwrap_or(4.0) - .clamp(0.0, 128.0), - ); - self.explosion_speed_factor.store( - nbt.get_float("explosion_speed_factor") - .unwrap_or(1.0) - .clamp(0.0, 128.0), - ); - } - - fn explosion_strength( - base: f32, - speed_factor: f32, - horizontal_speed_squared: f64, - random: f32, - ) -> f32 { - let speed = horizontal_speed_squared.sqrt().min(5.0) as f32; - base + speed_factor * random * 1.5 * speed - } -} - -#[cfg(test)] -mod tests { - use super::TntMinecart; - - #[test] - fn tnt_minecart_explosion_bonus_is_speed_capped() { - assert_eq!(TntMinecart::explosion_strength(4.0, 1.0, 100.0, 1.0), 11.5); - assert_eq!(TntMinecart::explosion_strength(4.0, 1.0, 100.0, 0.0), 4.0); - } -} diff --git a/pumpkin/src/entity/vehicle/vehicle.rs b/pumpkin/src/entity/vehicle/vehicle.rs index 09c4ff93e..142306587 100644 --- a/pumpkin/src/entity/vehicle/vehicle.rs +++ b/pumpkin/src/entity/vehicle/vehicle.rs @@ -8,7 +8,6 @@ use crate::entity::EntityBase; use crate::world::loot::{LootContextParameters, LootTableExt}; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::tracked_data::TrackedData; -use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_util::GameMode; pub struct VehicleEntity { @@ -67,42 +66,25 @@ impl VehicleEntity { pub fn send_wobble_metadata(&self) { self.entity.send_meta_data( &[ - Metadata::new( - TrackedData::DAMAGE_WOBBLE_TICKS, - MetaDataType::INTEGER, - VarInt(self.get_hurt_time()), - ), - Metadata::new( - TrackedData::DAMAGE_WOBBLE_SIDE, - MetaDataType::INTEGER, - VarInt(self.get_hurt_dir()), - ), Metadata::new( TrackedData::ID_HURT, - MetaDataType::INT, - VarInt(self.get_hurt_time()), + MetaDataType::INTEGER, + self.get_hurt_time(), ), Metadata::new( TrackedData::ID_HURTDIR, - MetaDataType::INT, - VarInt(self.get_hurt_dir()), + MetaDataType::INTEGER, + self.get_hurt_dir(), ), ], None, ); self.entity.send_meta_data( - &[ - Metadata::new( - TrackedData::DAMAGE_WOBBLE_STRENGTH, - MetaDataType::FLOAT, - self.get_damage(), - ), - Metadata::new( - TrackedData::ID_DAMAGE, - MetaDataType::FLOAT, - self.get_damage(), - ), - ], + &[Metadata::new( + TrackedData::ID_DAMAGE, + MetaDataType::FLOAT, + self.get_damage(), + )], None, ); } @@ -134,7 +116,16 @@ impl VehicleEntity { return true; } - let new_strength = self.apply_damage_wobble(amount); + let current_side = self.get_hurt_dir(); + self.set_hurt_dir(-current_side); + self.set_hurt_time(10); + self.entity.velocity_dirty.store(true, Ordering::SeqCst); + + let current_strength = self.get_damage(); + let new_strength = current_strength + amount * 10.0; + self.set_damage(new_strength); + + self.send_wobble_metadata(); let is_creative = source .and_then(|s| s.get_player()) @@ -150,64 +141,4 @@ impl VehicleEntity { true } - - /// Applies the standard minecart damage wobble without destroying the vehicle. - /// TNT minecarts use this before deciding whether damage primes or breaks them. - pub fn apply_damage_wobble(&self, amount: f32) -> f32 { - let current_side = self.get_hurt_dir(); - self.set_hurt_dir(-current_side); - self.set_hurt_time(10); - self.entity.velocity_dirty.store(true, Ordering::SeqCst); - - let current_strength = self.get_damage(); - let new_strength = current_strength + amount * 10.0; - self.set_damage(new_strength); - - self.send_wobble_metadata(); - new_strength - } -} - -#[cfg(test)] -mod tests { - use pumpkin_data::meta_data_type::MetaDataType; - use pumpkin_data::tracked_data::TrackedData; - use pumpkin_protocol::codec::var_int::VarInt; - use pumpkin_protocol::java::client::play::Metadata; - use pumpkin_util::version::JavaMinecraftVersion; - - fn wobble_integer_metadata(version: JavaMinecraftVersion) -> Vec { - let mut bytes = Vec::new(); - for metadata in [ - Metadata::new( - TrackedData::DAMAGE_WOBBLE_TICKS, - MetaDataType::INTEGER, - VarInt(10), - ), - Metadata::new( - TrackedData::DAMAGE_WOBBLE_SIDE, - MetaDataType::INTEGER, - VarInt(-1), - ), - Metadata::new(TrackedData::ID_HURT, MetaDataType::INT, VarInt(10)), - Metadata::new(TrackedData::ID_HURTDIR, MetaDataType::INT, VarInt(-1)), - ] { - metadata.write(&mut bytes, &version).unwrap(); - } - bytes - } - - #[test] - fn wobble_integers_serialize_for_legacy_and_current_clients() { - let expected = vec![8, 1, 10, 9, 1, 0xff, 0xff, 0xff, 0xff, 0x0f]; - - assert_eq!( - wobble_integer_metadata(JavaMinecraftVersion::V_1_21_11), - expected - ); - assert_eq!( - wobble_integer_metadata(JavaMinecraftVersion::V_26_2), - expected - ); - } } diff --git a/pumpkin/src/item/items/minecart.rs b/pumpkin/src/item/items/minecart.rs index c1b12df89..20ab9589f 100644 --- a/pumpkin/src/item/items/minecart.rs +++ b/pumpkin/src/item/items/minecart.rs @@ -88,7 +88,6 @@ impl ItemBehaviour for MinecartItem { ); let minecart_entity = Arc::new(MinecartEntity::new(entity)); world.spawn_entity(minecart_entity).await; - item.decrement_unless_creative(player.gamemode.load(), 1); }) } diff --git a/pumpkin/src/world/explosion.rs b/pumpkin/src/world/explosion.rs index 19317b671..a5e68fae0 100644 --- a/pumpkin/src/world/explosion.rs +++ b/pumpkin/src/world/explosion.rs @@ -18,34 +18,12 @@ use super::{BlockFlags, World}; pub struct Explosion { power: f32, pos: Vector3, - preserve_rails: bool, } impl Explosion { #[must_use] pub const fn new(power: f32, pos: Vector3) -> Self { - Self { - power, - pos, - preserve_rails: false, - } - } - - #[must_use] - pub const fn preserving_rails(mut self) -> Self { - self.preserve_rails = true; - self - } - - fn protects_rail(&self, world: &World, pos: &BlockPos, block: &Block) -> bool { - self.preserve_rails && (Self::is_rail(block) || Self::is_rail(world.get_block(&pos.up()))) - } - - fn is_rail(block: &Block) -> bool { - block.id == Block::RAIL.id - || block.id == Block::POWERED_RAIL.id - || block.id == Block::DETECTOR_RAIL.id - || block.id == Block::ACTIVATOR_RAIL.id + Self { power, pos } } fn get_blocks_to_destroy( @@ -135,16 +113,12 @@ impl Explosion { ); if !state.is_air() || !fluid_state.is_empty { - let protects_rail = self.protects_rail(world, &block_pos, block); - let resistance = if protects_rail { - 0.0 - } else { - fluid_state.blast_resistance.max(block.blast_resistance) - }; + let resistance = + fluid_state.blast_resistance.max(block.blast_resistance); h -= (resistance + 0.3) * 0.3; - if h > 0.0 && !protects_rail { + if h > 0.0 { map.insert(block_pos, (block, state)); } } @@ -333,22 +307,3 @@ impl Explosion { blocks.len() as u32 } } - -#[cfg(test)] -mod tests { - use super::Explosion; - use pumpkin_data::Block; - - #[test] - fn tnt_minecart_rail_protection_covers_every_rail_type() { - for rail in [ - &Block::RAIL, - &Block::POWERED_RAIL, - &Block::DETECTOR_RAIL, - &Block::ACTIVATOR_RAIL, - ] { - assert!(Explosion::is_rail(rail)); - } - assert!(!Explosion::is_rail(&Block::STONE)); - } -} diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 416b8a45b..665769c23 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -37,7 +37,7 @@ use crate::{ {OnNeighborUpdateArgs, OnScheduledTickArgs}, }, command::client_suggestions, - entity::{Entity, EntityBase, NBTStorage, RemovalReason, player::Player, r#type::from_type}, + entity::{Entity, EntityBase, NBTStorage, player::Player, r#type::from_type}, error::PumpkinError, net::{ClientPlatform, java::JavaClient}, plugin::{ @@ -391,11 +391,7 @@ impl World { /// currently in; the chunk is rewritten from scratch every unload cycle, so /// there is nothing stale to deduplicate. async fn save_entity(&self, entity: &Arc) { - let base_entity = entity.get_entity(); - if base_entity.is_removed() { - return; - } - let current_chunk = base_entity.block_pos.load().chunk_position(); + let current_chunk = entity.get_entity().block_pos.load().chunk_position(); let mut nbt = NbtCompound::new(); entity.write_nbt(&mut nbt).await; let chunk = self.level.get_entity_chunk(current_chunk).await; @@ -3348,20 +3344,6 @@ impl World { pub async fn explode(self: &Arc, position: Vector3, power: f32) { let explosion = Explosion::new(power, position); - self.run_explosion(explosion, position, power).await; - } - - pub async fn explode_tnt_minecart(self: &Arc, position: Vector3, power: f32) { - let explosion = Explosion::new(power, position).preserving_rails(); - self.run_explosion(explosion, position, power).await; - } - - async fn run_explosion( - self: &Arc, - explosion: Explosion, - position: Vector3, - power: f32, - ) { let block_count = explosion.explode(self).await; let particle = if power < 2.0 { Particle::Explosion @@ -3793,7 +3775,6 @@ impl World { .client .enqueue_packet(&base_entity.create_spawn_packet()) .await; - player.try_restore_vehicle(&entity).await; entities_to_add.push(entity); } @@ -3815,7 +3796,6 @@ impl World { .client .enqueue_packet(&base_entity.create_spawn_packet()) .await; - player.try_restore_vehicle(entity).await; } } } @@ -4299,15 +4279,6 @@ impl World { #[allow(clippy::unused_async)] pub async fn remove_entity(&self, entity: &dyn EntityBase) { let base_entity = entity.get_entity(); - if base_entity - .removal_reason - .swap(Some(RemovalReason::Discarded)) - .is_some() - { - return; - } - base_entity.removed.store(true, Ordering::Release); - self.spawn_state.load().remove_entity(self, entity); self.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone();