mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add chest, furnace, hopper, and TNT minecarts (#2376)
* feat(world): support generated loot minecarts * Minecart Vanila Parity * Rebased And Added Furnace, Hopper, TNT Minecarts * Refactor minecart variants into dedicated modules --------- Co-authored-by: Zoltán Virágh <zoltan.viragh@cloudtalk.io>
This commit is contained in:
@@ -3226,6 +3226,28 @@ 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;
|
||||
|
||||
@@ -121,6 +121,45 @@ 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.
|
||||
@@ -532,6 +571,7 @@ 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,6 +830,7 @@ 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(),
|
||||
@@ -958,6 +999,16 @@ 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
|
||||
@@ -1004,6 +1055,21 @@ 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();
|
||||
@@ -4755,6 +4821,18 @@ 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);
|
||||
})
|
||||
}
|
||||
@@ -4825,6 +4903,7 @@ 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);
|
||||
})
|
||||
}
|
||||
@@ -5750,7 +5829,9 @@ impl InventoryPlayer for Player {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::bedrock_inventory_slot;
|
||||
use super::{bedrock_inventory_slot, read_root_vehicle, write_root_vehicle};
|
||||
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn player_screen_slots_map_to_bedrock_inventory() {
|
||||
@@ -5761,4 +5842,39 @@ mod tests {
|
||||
assert_eq!(bedrock_inventory_slot(8), None);
|
||||
assert_eq!(bedrock_inventory_slot(45), None);
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,7 +508,14 @@ impl EntityBase for ArrowEntity {
|
||||
}
|
||||
|
||||
let damage_succeeded = target
|
||||
.damage(&*target, damage as f32, DamageType::ARROW)
|
||||
.damage_with_context(
|
||||
&*target,
|
||||
damage as f32,
|
||||
DamageType::ARROW,
|
||||
Some(hit_pos),
|
||||
None,
|
||||
Some(self),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(living) = target.get_living_entity() {
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
mod chest;
|
||||
mod container;
|
||||
mod furnace;
|
||||
mod hopper;
|
||||
mod rideable;
|
||||
mod tnt;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use pumpkin_protocol::java::server::play::SPlayerInput;
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::{
|
||||
entity::{
|
||||
Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player,
|
||||
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, 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,
|
||||
@@ -41,21 +62,84 @@ const RAIL_HEIGHT_OFFSET: f64 = 0.0625;
|
||||
|
||||
pub struct MinecartEntity {
|
||||
pub vehicle: VehicleEntity,
|
||||
pub active: AtomicBool,
|
||||
pub tnt_fuse: AtomicI32,
|
||||
kind: MinecartKind,
|
||||
}
|
||||
|
||||
enum MinecartKind {
|
||||
Rideable(RideableMinecart),
|
||||
Chest(ChestMinecart),
|
||||
Furnace(FurnaceMinecart),
|
||||
Hopper(HopperMinecart),
|
||||
Tnt(TntMinecart),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl MinecartEntity {
|
||||
pub const fn new(entity: Entity) -> Self {
|
||||
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 {
|
||||
vehicle: VehicleEntity::new(entity),
|
||||
active: AtomicBool::new(false),
|
||||
tnt_fuse: AtomicI32::new(-1),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NBTStorage for MinecartEntity {}
|
||||
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).await,
|
||||
MinecartKind::Furnace(minecart) => minecart.read_nbt(nbt),
|
||||
MinecartKind::Hopper(minecart) => minecart.read_nbt(nbt).await,
|
||||
MinecartKind::Tnt(minecart) => minecart.read_nbt(nbt),
|
||||
MinecartKind::Rideable(_) | MinecartKind::Other => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityBase for MinecartEntity {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
@@ -66,6 +150,9 @@ 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();
|
||||
@@ -111,7 +198,9 @@ impl EntityBase for MinecartEntity {
|
||||
let props = PoweredRailLikeProperties::from_state_id(state_id, block);
|
||||
let powered = props.powered;
|
||||
|
||||
self.active.store(powered, Ordering::Relaxed);
|
||||
if is_activator_rail && let MinecartKind::Hopper(minecart) = &self.kind {
|
||||
minecart.set_enabled(!powered);
|
||||
}
|
||||
|
||||
if powered {
|
||||
if is_powered_rail {
|
||||
@@ -137,27 +226,27 @@ impl EntityBase for MinecartEntity {
|
||||
}
|
||||
self.vehicle.entity.send_velocity();
|
||||
} else if is_activator_rail {
|
||||
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,
|
||||
);
|
||||
match &self.kind {
|
||||
MinecartKind::Tnt(minecart) => {
|
||||
minecart.prime(&self.vehicle.entity, 80);
|
||||
}
|
||||
} 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
|
||||
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 is_powered_rail {
|
||||
@@ -171,27 +260,10 @@ impl EntityBase for MinecartEntity {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
if let MinecartKind::Tnt(minecart) = &self.kind
|
||||
&& minecart.tick(&self.vehicle.entity).await
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut velocity = self.vehicle.entity.velocity.load();
|
||||
@@ -352,6 +424,24 @@ 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;
|
||||
@@ -363,7 +453,7 @@ impl EntityBase for MinecartEntity {
|
||||
self.vehicle.entity.send_pos_rot();
|
||||
|
||||
#[allow(clippy::useless_let_if_seq)]
|
||||
let mut friction = 0.98; // Default off-rail air drag
|
||||
let mut friction = 0.95; // Vanilla minecart air drag
|
||||
|
||||
if is_on_rails {
|
||||
let passengers = self.vehicle.entity.passengers.lock().await;
|
||||
@@ -392,7 +482,14 @@ impl EntityBase for MinecartEntity {
|
||||
}
|
||||
}
|
||||
|
||||
let mut next_vel = velocity.multiply(friction, friction, friction);
|
||||
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)
|
||||
};
|
||||
if next_vel.length() < 0.005 {
|
||||
next_vel = Vector3::new(0.0, 0.0, 0.0);
|
||||
}
|
||||
@@ -401,6 +498,10 @@ impl EntityBase for MinecartEntity {
|
||||
self.vehicle.entity.send_velocity();
|
||||
}
|
||||
}
|
||||
|
||||
if let MinecartKind::Hopper(minecart) = &self.kind {
|
||||
minecart.tick(&self.vehicle.entity).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -564,6 +665,9 @@ 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -575,47 +679,108 @@ impl EntityBase for MinecartEntity {
|
||||
&'a self,
|
||||
_caller: &'a dyn EntityBase,
|
||||
amount: f32,
|
||||
_damage_type: pumpkin_data::damage::DamageType,
|
||||
damage_type: 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 { self.vehicle.damage_with_context(amount, source).await })
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
if player.get_entity().is_sneaking() {
|
||||
return false;
|
||||
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 !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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
48
crates/pumpkin/src/entity/vehicle/minecart/chest.rs
Normal file
48
crates/pumpkin/src/entity/vehicle/minecart/chest.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
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) async fn read_nbt(&self, nbt: &NbtCompound) {
|
||||
self.inventory.read_nbt(nbt).await;
|
||||
}
|
||||
}
|
||||
276
crates/pumpkin/src/entity/vehicle/minecart/container.rs
Normal file
276
crates/pumpkin/src/entity/vehicle/minecart/container.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use std::any::Any;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
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};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
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: RwLock<Vec<ItemStack>>,
|
||||
size: usize,
|
||||
loot_table: Mutex<Option<(String, i64)>>,
|
||||
drops_claimed: AtomicBool,
|
||||
}
|
||||
|
||||
impl MinecartInventory {
|
||||
pub(super) fn new(size: usize) -> Self {
|
||||
Self {
|
||||
items: RwLock::new(vec![ItemStack::EMPTY.clone(); size]),
|
||||
size,
|
||||
loot_table: Mutex::new(None),
|
||||
drops_claimed: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn claim_drops(&self) -> bool {
|
||||
!self.drops_claimed.swap(true, Ordering::AcqRel)
|
||||
}
|
||||
|
||||
pub(super) async 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().await = loot_table;
|
||||
|
||||
if !has_loot_table {
|
||||
let mut items = self.items.write().await;
|
||||
items.fill_with(|| ItemStack::EMPTY.clone());
|
||||
self.read_data(nbt, &mut items);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn write_nbt(&self, nbt: &mut NbtCompound) {
|
||||
let loot_table = self.loot_table.lock().await.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) async fn has_loot_table(&self) -> bool {
|
||||
self.loot_table.lock().await.is_some()
|
||||
}
|
||||
|
||||
pub(super) async fn unpack_loot(self: &Arc<Self>) {
|
||||
let loot_table = self.loot_table.lock().await.take();
|
||||
let Some((loot_table, seed)) = loot_table else {
|
||||
return;
|
||||
};
|
||||
let Some(table) = get_chest_loot_table(&loot_table) else {
|
||||
*self.loot_table.lock().await = 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.size
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> InventoryFuture<'_, bool> {
|
||||
Box::pin(async move {
|
||||
let items = self.items.read().await;
|
||||
items.iter().all(ItemStack::is_empty)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
|
||||
Box::pin(async move { self.items.read().await[slot].clone() })
|
||||
}
|
||||
|
||||
fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
|
||||
Box::pin(async move {
|
||||
let mut items = self.items.write().await;
|
||||
std::mem::replace(&mut items[slot], ItemStack::EMPTY.clone())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> {
|
||||
Box::pin(async move {
|
||||
let mut items = self.items.write().await;
|
||||
if !items[slot].is_empty() && amount > 0 {
|
||||
items[slot].split(amount)
|
||||
} else {
|
||||
ItemStack::EMPTY.clone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
self.items.write().await[slot] = 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 {
|
||||
self.items
|
||||
.write()
|
||||
.await
|
||||
.fill_with(|| 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().await {
|
||||
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().await {
|
||||
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).await;
|
||||
|
||||
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).await;
|
||||
let stack = restored.get_stack(8).await;
|
||||
assert_eq!(stack.get_item().id, Item::POWERED_RAIL.id);
|
||||
assert_eq!(stack.item_count, 3);
|
||||
}
|
||||
}
|
||||
160
crates/pumpkin/src/entity/vehicle/minecart/furnace.rs
Normal file
160
crates/pumpkin/src/entity/vehicle/minecart/furnace.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
142
crates/pumpkin/src/entity/vehicle/minecart/hopper.rs
Normal file
142
crates/pumpkin/src/entity/vehicle/minecart/hopper.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
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;
|
||||
if stack.is_empty()
|
||||
|| !source.can_transfer_to(self.inventory.as_ref(), slot, &stack)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let backup = stack.clone();
|
||||
let one = source.remove_stack_specific(slot, 1).await;
|
||||
if HopperBlockEntity::add_one_item(source.as_ref(), self.inventory.as_ref(), one)
|
||||
.await
|
||||
{
|
||||
return;
|
||||
}
|
||||
source.set_stack(slot, backup).await;
|
||||
}
|
||||
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) async fn read_nbt(&self, nbt: &NbtCompound) {
|
||||
self.inventory.read_nbt(nbt).await;
|
||||
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);
|
||||
}
|
||||
}
|
||||
29
crates/pumpkin/src/entity/vehicle/minecart/rideable.rs
Normal file
29
crates/pumpkin/src/entity/vehicle/minecart/rideable.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
157
crates/pumpkin/src/entity/vehicle/minecart/tnt.rs
Normal file
157
crates/pumpkin/src/entity/vehicle/minecart/tnt.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
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_protocol::bedrock::server::actor_event::ActorEventType;
|
||||
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,
|
||||
Some(ActorEventType::CartWithPrimeTNT),
|
||||
);
|
||||
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,6 +8,7 @@ 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,24 +68,41 @@ impl VehicleEntity {
|
||||
self.entity.send_meta_data(
|
||||
&[
|
||||
Metadata::new(
|
||||
TrackedData::ID_HURT,
|
||||
TrackedData::DAMAGE_WOBBLE_TICKS,
|
||||
MetaDataType::INTEGER,
|
||||
self.get_hurt_time(),
|
||||
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()),
|
||||
),
|
||||
Metadata::new(
|
||||
TrackedData::ID_HURTDIR,
|
||||
MetaDataType::INTEGER,
|
||||
self.get_hurt_dir(),
|
||||
MetaDataType::INT,
|
||||
VarInt(self.get_hurt_dir()),
|
||||
),
|
||||
],
|
||||
None,
|
||||
);
|
||||
self.entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
TrackedData::ID_DAMAGE,
|
||||
MetaDataType::FLOAT,
|
||||
self.get_damage(),
|
||||
)],
|
||||
&[
|
||||
Metadata::new(
|
||||
TrackedData::DAMAGE_WOBBLE_STRENGTH,
|
||||
MetaDataType::FLOAT,
|
||||
self.get_damage(),
|
||||
),
|
||||
Metadata::new(
|
||||
TrackedData::ID_DAMAGE,
|
||||
MetaDataType::FLOAT,
|
||||
self.get_damage(),
|
||||
),
|
||||
],
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -116,16 +134,7 @@ impl VehicleEntity {
|
||||
return true;
|
||||
}
|
||||
|
||||
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 new_strength = self.apply_damage_wobble(amount);
|
||||
|
||||
let is_creative = source
|
||||
.and_then(|s| s.get_player())
|
||||
@@ -141,4 +150,64 @@ 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,6 +88,7 @@ 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,12 +18,34 @@ 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 }
|
||||
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
|
||||
}
|
||||
|
||||
fn get_blocks_to_destroy(
|
||||
@@ -113,12 +135,16 @@ impl Explosion {
|
||||
);
|
||||
|
||||
if !state.is_air() || !fluid_state.is_empty {
|
||||
let resistance =
|
||||
fluid_state.blast_resistance.max(block.blast_resistance);
|
||||
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)
|
||||
};
|
||||
|
||||
h -= (resistance + 0.3) * 0.3;
|
||||
|
||||
if h > 0.0 {
|
||||
if h > 0.0 && !protects_rail {
|
||||
map.insert(block_pos, (block, state));
|
||||
}
|
||||
}
|
||||
@@ -307,3 +333,22 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::{
|
||||
{OnNeighborUpdateArgs, OnScheduledTickArgs},
|
||||
},
|
||||
command::client_suggestions,
|
||||
entity::{Entity, EntityBase, NBTStorage, player::Player, r#type::from_type},
|
||||
entity::{Entity, EntityBase, NBTStorage, RemovalReason, player::Player, r#type::from_type},
|
||||
error::PumpkinError,
|
||||
net::{ClientPlatform, java::JavaClient},
|
||||
plugin::{
|
||||
@@ -400,7 +400,11 @@ 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 current_chunk = entity.get_entity().block_pos.load().chunk_position();
|
||||
let base_entity = entity.get_entity();
|
||||
if base_entity.is_removed() {
|
||||
return;
|
||||
}
|
||||
let current_chunk = base_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;
|
||||
@@ -3357,6 +3361,20 @@ 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
|
||||
@@ -3785,6 +3803,7 @@ impl World {
|
||||
base_entity.velocity.store(Vector3::default());
|
||||
|
||||
player.client.enqueue_spawn_packet(&entity).await;
|
||||
player.try_restore_vehicle(&entity).await;
|
||||
entities_to_add.push(entity);
|
||||
}
|
||||
|
||||
@@ -3803,6 +3822,7 @@ impl World {
|
||||
let base_entity = entity.get_entity();
|
||||
if base_entity.chunk_pos.load() == position {
|
||||
player.client.enqueue_spawn_packet(entity).await;
|
||||
player.try_restore_vehicle(entity).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4284,6 +4304,15 @@ 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();
|
||||
|
||||
Reference in New Issue
Block a user