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. /// 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 { Box::pin(async move {
self.sync_state().await; self.sync_state().await;
}) })

View File

@ -25,6 +25,25 @@ interface player {
walk-speed: f32, 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. /// A handle to a player on the server.
resource player { resource player {
/// Returns the player as an entity. /// Returns the player as an entity.
@ -162,36 +181,6 @@ interface player {
/// Adds experience points to the player. /// Adds experience points to the player.
add-experience-points: func(points: s32); 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. /// Returns whether the player is currently flying.
is-flying: func() -> bool; is-flying: func() -> bool;
/// Sets whether the player should be flying. /// Sets whether the player should be flying.
@ -204,5 +193,13 @@ interface player {
/// Returns the player's IP address. /// Returns the player's IP address.
get-ip: func() -> string; 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, hard,
} }
/// Represents the three main Minecraft dimensions.
enum dimension {
overworld,
nether,
end,
}
variant command-sender {
console,
player(player),
}
/// Represents the global server instance. /// Represents the global server instance.
resource server { resource server {
/// Returns the current difficulty level of the 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"). /// Gets a world by its dimension name (e.g., "minecraft:overworld").
get-world-by-name: func(name: string) -> option<%world>; 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. /// Broadcasts a system chat message to all players.
broadcast: func(message: string); broadcast: func(message: string);

View File

@ -114,10 +114,27 @@ interface %world {
get-head-yaw: func() -> f32; get-head-yaw: func() -> f32;
is-on-ground: func() -> bool; is-on-ground: func() -> bool;
is-sneaking: func() -> bool; is-sneaking: func() -> bool;
set-sneaking: func(sneaking: bool);
is-sprinting: func() -> bool; is-sprinting: func() -> bool;
set-sprinting: func(sprinting: bool);
is-swimming: func() -> bool;
set-swimming: func(swimming: bool);
is-invisible: func() -> bool; is-invisible: func() -> bool;
set-invisible: func(invisible: bool);
is-glowing: func() -> 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); teleport: func(pos: position, world-ref: %world);
set-velocity: func(velocity: position); set-velocity: func(velocity: position);
@ -137,6 +154,27 @@ interface %world {
set-fire-ticks: func(ticks: s32); set-fire-ticks: func(ticks: s32);
remove: func(); 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). /// 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> { impl<T: Math + PartialOrd + Copy> Vector3<T> {
/// Calculates the squared length (magnitude) of the vector. /// Calculates the squared length (magnitude) of the vector.
/// ///

View File

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

View File

@ -1,3 +1,4 @@
use std::sync::Arc;
use wasmtime::component::Resource; use wasmtime::component::Resource;
use crate::plugin::loader::wasm::wasm_host::{ use crate::plugin::loader::wasm::wasm_host::{
@ -7,8 +8,11 @@ use crate::plugin::loader::wasm::wasm_host::{
common::{EntityPose, Position}, common::{EntityPose, Position},
entity::Host, entity::Host,
text::TextComponent, 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; use pumpkin_data::entity::EntityPose as InternalEntityPose;
@ -176,6 +180,100 @@ impl HostEntity for PluginHostState {
Ok(to_wasm_position(entity.get_entity().velocity.load())) 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> { async fn get_pose(&mut self, entity: Resource<Entity>) -> wasmtime::Result<EntityPose> {
let entity = entity_from_resource(self, &entity)?; let entity = entity_from_resource(self, &entity)?;
Ok(map_entity_pose(entity.get_entity().pose.load())) Ok(map_entity_pose(entity.get_entity().pose.load()))
@ -295,6 +393,43 @@ impl HostEntity for PluginHostState {
Ok(()) 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<()> { async fn drop(&mut self, rep: Resource<Entity>) -> wasmtime::Result<()> {
let _ = self let _ = self
.resource_table .resource_table

View File

@ -16,14 +16,18 @@ use crate::{
events::{ events::{
from_wasm_game_mode, from_wasm_position, to_wasm_game_mode, to_wasm_position, 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_inventory::player::player_inventory::PlayerInventory;
use pumpkin_util::permission::PermissionLvl; use pumpkin_util::permission::PermissionLvl;
fn player_from_resource( pub fn player_from_resource(
state: &PluginHostState, state: &PluginHostState,
player: &Resource<Player>, player: &Resource<Player>,
) -> wasmtime::Result<std::sync::Arc<crate::entity::player::Player>> { ) -> wasmtime::Result<std::sync::Arc<crate::entity::player::Player>> {
@ -657,116 +661,6 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(()) 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> { async fn is_flying(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?; let player = player_from_resource(self, &player)?;
Ok(player.is_flying().await) Ok(player.is_flying().await)
@ -824,6 +718,85 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(player.get_ip().await) 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<()> { async fn drop(&mut self, rep: Resource<Player>) -> wasmtime::Result<()> {
let _ = self let _ = self
.resource_table .resource_table

View File

@ -2,6 +2,9 @@ use pumpkin_util::text::TextComponent;
use uuid::Uuid; use uuid::Uuid;
use wasmtime::component::Resource; use wasmtime::component::Resource;
use crate::command::CommandSender;
use pumpkin::plugin::server::CommandSender as WasmCommandSender;
use super::player::text_component_from_resource; use super::player::text_component_from_resource;
use crate::plugin::loader::wasm::wasm_host::{ use crate::plugin::loader::wasm::wasm_host::{
state::{PluginHostState, ServerResource}, state::{PluginHostState, ServerResource},
@ -9,7 +12,7 @@ use crate::plugin::loader::wasm::wasm_host::{
self, self,
plugin::{ plugin::{
player::Player, 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<()> { async fn broadcast(&mut self, _rep: Resource<Server>, message: String) -> wasmtime::Result<()> {
let server = self let server = self
.server .server
@ -192,6 +217,39 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
Ok(()) 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> { async fn get_max_players(&mut self, _rep: Resource<Server>) -> wasmtime::Result<u32> {
let server = self let server = self
.server .server

View File

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

View File

@ -341,6 +341,54 @@ impl Server {
.unwrap() .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. /// Adds a new player to the server.
/// ///
/// This function takes an `Arc<Client>` representing the connected client and performs the following actions: /// 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 // Wrap in Arc after data is loaded
let mut player = Arc::new(player); let player = Arc::new(player);
Arc::get_mut(&mut player).unwrap().this = Arc::downgrade(&player);
send_cancellable! {{ send_cancellable! {{
self; self;

View File

@ -381,12 +381,11 @@ pub async fn spawn_category_for_position(
chunk_pos: &Vector2<i32>, chunk_pos: &Vector2<i32>,
spawn_state: &SpawnState, spawn_state: &SpawnState,
) { ) {
// TODO StructureManager structureManager = level.structureManager();
// TODO blockState.isRedstoneConductor(chunk, pos) is true then return
let mut batch_buffer = vec![]; let mut batch_buffer = vec![];
let mut spawn_cluster_size = 0; let mut spawn_cluster_size = 0;
let mut new_pos = pos; let mut new_pos = pos;
let player_positions: Vec<_> = world.players.load().iter().map(|p| p.position()).collect(); let player_positions: Vec<_> = world.players.load().iter().map(|p| p.position()).collect();
for _ in 0..3 { for _ in 0..3 {
let mut new_x = new_pos.0.x; let mut new_x = new_pos.0.x;
let mut new_z = new_pos.0.z; 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_x += rng().random_range(0..6) - rng().random_range(0..6);
new_z += 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); 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) { if !is_right_distance_to_player_and_spawn_point(&new_pos, player_distance, chunk_pos) {
inc += 1; inc += 1;
continue; continue;
} }
let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else { let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else {
break 'outer; break 'outer;
}; };
random_group_size = rng().random_range(spawner.min_count..=spawner.max_count); random_group_size = rng().random_range(spawner.min_count..=spawner.max_count);
let entity_type = let entity_type =
&EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap(); &EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap();
if !is_valid_spawn_position_for_type( if !is_valid_spawn_position_for_type(
world, world,
&new_pos, &new_pos,
@ -426,21 +434,19 @@ pub async fn spawn_category_for_position(
inc += 1; inc += 1;
continue; 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 entity
.get_entity() .get_entity()
.set_rotation(rng().random::<f32>() * 360., 0.); .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; spawn_cluster_size += 1;
//group_size += 1;
batch_buffer.push(entity); batch_buffer.push(entity);
spawn_state.after_spawn(entity_type, &new_pos, world).await; spawn_state.after_spawn(entity_type, &new_pos, world).await;
if spawn_cluster_size >= entity_type.limit_per_chunk { if spawn_cluster_size >= entity_type.limit_per_chunk {
return; return;
} }
//TODO mob.isMaxGroupSizeReached(p)
inc += 1; 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::InLava => world.get_fluid(block_pos).await.has_tag(&MINECRAFT_LAVA),
SpawnLocation::InWater => { SpawnLocation::InWater => {
// TODO !level.getBlockState(blockPos).isRedstoneConductor(level, blockPos) // 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) world.get_fluid(block_pos).await.has_tag(&MINECRAFT_WATER)
&& !above_state.is_full_cube()
} }
SpawnLocation::OnGround => { SpawnLocation::OnGround => {
let down = world.get_block_state(&block_pos.down()).await; let down = world.get_block_state(&block_pos.down()).await;
let up = world.get_block_state(&block_pos.up()).await; let up = world.get_block_state(&block_pos.up()).await;
let cur = world.get_block_state(block_pos).await; let cur = world.get_block_state(block_pos).await;
// TODO: blockState.allowsSpawning // 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) is_valid_empty_spawn_block(cur) && is_valid_empty_spawn_block(up)
} else { } else {
false false
@ -620,10 +631,20 @@ pub async fn is_spawn_position_ok(
#[must_use] #[must_use]
pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool { pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool {
// TODO: emitsRedstonePower if state.is_full_cube() {
if state.is_full_cube() || state.is_liquid() {
return false; return false;
} }
// TODO !entityType.isBlockDangerous(blockState); // if state.is_signal_source() {
!Block::from_state_id(state.id).has_tag(&MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE) // 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
} }