From 4f69e2cb7a38010e9d25124546043cab0458a84a Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 10 May 2026 17:53:13 +0200 Subject: [PATCH] feat: add fishing rod --- pumpkin-nbt/src/serializer.rs | 7 +- pumpkin-world/src/chunk/format/linear.rs | 42 ++- pumpkin-world/src/chunk/format/pump.rs | 53 ++-- pumpkin-world/src/chunk/io/file_manager.rs | 10 +- pumpkin-world/src/chunk/mod.rs | 4 +- pumpkin-world/src/chunk_system/schedule.rs | 4 +- pumpkin/src/entity/living.rs | 33 ++- pumpkin/src/entity/player.rs | 2 + .../src/entity/projectile/fishing_bobber.rs | 255 ++++++++++++++++++ pumpkin/src/entity/projectile/mod.rs | 2 + pumpkin/src/item/items/crossbow.rs | 16 +- pumpkin/src/item/items/fishing_rod.rs | 89 ++++++ pumpkin/src/item/items/mod.rs | 7 +- pumpkin/src/world/mod.rs | 2 +- 14 files changed, 443 insertions(+), 83 deletions(-) create mode 100644 pumpkin/src/entity/projectile/fishing_bobber.rs create mode 100644 pumpkin/src/item/items/fishing_rod.rs diff --git a/pumpkin-nbt/src/serializer.rs b/pumpkin-nbt/src/serializer.rs index dffb6ad55..101f81ab2 100644 --- a/pumpkin-nbt/src/serializer.rs +++ b/pumpkin-nbt/src/serializer.rs @@ -266,7 +266,12 @@ impl ser::Serializer for &mut Serializer { // 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(self, value: &T) -> Result<()> { diff --git a/pumpkin-world/src/chunk/format/linear.rs b/pumpkin-world/src/chunk/format/linear.rs index 3c5ad0354..c892d7198 100644 --- a/pumpkin-world/src/chunk/format/linear.rs +++ b/pumpkin-world/src/chunk/format/linear.rs @@ -52,7 +52,9 @@ impl LinearV2Superblock { fn from_bytes(buf: &[u8]) -> Result { 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 = (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 { 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 { 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 ChunkSerializer for LinearV2File { 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 ChunkSerializer for LinearV2File { 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 ChunkSerializer for LinearV2File { 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 = Vec::with_capacity(bucket_count); for _ in 0..bucket_count { @@ -472,7 +490,9 @@ impl ChunkSerializer for LinearV2File { // 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 ChunkSerializer for LinearV2File { .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); diff --git a/pumpkin-world/src/chunk/format/pump.rs b/pumpkin-world/src/chunk/format/pump.rs index 3aef3b720..b963e32eb 100644 --- a/pumpkin-world/src/chunk/format/pump.rs +++ b/pumpkin-world/src/chunk/format/pump.rs @@ -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 { pub data: PumpData, @@ -20,17 +20,13 @@ pub struct PumpFile { pub struct PumpData { pub x: i32, pub z: i32, - pub chunks: Vec>>, + pub chunks: BTreeMap>, } impl Default for PumpFile { 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 { 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,31 +112,18 @@ 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] { - let bytes = Bytes::copy_from_slice(chunk_bytes); - match D::from_bytes(&bytes, pos) { - Ok(data) => { - let _ = stream.send(LoadedData::Loaded(data)).await; - } - Err(e) => { - let _ = stream.send(LoadedData::Error((pos, e))).await; - } + 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) => { + let _ = stream.send(LoadedData::Loaded(data)).await; + } + Err(e) => { + let _ = stream.send(LoadedData::Error((pos, e))).await; } - } 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; + let _ = stream.send(LoadedData::Missing(pos)).await; } } } diff --git a/pumpkin-world/src/chunk/io/file_manager.rs b/pumpkin-world/src/chunk/io/file_manager.rs index cff3a6789..c1e927efb 100644 --- a/pumpkin-world/src/chunk/io/file_manager.rs +++ b/pumpkin-world/src/chunk/io/file_manager.rs @@ -111,7 +111,7 @@ impl> ChunkSerializerLazyLoader { 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. }; diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index 3512469cf..769401f46 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -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}")] diff --git a/pumpkin-world/src/chunk_system/schedule.rs b/pumpkin-world/src/chunk_system/schedule.rs index e9d8201f3..95e3af032 100644 --- a/pumpkin-world/src/chunk_system/schedule.rs +++ b/pumpkin-world/src/chunk_system/schedule.rs @@ -549,7 +549,9 @@ impl GenerationSchedule { } let sc = Arc::strong_count(&chunk); if sc == 1 { - chunks.push((pos, Chunk::Level(chunk))); + if chunk.is_dirty() { + chunks.push((pos, Chunk::Level(chunk))); + } self.chunk_map.remove(&pos); } else { warn!( diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 1c6ec94cf..43f0e783e 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -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,21 +1968,20 @@ impl EntityBase for LivingEntity { disable_chance = 1.0; } - if rand::random::() < disable_chance { - if let Some(victim_player) = caller.get_player() { - victim_player - .start_cooldown("minecraft:shield".to_string(), 100) - .await; - self.clear_active_hand().await; + if rand::random::() < 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, - 30, - )) - .await; - } + world + .broadcast_packet_all(&CEntityStatus::new( + self.entity.entity_id, + 30, + )) + .await; } } } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 2a88c71fe..057160def 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -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), } } diff --git a/pumpkin/src/entity/projectile/fishing_bobber.rs b/pumpkin/src/entity/projectile/fishing_bobber.rs new file mode 100644 index 000000000..1922ed0ad --- /dev/null +++ b/pumpkin/src/entity/projectile/fishing_bobber.rs @@ -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::().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, _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::().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, + server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.process_tick(caller, server).await; + }) + } +} diff --git a/pumpkin/src/entity/projectile/mod.rs b/pumpkin/src/entity/projectile/mod.rs index 66b2b61de..b1db14a8d 100644 --- a/pumpkin/src/entity/projectile/mod.rs +++ b/pumpkin/src/entity/projectile/mod.rs @@ -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 { diff --git a/pumpkin/src/item/items/crossbow.rs b/pumpkin/src/item/items/crossbow.rs index 080926dca..e532abee2 100644 --- a/pumpkin/src/item/items/crossbow.rs +++ b/pumpkin/src/item/items/crossbow.rs @@ -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::() { - has_multishot = enchantments - .enchantment - .iter() - .any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT); - } + let has_multishot = + stack + .get_data_component::() + .is_some_and(|enchantments| { + enchantments + .enchantment + .iter() + .any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT) + }); let world = player.world(); world diff --git a/pumpkin/src/item/items/fishing_rod.rs b/pumpkin/src/item/items/fishing_rod.rs new file mode 100644 index 000000000..332d2ff8d --- /dev/null +++ b/pumpkin/src/item/items/fishing_rod.rs @@ -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 + 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 = 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::() + { + 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 + } +} diff --git a/pumpkin/src/item/items/mod.rs b/pumpkin/src/item/items/mod.rs index e722e733c..31c44aded 100644 --- a/pumpkin/src/item/items/mod.rs +++ b/pumpkin/src/item/items/mod.rs @@ -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 { 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. diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 547b7e62c..c325cc0ec 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -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 {