chore: some more plugin api features

This commit is contained in:
Alexander Medvedev
2026-04-03 20:28:03 +02:00
parent 1437f33864
commit 18db9551ea
7 changed files with 418 additions and 4 deletions

View File

@@ -44,6 +44,10 @@ interface player {
teleport: func (position: position, yaw: f32, pitch: f32) -> result<_, string>;
teleport-world: func (world-ref: %world, position: position, yaw: option<f32>, pitch: option<f32>) -> result<_, string>;
kick: func (message: text-component) -> result<_, string>;
respawn: func () -> result<_, string>;
ban: func (reason: option<text-component>) -> result<_, string>;
ban-ip: func (reason: option<text-component>) -> result<_, string>;
// Inventory
get-selected-slot: func () -> result<u8, string>;
@@ -52,9 +56,17 @@ interface player {
// Status
get-health: func () -> result<f32, string>;
set-health: func (health: f32) -> result<_, string>;
get-max-health: func () -> result<f32, string>;
set-max-health: func (max-health: f32) -> result<_, string>;
get-food-level: func () -> result<u8, string>;
set-food-level: func (food-level: u8) -> result<_, string>;
get-saturation: func () -> result<f32, string>;
set-saturation: func (saturation: f32) -> result<_, string>;
get-exhaustion: func () -> result<f32, string>;
set-exhaustion: func (exhaustion: f32) -> result<_, string>;
get-absorption: func () -> result<f32, string>;
set-absorption: func (absorption: f32) -> result<_, string>;
get-experience-level: func() -> result<s32, string>;
get-experience-progress: func() -> result<f32, string>;
@@ -66,12 +78,16 @@ interface player {
add-experience-points: func(points: s32) -> result<_, string>;
is-sneaking: func() -> result<bool, string>;
set-sneaking: func(sneaking: bool) -> result<_, string>;
is-sprinting: func() -> result<bool, string>;
set-sprinting: func(sprinting: bool) -> result<_, string>;
is-on-ground: func() -> result<bool, string>;
is-flying: func() -> result<bool, string>;
set-flying: func(flying: bool) -> result<_, string>;
get-abilities: func() -> result<player-abilities, string>;
set-abilities: func(abilities: player-abilities) -> result<_, string>;
get-ip: func() -> result<string, string>;
}
}

View File

@@ -1,4 +1,7 @@
interface %world {
use common.{position};
use text.{text-component};
enum piston-behavior {
normal,
destroy,
@@ -39,11 +42,33 @@ interface %world {
resource %world {
get-id: func() -> string;
get-block-state-id: func(pos: block-pos) -> u16;
get-block-state: func(pos: block-pos) -> block-state;
set-block-state: func(pos: block-pos, state: u16, update-flags: block-flags);
get-time-of-day: func() -> u64;
set-time-of-day: func(time: u64);
get-world-age: func() -> u64;
get-dimension: func() -> string;
get-top-block-y: func(x: s32, z: s32) -> s32;
get-motion-blocking-height: func(x: s32, z: s32) -> s32;
is-raining: func() -> bool;
set-raining: func(raining: bool);
is-thundering: func() -> bool;
set-thundering: func(thundering: bool);
broadcast-system-message: func(message: text-component, overlay: bool);
}
}
}

View File

@@ -150,6 +150,23 @@ impl HungerManager {
.store((current + saturation).min(f32::from(self.level.load())));
}
pub fn set_level(&self, level: u8) {
self.level.store(level.min(MAX_FOOD));
}
pub fn set_saturation(&self, saturation: f32) {
self.saturation
.store(saturation.min(f32::from(self.level.load())));
}
pub fn get_exhaustion(&self) -> f32 {
self.exhaustion.load()
}
pub fn set_exhaustion(&self, exhaustion: f32) {
self.exhaustion.store(exhaustion.min(MAX_EXHAUSTION));
}
pub fn restart(&self) {
self.level.store(MAX_FOOD);
self.saturation.store(5.0);

View File

@@ -83,6 +83,7 @@ use crate::block::blocks::bed::BedBlock;
use crate::command::context::command_source::CommandSource;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::{CommandSender, client_suggestions};
use crate::data::SaveJSONConfiguration;
use crate::entity::{EntityBaseFuture, NbtFuture, TeleportFuture};
use crate::net::{ClientPlatform, GameProfile};
use crate::net::{DisconnectReason, PlayerConfig};
@@ -2119,6 +2120,114 @@ impl Player {
self.send_health().await;
}
pub async fn set_max_health(&self, max_health: f32) {
self.living_entity.set_max_health(max_health).await;
self.send_health().await;
}
pub async fn set_food_level(&self, food_level: u8) {
self.hunger_manager.set_level(food_level);
self.send_health().await;
}
pub async fn set_saturation(&self, saturation: f32) {
self.hunger_manager.set_saturation(saturation);
self.send_health().await;
}
pub fn get_exhaustion(&self) -> f32 {
self.hunger_manager.get_exhaustion()
}
pub async fn set_exhaustion(&self, exhaustion: f32) {
self.hunger_manager.set_exhaustion(exhaustion);
self.send_health().await;
}
pub fn get_absorption(&self) -> f32 {
self.living_entity.get_absorption()
}
pub async fn set_absorption(&self, absorption: f32) {
self.living_entity.set_absorption(absorption).await;
}
pub async fn get_ip(&self) -> String {
self.client.address().await.to_string()
}
pub async fn respawn(self: &Arc<Self>) {
self.world().respawn_player(self, false).await;
}
pub async fn ban(&self, server: &Server, reason: Option<TextComponent>) {
let mut banned_players = server.data.banned_player_list.write().await;
let string_reason = reason
.clone()
.map_or_else(|| "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text);
if banned_players
.banned_players
.iter()
.any(|entry| entry.uuid == self.gameprofile.id)
{
return;
}
banned_players.banned_players.push(
crate::data::banlist_serializer::BannedPlayerEntry::new(
&self.gameprofile,
"Plugin".to_string(),
None,
string_reason,
),
);
banned_players.save();
drop(banned_players);
let kick_reason = reason.unwrap_or_else(|| {
TextComponent::translate(translation::MULTIPLAYER_DISCONNECT_BANNED, [])
});
self.kick(DisconnectReason::Kicked, kick_reason).await;
}
pub async fn ban_ip(&self, server: &Server, reason: Option<TextComponent>) {
let mut banned_ips = server.data.banned_ip_list.write().await;
let string_reason = reason
.clone()
.map_or_else(|| "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text);
let target_ip = self.client.address().await.ip();
if banned_ips.get_entry(&target_ip).is_some() {
return;
}
banned_ips
.banned_ips
.push(crate::data::banlist_serializer::BannedIpEntry::new(
target_ip,
"Plugin".to_string(),
None,
string_reason,
));
banned_ips.save();
drop(banned_ips);
let kick_reason = reason.unwrap_or_else(|| {
TextComponent::translate(translation::MULTIPLAYER_DISCONNECT_IP_BANNED, [])
});
let affected = server.get_players_by_ip(target_ip).await;
for target in affected {
target
.kick(DisconnectReason::Kicked, kick_reason.clone())
.await;
}
}
pub fn tick_client_load_timeout(&self) {
if !self.client_loaded.load(Ordering::Relaxed) {
let timeout = self.client_loaded_timeout.load(Ordering::Relaxed);

View File

@@ -331,6 +331,46 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(())
}
async fn respawn(&mut self, player: Resource<Player>) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.respawn().await;
Ok(())
}
async fn ban(
&mut self,
player: Resource<Player>,
reason: Option<Resource<pumpkin::plugin::text::TextComponent>>,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
let Some(server) = self.server.as_ref() else {
return Err("server not available".to_string());
};
let reason = match reason {
Some(r) => Some(text_component_from_resource(self, &r)?),
None => None,
};
player.ban(server, reason).await;
Ok(())
}
async fn ban_ip(
&mut self,
player: Resource<Player>,
reason: Option<Resource<pumpkin::plugin::text::TextComponent>>,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
let Some(server) = self.server.as_ref() else {
return Err("server not available".to_string());
};
let reason = match reason {
Some(r) => Some(text_component_from_resource(self, &r)?),
None => None,
};
player.ban_ip(server, reason).await;
Ok(())
}
async fn get_selected_slot(&mut self, player: Resource<Player>) -> Result<u8, String> {
let player = player_from_resource(self, &player)?;
Ok(player.inventory.get_selected_slot())
@@ -371,21 +411,83 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(player.living_entity.health.load())
}
async fn set_health(&mut self, player: Resource<Player>, health: f32) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_health(health).await;
Ok(())
}
async fn get_max_health(&mut self, player: Resource<Player>) -> Result<f32, String> {
let player = player_from_resource(self, &player)?;
Ok(player.living_entity.get_max_health())
}
async fn set_max_health(
&mut self,
player: Resource<Player>,
max_health: f32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_max_health(max_health).await;
Ok(())
}
async fn get_food_level(&mut self, player: Resource<Player>) -> Result<u8, String> {
let player = player_from_resource(self, &player)?;
Ok(player.hunger_manager.level.load())
}
async fn set_food_level(&mut self, player: Resource<Player>, level: u8) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_food_level(level).await;
Ok(())
}
async fn get_saturation(&mut self, player: Resource<Player>) -> Result<f32, String> {
let player = player_from_resource(self, &player)?;
Ok(player.hunger_manager.saturation.load())
}
async fn set_saturation(
&mut self,
player: Resource<Player>,
saturation: f32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_saturation(saturation).await;
Ok(())
}
async fn get_exhaustion(&mut self, player: Resource<Player>) -> Result<f32, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_exhaustion())
}
async fn set_exhaustion(
&mut self,
player: Resource<Player>,
exhaustion: f32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_exhaustion(exhaustion).await;
Ok(())
}
async fn get_absorption(&mut self, player: Resource<Player>) -> Result<f32, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_absorption())
}
async fn set_absorption(
&mut self,
player: Resource<Player>,
absorption: f32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_absorption(absorption).await;
Ok(())
}
async fn get_experience_level(&mut self, player: Resource<Player>) -> Result<i32, String> {
let player = player_from_resource(self, &player)?;
Ok(player.experience_level.load(Ordering::Relaxed))
@@ -468,11 +570,31 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(player.get_entity().sneaking.load(Ordering::Relaxed))
}
async fn set_sneaking(
&mut self,
player: Resource<Player>,
sneaking: bool,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_sneaking(sneaking).await;
Ok(())
}
async fn is_sprinting(&mut self, player: Resource<Player>) -> Result<bool, String> {
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,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.get_entity().set_sprinting(sprinting).await;
Ok(())
}
async fn is_on_ground(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().on_ground.load(Ordering::Relaxed))
@@ -530,6 +652,11 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(())
}
async fn get_ip(&mut self, player: Resource<Player>) -> Result<String, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_ip().await)
}
async fn drop(&mut self, rep: Resource<Player>) -> wasmtime::Result<()> {
let _ = self
.resource_table

View File

@@ -3,16 +3,29 @@ use pumpkin_util::math::position::BlockPos;
use pumpkin_world::world::BlockFlags;
use wasmtime::component::Resource;
use pumpkin_world::world::SimpleWorld;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1_0::pumpkin::plugin::world::{
BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState,
PistonBehavior as WitPistonBehavior,
};
use crate::plugin::loader::wasm::wasm_host::{
DowncastResourceExt,
state::{PluginHostState, WorldResource},
state::{PluginHostState, TextComponentResource, WorldResource},
wit::v0_1_0::pumpkin::{self, plugin::world::World},
};
fn text_component_from_resource(
state: &PluginHostState,
text: &Resource<pumpkin::plugin::text::TextComponent>,
) -> Result<pumpkin_util::text::TextComponent, String> {
state
.resource_table
.get::<TextComponentResource>(&Resource::new_own(text.rep()))
.map_err(|_| "invalid text-component resource handle".to_string())
.map(|resource| resource.provider.clone())
}
impl DowncastResourceExt<WorldResource> for Resource<World> {
fn downcast_ref<'a>(&'a self, state: &'a mut PluginHostState) -> &'a WorldResource {
state
@@ -126,10 +139,78 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
// Update the world
world_ref
.provider
.clone()
.set_block_state(&internal_pos, state, internal_flags)
.await;
}
async fn get_time_of_day(&mut self, world: Resource<World>) -> u64 {
let world_ref = world.downcast_ref(self);
world_ref.provider.get_time_of_day().await as u64
}
async fn set_time_of_day(&mut self, world: Resource<World>, time: u64) {
let world_ref = world.downcast_ref(self);
world_ref.provider.set_time_of_day(time as i64).await;
}
async fn get_world_age(&mut self, world: Resource<World>) -> u64 {
let world_ref = world.downcast_ref(self);
world_ref.provider.get_world_age().await as u64
}
async fn get_dimension(&mut self, world: Resource<World>) -> String {
let world_ref = world.downcast_ref(self);
world_ref.provider.dimension.minecraft_name.to_string()
}
async fn get_top_block_y(&mut self, world: Resource<World>, x: i32, z: i32) -> i32 {
let world_ref = world.downcast_ref(self);
world_ref
.provider
.get_top_block(pumpkin_util::math::vector2::Vector2::new(x, z))
.await
}
async fn get_motion_blocking_height(&mut self, world: Resource<World>, x: i32, z: i32) -> i32 {
let world_ref = world.downcast_ref(self);
world_ref.provider.get_motion_blocking_height(x, z).await
}
async fn is_raining(&mut self, world: Resource<World>) -> bool {
let world_ref = world.downcast_ref(self);
world_ref.provider.is_raining().await
}
async fn set_raining(&mut self, world: Resource<World>, raining: bool) {
let world_ref = world.downcast_ref(self);
world_ref.provider.set_raining(raining).await;
}
async fn is_thundering(&mut self, world: Resource<World>) -> bool {
let world_ref = world.downcast_ref(self);
world_ref.provider.is_thundering().await
}
async fn set_thundering(&mut self, world: Resource<World>, thundering: bool) {
let world_ref = world.downcast_ref(self);
world_ref.provider.set_thundering(thundering).await;
}
async fn broadcast_system_message(
&mut self,
world: Resource<World>,
message: Resource<pumpkin::plugin::text::TextComponent>,
overlay: bool,
) {
let message = text_component_from_resource(self, &message).unwrap();
let world_ref = world.downcast_ref(self);
world_ref
.provider
.broadcast_system_message(&message, overlay)
.await;
}
async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> {
let _ = self
.resource_table

View File

@@ -67,7 +67,7 @@ use pumpkin_protocol::bedrock::client::set_actor_data::{
};
use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData};
use pumpkin_protocol::bedrock::frame_set::FrameSet;
use pumpkin_protocol::java::client::play::CPlayerSpawnPosition;
use pumpkin_protocol::java::client::play::{CPlayerSpawnPosition, CSystemChatMessage};
use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata};
use pumpkin_protocol::{
BClientPacket, ClientPacket, IdOr, SoundEvent,
@@ -486,6 +486,11 @@ impl World {
Self::broadcast_java_grouped(packet, recipients_by_version).await;
}
pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) {
let packet = CSystemChatMessage::new(message, overlay);
self.broadcast_packet_all(&packet).await;
}
pub async fn broadcast_message(
&self,
message: &TextComponent,
@@ -1396,6 +1401,40 @@ impl World {
spawn_for_chunk(self, chunk_pos, chunk, spawn_state, spawn_list).await;
}
pub async fn set_time_of_day(&self, time: i64) {
let mut level_time = self.level_time.lock().await;
level_time.set_time(time);
level_time.send_time(self).await;
}
pub async fn is_raining(&self) -> bool {
self.weather.lock().await.raining
}
pub async fn set_raining(&self, raining: bool) {
let mut weather = self.weather.lock().await;
if weather.raining != raining {
let thunder = weather.thundering;
weather
.set_weather_parameters(self, 0, 0, raining, thunder)
.await;
}
}
pub async fn is_thundering(&self) -> bool {
self.weather.lock().await.thundering
}
pub async fn set_thundering(&self, thundering: bool) {
let mut weather = self.weather.lock().await;
if weather.thundering != thundering {
let raining = weather.raining;
weather
.set_weather_parameters(self, 0, 0, raining, thundering)
.await;
}
}
/// Gets the y position of the first non air block from the top down
pub async fn get_top_block(&self, position: Vector2<i32>) -> i32 {
for y in (self.dimension.min_y..self.dimension.height).rev() {