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:
ZlordHUN
2026-08-10 12:36:02 +02:00
committed by GitHub
parent fcb26179d3
commit bf48492121
14 changed files with 1375 additions and 109 deletions

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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() {

View File

@@ -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
})
}

View 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;
}
}

View 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);
}
}

View 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,
);
}
}

View 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);
}
}

View 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
}
}

View 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);
}
}

View File

@@ -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
);
}
}

View File

@@ -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);
})
}

View File

@@ -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));
}
}

View File

@@ -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();