fix: entity spawn height

This commit is contained in:
Alexander Medvedev
2026-04-26 15:59:52 +02:00
parent 7f21c3cfe2
commit 6ea869c4a0
13 changed files with 513 additions and 196 deletions

View File

@@ -773,7 +773,7 @@ pub trait ScreenHandler: Send + Sync {
}
/// Cancels any client-side changes and resynchronizes the state.
fn cancel<'a>(&'a mut self, _player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
fn cancel(&mut self) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
self.sync_state().await;
})

View File

@@ -25,6 +25,25 @@ interface player {
walk-speed: f32,
}
/// Represents a player's skin textures and metadata.
record player-skin {
/// The base64-encoded texture data (usually JSON containing skin/cape URLs).
value: string,
/// The optional signature for the texture data.
signature: option<string>,
}
/// Flags representing visible skin parts.
flags skin-parts {
cape,
jacket,
left-sleeve,
right-sleeve,
left-pants-leg,
right-pants-leg,
hat,
}
/// A handle to a player on the server.
resource player {
/// Returns the player as an entity.
@@ -162,36 +181,6 @@ interface player {
/// Adds experience points to the player.
add-experience-points: func(points: s32);
/// Returns whether the player is currently sneaking.
is-sneaking: func() -> bool;
/// Sets whether the player is currently sneaking.
set-sneaking: func(sneaking: bool);
/// Returns whether the player is currently sprinting.
is-sprinting: func() -> bool;
/// Sets whether the player is currently sprinting.
set-sprinting: func(sprinting: bool);
/// Returns whether the player is currently swimming.
is-swimming: func() -> bool;
/// Sets whether the player is currently swimming.
set-swimming: func(swimming: bool);
/// Returns whether the player is currently invisible.
is-invisible: func() -> bool;
/// Sets whether the player is currently invisible.
set-invisible: func(invisible: bool);
/// Returns whether the player is currently glowing.
is-glowing: func() -> bool;
/// Sets whether the player is currently glowing.
set-glowing: func(glowing: bool);
/// Returns whether the player is currently flying due to a fall.
is-fall-flying: func() -> bool;
/// Sets whether the player is currently flying due to a fall.
set-fall-flying: func(fall-flying: bool);
/// Returns whether the player is currently on fire. Note: this is only visually like fire, there is no actual logic.
is-on-fire: func() -> bool;
/// Sets whether the player is visually on fire. Note: this is only visually like fire, there is no actual logic.
set-on-fire: func(on-fire: bool);
/// Returns whether the player is currently on the ground.
is-on-ground: func() -> bool;
/// Returns whether the player is currently flying.
is-flying: func() -> bool;
/// Sets whether the player should be flying.
@@ -204,5 +193,13 @@ interface player {
/// Returns the player's IP address.
get-ip: func() -> string;
/// Returns the player's current skin textures.
get-skin: func() -> option<player-skin>;
/// Returns the currently visible skin parts.
get-skin-parts: func() -> skin-parts;
/// Sets which skin parts are visible.
set-skin-parts: func(parts: skin-parts);
}
}

View File

@@ -16,6 +16,18 @@ interface server {
hard,
}
/// Represents the three main Minecraft dimensions.
enum dimension {
overworld,
nether,
end,
}
variant command-sender {
console,
player(player),
}
/// Represents the global server instance.
resource server {
/// Returns the current difficulty level of the server.
@@ -47,6 +59,12 @@ interface server {
/// Gets a world by its dimension name (e.g., "minecraft:overworld").
get-world-by-name: func(name: string) -> option<%world>;
execute-command: func(command: string, sender: command-sender);
/// Creates or loads a world by its name and dimension.
/// If a world with the given name already exists, it will be returned.
create-world: func(name: string, dimension: dimension) -> %world;
/// Broadcasts a system chat message to all players.
broadcast: func(message: string);

View File

@@ -114,10 +114,27 @@ interface %world {
get-head-yaw: func() -> f32;
is-on-ground: func() -> bool;
is-sneaking: func() -> bool;
set-sneaking: func(sneaking: bool);
is-sprinting: func() -> bool;
set-sprinting: func(sprinting: bool);
is-swimming: func() -> bool;
set-swimming: func(swimming: bool);
is-invisible: func() -> bool;
set-invisible: func(invisible: bool);
is-glowing: func() -> bool;
set-glowing: func(glowing: bool);
is-fall-flying: func() -> bool;
set-fall-flying: func(fall-flying: bool);
is-on-fire: func() -> bool;
set-on-fire: func(on-fire: bool);
teleport: func(pos: position, world-ref: %world);
set-velocity: func(velocity: position);
@@ -137,6 +154,27 @@ interface %world {
set-fire-ticks: func(ticks: s32);
remove: func();
/// Performs a raycast from the entity's eye position in its looking direction.
raycast: func(max-distance: f64, fluid-handling: bool) -> option<raycast-result>;
}
/// Result of a raycast operation.
record raycast-result {
/// The block position that was hit.
pos: block-pos,
/// The face of the block that was hit.
face: block-direction,
}
/// Represents a cardinal direction or block face.
enum block-direction {
down,
up,
north,
south,
west,
east,
}
/// Represents a Minecraft world (dimension).

View File

@@ -106,6 +106,21 @@ impl<T> Vector3<T> {
}
}
impl Vector3<f64> {
#[must_use]
pub fn from_yaw_pitch(yaw: f32, pitch: f32) -> Self {
let yaw_rad = f64::from(yaw).to_radians();
let pitch_rad = f64::from(pitch).to_radians();
let cos_pitch = pitch_rad.cos();
let sin_pitch = pitch_rad.sin();
let cos_yaw = yaw_rad.cos();
let sin_yaw = yaw_rad.sin();
Self::new(-cos_pitch * sin_yaw, -sin_pitch, cos_pitch * cos_yaw)
}
}
impl<T: Math + PartialOrd + Copy> Vector3<T> {
/// Calculates the squared length (magnitude) of the vector.
///

View File

@@ -136,6 +136,15 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
self
}
fn get_eye_pos(&self) -> Vector3<f64> {
self.get_entity().get_eye_pos()
}
fn get_looking_vector(&self) -> Vector3<f64> {
let entity = self.get_entity();
Vector3::from_yaw_pitch(entity.yaw.load(), entity.pitch.load())
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();

View File

@@ -88,7 +88,6 @@ use crate::entity::{EntityBaseFuture, NbtFuture, TeleportFuture};
use crate::net::{ClientPlatform, GameProfile};
use crate::net::{DisconnectReason, PlayerConfig};
use crate::plugin::player::exp_change::PlayerExpChangeEvent;
use crate::plugin::player::inventory_close::InventoryCloseEvent;
use crate::plugin::player::inventory_interact::InventoryClickEvent;
use crate::plugin::player::player_change_world::PlayerChangeWorldEvent;
use crate::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent;
@@ -466,14 +465,9 @@ pub struct Player {
pub tab_list_order: AtomicI32,
pub tab_list_latency: AtomicI32,
pub tab_list_listed: AtomicBool,
pub this: Weak<Self>,
}
impl Player {
pub fn this(&self) -> Arc<Self> {
self.this.upgrade().expect("Player should have a reference")
}
#[expect(clippy::too_many_lines)]
pub async fn new(
client: ClientPlatform,
@@ -603,7 +597,6 @@ impl Player {
tab_list_order: AtomicI32::new(0),
tab_list_latency: AtomicI32::new(0),
tab_list_listed: AtomicBool::new(false),
this: Weak::new(),
}
}
@@ -2897,20 +2890,21 @@ impl Player {
}
pub async fn on_handled_screen_closed(&self) {
let window_type = {
let handler_lock = self.current_screen_handler.lock().await;
let mut handler = handler_lock.lock().await;
let wt = handler.window_type();
handler.on_closed(self).await;
wt
};
// let window_type = {
// let handler_lock = self.current_screen_handler.lock().await;
// let mut handler = handler_lock.lock().await;
// let wt = handler.window_type();
// handler.on_closed(self).await;
// wt
// };
if let Some(server) = self.living_entity.entity.world.load().server.upgrade() {
server
.plugin_manager
.fire(InventoryCloseEvent::new(&self.this(), window_type))
.await;
}
// TODO
// if let Some(server) = self.living_entity.entity.world.load().server.upgrade() {
// server
// .plugin_manager
// .fire(InventoryCloseEvent::new(&self, window_type))
// .await;
// }
let player_screen_handler: Arc<Mutex<dyn ScreenHandler>> =
self.player_screen_handler.clone();
@@ -3026,7 +3020,7 @@ impl Player {
}
#[allow(clippy::too_many_lines)]
pub async fn on_slot_click(&self, packet: SClickSlot, server: &Server) {
pub async fn on_slot_click(self: &Arc<Self>, packet: SClickSlot, server: &Server) {
self.update_last_action_time();
let screen_handler_arc = self.current_screen_handler.lock().await.clone();
let mut screen_handler = screen_handler_arc.lock().await;
@@ -3050,7 +3044,7 @@ impl Player {
return;
}
if !screen_handler.can_use(self) {
if !screen_handler.can_use(self.as_ref()) {
warn!(
"Player {} interacted with invalid menu {:?}",
self.gameprofile.name,
@@ -3133,7 +3127,7 @@ impl Player {
send_cancellable! {{
server;
InventoryClickEvent::new(
&self.this(),
self,
screen_handler.window_type(),
click_type,
slot,
@@ -3144,7 +3138,7 @@ impl Player {
);
'after: {}
'cancelled: {
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
}}
@@ -3158,48 +3152,48 @@ impl Player {
if is_container_slot {
if !cursor_stack.is_empty() && !allow_put_items {
drop(cursor_stack);
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
if cursor_stack.is_empty() && !allow_grab_items {
drop(cursor_stack);
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
}
}
SlotActionType::QuickMove => {
if is_container_slot && !allow_grab_items {
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
if !is_container_slot && !allow_put_items {
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
}
SlotActionType::Swap => {
if is_container_slot && (!allow_grab_items || !allow_put_items) {
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
}
SlotActionType::Throw => {
if is_container_slot && !allow_grab_items {
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
}
SlotActionType::QuickCraft => {
if !allow_put_items {
// Dragging items into slots
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
}
SlotActionType::PickupAll => {
if !allow_grab_items {
screen_handler.cancel(self).await;
screen_handler.cancel().await;
return;
}
}
@@ -3218,7 +3212,7 @@ impl Player {
i32::from(slot),
i32::from(packet.button),
packet.mode.clone(),
self,
self.as_ref(),
)
.await;

View File

@@ -1,3 +1,4 @@
use std::sync::Arc;
use wasmtime::component::Resource;
use crate::plugin::loader::wasm::wasm_host::{
@@ -7,8 +8,11 @@ use crate::plugin::loader::wasm::wasm_host::{
common::{EntityPose, Position},
entity::Host,
text::TextComponent,
world::{Entity, HostEntity, World},
world::{
BlockPos as WitBlockPos, Entity, HostEntity, RaycastResult as WitRaycastResult, World,
},
},
wit::v0_1::world::to_wasm_block_direction,
};
use pumpkin_data::entity::EntityPose as InternalEntityPose;
@@ -176,6 +180,100 @@ impl HostEntity for PluginHostState {
Ok(to_wasm_position(entity.get_entity().velocity.load()))
}
async fn set_sneaking(
&mut self,
entity: Resource<Entity>,
sneaking: bool,
) -> wasmtime::Result<()> {
let entity = entity_from_resource(self, &entity)?;
entity.get_entity().set_sneaking(sneaking).await;
Ok(())
}
async fn set_sprinting(
&mut self,
entity: Resource<Entity>,
sprinting: bool,
) -> wasmtime::Result<()> {
let entity = entity_from_resource(self, &entity)?;
entity.get_entity().set_sprinting(sprinting).await;
Ok(())
}
async fn is_swimming(&mut self, entity: Resource<Entity>) -> wasmtime::Result<bool> {
let entity = entity_from_resource(self, &entity)?;
Ok(entity
.get_entity()
.swimming
.load(std::sync::atomic::Ordering::Relaxed))
}
async fn set_swimming(
&mut self,
entity: Resource<Entity>,
swimming: bool,
) -> wasmtime::Result<()> {
let entity = entity_from_resource(self, &entity)?;
entity.get_entity().set_swimming(swimming).await;
Ok(())
}
async fn set_invisible(
&mut self,
entity: Resource<Entity>,
invisible: bool,
) -> wasmtime::Result<()> {
let entity = entity_from_resource(self, &entity)?;
entity.get_entity().set_invisible(invisible).await;
Ok(())
}
async fn set_glowing(
&mut self,
entity: Resource<Entity>,
glowing: bool,
) -> wasmtime::Result<()> {
let entity = entity_from_resource(self, &entity)?;
entity.get_entity().set_glowing(glowing).await;
Ok(())
}
async fn is_fall_flying(&mut self, entity: Resource<Entity>) -> wasmtime::Result<bool> {
let entity = entity_from_resource(self, &entity)?;
Ok(entity
.get_entity()
.fall_flying
.load(std::sync::atomic::Ordering::Relaxed))
}
async fn set_fall_flying(
&mut self,
entity: Resource<Entity>,
fall_flying: bool,
) -> wasmtime::Result<()> {
let entity = entity_from_resource(self, &entity)?;
entity.get_entity().set_fall_flying(fall_flying).await;
Ok(())
}
async fn is_on_fire(&mut self, entity: Resource<Entity>) -> wasmtime::Result<bool> {
let entity = entity_from_resource(self, &entity)?;
Ok(entity
.get_entity()
.has_visual_fire
.load(std::sync::atomic::Ordering::Relaxed))
}
async fn set_on_fire(
&mut self,
entity: Resource<Entity>,
on_fire: bool,
) -> wasmtime::Result<()> {
let entity = entity_from_resource(self, &entity)?;
entity.get_entity().set_on_fire(on_fire).await;
Ok(())
}
async fn get_pose(&mut self, entity: Resource<Entity>) -> wasmtime::Result<EntityPose> {
let entity = entity_from_resource(self, &entity)?;
Ok(map_entity_pose(entity.get_entity().pose.load()))
@@ -295,6 +393,43 @@ impl HostEntity for PluginHostState {
Ok(())
}
async fn raycast(
&mut self,
entity: Resource<Entity>,
max_distance: f64,
_fluid_handling: bool,
) -> wasmtime::Result<Option<WitRaycastResult>> {
let entity = entity_from_resource(self, &entity)?;
let start = entity.get_eye_pos();
let direction = entity.get_looking_vector();
let end = start + direction * max_distance;
let world = entity.get_entity().world.load_full();
let hit = world
.raycast(
start,
end,
|pos: &pumpkin_util::math::position::BlockPos, w: &Arc<crate::world::World>| {
let pos = *pos;
let world = w.clone();
async move {
let block = world.get_block_state(&pos).await;
!block.is_air()
}
},
)
.await;
Ok(hit.map(|(pos, face)| WitRaycastResult {
pos: WitBlockPos {
x: pos.0.x,
y: pos.0.y,
z: pos.0.z,
},
face: to_wasm_block_direction(face),
}))
}
async fn drop(&mut self, rep: Resource<Entity>) -> wasmtime::Result<()> {
let _ = self
.resource_table

View File

@@ -16,14 +16,18 @@ use crate::{
events::{
from_wasm_game_mode, from_wasm_position, to_wasm_game_mode, to_wasm_position,
},
pumpkin::{self, plugin::player::Player, plugin::world::World},
pumpkin::{
self,
plugin::player::{Player, PlayerSkin, SkinParts},
plugin::world::World,
},
},
},
};
use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_util::permission::PermissionLvl;
fn player_from_resource(
pub fn player_from_resource(
state: &PluginHostState,
player: &Resource<Player>,
) -> wasmtime::Result<std::sync::Arc<crate::entity::player::Player>> {
@@ -657,116 +661,6 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(())
}
async fn is_sneaking(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().sneaking.load(Ordering::Relaxed))
}
async fn set_sneaking(
&mut self,
player: Resource<Player>,
sneaking: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_sneaking(sneaking).await;
Ok(())
}
async fn is_sprinting(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().sprinting.load(Ordering::Relaxed))
}
async fn set_sprinting(
&mut self,
player: Resource<Player>,
sprinting: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_sprinting(sprinting).await;
Ok(())
}
async fn is_swimming(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().swimming.load(Ordering::Relaxed))
}
async fn set_swimming(
&mut self,
player: Resource<Player>,
swimming: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_swimming(swimming).await;
Ok(())
}
async fn is_invisible(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().invisible.load(Ordering::Relaxed))
}
async fn set_invisible(
&mut self,
player: Resource<Player>,
invisible: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_invisible(invisible).await;
Ok(())
}
async fn is_glowing(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().glowing.load(Ordering::Relaxed))
}
async fn set_glowing(
&mut self,
player: Resource<Player>,
glowing: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_glowing(glowing).await;
Ok(())
}
async fn is_fall_flying(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().fall_flying.load(Ordering::Relaxed))
}
async fn set_fall_flying(
&mut self,
player: Resource<Player>,
fall_flying: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_fall_flying(fall_flying).await;
Ok(())
}
async fn is_on_fire(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().has_visual_fire.load(Ordering::Relaxed))
}
async fn set_on_fire(
&mut self,
player: Resource<Player>,
on_fire: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_on_fire(on_fire).await;
Ok(())
}
async fn is_on_ground(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().on_ground.load(Ordering::Relaxed))
}
async fn is_flying(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.is_flying().await)
@@ -824,6 +718,85 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(player.get_ip().await)
}
async fn get_skin(&mut self, player: Resource<Player>) -> wasmtime::Result<Option<PlayerSkin>> {
let player = player_from_resource(self, &player)?;
Ok(player
.gameprofile
.properties
.iter()
.find(|p| p.name == "textures")
.map(|p| PlayerSkin {
value: p.value.clone(),
signature: p.signature.clone(),
}))
}
async fn get_skin_parts(&mut self, player: Resource<Player>) -> wasmtime::Result<SkinParts> {
let player = player_from_resource(self, &player)?;
let mask = player.config.load().skin_parts;
let mut parts = SkinParts::empty();
if mask & 0x01 != 0 {
parts |= SkinParts::CAPE;
}
if mask & 0x02 != 0 {
parts |= SkinParts::JACKET;
}
if mask & 0x04 != 0 {
parts |= SkinParts::LEFT_SLEEVE;
}
if mask & 0x08 != 0 {
parts |= SkinParts::RIGHT_SLEEVE;
}
if mask & 0x10 != 0 {
parts |= SkinParts::LEFT_PANTS_LEG;
}
if mask & 0x20 != 0 {
parts |= SkinParts::RIGHT_PANTS_LEG;
}
if mask & 0x40 != 0 {
parts |= SkinParts::HAT;
}
Ok(parts)
}
async fn set_skin_parts(
&mut self,
player: Resource<Player>,
parts: SkinParts,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let mut mask = 0u8;
if parts.contains(SkinParts::CAPE) {
mask |= 0x01;
}
if parts.contains(SkinParts::JACKET) {
mask |= 0x02;
}
if parts.contains(SkinParts::LEFT_SLEEVE) {
mask |= 0x04;
}
if parts.contains(SkinParts::RIGHT_SLEEVE) {
mask |= 0x08;
}
if parts.contains(SkinParts::LEFT_PANTS_LEG) {
mask |= 0x10;
}
if parts.contains(SkinParts::RIGHT_PANTS_LEG) {
mask |= 0x20;
}
if parts.contains(SkinParts::HAT) {
mask |= 0x40;
}
{
let mut config = (**player.config.load()).clone();
config.skin_parts = mask;
player.config.store(Arc::new(config));
};
player.send_client_information().await;
Ok(())
}
async fn drop(&mut self, rep: Resource<Player>) -> wasmtime::Result<()> {
let _ = self
.resource_table

View File

@@ -2,6 +2,9 @@ use pumpkin_util::text::TextComponent;
use uuid::Uuid;
use wasmtime::component::Resource;
use crate::command::CommandSender;
use pumpkin::plugin::server::CommandSender as WasmCommandSender;
use super::player::text_component_from_resource;
use crate::plugin::loader::wasm::wasm_host::{
state::{PluginHostState, ServerResource},
@@ -9,7 +12,7 @@ use crate::plugin::loader::wasm::wasm_host::{
self,
plugin::{
player::Player,
server::{Difficulty, Server},
server::{Difficulty, Dimension, Server},
},
},
};
@@ -156,6 +159,28 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
}))
}
async fn create_world(
&mut self,
_rep: Resource<Server>,
name: String,
dimension: Dimension,
) -> wasmtime::Result<Resource<pumpkin::plugin::world::World>> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
let internal_dim = match dimension {
Dimension::Overworld => pumpkin_data::dimension::Dimension::OVERWORLD,
Dimension::Nether => pumpkin_data::dimension::Dimension::THE_NETHER,
Dimension::End => pumpkin_data::dimension::Dimension::THE_END,
};
let world = server.create_world(name, internal_dim).await;
self.add_world(world)
.map_err(|_| wasmtime::Error::msg("failed to add world resource"))
}
async fn broadcast(&mut self, _rep: Resource<Server>, message: String) -> wasmtime::Result<()> {
let server = self
.server
@@ -192,6 +217,39 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
Ok(())
}
async fn execute_command(
&mut self,
_rep: Resource<Server>,
command: String,
sender: WasmCommandSender,
) -> wasmtime::Result<()> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
let native_sender = match sender {
WasmCommandSender::Console => CommandSender::Console,
WasmCommandSender::Player(player_res) => {
// Extract the native Player reference from the WASM resource
let player_resource =
self.resource_table
.get::<crate::plugin::loader::wasm::wasm_host::state::PlayerResource>(
&Resource::new_own(player_res.rep()),
)?;
CommandSender::Player(player_resource.provider.clone())
}
};
let dispatcher = server.command_dispatcher.read().await;
dispatcher
.handle_command(&native_sender.into_source(server).await, &command)
.await;
Ok(())
}
async fn get_max_players(&mut self, _rep: Resource<Server>) -> wasmtime::Result<u32> {
let server = self
.server

View File

@@ -1,3 +1,4 @@
use pumpkin_data::BlockDirection as InternalBlockDirection;
use pumpkin_data::block_state::PistonBehavior;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::world::{BlockFlags, SimpleWorld};
@@ -5,8 +6,8 @@ use std::sync::Arc;
use wasmtime::component::Resource;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::{
BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState,
PistonBehavior as WitPistonBehavior,
BlockDirection as WitBlockDirection, BlockFlags as WitBlockFlags, BlockPos as WitBlockPos,
BlockState as WitBlockState, PistonBehavior as WitPistonBehavior,
};
use crate::plugin::loader::wasm::wasm_host::{
state::{PluginHostState, TextComponentResource, WorldResource},
@@ -14,6 +15,17 @@ use crate::plugin::loader::wasm::wasm_host::{
};
use crate::world::explosion::Explosion;
pub(crate) const fn to_wasm_block_direction(dir: InternalBlockDirection) -> WitBlockDirection {
match dir {
InternalBlockDirection::Down => WitBlockDirection::Down,
InternalBlockDirection::Up => WitBlockDirection::Up,
InternalBlockDirection::North => WitBlockDirection::North,
InternalBlockDirection::South => WitBlockDirection::South,
InternalBlockDirection::West => WitBlockDirection::West,
InternalBlockDirection::East => WitBlockDirection::East,
}
}
// --- Trapping Helpers ---
impl PluginHostState {
fn get_world_res(&self, res: &Resource<World>) -> wasmtime::Result<&WorldResource> {

View File

@@ -341,6 +341,54 @@ impl Server {
.unwrap()
}
pub async fn create_world(self: &Arc<Self>, name: String, dimension: Dimension) -> Arc<World> {
{
let worlds = self.worlds.load();
if let Some(world) = worlds
.iter()
.find(|w| w.get_world_name() == name && w.dimension == dimension)
{
return world.clone();
}
}
let server = self.clone();
let name_clone = name.clone();
tokio::task::spawn_blocking(move || {
let world_path = server.basic_config.get_world_path().join(name_clone);
let registry = server.block_registry.clone();
let l_info = server.level_info.clone();
let weak = Arc::downgrade(&server);
let config = Arc::new(server.advanced_config.world.clone());
let seed = server.level_info.load().world_gen_settings.seed;
// TODO: gen_pool should be reused
let world = World::load(
pumpkin_world::dimension::into_level(
dimension,
&config,
world_path,
registry.clone(),
seed,
None,
),
l_info,
dimension,
registry,
weak,
);
let world_arc = Arc::new(world);
server.worlds.rcu(|worlds| {
let mut new_worlds = (**worlds).clone();
new_worlds.push(world_arc.clone());
new_worlds
});
world_arc
})
.await
.expect("World creation panicked")
}
/// Adds a new player to the server.
///
/// This function takes an `Arc<Client>` representing the connected client and performs the following actions:
@@ -430,8 +478,7 @@ impl Server {
}
// Wrap in Arc after data is loaded
let mut player = Arc::new(player);
Arc::get_mut(&mut player).unwrap().this = Arc::downgrade(&player);
let player = Arc::new(player);
send_cancellable! {{
self;

View File

@@ -381,12 +381,11 @@ pub async fn spawn_category_for_position(
chunk_pos: &Vector2<i32>,
spawn_state: &SpawnState,
) {
// TODO StructureManager structureManager = level.structureManager();
// TODO blockState.isRedstoneConductor(chunk, pos) is true then return
let mut batch_buffer = vec![];
let mut spawn_cluster_size = 0;
let mut new_pos = pos;
let player_positions: Vec<_> = world.players.load().iter().map(|p| p.position()).collect();
for _ in 0..3 {
let mut new_x = new_pos.0.x;
let mut new_z = new_pos.0.z;
@@ -398,18 +397,27 @@ pub async fn spawn_category_for_position(
new_x += rng().random_range(0..6) - rng().random_range(0..6);
new_z += rng().random_range(0..6) - rng().random_range(0..6);
new_pos = BlockPos::new(new_x, new_pos.0.y, new_z);
let new_pos_center = new_pos.to_centered_f64();
let player_distance = get_nearest_player(&new_pos_center, &player_positions);
let spawn_pos_f64 = Vector3::new(
f64::from(new_pos.0.x) + 0.5,
f64::from(new_pos.0.y),
f64::from(new_pos.0.z) + 0.5,
);
let player_distance = get_nearest_player(&spawn_pos_f64, &player_positions);
if !is_right_distance_to_player_and_spawn_point(&new_pos, player_distance, chunk_pos) {
inc += 1;
continue;
}
let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else {
break 'outer;
};
random_group_size = rng().random_range(spawner.min_count..=spawner.max_count);
let entity_type =
&EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap();
if !is_valid_spawn_position_for_type(
world,
&new_pos,
@@ -426,21 +434,19 @@ pub async fn spawn_category_for_position(
inc += 1;
continue;
}
let entity = from_type(entity_type, new_pos_center, world, Uuid::new_v4()).await;
let entity = from_type(entity_type, spawn_pos_f64, world, Uuid::new_v4()).await;
entity
.get_entity()
.set_rotation(rng().random::<f32>() * 360., 0.);
// TODO isValidPositionForMob(level, mob, f)
// TODO spawnGroupData = mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.NATURAL, spawnGroupData);
spawn_cluster_size += 1;
//group_size += 1;
batch_buffer.push(entity);
spawn_state.after_spawn(entity_type, &new_pos, world).await;
if spawn_cluster_size >= entity_type.limit_per_chunk {
return;
}
//TODO mob.isMaxGroupSizeReached(p)
inc += 1;
}
}
@@ -601,14 +607,19 @@ pub async fn is_spawn_position_ok(
SpawnLocation::InLava => world.get_fluid(block_pos).await.has_tag(&MINECRAFT_LAVA),
SpawnLocation::InWater => {
// TODO !level.getBlockState(blockPos).isRedstoneConductor(level, blockPos)
let above_state = world.get_block_state(&block_pos.up()).await;
world.get_fluid(block_pos).await.has_tag(&MINECRAFT_WATER)
&& !above_state.is_full_cube()
}
SpawnLocation::OnGround => {
let down = world.get_block_state(&block_pos.down()).await;
let up = world.get_block_state(&block_pos.up()).await;
let cur = world.get_block_state(block_pos).await;
// TODO: blockState.allowsSpawning
if down.is_side_solid(BlockDirection::Up) {
let is_valid_spawn_below =
down.is_side_solid(BlockDirection::Up) && down.luminance < 14;
if is_valid_spawn_below {
is_valid_empty_spawn_block(cur) && is_valid_empty_spawn_block(up)
} else {
false
@@ -620,10 +631,20 @@ pub async fn is_spawn_position_ok(
#[must_use]
pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool {
// TODO: emitsRedstonePower
if state.is_full_cube() || state.is_liquid() {
if state.is_full_cube() {
return false;
}
// TODO !entityType.isBlockDangerous(blockState);
!Block::from_state_id(state.id).has_tag(&MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE)
// if state.is_signal_source() {
// return false;
// }
if state.is_liquid() {
return false;
}
if Block::from_state_id(state.id).has_tag(&MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE) {
return false;
}
// TODO: !entityType.isBlockDangerous(blockState)
// (e.g., preventing spawns inside Sweet Berry Bushes, Wither Roses, or Fire)
true
}