Revert "feat: Add Mineshaft Generation (#2740)"

This reverts commit 09d23bd15f.
This commit is contained in:
Alexander Medvedev 2026-08-05 14:31:26 +02:00
parent b55feecd74
commit a748aae07e
22 changed files with 240 additions and 3597 deletions

View File

@ -122,16 +122,6 @@ impl BlockState {
self.state_flags & IS_SOLID_BLOCK != 0 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] #[must_use]
pub const fn has_random_ticks(&self) -> bool { pub const fn has_random_ticks(&self) -> bool {
self.state_flags & HAS_RANDOM_TICKS != 0 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 EAST_SIDE_SOLID: u8 = 1 << 5;
const DOWN_CENTER_SOLID: u8 = 1 << 6; const DOWN_CENTER_SOLID: u8 = 1 << 6;
const UP_CENTER_SOLID: u8 = 1 << 7; 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());
}
}

View File

@ -4,7 +4,7 @@ use std::{
}; };
use legacy_rand::{LegacyRand, LegacySplitter}; use legacy_rand::{LegacyRand, LegacySplitter};
use xoroshiro128::{WorldgenXoroshiro, Xoroshiro, XoroshiroSplitter}; use xoroshiro128::{Xoroshiro, XoroshiroSplitter};
mod gaussian; mod gaussian;
pub mod legacy_rand; pub mod legacy_rand;
@ -35,8 +35,6 @@ pub enum RandomGenerator {
Xoroshiro(Xoroshiro), Xoroshiro(Xoroshiro),
/// Legacy random number generator (compatible with older Minecraft versions). /// Legacy random number generator (compatible with older Minecraft versions).
Legacy(LegacyRand), Legacy(LegacyRand),
/// Xoroshiro source accessed through Vanilla's `WorldgenRandom` bit API.
WorldgenXoroshiro(WorldgenXoroshiro),
} }
/// Unified random number deriver enum for creating child RNGs. /// Unified random number deriver enum for creating child RNGs.

View File

@ -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<f64>,
}
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<f64> {
self.internal_next_gaussian
}
fn set_stored_next_gaussian(&mut self, value: Option<f64>) {
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. /// A splitter for creating derived Xoroshiro random generators.
/// ///
/// This struct allows for deterministic creation of multiple independent /// This struct allows for deterministic creation of multiple independent
@ -361,23 +265,10 @@ impl RandomDeriverImpl for XoroshiroSplitter {
mod tests { mod tests {
use crate::random::{RandomDeriverImpl, RandomImpl}; 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 // 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] #[test]
fn mix_stafford_13_test() { fn mix_stafford_13_test() {
let values: [(u64, i64); 31] = [ let values: [(u64, i64); 31] = [

View File

@ -10,12 +10,12 @@ use pumpkin_data::structures::{
use pumpkin_data::tag::RegistryKey; use pumpkin_data::tag::RegistryKey;
use pumpkin_data::{Block, BlockState, block_properties::blocks_movement, chunk::Biome}; use pumpkin_data::{Block, BlockState, block_properties::blocks_movement, chunk::Biome};
use pumpkin_data::{BlockId, BlockStateId, tag}; use pumpkin_data::{BlockId, BlockStateId, tag};
use pumpkin_util::random::RandomImpl; use pumpkin_util::random::xoroshiro128::XoroshiroSplitter;
use pumpkin_util::random::xoroshiro128::{WorldgenXoroshiro, XoroshiroSplitter}; use pumpkin_util::random::{RandomImpl, get_carver_seed};
use pumpkin_util::{ use pumpkin_util::{
HeightMap, HeightMap,
math::{block_box::BlockBox, position::BlockPos, vector3::Vector3}, math::{block_box::BlockBox, position::BlockPos, vector3::Vector3},
random::{RandomGenerator, get_decorator_seed}, random::{RandomGenerator, get_decorator_seed, xoroshiro128::Xoroshiro},
}; };
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -66,18 +66,6 @@ enum ActiveSupplier {
End(TheEndBiomeSupplier), End(TheEndBiomeSupplier),
} }
fn structures_for_step(step: usize) -> Vec<StructureKeys> {
let mut structures = StructureKeys::all_names()
.iter()
.filter_map(|name| StructureKeys::from_name(name))
.filter(|key| Structure::get(key).step.ordinal() == step)
.collect::<Vec<_>>();
// 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 { pub trait GenerationCache: HeightLimitView + BlockAccessor {
fn get_center_chunk_mut(&mut self) -> &mut ProtoChunk; fn get_center_chunk_mut(&mut self) -> &mut ProtoChunk;
fn get_center_chunk(&self) -> &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 start_block_z = chunk_pos::start_block_z(center_z);
let origin_pos = BlockPos::new(start_block_x, min_y, start_block_z); let origin_pos = BlockPos::new(start_block_x, min_y, start_block_z);
let population_seed = WorldgenXoroshiro::get_population_seed( let population_seed =
random_config.seed, Xoroshiro::get_population_seed(random_config.seed, start_block_x, start_block_z);
start_block_x,
start_block_z,
);
for step in 0..11 { for step in 0..11 {
Self::generate_structure_step( Self::generate_structure_step(
@ -1117,9 +1102,8 @@ impl ProtoChunk {
for (p, feature_enum) in features_to_run.into_iter().enumerate() { for (p, feature_enum) in features_to_run.into_iter().enumerate() {
if let Some(feature) = PLACED_FEATURES.get(&feature_enum) { if let Some(feature) = PLACED_FEATURES.get(&feature_enum) {
let decorator_seed = get_decorator_seed(population_seed, p as u64, step as u64); let decorator_seed = get_decorator_seed(population_seed, p as u64, step as u64);
let mut random = RandomGenerator::WorldgenXoroshiro( let mut random =
WorldgenXoroshiro::from_seed(decorator_seed), RandomGenerator::Xoroshiro(Xoroshiro::from_seed(decorator_seed));
);
feature.generate( feature.generate(
cache, cache,
@ -1144,7 +1128,7 @@ impl ProtoChunk {
population_seed: u64, population_seed: u64,
world_seed: i64, world_seed: i64,
) { ) {
let mut tasks = FxHashMap::<StructureKeys, Vec<_>>::default(); let mut tasks = Vec::new();
{ {
let center_chunk = cache.get_center_chunk(); let center_chunk = cache.get_center_chunk();
let center_x = center_chunk.x; let center_x = center_chunk.x;
@ -1156,16 +1140,14 @@ impl ProtoChunk {
continue; continue;
} }
let collector = match instance { match instance {
StructureInstance::Start(pos) => pos.collector.clone(), StructureInstance::Start(pos) => tasks.push(pos.collector.clone()),
StructureInstance::Reference(collector) => collector.clone(), StructureInstance::Reference(collector) => {
}; let collector_arc = collector.clone();
let structure_tasks = tasks.entry(*id).or_default(); if !tasks.iter().any(|t| Arc::ptr_eq(t, &collector_arc)) {
if !structure_tasks tasks.push(collector_arc);
.iter() }
.any(|task| Arc::ptr_eq(task, &collector)) }
{
structure_tasks.push(collector);
} }
} }
@ -1186,26 +1168,28 @@ impl ProtoChunk {
continue; continue;
} }
let collector = match instance { match instance {
StructureInstance::Start(pos) => { StructureInstance::Start(pos) => {
let start_x = chunk_pos::start_block_x(center_x); let start_x = chunk_pos::start_block_x(center_x);
let start_z = chunk_pos::start_block_z(center_z); let start_z = chunk_pos::start_block_z(center_z);
let end_x = start_x + 15; let end_x = start_x + 15;
let end_z = start_z + 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) .intersects_raw_xz(start_x, start_z, end_x, end_z)
.then(|| pos.collector.clone())
}
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); let collector_arc = pos.collector.clone();
if !tasks.iter().any(|t| Arc::ptr_eq(t, &collector_arc)) {
tasks.push(collector_arc);
}
}
}
StructureInstance::Reference(collector) => {
let collector_arc = collector.clone();
if !tasks.iter().any(|t| Arc::ptr_eq(t, &collector_arc)) {
tasks.push(collector_arc);
}
} }
} }
} }
@ -1214,20 +1198,15 @@ 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(); let chunk = cache.get_center_chunk_mut();
for (index, key) in structures_for_step(step).into_iter().enumerate() { for collector_arc in tasks {
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(); let mut collector = collector_arc.lock().unwrap();
collector.generate_in_chunk(chunk, block_registry, &mut random, world_seed); collector.generate_in_chunk(chunk, block_registry, &mut random, world_seed);
} }
} }
}
#[must_use] #[must_use]
pub fn get_allowed_biomes(set: &StructureSet) -> Vec<u16> { pub fn get_allowed_biomes(set: &StructureSet) -> Vec<u16> {
@ -1291,7 +1270,9 @@ impl ProtoChunk {
} }
let mut candidates = set.structures.to_vec(); 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(); let mut total_weight: u32 = candidates.iter().map(|e| e.weight).sum();
@ -1626,48 +1607,3 @@ impl GenerationCache for ProtoChunk {
None 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);
}
}
}

View File

@ -11,23 +11,14 @@ use crate::{
noise::router::multi_noise_sampler::MultiNoiseSampler, noise::router::multi_noise_sampler::MultiNoiseSampler,
structure::structures::{ structure::structures::{
StructureGenerator, StructureGeneratorContext, StructurePosition, StructureGenerator, StructureGeneratorContext, StructurePosition,
buried_treasure::BuriedTreasureGenerator, buried_treasure::BuriedTreasureGenerator, create_chunk_random,
create_chunk_random, desert_pyramid::DesertPyramidGenerator, end_city::EndCityGenerator,
desert_pyramid::DesertPyramidGenerator, igloo::IglooGenerator, jigsaw::JigsawGenerator, jungle_temple::JungleTempleGenerator,
end_city::EndCityGenerator, mansion::MansionGenerator, mineshaft::MineshaftGenerator,
igloo::IglooGenerator, nether_fortress::NetherFortressGenerator, nether_fossil::NetherFossilGenerator,
jigsaw::JigsawGenerator, ocean_monument::OceanMonumentGenerator, ocean_ruin::OceanRuinGenerator,
jungle_temple::JungleTempleGenerator, ruined_portal::RuinedPortalGenerator, shipwreck::ShipwreckGenerator,
mansion::MansionGenerator, stronghold::StrongholdGenerator, swamp_hut::SwampHutGenerator,
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,
}, },
}, },
}; };
@ -127,11 +118,7 @@ pub fn generate_structure_position(
StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context), StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context),
StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => { StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => {
let generator = MineshaftGenerator { let generator = MineshaftGenerator {
mineshaft_type: if *key == StructureKeys::MineshaftMesa { is_mesa: *key == StructureKeys::MineshaftMesa,
MineshaftType::Mesa
} else {
MineshaftType::Normal
},
}; };
generator.get_structure_position(context) generator.get_structure_position(context)
} }

View File

@ -6,7 +6,7 @@ use pumpkin_data::structures::{
use pumpkin_util::{ use pumpkin_util::{
math::floor_div, math::floor_div,
random::{ random::{
RandomGenerator, RandomImpl, get_region_seed, legacy_rand::LegacyRand, RandomGenerator, RandomImpl, get_carver_seed, get_region_seed, legacy_rand::LegacyRand,
xoroshiro128::Xoroshiro, xoroshiro128::Xoroshiro,
}, },
}; };
@ -17,7 +17,7 @@ use crate::ProtoChunk;
use dashmap::DashMap; use dashmap::DashMap;
use pumpkin_data::structures::StructureKeys; 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 /// 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). /// rather than localized chunk-based math (e.g., Strongholds using Concentric Rings).
/// ///
@ -267,7 +267,8 @@ fn should_generate_frequency(
random.next_f32() < frequency random.next_f32() < frequency
} }
FrequencyReductionMethod::LegacyType3 => { 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) random.next_f64() < f64::from(frequency)
} }
} }
@ -353,10 +354,7 @@ fn is_start_chunk_random_spread(
mod tests { mod tests {
use pumpkin_data::{ use pumpkin_data::{
dimension::Dimension, dimension::Dimension,
structures::{ structures::{RandomSpreadStructurePlacement, StructurePlacementCalculator, StructureSet},
FrequencyReductionMethod, RandomSpreadStructurePlacement, StructurePlacementCalculator,
StructureSet,
},
}; };
use pumpkin_util::random::{ use pumpkin_util::random::{
RandomGenerator, RandomImpl, get_region_seed, legacy_rand::LegacyRand, RandomGenerator, RandomImpl, get_region_seed, legacy_rand::LegacyRand,
@ -369,7 +367,7 @@ mod tests {
get_world_gen, get_world_gen,
structure::placement::{ structure::placement::{
GlobalStructureCache, apply_frequency_reduction, get_start_chunk_random_spread, 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); 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] #[test]
fn pillager_outposts_respect_the_village_exclusion_zone() { fn pillager_outposts_respect_the_village_exclusion_zone() {
let seed = 0; let seed = 0;

File diff suppressed because it is too large Load Diff

View File

@ -579,9 +579,7 @@ impl StructurePiecesCollector {
let chunk_z = start_block_z(chunk.z); let chunk_z = start_block_z(chunk.z);
let chunk_box = BlockBox::new( let chunk_box = BlockBox::new(
chunk_x, chunk_x,
// Vanilla's writable structure area deliberately excludes the chunk.bottom_y() as i32,
// dimension's bottommost block layer.
chunk.bottom_y() as i32 + 1,
chunk_z, chunk_z,
chunk_x + 15, chunk_x + 15,
chunk.bottom_y() as i32 + chunk.height() as i32 - 1, chunk.bottom_y() as i32 + chunk.height() as i32 - 1,

View File

@ -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<VarInt> = 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)] #[allow(clippy::too_many_lines)]
pub async fn remove_passenger(&self, passenger_id: i32) { pub async fn remove_passenger(&self, passenger_id: i32) {
let mut passengers = self.passengers.lock().await; let mut passengers = self.passengers.lock().await;

View File

@ -120,45 +120,6 @@ use pumpkin_world::chunk_system::ChunkLoading;
const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128 const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128
const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20 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<Uuid> {
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 pub const DATA_VERSION: i32 = 4903; // 26.2
/// Food exhaustion applied for every block a player mines. /// Food exhaustion applied for every block a player mines.
@ -551,7 +512,6 @@ pub struct Player {
pub experience_pick_up_delay: Mutex<u32>, pub experience_pick_up_delay: Mutex<u32>,
pub chunk_manager: Mutex<ChunkManager>, pub chunk_manager: Mutex<ChunkManager>,
pub has_played_before: AtomicBool, pub has_played_before: AtomicBool,
root_vehicle_uuid: AtomicCell<Option<Uuid>>,
pub chat_session: Arc<Mutex<ChatSession>>, pub chat_session: Arc<Mutex<ChatSession>>,
pub signature_cache: Mutex<MessageCache>, pub signature_cache: Mutex<MessageCache>,
pub player_screen_handler: Arc<Mutex<PlayerScreenHandler>>, pub player_screen_handler: Arc<Mutex<PlayerScreenHandler>>,
@ -790,7 +750,6 @@ impl Player {
last_food_saturation: AtomicBool::new(true), last_food_saturation: AtomicBool::new(true),
subscribed_debug_sample: AtomicBool::new(false), subscribed_debug_sample: AtomicBool::new(false),
has_played_before: 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 chat_session: Arc::new(Mutex::new(ChatSession::default())), // Placeholder value until the player actually sets their session id
signature_cache: Mutex::new(MessageCache::default()), signature_cache: Mutex::new(MessageCache::default()),
player_screen_handler: player_screen_handler.clone(), player_screen_handler: player_screen_handler.clone(),
@ -959,16 +918,6 @@ impl Player {
/// Removes the [`Player`] out of the current [`World`]. /// Removes the [`Player`] out of the current [`World`].
pub async fn remove(self: &Arc<Self>) { pub async fn remove(self: &Arc<Self>) {
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 self.stats
.lock() .lock()
.await .await
@ -1015,21 +964,6 @@ impl Player {
//self.world().level.list_cached(); //self.world().level.list_cached();
} }
pub(crate) async fn try_restore_vehicle(self: &Arc<Self>, vehicle: &Arc<dyn EntityBase>) {
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)] #[expect(clippy::too_many_lines)]
pub async fn attack(&self, victim: Arc<dyn EntityBase>) { pub async fn attack(&self, victim: Arc<dyn EntityBase>) {
let world = self.world(); let world = self.world();
@ -4733,18 +4667,6 @@ impl NBTStorage for Player {
nbt.put_bool("SpawnForced", respawn.force); nbt.put_bool("SpawnForced", respawn.force);
} }
nbt.put_int("XpSeed", self.enchantment_seed.load(Ordering::Relaxed)); 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); self.stats.lock().await.write_nbt(nbt);
}) })
} }
@ -4815,7 +4737,6 @@ impl NBTStorage for Player {
nbt.get_int("XpSeed").unwrap_or(rand::random()), nbt.get_int("XpSeed").unwrap_or(rand::random()),
Ordering::Relaxed, Ordering::Relaxed,
); );
self.root_vehicle_uuid.store(read_root_vehicle(nbt));
self.stats.lock().await.read_nbt(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));
}
}

View File

@ -440,7 +440,6 @@ impl EntityBase for ArrowEntity {
}) })
} }
#[expect(clippy::too_many_lines)]
fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> { fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> {
Box::pin(async move { Box::pin(async move {
let entity = self.get_entity(); let entity = self.get_entity();
@ -505,14 +504,7 @@ impl EntityBase for ArrowEntity {
} }
let damage_succeeded = target let damage_succeeded = target
.damage_with_context( .damage(&*target, damage as f32, DamageType::ARROW)
&*target,
damage as f32,
DamageType::ARROW,
Some(hit_pos),
None,
Some(self),
)
.await; .await;
if let Some(living) = target.get_living_entity() { if let Some(living) = target.get_living_entity() {

View File

@ -1,43 +1,22 @@
mod chest;
mod container;
mod furnace;
mod hopper;
mod rideable;
mod tnt;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use pumpkin_protocol::java::server::play::SPlayerInput; use pumpkin_protocol::java::server::play::SPlayerInput;
use rand::RngExt;
use crate::{ use crate::{
entity::{ entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity, Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player,
player::Player,
}, },
server::Server, server::Server,
}; };
use pumpkin_data::Block; use pumpkin_data::Block;
use pumpkin_data::block_properties::{BlockProperties, PoweredRailLikeProperties}; use pumpkin_data::block_properties::{BlockProperties, PoweredRailLikeProperties};
use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; 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::position::BlockPos;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::inventory::Inventory;
use crate::entity::vehicle::vehicle::VehicleEntity; 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( const fn get_exits(
shape: pumpkin_data::block_properties::RailShape, shape: pumpkin_data::block_properties::RailShape,
@ -59,96 +38,21 @@ const fn get_exits(
pub struct MinecartEntity { pub struct MinecartEntity {
pub vehicle: VehicleEntity, pub vehicle: VehicleEntity,
kind: MinecartKind, pub active: AtomicBool,
} pub tnt_fuse: AtomicI32,
enum MinecartKind {
Rideable(RideableMinecart),
Chest(ChestMinecart),
Furnace(FurnaceMinecart),
Hopper(HopperMinecart),
Tnt(TntMinecart),
Other,
} }
impl MinecartEntity { impl MinecartEntity {
const DEFAULT_GRAVITY: f64 = 0.04; pub const fn new(entity: Entity) -> Self {
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,
};
Self { Self {
vehicle: VehicleEntity::new(entity), vehicle: VehicleEntity::new(entity),
kind, active: AtomicBool::new(false),
tnt_fuse: AtomicI32::new(-1),
}
} }
} }
const fn container(&self) -> Option<&Arc<MinecartInventory>> { impl NBTStorage for MinecartEntity {}
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<f64>, touching_water: bool) -> Vector3<f64> {
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 EntityBase for MinecartEntity { impl EntityBase for MinecartEntity {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
@ -159,9 +63,6 @@ impl EntityBase for MinecartEntity {
) -> EntityBaseFuture<'a, ()> { ) -> EntityBaseFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
self.vehicle.tick(); self.vehicle.tick();
if let MinecartKind::Furnace(minecart) = &self.kind {
minecart.tick(&self.vehicle.entity);
}
let world = self.vehicle.entity.world.load(); let world = self.vehicle.entity.world.load();
let pos = self.vehicle.entity.pos.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 { if is_powered_rail || is_activator_rail {
let props = PoweredRailLikeProperties::from_state_id(state_id, block); let props = PoweredRailLikeProperties::from_state_id(state_id, block);
let powered = props.powered; let powered = props.powered;
if is_activator_rail && let MinecartKind::Hopper(minecart) = &self.kind { self.active.store(powered, Ordering::Relaxed);
minecart.set_enabled(!powered);
}
if powered { if powered {
if is_powered_rail { if is_powered_rail {
@ -243,27 +134,27 @@ impl EntityBase for MinecartEntity {
} }
self.vehicle.entity.send_velocity(); self.vehicle.entity.send_velocity();
} else if is_activator_rail { } else if is_activator_rail {
match &self.kind { let passengers = self.vehicle.entity.passengers.lock().await.clone();
MinecartKind::Tnt(minecart) => {
minecart.prime(&self.vehicle.entity, 80);
}
MinecartKind::Rideable(_) => {
let passengers =
self.vehicle.entity.passengers.lock().await.clone();
for passenger in passengers { for passenger in passengers {
self.vehicle let passenger_id = passenger.get_entity().entity_id;
.entity self.vehicle.entity.remove_passenger(passenger_id).await;
.remove_passenger(passenger.get_entity().entity_id)
.await;
} }
if self.vehicle.get_hurt_time() == 0 {
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,
);
}
} else if self.vehicle.get_hurt_time() == 0 {
self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir()); self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir());
self.vehicle.set_hurt_time(10); self.vehicle.set_hurt_time(10);
self.vehicle.set_damage(50.0); self.vehicle.set_damage(50.0);
self.vehicle.send_wobble_metadata(); // TODO: Send entity status
}
}
_ => {}
} }
} }
} else if is_powered_rail { } else if is_powered_rail {
@ -277,10 +168,27 @@ impl EntityBase for MinecartEntity {
} }
} }
if let MinecartKind::Tnt(minecart) = &self.kind if self.vehicle.entity.entity_type.id == EntityType::TNT_MINECART.id {
&& minecart.tick(&self.vehicle.entity).await let fuse = self.tnt_fuse.load(Ordering::Relaxed);
{ if fuse > 0 {
return; 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(); let mut velocity = self.vehicle.entity.velocity.load();
@ -431,24 +339,6 @@ impl EntityBase for MinecartEntity {
if velocity.length() > 0.001 { if velocity.length() > 0.001 {
self.move_entity(caller, velocity).await; 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 new_pos = self.vehicle.entity.pos.load();
let passengers = self.vehicle.entity.passengers.lock().await; let passengers = self.vehicle.entity.passengers.lock().await;
@ -460,7 +350,7 @@ impl EntityBase for MinecartEntity {
self.vehicle.entity.send_pos_rot(); self.vehicle.entity.send_pos_rot();
#[allow(clippy::useless_let_if_seq)] #[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 { if is_on_rails {
let passengers = self.vehicle.entity.passengers.lock().await; let passengers = self.vehicle.entity.passengers.lock().await;
@ -489,23 +379,12 @@ impl EntityBase for MinecartEntity {
} }
} }
let mut next_vel = let mut next_vel = velocity.multiply(friction, friction, friction);
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)
};
if next_vel.length() < 0.005 { if next_vel.length() < 0.005 {
next_vel = Vector3::new(0.0, 0.0, 0.0); next_vel = Vector3::new(0.0, 0.0, 0.0);
} }
self.vehicle.entity.velocity.store(next_vel); 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<'_, ()> { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move { Box::pin(async move {
self.vehicle.send_wobble_metadata(); 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, &'a self,
_caller: &'a dyn EntityBase, _caller: &'a dyn EntityBase,
amount: f32, amount: f32,
damage_type: DamageType, _damage_type: pumpkin_data::damage::DamageType,
_position: Option<Vector3<f64>>, _position: Option<Vector3<f64>>,
source: Option<&'a dyn EntityBase>, source: Option<&'a dyn EntityBase>,
cause: Option<&'a dyn EntityBase>, _cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> { ) -> EntityBaseFuture<'a, bool> {
Box::pin(async move { Box::pin(async move { self.vehicle.damage_with_context(amount, source).await })
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<dyn Inventory> = 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
})
} }
fn interact<'a>( fn interact<'a>(
&'a self, &'a self,
player: &'a Arc<Player>, player: &'a Arc<Player>,
item_stack: &'a mut ItemStack, _item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> { ) -> EntityBaseFuture<'a, bool> {
Box::pin(async move { Box::pin(async move {
match &self.kind { if player.get_entity().is_sneaking() {
MinecartKind::Chest(minecart) => { return false;
minecart.interact(&self.vehicle.entity, player).await
} }
MinecartKind::Furnace(minecart) => {
minecart.interact(&self.vehicle.entity, player, item_stack) if !self.vehicle.entity.passengers.lock().await.is_empty() {
return false;
} }
MinecartKind::Hopper(minecart) => {
minecart.interact(&self.vehicle.entity, player).await if player.get_entity().has_vehicle().await {
} return false;
MinecartKind::Rideable(minecart) => {
minecart.interact(&self.vehicle.entity, player).await
}
MinecartKind::Tnt(_) | MinecartKind::Other => 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<dyn EntityBase>)
.await;
true
}) })
} }
@ -860,22 +675,3 @@ impl EntityBase for MinecartEntity {
self 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)
);
}
}

View File

@ -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<MinecartInventory>,
}
impl ChestMinecart {
pub(super) fn new() -> Self {
Self {
inventory: Arc::new(MinecartInventory::new(27)),
}
}
pub(super) const fn inventory(&self) -> &Arc<MinecartInventory> {
&self.inventory
}
pub(super) async fn interact(&self, entity: &Entity, player: &Arc<Player>) -> 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);
}
}

View File

@ -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<Mutex<ItemStack>>]>,
loot_table: StdMutex<Option<(String, i64)>>,
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<Self>) {
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<dyn Inventory> = 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<Mutex<ItemStack>>> {
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<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
for slot in &self.items {
*slot.lock().await = ItemStack::EMPTY.clone();
}
})
}
}
struct MinecartScreenFactory {
inventory: Arc<MinecartInventory>,
title: TextComponent,
hopper: bool,
}
impl ScreenHandlerFactory for MinecartScreenFactory {
fn create_screen_handler<'a>(
&'a self,
sync_id: u8,
player_inventory: &'a Arc<PlayerInventory>,
_player: &'a dyn InventoryPlayer,
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
Box::pin(async move {
let inventory: Arc<dyn Inventory> = 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<Player>,
inventory: &Arc<MinecartInventory>,
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<f64>,
) -> Vector3<f64> {
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);
}
}

View File

@ -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<Vector3<f64>>,
}
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<f64>) -> Vector3<f64> {
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,
);
}
}

View File

@ -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<MinecartInventory>,
}
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<MinecartInventory> {
&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<Player>) -> 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);
}
}

View File

@ -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<Player>) -> 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<dyn EntityBase>)
.await;
true
}
}

View File

@ -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<f32>,
explosion_speed_factor: AtomicCell<f32>,
}
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);
}
}

View File

@ -8,7 +8,6 @@ use crate::entity::EntityBase;
use crate::world::loot::{LootContextParameters, LootTableExt}; use crate::world::loot::{LootContextParameters, LootTableExt};
use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tracked_data::TrackedData; use pumpkin_data::tracked_data::TrackedData;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_util::GameMode; use pumpkin_util::GameMode;
pub struct VehicleEntity { pub struct VehicleEntity {
@ -67,42 +66,25 @@ impl VehicleEntity {
pub fn send_wobble_metadata(&self) { pub fn send_wobble_metadata(&self) {
self.entity.send_meta_data( 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( Metadata::new(
TrackedData::ID_HURT, TrackedData::ID_HURT,
MetaDataType::INT, MetaDataType::INTEGER,
VarInt(self.get_hurt_time()), self.get_hurt_time(),
), ),
Metadata::new( Metadata::new(
TrackedData::ID_HURTDIR, TrackedData::ID_HURTDIR,
MetaDataType::INT, MetaDataType::INTEGER,
VarInt(self.get_hurt_dir()), self.get_hurt_dir(),
), ),
], ],
None, None,
); );
self.entity.send_meta_data( self.entity.send_meta_data(
&[ &[Metadata::new(
Metadata::new(
TrackedData::DAMAGE_WOBBLE_STRENGTH,
MetaDataType::FLOAT,
self.get_damage(),
),
Metadata::new(
TrackedData::ID_DAMAGE, TrackedData::ID_DAMAGE,
MetaDataType::FLOAT, MetaDataType::FLOAT,
self.get_damage(), self.get_damage(),
), )],
],
None, None,
); );
} }
@ -134,7 +116,16 @@ impl VehicleEntity {
return true; 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 let is_creative = source
.and_then(|s| s.get_player()) .and_then(|s| s.get_player())
@ -150,64 +141,4 @@ impl VehicleEntity {
true 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<u8> {
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
);
}
} }

View File

@ -88,7 +88,6 @@ impl ItemBehaviour for MinecartItem {
); );
let minecart_entity = Arc::new(MinecartEntity::new(entity)); let minecart_entity = Arc::new(MinecartEntity::new(entity));
world.spawn_entity(minecart_entity).await; world.spawn_entity(minecart_entity).await;
item.decrement_unless_creative(player.gamemode.load(), 1);
}) })
} }

View File

@ -18,34 +18,12 @@ use super::{BlockFlags, World};
pub struct Explosion { pub struct Explosion {
power: f32, power: f32,
pos: Vector3<f64>, pos: Vector3<f64>,
preserve_rails: bool,
} }
impl Explosion { impl Explosion {
#[must_use] #[must_use]
pub const fn new(power: f32, pos: Vector3<f64>) -> Self { pub const fn new(power: f32, pos: Vector3<f64>) -> Self {
Self { Self { power, pos }
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
} }
fn get_blocks_to_destroy( fn get_blocks_to_destroy(
@ -135,16 +113,12 @@ impl Explosion {
); );
if !state.is_air() || !fluid_state.is_empty { if !state.is_air() || !fluid_state.is_empty {
let protects_rail = self.protects_rail(world, &block_pos, block); let resistance =
let resistance = if protects_rail { fluid_state.blast_resistance.max(block.blast_resistance);
0.0
} else {
fluid_state.blast_resistance.max(block.blast_resistance)
};
h -= (resistance + 0.3) * 0.3; h -= (resistance + 0.3) * 0.3;
if h > 0.0 && !protects_rail { if h > 0.0 {
map.insert(block_pos, (block, state)); map.insert(block_pos, (block, state));
} }
} }
@ -333,22 +307,3 @@ impl Explosion {
blocks.len() as u32 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));
}
}

View File

@ -37,7 +37,7 @@ use crate::{
{OnNeighborUpdateArgs, OnScheduledTickArgs}, {OnNeighborUpdateArgs, OnScheduledTickArgs},
}, },
command::client_suggestions, 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, error::PumpkinError,
net::{ClientPlatform, java::JavaClient}, net::{ClientPlatform, java::JavaClient},
plugin::{ plugin::{
@ -391,11 +391,7 @@ impl World {
/// currently in; the chunk is rewritten from scratch every unload cycle, so /// currently in; the chunk is rewritten from scratch every unload cycle, so
/// there is nothing stale to deduplicate. /// there is nothing stale to deduplicate.
async fn save_entity(&self, entity: &Arc<dyn EntityBase>) { async fn save_entity(&self, entity: &Arc<dyn EntityBase>) {
let base_entity = entity.get_entity(); let current_chunk = entity.get_entity().block_pos.load().chunk_position();
if base_entity.is_removed() {
return;
}
let current_chunk = base_entity.block_pos.load().chunk_position();
let mut nbt = NbtCompound::new(); let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await; entity.write_nbt(&mut nbt).await;
let chunk = self.level.get_entity_chunk(current_chunk).await; let chunk = self.level.get_entity_chunk(current_chunk).await;
@ -3348,20 +3344,6 @@ impl World {
pub async fn explode(self: &Arc<Self>, position: Vector3<f64>, power: f32) { pub async fn explode(self: &Arc<Self>, position: Vector3<f64>, power: f32) {
let explosion = Explosion::new(power, position); let explosion = Explosion::new(power, position);
self.run_explosion(explosion, position, power).await;
}
pub async fn explode_tnt_minecart(self: &Arc<Self>, position: Vector3<f64>, power: f32) {
let explosion = Explosion::new(power, position).preserving_rails();
self.run_explosion(explosion, position, power).await;
}
async fn run_explosion(
self: &Arc<Self>,
explosion: Explosion,
position: Vector3<f64>,
power: f32,
) {
let block_count = explosion.explode(self).await; let block_count = explosion.explode(self).await;
let particle = if power < 2.0 { let particle = if power < 2.0 {
Particle::Explosion Particle::Explosion
@ -3793,7 +3775,6 @@ impl World {
.client .client
.enqueue_packet(&base_entity.create_spawn_packet()) .enqueue_packet(&base_entity.create_spawn_packet())
.await; .await;
player.try_restore_vehicle(&entity).await;
entities_to_add.push(entity); entities_to_add.push(entity);
} }
@ -3815,7 +3796,6 @@ impl World {
.client .client
.enqueue_packet(&base_entity.create_spawn_packet()) .enqueue_packet(&base_entity.create_spawn_packet())
.await; .await;
player.try_restore_vehicle(entity).await;
} }
} }
} }
@ -4299,15 +4279,6 @@ impl World {
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]
pub async fn remove_entity(&self, entity: &dyn EntityBase) { pub async fn remove_entity(&self, entity: &dyn EntityBase) {
let base_entity = entity.get_entity(); 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.spawn_state.load().remove_entity(self, entity);
self.entities.rcu(|current_entities| { self.entities.rcu(|current_entities| {
let mut new_entities = (**current_entities).clone(); let mut new_entities = (**current_entities).clone();