feat: add fishing rod

This commit is contained in:
Alexander Medvedev 2026-05-10 17:53:13 +02:00
parent 1abfb195de
commit 4f69e2cb7a
14 changed files with 443 additions and 83 deletions

View File

@ -266,7 +266,12 @@ impl<W: Write> ser::Serializer for &mut Serializer<W> {
// Just skip serializing if value is none
fn serialize_none(self) -> Result<()> {
Ok(())
match self.state {
State::FirstListElement { .. } | State::ListElement | State::CheckedListElement => Err(
Error::SerdeError("NBT lists do not support null/none values".to_string()),
),
_ => Ok(()),
}
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<()> {

View File

@ -52,7 +52,9 @@ impl LinearV2Superblock {
fn from_bytes(buf: &[u8]) -> Result<Self, ChunkReadingError> {
if buf.len() < Self::SIZE {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let mut b = buf;
let mut sig = [0u8; 8];
@ -150,14 +152,18 @@ impl NbtFeatures {
let mut map = HashMap::new();
loop {
if !buf.has_remaining() {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let key_len = buf.get_u8();
if key_len == 0 {
break;
}
if buf.remaining() < key_len as usize + 4 {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let key_bytes: Vec<u8> = (0..key_len).map(|_| buf.get_u8()).collect();
let key = String::from_utf8(key_bytes).map_err(|_| ChunkReadingError::InvalidHeader)?;
@ -199,7 +205,9 @@ impl BucketSizeEntry {
fn from_bytes(buf: &mut impl Buf) -> Result<Self, ChunkReadingError> {
if buf.remaining() < Self::SIZE {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
Ok(Self {
size: buf.get_u32(),
@ -251,7 +259,9 @@ impl BucketChunkEntry {
fn read_from(buf: &mut Bytes) -> Result<Self, ChunkReadingError> {
if buf.remaining() < Self::FIXED_SIZE {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let size = buf.get_u32() as usize;
let timestamp = buf.get_u64();
@ -266,7 +276,9 @@ impl BucketChunkEntry {
"Linear v2: not enough bytes for chunk (need {size}, have {})",
buf.remaining()
);
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let data = buf.split_to(size);
Ok(Self {
@ -440,7 +452,9 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
let mut buf = raw_file;
if buf.remaining() < LinearV2Superblock::SIZE {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let superblock_bytes = buf.split_to(LinearV2Superblock::SIZE);
let superblock = LinearV2Superblock::from_bytes(&superblock_bytes)?;
@ -448,7 +462,9 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
let bucket_count = Self::bucket_count(grid_size);
if buf.remaining() < ChunkBitmap::SIZE {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
// We read the bitmap but per the spec it is not yet reliable, so we
// do not use it to short-circuit — actual presence is determined by
@ -461,7 +477,9 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
let total_bucket_meta = bucket_count * BucketSizeEntry::SIZE;
if buf.remaining() < total_bucket_meta {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let mut bucket_entries: Vec<BucketSizeEntry> = Vec::with_capacity(bucket_count);
for _ in 0..bucket_count {
@ -472,7 +490,9 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
// consuming the bucket data bytes yet.
let total_compressed: usize = bucket_entries.iter().map(|e| e.size as usize).sum();
if buf.remaining() < total_compressed + SIGNATURE.len() {
return Err(ChunkReadingError::IoError(ErrorKind::UnexpectedEof));
return Err(ChunkReadingError::IoError(std::io::Error::from(
ErrorKind::UnexpectedEof,
)));
}
let footer_offset = total_compressed;
{
@ -510,7 +530,7 @@ impl<S: SingleChunkDataSerializer> ChunkSerializer for LinearV2File<S> {
.map_err(|_| ChunkReadingError::RegionIsInvalid)?;
decoder
.read_to_end(&mut decompressed)
.map_err(|e| ChunkReadingError::IoError(e.kind()))?
.map_err(ChunkReadingError::IoError)?
};
buf.advance(compressed_size);

View File

@ -1,7 +1,8 @@
use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::path::PathBuf;
use crate::chunk::format::anvil::{CHUNK_COUNT, SingleChunkDataSerializer};
use crate::chunk::format::anvil::SingleChunkDataSerializer;
use crate::chunk::io::{ChunkSerializer, LoadedData};
use crate::chunk::{ChunkReadingError, ChunkWritingError};
use bytes::Bytes;
@ -9,7 +10,6 @@ use pumpkin_util::math::vector2::Vector2;
use ruzstd::decoding::StreamingDecoder;
use ruzstd::encoding::{CompressionLevel, compress_to_vec};
use serde::{Deserialize, Serialize};
use tracing::warn;
pub struct PumpFile<D> {
pub data: PumpData,
@ -20,17 +20,13 @@ pub struct PumpFile<D> {
pub struct PumpData {
pub x: i32,
pub z: i32,
pub chunks: Vec<Option<Vec<u8>>>,
pub chunks: BTreeMap<String, Vec<u8>>,
}
impl<D> Default for PumpFile<D> {
fn default() -> Self {
Self {
data: PumpData {
x: 0,
z: 0,
chunks: vec![None; CHUNK_COUNT],
},
data: PumpData::default(),
_phantom: PhantomData,
}
}
@ -66,10 +62,10 @@ where
fn read(r: Bytes) -> Result<Self, ChunkReadingError> {
let mut decoder = StreamingDecoder::new(&r[..])
.map_err(|e| ChunkReadingError::IoError(std::io::Error::other(e.to_string()).kind()))?;
.map_err(|e| ChunkReadingError::IoError(std::io::Error::other(e.to_string())))?;
let mut decompressed = Vec::new();
std::io::Read::read_to_end(&mut decoder, &mut decompressed)
.map_err(|e| ChunkReadingError::IoError(e.kind()))?;
.map_err(ChunkReadingError::IoError)?;
let data: PumpData = pumpkin_nbt::from_bytes_unnamed(std::io::Cursor::new(decompressed))
.map_err(|e| {
@ -101,11 +97,7 @@ where
.await
.map_err(|e| ChunkWritingError::ChunkSerializingError(e.to_string()))?;
if index < self.data.chunks.len() {
self.data.chunks[index] = Some(bytes.to_vec());
} else {
warn!("Chunk position {}, {} out of range for Pump region", x, z);
}
self.data.chunks.insert(index.to_string(), bytes.to_vec());
Ok(())
}
@ -120,8 +112,7 @@ where
let rel_z = pos.y.rem_euclid(32);
let index = (rel_x + rel_z * 32) as usize;
if index < self.data.chunks.len() {
if let Some(chunk_bytes) = &self.data.chunks[index] {
if let Some(chunk_bytes) = self.data.chunks.get(&index.to_string()) {
let bytes = Bytes::copy_from_slice(chunk_bytes);
match D::from_bytes(&bytes, pos) {
Ok(data) => {
@ -134,18 +125,6 @@ where
} else {
let _ = stream.send(LoadedData::Missing(pos)).await;
}
} else {
let _ = stream
.send(LoadedData::Error((
pos,
ChunkReadingError::ParsingError(
crate::chunk::ChunkParsingError::ErrorDeserializingChunk(
"Chunk out of range".to_string(),
),
),
)))
.await;
}
}
}
}

View File

@ -111,7 +111,7 @@ impl<S: ChunkSerializer<WriteBackend = PathBuf>> ChunkSerializerLazyLoader<S> {
trace!("File not found, using default for: {}", self.path.display());
Ok(S::default())
}
Err(err) => Err(ChunkReadingError::IoError(err.kind())),
Err(err) => Err(ChunkReadingError::IoError(err)),
}
}
}
@ -338,8 +338,10 @@ where
error!("I/O error reading region before write: {err}");
return Err(ChunkWritingError::IoError(err));
}
Err(_) => {
return Err(ChunkWritingError::IoError(std::io::ErrorKind::Other));
Err(err) => {
return Err(ChunkWritingError::IoError(std::io::Error::other(
err.to_string(),
)));
}
};
@ -377,7 +379,7 @@ where
serializer
.write(&path)
.await
.map_err(|e| ChunkWritingError::IoError(e.kind()))?
.map_err(ChunkWritingError::IoError)?
// Read-lock released here.
};

View File

@ -30,7 +30,7 @@ pub const SUBCHUNK_VOLUME: usize = CHUNK_AREA * CHUNK_WIDTH;
#[derive(Error, Debug)]
pub enum ChunkReadingError {
#[error("Io error: {0}")]
IoError(std::io::ErrorKind),
IoError(std::io::Error),
#[error("Invalid header")]
InvalidHeader,
#[error("Region is invalid")]
@ -46,7 +46,7 @@ pub enum ChunkReadingError {
#[derive(Error, Debug)]
pub enum ChunkWritingError {
#[error("Io error: {0}")]
IoError(std::io::ErrorKind),
IoError(std::io::Error),
#[error("Compression error {0}")]
Compression(CompressionError),
#[error("Chunk serializing error: {0}")]

View File

@ -549,7 +549,9 @@ impl GenerationSchedule {
}
let sc = Arc::strong_count(&chunk);
if sc == 1 {
if chunk.is_dirty() {
chunks.push((pos, Chunk::Level(chunk)));
}
self.chunk_map.remove(&pos);
} else {
warn!(

View File

@ -20,7 +20,7 @@ use std::{collections::HashMap, sync::atomic::AtomicI32};
use tracing::warn;
use super::experience_orb::ExperienceOrbEntity;
use super::{Entity, EntityBase, NBTStorage};
use super::{Entity, EntityBase, NBTStorage, NBTStorageInit};
use crate::block::OnLandedUponArgs;
use crate::entity::attributes::AttributeInstance;
use crate::entity::attributes::Modifier;
@ -47,8 +47,8 @@ use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::{
Animation, CEntityAnimation, CHurtAnimation, CSetPlayerInventory, CTakeItemEntity,
CUpdateMobEffect,
Animation, CEntityAnimation, CEntityStatus, CHurtAnimation, CSetPlayerInventory,
CTakeItemEntity, CUpdateMobEffect,
};
use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer,
@ -1968,14 +1968,14 @@ impl EntityBase for LivingEntity {
disable_chance = 1.0;
}
if rand::random::<f32>() < disable_chance {
if let Some(victim_player) = caller.get_player() {
if rand::random::<f32>() < disable_chance
&& let Some(victim_player) = caller.get_player()
{
victim_player
.start_cooldown("minecraft:shield".to_string(), 100)
.await;
self.clear_active_hand().await;
use pumpkin_protocol::java::client::play::CEntityStatus;
world
.broadcast_packet_all(&CEntityStatus::new(
self.entity.entity_id,
@ -1985,7 +1985,6 @@ impl EntityBase for LivingEntity {
}
}
}
}
let active_hand = self.active_hand.lock().await;
if let Some(hand) = *active_hand {

View File

@ -497,6 +497,7 @@ pub struct Player {
pub tab_list_latency: AtomicI32,
pub tab_list_listed: AtomicBool,
pub enchantment_seed: AtomicI32,
pub fishing_bobber: AtomicI32,
}
impl Player {
@ -633,6 +634,7 @@ impl Player {
tab_list_order: AtomicI32::new(0),
tab_list_latency: AtomicI32::new(0),
tab_list_listed: AtomicBool::new(false),
fishing_bobber: AtomicI32::new(-1),
}
}

View File

@ -0,0 +1,255 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use crate::entity::projectile::{ProjectileHit, is_projectile};
use crate::{
entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player,
},
server::Server,
};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::vector3::Vector3;
pub struct FishingBobberEntity {
pub entity: Entity,
pub owner_id: i32,
pub hooked_entity_id: AtomicI32,
pub in_ground: AtomicBool,
pub has_hit: AtomicBool,
pub wait_countdown: AtomicI32,
pub bite_countdown: AtomicI32,
}
impl FishingBobberEntity {
const WATER_INERTIA: f64 = 0.8;
const AIR_INERTIA: f64 = 0.92;
const GRAVITY: f64 = 0.03;
pub fn new(entity: Entity, owner: &Player) -> Self {
let mut owner_pos = owner.living_entity.entity.pos.load();
owner_pos.y += owner.living_entity.entity.get_eye_height() - 0.1;
entity.pos.store(owner_pos);
Self {
entity,
owner_id: owner.living_entity.entity.entity_id,
hooked_entity_id: AtomicI32::new(0),
in_ground: AtomicBool::new(false),
has_hit: AtomicBool::new(false),
wait_countdown: AtomicI32::new(rand::random::<i32>().abs() % 600 + 100),
bite_countdown: AtomicI32::new(0),
}
}
pub async fn reel_in(&self, player: &Player) -> i32 {
let world = self.entity.world.load();
let hooked_id = self.hooked_entity_id.load(Ordering::Relaxed);
if hooked_id != 0
&& let Some(hooked) = world.get_entity_by_id(hooked_id)
{
let player_pos = player.living_entity.entity.pos.load();
let hooked_pos = hooked.get_entity().pos.load();
let delta = player_pos - hooked_pos;
let motion =
delta
.multiply(0.1, 0.1, 0.1)
.add_raw(0.0, delta.length().sqrt() * 0.08, 0.0);
hooked.get_entity().add_velocity(motion).await;
return 1;
}
if self.bite_countdown.load(Ordering::Relaxed) > 0 {
// Caught something!
// TODO: Use actual loot tables. For now, just give a raw cod.
use pumpkin_data::item::Item;
let _item_stack = ItemStack::new(1, &Item::COD);
// player.inventory().add_item(item_stack).await; // Need public add_item
world
.play_sound(
Sound::EntityExperienceOrbPickup,
SoundCategory::Neutral,
&player.position(),
)
.await;
return 1;
}
0
}
#[expect(clippy::too_many_lines)]
pub async fn process_tick<'a>(&'a self, caller: &'a Arc<dyn EntityBase>, _server: &'a Server) {
let entity = self.get_entity();
let world = entity.world.load();
if self.in_ground.load(Ordering::Relaxed) {
return;
}
let hooked_id = self.hooked_entity_id.load(Ordering::Relaxed);
if hooked_id != 0 {
if let Some(hooked) = world.get_entity_by_id(hooked_id) {
if hooked.get_entity().removed.load(Ordering::Relaxed) {
self.hooked_entity_id.store(0, Ordering::Relaxed);
} else {
let mut hooked_pos = hooked.get_entity().pos.load();
hooked_pos.y += hooked.get_entity().get_eye_height() * 0.8;
entity.set_pos(hooked_pos);
return;
}
} else {
self.hooked_entity_id.store(0, Ordering::Relaxed);
}
}
let mut velocity = entity.velocity.load();
let start_pos = entity.pos.load();
if entity.touching_water.load(Ordering::Relaxed) {
velocity.y += 0.02; // Buoyancy
let bite = self.bite_countdown.load(Ordering::Relaxed);
if bite > 0 {
self.bite_countdown.store(bite - 1, Ordering::Relaxed);
if bite % 5 == 0 {
world
.spawn_particle(
entity.pos.load(),
Vector3::new(0.1f32, 0.1f32, 0.1f32),
0.0,
5,
pumpkin_data::particle::Particle::Bubble,
)
.await;
}
} else {
let wait = self.wait_countdown.load(Ordering::Relaxed);
if wait > 0 {
self.wait_countdown.store(wait - 1, Ordering::Relaxed);
} else {
// Start bite
self.bite_countdown.store(40, Ordering::Relaxed);
self.wait_countdown
.store(rand::random::<i32>().abs() % 600 + 100, Ordering::Relaxed);
world
.play_sound(
Sound::EntityFishingBobberSplash,
SoundCategory::Neutral,
&entity.pos.load(),
)
.await;
}
}
} else {
velocity.y -= Self::GRAVITY;
}
let inertia = if entity.touching_water.load(Ordering::Relaxed) {
Self::WATER_INERTIA
} else {
Self::AIR_INERTIA
};
velocity = velocity.multiply(inertia, inertia, inertia);
entity.velocity.store(velocity);
let new_pos = start_pos.add(&velocity);
let search_box = BoundingBox::new(
Vector3::new(
start_pos.x.min(new_pos.x),
start_pos.y.min(new_pos.y),
start_pos.z.min(new_pos.z),
),
Vector3::new(
start_pos.x.max(new_pos.x),
start_pos.y.max(new_pos.y),
start_pos.z.max(new_pos.z),
),
)
.expand(0.3, 0.3, 0.3);
// Basic block collision to stop bobber
let (block_cols, _) = world
.get_block_collisions(search_box, caller.as_ref())
.await;
if !block_cols.is_empty() {
self.in_ground.store(true, Ordering::Relaxed);
entity.velocity.store(Vector3::new(0.0, 0.0, 0.0));
return;
}
entity.set_pos(new_pos);
let candidates = world.get_entities_at_box(&search_box);
for cand in candidates {
if cand.get_entity().entity_id == self.owner_id
|| cand.get_entity().entity_id == entity.entity_id
{
continue;
}
if is_projectile(cand.get_entity().entity_type) {
continue;
}
let ebb = cand.get_entity().bounding_box.load().expand(0.3, 0.3, 0.3);
if ebb.intersects(&search_box) {
self.hooked_entity_id
.store(cand.get_entity().entity_id, Ordering::Relaxed);
entity
.send_meta_data(&[Metadata::new(
TrackedData::HOOKED_ENTITY,
MetaDataType::INT,
cand.get_entity().entity_id + 1,
)])
.await;
return;
}
}
}
}
impl NBTStorage for FishingBobberEntity {}
impl EntityBase for FishingBobberEntity {
fn get_entity(&self) -> &Entity {
&self.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn on_hit(&self, _hit: ProjectileHit) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
self.has_hit.store(true, Ordering::Relaxed);
})
}
fn tick<'a>(
&'a self,
caller: &'a Arc<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.process_tick(caller, server).await;
})
}
}

View File

@ -15,6 +15,7 @@ pub mod ender_pearl;
pub mod eye_of_ender;
pub mod fireball;
pub mod firework_rocket;
pub mod fishing_bobber;
pub mod lingering_potion;
pub mod shulker_bullet;
pub mod small_fireball;
@ -35,6 +36,7 @@ pub fn is_projectile(entity_type: &EntityType) -> bool {
|| *entity_type == EntityType::SHULKER_BULLET
|| *entity_type == EntityType::FIREBALL
|| *entity_type == EntityType::SMALL_FIREBALL
|| *entity_type == EntityType::FISHING_BOBBER
}
pub struct ThrownItemEntity {

View File

@ -134,13 +134,15 @@ impl CrossbowItem {
.cloned();
if let Some(charged) = projectiles {
let mut has_multishot = false;
if let Some(enchantments) = stack.get_data_component::<EnchantmentsImpl>() {
has_multishot = enchantments
let has_multishot =
stack
.get_data_component::<EnchantmentsImpl>()
.is_some_and(|enchantments| {
enchantments
.enchantment
.iter()
.any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT);
}
.any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT)
});
let world = player.world();
world

View File

@ -0,0 +1,89 @@
use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::entity::player::Player;
use crate::entity::projectile::fishing_bobber::FishingBobberEntity;
use crate::entity::{Entity, EntityBase};
use crate::item::{ItemBehaviour, ItemMetadata};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::sound::{Sound, SoundCategory};
pub struct FishingRodItem;
impl ItemMetadata for FishingRodItem {
fn ids() -> Box<[u16]> {
Box::new([Item::FISHING_ROD.id])
}
}
impl ItemBehaviour for FishingRodItem {
fn normal_use<'a>(
&'a self,
_item: &'a Item,
player: &'a Player,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
let world = player.world();
let bobber_id = player.fishing_bobber.load(Ordering::Relaxed);
if bobber_id == -1 {
// Cast
world
.play_sound(
Sound::EntityFishingBobberThrow,
SoundCategory::Neutral,
&player.position(),
)
.await;
let bobber_entity = Entity::new(
world.clone(),
player.position(),
&EntityType::FISHING_BOBBER,
);
let bobber = FishingBobberEntity::new(bobber_entity, player);
let look_vec = player.living_entity.get_looking_vector();
bobber
.entity
.velocity
.store(look_vec.multiply(1.5, 1.5, 1.5));
player
.fishing_bobber
.store(bobber.entity.entity_id, Ordering::Relaxed);
let bobber_arc: Arc<FishingBobberEntity> = Arc::new(bobber);
world.spawn_entity(bobber_arc).await;
} else {
// Reel in
if let Some(bobber_base) = world.get_entity_by_id(bobber_id) {
if let Some(bobber) =
bobber_base.cast_any().downcast_ref::<FishingBobberEntity>()
{
let _result = bobber.reel_in(player).await;
// TODO: give items
}
bobber_base.get_entity().remove().await;
}
player.fishing_bobber.store(-1, Ordering::Relaxed);
world
.play_sound(
Sound::EntityFishingBobberRetrieve,
SoundCategory::Neutral,
&player.position(),
)
.await;
}
})
}
fn as_any(&self) -> &dyn Any {
self
}
}

View File

@ -11,6 +11,7 @@ pub mod end_crystal;
pub mod ender_eye;
pub mod ender_pearl;
pub mod firework_rocket;
pub mod fishing_rod;
pub mod glowing_ink_sac;
pub mod hoe;
pub mod honeycomb;
@ -31,12 +32,14 @@ pub mod wind_charge;
use crate::item::items::armor_stand::ArmorStandItem;
use crate::item::items::boat::BoatItem;
use crate::item::items::end_crystal::EndCrystalItem;
use crate::item::items::firework_rocket::FireworkRocketItem;
use crate::item::items::map::MapItem;
use crate::item::items::minecart::MinecartItem;
use crate::item::items::name_tag::NameTagItem;
use crate::item::items::spawn_egg::SpawnEggItem;
use crate::item::items::wind_charge::WindChargeItem;
use firework_rocket::FireworkRocketItem;
use fishing_rod::FishingRodItem;
use glowing_ink_sac::GlowingInkSacItem;
use super::registry::ItemRegistry;
use crate::item::items::potions::{LingeringPotionItem, PotionItem, SplashPotionItem};
@ -49,7 +52,6 @@ use dye::DyeItem;
use egg::EggItem;
use ender_eye::EnderEyeItem;
use ender_pearl::EnderPearlItem;
use glowing_ink_sac::GlowingInkSacItem;
use hoe::HoeItem;
use honeycomb::HoneyCombItem;
use ignite::fire_charge::FireChargeItem;
@ -76,6 +78,7 @@ pub fn default_registry() -> Arc<ItemRegistry> {
manager.register(SwordItem);
manager.register(MaceItem);
manager.register(TridentItem);
manager.register(FishingRodItem);
// TODO: Register CrossbowItem with per-shot durability cost.
// TODO: Register BrushItem with per-stroke durability cost.
// TODO: Register CarrotOnAStickItem and WarpedFungusOnAStickItem with boost durability costs.

View File

@ -881,7 +881,7 @@ impl World {
}
let block_entity_elapsed = block_entity_start.elapsed();
//self.level.chunk_loading.lock().unwrap().send_change();
self.level.chunk_loading.lock().unwrap().send_change();
// Tick the End dragon fight (only on THE_END worlds).
if let Some(ref fight_mutex) = self.dragon_fight {