diff --git a/pumpkin-registry/src/lib.rs b/pumpkin-registry/src/lib.rs index 6b9ea4966..8b8fbc845 100644 --- a/pumpkin-registry/src/lib.rs +++ b/pumpkin-registry/src/lib.rs @@ -72,7 +72,7 @@ pub struct SyncedRegistry { instrument: IndexMap, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DimensionType { Overworld, OverworldCaves, diff --git a/pumpkin-util/src/math/position.rs b/pumpkin-util/src/math/position.rs index 06860a0d9..ea9677f1e 100644 --- a/pumpkin-util/src/math/position.rs +++ b/pumpkin-util/src/math/position.rs @@ -9,6 +9,65 @@ use crate::math::vector2::Vector2; use num_traits::Euclid; use serde::{Deserialize, Serialize}; +pub struct BlockPosIterator { + start_x: i32, + start_y: i32, + start_z: i32, + end_x: i32, + end_y: i32, + index: usize, + count: usize, +} + +impl BlockPosIterator { + pub fn new( + start_x: i32, + start_y: i32, + start_z: i32, + end_x: i32, + end_y: i32, + end_z: i32, + ) -> Self { + let count_x = end_x - start_x + 1; + let count_y = end_y - start_y + 1; + let count_z = end_z - start_z + 1; + let count = (count_x * count_y * count_z) as usize; + BlockPosIterator { + start_x, + start_y, + start_z, + end_x, + end_y, + index: 0, + count, + } + } +} + +impl Iterator for BlockPosIterator { + type Item = BlockPos; + + fn next(&mut self) -> Option { + if self.index >= self.count { + return None; + } + + let size_x = (self.end_x - self.start_x + 1) as usize; + let size_y = (self.end_y - self.start_y + 1) as usize; + + let x_offset = self.index % size_x; + let y_offset = (self.index / size_x) % size_y; + let z_offset = (self.index / size_x) / size_y; + + let x = self.start_x + x_offset as i32; + let y = self.start_y + y_offset as i32; + let z = self.start_z + z_offset as i32; + + self.index += 1; + Some(BlockPos::new(x, y, z)) + } +} + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] /// Aka Block Position pub struct BlockPos(pub Vector3); @@ -18,6 +77,28 @@ impl BlockPos { Self(Vector3::new(x, y, z)) } + pub fn iterate(start: BlockPos, end: BlockPos) -> BlockPosIterator { + BlockPosIterator::new( + start.0.x.min(end.0.x), + start.0.y.min(end.0.y), + start.0.z.min(end.0.z), + start.0.x.max(end.0.x), + start.0.y.max(end.0.y), + start.0.z.max(end.0.z), + ) + } + + pub fn iterate_block_pos( + start_x: i32, + start_y: i32, + start_z: i32, + end_x: i32, + end_y: i32, + end_z: i32, + ) -> BlockPosIterator { + BlockPosIterator::new(start_x, start_y, start_z, end_x, end_y, end_z) + } + pub fn chunk_and_chunk_relative_position(&self) -> (Vector2, Vector3) { let (z_chunk, z_rem) = self.0.z.div_rem_euclid(&16); let (x_chunk, x_rem) = self.0.x.div_rem_euclid(&16); @@ -75,6 +156,14 @@ impl BlockPos { BlockPos(self.0 + offset) } + pub fn offset_dir(&self, offset: Vector3, direction: i32) -> Self { + BlockPos(Vector3::new( + self.0.x + offset.x * direction, + self.0.y + offset.y * direction, + self.0.z + offset.z * direction, + )) + } + pub fn up(&self) -> Self { self.offset(Vector3::new(0, 1, 0)) } diff --git a/pumpkin/src/block/blocks/fire/mod.rs b/pumpkin/src/block/blocks/fire/mod.rs index d7a41ec4a..2a3bdec91 100644 --- a/pumpkin/src/block/blocks/fire/mod.rs +++ b/pumpkin/src/block/blocks/fire/mod.rs @@ -4,9 +4,12 @@ use crate::block::pumpkin_block::PumpkinBlock; use crate::entity::player::Player; use crate::server::Server; use crate::world::World; +use crate::world::portal::NetherPortal; use async_trait::async_trait; +use pumpkin_data::block_properties::HorizontalAxis; use pumpkin_data::world::WorldEvent; use pumpkin_data::{Block, BlockState}; +use pumpkin_registry::DimensionType; use pumpkin_util::math::position::BlockPos; use pumpkin_world::BlockStateId; use pumpkin_world::block::BlockDirection; @@ -33,16 +36,24 @@ impl FireBlockBase { impl PumpkinBlock for FireBlockBase { async fn placed( &self, - _world: &Arc, + world: &Arc, _block: &Block, state_id: BlockStateId, - _pos: &BlockPos, + pos: &BlockPos, old_state_id: BlockStateId, _notify: bool, ) { if old_state_id == state_id { return; } + let dimension = world.dimension_type; + // First lets check if we are in OverWorld or Nether, its not possible to place an Nether portal in other dimensions in Vanilla + if dimension == DimensionType::Overworld || dimension == DimensionType::TheNether { + if let Some(portal) = NetherPortal::get_new_portal(world, pos, HorizontalAxis::X).await + { + portal.create(world).await; + } + } } async fn can_place_at( diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs index 75417a940..c33a18563 100644 --- a/pumpkin/src/block/blocks/mod.rs +++ b/pumpkin/src/block/blocks/mod.rs @@ -19,6 +19,7 @@ pub mod glass_panes; pub mod iron_bars; pub mod jukebox; pub mod logs; +pub mod nether_portal; pub mod redstone; pub mod signs; pub mod slabs; diff --git a/pumpkin/src/block/blocks/nether_portal.rs b/pumpkin/src/block/blocks/nether_portal.rs new file mode 100644 index 000000000..3d08f53f4 --- /dev/null +++ b/pumpkin/src/block/blocks/nether_portal.rs @@ -0,0 +1,41 @@ +use crate::block::pumpkin_block::PumpkinBlock; +use crate::world::World; +use crate::world::portal::NetherPortal; +use async_trait::async_trait; +use pumpkin_data::Block; +use pumpkin_data::block_properties::{Axis, BlockProperties, NetherPortalLikeProperties}; +use pumpkin_macros::pumpkin_block; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::BlockStateId; +use pumpkin_world::block::BlockDirection; + +#[pumpkin_block("minecraft:nether_portal")] +pub struct NetherPortalBlock; + +#[async_trait] +impl PumpkinBlock for NetherPortalBlock { + async fn get_state_for_neighbor_update( + &self, + world: &World, + _block: &Block, + state: BlockStateId, + pos: &BlockPos, + direction: BlockDirection, + _neighbor_pos: &BlockPos, + neighbor_state: BlockStateId, + ) -> BlockStateId { + let axis = direction.to_axis(); + let is_horizontal = axis == Axis::X && axis == Axis::Z; + let state_axis = + NetherPortalLikeProperties::from_state_id(state, &Block::NETHER_PORTAL).axis; + if is_horizontal + || neighbor_state == state + || NetherPortal::get_on_axis(world, pos, state_axis) + .await + .is_some_and(|e| e.was_already_valid()) + { + return state; + } + Block::AIR.default_state_id + } +} diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index 35e1c5021..beb7cec12 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -9,6 +9,7 @@ use blocks::fire::soul_fire::SoulFireBlock; use blocks::glass_panes::GlassPaneBlock; use blocks::iron_bars::IronBarsBlock; use blocks::logs::LogBlock; +use blocks::nether_portal::NetherPortalBlock; use blocks::redstone::buttons::ButtonBlock; use blocks::redstone::observer::ObserverBlock; use blocks::redstone::piston::PistonBlock; @@ -85,6 +86,7 @@ pub fn default_registry() -> Arc { manager.register(TNTBlock); manager.register(TorchBlock); manager.register(WallBlock); + manager.register(NetherPortalBlock); // Fire manager.register(SoulFireBlock); diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 96f2960e3..eee5e8a02 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -6,6 +6,7 @@ use std::{ pub mod chunker; pub mod explosion; +pub mod portal; pub mod time; use crate::{ diff --git a/pumpkin/src/world/portal.rs b/pumpkin/src/world/portal.rs new file mode 100644 index 000000000..c47452223 --- /dev/null +++ b/pumpkin/src/world/portal.rs @@ -0,0 +1,251 @@ +use std::sync::Arc; + +use pumpkin_data::{ + Block, BlockState, + block_properties::{BlockProperties, HorizontalAxis, NetherPortalLikeProperties}, + tag::Tagable, +}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::BlockDirection; + +use super::{BlockFlags, World}; + +pub struct NetherPortal { + axis: HorizontalAxis, + found_portal_blocks: u32, + negative_direction: BlockDirection, + lower_conor: BlockPos, + width: u32, + height: u32, +} + +impl NetherPortal { + /// This is Vanilla + const MIN_WIDTH: u32 = 2; + const MAX_WIDTH: u32 = 21; + + const MAX_HEIGHT: u32 = 21; + const MIN_HEIGHT: u32 = 3; + + const FRAME_BLOCK: Block = Block::OBSIDIAN; + + #[must_use] + pub fn is_valid(&self) -> bool { + self.width >= Self::MIN_WIDTH + && self.width <= Self::MAX_WIDTH + && self.height >= Self::MIN_WIDTH + && self.height <= Self::MAX_HEIGHT + } + + #[must_use] + pub fn was_already_valid(&self) -> bool { + self.is_valid() && self.found_portal_blocks == self.width * self.height + } + + pub async fn create(&self, world: &Arc) { + let mut props = NetherPortalLikeProperties::default(&Block::NETHER_PORTAL); + props.axis = self.axis; + let state = props.to_state_id(&Block::NETHER_PORTAL); + // We remove 1 block because of the frame border + let blocks = BlockPos::iterate( + self.lower_conor, + self.lower_conor + .offset_dir(BlockDirection::Up.to_offset(), self.height as i32 - 1) + .offset_dir(self.negative_direction.to_offset(), self.width as i32 - 1), + ); + for pos in blocks { + world + .set_block_state( + &pos, + state, + BlockFlags::NOTIFY_LISTENERS | BlockFlags::FORCE_STATE, + ) + .await; + } + } + + pub async fn get_new_portal( + world: &World, + pos: &BlockPos, + first_axis: HorizontalAxis, + ) -> Option { + // We check both axis here X and Z + if let Some(portal) = Self::get_on_axis(world, pos, first_axis).await { + if portal.is_valid() && portal.found_portal_blocks == 0 { + return Some(portal); + } + } + let next_axis = if first_axis == HorizontalAxis::X { + HorizontalAxis::Z + } else { + HorizontalAxis::X + }; + if let Some(portal) = Self::get_on_axis(world, pos, next_axis).await { + if portal.is_valid() && portal.found_portal_blocks == 0 { + return Some(portal); + } + } + None + } + + pub async fn get_on_axis(world: &World, pos: &BlockPos, axis: HorizontalAxis) -> Option { + let direction = if axis == HorizontalAxis::X { + BlockDirection::West + } else { + BlockDirection::South + }; + let cornor = Self::get_lower_cornor(world, direction, pos).await?; + let width = Self::get_width(world, &cornor, &direction).await; + if !(Self::MIN_WIDTH..=Self::MAX_WIDTH).contains(&width) { + return None; + } + let mut found_portal_blocks = 0; + let height = + Self::get_height(world, &cornor, &direction, width, &mut found_portal_blocks).await?; + Some(Self { + axis, + found_portal_blocks, + negative_direction: direction, + lower_conor: cornor, + width, + height, + }) + } + + async fn get_lower_cornor( + world: &World, + direction: BlockDirection, + pos: &BlockPos, + ) -> Option { + // TODO: check max getBottomY + let limit_y = pos.0.y - Self::MAX_HEIGHT as i32; + let mut pos = *pos; + while pos.0.y > limit_y { + let (block, state) = world.get_block_and_block_state(&pos.down()).await.unwrap(); + if !Self::valid_state_inside_portal(&block, &state) { + break; + } + pos = pos.down(); + } + let neg_dir = direction.opposite(); + let width = (Self::get_width(world, &pos, &neg_dir).await as i32) - 1; + if width < 0 { + return None; + } + Some(pos.offset_dir(neg_dir.to_offset(), width as i32)) + } + + async fn get_width( + world: &World, + original_lower_corner: &BlockPos, + negative_dir: &BlockDirection, + ) -> u32 { + let mut lower_corner; + for i in 0..=Self::MAX_WIDTH { + lower_corner = original_lower_corner.offset_dir(negative_dir.to_offset(), i as i32); + let (block, block_state) = world + .get_block_and_block_state(&lower_corner) + .await + .unwrap(); + if !Self::valid_state_inside_portal(&block, &block_state) { + if Self::FRAME_BLOCK != block { + break; + } + return i; + } + let block = world.get_block(&lower_corner.down()).await.unwrap(); + if Self::FRAME_BLOCK != block { + break; + } + } + 0 + } + + async fn get_height( + world: &World, + lower_corner: &BlockPos, + negative_dir: &BlockDirection, + width: u32, + found_portal_blocks: &mut u32, + ) -> Option { + let height = Self::get_potential_height( + world, + lower_corner, + negative_dir, + width, + found_portal_blocks, + ) + .await; + if !(Self::MIN_HEIGHT..=Self::MAX_HEIGHT).contains(&height) + || !Self::is_horizontal_frame_valid(world, lower_corner, negative_dir, width, height) + .await + { + return None; + } + Some(height) + } + + async fn get_potential_height( + world: &World, + lower_corner: &BlockPos, + negative_dir: &BlockDirection, + width: u32, + found_portal_blocks: &mut u32, + ) -> u32 { + for i in 0..Self::MAX_HEIGHT as i32 { + let mut pos = lower_corner + .offset_dir(BlockDirection::Up.to_offset(), i) + .offset_dir(negative_dir.to_offset(), -1); + if world.get_block(&pos).await.unwrap() != Self::FRAME_BLOCK { + return i as u32; + } + + pos = lower_corner + .offset_dir(BlockDirection::Up.to_offset(), i) + .offset_dir(negative_dir.to_offset(), width as i32); + if world.get_block(&pos).await.unwrap() != Self::FRAME_BLOCK { + return i as u32; + } + + for j in 0..width { + pos = lower_corner + .offset_dir(BlockDirection::Up.to_offset(), i) + .offset_dir(negative_dir.to_offset(), j as i32); + let (block, block_state) = world.get_block_and_block_state(&pos).await.unwrap(); + if !Self::valid_state_inside_portal(&block, &block_state) { + return i as u32; + } + if block == Block::NETHER_PORTAL { + *found_portal_blocks += 1; + } + } + } + 21 + } + + async fn is_horizontal_frame_valid( + world: &World, + lower_corner: &BlockPos, + dir: &BlockDirection, + width: u32, + height: u32, + ) -> bool { + let mut pos; + for i in 0..width { + pos = lower_corner + .offset_dir(BlockDirection::Up.to_offset(), height as i32) + .offset_dir(dir.to_offset(), i as i32); + if Self::FRAME_BLOCK != world.get_block(&pos).await.unwrap() { + return false; + } + } + true + } + + /// What is allowed to be inside the Portal frame + fn valid_state_inside_portal(block: &Block, state: &BlockState) -> bool { + state.is_air() + || block.is_tagged_with("minecraft:fire").unwrap() + || block == &Block::NETHER_PORTAL + } +}