Add Nether Portals

This commit is contained in:
Alexander Medvedev
2025-05-04 16:58:39 +02:00
parent bfc2f6794a
commit ffa61c8aac
8 changed files with 399 additions and 3 deletions

View File

@@ -72,7 +72,7 @@ pub struct SyncedRegistry {
instrument: IndexMap<String, Instrument>,
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DimensionType {
Overworld,
OverworldCaves,

View File

@@ -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<Self::Item> {
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<i32>);
@@ -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<i32>, Vector3<i32>) {
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<i32>, 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))
}

View File

@@ -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>,
world: &Arc<World>,
_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(

View File

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

View File

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

View File

@@ -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<BlockRegistry> {
manager.register(TNTBlock);
manager.register(TorchBlock);
manager.register(WallBlock);
manager.register(NetherPortalBlock);
// Fire
manager.register(SoulFireBlock);

View File

@@ -6,6 +6,7 @@ use std::{
pub mod chunker;
pub mod explosion;
pub mod portal;
pub mod time;
use crate::{

251
pumpkin/src/world/portal.rs Normal file
View File

@@ -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<World>) {
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<Self> {
// 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<Self> {
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<BlockPos> {
// 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<u32> {
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
}
}