From dafc2b2000c92d369c90d467044099ad44bdacf2 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 25 Aug 2026 11:36:51 +0200 Subject: [PATCH] fix: https://github.com/Pumpkin-MC/Pumpkin/issues/3029 --- crates/pumpkin-protocol/src/rcon.rs | 31 +- crates/pumpkin/src/block/blocks/amethyst.rs | 102 +- crates/pumpkin/src/block/blocks/chests.rs | 178 +--- crates/pumpkin/src/block/blocks/ice.rs | 209 ++++ crates/pumpkin/src/block/blocks/leaves.rs | 111 ++ crates/pumpkin/src/block/blocks/mod.rs | 4 + .../pumpkin/src/block/blocks/nether_portal.rs | 112 +- crates/pumpkin/src/block/blocks/nylium.rs | 300 ++++++ .../src/block/blocks/plant/chorus_flower.rs | 184 +++- .../src/block/blocks/plant/chorus_plant.rs | 3 +- .../pumpkin/src/block/blocks/plant/cocoa.rs | 150 +++ .../src/block/blocks/plant/eyeblossom.rs | 189 ++++ .../pumpkin/src/block/blocks/plant/flower.rs | 35 - .../block/blocks/plant/mangrove_propagule.rs | 169 +++ crates/pumpkin/src/block/blocks/plant/mod.rs | 3 + .../src/block/blocks/plant/mushroom_plant.rs | 314 +++++- .../pumpkin/src/block/blocks/plant/sapling.rs | 89 +- .../src/block/blocks/redstone/copper_bulb.rs | 38 +- crates/pumpkin/src/block/blocks/turtle_egg.rs | 157 ++- crates/pumpkin/src/block/blocks/vine.rs | 833 +++++++++++---- .../src/block/blocks/weathering_copper.rs | 983 ++++++++++++++++++ crates/pumpkin/src/block/registry.rs | 28 +- crates/pumpkin/src/item/items/axe.rs | 64 +- crates/pumpkin/src/net/rcon/mod.rs | 99 +- 24 files changed, 3820 insertions(+), 565 deletions(-) create mode 100644 crates/pumpkin/src/block/blocks/ice.rs create mode 100644 crates/pumpkin/src/block/blocks/leaves.rs create mode 100644 crates/pumpkin/src/block/blocks/nylium.rs create mode 100644 crates/pumpkin/src/block/blocks/plant/cocoa.rs create mode 100644 crates/pumpkin/src/block/blocks/plant/eyeblossom.rs create mode 100644 crates/pumpkin/src/block/blocks/plant/mangrove_propagule.rs create mode 100644 crates/pumpkin/src/block/blocks/weathering_copper.rs diff --git a/crates/pumpkin-protocol/src/rcon.rs b/crates/pumpkin-protocol/src/rcon.rs index 0a54e96ba..ce5666380 100644 --- a/crates/pumpkin-protocol/src/rcon.rs +++ b/crates/pumpkin-protocol/src/rcon.rs @@ -5,19 +5,19 @@ use thiserror::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ServerboundPacket { /// Typically, the first packet sent by the client, which is used to authenticate the connection with the server. - Auth = 2, + Auth = 3, /// This packet type represents a command issued by a client to the server. This can be a `ConCommand` such as `/kill ` or `/weather clear`. /// The response will vary depending on the command issued. - ExecCommand = 3, + ExecCommand = 2, } impl ServerboundPacket { #[must_use] - pub const fn from_i32(n: i32) -> Self { + pub const fn from_i32(n: i32) -> Option { match n { - // 3 => Self::Auth, - 2 => Self::ExecCommand, - _ => Self::Auth, + 3 => Some(Self::Auth), + 2 => Some(Self::ExecCommand), + _ => None, } } } @@ -25,8 +25,7 @@ impl ServerboundPacket { #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Server -> Client pub enum ClientboundPacket { - /// This packet is a notification of the connection's current auth status. When the server receives an auth request, it will respond with an empty `SERVERDATA_RESPONSE_VALUE`, - /// followed immediately by a `SERVERDATA_AUTH_RESPONSE` indicating whether authentication succeeded or failed. Note that the status code is returned in the packet id field, so when pairing the response with the original auth request, you may need to look at the packet id of the `SERVERDATA_RESPONSE_VALUE`. + /// This packet is a notification of the connection's current auth status. AuthResponse = 2, /// A `SERVERDATA_RESPONSE` packet is the response to a `SERVERDATA_EXECCOMMAND` request. Output = 0, @@ -52,15 +51,17 @@ impl ClientboundPacket { pub enum PacketError { #[error("Invalid length")] InvalidLength, - #[error("Failed to send packet")] + #[error("Failed to send packet: {0}")] FailedSend(std::io::Error), #[error("Missing packet null terminator")] MissingNullTerminator, - #[error("Invalid packet string body")] + #[error("Invalid packet string body: {0}")] InvalidBody(std::str::Utf8Error), + #[error("Unknown packet type: {0}")] + UnknownPacketType(i32), } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] /// Serverbound packet pub struct Packet { id: i32, @@ -107,6 +108,8 @@ impl Packet { .map_err(|_| PacketError::InvalidLength)?, ); + let ptype = ServerboundPacket::from_i32(ty).ok_or(PacketError::UnknownPacketType(ty))?; + // Calculate body boundaries (starts after len, id, and ty -> 4+4+4 = 12) // Ends 2 bytes before the total length (excluding the two trailing null bytes) let body_start = 12; @@ -122,11 +125,7 @@ impl Packet { .into(); incoming.drain(0..total_packet_len); - Ok(Some(Self { - id, - ptype: ServerboundPacket::from_i32(ty), - body, - })) + Ok(Some(Self { id, ptype, body })) } #[must_use] diff --git a/crates/pumpkin/src/block/blocks/amethyst.rs b/crates/pumpkin/src/block/blocks/amethyst.rs index 7a40090d2..cb2bfab66 100644 --- a/crates/pumpkin/src/block/blocks/amethyst.rs +++ b/crates/pumpkin/src/block/blocks/amethyst.rs @@ -1,17 +1,24 @@ -use crate::block::BlockBehaviour; -use crate::block::BlockFuture; -use crate::block::BlockMetadata; -use crate::block::CanPlaceAtArgs; -use crate::block::GetStateForNeighborUpdateArgs; -use crate::block::OnPlaceArgs; -use crate::block::blocks::abstract_wall_mounting::WallMountedBlock; -use pumpkin_data::Block; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockId; -use pumpkin_data::BlockStateId; -use pumpkin_data::FacingExt; -use pumpkin_data::block_properties::AmethystClusterLikeProperties; -use pumpkin_data::block_properties::BlockProperties; +use pumpkin_data::{ + Block, BlockDirection, BlockId, BlockStateId, FacingExt, + block_properties::{AmethystClusterLikeProperties, BlockProperties}, +}; +use pumpkin_macros::pumpkin_block; +use pumpkin_world::world::BlockFlags; +use rand::RngExt; + +use crate::block::{ + BlockBehaviour, BlockFuture, BlockMetadata, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, + OnPlaceArgs, RandomTickArgs, blocks::abstract_wall_mounting::WallMountedBlock, +}; + +const ALL_DIRECTIONS: [BlockDirection; 6] = [ + BlockDirection::Down, + BlockDirection::Up, + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, +]; pub struct AmethystBlock; @@ -34,7 +41,7 @@ impl BlockBehaviour for AmethystBlock { args.block.default_state.id, args.block, ); - props.facing = args.direction.to_facing().opposite(); + props.facing = args.direction.to_facing(); props.waterlogged = args.replacing.water_source(); props.to_state_id(args.block) }) @@ -60,6 +67,69 @@ impl BlockBehaviour for AmethystBlock { impl WallMountedBlock for AmethystBlock { fn get_direction(&self, state_id: BlockStateId, block: &Block) -> BlockDirection { let props = AmethystClusterLikeProperties::from_state_id(state_id, block); - props.facing.to_block_direction() + props.facing.to_block_direction().opposite() } } + +#[pumpkin_block("minecraft:budding_amethyst")] +pub struct BuddingAmethystBlock; + +impl BlockBehaviour for BuddingAmethystBlock { + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if rand::rng().random_range(0..5) == 0 { + let grow_direction = { + let mut rng = rand::rng(); + ALL_DIRECTIONS[rng.random_range(0..ALL_DIRECTIONS.len())] + }; + let grow_pos = args.position.offset(grow_direction.to_offset()); + let (relative_block, relative_state) = args.world.get_block_and_state(&grow_pos); + let relative_state_id = relative_state.id; + + let next_stage_and_water = + if can_cluster_grow_at_state(relative_block, relative_state_id) { + Some((&Block::SMALL_AMETHYST_BUD, relative_block == &Block::WATER)) + } else if relative_block == &Block::SMALL_AMETHYST_BUD { + let props = AmethystClusterLikeProperties::from_state_id( + relative_state_id, + &Block::SMALL_AMETHYST_BUD, + ); + (props.facing == grow_direction.to_facing()) + .then_some((&Block::MEDIUM_AMETHYST_BUD, props.waterlogged)) + } else if relative_block == &Block::MEDIUM_AMETHYST_BUD { + let props = AmethystClusterLikeProperties::from_state_id( + relative_state_id, + &Block::MEDIUM_AMETHYST_BUD, + ); + (props.facing == grow_direction.to_facing()) + .then_some((&Block::LARGE_AMETHYST_BUD, props.waterlogged)) + } else if relative_block == &Block::LARGE_AMETHYST_BUD { + let props = AmethystClusterLikeProperties::from_state_id( + relative_state_id, + &Block::LARGE_AMETHYST_BUD, + ); + (props.facing == grow_direction.to_facing()) + .then_some((&Block::AMETHYST_CLUSTER, props.waterlogged)) + } else { + None + }; + + if let Some((next_stage, waterlogged)) = next_stage_and_water { + let mut target_props = AmethystClusterLikeProperties::default(next_stage); + target_props.facing = grow_direction.to_facing(); + target_props.waterlogged = waterlogged; + let target_state_id = target_props.to_state_id(next_stage); + args.world + .set_block_state(&grow_pos, target_state_id, BlockFlags::NOTIFY_ALL) + .await; + } + } + }) + } +} + +#[must_use] +pub fn can_cluster_grow_at_state(block: &Block, state_id: BlockStateId) -> bool { + block.default_state.is_air() + || (block == &Block::WATER && state_id == Block::WATER.default_state.id) +} diff --git a/crates/pumpkin/src/block/blocks/chests.rs b/crates/pumpkin/src/block/blocks/chests.rs index 282d8c39b..891ffdf36 100644 --- a/crates/pumpkin/src/block/blocks/chests.rs +++ b/crates/pumpkin/src/block/blocks/chests.rs @@ -317,6 +317,40 @@ impl BlockBehaviour for ChestBlock { #[pumpkin_block_from_tag("minecraft:copper_chests")] pub struct CopperChestBlock; +impl + crate::block::blocks::weathering_copper::ChangeOverTimeBlock< + crate::block::blocks::weathering_copper::WeatherState, + > for CopperChestBlock +{ + fn get_age( + &self, + block: &Block, + ) -> Option { + crate::block::blocks::weathering_copper::get_weather_state(block) + } + + fn get_chance_modifier( + &self, + age: crate::block::blocks::weathering_copper::WeatherState, + ) -> f32 { + crate::block::blocks::weathering_copper::get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + crate::block::blocks::weathering_copper::get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + crate::block::blocks::weathering_copper::get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + crate::block::blocks::weathering_copper::get_first(block) + } +} + +impl crate::block::blocks::weathering_copper::WeatheringCopper for CopperChestBlock {} + impl BlockBehaviour for CopperChestBlock { fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { on_place_chest_impl(&args) }) @@ -363,8 +397,12 @@ impl BlockBehaviour for CopperChestBlock { return; } - // Try to oxidize the copper chest - try_oxidize_copper_chest(args.world, args.position, args.block, chest_props).await; + crate::block::blocks::weathering_copper::change_over_time( + args.world, + args.position, + args.block, + ) + .await; }) } @@ -376,142 +414,6 @@ impl BlockBehaviour for CopperChestBlock { } } -/// Copper oxidation levels with their ordinal values -const COPPER_CHEST_OXIDATION: &[(&Block, &Block, u8)] = &[ - (&Block::COPPER_CHEST, &Block::EXPOSED_COPPER_CHEST, 0), - ( - &Block::EXPOSED_COPPER_CHEST, - &Block::WEATHERED_COPPER_CHEST, - 1, - ), - ( - &Block::WEATHERED_COPPER_CHEST, - &Block::OXIDIZED_COPPER_CHEST, - 2, - ), -]; - -/// Get the oxidation level ordinal for a block (None if not oxidizable copper chest) -fn get_oxidation_level(block: &Block) -> Option { - // Check non-waxed variants - if block == &Block::COPPER_CHEST { - return Some(0); - } - if block == &Block::EXPOSED_COPPER_CHEST { - return Some(1); - } - if block == &Block::WEATHERED_COPPER_CHEST { - return Some(2); - } - if block == &Block::OXIDIZED_COPPER_CHEST { - return Some(3); - } - // Waxed variants don't oxidize - None -} - -/// Try to oxidize a copper chest to its next oxidation level. -/// Uses vanilla's degradation algorithm with neighbor checking. -async fn try_oxidize_copper_chest( - world: &Arc, - position: &BlockPos, - current_block: &Block, - chest_props: ChestLikeProperties, -) { - use rand::RngExt; - - // Base chance per random tick: ~5.69% - const BASE_DEGRADATION_CHANCE: f32 = 0.056_888_89; - - // First roll: only ~5.69% chance to even attempt oxidation - if rand::rng().random::() >= BASE_DEGRADATION_CHANCE { - return; - } - - // Find the next oxidation level - let (next_block, current_level) = match COPPER_CHEST_OXIDATION - .iter() - .find(|(from, _, _)| *from == current_block) - { - Some((_, to, level)) => (*to, *level), - None => return, // Already fully oxidized or waxed - }; - - // Scan neighbors in 4-block Manhattan distance to calculate oxidation chance - let (same_level_count, higher_level_count) = - count_neighbor_oxidation_levels(world, position, current_level); - - // If we found any neighbors at a LOWER level, oxidation is blocked - // (This is handled in count_neighbor_oxidation_levels by returning early) - - // Calculate weighted probability: ((higher + 1) / (higher + same + 1))^2 * multiplier - let ratio = - (higher_level_count + 1) as f32 / (higher_level_count + same_level_count + 1) as f32; - // Multiplier is 0.75 for UNAFFECTED (level 0), 1.0 for others - let multiplier = if current_level == 0 { 0.75 } else { 1.0 }; - let final_chance = ratio * ratio * multiplier; - - if rand::rng().random::() >= final_chance { - return; - } - - // Apply oxidation with same properties - let new_state_id = chest_props.to_state_id(next_block); - world - .set_block_state(position, new_state_id, BlockFlags::NOTIFY_LISTENERS) - .await; -} - -/// Count copper blocks at same and higher oxidation levels within 4-block Manhattan distance. -/// Returns (same, higher) counts, or (0, 0) if a lower-level neighbor was found (blocking oxidation). -fn count_neighbor_oxidation_levels( - world: &Arc, - center: &BlockPos, - current_level: u8, -) -> (i32, i32) { - use std::cmp::Ordering; - - let mut same_level_count = 0i32; - let mut higher_level_count = 0i32; - - // Iterate in a 4-block Manhattan distance (9x9x9 cube checked with distance filter) - for dx in -4i32..=4 { - for dy in -4i32..=4 { - for dz in -4i32..=4 { - let manhattan_dist = dx.abs() + dy.abs() + dz.abs(); - if manhattan_dist > 4 || manhattan_dist == 0 { - continue; - } - - let neighbor_pos = BlockPos(pumpkin_util::math::vector3::Vector3::new( - center.0.x + dx, - center.0.y + dy, - center.0.z + dz, - )); - - let neighbor_block = world.get_block(&neighbor_pos); - - if let Some(neighbor_level) = get_oxidation_level(neighbor_block) { - match neighbor_level.cmp(¤t_level) { - Ordering::Less => { - // Found a neighbor at lower oxidation level - block oxidation entirely - return (0, 0); - } - Ordering::Greater => { - higher_level_count += 1; - } - Ordering::Equal => { - same_level_count += 1; - } - } - } - } - } - } - - (same_level_count, higher_level_count) -} - /// Trapped chests have the same behavior as wooden chests but also emit redstone power based on viewer count. #[pumpkin_block("minecraft:trapped_chest")] pub struct TrappedChestBlock; diff --git a/crates/pumpkin/src/block/blocks/ice.rs b/crates/pumpkin/src/block/blocks/ice.rs new file mode 100644 index 000000000..01aa2247a --- /dev/null +++ b/crates/pumpkin/src/block/blocks/ice.rs @@ -0,0 +1,209 @@ +use std::sync::Arc; + +use pumpkin_data::block_properties::{BlockProperties, NetherWartLikeProperties, blocks_movement}; +use pumpkin_data::dimension::Dimension; +use pumpkin_data::{Block, BlockDirection, BlockId, BlockState, BlockStateId, Enchantment}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::tick::TickPriority; +use pumpkin_world::world::BlockFlags; +use rand::RngExt; + +use crate::block::{ + BlockBehaviour, BlockFuture, BlockMetadata, BrokenArgs, OnNeighborUpdateArgs, + OnScheduledTickArgs, PlacedArgs, RandomTickArgs, +}; +use crate::world::World; + +/// Melts ice at the given position into water (or removes it in ultrawarm dimensions like the Nether). +pub async fn melt(world: &Arc, position: &BlockPos) { + if world.dimension == Dimension::THE_NETHER { + world + .set_block_state(position, BlockStateId::AIR, BlockFlags::NOTIFY_ALL) + .await; + } else { + world + .set_block_state( + position, + Block::WATER.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + world.update_neighbors(position, None).await; + } +} + +/// Returns whether the frosted ice block at `pos` has fewer adjacent frosted ice blocks than `limit`. +pub fn fewer_neighbors_than(world: &World, pos: &BlockPos, limit: usize) -> bool { + let mut count = 0; + for dir in BlockDirection::all() { + let neighbor_pos = pos.offset(dir.to_offset()); + if world.get_block(&neighbor_pos) == &Block::FROSTED_ICE { + count += 1; + if count >= limit { + return false; + } + } + } + true +} + +/// Slightly melts the frosted ice at `pos`. +/// +/// Increments its age if `age < 3`, or completely melts it if `age >= 3`. +/// Returns `true` if it completely melted, or `false` if it just aged. +async fn slightly_melt(world: &Arc, pos: &BlockPos, block: &Block, age: u8) -> bool { + if age < 3 { + let mut new_props = NetherWartLikeProperties::default(block); + new_props.r#age = age + 1; + world + .set_block_state(pos, new_props.to_state_id(block), BlockFlags::NOTIFY_ALL) + .await; + false + } else { + melt(world, pos).await; + true + } +} + +pub struct IceBlock; + +impl BlockMetadata for IceBlock { + fn ids() -> Box<[BlockId]> { + [BlockId::ICE].into() + } +} + +impl BlockBehaviour for IceBlock { + fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let held_item = args.player.inventory().held_item().await; + let has_silk_touch = held_item.get_enchantment_level(&Enchantment::SILK_TOUCH) > 0; + if !has_silk_touch { + if args.world.dimension == Dimension::THE_NETHER { + args.world + .set_block_state(args.position, BlockStateId::AIR, BlockFlags::NOTIFY_ALL) + .await; + return; + } + + let below_pos = args.position.down(); + let (below_block, below_state_id) = args.world.get_block_and_state_id(&below_pos); + let below_state = BlockState::from_id(below_state_id); + if blocks_movement(below_state, below_block.id) + || below_state.is_liquid() + || below_state.is_solid() + { + args.world + .set_block_state( + args.position, + Block::WATER.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let state = args.world.get_block_state(args.position); + let block_light = args.world.get_block_light_level(args.position).unwrap_or(0); + if block_light > (11u8.saturating_sub(state.opacity)) { + melt(args.world, args.position).await; + } + }) + } +} + +pub struct FrostedIceBlock; + +impl BlockMetadata for FrostedIceBlock { + fn ids() -> Box<[BlockId]> { + [BlockId::FROSTED_ICE].into() + } +} + +impl BlockBehaviour for FrostedIceBlock { + fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let delay = rand::rng().random_range(60..=120); + args.world + .schedule_block_tick(args.block, *args.position, delay, TickPriority::Normal); + }) + } + + fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let should_check_melt = rand::rng().random_range(0..3) == 0 + || fewer_neighbors_than(args.world, args.position, 4); + + if should_check_melt { + let state_id = args.world.get_block_state_id(args.position); + let state = args.world.get_block_state(args.position); + let props = NetherWartLikeProperties::from_state_id(state_id, args.block); + let age = props.r#age; + + let brightness = if args.world.dimension == Dimension::THE_END { + args.world.get_block_light_level(args.position).unwrap_or(0) + } else { + args.world.get_max_local_raw_brightness(args.position) + }; + + let threshold = 11u8.saturating_sub(age).saturating_sub(state.opacity); + if brightness > threshold + && slightly_melt(args.world, args.position, args.block, age).await + { + for dir in BlockDirection::all() { + let neighbor_pos = args.position.offset(dir.to_offset()); + let (neighbor_block, neighbor_state_id) = + args.world.get_block_and_state_id(&neighbor_pos); + if neighbor_block == &Block::FROSTED_ICE { + let neighbor_props = NetherWartLikeProperties::from_state_id( + neighbor_state_id, + neighbor_block, + ); + if !slightly_melt( + args.world, + &neighbor_pos, + neighbor_block, + neighbor_props.r#age, + ) + .await + { + let delay = rand::rng().random_range(20..=40); + args.world.schedule_block_tick( + neighbor_block, + neighbor_pos, + delay, + TickPriority::Normal, + ); + } + } + } + return; + } + } + + let delay = rand::rng().random_range(20..=40); + args.world + .schedule_block_tick(args.block, *args.position, delay, TickPriority::Normal); + }) + } + + fn on_neighbor_update<'a>(&'a self, args: OnNeighborUpdateArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if args.source_block == &Block::FROSTED_ICE + && fewer_neighbors_than(args.world, args.position, 2) + { + melt(args.world, args.position).await; + } + }) + } + + fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + IceBlock.broken(args).await; + }) + } +} diff --git a/crates/pumpkin/src/block/blocks/leaves.rs b/crates/pumpkin/src/block/blocks/leaves.rs new file mode 100644 index 000000000..c0323facf --- /dev/null +++ b/crates/pumpkin/src/block/blocks/leaves.rs @@ -0,0 +1,111 @@ +use pumpkin_data::{ + Block, BlockDirection, BlockStateId, + block_properties::{BlockProperties, OakLeavesLikeProperties}, + tag, + tag::Taggable, +}; +use pumpkin_macros::pumpkin_block_from_tag; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::{ + tick::TickPriority, + world::{BlockAccessor, BlockFlags}, +}; + +use crate::block::{ + BlockBehaviour, BlockFuture, GetStateForNeighborUpdateArgs, OnPlaceArgs, OnScheduledTickArgs, + RandomTickArgs, +}; + +pub const DECAY_DISTANCE: u8 = 7; + +#[pumpkin_block_from_tag("minecraft:leaves")] +pub struct LeavesBlock; + +#[must_use] +pub fn get_distance_at(block: &Block, state_id: BlockStateId) -> u8 { + if block.has_tag(&tag::Block::MINECRAFT_PREVENTS_NEARBY_LEAF_DECAY) { + 0 + } else if block.has_tag(&tag::Block::MINECRAFT_LEAVES) { + OakLeavesLikeProperties::from_state_id(state_id, block).distance + } else { + DECAY_DISTANCE + } +} + +#[must_use] +pub fn update_distance( + world: &dyn BlockAccessor, + pos: &BlockPos, + mut props: OakLeavesLikeProperties, +) -> OakLeavesLikeProperties { + let mut min_distance = DECAY_DISTANCE; + for direction in BlockDirection::all() { + let neighbor_pos = pos.offset(direction.to_offset()); + let (neighbor_block, neighbor_state) = world.get_block_and_state(&neighbor_pos); + let dist = get_distance_at(neighbor_block, neighbor_state.id).saturating_add(1); + min_distance = min_distance.min(dist); + if min_distance == 1 { + break; + } + } + props.distance = min_distance.min(DECAY_DISTANCE); + props +} + +impl BlockBehaviour for LeavesBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let mut props = + OakLeavesLikeProperties::from_state_id(args.block.default_state.id, args.block); + props.persistent = true; + props.waterlogged = args.replacing.water_source(); + props = update_distance(args.world, args.position, props); + props.to_state_id(args.block) + }) + } + + fn get_state_for_neighbor_update<'a>( + &'a self, + args: GetStateForNeighborUpdateArgs<'a>, + ) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let neighbor_block = args.world.get_block(args.neighbor_position); + let distance_from_neighbor = + get_distance_at(neighbor_block, args.neighbor_state_id).saturating_add(1); + let current_props = OakLeavesLikeProperties::from_state_id(args.state_id, args.block); + + if distance_from_neighbor != 1 || current_props.distance != distance_from_neighbor { + args.world + .schedule_block_tick(args.block, *args.position, 1, TickPriority::Normal); + } + + args.state_id + }) + } + + fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let state_id = args.world.get_block_state_id(args.position); + let props = OakLeavesLikeProperties::from_state_id(state_id, args.block); + let updated_props = update_distance(&**args.world, args.position, props); + let new_state_id = updated_props.to_state_id(args.block); + if new_state_id != state_id { + args.world + .set_block_state(args.position, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + } + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let state_id = args.world.get_block_state_id(args.position); + let props = OakLeavesLikeProperties::from_state_id(state_id, args.block); + if !props.persistent && props.distance == DECAY_DISTANCE { + args.world + .break_block(args.position, None, BlockFlags::empty()) + .await; + } + }) + } +} diff --git a/crates/pumpkin/src/block/blocks/mod.rs b/crates/pumpkin/src/block/blocks/mod.rs index 936972a2c..4ed73d03f 100644 --- a/crates/pumpkin/src/block/blocks/mod.rs +++ b/crates/pumpkin/src/block/blocks/mod.rs @@ -43,6 +43,7 @@ pub mod iron_bars; pub mod slabs; pub mod stairs; pub mod walls; +pub mod weathering_copper; // Decorative & furnishings pub mod banners; @@ -73,6 +74,7 @@ pub mod carved_pumpkin; pub mod dripstone; pub mod farmland; pub mod hay; +pub mod leaves; pub mod logs; pub mod mangrove_roots; pub mod plant; @@ -87,7 +89,9 @@ pub mod dirt_path; pub mod dragon_egg; pub mod falling; pub mod grass_block; +pub mod ice; pub mod infested; +pub mod nylium; pub mod powder_snow; pub mod rooted_dirt; pub mod snow; diff --git a/crates/pumpkin/src/block/blocks/nether_portal.rs b/crates/pumpkin/src/block/blocks/nether_portal.rs index 440733eb6..199004bdc 100644 --- a/crates/pumpkin/src/block/blocks/nether_portal.rs +++ b/crates/pumpkin/src/block/blocks/nether_portal.rs @@ -1,28 +1,32 @@ -use std::sync::Arc; +use std::sync::{Arc, atomic::Ordering}; -use crate::block::{ - BlockBehaviour, BlockFuture, GetStateForNeighborUpdateArgs, OnEntityCollisionArgs, - OnStateReplacedArgs, +use pumpkin_data::{ + Block, BlockDirection, BlockState, BlockStateId, Rotation, + block_properties::{Axis, BlockProperties, HorizontalAxis, NetherPortalLikeProperties}, + dimension::Dimension, + entity::EntityType, }; -use crate::entity::EntityBase; -use crate::world::World; -use crate::world::portal::nether::NetherPortal; -use pumpkin_data::Block; -use pumpkin_data::BlockStateId; -use pumpkin_data::block_properties::{ - Axis, BlockProperties, HorizontalAxis, NetherPortalLikeProperties, -}; -use pumpkin_data::dimension::Dimension; -use pumpkin_data::entity::EntityType; use pumpkin_macros::pumpkin_block; -use pumpkin_util::GameMode; +use pumpkin_util::{Difficulty, GameMode, math::vector3::Vector3}; +use rand::RngExt; +use uuid::Uuid; + +use crate::{ + block::{ + BlockBehaviour, BlockFuture, GetStateForNeighborUpdateArgs, OnEntityCollisionArgs, + OnStateReplacedArgs, RandomTickArgs, + }, + entity::{EntityBase, r#type::from_type}, + world::{World, portal::nether::NetherPortal}, +}; #[pumpkin_block("minecraft:nether_portal")] pub struct NetherPortalBlock; impl NetherPortalBlock { /// Gets the portal delay time based on entity type and gamemode - fn get_portal_time(world: &Arc, entity: &dyn EntityBase) -> u32 { + #[must_use] + pub fn get_portal_time(world: &Arc, entity: &dyn EntityBase) -> u32 { let entity_type = entity.get_entity().entity_type; let level_info = world.level_info.load(); match entity_type.id { @@ -68,6 +72,62 @@ impl BlockBehaviour for NetherPortalBlock { }) } + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let level_info = args.world.level_info.load(); + let difficulty = level_info.difficulty; + if !level_info.game_rules.spawn_monsters + || !level_info.game_rules.spawn_mobs + || difficulty == Difficulty::Peaceful + || (args.world.dimension != Dimension::OVERWORLD + && args.world.dimension != Dimension::OVERWORLD_CAVES) + { + return; + } + + let difficulty_id = difficulty as u32; + let roll = rand::rng().random_range(0..2000); + if roll >= difficulty_id { + return; + } + + let player_close = args + .world + .get_closest_player(args.position.to_centered_f64(), 128.0) + .is_some(); + if !player_close { + return; + } + + let mut bottom_pos = *args.position; + while args.world.get_block(&bottom_pos) == &Block::NETHER_PORTAL { + bottom_pos = bottom_pos.down(); + } + + if args + .world + .get_block_state(&bottom_pos) + .is_side_solid(BlockDirection::Up) + { + let spawn_pos = Vector3::new( + bottom_pos.0.x as f64 + 0.5, + (bottom_pos.0.y + 1) as f64, + bottom_pos.0.z as f64 + 0.5, + ); + let mob = from_type( + &EntityType::ZOMBIFIED_PIGLIN, + spawn_pos, + args.world, + Uuid::new_v4(), + ); + mob.get_entity() + .portal_cooldown + .store(300, Ordering::Relaxed); + args.world.spawn_entity(mob).await; + } + }) + } + fn on_entity_collision<'a>(&'a self, args: OnEntityCollisionArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { let target_world = @@ -102,4 +162,24 @@ impl BlockBehaviour for NetherPortalBlock { poi_storage.remove(args.position); }) } + + fn rotate( + &self, + block: &Block, + state_id: BlockStateId, + rotation: Rotation, + ) -> &'static BlockState { + match rotation { + Rotation::Clockwise90 | Rotation::CounterClockwise90 => { + let mut props = NetherPortalLikeProperties::from_state_id(state_id, block); + props.axis = match props.axis { + HorizontalAxis::X => HorizontalAxis::Z, + HorizontalAxis::Z => HorizontalAxis::X, + }; + let new_state_id = props.to_state_id(block); + BlockState::from_id(new_state_id) + } + _ => BlockState::from_id(state_id), + } + } } diff --git a/crates/pumpkin/src/block/blocks/nylium.rs b/crates/pumpkin/src/block/blocks/nylium.rs new file mode 100644 index 000000000..2ea1f9210 --- /dev/null +++ b/crates/pumpkin/src/block/blocks/nylium.rs @@ -0,0 +1,300 @@ +use std::sync::Arc; + +use pumpkin_data::tag::Taggable; +use pumpkin_data::{Block, tag}; +use pumpkin_macros::pumpkin_block_from_tag; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::world::BlockFlags; +use rand::RngExt; + +use crate::block::{BlockBehaviour, BlockFuture, BonemealArgs, RandomTickArgs}; +use crate::world::World; + +#[pumpkin_block_from_tag("minecraft:nylium")] +pub struct NyliumBlock; + +impl NyliumBlock { + fn can_be_nylium(world: &World, pos: &BlockPos) -> bool { + let above_pos = pos.up(); + if !world.is_loaded(&above_pos) { + return true; + } + let above_state = world.get_block_state(&above_pos); + above_state.opacity < 15 + } +} + +impl BlockBehaviour for NyliumBlock { + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if !Self::can_be_nylium(args.world, args.position) { + args.world + .set_block_state( + args.position, + Block::NETHERRACK.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } + }) + } + + fn is_valid_bonemeal_target(&self, args: BonemealArgs<'_>) -> bool { + let above = args.position.up(); + args.world.is_in_height_limit(above.0.y) + && args.world.is_loaded(&above) + && args.world.get_block_state(&above).is_air() + } + + fn is_bonemeal_success(&self, _args: BonemealArgs<'_>) -> bool { + true + } + + fn perform_bonemeal<'a>(&'a self, args: BonemealArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let world = args.world; + let block = args.block; + let above_pos = args.position.up(); + + if block == &Block::CRIMSON_NYLIUM { + place_crimson_vegetation(world, &above_pos).await; + } else if block == &Block::WARPED_NYLIUM { + place_warped_vegetation(world, &above_pos).await; + place_nether_sprouts(world, &above_pos).await; + if rand::rng().random_range(0..8) == 0 { + place_twisting_vines(world, &above_pos).await; + } + } + }) + } +} + +async fn place_crimson_vegetation(world: &Arc, origin: &BlockPos) { + for _ in 0..9 { + let dx = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let dy = rand::rng().random_range(0..1) - rand::rng().random_range(0..1); + let dz = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let target_pos = origin.add(dx, dy, dz); + + if !world.is_in_height_limit(target_pos.0.y) + || !world.is_loaded(&target_pos) + || !world.get_block_state(&target_pos).is_air() + { + continue; + } + + let below_pos = target_pos.down(); + if !world.is_loaded(&below_pos) { + continue; + } + let below_block = world.get_block(&below_pos); + if !below_block.has_tag(&tag::Block::MINECRAFT_NYLIUM) { + continue; + } + + let roll = rand::rng().random_range(0..99); + let placed_block = if roll < 87 { + &Block::CRIMSON_ROOTS + } else if roll < 98 { + &Block::CRIMSON_FUNGUS + } else { + &Block::WARPED_FUNGUS + }; + + let state = placed_block.default_state; + if !world.block_registry.can_place_at( + None, + Some(world), + world.as_ref(), + None, + placed_block, + state, + &target_pos, + None, + None, + ) { + continue; + } + + world + .set_block_state(&target_pos, state.id, BlockFlags::NOTIFY_ALL) + .await; + } +} + +async fn place_warped_vegetation(world: &Arc, origin: &BlockPos) { + for _ in 0..9 { + let dx = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let dy = rand::rng().random_range(0..1) - rand::rng().random_range(0..1); + let dz = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let target_pos = origin.add(dx, dy, dz); + + if !world.is_in_height_limit(target_pos.0.y) + || !world.is_loaded(&target_pos) + || !world.get_block_state(&target_pos).is_air() + { + continue; + } + + let below_pos = target_pos.down(); + if !world.is_loaded(&below_pos) { + continue; + } + let below_block = world.get_block(&below_pos); + if !below_block.has_tag(&tag::Block::MINECRAFT_NYLIUM) { + continue; + } + + let roll = rand::rng().random_range(0..100); + let placed_block = if roll < 85 { + &Block::WARPED_ROOTS + } else if roll < 86 { + &Block::CRIMSON_ROOTS + } else if roll < 99 { + &Block::WARPED_FUNGUS + } else { + &Block::CRIMSON_FUNGUS + }; + + let state = placed_block.default_state; + if !world.block_registry.can_place_at( + None, + Some(world), + world.as_ref(), + None, + placed_block, + state, + &target_pos, + None, + None, + ) { + continue; + } + + world + .set_block_state(&target_pos, state.id, BlockFlags::NOTIFY_ALL) + .await; + } +} + +async fn place_nether_sprouts(world: &Arc, origin: &BlockPos) { + for _ in 0..9 { + let dx = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let dy = rand::rng().random_range(0..1) - rand::rng().random_range(0..1); + let dz = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let target_pos = origin.add(dx, dy, dz); + + if !world.is_in_height_limit(target_pos.0.y) + || !world.is_loaded(&target_pos) + || !world.get_block_state(&target_pos).is_air() + { + continue; + } + + let below_pos = target_pos.down(); + if !world.is_loaded(&below_pos) { + continue; + } + let below_block = world.get_block(&below_pos); + if !below_block.has_tag(&tag::Block::MINECRAFT_NYLIUM) { + continue; + } + + let placed_block = &Block::NETHER_SPROUTS; + let state = placed_block.default_state; + if !world.block_registry.can_place_at( + None, + Some(world), + world.as_ref(), + None, + placed_block, + state, + &target_pos, + None, + None, + ) { + continue; + } + + world + .set_block_state(&target_pos, state.id, BlockFlags::NOTIFY_ALL) + .await; + } +} + +async fn place_twisting_vines(world: &Arc, origin: &BlockPos) { + for _ in 0..9 { + let dx = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let dy = rand::rng().random_range(0..1) - rand::rng().random_range(0..1); + let dz = rand::rng().random_range(0..3) - rand::rng().random_range(0..3); + let mut target_pos = origin.add(dx, dy, dz); + + if !find_twisting_vines_target_y(world, &mut target_pos) { + continue; + } + + let mut height = rand::rng().random_range(0..2) + 1; + if rand::rng().random_range(0..6) == 0 { + height *= 2; + } + if rand::rng().random_range(0..10) == 0 { + height = 1; + } + + let mut current_pos = target_pos; + for i in 0..height { + if !world.is_in_height_limit(current_pos.0.y) + || !world.is_loaded(¤t_pos) + || !world.get_block_state(¤t_pos).is_air() + { + break; + } + + let is_top = i == height - 1 + || !world.is_in_height_limit(current_pos.up().0.y) + || !world.is_loaded(¤t_pos.up()) + || !world.get_block_state(¤t_pos.up()).is_air(); + + if is_top { + world + .set_block_state( + ¤t_pos, + Block::TWISTING_VINES.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + break; + } + world + .set_block_state( + ¤t_pos, + Block::TWISTING_VINES_PLANT.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + current_pos = current_pos.up(); + } + } +} + +fn find_twisting_vines_target_y(world: &World, pos: &mut BlockPos) -> bool { + let mut current = *pos; + for _ in 0..1 { + if world.is_loaded(¤t) && world.get_block_state(¤t).is_air() { + let below = current.down(); + if world.is_loaded(&below) { + let block_below = world.get_block(&below); + if block_below == &Block::WARPED_NYLIUM + || block_below == &Block::WARPED_WART_BLOCK + || block_below == &Block::TWISTING_VINES + || block_below == &Block::TWISTING_VINES_PLANT + { + *pos = current; + return true; + } + } + } + current = current.down(); + } + false +} diff --git a/crates/pumpkin/src/block/blocks/plant/chorus_flower.rs b/crates/pumpkin/src/block/blocks/plant/chorus_flower.rs index c751acfed..6f3044422 100644 --- a/crates/pumpkin/src/block/blocks/plant/chorus_flower.rs +++ b/crates/pumpkin/src/block/blocks/plant/chorus_flower.rs @@ -1,18 +1,37 @@ +use std::sync::Arc; + use pumpkin_data::{ Block, BlockDirection, BlockStateId, + block_properties::{BlockProperties, ChorusFlowerLikeProperties}, tag::{self, Taggable}, + world::WorldEvent, }; use pumpkin_macros::pumpkin_block; -use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; use pumpkin_world::{ tick::TickPriority, world::{BlockAccessor, BlockFlags}, }; +use rand::RngExt; -use crate::block::{ - BlockBehaviour, BlockFuture, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnScheduledTickArgs, +use super::chorus_plant; +use crate::{ + block::{ + BlockBehaviour, BlockFuture, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, + OnScheduledTickArgs, RandomTickArgs, + }, + world::World, }; +pub const DEAD_AGE: u8 = 5; + +const HORIZONTAL_DIRECTIONS: [BlockDirection; 4] = [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, +]; + #[pumpkin_block("minecraft:chorus_flower")] pub struct ChorusFlowerBlock; @@ -43,10 +62,157 @@ impl BlockBehaviour for ChorusFlowerBlock { } }) } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let above = args.position.up(); + let max_y = args.world.dimension.min_y + args.world.dimension.height - 1; + if args.world.get_block(&above).default_state.is_air() && above.0.y <= max_y { + let state_id = args.world.get_block_state_id(args.position); + let state_props = ChorusFlowerLikeProperties::from_state_id(state_id, args.block); + let current_age = state_props.age; + if current_age < DEAD_AGE { + let mut grow_upwards = false; + let mut pillar_on_support_block = false; + let below_pos = args.position.down(); + let (below_block, _) = args.world.get_block_and_state(&below_pos); + + if below_block.has_tag(&tag::Block::MINECRAFT_SUPPORTS_CHORUS_FLOWER) { + grow_upwards = true; + } else if below_block == &Block::CHORUS_PLANT { + let mut height = 1; + for _ in 0..4 { + let test_pos = args.position.offset(Vector3::new(0, -(height + 1), 0)); + let (test_block, _) = args.world.get_block_and_state(&test_pos); + if test_block != &Block::CHORUS_PLANT { + if test_block.has_tag(&tag::Block::MINECRAFT_SUPPORTS_CHORUS_FLOWER) + { + pillar_on_support_block = true; + } + break; + } + height += 1; + } + + let max_chance = if pillar_on_support_block { 5 } else { 4 }; + if height < 2 || height <= rand::rng().random_range(0..max_chance) { + grow_upwards = true; + } + } else if below_block.default_state.is_air() { + grow_upwards = true; + } + + let above_2 = args.position.offset(Vector3::new(0, 2, 0)); + if grow_upwards + && all_neighbors_empty(args.world.as_ref(), &above, None) + && args.world.get_block(&above_2).default_state.is_air() + { + let plant_state_id = chorus_plant::get_state_with_connections( + args.world.as_ref(), + &Block::CHORUS_PLANT, + args.position, + ); + args.world + .set_block_state(args.position, plant_state_id, BlockFlags::NOTIFY_ALL) + .await; + place_grown_flower(args.world, &above, current_age).await; + } else if current_age < 4 { + let mut num_branch_attempts = rand::rng().random_range(0..4); + if pillar_on_support_block { + num_branch_attempts += 1; + } + + let mut created_branch = false; + + for _ in 0..num_branch_attempts { + let direction = HORIZONTAL_DIRECTIONS[rand::rng().random_range(0..4)]; + let target = args.position.offset(direction.to_offset()); + let target_below = target.down(); + + if args.world.get_block(&target).default_state.is_air() + && args.world.get_block(&target_below).default_state.is_air() + && all_neighbors_empty( + args.world.as_ref(), + &target, + Some(direction.opposite()), + ) + { + place_grown_flower(args.world, &target, current_age + 1).await; + created_branch = true; + } + } + + if created_branch { + let plant_state_id = chorus_plant::get_state_with_connections( + args.world.as_ref(), + &Block::CHORUS_PLANT, + args.position, + ); + args.world + .set_block_state( + args.position, + plant_state_id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } else { + place_dead_flower(args.world, args.position).await; + } + } else { + place_dead_flower(args.world, args.position).await; + } + } + } + }) + } } -fn can_survive(block_accessor: &dyn BlockAccessor, pos: &BlockPos) -> bool { - let block_below = block_accessor.get_block(&pos.down()); +pub async fn place_grown_flower(world: &Arc, pos: &BlockPos, age: u8) { + let mut props = ChorusFlowerLikeProperties::default(&Block::CHORUS_FLOWER); + props.age = age; + world + .set_block_state( + pos, + props.to_state_id(&Block::CHORUS_FLOWER), + BlockFlags::NOTIFY_ALL, + ) + .await; + world.sync_world_event(WorldEvent::SoundChorusGrow, *pos, 0); +} + +pub async fn place_dead_flower(world: &Arc, pos: &BlockPos) { + let mut props = ChorusFlowerLikeProperties::default(&Block::CHORUS_FLOWER); + props.age = DEAD_AGE; + world + .set_block_state( + pos, + props.to_state_id(&Block::CHORUS_FLOWER), + BlockFlags::NOTIFY_ALL, + ) + .await; + world.sync_world_event(WorldEvent::SoundChorusDeath, *pos, 0); +} + +#[must_use] +pub fn all_neighbors_empty( + world: &dyn BlockAccessor, + pos: &BlockPos, + ignore: Option, +) -> bool { + for direction in HORIZONTAL_DIRECTIONS { + if Some(direction) != ignore { + let neighbor_pos = pos.offset(direction.to_offset()); + if !world.get_block(&neighbor_pos).default_state.is_air() { + return false; + } + } + } + true +} + +#[must_use] +pub fn can_survive(block_accessor: &dyn BlockAccessor, pos: &BlockPos) -> bool { + let (block_below, _) = block_accessor.get_block_and_state(&pos.down()); if block_below == &Block::CHORUS_PLANT || block_below.has_tag(&tag::Block::MINECRAFT_SUPPORTS_CHORUS_FLOWER) @@ -54,21 +220,21 @@ fn can_survive(block_accessor: &dyn BlockAccessor, pos: &BlockPos) -> bool { return true; } - if !block_below.is_air() { + if !block_below.default_state.is_air() { return false; } // Below is air: the flower is the tip of a horizontal branch. - // Exactly one horizontal neighbor must be a chorus plant stem. + // Exactly one horizontal neighbor must be a chorus plant stem, and other horizontal neighbors must be air. let mut plant_count = 0u32; - for dir in BlockDirection::horizontal() { + for dir in HORIZONTAL_DIRECTIONS { let neighbor = block_accessor.get_block(&pos.offset(dir.to_offset())); if neighbor == &Block::CHORUS_PLANT { plant_count += 1; if plant_count > 1 { return false; } - } else if !neighbor.is_air() { + } else if !neighbor.default_state.is_air() { return false; } } diff --git a/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs b/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs index 600ea850d..43b684757 100644 --- a/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs +++ b/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs @@ -76,7 +76,8 @@ impl BlockBehaviour for ChorusPlantBlock { } } -fn get_state_with_connections( +#[must_use] +pub fn get_state_with_connections( block_accessor: &dyn BlockAccessor, block: &Block, pos: &BlockPos, diff --git a/crates/pumpkin/src/block/blocks/plant/cocoa.rs b/crates/pumpkin/src/block/blocks/plant/cocoa.rs new file mode 100644 index 000000000..01fd741cf --- /dev/null +++ b/crates/pumpkin/src/block/blocks/plant/cocoa.rs @@ -0,0 +1,150 @@ +use pumpkin_data::block_properties::{BlockProperties, CocoaLikeProperties, HorizontalFacing}; +use pumpkin_data::tag::Taggable; +use pumpkin_data::{ + Block, BlockState, BlockStateId, FacingExt, HorizontalFacingExt, Mirror, Rotation, tag, +}; +use pumpkin_macros::pumpkin_block; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::world::{BlockAccessor, BlockFlags}; + +use crate::block::{ + BlockBehaviour, BlockFuture, BonemealArgs, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, + OnPlaceArgs, RandomTickArgs, +}; +use crate::entity::EntityBase; + +pub const MAX_AGE: u8 = 2; + +type CocoaProperties = CocoaLikeProperties; + +#[pumpkin_block("minecraft:cocoa")] +pub struct CocoaBlock; + +impl CocoaBlock { + #[must_use] + pub fn can_survive( + world: &dyn BlockAccessor, + pos: &BlockPos, + facing: HorizontalFacing, + ) -> bool { + let support_pos = pos.offset(facing.to_offset()); + let block = world.get_block(&support_pos); + block.has_tag(&tag::Block::MINECRAFT_SUPPORTS_COCOA) + } +} + +impl BlockBehaviour for CocoaBlock { + fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { + let state_id = args.block_accessor.get_block_state_id(args.position); + if state_id != Block::AIR.default_state.id { + let props = CocoaProperties::from_state_id(state_id, args.block); + return Self::can_survive(args.block_accessor, args.position, props.facing); + } + for facing in [ + HorizontalFacing::North, + HorizontalFacing::South, + HorizontalFacing::West, + HorizontalFacing::East, + ] { + if Self::can_survive(args.block_accessor, args.position, facing) { + return true; + } + } + false + } + + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let mut props = CocoaProperties::default(args.block); + props.age = 0; + + let directions = args.player.get_entity().get_entity_facing_order(); + for dir in directions { + if let Some(facing) = dir.to_horizontal_facing() + && Self::can_survive(args.world, args.position, facing) + { + props.facing = facing; + return props.to_state_id(args.block); + } + } + + Block::AIR.default_state.id + }) + } + + fn get_state_for_neighbor_update<'a>( + &'a self, + args: GetStateForNeighborUpdateArgs<'a>, + ) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let props = CocoaProperties::from_state_id(args.state_id, args.block); + if args.direction == props.facing.to_block_direction() + && !Self::can_survive(args.world, args.position, props.facing) + { + return Block::AIR.default_state.id; + } + args.state_id + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if rand::random::().is_multiple_of(5) { + let state_id = args.world.get_block_state_id(args.position); + let mut props = CocoaProperties::from_state_id(state_id, args.block); + if props.age < MAX_AGE { + props.age += 1; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + }) + } + + fn is_valid_bonemeal_target(&self, args: BonemealArgs<'_>) -> bool { + let props = CocoaProperties::from_state_id(args.state_id, args.block); + props.age < MAX_AGE + } + + fn is_bonemeal_success(&self, _args: BonemealArgs<'_>) -> bool { + true + } + + fn perform_bonemeal<'a>(&'a self, args: BonemealArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let mut props = CocoaProperties::from_state_id(args.state_id, args.block); + if props.age < MAX_AGE { + props.age += 1; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + }) + } + + fn rotate( + &self, + block: &Block, + state_id: BlockStateId, + rotation: Rotation, + ) -> &'static BlockState { + let mut props = CocoaProperties::from_state_id(state_id, block); + props.facing = rotation.rotate_horizontal(props.facing); + BlockState::from_id(props.to_state_id(block)) + } + + fn mirror(&self, block: &Block, state_id: BlockStateId, mirror: Mirror) -> &'static BlockState { + let mut props = CocoaProperties::from_state_id(state_id, block); + props.facing = mirror.mirror_horizontal(props.facing); + BlockState::from_id(props.to_state_id(block)) + } +} diff --git a/crates/pumpkin/src/block/blocks/plant/eyeblossom.rs b/crates/pumpkin/src/block/blocks/plant/eyeblossom.rs new file mode 100644 index 000000000..d8c0234bd --- /dev/null +++ b/crates/pumpkin/src/block/blocks/plant/eyeblossom.rs @@ -0,0 +1,189 @@ +use std::sync::Arc; + +use pumpkin_data::{ + Block, BlockId, BlockStateId, + dimension::Dimension, + effect::StatusEffect, + entity::EntityType, + particle::Particle, + sound::{Sound, SoundCategory}, +}; +use pumpkin_util::{ + Difficulty, + math::{position::BlockPos, vector3::Vector3}, +}; +use pumpkin_world::{tick::TickPriority, world::BlockFlags}; +use rand::RngExt; + +use crate::{ + block::{ + BlockBehaviour, BlockFuture, BlockMetadata, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, + OnEntityCollisionArgs, OnScheduledTickArgs, RandomTickArgs, blocks::plant::PlantBlockBase, + }, + world::World, +}; + +const EYEBLOSSOM_XZ_RANGE: i32 = 3; +const EYEBLOSSOM_Y_RANGE: i32 = 2; + +pub struct EyeblossomBlock; + +impl BlockMetadata for EyeblossomBlock { + fn ids() -> Box<[BlockId]> { + Box::new([BlockId::OPEN_EYEBLOSSOM, BlockId::CLOSED_EYEBLOSSOM]) + } +} + +impl BlockBehaviour for EyeblossomBlock { + fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { + ::can_place_at(self, args.block_accessor, args.position) + } + + fn get_state_for_neighbor_update<'a>( + &'a self, + args: GetStateForNeighborUpdateArgs<'a>, + ) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + ::get_state_for_neighbor_update( + self, + args.world, + args.position, + args.state_id, + ) + .await + }) + } + + fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if !::can_place_at(self, args.world.as_ref(), args.position) { + args.world + .break_block(args.position, None, BlockFlags::empty()) + .await; + return; + } + + let was_open = args.block == &Block::OPEN_EYEBLOSSOM; + if try_changing_state(args.world, args.block, args.position).await { + let sound = if was_open { + Sound::BlockEyeblossomClose + } else { + Sound::BlockEyeblossomOpen + }; + args.world.play_sound( + sound, + SoundCategory::Blocks, + &args.position.to_centered_f64(), + ); + } + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let was_open = args.block == &Block::OPEN_EYEBLOSSOM; + if try_changing_state(args.world, args.block, args.position).await { + let sound = if was_open { + Sound::BlockEyeblossomCloseLong + } else { + Sound::BlockEyeblossomOpenLong + }; + args.world.play_sound( + sound, + SoundCategory::Blocks, + &args.position.to_centered_f64(), + ); + } + }) + } + + fn on_entity_collision<'a>(&'a self, args: OnEntityCollisionArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if args.world.level_info.load().difficulty == Difficulty::Peaceful { + return; + } + + if args.entity.get_entity().entity_type == &EntityType::BEE + && let Some(living_entity) = args.entity.get_living_entity() + { + let effect = pumpkin_data::potion::Effect { + effect_type: &StatusEffect::POISON, + duration: 25, + amplifier: 0, + ambient: false, + show_particles: true, + show_icon: true, + blend: true, + }; + living_entity.add_effect(effect).await; + } + }) + } +} + +impl PlantBlockBase for EyeblossomBlock {} + +pub async fn try_changing_state(world: &Arc, current_block: &Block, pos: &BlockPos) -> bool { + let is_open = current_block == &Block::OPEN_EYEBLOSSOM; + let should_be_open = if world.dimension == Dimension::OVERWORLD + || world.dimension == Dimension::OVERWORLD_CAVES + { + world.level_time.lock().await.is_night() + } else { + is_open + }; + + if should_be_open == is_open { + return false; + } + + let new_block = if is_open { + &Block::CLOSED_EYEBLOSSOM + } else { + &Block::OPEN_EYEBLOSSOM + }; + + world + .set_block_state(pos, new_block.default_state.id, BlockFlags::NOTIFY_ALL) + .await; + + world.spawn_particle( + pos.to_centered_f64(), + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 1, + Particle::Trail, + ); + + let mut rng = rand::rng(); + for dx in -EYEBLOSSOM_XZ_RANGE..=EYEBLOSSOM_XZ_RANGE { + for dy in -EYEBLOSSOM_Y_RANGE..=EYEBLOSSOM_Y_RANGE { + for dz in -EYEBLOSSOM_XZ_RANGE..=EYEBLOSSOM_XZ_RANGE { + if dx == 0 && dy == 0 && dz == 0 { + continue; + } + let nearby_pos = pos.offset(Vector3::new(dx, dy, dz)); + let nearby_block = world.get_block(&nearby_pos); + if nearby_block == current_block { + let dist_sqr = (dx * dx + dy * dy + dz * dz) as f64; + let distance = dist_sqr.sqrt(); + let min_delay = (distance * 5.0) as u8; + let max_delay = (distance * 10.0) as u8; + let delay = if min_delay >= max_delay { + min_delay + } else { + rng.random_range(min_delay..=max_delay) + }; + world.schedule_block_tick( + current_block, + nearby_pos, + delay.max(1), + TickPriority::Normal, + ); + } + } + } + } + + true +} diff --git a/crates/pumpkin/src/block/blocks/plant/flower.rs b/crates/pumpkin/src/block/blocks/plant/flower.rs index d47144db7..41882f752 100644 --- a/crates/pumpkin/src/block/blocks/plant/flower.rs +++ b/crates/pumpkin/src/block/blocks/plant/flower.rs @@ -1,14 +1,9 @@ -use pumpkin_data::Block; use pumpkin_data::BlockStateId; -use pumpkin_data::dimension::Dimension; use pumpkin_macros::pumpkin_block_from_tag; -use pumpkin_world::world::BlockFlags; use crate::block::blocks::plant::PlantBlockBase; use crate::block::{BlockBehaviour, BlockFuture, CanPlaceAtArgs, GetStateForNeighborUpdateArgs}; -use crate::block::RandomTickArgs; - #[pumpkin_block_from_tag("minecraft:small_flowers")] pub struct FlowerBlock; @@ -31,36 +26,6 @@ impl BlockBehaviour for FlowerBlock { .await }) } - - fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { - Box::pin(async move { - //TODO add trail particule - if (args.world.dimension.eq(&Dimension::OVERWORLD) - || args.world.dimension.eq(&Dimension::OVERWORLD_CAVES)) - && args.block.eq(&Block::CLOSED_EYEBLOSSOM) - && args.world.level_time.lock().await.time_of_day % 24000 > 14500 - { - args.world - .set_block_state( - args.position, - Block::OPEN_EYEBLOSSOM.default_state.id, - BlockFlags::NOTIFY_ALL, - ) - .await; - } - if args.block.eq(&Block::OPEN_EYEBLOSSOM) - && args.world.level_time.lock().await.time_of_day % 24000 <= 14500 - { - args.world - .set_block_state( - args.position, - Block::CLOSED_EYEBLOSSOM.default_state.id, - BlockFlags::NOTIFY_ALL, - ) - .await; - } - }) - } } impl PlantBlockBase for FlowerBlock {} diff --git a/crates/pumpkin/src/block/blocks/plant/mangrove_propagule.rs b/crates/pumpkin/src/block/blocks/plant/mangrove_propagule.rs new file mode 100644 index 000000000..057c6ef98 --- /dev/null +++ b/crates/pumpkin/src/block/blocks/plant/mangrove_propagule.rs @@ -0,0 +1,169 @@ +use std::sync::Arc; + +use pumpkin_data::{ + Block, BlockStateId, + block_properties::{BlockProperties, MangrovePropaguleLikeProperties}, + tag, + tag::Taggable, +}; +use pumpkin_macros::pumpkin_block; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::world::{BlockAccessor, BlockFlags}; + +use crate::block::{ + BlockBehaviour, BlockFuture, BonemealArgs, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, + OnPlaceArgs, RandomTickArgs, +}; +use crate::world::World; + +pub const MAX_AGE: u8 = 4; + +#[pumpkin_block("mangrove_propagule")] +pub struct MangrovePropaguleBlock; + +impl MangrovePropaguleBlock { + #[must_use] + pub const fn is_hanging(props: &MangrovePropaguleLikeProperties) -> bool { + props.hanging + } + + #[must_use] + pub const fn is_fully_grown(props: &MangrovePropaguleLikeProperties) -> bool { + props.age == MAX_AGE + } + + #[must_use] + pub fn can_survive( + world: &dyn BlockAccessor, + pos: &BlockPos, + props: &MangrovePropaguleLikeProperties, + ) -> bool { + if props.hanging { + let above_pos = pos.up(); + let above_block = world.get_block(&above_pos); + above_block.has_tag(&tag::Block::MINECRAFT_SUPPORTS_HANGING_MANGROVE_PROPAGULE) + } else { + let below_pos = pos.down(); + let below_block = world.get_block(&below_pos); + below_block.has_tag(&tag::Block::MINECRAFT_SUPPORTS_MANGROVE_PROPAGULE) + } + } + + #[must_use] + pub fn create_new_hanging_propagule(age: u8) -> BlockStateId { + let mut props = MangrovePropaguleLikeProperties::default(&Block::MANGROVE_PROPAGULE); + props.hanging = true; + props.age = age.min(MAX_AGE); + props.to_state_id(&Block::MANGROVE_PROPAGULE) + } + + async fn advance_tree( + world: &Arc, + pos: &BlockPos, + block: &Block, + mut props: MangrovePropaguleLikeProperties, + ) { + if props.stage == 0 { + props.stage = 1; + world + .set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_ALL) + .await; + } else { + use crate::plugin::api::events::world::structure_grow::{StructureGrowEvent, TreeType}; + let mut event = StructureGrowEvent::new(*pos, TreeType::Mangrove, false); + if let Some(server) = world.server.upgrade() { + server.plugin_manager.fire(&server, &mut event).await; + } + let _ = event.cancelled; + } + } +} + +impl BlockBehaviour for MangrovePropaguleBlock { + fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { + let state_id = args.block_accessor.get_block_state_id(args.position); + let props = MangrovePropaguleLikeProperties::from_state_id(state_id, args.block); + Self::can_survive(args.block_accessor, args.position, &props) + } + + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let mut props = MangrovePropaguleLikeProperties::from_state_id( + args.block.default_state.id, + args.block, + ); + props.hanging = false; + props.age = MAX_AGE; + props.stage = 0; + props.waterlogged = args.replacing.water_source(); + props.to_state_id(args.block) + }) + } + + fn get_state_for_neighbor_update<'a>( + &'a self, + args: GetStateForNeighborUpdateArgs<'a>, + ) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let props = MangrovePropaguleLikeProperties::from_state_id(args.state_id, args.block); + if !Self::can_survive(args.world, args.position, &props) { + return Block::AIR.default_state.id; + } + args.state_id + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let state_id = args.world.get_block_state_id(args.position); + let mut props = MangrovePropaguleLikeProperties::from_state_id(state_id, args.block); + if !props.hanging { + if rand::random::().is_multiple_of(7) { + Self::advance_tree(args.world, args.position, args.block, props).await; + } + } else if props.age < MAX_AGE { + props.age += 1; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + }) + } + + fn is_valid_bonemeal_target(&self, args: BonemealArgs<'_>) -> bool { + let props = MangrovePropaguleLikeProperties::from_state_id(args.state_id, args.block); + !props.hanging || props.age < MAX_AGE + } + + fn is_bonemeal_success(&self, args: BonemealArgs<'_>) -> bool { + let props = MangrovePropaguleLikeProperties::from_state_id(args.state_id, args.block); + if props.hanging { + props.age < MAX_AGE + } else { + rand::random::() < 0.45 + } + } + + fn perform_bonemeal<'a>(&'a self, args: BonemealArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let mut props = + MangrovePropaguleLikeProperties::from_state_id(args.state_id, args.block); + if props.hanging && props.age < MAX_AGE { + props.age += 1; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } else { + Self::advance_tree(args.world, args.position, args.block, props).await; + } + }) + } +} diff --git a/crates/pumpkin/src/block/blocks/plant/mod.rs b/crates/pumpkin/src/block/blocks/plant/mod.rs index 4da00889f..504b211ac 100644 --- a/crates/pumpkin/src/block/blocks/plant/mod.rs +++ b/crates/pumpkin/src/block/blocks/plant/mod.rs @@ -11,14 +11,17 @@ pub mod cactus; pub mod cactus_flower; pub mod chorus_flower; pub mod chorus_plant; +pub mod cocoa; pub mod crop; pub mod dry_vegetation; +pub mod eyeblossom; pub mod flower; pub mod flowerbed; pub mod fungus; pub mod kelp; pub mod leaf_litter; pub mod lily_pad; +pub mod mangrove_propagule; pub mod mushroom_plant; pub mod nether_sprouts; pub mod roots; diff --git a/crates/pumpkin/src/block/blocks/plant/mushroom_plant.rs b/crates/pumpkin/src/block/blocks/plant/mushroom_plant.rs index 3b3d6a5b1..6a02b6ea5 100644 --- a/crates/pumpkin/src/block/blocks/plant/mushroom_plant.rs +++ b/crates/pumpkin/src/block/blocks/plant/mushroom_plant.rs @@ -1,12 +1,18 @@ +use std::sync::Arc; + +use pumpkin_data::block_properties::{BlockProperties, BrownMushroomBlockLikeProperties}; use pumpkin_data::tag::Taggable; -use pumpkin_data::{BlockId, BlockStateId, tag}; +use pumpkin_data::{Block, BlockId, BlockStateId, tag}; use pumpkin_util::math::position::BlockPos; -use pumpkin_world::world::BlockAccessor; +use pumpkin_world::world::{BlockAccessor, BlockFlags}; +use rand::RngExt; use crate::block::{ - BlockBehaviour, BlockFuture, BlockMetadata, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, - blocks::plant::PlantBlockBase, + BlockBehaviour, BlockFuture, BlockMetadata, BonemealArgs, CanPlaceAtArgs, + GetStateForNeighborUpdateArgs, RandomTickArgs, blocks::plant::PlantBlockBase, }; +use crate::plugin::api::events::world::structure_grow::{StructureGrowEvent, TreeType}; +use crate::world::World; pub struct MushroomPlantBlock; @@ -16,9 +22,210 @@ impl BlockMetadata for MushroomPlantBlock { } } +fn mushroom_tree_height(rng: &mut impl rand::Rng) -> i32 { + let mut height = rng.random_range(0..3) + 4; + if rng.random_range(0..12) == 0 { + height *= 2; + } + height +} + +impl MushroomPlantBlock { + pub fn can_survive( + block_accessor: &dyn BlockAccessor, + world: Option<&World>, + pos: &BlockPos, + ) -> bool { + let below_pos = pos.down(); + let below_block = block_accessor.get_block(&below_pos); + if below_block.has_tag(&tag::Block::MINECRAFT_OVERRIDES_MUSHROOM_LIGHT_REQUIREMENT) { + return true; + } + + let below_state = block_accessor.get_block_state(&below_pos); + let is_solid_render = + below_state.is_solid() && (below_state.is_full_cube() || below_state.is_solid_block()); + if !is_solid_render { + return false; + } + + if let Some(world) = world + && world.get_max_local_raw_brightness(pos) >= 13 + { + return false; + } + + true + } + + pub async fn grow_mushroom( + world: &Arc, + pos: &BlockPos, + block: &Block, + _state_id: BlockStateId, + ) -> bool { + let species = if block == &Block::BROWN_MUSHROOM { + TreeType::BrownMushroom + } else if block == &Block::RED_MUSHROOM { + TreeType::RedMushroom + } else { + TreeType::Custom + }; + + let mut event = StructureGrowEvent::new(*pos, species, true); + if let Some(server) = world.server.upgrade() { + server.plugin_manager.fire(&server, &mut event).await; + } + if event.cancelled { + return false; + } + + let tree_height = mushroom_tree_height(&mut rand::rng()); + + if !world.is_in_height_limit(pos.0.y + tree_height + 1) { + return false; + } + + let foliage_radius = if block == &Block::BROWN_MUSHROOM { + 3 + } else { + 2 + }; + for dy in 0..=tree_height + 1 { + let radius = if dy <= 3 { 0 } else { foliage_radius }; + for dx in -radius..=radius { + for dz in -radius..=radius { + let check_pos = pos.add(dx, dy, dz); + if check_pos == *pos { + continue; + } + if !world.is_loaded(&check_pos) { + return false; + } + let check_state = world.get_block_state(&check_pos); + let check_block = world.get_block(&check_pos); + let can_replace = check_state.is_air() + || check_block.has_tag(&tag::Block::MINECRAFT_LEAVES) + || check_block.has_tag(&tag::Block::MINECRAFT_REPLACEABLE_BY_MUSHROOMS) + || check_state.replaceable(); + if !can_replace { + return false; + } + } + } + } + + world + .set_block_state(pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL) + .await; + + if block == &Block::BROWN_MUSHROOM { + place_huge_brown_mushroom(world, pos, tree_height).await; + } else if block == &Block::RED_MUSHROOM { + place_huge_red_mushroom(world, pos, tree_height).await; + } + + true + } +} + +async fn place_huge_brown_mushroom(world: &Arc, pos: &BlockPos, tree_height: i32) { + let radius = 3; + let cap_y = pos.0.y + tree_height; + for j in -radius..=radius { + for k in -radius..=radius { + let on_x_edge = j == -radius || j == radius; + let on_z_edge = k == -radius || k == radius; + + if on_x_edge && on_z_edge { + continue; + } + + let props = BrownMushroomBlockLikeProperties { + up: true, + down: false, + west: j == -radius || (on_z_edge && j == 1 - radius), + east: j == radius || (on_z_edge && j == radius - 1), + north: k == -radius || (on_x_edge && k == 1 - radius), + south: k == radius || (on_x_edge && k == radius - 1), + }; + let state_id = props.to_state_id(&Block::BROWN_MUSHROOM_BLOCK); + let cap_pos = BlockPos::new(pos.0.x + j, cap_y, pos.0.z + k); + world + .set_block_state(&cap_pos, state_id, BlockFlags::NOTIFY_ALL) + .await; + } + } + + let stem_props = BrownMushroomBlockLikeProperties { + up: false, + down: false, + north: true, + east: true, + south: true, + west: true, + }; + let stem_state = stem_props.to_state_id(&Block::MUSHROOM_STEM); + for i in 0..tree_height { + let stem_pos = BlockPos::new(pos.0.x, pos.0.y + i, pos.0.z); + world + .set_block_state(&stem_pos, stem_state, BlockFlags::NOTIFY_ALL) + .await; + } +} + +async fn place_huge_red_mushroom(world: &Arc, pos: &BlockPos, tree_height: i32) { + let radius = 2; + for i in (tree_height - 3)..=tree_height { + let j = if i < tree_height { radius } else { radius - 1 }; + let k = radius - 2; + + for l in -j..=j { + for m in -j..=j { + let on_x_edge = l == -j || l == j; + let on_z_edge = m == -j || m == j; + + if i < tree_height && on_x_edge && on_z_edge { + continue; + } + + let props = BrownMushroomBlockLikeProperties { + up: i >= tree_height - 1, + down: false, + west: l < -k, + east: l > k, + north: m < -k, + south: m > k, + }; + let state_id = props.to_state_id(&Block::RED_MUSHROOM_BLOCK); + let cap_pos = BlockPos::new(pos.0.x + l, pos.0.y + i, pos.0.z + m); + world + .set_block_state(&cap_pos, state_id, BlockFlags::NOTIFY_ALL) + .await; + } + } + } + + let stem_props = BrownMushroomBlockLikeProperties { + up: false, + down: false, + north: true, + east: true, + south: true, + west: true, + }; + let stem_state = stem_props.to_state_id(&Block::MUSHROOM_STEM); + for i in 0..tree_height { + let stem_pos = BlockPos::new(pos.0.x, pos.0.y + i, pos.0.z); + world + .set_block_state(&stem_pos, stem_state, BlockFlags::NOTIFY_ALL) + .await; + } +} + impl BlockBehaviour for MushroomPlantBlock { fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { - ::can_place_at(self, args.block_accessor, args.position) + Self::can_survive(args.block_accessor, args.world, args.position) } fn get_state_for_neighbor_update<'a>( @@ -26,13 +233,89 @@ impl BlockBehaviour for MushroomPlantBlock { args: GetStateForNeighborUpdateArgs<'a>, ) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { - ::get_state_for_neighbor_update( - self, - args.world, - args.position, - args.state_id, - ) - .await + if !Self::can_survive(args.world, Some(args.world), args.position) { + return Block::AIR.default_state.id; + } + args.state_id + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if rand::rng().random_range(0..25) != 0 { + return; + } + let pos = *args.position; + let world = args.world; + let this_block = args.block; + let state_id = world.get_block_state_id(&pos); + + let mut max = 5; + for dx in -4..=4 { + for dy in -1..=1 { + for dz in -4..=4 { + let check_pos = pos.add(dx, dy, dz); + if world.is_loaded(&check_pos) && world.get_block(&check_pos) == this_block + { + max -= 1; + if max <= 0 { + return; + } + } + } + } + } + + let mut current_pos = pos; + let mut offset = current_pos.add( + rand::rng().random_range(0..3) - 1, + rand::rng().random_range(0..2) - rand::rng().random_range(0..2), + rand::rng().random_range(0..3) - 1, + ); + + for _ in 0..4 { + if world.is_loaded(&offset) + && world.get_block_state(&offset).is_air() + && Self::can_survive(world.as_ref(), Some(world.as_ref()), &offset) + { + current_pos = offset; + } + offset = current_pos.add( + rand::rng().random_range(0..3) - 1, + rand::rng().random_range(0..2) - rand::rng().random_range(0..2), + rand::rng().random_range(0..3) - 1, + ); + } + + if world.is_loaded(&offset) + && world.get_block_state(&offset).is_air() + && Self::can_survive(world.as_ref(), Some(world.as_ref()), &offset) + { + world + .set_block_state(&offset, state_id, BlockFlags::NOTIFY_LISTENERS) + .await; + } + }) + } + + fn is_valid_bonemeal_target(&self, args: BonemealArgs<'_>) -> bool { + let foliage_radius = if args.block == &Block::BROWN_MUSHROOM { + 3 + } else { + 2 + }; + let min_height = 4 + foliage_radius; + args.world + .is_in_height_limit(args.position.0.y + min_height) + } + + fn is_bonemeal_success(&self, _args: BonemealArgs<'_>) -> bool { + rand::rng().random::() < 0.4 + } + + fn perform_bonemeal<'a>(&'a self, args: BonemealArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + Self::grow_mushroom(args.world, args.position, args.block, args.state_id).await; }) } } @@ -40,7 +323,10 @@ impl BlockBehaviour for MushroomPlantBlock { impl PlantBlockBase for MushroomPlantBlock { fn can_plant_on_top(&self, block_accessor: &dyn BlockAccessor, pos: &BlockPos) -> bool { let block = block_accessor.get_block(pos); - block.has_tag(&tag::Block::MINECRAFT_OVERRIDES_MUSHROOM_LIGHT_REQUIREMENT) - // TODO: Check light level and isOpaqueFullCube + if block.has_tag(&tag::Block::MINECRAFT_OVERRIDES_MUSHROOM_LIGHT_REQUIREMENT) { + return true; + } + let state = block_accessor.get_block_state(pos); + state.is_solid() && (state.is_full_cube() || state.is_solid_block()) } } diff --git a/crates/pumpkin/src/block/blocks/plant/sapling.rs b/crates/pumpkin/src/block/blocks/plant/sapling.rs index 2ba52acbc..0ebec3391 100644 --- a/crates/pumpkin/src/block/blocks/plant/sapling.rs +++ b/crates/pumpkin/src/block/blocks/plant/sapling.rs @@ -1,42 +1,66 @@ -use pumpkin_data::BlockStateId; -use pumpkin_data::block_properties::BlockProperties; +use std::sync::Arc; + +use pumpkin_data::{ + Block, BlockStateId, + block_properties::{BlockProperties, OakSaplingLikeProperties}, +}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockFlags; -use std::sync::Arc; use crate::block::blocks::plant::PlantBlockBase; use crate::block::{ - BlockBehaviour, BlockFuture, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, RandomTickArgs, + BlockBehaviour, BlockFuture, BonemealArgs, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, + RandomTickArgs, }; +use crate::plugin::api::events::world::structure_grow::{StructureGrowEvent, TreeType}; use crate::world::World; -type SaplingProperties = pumpkin_data::block_properties::OakSaplingLikeProperties; - #[pumpkin_block_from_tag("minecraft:saplings")] pub struct SaplingBlock; impl SaplingBlock { - async fn generate(&self, world: &Arc, pos: &BlockPos) { - use crate::plugin::api::events::world::structure_grow::{StructureGrowEvent, TreeType}; - let mut event = StructureGrowEvent::new(*pos, TreeType::Oak, false); + #[must_use] + pub fn get_tree_type(block: &Block) -> TreeType { + match block.name { + "oak_sapling" => TreeType::Oak, + "spruce_sapling" => TreeType::Spruce, + "birch_sapling" => TreeType::Birch, + "jungle_sapling" => TreeType::Jungle, + "acacia_sapling" => TreeType::Acacia, + "dark_oak_sapling" | "pale_oak_sapling" => TreeType::DarkOak, + "cherry_sapling" => TreeType::Cherry, + "azalea" | "flowering_azalea" => TreeType::Azalea, + "mangrove_propagule" => TreeType::Mangrove, + _ => TreeType::Custom, + } + } + + pub async fn advance_tree( + world: &Arc, + pos: &BlockPos, + block: &Block, + state_id: BlockStateId, + bone_meal: bool, + ) { + if OakSaplingLikeProperties::handles_block_id(block.id) { + let mut props = OakSaplingLikeProperties::from_state_id(state_id, block); + if props.stage == 0 { + props.stage = 1; + world + .set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_ALL) + .await; + return; + } + } + + let tree_type = Self::get_tree_type(block); + let mut event = StructureGrowEvent::new(*pos, tree_type, bone_meal); if let Some(server) = world.server.upgrade() { server.plugin_manager.fire(&server, &mut event).await; } - if event.cancelled { - return; - } - - let (block, state) = world.get_block_and_state_id(pos); - let mut props = SaplingProperties::from_state_id(state, block); - if props.stage == 0 { - props.stage = 1; - world - .set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_ALL) - .await; - } else { - //TODO generate tree - } + let _ = event.cancelled; + // TODO: Generate tree once tree feature generation in world is hooked up } } @@ -62,7 +86,24 @@ impl BlockBehaviour for SaplingBlock { fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { - self.generate(args.world, args.position).await; + if rand::random::().is_multiple_of(7) { + let state_id = args.world.get_block_state_id(args.position); + Self::advance_tree(args.world, args.position, args.block, state_id, false).await; + } + }) + } + + fn is_valid_bonemeal_target(&self, _args: BonemealArgs<'_>) -> bool { + true + } + + fn is_bonemeal_success(&self, _args: BonemealArgs<'_>) -> bool { + rand::random::() < 0.45 + } + + fn perform_bonemeal<'a>(&'a self, args: BonemealArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + Self::advance_tree(args.world, args.position, args.block, args.state_id, true).await; }) } } diff --git a/crates/pumpkin/src/block/blocks/redstone/copper_bulb.rs b/crates/pumpkin/src/block/blocks/redstone/copper_bulb.rs index e4e243e45..cfba102fc 100644 --- a/crates/pumpkin/src/block/blocks/redstone/copper_bulb.rs +++ b/crates/pumpkin/src/block/blocks/redstone/copper_bulb.rs @@ -1,5 +1,11 @@ use crate::block::blocks::redstone::block_receives_redstone_power; -use crate::block::{BlockBehaviour, BlockFuture, BlockMetadata, OnNeighborUpdateArgs, OnPlaceArgs}; +use crate::block::blocks::weathering_copper::{ + ChangeOverTimeBlock, WeatherState, WeatheringCopper, change_over_time, get_chance_modifier, + get_first, get_next, get_previous, get_weather_state, +}; +use crate::block::{ + BlockBehaviour, BlockFuture, BlockMetadata, OnNeighborUpdateArgs, OnPlaceArgs, RandomTickArgs, +}; use pumpkin_data::BlockId; use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::BlockProperties; @@ -10,6 +16,30 @@ type CopperBulbLikeProperties = pumpkin_data::block_properties::CopperBulbLikePr pub struct CopperBulbBlock; +impl ChangeOverTimeBlock for CopperBulbBlock { + fn get_age(&self, block: &pumpkin_data::Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &pumpkin_data::Block) -> Option<&'static pumpkin_data::Block> { + get_next(block) + } + + fn get_previous(&self, block: &pumpkin_data::Block) -> Option<&'static pumpkin_data::Block> { + get_previous(block) + } + + fn get_first(&self, block: &pumpkin_data::Block) -> Option<&'static pumpkin_data::Block> { + get_first(block) + } +} + +impl WeatheringCopper for CopperBulbBlock {} + impl BlockMetadata for CopperBulbBlock { fn ids() -> Box<[BlockId]> { [ @@ -73,4 +103,10 @@ impl BlockBehaviour for CopperBulbBlock { } }) } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + change_over_time(args.world, args.position, args.block).await; + }) + } } diff --git a/crates/pumpkin/src/block/blocks/turtle_egg.rs b/crates/pumpkin/src/block/blocks/turtle_egg.rs index cdcc87bd9..df86b7708 100644 --- a/crates/pumpkin/src/block/blocks/turtle_egg.rs +++ b/crates/pumpkin/src/block/blocks/turtle_egg.rs @@ -1,25 +1,97 @@ +use std::sync::Arc; + use pumpkin_data::block_properties::{BlockProperties, TurtleEggLikeProperties}; -use pumpkin_data::entity::EntityPose; +use pumpkin_data::entity::{EntityPose, EntityType}; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tag::Taggable; -use pumpkin_data::{BlockDirection, BlockStateId, tag}; +use pumpkin_data::{Block, BlockDirection, BlockStateId, tag}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; use pumpkin_world::tick::TickPriority; use pumpkin_world::world::{BlockAccessor, BlockFlags}; +use uuid::Uuid; use crate::block::{ BlockBehaviour, BlockFuture, BlockIsReplacing, BrokenArgs, CanPlaceAtArgs, CanUpdateAtArgs, - GetStateForNeighborUpdateArgs, OnLandedUponArgs, OnPlaceArgs, OnScheduledTickArgs, - RandomTickArgs, + GetStateForNeighborUpdateArgs, OnEntityStepArgs, OnLandedUponArgs, OnPlaceArgs, + OnScheduledTickArgs, RandomTickArgs, }; use crate::entity::EntityBase; +use crate::entity::r#type::from_type; +use crate::world::World; type TurtleEggProperties = TurtleEggLikeProperties; #[pumpkin_block("minecraft:turtle_egg")] pub struct TurtleEggBlock; +impl TurtleEggBlock { + #[must_use] + pub fn is_sand(world: &dyn BlockAccessor, pos: &BlockPos) -> bool { + world.get_block(pos).has_tag(&tag::Block::MINECRAFT_SAND) + } + + #[must_use] + pub fn on_sand(world: &dyn BlockAccessor, pos: &BlockPos) -> bool { + Self::is_sand(world, &pos.down()) + } + + #[must_use] + pub fn can_destroy_egg(world: &World, entity: &dyn EntityBase) -> bool { + let entity_type = entity.get_entity().entity_type; + if entity_type.id == EntityType::TURTLE.id || entity_type.id == EntityType::BAT.id { + return false; + } + if entity.get_living_entity().is_none() { + return false; + } + if entity_type.id == EntityType::PLAYER.id { + return true; + } + world.level_info.load().game_rules.mob_griefing + } + + pub async fn decrease_eggs( + world: &Arc, + pos: &BlockPos, + state_id: BlockStateId, + block: &Block, + ) { + world.play_sound( + Sound::EntityTurtleEggBreak, + SoundCategory::Blocks, + &pos.to_f64(), + ); + + let props = TurtleEggProperties::from_state_id(state_id, block); + if props.eggs <= 1 { + world.break_block(pos, None, BlockFlags::empty()).await; + } else { + let mut new_props = props; + new_props.eggs -= 1; + world + .set_block_state(pos, new_props.to_state_id(block), BlockFlags::NOTIFY_ALL) + .await; + } + } + + pub async fn destroy_egg( + world: &Arc, + pos: &BlockPos, + state_id: BlockStateId, + block: &Block, + entity: &dyn EntityBase, + randomness: u32, + ) { + if Self::can_destroy_egg(world, entity) + && (randomness <= 1 || rand::random::().is_multiple_of(randomness)) + { + Self::decrease_eggs(world, pos, state_id, block).await; + } + } +} + impl BlockBehaviour for TurtleEggBlock { fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { @@ -72,14 +144,48 @@ impl BlockBehaviour for TurtleEggBlock { }) } + fn on_entity_step<'a>(&'a self, args: OnEntityStepArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if !args.entity.get_entity().is_sneaking() { + Self::destroy_egg( + args.world, + args.position, + args.state.id, + args.block, + args.entity, + 100, + ) + .await; + } + }) + } + + fn on_landed_upon<'a>(&'a self, args: OnLandedUponArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if let Some(living) = args.entity.get_living_entity() { + living + .handle_fall_damage(args.entity, args.fall_distance, 1.0) + .await; + } + + if args.entity.get_entity().entity_type.id != EntityType::ZOMBIE.id { + let entity_pos = args.entity.get_entity().pos.load(); + let pos = BlockPos(Vector3::new( + entity_pos.x.floor() as i32, + entity_pos.y.floor() as i32, + entity_pos.z.floor() as i32, + )); + let (block, state) = args.world.get_block_and_state(&pos); + if block == &Block::TURTLE_EGG { + Self::destroy_egg(args.world, &pos, state.id, block, args.entity, 3).await; + } + } + }) + } + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { - // Turtle eggs can only hatch when placed on sand - if !args - .world - .get_block(&args.position.down()) - .has_tag(&tag::Block::MINECRAFT_SAND) - { + if !Self::on_sand(args.world.as_ref(), args.position) { return; } @@ -111,25 +217,20 @@ impl BlockBehaviour for TurtleEggBlock { SoundCategory::Blocks, &args.position.to_f64(), ); - } - }) - } - fn on_landed_upon<'a>(&'a self, args: OnLandedUponArgs<'a>) -> BlockFuture<'a, ()> { - Box::pin(async move { - if let Some(living) = args.entity.get_living_entity() { - living - .handle_fall_damage(args.entity, args.fall_distance, 1.0) - .await; - } - - // Entity falling onto turtle egg breaks an egg - if args.fall_distance > 0.5 { - args.world.play_sound( - Sound::EntityTurtleEggBreak, - SoundCategory::Blocks, - &args.entity.get_entity().pos.load(), - ); + if args.world.level_info.load().game_rules.spawn_mobs { + for i in 0..props.eggs { + let spawn_pos = Vector3::new( + args.position.0.x as f64 + 0.3 + f64::from(i) * 0.2, + args.position.0.y as f64, + args.position.0.z as f64 + 0.3, + ); + let turtle = + from_type(&EntityType::TURTLE, spawn_pos, args.world, Uuid::new_v4()); + turtle.get_entity().set_age(-24000); + args.world.spawn_entity(turtle).await; + } + } } }) } diff --git a/crates/pumpkin/src/block/blocks/vine.rs b/crates/pumpkin/src/block/blocks/vine.rs index 67e001ed6..af71cf237 100644 --- a/crates/pumpkin/src/block/blocks/vine.rs +++ b/crates/pumpkin/src/block/blocks/vine.rs @@ -1,140 +1,200 @@ -use std::collections::HashSet; - use crate::{ block::{ - BlockBehaviour, BlockFuture, BlockIsReplacing, CanPlaceAtArgs, CanUpdateAtArgs, - GetStateForNeighborUpdateArgs, OnPlaceArgs, UseWithItemArgs, registry::BlockActionResult, + BlockBehaviour, BlockFuture, CanPlaceAtArgs, CanUpdateAtArgs, + GetStateForNeighborUpdateArgs, OnPlaceArgs, RandomTickArgs, UseWithItemArgs, + registry::BlockActionResult, }, entity::{EntityBase, player::Player}, + world::World, }; use pumpkin_data::{ - Block, BlockDirection, BlockStateId, FacingExt, + Block, BlockDirection, BlockState, BlockStateId, FacingExt, block_properties::{BlockProperties, VineLikeProperties}, + block_rotation::{Mirror, Rotation}, + game_rules::{GameRule, GameRuleValue}, item::Item, + tag::{self, Taggable}, }; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::{BlockAccessor, BlockFlags}; +use rand::RngExt; #[pumpkin_block("minecraft:vine")] pub struct VineBlock; -impl BlockBehaviour for VineBlock { - fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { - Box::pin(async move { - if let BlockIsReplacing::Itself(state_id) = args.replacing { - let (Some(direction), _) = get_accurate_direction( - args.world, - args.position, - Some(args.player), - args.direction, - true, - ) else { - return Block::AIR.default_state.id; - }; - let mut props = VineLikeProperties::from_state_id(state_id, args.block); - vine_direction_mapper(direction, &mut props); - return props.to_state_id(args.block); - } - let (Some(direction), _) = get_accurate_direction( - args.world, - args.position, - Some(args.player), - args.direction, - false, - ) else { - return Block::AIR.default_state.id; - }; - let mut props = VineLikeProperties::default(args.block); - vine_direction_mapper(direction, &mut props); - props.to_state_id(args.block) - }) +/// Checks if a neighboring block can support a vine attached to the face towards it. +/// +/// In vanilla, this is `MultifaceBlock.canAttachTo`: face is full/solid or block is leaves. +#[must_use] +pub fn is_acceptable_neighbour( + block: &Block, + block_state: &BlockState, + direction_to_neighbour: BlockDirection, +) -> bool { + block_state.is_side_solid(direction_to_neighbour.opposite()) + || block.has_tag(&tag::Block::MINECRAFT_LEAVES) +} + +/// Checks if a face of the vine at `pos` can be supported by adjacent blocks or hanging from above. +#[must_use] +pub fn can_support_at_face( + world: &dyn BlockAccessor, + pos: &BlockPos, + direction: BlockDirection, +) -> bool { + if direction == BlockDirection::Down { + return false; } - fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { - can_place_vine_at( - args.block_accessor, - args.position, - args.direction, - args.player, - false, - ) + + let relative = pos.offset(direction.to_offset()); + let (neighbour_block, neighbour_state) = world.get_block_and_state(&relative); + if is_acceptable_neighbour(neighbour_block, neighbour_state, direction) { + return true; } - fn get_state_for_neighbor_update<'a>( - &'a self, - args: GetStateForNeighborUpdateArgs<'a>, - ) -> BlockFuture<'a, BlockStateId> { - Box::pin(async move { - let old_props = VineLikeProperties::from_state_id(args.state_id, args.block); - let old_directions = get_vine_block_directions(old_props); - let mut new_directions = old_directions.clone(); - for old_dir in old_directions { - let support_block = args - .world - .get_block(&args.position.offset(old_dir.to_offset())); - if !supports_vine(support_block) - && !is_top_block_full_vine(args.world, args.position) - { - new_directions.remove(&old_dir); - } - } - if new_directions.is_empty() { - return Block::AIR.default_state.id; - } - let mut new_props = VineLikeProperties::default(args.block); - for new_dir in new_directions { - vine_direction_mapper(new_dir, &mut new_props); - } - - new_props.to_state_id(args.block) - }) + if direction == BlockDirection::Up { + return false; } - fn can_update_at(&self, args: CanUpdateAtArgs<'_>) -> bool { - get_accurate_direction( - args.world, - args.position, - Some(args.player), - args.direction, - true, - ) - .0 - .is_some() - } - fn use_with_item<'a>( - &'a self, - args: UseWithItemArgs<'a>, - ) -> BlockFuture<'a, BlockActionResult> { - Box::pin(async move { - let state = args.world.get_block_state(args.position); - let mut props = VineLikeProperties::from_state_id(state.id, args.block); - let item = args.item_stack.item; - - if item.id != Item::VINE.id { - return BlockActionResult::Pass; - } - let (Some(accurate_dir), _) = get_accurate_direction( - args.world.as_ref(), - args.position, - Some(args.player), - BlockDirection::Down, - true, - ) else { - return BlockActionResult::Fail; - }; - vine_direction_mapper(accurate_dir, &mut props); - - args.world - .set_block_state( - args.position, - props.to_state_id(args.block), - BlockFlags::NOTIFY_ALL, - ) - .await; - BlockActionResult::Consume - }) + let (above_block, above_state) = world.get_block_and_state(&pos.up()); + if above_block == &Block::VINE { + let above_props = VineLikeProperties::from_state_id(above_state.id, above_block); + has_face_property(&above_props, direction) + } else { + false } } + +/// Recalculates vine faces based on current support blocks and vines above. +#[must_use] +pub fn get_updated_state( + mut props: VineLikeProperties, + world: &dyn BlockAccessor, + pos: &BlockPos, + _block: &Block, +) -> VineLikeProperties { + let above_pos = pos.up(); + if props.up { + let (above_block, above_state) = world.get_block_and_state(&above_pos); + props.up = is_acceptable_neighbour(above_block, above_state, BlockDirection::Up); + } + + let mut above_props: Option = None; + + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + if has_face_property(&props, direction) { + let mut can_support = can_support_at_face(world, pos, direction); + if !can_support { + let above_p = above_props.get_or_insert_with(|| { + let (above_block, above_state) = world.get_block_and_state(&above_pos); + if above_block == &Block::VINE { + VineLikeProperties::from_state_id(above_state.id, above_block) + } else { + VineLikeProperties { + north: false, + south: false, + east: false, + west: false, + up: false, + } + } + }); + can_support = has_face_property(above_p, direction); + } + set_face_property(&mut props, direction, can_support); + } + } + + props +} + +#[must_use] +pub const fn has_face_property(props: &VineLikeProperties, direction: BlockDirection) -> bool { + match direction { + BlockDirection::Up => props.up, + BlockDirection::North => props.north, + BlockDirection::South => props.south, + BlockDirection::West => props.west, + BlockDirection::East => props.east, + BlockDirection::Down => false, + } +} + +pub const fn set_face_property( + props: &mut VineLikeProperties, + direction: BlockDirection, + value: bool, +) { + match direction { + BlockDirection::Up => props.up = value, + BlockDirection::North => props.north = value, + BlockDirection::South => props.south = value, + BlockDirection::West => props.west = value, + BlockDirection::East => props.east = value, + BlockDirection::Down => {} + } +} + +#[must_use] +pub const fn count_faces(props: &VineLikeProperties) -> usize { + let mut count = 0; + if props.up { + count += 1; + } + if props.north { + count += 1; + } + if props.south { + count += 1; + } + if props.west { + count += 1; + } + if props.east { + count += 1; + } + count +} + +#[must_use] +pub const fn has_horizontal_connection(props: &VineLikeProperties) -> bool { + props.north || props.east || props.south || props.west +} + +/// Checks if vine density in a 9x3x9 volume is below 5 (vanilla `canSpread`). +#[must_use] +pub fn can_spread(world: &World, pos: &BlockPos) -> bool { + let min_x = pos.0.x - 4; + let max_x = pos.0.x + 4; + let min_y = pos.0.y - 1; + let max_y = pos.0.y + 1; + let min_z = pos.0.z - 4; + let max_z = pos.0.z + 4; + + let mut count = 0; + for x in min_x..=max_x { + for y in min_y..=max_y { + for z in min_z..=max_z { + let check_pos = BlockPos::new(x, y, z); + if world.get_block(&check_pos) == &Block::VINE { + count += 1; + if count >= 5 { + return false; + } + } + } + } + } + true +} + +#[must_use] pub fn get_nearest_looking_directions( player: &Player, replace_clicked: bool, @@ -154,144 +214,457 @@ pub fn get_nearest_looking_directions( if !replace_clicked { let target = clicked_face.opposite(); - let mut index = 0; - while index < directions.len() && directions[index] != target { index += 1; } - if index > 0 { + if index > 0 && index < directions.len() { directions.copy_within(0..index, 1); directions[0] = target; } } directions } -fn can_place_vine_at( - block_accessor: &dyn BlockAccessor, - block_pos: &BlockPos, - click_direction_wrapper: Option, - player_wrapper: Option<&Player>, - replacing: bool, -) -> bool { - let Some(click_direction) = click_direction_wrapper else { - return false; - }; - let (direction, _) = get_accurate_direction( - block_accessor, - block_pos, - player_wrapper, - click_direction, - replacing, - ); - let Some(direction) = direction else { - return false; - }; - let support_pos = block_pos.offset(direction.to_offset()); - let (support_block, _support_block_state) = block_accessor.get_block_and_state(&support_pos); - if !supports_vine(support_block) && !is_top_block_full_vine(block_accessor, block_pos) { - return false; - } - true -} -const fn supports_vine(support_block: &Block) -> bool { - if support_block.default_state.is_full_cube() { - return true; - } - false -} -//returns (accurate direction, boolean) -// true if this direction is for hanging vine -// false if it is not -fn get_accurate_direction( - block_accessor: &dyn BlockAccessor, - block_pos: &BlockPos, - player_wrapper: Option<&Player>, - click_direction: BlockDirection, - replacing: bool, -) -> (Option, bool) { - let clicked_block = block_accessor.get_block(&block_pos.offset(click_direction.to_offset())); - if !replacing && clicked_block == &Block::VINE && click_direction != BlockDirection::Up { - return (None, false); - } +impl BlockBehaviour for VineBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let (clicked_block, clicked_state_id) = + args.world.get_block_and_state_id(args.position); + let clicked_is_vine = clicked_block == &Block::VINE; + let mut result = if clicked_is_vine { + VineLikeProperties::from_state_id(clicked_state_id, args.block) + } else { + VineLikeProperties::default(args.block) + }; - if click_direction != BlockDirection::Down && supports_vine(clicked_block) { - return (Some(click_direction), false); - } - let (replacing_block, replacing_block_state) = block_accessor.get_block_and_state(block_pos); - let already_active_directions = if replacing_block == &Block::VINE { - let props = VineLikeProperties::from_state_id(replacing_block_state.id, replacing_block); - get_vine_block_directions(props) - } else { - HashSet::new() - }; - if let Some(player) = player_wrapper { - let mut up = false; - for dir in get_nearest_looking_directions(player, replacing, click_direction) { - if dir != BlockDirection::Down && !already_active_directions.contains(&dir) { - let support_pos = block_pos.offset(dir.to_offset()); - let (support_block, _support_block_state) = - block_accessor.get_block_and_state(&support_pos); - if !supports_vine(support_block) { - //handler for hanging vine - if is_top_block_full_vine(block_accessor, block_pos) { - if dir == BlockDirection::Up { - continue; - } - return (Some(dir), true); + let nearest_directions = + get_nearest_looking_directions(args.player, clicked_is_vine, args.direction); + + for direction in nearest_directions { + if direction != BlockDirection::Down { + let face_occupied = clicked_is_vine && has_face_property(&result, direction); + if !face_occupied && can_support_at_face(args.world, args.position, direction) { + set_face_property(&mut result, direction, true); + return result.to_state_id(args.block); } - continue; - } - if dir == BlockDirection::Up && !replacing { - up = true; - continue; } + } - return (Some(dir), false); + if clicked_is_vine && count_faces(&result) > 0 { + result.to_state_id(args.block) + } else { + Block::AIR.default_state.id + } + }) + } + + fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { + let (clicked_block, clicked_state) = args.block_accessor.get_block_and_state(args.position); + let clicked_is_vine = clicked_block == &Block::VINE; + let result = if clicked_is_vine { + VineLikeProperties::from_state_id(clicked_state.id, args.block) + } else { + VineLikeProperties::default(args.block) + }; + + if clicked_is_vine && count_faces(&result) >= 5 { + return false; + } + + let nearest_directions = args.player.map_or_else( + || { + args.direction.map_or( + [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + BlockDirection::Up, + BlockDirection::Down, + ], + |click_dir| { + [ + click_dir.opposite(), + BlockDirection::Up, + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] + }, + ) + }, + |player| { + let click_dir = args.direction.unwrap_or(BlockDirection::Down); + get_nearest_looking_directions(player, clicked_is_vine, click_dir) + }, + ); + + for direction in nearest_directions { + if direction != BlockDirection::Down { + let face_occupied = clicked_is_vine && has_face_property(&result, direction); + if !face_occupied + && can_support_at_face(args.block_accessor, args.position, direction) + { + return true; + } } } - if up { - return (Some(BlockDirection::Up), false); + + clicked_is_vine && count_faces(&result) > 0 + } + + fn get_state_for_neighbor_update<'a>( + &'a self, + args: GetStateForNeighborUpdateArgs<'a>, + ) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + if args.direction == BlockDirection::Down { + return args.state_id; + } + + let updated_props = get_updated_state( + VineLikeProperties::from_state_id(args.state_id, args.block), + args.world, + args.position, + args.block, + ); + if count_faces(&updated_props) == 0 { + Block::AIR.default_state.id + } else { + updated_props.to_state_id(args.block) + } + }) + } + + fn can_update_at(&self, args: CanUpdateAtArgs<'_>) -> bool { + let (clicked_block, clicked_state) = args.world.get_block_and_state(args.position); + if clicked_block != &Block::VINE { + return false; } + let props = VineLikeProperties::from_state_id(clicked_state.id, clicked_block); + count_faces(&props) < 5 } - (None, false) -} -fn is_top_block_full_vine(block_accessor: &dyn BlockAccessor, block_pos: &BlockPos) -> bool { - let (top_block, top_block_state) = block_accessor.get_block_and_state(&block_pos.up()); - if top_block != &Block::VINE { - return false; + + fn use_with_item<'a>( + &'a self, + args: UseWithItemArgs<'a>, + ) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + if args.item_stack.item.id != Item::VINE.id { + return BlockActionResult::Pass; + } + + let state = args.world.get_block_state(args.position); + let mut props = VineLikeProperties::from_state_id(state.id, args.block); + if count_faces(&props) >= 5 { + return BlockActionResult::Pass; + } + + let nearest_directions = + get_nearest_looking_directions(args.player, true, BlockDirection::Down); + + for direction in nearest_directions { + if direction != BlockDirection::Down { + let face_occupied = has_face_property(&props, direction); + if !face_occupied + && can_support_at_face(&**args.world, args.position, direction) + { + set_face_property(&mut props, direction, true); + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + return BlockActionResult::Consume; + } + } + } + + BlockActionResult::Pass + }) } - let props = VineLikeProperties::from_state_id(top_block_state.id, top_block); - props.up && props.west && props.east && props.north && props.south -} -fn get_vine_block_directions(props: VineLikeProperties) -> HashSet { - let mut set = HashSet::new(); - if props.north { - set.insert(BlockDirection::North); + + #[expect(clippy::too_many_lines)] + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let do_spread = matches!( + args.world + .level_info + .load() + .game_rules + .get(&GameRule::SpreadVines), + GameRuleValue::Bool(true) + ); + if !do_spread { + return; + } + + if rand::rng().random_range(0..4) != 0 { + return; + } + + let test_direction = BlockDirection::all()[rand::rng().random_range(0..6)]; + let above_pos = args.position.up(); + let state_id = args.world.get_block_state_id(args.position); + let state_props = VineLikeProperties::from_state_id(state_id, args.block); + + if test_direction.is_horizontal() && !has_face_property(&state_props, test_direction) { + if can_spread(args.world, args.position) { + let test_pos = args.position.offset(test_direction.to_offset()); + let (edge_block, edge_state) = args.world.get_block_and_state(&test_pos); + if edge_block.default_state.is_air() { + let cw_direction = test_direction.rotate_clockwise(); + let ccw_direction = test_direction.rotate_counter_clockwise(); + let cw_has_connecting_face = has_face_property(&state_props, cw_direction); + let ccw_has_connecting_face = + has_face_property(&state_props, ccw_direction); + let cw_test_pos = test_pos.offset(cw_direction.to_offset()); + let ccw_test_pos = test_pos.offset(ccw_direction.to_offset()); + + let (cw_test_block, cw_test_state) = + args.world.get_block_and_state(&cw_test_pos); + let (ccw_test_block, ccw_test_state) = + args.world.get_block_and_state(&ccw_test_pos); + + if cw_has_connecting_face + && is_acceptable_neighbour(cw_test_block, cw_test_state, cw_direction) + { + let mut new_props = VineLikeProperties::default(args.block); + set_face_property(&mut new_props, cw_direction, true); + args.world + .set_block_state( + &test_pos, + new_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } else if ccw_has_connecting_face + && is_acceptable_neighbour( + ccw_test_block, + ccw_test_state, + ccw_direction, + ) + { + let mut new_props = VineLikeProperties::default(args.block); + set_face_property(&mut new_props, ccw_direction, true); + args.world + .set_block_state( + &test_pos, + new_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } else { + let opposite = test_direction.opposite(); + let (cw_support_block, cw_support_state) = + args.world.get_block_and_state( + &args.position.offset(cw_direction.to_offset()), + ); + let (ccw_support_block, ccw_support_state) = + args.world.get_block_and_state( + &args.position.offset(ccw_direction.to_offset()), + ); + + if cw_has_connecting_face + && cw_test_block.default_state.is_air() + && is_acceptable_neighbour( + cw_support_block, + cw_support_state, + opposite, + ) + { + let mut new_props = VineLikeProperties::default(args.block); + set_face_property(&mut new_props, opposite, true); + args.world + .set_block_state( + &cw_test_pos, + new_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } else if ccw_has_connecting_face + && ccw_test_block.default_state.is_air() + && is_acceptable_neighbour( + ccw_support_block, + ccw_support_state, + opposite, + ) + { + let mut new_props = VineLikeProperties::default(args.block); + set_face_property(&mut new_props, opposite, true); + args.world + .set_block_state( + &ccw_test_pos, + new_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } else if rand::rng().random_range(0.0..1.0f32) < 0.05 + && is_acceptable_neighbour( + args.world.get_block(&test_pos.up()), + args.world.get_block_state(&test_pos.up()), + BlockDirection::Up, + ) + { + let mut new_props = VineLikeProperties::default(args.block); + new_props.up = true; + args.world + .set_block_state( + &test_pos, + new_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + } else if is_acceptable_neighbour(edge_block, edge_state, test_direction) { + let mut new_props = state_props; + set_face_property(&mut new_props, test_direction, true); + args.world + .set_block_state( + args.position, + new_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + } else if test_direction == BlockDirection::Up + && args.position.0.y < args.world.dimension.min_y + args.world.dimension.height - 1 + { + if can_support_at_face(&**args.world, args.position, test_direction) { + let mut new_props = state_props; + new_props.up = true; + args.world + .set_block_state( + args.position, + new_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + return; + } + + if args.world.get_block(&above_pos).default_state.is_air() { + if !can_spread(args.world, args.position) { + return; + } + + let mut above_props = state_props; + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let rel_pos = above_pos.offset(direction.to_offset()); + let (rel_block, rel_state) = args.world.get_block_and_state(&rel_pos); + if rand::rng().random_range(0..2) == 0 + || !is_acceptable_neighbour(rel_block, rel_state, direction) + { + set_face_property(&mut above_props, direction, false); + } + } + + if has_horizontal_connection(&above_props) { + args.world + .set_block_state( + &above_pos, + above_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + } else if args.position.0.y > args.world.dimension.min_y { + let below_pos = args.position.down(); + let (below_block, below_state) = args.world.get_block_and_state(&below_pos); + if below_block.default_state.is_air() || below_block == &Block::VINE { + let before_props = if below_block.default_state.is_air() { + VineLikeProperties::default(args.block) + } else { + VineLikeProperties::from_state_id(below_state.id, below_block) + }; + + let mut after_props = before_props; + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + if rand::rng().random_range(0..2) == 0 + && has_face_property(&state_props, direction) + { + set_face_property(&mut after_props, direction, true); + } + } + + if before_props != after_props && has_horizontal_connection(&after_props) { + args.world + .set_block_state( + &below_pos, + after_props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + } + }) } - if props.south { - set.insert(BlockDirection::South); + + fn rotate( + &self, + block: &Block, + state_id: BlockStateId, + rotation: Rotation, + ) -> &'static BlockState { + let props = VineLikeProperties::from_state_id(state_id, block); + let mut rotated_props = props; + match rotation { + Rotation::Rotate180 => { + rotated_props.north = props.south; + rotated_props.east = props.west; + rotated_props.south = props.north; + rotated_props.west = props.east; + } + Rotation::CounterClockwise90 => { + rotated_props.north = props.east; + rotated_props.east = props.south; + rotated_props.south = props.west; + rotated_props.west = props.north; + } + Rotation::Clockwise90 => { + rotated_props.north = props.west; + rotated_props.east = props.north; + rotated_props.south = props.east; + rotated_props.west = props.south; + } + Rotation::None => {} + } + BlockState::from_id(rotated_props.to_state_id(block)) } - if props.east { - set.insert(BlockDirection::East); - } - if props.west { - set.insert(BlockDirection::West); - } - if props.up { - set.insert(BlockDirection::Up); - } - set -} -const fn vine_direction_mapper(direction: BlockDirection, props: &mut VineLikeProperties) { - match direction { - BlockDirection::Down => (), - BlockDirection::Up => props.up = true, - BlockDirection::North => props.north = true, - BlockDirection::South => props.south = true, - BlockDirection::West => props.west = true, - BlockDirection::East => props.east = true, + + fn mirror(&self, block: &Block, state_id: BlockStateId, mirror: Mirror) -> &'static BlockState { + let props = VineLikeProperties::from_state_id(state_id, block); + let mut mirrored_props = props; + match mirror { + Mirror::LeftRight => { + mirrored_props.north = props.south; + mirrored_props.south = props.north; + } + Mirror::FrontBack => { + mirrored_props.east = props.west; + mirrored_props.west = props.east; + } + Mirror::None => {} + } + BlockState::from_id(mirrored_props.to_state_id(block)) } } diff --git a/crates/pumpkin/src/block/blocks/weathering_copper.rs b/crates/pumpkin/src/block/blocks/weathering_copper.rs new file mode 100644 index 000000000..54da43176 --- /dev/null +++ b/crates/pumpkin/src/block/blocks/weathering_copper.rs @@ -0,0 +1,983 @@ +use std::sync::Arc; + +use pumpkin_data::block_properties::{ + BlockProperties, ChestLikeProperties, ChestType, CopperBulbLikeProperties, + CopperGolemStatueLikeProperties, DoubleBlockHalf, IronChainLikeProperties, + LanternLikeProperties, MangroveRootsLikeProperties, OakDoorLikeProperties, + OakFenceLikeProperties, OakStairsLikeProperties, OakTrapdoorLikeProperties, + ResinBrickSlabLikeProperties, +}; +use pumpkin_data::tag::Taggable; +use pumpkin_data::{Block, BlockId, BlockState, BlockStateId, Mirror, Rotation}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::world::BlockFlags; + +use crate::block::blocks::doors::DoorBlock; +use crate::block::blocks::slabs::SlabBlock; +use crate::block::blocks::stairs::StairBlock; +use crate::block::blocks::trapdoor::TrapDoorBlock; +use crate::block::registry::BlockActionResult; +use crate::block::{ + BlockBehaviour, BlockFuture, BlockMetadata, BrokenArgs, CanPlaceAtArgs, CanUpdateAtArgs, + GetStateForNeighborUpdateArgs, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs, + OnStateReplacedArgs, PlacedArgs, RandomTickArgs, +}; +use crate::world::World; + +/// Base chance per random tick to attempt degradation (~5.69%). +pub const BASE_DEGRADATION_CHANCE: f32 = 0.056_888_89; + +/// Scan distance in Manhattan metric for neighboring copper blocks. +pub const SCAN_DISTANCE: i32 = 4; + +/// Weathering states for oxidizable copper blocks. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WeatherState { + Unaffected = 0, + Exposed = 1, + Weathered = 2, + Oxidized = 3, +} + +impl WeatherState { + #[must_use] + pub const fn ordinal(&self) -> usize { + *self as usize + } + + #[must_use] + pub const fn from_ordinal(ordinal: usize) -> Option { + match ordinal { + 0 => Some(Self::Unaffected), + 1 => Some(Self::Exposed), + 2 => Some(Self::Weathered), + 3 => Some(Self::Oxidized), + _ => None, + } + } + + #[must_use] + pub const fn next(&self) -> Option { + match self { + Self::Unaffected => Some(Self::Exposed), + Self::Exposed => Some(Self::Weathered), + Self::Weathered => Some(Self::Oxidized), + Self::Oxidized => None, + } + } + + #[must_use] + pub const fn previous(&self) -> Option { + match self { + Self::Unaffected => None, + Self::Exposed => Some(Self::Unaffected), + Self::Weathered => Some(Self::Exposed), + Self::Oxidized => Some(Self::Weathered), + } + } + + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::Unaffected => "unaffected", + Self::Exposed => "exposed", + Self::Weathered => "weathered", + Self::Oxidized => "oxidized", + } + } + + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + "unaffected" => Some(Self::Unaffected), + "exposed" => Some(Self::Exposed), + "weathered" => Some(Self::Weathered), + "oxidized" => Some(Self::Oxidized), + _ => None, + } + } +} + +/// Generic interface for blocks that change over time based on random ticks. +pub trait ChangeOverTimeBlock { + const SCAN_DISTANCE: i32 = SCAN_DISTANCE; + const BASE_DEGRADATION_CHANCE: f32 = BASE_DEGRADATION_CHANCE; + + fn get_age(&self, block: &Block) -> Option; + fn get_chance_modifier(&self, age: T) -> f32; + fn get_next(&self, block: &Block) -> Option<&'static Block>; + fn get_previous(&self, block: &Block) -> Option<&'static Block>; + fn get_first(&self, block: &Block) -> Option<&'static Block>; +} + +/// Trait implemented by all weathering copper blocks. +pub trait WeatheringCopper: ChangeOverTimeBlock { + fn get_age(&self, block: &Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + get_first(block) + } +} + +/// All 14 copper families with their 4 weathering stages (Unaffected, Exposed, Weathered, Oxidized). +const COPPER_PROGRESSIONS: &[(&Block, &Block, &Block, &Block)] = &[ + ( + &Block::COPPER_BLOCK, + &Block::EXPOSED_COPPER, + &Block::WEATHERED_COPPER, + &Block::OXIDIZED_COPPER, + ), + ( + &Block::CUT_COPPER, + &Block::EXPOSED_CUT_COPPER, + &Block::WEATHERED_CUT_COPPER, + &Block::OXIDIZED_CUT_COPPER, + ), + ( + &Block::CHISELED_COPPER, + &Block::EXPOSED_CHISELED_COPPER, + &Block::WEATHERED_CHISELED_COPPER, + &Block::OXIDIZED_CHISELED_COPPER, + ), + ( + &Block::CUT_COPPER_SLAB, + &Block::EXPOSED_CUT_COPPER_SLAB, + &Block::WEATHERED_CUT_COPPER_SLAB, + &Block::OXIDIZED_CUT_COPPER_SLAB, + ), + ( + &Block::CUT_COPPER_STAIRS, + &Block::EXPOSED_CUT_COPPER_STAIRS, + &Block::WEATHERED_CUT_COPPER_STAIRS, + &Block::OXIDIZED_CUT_COPPER_STAIRS, + ), + ( + &Block::COPPER_DOOR, + &Block::EXPOSED_COPPER_DOOR, + &Block::WEATHERED_COPPER_DOOR, + &Block::OXIDIZED_COPPER_DOOR, + ), + ( + &Block::COPPER_TRAPDOOR, + &Block::EXPOSED_COPPER_TRAPDOOR, + &Block::WEATHERED_COPPER_TRAPDOOR, + &Block::OXIDIZED_COPPER_TRAPDOOR, + ), + ( + &Block::COPPER_GRATE, + &Block::EXPOSED_COPPER_GRATE, + &Block::WEATHERED_COPPER_GRATE, + &Block::OXIDIZED_COPPER_GRATE, + ), + ( + &Block::COPPER_BULB, + &Block::EXPOSED_COPPER_BULB, + &Block::WEATHERED_COPPER_BULB, + &Block::OXIDIZED_COPPER_BULB, + ), + ( + &Block::COPPER_LANTERN, + &Block::EXPOSED_COPPER_LANTERN, + &Block::WEATHERED_COPPER_LANTERN, + &Block::OXIDIZED_COPPER_LANTERN, + ), + ( + &Block::COPPER_CHEST, + &Block::EXPOSED_COPPER_CHEST, + &Block::WEATHERED_COPPER_CHEST, + &Block::OXIDIZED_COPPER_CHEST, + ), + ( + &Block::COPPER_GOLEM_STATUE, + &Block::EXPOSED_COPPER_GOLEM_STATUE, + &Block::WEATHERED_COPPER_GOLEM_STATUE, + &Block::OXIDIZED_COPPER_GOLEM_STATUE, + ), + ( + &Block::COPPER_BARS, + &Block::EXPOSED_COPPER_BARS, + &Block::WEATHERED_COPPER_BARS, + &Block::OXIDIZED_COPPER_BARS, + ), + ( + &Block::COPPER_CHAIN, + &Block::EXPOSED_COPPER_CHAIN, + &Block::WEATHERED_COPPER_CHAIN, + &Block::OXIDIZED_COPPER_CHAIN, + ), +]; + +/// Returns the next oxidized block in sequence, or `None` if already oxidized or not a weathering copper block. +#[must_use] +pub fn get_next(block: &Block) -> Option<&'static Block> { + for &(unaffected, exposed, weathered, oxidized) in COPPER_PROGRESSIONS { + if block == unaffected { + return Some(exposed); + } + if block == exposed { + return Some(weathered); + } + if block == weathered { + return Some(oxidized); + } + } + None +} + +/// Returns the previous deoxidized block in sequence, or `None` if unaffected or not a weathering copper block. +#[must_use] +pub fn get_previous(block: &Block) -> Option<&'static Block> { + for &(unaffected, exposed, weathered, oxidized) in COPPER_PROGRESSIONS { + if block == oxidized { + return Some(weathered); + } + if block == weathered { + return Some(exposed); + } + if block == exposed { + return Some(unaffected); + } + } + None +} + +/// Returns the first unaffected block in this copper family, or `None` if not a weathering copper block. +#[must_use] +pub fn get_first(block: &Block) -> Option<&'static Block> { + for &(unaffected, exposed, weathered, oxidized) in COPPER_PROGRESSIONS { + if block == unaffected || block == exposed || block == weathered || block == oxidized { + return Some(unaffected); + } + } + None +} + +/// Returns the `WeatherState` of the given block, or `None` if it is not an unwaxed weathering copper block. +#[must_use] +pub fn get_weather_state(block: &Block) -> Option { + for &(unaffected, exposed, weathered, oxidized) in COPPER_PROGRESSIONS { + if block == unaffected { + return Some(WeatherState::Unaffected); + } + if block == exposed { + return Some(WeatherState::Exposed); + } + if block == weathered { + return Some(WeatherState::Weathered); + } + if block == oxidized { + return Some(WeatherState::Oxidized); + } + } + None +} + +/// Returns true if the block can weather further (has a next state). +#[must_use] +pub fn is_weathering(block: &Block) -> bool { + get_next(block).is_some() +} + +/// Returns chance modifier for a given weather state (0.75 for Unaffected, 1.0 otherwise). +#[must_use] +pub const fn get_chance_modifier(state: WeatherState) -> f32 { + match state { + WeatherState::Unaffected => 0.75, + WeatherState::Exposed | WeatherState::Weathered | WeatherState::Oxidized => 1.0, + } +} + +/// Converts a block state ID of `from_block` to the corresponding state ID of `to_block`, preserving all block properties. +#[must_use] +pub fn with_properties_of( + from_block: &Block, + from_state_id: BlockStateId, + to_block: &Block, +) -> BlockStateId { + // 1. Full blocks without properties + if from_block == &Block::COPPER_BLOCK + || from_block == &Block::EXPOSED_COPPER + || from_block == &Block::WEATHERED_COPPER + || from_block == &Block::OXIDIZED_COPPER + || from_block == &Block::CUT_COPPER + || from_block == &Block::EXPOSED_CUT_COPPER + || from_block == &Block::WEATHERED_CUT_COPPER + || from_block == &Block::OXIDIZED_CUT_COPPER + || from_block == &Block::CHISELED_COPPER + || from_block == &Block::EXPOSED_CHISELED_COPPER + || from_block == &Block::WEATHERED_CHISELED_COPPER + || from_block == &Block::OXIDIZED_CHISELED_COPPER + { + return to_block.default_state.id; + } + + // 2. Stairs + if from_block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_STAIRS) { + let props = OakStairsLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 3. Slabs + if from_block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_SLABS) { + let props = ResinBrickSlabLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 4. Doors + if from_block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_DOORS) { + let props = OakDoorLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 5. Trapdoors + if from_block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_TRAPDOORS) { + let props = OakTrapdoorLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 6. Copper Bulbs + if from_block == &Block::COPPER_BULB + || from_block == &Block::EXPOSED_COPPER_BULB + || from_block == &Block::WEATHERED_COPPER_BULB + || from_block == &Block::OXIDIZED_COPPER_BULB + { + let props = CopperBulbLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 7. Copper Grates + if from_block == &Block::COPPER_GRATE + || from_block == &Block::EXPOSED_COPPER_GRATE + || from_block == &Block::WEATHERED_COPPER_GRATE + || from_block == &Block::OXIDIZED_COPPER_GRATE + { + let props = MangroveRootsLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 8. Copper Lanterns + if from_block == &Block::COPPER_LANTERN + || from_block == &Block::EXPOSED_COPPER_LANTERN + || from_block == &Block::WEATHERED_COPPER_LANTERN + || from_block == &Block::OXIDIZED_COPPER_LANTERN + { + let props = LanternLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 9. Copper Chests + if from_block == &Block::COPPER_CHEST + || from_block == &Block::EXPOSED_COPPER_CHEST + || from_block == &Block::WEATHERED_COPPER_CHEST + || from_block == &Block::OXIDIZED_COPPER_CHEST + { + let props = ChestLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 10. Copper Golem Statues + if from_block == &Block::COPPER_GOLEM_STATUE + || from_block == &Block::EXPOSED_COPPER_GOLEM_STATUE + || from_block == &Block::WEATHERED_COPPER_GOLEM_STATUE + || from_block == &Block::OXIDIZED_COPPER_GOLEM_STATUE + { + let props = CopperGolemStatueLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 11. Copper Bars + if from_block == &Block::COPPER_BARS + || from_block == &Block::EXPOSED_COPPER_BARS + || from_block == &Block::WEATHERED_COPPER_BARS + || from_block == &Block::OXIDIZED_COPPER_BARS + { + let props = OakFenceLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // 12. Copper Chains + if from_block == &Block::COPPER_CHAIN + || from_block == &Block::EXPOSED_COPPER_CHAIN + || from_block == &Block::WEATHERED_COPPER_CHAIN + || from_block == &Block::OXIDIZED_COPPER_CHAIN + { + let props = IronChainLikeProperties::from_state_id(from_state_id, from_block); + return props.to_state_id(to_block); + } + + // Fallback: use relative state index + let offset = from_state_id + .as_u16() + .saturating_sub(from_block.default_state.id.as_u16()) as usize; + to_block + .states + .get(offset) + .map_or(to_block.default_state.id, |s| s.id) +} + +/// Returns the next block state for a weathering copper block, preserving all properties. +#[must_use] +pub fn get_next_state(current_block: &Block, state_id: BlockStateId) -> Option { + let next_block = get_next(current_block)?; + Some(with_properties_of(current_block, state_id, next_block)) +} + +/// Returns the previous block state for a weathering copper block, preserving all properties. +#[must_use] +pub fn get_previous_state(current_block: &Block, state_id: BlockStateId) -> Option { + let prev_block = get_previous(current_block)?; + Some(with_properties_of(current_block, state_id, prev_block)) +} + +/// Returns the first (unaffected) block state in this family, preserving all properties. +#[must_use] +pub fn get_first_state(current_block: &Block, state_id: BlockStateId) -> BlockStateId { + get_first(current_block).map_or(state_id, |first_block| { + if first_block == current_block { + state_id + } else { + with_properties_of(current_block, state_id, first_block) + } + }) +} + +/// Scans neighbors within Manhattan distance 4 for other weathering copper blocks. +/// +/// Returns `None` if any neighbor has a lower oxidation level (which completely suppresses oxidation). +/// Returns `Some((same_age_count, higher_age_count))` otherwise. +#[must_use] +pub fn scan_neighbor_oxidation_levels( + world: &World, + center: &BlockPos, + current_age: WeatherState, +) -> Option<(usize, usize)> { + let mut same_age_count = 0; + let mut higher_age_count = 0; + + for dx in -SCAN_DISTANCE..=SCAN_DISTANCE { + for dy in -SCAN_DISTANCE..=SCAN_DISTANCE { + for dz in -SCAN_DISTANCE..=SCAN_DISTANCE { + let manhattan_dist = dx.abs() + dy.abs() + dz.abs(); + if manhattan_dist > SCAN_DISTANCE || manhattan_dist == 0 { + continue; + } + + let neighbor_pos = BlockPos(Vector3::new( + center.0.x + dx, + center.0.y + dy, + center.0.z + dz, + )); + + let neighbor_block = world.get_block(&neighbor_pos); + if let Some(neighbor_age) = get_weather_state(neighbor_block) { + if neighbor_age < current_age { + // Suppressed by a lower-age neighbor nearby + return None; + } + if neighbor_age > current_age { + higher_age_count += 1; + } else { + same_age_count += 1; + } + } + } + } + } + + Some((same_age_count, higher_age_count)) +} + +/// Executes a random tick change-over-time attempt on a weathering copper block using vanilla's probability formula. +pub async fn change_over_time(world: &Arc, position: &BlockPos, block: &Block) { + use rand::RngExt; + + // 1. Roll base degradation chance (~5.69%) + if rand::rng().random::() >= BASE_DEGRADATION_CHANCE { + return; + } + + // 2. Must be an oxidizable block with a next state + let Some(current_age) = get_weather_state(block) else { + return; + }; + let Some(next_block) = get_next(block) else { + return; + }; + + // 3. Scan neighbors within Manhattan distance 4 + let Some((same_age_count, higher_age_count)) = + scan_neighbor_oxidation_levels(world, position, current_age) + else { + return; + }; + + // 4. Calculate chance: ((higher + 1) / (higher + same + 1))^2 * chance_modifier + let ratio = (higher_age_count + 1) as f32 / (higher_age_count + same_age_count + 1) as f32; + let chance = ratio * ratio * get_chance_modifier(current_age); + + if rand::rng().random::() >= chance { + return; + } + + // 5. Apply state change + let current_state_id = world.get_block_state_id(position); + let new_state_id = with_properties_of(block, current_state_id, next_block); + + world + .set_block_state(position, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + + // Special handling for multi-block structures: + // Door: update upper half if present + if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_DOORS) { + let door_props = OakDoorLikeProperties::from_state_id(current_state_id, block); + if door_props.half == DoubleBlockHalf::Lower { + let top_pos = position.up(); + let (top_block, top_state_id) = world.get_block_and_state_id(&top_pos); + if top_block == block { + let top_new_state_id = with_properties_of(top_block, top_state_id, next_block); + world + .set_block_state(&top_pos, top_new_state_id, BlockFlags::NOTIFY_ALL) + .await; + } + } + } + // Chest: update right companion chest if double chest + else if block == &Block::COPPER_CHEST + || block == &Block::EXPOSED_COPPER_CHEST + || block == &Block::WEATHERED_COPPER_CHEST + { + let chest_props = ChestLikeProperties::from_state_id(current_state_id, block); + if chest_props.r#type == ChestType::Left { + let right_dir = chest_props.facing.rotate_clockwise(); + let right_pos = position.offset(right_dir.to_offset()); + let (right_block, right_state_id) = world.get_block_and_state_id(&right_pos); + if right_block == block { + let right_new_state_id = + with_properties_of(right_block, right_state_id, next_block); + world + .set_block_state(&right_pos, right_new_state_id, BlockFlags::NOTIFY_LISTENERS) + .await; + } + } + } +} + +// --------------------------------------------------------------------------- +// Block implementations +// --------------------------------------------------------------------------- + +/// Handles standard full weathering copper blocks, grates, bars, chains, lanterns, and statues. +#[derive(Default)] +pub struct WeatheringCopperBlock; + +impl ChangeOverTimeBlock for WeatheringCopperBlock { + fn get_age(&self, block: &Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + get_first(block) + } +} + +impl WeatheringCopper for WeatheringCopperBlock {} + +impl BlockMetadata for WeatheringCopperBlock { + fn ids() -> Box<[BlockId]> { + [ + BlockId::COPPER_BLOCK, + BlockId::EXPOSED_COPPER, + BlockId::WEATHERED_COPPER, + BlockId::OXIDIZED_COPPER, + BlockId::CUT_COPPER, + BlockId::EXPOSED_CUT_COPPER, + BlockId::WEATHERED_CUT_COPPER, + BlockId::OXIDIZED_CUT_COPPER, + BlockId::CHISELED_COPPER, + BlockId::EXPOSED_CHISELED_COPPER, + BlockId::WEATHERED_CHISELED_COPPER, + BlockId::OXIDIZED_CHISELED_COPPER, + BlockId::COPPER_BARS, + BlockId::EXPOSED_COPPER_BARS, + BlockId::WEATHERED_COPPER_BARS, + BlockId::OXIDIZED_COPPER_BARS, + BlockId::COPPER_CHAIN, + BlockId::EXPOSED_COPPER_CHAIN, + BlockId::WEATHERED_COPPER_CHAIN, + BlockId::OXIDIZED_COPPER_CHAIN, + BlockId::COPPER_LANTERN, + BlockId::EXPOSED_COPPER_LANTERN, + BlockId::WEATHERED_COPPER_LANTERN, + BlockId::OXIDIZED_COPPER_LANTERN, + BlockId::COPPER_GOLEM_STATUE, + BlockId::EXPOSED_COPPER_GOLEM_STATUE, + BlockId::WEATHERED_COPPER_GOLEM_STATUE, + BlockId::OXIDIZED_COPPER_GOLEM_STATUE, + ] + .into() + } +} + +impl BlockBehaviour for WeatheringCopperBlock { + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + change_over_time(args.world, args.position, args.block).await; + }) + } +} + +/// Weathering copper stair blocks. +#[derive(Default)] +pub struct WeatheringCopperStairBlock; + +impl ChangeOverTimeBlock for WeatheringCopperStairBlock { + fn get_age(&self, block: &Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + get_first(block) + } +} + +impl WeatheringCopper for WeatheringCopperStairBlock {} + +impl BlockMetadata for WeatheringCopperStairBlock { + fn ids() -> Box<[BlockId]> { + [ + BlockId::CUT_COPPER_STAIRS, + BlockId::EXPOSED_CUT_COPPER_STAIRS, + BlockId::WEATHERED_CUT_COPPER_STAIRS, + BlockId::OXIDIZED_CUT_COPPER_STAIRS, + ] + .into() + } +} + +impl BlockBehaviour for WeatheringCopperStairBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + StairBlock.on_place(args) + } + + fn on_neighbor_update<'a>(&'a self, args: OnNeighborUpdateArgs<'a>) -> BlockFuture<'a, ()> { + StairBlock.on_neighbor_update(args) + } + + fn rotate( + &self, + block: &Block, + state_id: BlockStateId, + rotation: Rotation, + ) -> &'static BlockState { + StairBlock.rotate(block, state_id, rotation) + } + + fn mirror(&self, block: &Block, state_id: BlockStateId, mirror: Mirror) -> &'static BlockState { + StairBlock.mirror(block, state_id, mirror) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + change_over_time(args.world, args.position, args.block).await; + }) + } +} + +/// Weathering copper trapdoor blocks. +#[derive(Default)] +pub struct WeatheringCopperTrapDoorBlock; + +impl ChangeOverTimeBlock for WeatheringCopperTrapDoorBlock { + fn get_age(&self, block: &Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + get_first(block) + } +} + +impl WeatheringCopper for WeatheringCopperTrapDoorBlock {} + +impl BlockMetadata for WeatheringCopperTrapDoorBlock { + fn ids() -> Box<[BlockId]> { + [ + BlockId::COPPER_TRAPDOOR, + BlockId::EXPOSED_COPPER_TRAPDOOR, + BlockId::WEATHERED_COPPER_TRAPDOOR, + BlockId::OXIDIZED_COPPER_TRAPDOOR, + ] + .into() + } +} + +impl BlockBehaviour for WeatheringCopperTrapDoorBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + TrapDoorBlock.on_place(args) + } + + fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { + TrapDoorBlock.normal_use(args) + } + + fn on_neighbor_update<'a>(&'a self, args: OnNeighborUpdateArgs<'a>) -> BlockFuture<'a, ()> { + TrapDoorBlock.on_neighbor_update(args) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + change_over_time(args.world, args.position, args.block).await; + }) + } +} + +/// Weathering copper slab blocks. +#[derive(Default)] +pub struct WeatheringCopperSlabBlock; + +impl ChangeOverTimeBlock for WeatheringCopperSlabBlock { + fn get_age(&self, block: &Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + get_first(block) + } +} + +impl WeatheringCopper for WeatheringCopperSlabBlock {} + +impl BlockMetadata for WeatheringCopperSlabBlock { + fn ids() -> Box<[BlockId]> { + [ + BlockId::CUT_COPPER_SLAB, + BlockId::EXPOSED_CUT_COPPER_SLAB, + BlockId::WEATHERED_CUT_COPPER_SLAB, + BlockId::OXIDIZED_CUT_COPPER_SLAB, + ] + .into() + } +} + +impl BlockBehaviour for WeatheringCopperSlabBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + SlabBlock.on_place(args) + } + + fn can_update_at(&self, args: CanUpdateAtArgs<'_>) -> bool { + SlabBlock.can_update_at(args) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + change_over_time(args.world, args.position, args.block).await; + }) + } +} + +/// Weathering copper door blocks. +#[derive(Default)] +pub struct WeatheringCopperDoorBlock; + +impl ChangeOverTimeBlock for WeatheringCopperDoorBlock { + fn get_age(&self, block: &Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + get_first(block) + } +} + +impl WeatheringCopper for WeatheringCopperDoorBlock {} + +impl BlockMetadata for WeatheringCopperDoorBlock { + fn ids() -> Box<[BlockId]> { + [ + BlockId::COPPER_DOOR, + BlockId::EXPOSED_COPPER_DOOR, + BlockId::WEATHERED_COPPER_DOOR, + BlockId::OXIDIZED_COPPER_DOOR, + ] + .into() + } +} + +impl BlockBehaviour for WeatheringCopperDoorBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + DoorBlock.on_place(args) + } + + fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { + DoorBlock.normal_use(args) + } + + fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { + DoorBlock.can_place_at(args) + } + + fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { + DoorBlock.placed(args) + } + + fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { + DoorBlock.broken(args) + } + + fn on_neighbor_update<'a>(&'a self, args: OnNeighborUpdateArgs<'a>) -> BlockFuture<'a, ()> { + DoorBlock.on_neighbor_update(args) + } + + fn get_state_for_neighbor_update<'a>( + &'a self, + args: GetStateForNeighborUpdateArgs<'a>, + ) -> BlockFuture<'a, BlockStateId> { + DoorBlock.get_state_for_neighbor_update(args) + } + + fn on_state_replaced<'a>(&'a self, args: OnStateReplacedArgs<'a>) -> BlockFuture<'a, ()> { + DoorBlock.on_state_replaced(args) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let state_id = args.world.get_block_state_id(args.position); + let door_props = OakDoorLikeProperties::from_state_id(state_id, args.block); + if door_props.half == DoubleBlockHalf::Lower { + change_over_time(args.world, args.position, args.block).await; + } + }) + } +} + +/// Weathering copper grate blocks. +#[derive(Default)] +pub struct WeatheringCopperGrateBlock; + +impl ChangeOverTimeBlock for WeatheringCopperGrateBlock { + fn get_age(&self, block: &Block) -> Option { + get_weather_state(block) + } + + fn get_chance_modifier(&self, age: WeatherState) -> f32 { + get_chance_modifier(age) + } + + fn get_next(&self, block: &Block) -> Option<&'static Block> { + get_next(block) + } + + fn get_previous(&self, block: &Block) -> Option<&'static Block> { + get_previous(block) + } + + fn get_first(&self, block: &Block) -> Option<&'static Block> { + get_first(block) + } +} + +impl WeatheringCopper for WeatheringCopperGrateBlock {} + +impl BlockMetadata for WeatheringCopperGrateBlock { + fn ids() -> Box<[BlockId]> { + [ + BlockId::COPPER_GRATE, + BlockId::EXPOSED_COPPER_GRATE, + BlockId::WEATHERED_COPPER_GRATE, + BlockId::OXIDIZED_COPPER_GRATE, + ] + .into() + } +} + +impl BlockBehaviour for WeatheringCopperGrateBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + let mut props = MangroveRootsLikeProperties::default(args.block); + props.waterlogged = args.replacing.water_source(); + props.to_state_id(args.block) + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + change_over_time(args.world, args.position, args.block).await; + }) + } +} diff --git a/crates/pumpkin/src/block/registry.rs b/crates/pumpkin/src/block/registry.rs index 238de05a3..c5ea96173 100644 --- a/crates/pumpkin/src/block/registry.rs +++ b/crates/pumpkin/src/block/registry.rs @@ -1,4 +1,4 @@ -use crate::block::blocks::amethyst::AmethystBlock; +use crate::block::blocks::amethyst::{AmethystBlock, BuddingAmethystBlock}; use crate::block::blocks::anvil::AnvilBlock; use crate::block::blocks::banners::BannerBlock; use crate::block::blocks::barrel::BarrelBlock; @@ -46,15 +46,18 @@ use crate::block::blocks::glazed_terracotta::GlazedTerracottaBlock; use crate::block::blocks::grass_block::GrassBlock; use crate::block::blocks::grindstone::GrindstoneBlock; use crate::block::blocks::hay::HayBlock; +use crate::block::blocks::ice::{FrostedIceBlock, IceBlock}; use crate::block::blocks::infested::InfestedBlock; use crate::block::blocks::iron_bars::IronBarsBlock; use crate::block::blocks::jigsaw::JigsawBlock; +use crate::block::blocks::leaves::LeavesBlock; use crate::block::blocks::logs::LogBlock; use crate::block::blocks::loom::LoomBlock; use crate::block::blocks::magma::MagmaBlock; use crate::block::blocks::mangrove_roots::MangroveRootsBlock; use crate::block::blocks::nether_portal::NetherPortalBlock; use crate::block::blocks::note::NoteBlock; +use crate::block::blocks::nylium::NyliumBlock; use crate::block::blocks::piston::piston::PistonBlock; use crate::block::blocks::piston::piston_extension::PistonExtensionBlock; use crate::block::blocks::piston::piston_head::PistonHeadBlock; @@ -67,6 +70,7 @@ use crate::block::blocks::plant::cactus::CactusBlock; use crate::block::blocks::plant::cactus_flower::CactusFlowerBlock; use crate::block::blocks::plant::chorus_flower::ChorusFlowerBlock; use crate::block::blocks::plant::chorus_plant::ChorusPlantBlock; +use crate::block::blocks::plant::cocoa::CocoaBlock; use crate::block::blocks::plant::crop::beetroot::BeetrootBlock; use crate::block::blocks::plant::crop::carrot::CarrotBlock; use crate::block::blocks::plant::crop::nether_wart::NetherWartBlock; @@ -75,12 +79,14 @@ use crate::block::blocks::plant::crop::sweet_berry_bush::SweetBerryBushBlock; use crate::block::blocks::plant::crop::torch_flower::TorchFlowerBlock; use crate::block::blocks::plant::crop::wheat::WheatBlock; use crate::block::blocks::plant::dry_vegetation::DryVegetationBlock; +use crate::block::blocks::plant::eyeblossom::EyeblossomBlock; use crate::block::blocks::plant::flower::FlowerBlock; use crate::block::blocks::plant::flowerbed::FlowerbedBlock; use crate::block::blocks::plant::fungus::FungusBlock; use crate::block::blocks::plant::kelp::KelpBlock; use crate::block::blocks::plant::leaf_litter::LeafLitterBlock; use crate::block::blocks::plant::lily_pad::LilyPadBlock; +use crate::block::blocks::plant::mangrove_propagule::MangrovePropaguleBlock; use crate::block::blocks::plant::mushroom_plant::MushroomPlantBlock; use crate::block::blocks::plant::nether_sprouts::NetherSproutsBlock; use crate::block::blocks::plant::roots::RootsBlock; @@ -203,6 +209,10 @@ use crate::block::blocks::shulker_box::ShulkerBoxBlock; use crate::block::blocks::skull_block::SkullBlock; use crate::block::blocks::smoker::SmokerBlock; use crate::block::blocks::stonecutter::StonecutterBlock; +use crate::block::blocks::weathering_copper::{ + WeatheringCopperBlock, WeatheringCopperDoorBlock, WeatheringCopperGrateBlock, + WeatheringCopperSlabBlock, WeatheringCopperStairBlock, WeatheringCopperTrapDoorBlock, +}; #[must_use] #[expect(clippy::too_many_lines)] @@ -214,9 +224,11 @@ pub fn default_registry() -> Arc { manager.register(BeaconBlock); manager.register(BedBlock); manager.register(SaplingBlock); + manager.register(MangrovePropaguleBlock); manager.register(CactusBlock); manager.register(ChorusFlowerBlock); manager.register(ChorusPlantBlock); + manager.register(CocoaBlock); manager.register(CarpetBlock); manager.register(CarvedPumpkinBlock); manager.register(WitherSkeletonSkullBlock); @@ -243,10 +255,13 @@ pub fn default_registry() -> Arc { manager.register(GlazedTerracottaBlock); manager.register(HayBlock); manager.register(GrindstoneBlock); + manager.register(IceBlock); + manager.register(FrostedIceBlock); manager.register(IronBarsBlock); manager.register(InfestedBlock); manager.register(JukeboxBlock); manager.register(LogBlock); + manager.register(LeavesBlock); manager.register(BambooBlock); manager.register(BambooSaplingBlock); manager.register(BannerBlock); @@ -275,6 +290,7 @@ pub fn default_registry() -> Arc { manager.register(BrushableBlock); manager.register(BushBlock); manager.register(FlowerBlock); + manager.register(EyeblossomBlock); manager.register(PotatoBlock); manager.register(BeetrootBlock); manager.register(TorchFlowerBlock); @@ -299,6 +315,14 @@ pub fn default_registry() -> Arc { manager.register(SpongeBlock); manager.register(PumpkinBlock); manager.register(WetSpongeBlock); + + // Weathering copper blocks + manager.register(WeatheringCopperBlock); + manager.register(WeatheringCopperDoorBlock); + manager.register(WeatheringCopperGrateBlock); + manager.register(WeatheringCopperSlabBlock); + manager.register(WeatheringCopperStairBlock); + manager.register(WeatheringCopperTrapDoorBlock); manager.register(CommandBlock); manager.register(JigsawBlock); manager.register(ComposterBlock); @@ -344,8 +368,10 @@ pub fn default_registry() -> Arc { manager.register(CoralPlantBlock); manager.register(CoralBlock); manager.register(AmethystBlock); + manager.register(BuddingAmethystBlock); manager.register(GrassBlock); manager.register(RootedDirtBlock); + manager.register(NyliumBlock); manager.register(BubbleColumnBlock); manager.register(FallingBlock); diff --git a/crates/pumpkin/src/item/items/axe.rs b/crates/pumpkin/src/item/items/axe.rs index ece660807..1d92ec263 100644 --- a/crates/pumpkin/src/item/items/axe.rs +++ b/crates/pumpkin/src/item/items/axe.rs @@ -4,7 +4,9 @@ use crate::entity::player::Player; use crate::item::{ItemBehaviour, ItemMetadata}; use crate::server::Server; use pumpkin_data::block_properties::BlockProperties; -use pumpkin_data::block_properties::{OakDoorLikeProperties, PaleOakWoodLikeProperties}; +use pumpkin_data::block_properties::{ + DoubleBlockHalf, OakDoorLikeProperties, PaleOakWoodLikeProperties, +}; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::tag::Taggable; use pumpkin_data::{Block, tag}; @@ -61,23 +63,44 @@ impl ItemBehaviour for AxeItem { } // Let's check if It's a door else if block.has_tag(&tag::Block::MINECRAFT_DOORS) { + // get block state of the old log. // get block state of the old log. let door_information = world.get_block_state_id(&location); - // get the log properties + // get the door properties let door_props = OakDoorLikeProperties::from_state_id(door_information, block); - // create new properties for the new log. - let mut new_door_properties = OakDoorLikeProperties::default(new_block); - // Set old axis to the new log. - new_door_properties.facing = door_props.facing; - new_door_properties.open = door_props.open; - new_door_properties.half = door_props.half; - new_door_properties.hinge = door_props.hinge; - new_door_properties.powered = door_props.powered; - new_door_properties.to_state_id(new_block) + let other_half_pos = match door_props.half { + DoubleBlockHalf::Lower => location.up(), + DoubleBlockHalf::Upper => location.down(), + }; + let (other_block, other_state_id) = + world.get_block_and_state_id(&other_half_pos); + if other_block == block { + let other_new_state_id = + crate::block::blocks::weathering_copper::with_properties_of( + other_block, + other_state_id, + new_block, + ); + world + .set_block_state( + &other_half_pos, + other_new_state_id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } + crate::block::blocks::weathering_copper::with_properties_of( + block, + door_information, + new_block, + ) } else { - new_block.default_state.id + crate::block::blocks::weathering_copper::with_properties_of( + block, + world.get_block_state_id(&location), + new_block, + ) }; - // TODO Implements trapdoors when It's implemented world .set_block_state(&location, new_state_id, BlockFlags::NOTIFY_ALL) .await; @@ -168,6 +191,21 @@ const fn get_deoxidized_equivalent(id: BlockId) -> Option { BlockId::OXIDIZED_COPPER_TRAPDOOR => Some(BlockId::WEATHERED_COPPER_TRAPDOOR), BlockId::WEATHERED_COPPER_TRAPDOOR => Some(BlockId::EXPOSED_COPPER_TRAPDOOR), BlockId::EXPOSED_COPPER_TRAPDOOR => Some(BlockId::COPPER_TRAPDOOR), + BlockId::OXIDIZED_COPPER_LANTERN => Some(BlockId::WEATHERED_COPPER_LANTERN), + BlockId::WEATHERED_COPPER_LANTERN => Some(BlockId::EXPOSED_COPPER_LANTERN), + BlockId::EXPOSED_COPPER_LANTERN => Some(BlockId::COPPER_LANTERN), + BlockId::OXIDIZED_COPPER_CHEST => Some(BlockId::WEATHERED_COPPER_CHEST), + BlockId::WEATHERED_COPPER_CHEST => Some(BlockId::EXPOSED_COPPER_CHEST), + BlockId::EXPOSED_COPPER_CHEST => Some(BlockId::COPPER_CHEST), + BlockId::OXIDIZED_COPPER_GOLEM_STATUE => Some(BlockId::WEATHERED_COPPER_GOLEM_STATUE), + BlockId::WEATHERED_COPPER_GOLEM_STATUE => Some(BlockId::EXPOSED_COPPER_GOLEM_STATUE), + BlockId::EXPOSED_COPPER_GOLEM_STATUE => Some(BlockId::COPPER_GOLEM_STATUE), + BlockId::OXIDIZED_COPPER_BARS => Some(BlockId::WEATHERED_COPPER_BARS), + BlockId::WEATHERED_COPPER_BARS => Some(BlockId::EXPOSED_COPPER_BARS), + BlockId::EXPOSED_COPPER_BARS => Some(BlockId::COPPER_BARS), + BlockId::OXIDIZED_COPPER_CHAIN => Some(BlockId::WEATHERED_COPPER_CHAIN), + BlockId::WEATHERED_COPPER_CHAIN => Some(BlockId::EXPOSED_COPPER_CHAIN), + BlockId::EXPOSED_COPPER_CHAIN => Some(BlockId::COPPER_CHAIN), _ => None, } } diff --git a/crates/pumpkin/src/net/rcon/mod.rs b/crates/pumpkin/src/net/rcon/mod.rs index 4de057961..75a695549 100644 --- a/crates/pumpkin/src/net/rcon/mod.rs +++ b/crates/pumpkin/src/net/rcon/mod.rs @@ -1,13 +1,18 @@ -use std::{net::SocketAddr, sync::atomic::Ordering}; +use std::{ + net::SocketAddr, + sync::{ + Arc, + atomic::{AtomicU32, Ordering}, + }, +}; use packet::{ClientboundPacket, Packet, PacketError, ServerboundPacket}; use pumpkin_config::RCONConfig; -use std::sync::Arc; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, select, }; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; use crate::command::CommandSender; use crate::{SHOULD_STOP, STOP_INTERRUPT, server::Server}; @@ -33,9 +38,11 @@ impl RCONServer { } }; - let password = Arc::new(config.password.clone()); + info!("RCON server is listening on {}", config.address); + + let password = Arc::new(config.password.clone()); + let connections = Arc::new(AtomicU32::new(0)); - let mut connections = 0; while !SHOULD_STOP.load(Ordering::Relaxed) { let await_new_client = || async { let t1 = listener.accept(); @@ -52,18 +59,29 @@ impl RCONServer { break; }; - if config.max_connections != 0 && connections >= config.max_connections { + let current_conns = connections.load(Ordering::Relaxed); + if config.max_connections != 0 && current_conns >= config.max_connections { + warn!( + "RCON ({}): Connection rejected, maximum connections ({}) reached", + address, config.max_connections + ); continue; } - connections += 1; + connections.fetch_add(1, Ordering::Relaxed); let mut client = RCONClient::new(connection, address); let password = password.clone(); let server = server.clone(); - tokio::spawn(async move { while !client.handle(&server, &password).await {} }); - debug!("closed RCON connection"); - connections -= 1; + let connections = connections.clone(); + tokio::spawn(async move { + while !client.handle(&server, &password).await {} + connections.fetch_sub(1, Ordering::Relaxed); + if server.advanced_config.networking.rcon.logging.quit { + info!("RCON ({}): Client disconnected", address); + } + debug!("closed RCON connection with {}", address); + }); } } } @@ -100,19 +118,33 @@ impl RCONClient { return true; } } - // If we get a close here, we might have a reply, which we still want to write. - let _ = self.poll(server, password).await.map_err(|e| { - error!("RCON error: {e}"); - self.closed = true; - }); + while !self.closed { + match self.receive_packet() { + Ok(Some(packet)) => { + if let Err(e) = self.process_packet(server, password, packet).await { + error!("RCON error: {e}"); + self.closed = true; + break; + } + } + Ok(None) => break, + Err(e) => { + error!("RCON packet error: {e}"); + self.closed = true; + break; + } + } + } } self.closed } - async fn poll(&mut self, server: &Arc, password: &str) -> Result<(), PacketError> { - let Some(packet) = self.receive_packet()? else { - return Ok(()); - }; + async fn process_packet( + &mut self, + server: &Arc, + password: &str, + packet: Packet, + ) -> Result<(), PacketError> { let config = &server.advanced_config.networking.rcon; match packet.get_type() { ServerboundPacket::Auth => { @@ -153,13 +185,34 @@ impl RCONClient { .await; let output = output.lock().await; - for line in output.iter() { + if output.is_empty() { if config.logging.commands { - info!("RCON ({}): {}", self.address, line); + info!( + "RCON ({}): Executed command: {}", + self.address, + packet.get_body() + ); } - self.send(ClientboundPacket::Output, packet.get_id(), line) + self.send(ClientboundPacket::Output, packet.get_id(), "") .await?; + } else { + for line in output.iter() { + if config.logging.commands { + info!("RCON ({}): {}", self.address, line); + } + self.send(ClientboundPacket::Output, packet.get_id(), line) + .await?; + } } + } else { + if config.logging.wrong_password { + info!( + "RCON ({}): Unauthenticated client tried to execute command", + self.address + ); + } + self.send(ClientboundPacket::AuthResponse, -1, "").await?; + self.closed = true; } } } @@ -184,7 +237,7 @@ impl RCONClient { ) -> Result<(), PacketError> { let buf = packet.write_buf(id, body); self.connection - .write(&buf) + .write_all(&buf) .await .map_err(PacketError::FailedSend)?; Ok(())