fix: end portal crash and island setup (#1945)

* fix: use noise_sampler

* fix: end portal teleportation and generation

* feature: dragon fight start (improved main end island)
This commit is contained in:
RB007
2026-03-31 03:21:11 -04:00
committed by GitHub
parent 5a0db0f50d
commit 3c43f9715e
12 changed files with 639 additions and 27 deletions

View File

@@ -1233,7 +1233,21 @@ fn value_to_placement_modifier_cf(v: &Value) -> TokenStream {
match type_str {
"minecraft:biome" => quote! { PlacementModifier::Biome(BiomePlacementModifier) },
"minecraft:in_square" => quote! { PlacementModifier::InSquare(SquarePlacementModifier) },
"minecraft:fixed_placement" => quote! { PlacementModifier::FixedPlacement },
"minecraft:fixed_placement" => {
let positions = v["positions"]
.as_array()
.map(|arr| {
arr.iter().map(|p| {
let coords = p.as_array().unwrap();
let x = coords[0].as_i64().unwrap_or(0) as i32;
let y = coords[1].as_i64().unwrap_or(0) as i32;
let z = coords[2].as_i64().unwrap_or(0) as i32;
quote! { BlockPos::new(#x, #y, #z) }
}).collect::<Vec<_>>()
})
.unwrap_or_default();
quote! { PlacementModifier::FixedPlacement(vec![#(#positions),*]) }
},
"minecraft:heightmap" => {
let hm = crate::placed_feature::value_to_height_map(
v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING"),

View File

@@ -108,7 +108,21 @@ fn value_to_placement_modifier(v: &Value) -> TokenStream {
match type_str {
"minecraft:biome" => quote! { PlacementModifier::Biome(BiomePlacementModifier) },
"minecraft:in_square" => quote! { PlacementModifier::InSquare(SquarePlacementModifier) },
"minecraft:fixed_placement" => quote! { PlacementModifier::FixedPlacement },
"minecraft:fixed_placement" => {
let positions = v["positions"]
.as_array()
.map(|arr| {
arr.iter().map(|p| {
let coords = p.as_array().unwrap();
let x = coords[0].as_i64().unwrap_or(0) as i32;
let y = coords[1].as_i64().unwrap_or(0) as i32;
let z = coords[2].as_i64().unwrap_or(0) as i32;
quote! { BlockPos::new(#x, #y, #z) }
}).collect::<Vec<_>>()
})
.unwrap_or_default();
quote! { PlacementModifier::FixedPlacement(vec![#(#positions),*]) }
},
"minecraft:heightmap" => {
let heightmap =
value_to_height_map(v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING"));

View File

@@ -841,7 +841,7 @@ fn build_placed_features() -> std::collections::HashMap<String, PlacedFeature> {
PlacedFeature {
feature: Feature::Named("end_platform".to_string()),
placement: vec![
PlacementModifier::FixedPlacement,
PlacementModifier::FixedPlacement(vec![BlockPos::new(100, 50, 0)]),
PlacementModifier::Biome(BiomePlacementModifier),
],
},

View File

@@ -289,8 +289,9 @@ impl GenerationCache for Cache {
fn ocean_floor_height_exclusive(&self, x: i32, z: i32) -> i32 {
let dx = (x >> 4) - self.x;
let dy = (z >> 4) - self.z;
debug_assert!(dx < self.size && dy < self.size);
debug_assert!(dx >= 0 && dy >= 0);
if dx < 0 || dy < 0 || dx >= self.size || dy >= self.size {
return 0;
}
match &self.chunks[(dx * self.size + dy) as usize] {
Chunk::Level(_data) => {
0 // todo missing
@@ -302,8 +303,13 @@ impl GenerationCache for Cache {
fn get_biome_for_terrain_gen(&self, x: i32, y: i32, z: i32) -> &'static Biome {
let dx = (x >> 4) - self.x;
let dy = (z >> 4) - self.z;
debug_assert!(dx < self.size && dy < self.size);
debug_assert!(dx >= 0 && dy >= 0);
let (dx, dy) = if dx < 0 || dy < 0 || dx >= self.size || dy >= self.size {
// Position is outside the cache — fall back to the centre chunk's biome
let mid = self.size / 2;
(mid, mid)
} else {
(dx, dy)
};
match &self.chunks[(dx * self.size + dy) as usize] {
Chunk::Level(data) => {
// Could this happen?

View File

@@ -1,5 +1,8 @@
use pumpkin_data::Block;
use pumpkin_util::{math::position::BlockPos, random::RandomGenerator};
use pumpkin_util::{
math::{position::BlockPos, vector3::Vector3},
random::RandomGenerator,
};
use crate::generation::proto_chunk::GenerationCache;
use crate::world::BlockRegistryExt;
@@ -18,18 +21,16 @@ impl EndPlatformFeature {
_random: &mut RandomGenerator,
pos: BlockPos,
) -> bool {
for _ in -2..2 {
for _ in -2..2 {
for t in -1..3 {
let state = if t == -1 {
for dx in -2i32..=2 {
for dz in -2i32..=2 {
for dy in -1i32..3 {
let state = if dy == -1 {
Block::OBSIDIAN.default_state
} else {
Block::AIR.default_state
};
if GenerationCache::get_block_state(chunk, &pos.0).0 == state.id {
continue;
}
chunk.set_block_state(&pos.0, state);
let target = Vector3::new(pos.0.x + dx, pos.0.y + dy, pos.0.z + dz);
chunk.set_block_state(&target, state);
}
}
}

View File

@@ -1,4 +1,7 @@
use pumpkin_data::Block;
use pumpkin_data::{
Block, BlockState,
block_properties::{BlockProperties, OakFenceLikeProperties},
};
use pumpkin_util::{
math::position::BlockPos,
random::{RandomGenerator, RandomImpl},
@@ -43,12 +46,17 @@ impl EndSpikeFeature {
) -> bool {
let mut spikes = self.spikes.clone();
if spikes.is_empty() {
for i in 0..10 {
let mut sizes: Vec<i32> = (0..10).collect();
for i in (1..10_usize).rev() {
let j = random.next_bounded_i32(i as i32 + 1) as usize;
sizes.swap(i, j);
}
for (i, &l) in sizes.iter().enumerate() {
let angle = 2.0 * (-std::f64::consts::PI + 0.3141592653589793 * i as f64);
let center_x = (42.0 * angle.cos()).floor() as i32;
let center_z = (42.0 * angle.sin()).floor() as i32;
let l = random.next_bounded_i32(10); // TODO
let radius = 2 + l / 3;
let height = 76 + l * 3;
let guarded = l == 1 || l == 2;
@@ -82,7 +90,7 @@ impl EndSpikeFeature {
),
BlockPos::new(
spike.center_x + radius,
chunk.height() as i32 + 10,
spike.height + 10,
spike.center_z + radius,
),
) {
@@ -100,6 +108,60 @@ impl EndSpikeFeature {
}
chunk.set_block_state(&pos.0, Block::AIR.default_state);
}
// TODO
// Bedrock cap serves as the crystal base, fire sits on top of it
chunk.set_block_state(
&pumpkin_util::math::vector3::Vector3::new(
spike.center_x,
spike.height,
spike.center_z,
),
Block::BEDROCK.default_state,
);
chunk.set_block_state(
&pumpkin_util::math::vector3::Vector3::new(
spike.center_x,
spike.height + 1,
spike.center_z,
),
Block::FIRE.default_state,
);
// Iron-bar cage for guarded spikes: 5x5 walls + open top frame at dy=3.
if spike.guarded {
for dy in 0_i32..=3 {
for dx in -2_i32..=2 {
for dz in -2_i32..=2 {
// Only place on perimeter walls and the top frame
let on_x_wall = dx.abs() == 2;
let on_z_wall = dz.abs() == 2;
let on_top = dy == 3;
if !on_x_wall && !on_z_wall && !on_top {
continue;
}
// Connectivity rules
let x_edge = on_x_wall || on_top;
let z_edge = on_z_wall || on_top;
let mut props = OakFenceLikeProperties::default(&Block::IRON_BARS);
props.north = x_edge && dz != 2;
props.south = x_edge && dz != -2;
props.west = z_edge && dx != 2;
props.east = z_edge && dx != -2;
let bar_state = BlockState::from_id(props.to_state_id(&Block::IRON_BARS));
chunk.set_block_state(
&pumpkin_util::math::vector3::Vector3::new(
spike.center_x + dx,
spike.height + dy,
spike.center_z + dz,
),
bar_state,
);
}
}
}
}
}
}

View File

@@ -121,7 +121,7 @@ pub enum PlacementModifier {
HeightRange(HeightRangePlacementModifier),
InSquare(SquarePlacementModifier),
RandomOffset(RandomOffsetPlacementModifier),
FixedPlacement,
FixedPlacement(Vec<BlockPos>),
}
impl PlacementModifier {
@@ -161,7 +161,7 @@ impl PlacementModifier {
Self::HeightRange(modifier) => modifier.get_positions(min_y, height, random, pos),
Self::InSquare(_) => SquarePlacementModifier::get_positions(random, pos),
Self::RandomOffset(modifier) => modifier.get_positions(random, pos),
Self::FixedPlacement => Box::new(iter::empty()),
Self::FixedPlacement(positions) => Box::new(positions.clone().into_iter()),
}
}
}

View File

@@ -713,9 +713,8 @@ impl ProtoChunk {
let v_count = noise_sampler.vertical_cell_block_count() as i32;
let horizontal_cells = CHUNK_DIM as i32 / h_count;
let min_y = self.bottom_y();
let minimum_cell_y = min_y / v_count as i8;
let cell_height = self.height() / v_count as u16;
let minimum_cell_y = noise_sampler.min_y() / v_count as i8;
let cell_height = noise_sampler.height() / v_count as u16;
let delta_y_step = 1.0 / v_count as f64;
let delta_x_z_step = 1.0 / h_count as f64;

View File

@@ -1599,7 +1599,26 @@ impl Entity {
let source_axis = source_portal.as_ref().map(|p| p.axis);
drop(portal_manager);
let (teleport_pos, new_yaw) = if let Some(dest_result) =
let is_end_portal = dest_world.dimension == Dimension::THE_END
|| self.world.load().dimension == Dimension::THE_END;
let (teleport_pos, new_yaw) = if is_end_portal {
if dest_world.dimension == Dimension::THE_END {
// Entering the End: spawn on the obsidian platform at (100, 50, 0)
(Vector3::new(100.5f64, 50.0f64, 0.5f64), None)
} else {
// Leaving the End through the exit portal: return to overworld spawn
let info = dest_world.level_info.load();
(
Vector3::new(
f64::from(info.spawn_x) + 0.5,
f64::from(info.spawn_y),
f64::from(info.spawn_z) + 0.5,
),
None,
)
}
} else if let Some(dest_result) =
NetherPortal::search_for_portal(&dest_world, target_pos).await
{
let base_pos = source_portal.as_ref().map_or_else(

View File

@@ -0,0 +1,391 @@
//! End Dragon fight manager. Handles the dragon lifecycle, end crystal
//! spawning, boss bar, and exit portal.
use std::sync::Arc;
use tracing::info;
use uuid::Uuid;
use pumpkin_data::{Block, entity::EntityType};
use pumpkin_util::{
math::{position::BlockPos, vector2::Vector2, vector3::Vector3},
text::TextComponent,
};
use pumpkin_world::world::BlockFlags;
use super::{
World,
bossbar::{Bossbar, BossbarColor, BossbarDivisions, BossbarFlags},
};
use crate::entity::{Entity, decoration::end_crystal::EndCrystalEntity};
const MAX_TICKS_BEFORE_DRAGON_RESPAWN: i32 = 1200;
const CRYSTAL_SCAN_INTERVAL: i32 = 100;
const PLAYER_SCAN_INTERVAL: i32 = 20;
const DRAGON_SPAWN_Y: f64 = 128.0;
const ARENA_RADIUS: f64 = 192.0;
pub struct DragonFight {
pub dragon_killed: bool,
pub previously_killed: bool,
pub needs_state_scanning: bool,
pub dragon_uuid: Option<Uuid>,
pub portal_location: Option<BlockPos>,
pub crystals_alive: u32,
ticks_since_dragon_seen: i32,
ticks_since_crystals_scanned: i32,
ticks_since_last_player_scan: i32,
bossbar_uuid: Uuid,
bossbar_players: Vec<Uuid>,
}
impl Default for DragonFight {
fn default() -> Self {
Self::new()
}
}
impl DragonFight {
#[must_use]
pub fn new() -> Self {
Self {
dragon_killed: false,
previously_killed: false,
needs_state_scanning: true,
dragon_uuid: None,
portal_location: None,
crystals_alive: 0,
ticks_since_dragon_seen: 0,
ticks_since_crystals_scanned: 0,
// Start over the threshold so the scan runs on the first tick with players
ticks_since_last_player_scan: PLAYER_SCAN_INTERVAL + 1,
bossbar_uuid: Uuid::new_v4(),
bossbar_players: Vec::new(),
}
}
pub async fn tick(&mut self, world: &Arc<World>) {
self.ticks_since_last_player_scan += 1;
if self.ticks_since_last_player_scan >= PLAYER_SCAN_INTERVAL {
self.update_players(world).await;
self.ticks_since_last_player_scan = 0;
}
// Nothing to do without players nearby
if self.bossbar_players.is_empty() {
return;
}
if self.needs_state_scanning {
self.scan_state(world).await;
self.needs_state_scanning = false;
}
if !self.dragon_killed {
self.ticks_since_dragon_seen += 1;
if self.dragon_uuid.is_none()
|| self.ticks_since_dragon_seen >= MAX_TICKS_BEFORE_DRAGON_RESPAWN
{
self.find_or_create_dragon(world).await;
self.ticks_since_dragon_seen = 0;
}
self.ticks_since_crystals_scanned += 1;
if self.ticks_since_crystals_scanned >= CRYSTAL_SCAN_INTERVAL {
self.update_crystal_count(world);
self.ticks_since_crystals_scanned = 0;
}
}
}
}
impl DragonFight {
/// Runs once on the first tick that has players. Figures out whether this
/// is a fresh world or a resumed one and brings the podium/dragon into a
/// consistent state.
async fn scan_state(&mut self, world: &Arc<World>) {
info!("Scanning End fight state...");
let has_active_portal = self.has_active_exit_portal(world).await;
if has_active_portal {
info!("Exit portal found dragon has been killed previously.");
self.previously_killed = true;
} else {
info!("No exit portal fight is fresh or in progress.");
self.previously_killed = false;
// Place the inactive podium if it hasn't been set yet
if self.portal_location.is_none() {
self.spawn_exit_portal(world, false).await;
}
// Spawn crystals on the spike caps
self.spawn_crystals(world).await;
}
// Check for a live dragon entity.
let existing_dragon = {
let entities = world.entities.load();
entities
.iter()
.find(|e| e.get_entity().entity_type == &EntityType::ENDER_DRAGON)
.map(|e| e.get_entity().entity_uuid)
};
if let Some(uuid) = existing_dragon {
if has_active_portal {
// The portal is active but a dragon entity still exists... so clean it up
if let Some(e) = world
.entities
.load()
.iter()
.find(|e| e.get_entity().entity_uuid == uuid)
{
e.get_entity().remove().await;
}
self.dragon_uuid = None;
self.dragon_killed = true;
} else {
info!("Found existing dragon entity.");
self.dragon_uuid = Some(uuid);
self.dragon_killed = false;
}
} else {
self.dragon_killed = true;
}
// If the world looks fresh but the dragon appears dead, reset so one gets spawned
if !self.previously_killed && self.dragon_killed {
self.dragon_killed = false;
}
}
/// Returns true if an active `END_PORTAL` block exists within an 8-chunk
/// radius of the origin. Samples one column per chunk for performance
async fn has_active_exit_portal(&self, world: &Arc<World>) -> bool {
for cx in -8i32..=8 {
for cz in -8i32..=8 {
let bx = cx * 16;
let bz = cz * 16;
for y in 30i32..=80 {
if world.get_block(&BlockPos::new(bx, y, bz)).await == &Block::END_PORTAL {
return true;
}
}
}
}
false
}
}
impl DragonFight {
async fn find_or_create_dragon(&mut self, world: &Arc<World>) {
let uuid = {
let entities = world.entities.load();
entities
.iter()
.find(|e| e.get_entity().entity_type == &EntityType::ENDER_DRAGON)
.map(|e| e.get_entity().entity_uuid)
};
if let Some(u) = uuid {
info!("Re-acquired existing dragon.");
self.dragon_uuid = Some(u);
} else {
info!("No dragon found \u{2013} spawning one.");
self.create_new_dragon(world).await;
}
}
async fn create_new_dragon(&mut self, world: &Arc<World>) {
let entity = Entity::new(
world.clone(),
Vector3::new(0.5, DRAGON_SPAWN_Y, 0.5),
&EntityType::ENDER_DRAGON,
);
let uuid = entity.entity_uuid;
world.spawn_entity(Arc::new(entity)).await;
self.dragon_uuid = Some(uuid);
info!("Spawned ender dragon.");
}
fn update_crystal_count(&mut self, world: &Arc<World>) {
self.crystals_alive = world
.entities
.load()
.iter()
.filter(|e| e.get_entity().entity_type == &EntityType::END_CRYSTAL)
.count() as u32;
}
/// Called when the dragon dies. Activates the exit portal, places the
/// dragon egg on a first kill, and hides the boss bar.
pub async fn set_dragon_killed(&mut self, world: &Arc<World>, killed_uuid: Uuid) {
if Some(killed_uuid) != self.dragon_uuid {
return;
}
self.update_bossbar_health(world, 0.0).await;
self.remove_all_bossbar(world).await;
self.spawn_exit_portal(world, true).await;
if !self.previously_killed
&& let Some(loc) = self.portal_location
{
// The bedrock pillar is 4 blocks tall, so the egg sits at podium_y + 4
let egg_pos = BlockPos::new(loc.0.x, loc.0.y + 4, loc.0.z);
world
.set_block_state(
&egg_pos,
Block::DRAGON_EGG.default_state.id,
BlockFlags::NOTIFY_ALL,
)
.await;
}
self.previously_killed = true;
self.dragon_killed = true;
self.dragon_uuid = None;
}
}
impl DragonFight {
/// Spawns end crystals on each obsidian spike. Scans downward from y=115
/// for the bedrock cap placed by the spike generator and places the crystal
/// one block above it. Falls back to y=78 if the chunk hasn't loaded yet.
pub async fn spawn_crystals(&mut self, world: &Arc<World>) {
// Don't re-spawn if crystals are already present (resumed world).
if world
.entities
.load()
.iter()
.any(|e| e.get_entity().entity_type == &EntityType::END_CRYSTAL)
{
return;
}
for i in 0..10usize {
let angle = 2.0 * (-std::f64::consts::PI + std::f64::consts::PI * 0.1 * i as f64);
let cx = (42.0f64 * angle.cos()).floor() as i32;
let cz = (42.0f64 * angle.sin()).floor() as i32;
let mut crystal_y: Option<i32> = None;
for y in (70..=115i32).rev() {
if world.get_block(&BlockPos::new(cx, y, cz)).await == &Block::BEDROCK {
crystal_y = Some(y + 1);
break;
}
}
let y = crystal_y.unwrap_or(78);
let entity = Entity::new(
world.clone(),
Vector3::new(cx as f64 + 0.5, y as f64, cz as f64 + 0.5),
&EntityType::END_CRYSTAL,
);
let crystal = Arc::new(EndCrystalEntity::new(entity));
crystal.set_show_bottom(true).await;
world.spawn_entity(crystal).await;
}
info!("Spawned end crystals on spike tops.");
}
}
impl DragonFight {
/// Places the exit podium centred at column (0, 0). Pass `active = true`
/// to fill the portal disc with `END_PORTAL` blocks after the dragon dies.
pub async fn spawn_exit_portal(&mut self, world: &Arc<World>, active: bool) {
if self.portal_location.is_none() {
let top_y = world.get_top_block(Vector2::new(0, 0)).await;
let mut portal_y = top_y;
// Walk down past any bedrock already at the centre (e.g. spike cap).
while portal_y > 63 {
if world.get_block(&BlockPos::new(0, portal_y, 0)).await != &Block::BEDROCK {
break;
}
portal_y -= 1;
}
portal_y = portal_y.max(world.min_y + 1);
self.portal_location = Some(BlockPos::new(0, portal_y, 0));
}
if let Some(loc) = self.portal_location {
super::end_podium::place(world, loc, active).await;
}
}
}
impl DragonFight {
fn make_bossbar(&self) -> Bossbar {
Bossbar {
uuid: self.bossbar_uuid,
title: TextComponent::translate("entity.minecraft.ender_dragon", []),
health: 1.0,
color: BossbarColor::Pink,
division: BossbarDivisions::NoDivision,
flags: BossbarFlags::DragonBar,
}
}
async fn update_bossbar_health(&self, world: &Arc<World>, health: f32) {
for player in world.players.load().iter() {
if self.bossbar_players.contains(&player.gameprofile.id) {
player
.update_bossbar_health(&self.bossbar_uuid, health)
.await;
}
}
}
async fn remove_all_bossbar(&mut self, world: &Arc<World>) {
for player in world.players.load().iter() {
if self.bossbar_players.contains(&player.gameprofile.id) {
player.remove_bossbar(self.bossbar_uuid).await;
}
}
self.bossbar_players.clear();
}
/// Syncs the boss bar recipient list
async fn update_players(&mut self, world: &Arc<World>) {
let players = world.players.load();
let current: Vec<Uuid> = players
.iter()
.filter(|p| {
let pos = p.living_entity.entity.pos.load();
let dx = pos.x;
let dy = pos.y - DRAGON_SPAWN_Y;
let dz = pos.z;
dx * dx + dy * dy + dz * dz < ARENA_RADIUS * ARENA_RADIUS
})
.map(|p| p.gameprofile.id)
.collect();
for &uid in &current {
if !self.bossbar_players.contains(&uid) {
if !self.dragon_killed
&& let Some(p) = players.iter().find(|p| p.gameprofile.id == uid)
{
p.send_bossbar(&self.make_bossbar()).await;
}
self.bossbar_players.push(uid);
}
}
let to_remove: Vec<Uuid> = self
.bossbar_players
.iter()
.filter(|uid| !current.contains(uid))
.copied()
.collect();
for uid in &to_remove {
if let Some(p) = players.iter().find(|p| &p.gameprofile.id == uid) {
p.remove_bossbar(self.bossbar_uuid).await;
}
self.bossbar_players.retain(|u| u != uid);
}
}
}

View File

@@ -0,0 +1,95 @@
//! Places the End exit podium structure (the beacon-like structure at (0, y, 0)).
//!
//! `active = false` = inactive podium: bedrock rim + air centre, placed before the dragon is killed.
//! `active = true` = active podium: bedrock rim + `END_PORTAL` tiles in the centre, placed after kill.
use std::sync::Arc;
use pumpkin_data::{
Block,
block_properties::{BlockProperties, HorizontalFacing, WallTorchLikeProperties},
};
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::world::BlockFlags;
use super::World;
/// Place the podium structure centred on `origin` into `world`.
pub async fn place(world: &Arc<World>, origin: BlockPos, active: bool) {
let ox = origin.0.x;
let oy = origin.0.y;
let oz = origin.0.z;
for y in (oy - 4)..=(oy + 32) {
for x in (ox - 4)..=(ox + 4) {
for z in (oz - 4)..=(oz + 4) {
let dx = (x - ox) as f64;
let dy = (y - oy) as f64;
let dz = (z - oz) as f64;
let dist_sq = dx * dx + dy * dy + dz * dz;
let inside_rim = dist_sq < 2.5 * 2.5; // inner 2-block disc
let near = inside_rim || dist_sq < 3.5 * 3.5; // outer rim up to 3.5
if !near {
continue;
}
let pos = BlockPos::new(x, y, z);
let state_id = match y.cmp(&oy) {
std::cmp::Ordering::Less => {
if inside_rim {
Block::BEDROCK.default_state.id
} else {
Block::END_STONE.default_state.id
}
}
std::cmp::Ordering::Greater => Block::AIR.default_state.id,
std::cmp::Ordering::Equal => {
if !inside_rim {
Block::BEDROCK.default_state.id
} else if active {
Block::END_PORTAL.default_state.id
} else {
Block::AIR.default_state.id
}
}
};
world
.set_block_state(&pos, state_id, BlockFlags::NOTIFY_ALL)
.await;
}
}
}
for y in oy..=(oy + 3) {
world
.set_block_state(
&BlockPos::new(ox, y, oz),
Block::BEDROCK.default_state.id,
BlockFlags::NOTIFY_ALL,
)
.await;
}
// Wall torches on N/S/E/W faces at pillar height 2
let torch_y = oy + 2;
// Torch position relative to origin and facing direction: (dx, dz, facing)
for (dx, dz, facing) in [
(0i32, -1i32, HorizontalFacing::North),
(0, 1, HorizontalFacing::South),
(-1, 0, HorizontalFacing::West),
(1, 0, HorizontalFacing::East),
] {
let props = WallTorchLikeProperties { facing };
let state_id = props.to_state_id(&Block::WALL_TORCH);
world
.set_block_state(
&BlockPos::new(ox + dx, torch_y, oz + dz),
state_id,
BlockFlags::NOTIFY_ALL,
)
.await;
}
}

View File

@@ -135,6 +135,8 @@ use tokio::sync::Mutex;
pub mod border;
pub mod bossbar;
pub mod custom_bossbar;
pub mod dragon_fight;
pub mod end_podium;
pub mod natural_spawner;
pub mod scoreboard;
pub mod weather;
@@ -202,6 +204,8 @@ pub struct World {
unsent_block_changes: Mutex<HashMap<BlockPos, u16>>,
/// POI storage for fast portal lookups
pub portal_poi: Mutex<portal::PortalPoiStorage>,
/// End Dragon fight manager (only present in `THE_END` dimension).
pub dragon_fight: Option<Mutex<dragon_fight::DragonFight>>,
}
impl PartialEq for World {
@@ -244,6 +248,8 @@ impl World {
synced_block_event_queue: Mutex::new(Vec::new()),
unsent_block_changes: Mutex::new(HashMap::new()),
portal_poi: Mutex::new(portal_poi),
dragon_fight: (dimension == Dimension::THE_END)
.then(|| Mutex::new(dragon_fight::DragonFight::new())),
server,
}
}
@@ -781,6 +787,11 @@ impl World {
//self.level.chunk_loading.lock().unwrap().send_change();
// Tick the End dragon fight (only on THE_END worlds).
if let Some(ref fight_mutex) = self.dragon_fight {
fight_mutex.lock().await.tick(self).await;
}
let total_elapsed = start.elapsed();
if total_elapsed.as_millis() > 50 {
debug!(