Revert "feat: Add Mineshaft Generation (#2740)"
This reverts commit 09d23bd15f.
This commit is contained in:
parent
b55feecd74
commit
a748aae07e
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
///
|
||||
/// 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] = [
|
||||
|
||||
@ -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<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 {
|
||||
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::<StructureKeys, Vec<_>>::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
||||
@ -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)]
|
||||
pub async fn remove_passenger(&self, passenger_id: i32) {
|
||||
let mut passengers = self.passengers.lock().await;
|
||||
|
||||
@ -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<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
|
||||
|
||||
/// Food exhaustion applied for every block a player mines.
|
||||
@ -551,7 +512,6 @@ pub struct Player {
|
||||
pub experience_pick_up_delay: Mutex<u32>,
|
||||
pub chunk_manager: Mutex<ChunkManager>,
|
||||
pub has_played_before: AtomicBool,
|
||||
root_vehicle_uuid: AtomicCell<Option<Uuid>>,
|
||||
pub chat_session: Arc<Mutex<ChatSession>>,
|
||||
pub signature_cache: Mutex<MessageCache>,
|
||||
pub player_screen_handler: Arc<Mutex<PlayerScreenHandler>>,
|
||||
@ -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<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
|
||||
.lock()
|
||||
.await
|
||||
@ -1015,21 +964,6 @@ impl Player {
|
||||
//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)]
|
||||
pub async fn attack(&self, victim: Arc<dyn EntityBase>) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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<MinecartInventory>> {
|
||||
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 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<Vector3<f64>>,
|
||||
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<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
|
||||
})
|
||||
Box::pin(async move { self.vehicle.damage_with_context(amount, source).await })
|
||||
}
|
||||
|
||||
fn interact<'a>(
|
||||
&'a self,
|
||||
player: &'a Arc<Player>,
|
||||
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<dyn EntityBase>)
|
||||
.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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -18,34 +18,12 @@ use super::{BlockFlags, World};
|
||||
pub struct Explosion {
|
||||
power: f32,
|
||||
pos: Vector3<f64>,
|
||||
preserve_rails: bool,
|
||||
}
|
||||
|
||||
impl Explosion {
|
||||
#[must_use]
|
||||
pub const fn new(power: f32, pos: Vector3<f64>) -> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<dyn EntityBase>) {
|
||||
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<Self>, position: Vector3<f64>, power: f32) {
|
||||
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 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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user