feat: add more plugin stuff

This commit is contained in:
Alexander Medvedev 2026-08-29 20:37:18 +02:00
parent f8961d1fe0
commit 1ffb0d855b
39 changed files with 13329 additions and 122 deletions

View File

@ -161,7 +161,7 @@ mod entity_pose;
#[cfg(feature = "entity_status")] #[cfg(feature = "entity_status")]
#[rustfmt::skip] #[rustfmt::skip]
#[path = "generated/entity_status.rs"] #[path = "generated/entity_status.rs"]
mod entity_status; pub mod entity_status;
#[cfg(feature = "entity_type")] #[cfg(feature = "entity_type")]
#[rustfmt::skip] #[rustfmt::skip]

View File

@ -0,0 +1,160 @@
pub use crate::generated::block::BlockType;
use crate::wit::pumpkin::plugin::world::{Block, BlockState};
/// Trait for converting types into a valid Minecraft block registry key (e.g. `"minecraft:stone"`).
pub trait IntoBlockKey {
/// Returns the namespaced block registry key.
fn into_block_key(self) -> String;
}
impl IntoBlockKey for BlockType {
fn into_block_key(self) -> String {
self.resource_location().to_string()
}
}
impl IntoBlockKey for &BlockType {
fn into_block_key(self) -> String {
self.resource_location().to_string()
}
}
impl IntoBlockKey for &str {
fn into_block_key(self) -> String {
if self.contains(':') {
self.to_string()
} else {
format!("minecraft:{self}")
}
}
}
impl IntoBlockKey for String {
fn into_block_key(self) -> String {
if self.contains(':') {
self
} else {
format!("minecraft:{self}")
}
}
}
impl IntoBlockKey for &String {
fn into_block_key(self) -> String {
self.as_str().into_block_key()
}
}
/// Extension trait providing typed helper methods on `Block`.
pub trait BlockTypeExt {
/// Gets a block definition by any valid block key (e.g. `BlockType::Stone`, `"minecraft:stone"`, or `"custom:block"`).
#[must_use]
fn of(key: impl IntoBlockKey) -> Option<Block>;
/// Returns the typed `BlockType` if this block corresponds to a known vanilla block,
/// or `None` if it is a custom, modded, or newer unknown block.
#[must_use]
fn get_block_type(&self) -> Option<BlockType>;
/// Checks if this block matches the given `BlockType`.
#[must_use]
fn is_block_type(&self, block_type: BlockType) -> bool;
/// Checks if this block matches the given block key.
#[must_use]
fn matches_block(&self, key: impl IntoBlockKey) -> bool;
}
impl BlockTypeExt for Block {
fn of(key: impl IntoBlockKey) -> Option<Block> {
Block::from_name(&key.into_block_key())
}
fn get_block_type(&self) -> Option<BlockType> {
BlockType::from_registry_key(&self.name)
}
fn is_block_type(&self, block_type: BlockType) -> bool {
self.get_block_type() == Some(block_type)
}
fn matches_block(&self, key: impl IntoBlockKey) -> bool {
let expected = key.into_block_key();
let expected_clean = expected.strip_prefix("minecraft:").unwrap_or(&expected);
let actual_clean = self.name.strip_prefix("minecraft:").unwrap_or(&self.name);
expected_clean == actual_clean
}
}
/// Extension trait providing typed helper methods on `BlockState`.
pub trait BlockStateTypeExt {
/// Returns the typed `BlockType` if this block state corresponds to a known vanilla block,
/// or `None` if it is a custom, modded, or newer unknown block.
#[must_use]
fn get_block_type(&self) -> Option<BlockType>;
/// Checks if this block state matches the given `BlockType`.
#[must_use]
fn is_block_type(&self, block_type: BlockType) -> bool;
/// Checks if this block state matches the given block key.
#[must_use]
fn matches_block(&self, key: impl IntoBlockKey) -> bool;
}
impl BlockStateTypeExt for BlockState {
fn get_block_type(&self) -> Option<BlockType> {
BlockType::from_registry_key(&self.block_name)
}
fn is_block_type(&self, block_type: BlockType) -> bool {
self.get_block_type() == Some(block_type)
}
fn matches_block(&self, key: impl IntoBlockKey) -> bool {
let expected = key.into_block_key();
let expected_clean = expected.strip_prefix("minecraft:").unwrap_or(&expected);
let actual_clean = self
.block_name
.strip_prefix("minecraft:")
.unwrap_or(&self.block_name);
expected_clean == actual_clean
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_to_resource_location() {
assert_eq!(BlockType::Stone.into_block_key(), "minecraft:stone");
assert_eq!(
BlockType::DiamondBlock.into_block_key(),
"minecraft:diamond_block"
);
assert_eq!(BlockType::OakLog.into_block_key(), "minecraft:oak_log");
assert_eq!(BlockType::Obsidian.into_block_key(), "minecraft:obsidian");
}
#[test]
fn string_to_resource_location() {
assert_eq!("stone".into_block_key(), "minecraft:stone");
assert_eq!("minecraft:stone".into_block_key(), "minecraft:stone");
assert_eq!("custom:ruby_block".into_block_key(), "custom:ruby_block");
}
#[test]
fn block_parsing() {
assert_eq!(BlockType::from_name("stone"), Some(BlockType::Stone));
assert_eq!(
BlockType::from_name("minecraft:stone"),
Some(BlockType::Stone)
);
assert_eq!(
BlockType::from_name("diamond_block"),
Some(BlockType::DiamondBlock)
);
assert_eq!(BlockType::from_name("custom:ruby_block"), None);
}
}

View File

@ -29,3 +29,23 @@ impl AttributeModifier {
Self::new(id, amount, ModifierOperation::MultiplyTotal) Self::new(id, amount, ModifierOperation::MultiplyTotal)
} }
} }
use crate::wit::pumpkin::plugin::attributes::Attribute;
use crate::wit::pumpkin::plugin::enchantments::AttributeModifierSlot;
use crate::wit::pumpkin::plugin::item_stack::ItemAttributeModifier;
impl ItemAttributeModifier {
/// Creates a new item attribute modifier for a specific equipment slot.
#[must_use]
pub const fn new(
attribute: Attribute,
modifier: AttributeModifier,
slot: AttributeModifierSlot,
) -> Self {
Self {
attribute,
modifier,
slot,
}
}
}

View File

@ -133,6 +133,32 @@ impl BanIpOptions {
} }
} }
/// Extension trait providing typed cooldown helpers for `Player`.
pub trait PlayerCooldownExt {
/// Sets a client-side item cooldown overlay using any item key or `Item` enum.
fn set_cooldown(&self, item: impl crate::item::IntoItemKey, ticks: i32);
/// Returns the remaining cooldown ticks for an item, if active.
fn get_cooldown(&self, item: impl crate::item::IntoItemKey) -> Option<i32>;
/// Checks if an item is currently on cooldown.
fn has_cooldown(&self, item: impl crate::item::IntoItemKey) -> bool;
}
impl PlayerCooldownExt for Player {
fn set_cooldown(&self, item: impl crate::item::IntoItemKey, ticks: i32) {
self.set_item_cooldown(&item.into_item_key(), ticks);
}
fn get_cooldown(&self, item: impl crate::item::IntoItemKey) -> Option<i32> {
self.get_item_cooldown(&item.into_item_key())
}
fn has_cooldown(&self, item: impl crate::item::IntoItemKey) -> bool {
self.has_item_cooldown(&item.into_item_key())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{CustomStatistic, StatisticCategory}; use crate::{CustomStatistic, StatisticCategory};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
#![allow(missing_docs)]
pub mod block;
pub mod item;

View File

@ -0,0 +1,126 @@
pub use crate::generated::item::Item;
use crate::wit::pumpkin::plugin::item_stack::ItemStack;
/// Trait for converting types into a valid Minecraft item registry key (e.g. `"minecraft:diamond"`).
pub trait IntoItemKey {
/// Returns the namespaced item registry key.
fn into_item_key(self) -> String;
}
impl IntoItemKey for Item {
fn into_item_key(self) -> String {
self.resource_location().to_string()
}
}
impl IntoItemKey for &Item {
fn into_item_key(self) -> String {
self.resource_location().to_string()
}
}
impl IntoItemKey for &str {
fn into_item_key(self) -> String {
if self.contains(':') {
self.to_string()
} else {
format!("minecraft:{self}")
}
}
}
impl IntoItemKey for String {
fn into_item_key(self) -> String {
if self.contains(':') {
self
} else {
format!("minecraft:{self}")
}
}
}
impl IntoItemKey for &String {
fn into_item_key(self) -> String {
self.as_str().into_item_key()
}
}
/// Extension trait providing typed helper constructors and utilities on `ItemStack`.
pub trait ItemStackExt {
/// Creates a new `ItemStack` from any valid item key (e.g. `Item::Diamond`, `"minecraft:diamond"`, or `"custom:item"`).
#[must_use]
fn of(item: impl IntoItemKey, count: u8) -> Self;
/// Returns the typed `Item` enum if this item stack corresponds to a known vanilla item,
/// or `None` if it is a custom, modded, or newer unknown item.
#[must_use]
fn get_item(&self) -> Option<Item>;
/// Returns whether this item matches the specified typed `Item` enum.
#[must_use]
fn is_item(&self, item: Item) -> bool;
/// Checks if this item stack matches a given item (either `Item` enum or string identifier).
#[must_use]
fn matches_item(&self, item: impl IntoItemKey) -> bool;
}
impl ItemStackExt for ItemStack {
fn of(item: impl IntoItemKey, count: u8) -> Self {
Self::new(&item.into_item_key(), count)
}
fn get_item(&self) -> Option<Item> {
Item::from_registry_key(&self.get_registry_key())
}
fn is_item(&self, item: Item) -> bool {
self.get_item() == Some(item)
}
fn matches_item(&self, item: impl IntoItemKey) -> bool {
let expected = item.into_item_key();
let actual = self.get_registry_key();
let expected_clean = expected.strip_prefix("minecraft:").unwrap_or(&expected);
let actual_clean = actual.strip_prefix("minecraft:").unwrap_or(&actual);
expected_clean == actual_clean
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn item_to_resource_location() {
assert_eq!(Item::Diamond.into_item_key(), "minecraft:diamond");
assert_eq!(
Item::NetheriteSword.into_item_key(),
"minecraft:netherite_sword"
);
assert_eq!(Item::AcaciaBoat.into_item_key(), "minecraft:acacia_boat");
assert_eq!(
Item::AllaySpawnEgg.into_item_key(),
"minecraft:allay_spawn_egg"
);
assert_eq!(Item::Tnt.into_item_key(), "minecraft:tnt");
}
#[test]
fn string_to_resource_location() {
assert_eq!("diamond".into_item_key(), "minecraft:diamond");
assert_eq!("minecraft:diamond".into_item_key(), "minecraft:diamond");
assert_eq!("custom:laser_gun".into_item_key(), "custom:laser_gun");
}
#[test]
fn item_parsing() {
assert_eq!(Item::from_name("diamond"), Some(Item::Diamond));
assert_eq!(Item::from_name("minecraft:diamond"), Some(Item::Diamond));
assert_eq!(
Item::from_name("netherite_sword"),
Some(Item::NetheriteSword)
);
assert_eq!(Item::from_name("custom:magic_wand"), None);
}
}

View File

@ -74,6 +74,8 @@ use crate::{
text::TextComponent, text::TextComponent,
}; };
/// Block definitions and block type helpers.
pub mod block;
/// Plugin command registration and handling utilities. /// Plugin command registration and handling utilities.
pub mod commands; pub mod commands;
/// Datapack management and query utilities. /// Datapack management and query utilities.
@ -87,8 +89,11 @@ pub mod events;
mod ext; mod ext;
/// Bedrock UI form builders. /// Bedrock UI form builders.
pub mod forms; pub mod forms;
pub(crate) mod generated;
/// Unified inventory and container management utilities. /// Unified inventory and container management utilities.
pub mod inventory; pub mod inventory;
/// Typed item definitions and `ItemStack` construction helpers.
pub mod item;
/// Specialized mob entity wrappers and helpers. /// Specialized mob entity wrappers and helpers.
pub mod mobs; pub mod mobs;
/// Constants for plugin permissions. /// Constants for plugin permissions.
@ -118,15 +123,18 @@ pub use wit::pumpkin::plugin::{
context::{self, Context, MarketplaceMetadata, Server}, context::{self, Context, MarketplaceMetadata, Server},
damage_types as damage_types_wit, data_components, datapack as datapack_wit, damage_types as damage_types_wit, data_components, datapack as datapack_wit,
display as display_wit, enchantments as enchantments_wit, entity, display as display_wit, enchantments as enchantments_wit, entity,
entity_statuses as entity_statuses_wit,
entity_types::EntityType, entity_types::EntityType,
event::{self as events_wit, EventType}, event::{self as events_wit, EventType},
gui, i18n, inventory as inventory_wit, ipc, item_stack, java_dialogs, java_packets, particles, game_events as game_events_wit, gui, i18n, inventory as inventory_wit, ipc, item_stack,
permission, player, recipe as recipe_wit, scoreboard, screens as screens_wit, server, java_dialogs, java_packets, particles, permission, player, potions as potions_wit,
statistics as statistics_wit, text, uuid, world, recipe as recipe_wit, scoreboard, screens as screens_wit, server, statistics as statistics_wit,
text, uuid, world,
}; };
// Convenience re-exports of commonly-used plugin types so plugin authors can // Convenience re-exports of commonly-used plugin types so plugin authors can
// name them directly (e.g. build an `ItemStack` for a GUI or `/give`). // name them directly (e.g. build an `ItemStack` for a GUI or `/give`).
pub use block::{BlockStateTypeExt, BlockType, BlockTypeExt, IntoBlockKey};
pub use damage_types_wit::DamageType; pub use damage_types_wit::DamageType;
pub use datapack::{DatapackInfo, DatapackManager, EnablePosition}; pub use datapack::{DatapackInfo, DatapackManager, EnablePosition};
pub use display::{ pub use display::{
@ -139,15 +147,19 @@ pub use enchantment::{
AttributeModifierSlot, CustomEnchantment, CustomEnchantmentValue, Enchantment, AttributeModifierSlot, CustomEnchantment, CustomEnchantmentValue, Enchantment,
EnchantmentBuilder, EnchantmentError, EnchantmentManager, RegistrableEnchantment, EnchantmentBuilder, EnchantmentError, EnchantmentManager, RegistrableEnchantment,
}; };
pub use entity_statuses_wit::EntityStatus;
pub use events::{EventHandler, FromIntoEvent}; pub use events::{EventHandler, FromIntoEvent};
pub use ext::player::PlayerEnderChestExt; pub use ext::player::{PlayerCooldownExt, PlayerEnderChestExt};
pub use game_events_wit::GameEvent;
pub use inventory::{Inventory, PlayerInventory}; pub use inventory::{Inventory, PlayerInventory};
pub use item::{IntoItemKey, Item, ItemStackExt};
pub use mobs::{ pub use mobs::{
Ageable, AgeableData, Cat, CatData, Creeper, CreeperData, DyeColor, Enderman, EndermanData, Ageable, AgeableData, Cat, CatData, Creeper, CreeperData, DyeColor, Enderman, EndermanData,
EntityCastExt, Fox, FoxData, IronGolem, IronGolemData, MobCast, MobData, Sheep, SheepData, EntityCastExt, Fox, FoxData, IronGolem, IronGolemData, MobCast, MobData, Sheep, SheepData,
Shulker, ShulkerData, Slime, SlimeData, Villager, VillagerData, VillagerProfession, Wolf, Shulker, ShulkerData, Slime, SlimeData, Villager, VillagerData, VillagerProfession, Wolf,
WolfData, Zombie, ZombieData, WolfData, Zombie, ZombieData,
}; };
pub use potions_wit::PotionType;
pub use recipe::{ pub use recipe::{
CookingRecipeBuilder, Ingredient, RecipeCategory, RecipeError, RecipeManager, CookingRecipeBuilder, Ingredient, RecipeCategory, RecipeError, RecipeManager,
RegistrableRecipe, ShapedRecipeBuilder, ShapelessRecipeBuilder, RegistrableRecipe, ShapedRecipeBuilder, ShapelessRecipeBuilder,
@ -155,7 +167,8 @@ pub use recipe::{
pub use screens_wit::Screen; pub use screens_wit::Screen;
pub use statistics_wit::{CustomStatistic, StatisticCategory}; pub use statistics_wit::{CustomStatistic, StatisticCategory};
pub use team::{PlayerTeamExt, ScoreboardTeamExt, Team, TeamSettingsBuilder}; pub use team::{PlayerTeamExt, ScoreboardTeamExt, Team, TeamSettingsBuilder};
pub use wit::pumpkin::plugin::item_stack::ItemStack; pub use wit::pumpkin::plugin::attributes::{Attribute, AttributeModifier, ModifierOperation};
pub use wit::pumpkin::plugin::item_stack::{ItemAttributeModifier, ItemStack};
pub use wit::pumpkin::plugin::player::Player; pub use wit::pumpkin::plugin::player::Player;
pub use wit::pumpkin::plugin::scoreboard::{CollisionRule, NametagVisibility, TeamSettings}; pub use wit::pumpkin::plugin::scoreboard::{CollisionRule, NametagVisibility, TeamSettings};
pub use wit::pumpkin::plugin::server::Dimension; pub use wit::pumpkin::plugin::server::Dimension;

View File

@ -11,6 +11,7 @@ pub struct CDeleteChat<'a> {
} }
impl<'a> CDeleteChat<'a> { impl<'a> CDeleteChat<'a> {
/// Creates a new `CDeleteChat` packet using a raw signature ID `VarInt`.
#[must_use] #[must_use]
pub const fn from_id(signature_id: VarInt) -> Self { pub const fn from_id(signature_id: VarInt) -> Self {
Self { Self {
@ -19,6 +20,21 @@ impl<'a> CDeleteChat<'a> {
} }
} }
/// Creates a new `CDeleteChat` packet using a 0-indexed signature cache ID.
///
/// In Minecraft's `MessageSignature.Packed`, cached signatures are written as `cache_id + 1`.
#[must_use]
pub const fn from_cache_id(cache_id: i32) -> Self {
Self {
signature_id: VarInt(cache_id + 1),
signature: None,
}
}
/// Creates a new `CDeleteChat` packet using a full 256-byte message signature.
///
/// In Minecraft's `MessageSignature.Packed`, a full signature is written as `VarInt(0)`
/// followed by the 256 raw signature bytes.
#[must_use] #[must_use]
pub const fn from_signature(signature: &'a [u8]) -> Self { pub const fn from_signature(signature: &'a [u8]) -> Self {
Self { Self {

View File

@ -1,6 +1,9 @@
use crate::ClientPacket;
use crate::Link; use crate::Link;
use crate::ser::NetworkWriteExt;
use pumpkin_data::packet::clientbound::play::SERVER_LINKS; use pumpkin_data::packet::clientbound::play::SERVER_LINKS;
use pumpkin_macros::java_packet; use pumpkin_macros::java_packet;
use pumpkin_util::version::JavaMinecraftVersion;
#[java_packet(SERVER_LINKS)] #[java_packet(SERVER_LINKS)]
pub struct CPlayServerLinks<'a> { pub struct CPlayServerLinks<'a> {
@ -13,3 +16,17 @@ impl<'a> CPlayServerLinks<'a> {
Self { links } Self { links }
} }
} }
impl ClientPacket for CPlayServerLinks<'_> {
fn write_packet_data(
&self,
mut write: impl std::io::Write,
_version: &JavaMinecraftVersion,
) -> Result<(), crate::ser::WritingError> {
write.write_var_int(&crate::VarInt(self.links.len() as i32))?;
for link in self.links {
link.write(&mut write)?;
}
Ok(())
}
}

View File

@ -25,7 +25,6 @@ use pumpkin_util::random::xoroshiro128::Xoroshiro;
use pumpkin_util::random::{RandomGenerator, get_seed}; use pumpkin_util::random::{RandomGenerator, get_seed};
use pumpkin_util::version::JavaMinecraftVersion; use pumpkin_util::version::JavaMinecraftVersion;
use rand::RngExt; use rand::RngExt;
use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::{AtomicI32, AtomicU8, Ordering}; use std::sync::atomic::{AtomicI32, AtomicU8, Ordering};
@ -1305,15 +1304,13 @@ pub trait PathAwareEntity: Mob + Send + Sync {
) >= 0.0 ) >= 0.0
} }
fn is_navigation<'a>(&'a self) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> { fn is_navigation(&self) -> bool {
Box::pin(async {
let navigator = self let navigator = self
.get_mob_entity() .get_mob_entity()
.navigator .navigator
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
!navigator.is_idle() !navigator.is_idle()
})
} }
// TODO: implement // TODO: implement

View File

@ -2619,6 +2619,29 @@ impl Entity {
} }
} }
/// Sets the number of ticks the entity has been frozen.
pub fn set_frozen_ticks(&self, ticks: i32) {
let new_frozen_ticks = ticks.clamp(0, Self::MAX_FROZEN_TICKS);
self.frozen_ticks.store(new_frozen_ticks, Ordering::Relaxed);
let mut bedrock_meta = SyncedActorDataList::new();
bedrock_meta.set(
entity_data_key::FREEZING_EFFECT_STRENGTH,
MetadataValue::Float(new_frozen_ticks as f32),
);
self.send_meta_data(
&[Metadata::new(
tracked_data::entity::DATA_TICKS_FROZEN,
VarInt(new_frozen_ticks),
)],
Some(&bedrock_meta),
);
}
/// Returns the number of ticks the entity has been frozen.
pub fn get_frozen_ticks(&self) -> i32 {
self.frozen_ticks.load(Ordering::Relaxed)
}
/// Sets the `Entity` yaw & pitch rotation /// Sets the `Entity` yaw & pitch rotation
pub fn set_rotation(&self, yaw: f32, pitch: f32) { pub fn set_rotation(&self, yaw: f32, pitch: f32) {
// TODO // TODO

View File

@ -260,15 +260,16 @@ use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::codec::var_long::VarLong; use pumpkin_protocol::codec::var_long::VarLong;
use pumpkin_protocol::codec::var_ulong::VarULong; use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_protocol::java::client::play::{ use pumpkin_protocol::java::client::play::{
Animation, CAcknowledgeBlockChange, CActionBar, CAwardStats, CChangeDifficulty, Animation, CAcknowledgeBlockChange, CActionBar, CAwardStats, CBlockUpdate, CChangeDifficulty,
CCloseContainer, CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CCloseContainer, CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation,
CEntityPositionSync, CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle, CEntityPositionSync, CEntityVelocity, CGameEvent, CHurtAnimation, CItemCooldown, CMapItemData,
CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, COpenBook, COpenScreen, COpenSignEditor, CParticle, CPlayServerLinks, CPlayerAbilities,
CSetCamera, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, CSetCamera,
CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetExperience,
CSubtitle, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle,
CUpdateMobEffect, CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect,
PlayerInfoFlags, PlayerSpawnData, PreviousMessage, Statistic, CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction, PlayerInfoFlags,
PlayerSpawnData, PreviousMessage, Statistic,
}; };
use pumpkin_protocol::java::server::play::{ use pumpkin_protocol::java::server::play::{
SClickSlot, SContainerButtonClick, SRenameItem, SlotActionType, SClickSlot, SContainerButtonClick, SRenameItem, SlotActionType,
@ -462,6 +463,8 @@ pub struct Player {
/// Whether the client has reported that it has loaded. /// Whether the client has reported that it has loaded.
pub client_loaded: AtomicBool, pub client_loaded: AtomicBool,
pub bedrock_spawned: AtomicBool, pub bedrock_spawned: AtomicBool,
/// Whether the player is frozen in place (movement locked for dialogues/cutscenes).
pub is_movement_locked: AtomicBool,
/// The amount of time (in ticks) the client has to report having finished loading before being timed out. /// The amount of time (in ticks) the client has to report having finished loading before being timed out.
pub client_loaded_timeout: AtomicU32, pub client_loaded_timeout: AtomicU32,
/// Counter for tracking chat and command spam. Decays each server tick. /// Counter for tracking chat and command spam. Decays each server tick.
@ -733,6 +736,7 @@ impl Player {
gamemode: AtomicCell::new(gamemode), gamemode: AtomicCell::new(gamemode),
previous_gamemode: AtomicCell::new(None), previous_gamemode: AtomicCell::new(None),
camera_target_id: AtomicCell::new(None), camera_target_id: AtomicCell::new(None),
is_movement_locked: AtomicBool::new(false),
// TODO: Send the CPlayerSpawnPosition packet when the client connects with proper values // TODO: Send the CPlayerSpawnPosition packet when the client connects with proper values
respawn_point: std::sync::Mutex::new(None), respawn_point: std::sync::Mutex::new(None),
sleeping_since: AtomicCell::new(None), sleeping_since: AtomicCell::new(None),
@ -2188,6 +2192,126 @@ impl Player {
self.try_send_client_packet(&packet); self.try_send_client_packet(&packet);
} }
/// Plays a custom sound event by identifier for the player.
pub fn play_custom_sound(
&self,
sound_name: &str,
category: SoundCategory,
position: &Vector3<f64>,
volume: f32,
pitch: f32,
) {
self.play_sound_event(
pumpkin_protocol::SoundEvent {
sound_name: sound_name.into(),
range: None,
},
category,
position,
volume,
pitch,
rand::random::<f64>(),
);
}
pub fn spawn_particles(
&self,
particle: Particle,
pos: Vector3<f64>,
count: u32,
offset: Vector3<f32>,
max_speed: f32,
) {
let packet = CParticle::new(
false,
false,
pos,
offset,
max_speed,
count as i32,
(particle.to_id() as i32).into(),
&[],
);
self.try_send_client_packet(&packet);
}
pub fn send_block_change(&self, location: BlockPos, state_id: u16) {
let packet = CBlockUpdate::new(location, (state_id as i32).into());
self.try_send_client_packet(&packet);
}
pub fn reset_block_change(&self, location: BlockPos) {
let state_id = self.world().get_block_state_id(&location);
let packet = CBlockUpdate::new(location, (state_id.as_u16() as i32).into());
self.try_send_client_packet(&packet);
}
pub fn send_hurt_animation(&self, yaw: f32) {
let packet = CHurtAnimation::new(self.entity_id().into(), yaw);
self.try_send_client_packet(&packet);
}
pub fn open_book(&self, hand: Hand) {
let hand_val = match hand {
Hand::Right => 0,
Hand::Left => 1,
};
let packet = COpenBook::new(hand_val.into());
self.try_send_client_packet(&packet);
}
pub fn open_sign_editor(&self, location: BlockPos, is_front_text: bool) {
let packet = COpenSignEditor::new(location, is_front_text);
self.try_send_client_packet(&packet);
}
pub fn set_velocity(&self, velocity: Vector3<f64>) {
self.living_entity.entity.set_velocity(velocity);
self.try_send_client_packet(&CEntityVelocity::new(self.entity_id().into(), velocity));
}
pub fn apply_knockback(&self, strength: f64, x: f64, z: f64) {
let current_vel = self.living_entity.entity.velocity.load();
let norm = x.hypot(z);
if norm > 0.0 {
let vx = current_vel.x / 2.0 - (x / norm) * strength;
let vz = current_vel.z / 2.0 - (z / norm) * strength;
let vy = (current_vel.y / 2.0 + strength).min(0.4);
self.set_velocity(Vector3::new(vx, vy, vz));
}
}
pub fn set_movement_locked(&self, locked: bool) {
self.is_movement_locked.store(locked, Ordering::Relaxed);
}
pub fn is_movement_locked(&self) -> bool {
self.is_movement_locked.load(Ordering::Relaxed)
}
pub fn set_freeze_ticks(&self, ticks: i32) {
self.living_entity.entity.set_frozen_ticks(ticks);
}
pub fn get_freeze_ticks(&self) -> i32 {
self.living_entity.entity.get_frozen_ticks()
}
pub fn send_game_event(
&self,
event: pumpkin_protocol::java::client::play::GameEvent,
value: f32,
) {
let packet = CGameEvent::new(event, value);
self.try_send_client_packet(&packet);
}
/// Sends custom server links to the player (displayed in the client Esc pause menu).
pub fn set_server_links(&self, links: &[pumpkin_protocol::Link<'_>]) {
let packet = CPlayServerLinks::new(links);
self.try_send_client_packet(&packet);
}
pub fn process_inbound_packets(&self) { pub fn process_inbound_packets(&self) {
const MAX_PACKETS_PER_TICK: usize = 64; const MAX_PACKETS_PER_TICK: usize = 64;
@ -2662,6 +2786,32 @@ impl Player {
self.living_entity.entity.entity_id self.living_entity.entity.entity_id
} }
/// Sets the player's camera target entity ID.
/// If `target_id` matches the player's own entity ID, resets the camera back to the player.
pub fn set_camera_entity_id(&self, target_id: i32) {
if target_id == self.entity_id() {
self.camera_target_id.store(None);
self.try_send_client_packet(&CSetCamera::new(self.entity_id().into()));
} else {
self.camera_target_id.store(Some(target_id));
self.try_send_client_packet(&CSetCamera::new(target_id.into()));
}
}
/// Resets the player's camera back to their own perspective.
pub fn reset_camera(&self) {
self.camera_target_id.store(None);
self.try_send_client_packet(&CSetCamera::new(self.entity_id().into()));
}
/// Gets the entity ID of the entity that the player's camera is currently attached to,
/// or the player's own entity ID if not overridden.
pub fn get_camera_entity_id(&self) -> i32 {
self.camera_target_id
.load()
.unwrap_or_else(|| self.entity_id())
}
pub fn world(&self) -> Arc<World> { pub fn world(&self) -> Arc<World> {
self.living_entity.entity.world.load_full() self.living_entity.entity.world.load_full()
} }

View File

@ -16,7 +16,7 @@ impl BedrockClient {
send_cancellable! {{ send_cancellable! {{
server; server;
PlayerChatEvent::new(player.clone(), packet.message.into_owned(), vec![]); PlayerChatEvent::new(player.clone(), packet.message.into_owned(), vec![], None);
'after: { 'after: {
info!("<chat> {}: {}", gameprofile.name, event.message); info!("<chat> {}: {}", gameprofile.name, event.message);

View File

@ -41,7 +41,12 @@ impl JavaClient {
send_cancellable! {{ send_cancellable! {{
server; server;
PlayerChatEvent::new(player.clone(), chat_message.message.to_string(), vec![]); PlayerChatEvent::new(
player.clone(),
chat_message.message.to_string(),
vec![],
chat_message.signature.map(<[u8]>::to_vec),
);
'after: { 'after: {
info!("<chat> {}: {}", gameprofile.name, event.message); info!("<chat> {}: {}", gameprofile.name, event.message);

View File

@ -64,6 +64,10 @@ impl JavaClient {
{ {
return; return;
} }
if player.is_movement_locked.load(Ordering::Relaxed) {
self.force_tp(player, player.get_entity().pos.load());
return;
}
// y = feet Y // y = feet Y
let position = packet.position; let position = packet.position;
if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() { if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() {
@ -197,6 +201,12 @@ impl JavaClient {
{ {
return; return;
} }
if player.is_movement_locked.load(Ordering::Relaxed) {
let entity = player.get_entity();
entity.set_rotation(packet.yaw, packet.pitch);
self.force_tp(player, entity.pos.load());
return;
}
// y = feet Y // y = feet Y
let position = packet.position; let position = packet.position;
if !position.x.is_finite() if !position.x.is_finite()

View File

@ -19,6 +19,9 @@ pub struct PlayerChatEvent {
/// The recipients of the message. If empty, the message is broadcasted to all players. /// The recipients of the message. If empty, the message is broadcasted to all players.
pub recipients: Vec<Arc<Player>>, pub recipients: Vec<Arc<Player>>,
/// The optional 256-byte cryptographic signature of the message.
pub signature: Option<Vec<u8>>,
} }
impl PlayerChatEvent { impl PlayerChatEvent {
@ -28,14 +31,21 @@ impl PlayerChatEvent {
/// - `player`: A reference to the player sending the message. /// - `player`: A reference to the player sending the message.
/// - `message`: The message being sent. /// - `message`: The message being sent.
/// - `recipients`: The recipients of the message. If empty, the message is broadcasted to all players. /// - `recipients`: The recipients of the message. If empty, the message is broadcasted to all players.
/// - `signature`: The optional cryptographic signature of the message.
/// ///
/// # Returns /// # Returns
/// A new instance of `PlayerChatEvent`. /// A new instance of `PlayerChatEvent`.
pub const fn new(player: Arc<Player>, message: String, recipients: Vec<Arc<Player>>) -> Self { pub const fn new(
player: Arc<Player>,
message: String,
recipients: Vec<Arc<Player>>,
signature: Option<Vec<u8>>,
) -> Self {
Self { Self {
player, player,
message, message,
recipients, recipients,
signature,
cancelled: false, cancelled: false,
} }
} }

View File

@ -322,6 +322,7 @@ impl ToFromWasmEvent for PlayerChatEvent {
player, player,
message: self.message.clone(), message: self.message.clone(),
recipients, recipients,
signature: self.signature.clone(),
cancelled: self.cancelled, cancelled: self.cancelled,
}) })
} }
@ -336,6 +337,7 @@ impl ToFromWasmEvent for PlayerChatEvent {
.into_iter() .into_iter()
.map(|recipient| consume_player(state, &recipient)) .map(|recipient| consume_player(state, &recipient))
.collect(), .collect(),
signature: data.signature,
cancelled: data.cancelled, cancelled: data.cancelled,
}, },
_ => panic!("unexpected event type"), _ => panic!("unexpected event type"),

View File

@ -1,10 +1,15 @@
use crate::plugin::loader::wasm::wasm_host::state::{ItemStackResource, PluginHostState}; use crate::plugin::loader::wasm::wasm_host::state::{ItemStackResource, PluginHostState};
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::attributes::{
Attribute as WitAttribute, AttributeModifier as WitAttributeModifier,
ModifierOperation as WitModifierOperation,
};
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::data_components::DataComponent as WitDataComponent; use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::data_components::DataComponent as WitDataComponent;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::enchantments::Enchantment as WitEnchantment; use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::enchantments::Enchantment as WitEnchantment;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::item_stack::{ use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::item_stack::{
CustomEnchantmentValue as WitCustomEnchantmentValue, CustomEnchantmentValue as WitCustomEnchantmentValue,
DataComponentValue as WitDataComponentValue, EnchantmentValue as WitEnchantmentValue, DataComponentValue as WitDataComponentValue, EnchantmentValue as WitEnchantmentValue,
Host as ItemStackInterfaceHost, HostItemStack, ItemStack as ItemStackHandle, Host as ItemStackInterfaceHost, HostItemStack,
ItemAttributeModifier as WitItemAttributeModifier, ItemStack as ItemStackHandle,
}; };
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::text::TextComponent as WitTextComponent; use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::text::TextComponent as WitTextComponent;
use std::sync::Arc; use std::sync::Arc;
@ -14,8 +19,12 @@ use wasmtime::component::Resource;
use super::common::{WitNbtTree, from_wit_nbt_tree, to_wit_nbt_tree}; use super::common::{WitNbtTree, from_wit_nbt_tree, to_wit_nbt_tree};
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::player::text_component_from_resource; use crate::plugin::loader::wasm::wasm_host::wit::v0_1::player::text_component_from_resource;
use pumpkin_data::Enchantment; use pumpkin_data::Enchantment;
use pumpkin_data::attributes::Attributes;
use pumpkin_data::data_component::DataComponent; use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{CustomNameImpl, EnchantmentsImpl, LoreImpl}; use pumpkin_data::data_component_impl::combat::{Modifier, Operation};
use pumpkin_data::data_component_impl::{
AttributeModifiersImpl, CustomNameImpl, EnchantmentsImpl, LoreImpl,
};
use pumpkin_nbt::tag::NbtTag; use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::codec::data_component::{deserialize, serialize}; use pumpkin_protocol::codec::data_component::{deserialize, serialize};
use std::borrow::Cow; use std::borrow::Cow;
@ -40,6 +49,30 @@ pub(crate) fn from_wit_enchantment(id: WitEnchantment) -> &'static Enchantment {
Enchantment::from_id(id as u8).expect("valid enchantment ID") Enchantment::from_id(id as u8).expect("valid enchantment ID")
} }
#[must_use]
pub fn to_wit_attribute(attr: &Attributes) -> WitAttribute {
// SAFETY: WIT enum is generated in the same order as the internal ID
unsafe { std::mem::transmute(attr.id) }
}
#[must_use]
pub const fn from_wit_item_operation(op: WitModifierOperation) -> Operation {
match op {
WitModifierOperation::Add => Operation::AddValue,
WitModifierOperation::MultiplyBase => Operation::AddMultipliedBase,
WitModifierOperation::MultiplyTotal => Operation::AddMultipliedTotal,
}
}
#[must_use]
pub const fn to_wit_item_operation(op: Operation) -> WitModifierOperation {
match op {
Operation::AddValue => WitModifierOperation::Add,
Operation::AddMultipliedBase => WitModifierOperation::MultiplyBase,
Operation::AddMultipliedTotal => WitModifierOperation::MultiplyTotal,
}
}
impl PluginHostState { impl PluginHostState {
pub fn get_item_stack( pub fn get_item_stack(
&self, &self,
@ -380,6 +413,101 @@ impl HostItemStack for PluginHostState {
Ok(false) Ok(false)
} }
async fn get_attribute_modifiers(
&mut self,
res: Resource<ItemStackHandle>,
) -> wasmtime::Result<Vec<WitItemAttributeModifier>> {
let stack = self.get_item_stack(&res)?;
let stack = stack.lock().await;
let mut modifiers = Vec::new();
if let Some(comp) = stack.get_data_component::<AttributeModifiersImpl>() {
for m in comp.attribute_modifiers.iter() {
modifiers.push(WitItemAttributeModifier {
attribute: to_wit_attribute(m.r#type),
modifier: WitAttributeModifier {
id: m.id.to_string(),
amount: m.amount,
operation: to_wit_item_operation(m.operation),
},
slot: super::enchantment::to_wit_slot(&m.slot),
});
}
}
Ok(modifiers)
}
async fn add_attribute_modifier(
&mut self,
res: Resource<ItemStackHandle>,
modifier: WitItemAttributeModifier,
) -> wasmtime::Result<()> {
let stack = self.get_item_stack(&res)?;
let mut stack = stack.lock().await;
let attr = super::living_entity::from_wit_attribute(modifier.attribute);
let slot = super::enchantment::to_data_slot(modifier.slot);
let op = from_wit_item_operation(modifier.modifier.operation);
let leaked_id: &'static str = Box::leak(modifier.modifier.id.into_boxed_str());
let mut current_mods = stack
.get_data_component::<AttributeModifiersImpl>()
.map_or_else(Vec::new, |comp| {
comp.attribute_modifiers.clone().into_owned()
});
current_mods.retain(|m| !(m.r#type == attr && m.id == leaked_id && m.slot == slot));
current_mods.push(Modifier {
r#type: attr,
id: leaked_id,
amount: modifier.modifier.amount,
operation: op,
slot,
});
stack.set_data_component(AttributeModifiersImpl {
attribute_modifiers: Cow::Owned(current_mods),
});
Ok(())
}
async fn remove_attribute_modifiers(
&mut self,
res: Resource<ItemStackHandle>,
attribute: WitAttribute,
) -> wasmtime::Result<()> {
let stack = self.get_item_stack(&res)?;
let mut stack = stack.lock().await;
let attr = super::living_entity::from_wit_attribute(attribute);
if let Some(comp) = stack.get_data_component::<AttributeModifiersImpl>() {
let mut current_mods = comp.attribute_modifiers.clone().into_owned();
current_mods.retain(|m| m.r#type != attr);
if current_mods.is_empty() {
stack
.patch
.retain(|(id, _)| *id != DataComponent::AttributeModifiers);
} else {
stack.set_data_component(AttributeModifiersImpl {
attribute_modifiers: Cow::Owned(current_mods),
});
}
}
Ok(())
}
async fn clear_attribute_modifiers(
&mut self,
res: Resource<ItemStackHandle>,
) -> wasmtime::Result<()> {
let stack = self.get_item_stack(&res)?;
let mut stack = stack.lock().await;
stack
.patch
.retain(|(id, _)| *id != DataComponent::AttributeModifiers);
Ok(())
}
async fn get_lore( async fn get_lore(
&mut self, &mut self,
res: Resource<ItemStackHandle>, res: Resource<ItemStackHandle>,

View File

@ -980,6 +980,21 @@ impl HostMob for PluginHostState {
Ok(false) Ok(false)
} }
async fn set_freeze_ticks(
&mut self,
this: Resource<WitMob>,
ticks: i32,
) -> wasmtime::Result<()> {
let entity = mob_from_resource(self, &this)?;
entity.get_entity().set_frozen_ticks(ticks);
Ok(())
}
async fn get_freeze_ticks(&mut self, this: Resource<WitMob>) -> wasmtime::Result<i32> {
let entity = mob_from_resource(self, &this)?;
Ok(entity.get_entity().get_frozen_ticks())
}
async fn drop(&mut self, rep: Resource<WitMob>) -> wasmtime::Result<()> { async fn drop(&mut self, rep: Resource<WitMob>) -> wasmtime::Result<()> {
let _ = self let _ = self
.resource_table .resource_table

View File

@ -84,6 +84,9 @@ impl pumpkin::plugin::damage_types::Host for PluginHostState {}
impl pumpkin::plugin::screens::Host for PluginHostState {} impl pumpkin::plugin::screens::Host for PluginHostState {}
impl pumpkin::plugin::statistics::Host for PluginHostState {} impl pumpkin::plugin::statistics::Host for PluginHostState {}
impl pumpkin::plugin::game_rules::Host for PluginHostState {} impl pumpkin::plugin::game_rules::Host for PluginHostState {}
impl pumpkin::plugin::game_events::Host for PluginHostState {}
impl pumpkin::plugin::potions::Host for PluginHostState {}
impl pumpkin::plugin::entity_statuses::Host for PluginHostState {}
pub fn add_to_linker(linker: &mut Linker<PluginHostState>) -> wasmtime::Result<()> { pub fn add_to_linker(linker: &mut Linker<PluginHostState>) -> wasmtime::Result<()> {
Plugin::add_to_linker::<_, HasSelf<_>>(linker, |state: &mut PluginHostState| state)?; Plugin::add_to_linker::<_, HasSelf<_>>(linker, |state: &mut PluginHostState| state)?;

View File

@ -48,6 +48,62 @@ use pumpkin_protocol::bedrock::client::set_actor_data::{
use pumpkin_protocol::codec::var_ulong::VarULong; use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_util::version::{BedrockMinecraftVersion, JavaMinecraftVersion}; use pumpkin_util::version::{BedrockMinecraftVersion, JavaMinecraftVersion};
const fn from_wit_client_game_event(
event: pumpkin::plugin::player::ClientGameEvent,
) -> pumpkin_protocol::java::client::play::GameEvent {
use pumpkin::plugin::player::ClientGameEvent as W;
use pumpkin_protocol::java::client::play::GameEvent as G;
match event {
W::NoRespawnBlockAvailable => G::NoRespawnBlockAvailable,
W::BeginRaining => G::BeginRaining,
W::EndRaining => G::EndRaining,
W::ChangeGameMode => G::ChangeGameMode,
W::WinGame => G::WinGame,
W::DemoEvent => G::DemoEvent,
W::ArrowHitPlayer => G::ArrowHitPlayer,
W::RainLevelChange => G::RainLevelChange,
W::ThunderLevelChange => G::ThunderLevelChange,
W::PlayPufferfishStringSound => G::PlayPufferfishStringSound,
W::PlayElderGuardianMobAppearance => G::PlayElderGuardianMobAppearance,
W::EnabledRespawnScreen => G::EnabledRespawnScreen,
W::LimitedCrafting => G::LimitedCrafting,
W::StartWaitingChunks => G::StartWaitingChunks,
}
}
pub(crate) fn from_wit_server_link(
state: &PluginHostState,
link: pumpkin::plugin::player::ServerLink,
) -> wasmtime::Result<(pumpkin_protocol::Label, String)> {
use pumpkin::plugin::player::{KnownServerLink, ServerLinkLabel};
let label = match link.label {
ServerLinkLabel::Known(known) => {
let link_type = match known {
KnownServerLink::BugReport => pumpkin_protocol::LinkType::BugReport,
KnownServerLink::CommunityGuidelines => {
pumpkin_protocol::LinkType::CommunityGuidelines
}
KnownServerLink::Support => pumpkin_protocol::LinkType::Support,
KnownServerLink::Status => pumpkin_protocol::LinkType::Status,
KnownServerLink::Feedback => pumpkin_protocol::LinkType::Feedback,
KnownServerLink::Community => pumpkin_protocol::LinkType::Community,
KnownServerLink::Website => pumpkin_protocol::LinkType::Website,
KnownServerLink::Forums => pumpkin_protocol::LinkType::Forums,
KnownServerLink::News => pumpkin_protocol::LinkType::News,
KnownServerLink::Announcements => pumpkin_protocol::LinkType::Announcements,
};
pumpkin_protocol::Label::BuiltIn(link_type)
}
ServerLinkLabel::Custom(text) => {
let text_res = state
.resource_table
.get::<TextComponentResource>(&Resource::new_own(text.rep()))?;
pumpkin_protocol::Label::TextComponent(Box::new(text_res.provider.clone()))
}
};
Ok((label, link.url))
}
const fn to_wasm_java_version( const fn to_wasm_java_version(
version: JavaMinecraftVersion, version: JavaMinecraftVersion,
) -> pumpkin::plugin::player::JavaMinecraftVersion { ) -> pumpkin::plugin::player::JavaMinecraftVersion {
@ -1512,6 +1568,363 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(()) Ok(())
} }
async fn delete_message_by_signature(
&mut self,
player: Resource<Player>,
signature: Vec<u8>,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
if let Some(client) = player.client.java() {
let packet =
pumpkin_protocol::java::client::play::CDeleteChat::from_signature(&signature);
client.send_packet(&packet).await;
}
Ok(())
}
async fn delete_message_by_id(
&mut self,
player: Resource<Player>,
signature_id: i32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
if let Some(client) = player.client.java() {
let packet =
pumpkin_protocol::java::client::play::CDeleteChat::from_cache_id(signature_id);
client.send_packet(&packet).await;
}
Ok(())
}
async fn set_camera(
&mut self,
player: Resource<Player>,
entity: Option<
Resource<
crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::world::Entity,
>,
>,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
if let Some(target_res) = entity {
let target =
crate::plugin::loader::wasm::wasm_host::wit::v0_1::entity::entity_from_resource(
self,
&target_res,
)?;
player.set_camera_entity_id(target.get_entity().entity_id);
} else {
player.reset_camera();
}
Ok(())
}
async fn set_camera_entity_id(
&mut self,
player: Resource<Player>,
entity_id: u32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.set_camera_entity_id(entity_id as i32);
Ok(())
}
async fn get_camera_entity_id(&mut self, player: Resource<Player>) -> wasmtime::Result<u32> {
let player = player_from_resource(self, &player)?;
Ok(player.get_camera_entity_id() as u32)
}
async fn reset_camera(&mut self, player: Resource<Player>) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.reset_camera();
Ok(())
}
async fn play_sound(
&mut self,
player: Resource<Player>,
sound: pumpkin::plugin::sounds::Sound,
category: pumpkin::plugin::sounds::SoundCategory,
volume: f32,
pitch: f32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let sound_name = format!("{sound:?}").to_lowercase().replace('_', ".");
let sound_data = pumpkin_data::sound::Sound::from_name(&sound_name)
.ok_or_else(|| wasmtime::Error::msg(format!("Unknown sound: {sound_name}")))?;
let internal_category = super::world::from_wit_sound_category(category);
let pos = player.position();
player.play_sound(
sound_data as u16,
internal_category,
&pos,
volume,
pitch,
rand::random::<f64>(),
);
Ok(())
}
async fn play_sound_at(
&mut self,
player: Resource<Player>,
pos: pumpkin::plugin::common::Position,
sound: pumpkin::plugin::sounds::Sound,
category: pumpkin::plugin::sounds::SoundCategory,
volume: f32,
pitch: f32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let sound_name = format!("{sound:?}").to_lowercase().replace('_', ".");
let sound_data = pumpkin_data::sound::Sound::from_name(&sound_name)
.ok_or_else(|| wasmtime::Error::msg(format!("Unknown sound: {sound_name}")))?;
let internal_category = super::world::from_wit_sound_category(category);
player.play_sound(
sound_data as u16,
internal_category,
&pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2),
volume,
pitch,
rand::random::<f64>(),
);
Ok(())
}
async fn stop_sound(
&mut self,
player: Resource<Player>,
sound: Option<pumpkin::plugin::sounds::Sound>,
category: Option<pumpkin::plugin::sounds::SoundCategory>,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let sound_rl = sound.and_then(|s| {
let sound_name = format!("{s:?}").to_lowercase().replace('_', ".");
pumpkin_data::sound::Sound::from_name(&sound_name).map(|s| s.to_name().into())
});
let cat = category.map(super::world::from_wit_sound_category);
player.stop_sound(sound_rl, cat);
Ok(())
}
async fn play_custom_sound(
&mut self,
player: Resource<Player>,
sound_name: String,
category: pumpkin::plugin::sounds::SoundCategory,
volume: f32,
pitch: f32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let internal_category = super::world::from_wit_sound_category(category);
let pos = player.position();
player.play_custom_sound(&sound_name, internal_category, &pos, volume, pitch);
Ok(())
}
async fn play_custom_sound_at(
&mut self,
player: Resource<Player>,
pos: pumpkin::plugin::common::Position,
sound_name: String,
category: pumpkin::plugin::sounds::SoundCategory,
volume: f32,
pitch: f32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let internal_category = super::world::from_wit_sound_category(category);
player.play_custom_sound(
&sound_name,
internal_category,
&pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2),
volume,
pitch,
);
Ok(())
}
async fn stop_custom_sound(
&mut self,
player: Resource<Player>,
sound_name: Option<String>,
category: Option<pumpkin::plugin::sounds::SoundCategory>,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let sound_rl = sound_name
.as_deref()
.map(pumpkin_util::resource_location::ResourceLocation::from);
let cat = category.map(super::world::from_wit_sound_category);
player.stop_sound(sound_rl, cat);
Ok(())
}
async fn spawn_particles(
&mut self,
player: Resource<Player>,
particle: pumpkin::plugin::particles::Particle,
pos: pumpkin::plugin::common::Position,
count: u32,
offset: pumpkin::plugin::common::Position,
max_speed: f32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let particle_data =
pumpkin_data::particle::Particle::from_id(particle as u16).ok_or_else(|| {
wasmtime::Error::msg(format!("Unknown particle ID: {}", particle as u16))
})?;
player.spawn_particles(
particle_data,
pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2),
count,
pumpkin_util::math::vector3::Vector3::new(
offset.0 as f32,
offset.1 as f32,
offset.2 as f32,
),
max_speed,
);
Ok(())
}
async fn send_block_change(
&mut self,
player: Resource<Player>,
pos: pumpkin::plugin::common::BlockPos,
block_id: u16,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.send_block_change(
pumpkin_util::math::position::BlockPos(pumpkin_util::math::vector3::Vector3::new(
pos.x, pos.y, pos.z,
)),
block_id,
);
Ok(())
}
async fn reset_block_change(
&mut self,
player: Resource<Player>,
pos: pumpkin::plugin::common::BlockPos,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.reset_block_change(pumpkin_util::math::position::BlockPos(
pumpkin_util::math::vector3::Vector3::new(pos.x, pos.y, pos.z),
));
Ok(())
}
async fn send_hurt_animation(
&mut self,
player: Resource<Player>,
yaw: f32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.send_hurt_animation(yaw);
Ok(())
}
async fn open_book(
&mut self,
player: Resource<Player>,
hand: pumpkin::plugin::common::Hand,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let hand = match hand {
pumpkin::plugin::common::Hand::Right => pumpkin_util::Hand::Right,
pumpkin::plugin::common::Hand::Left => pumpkin_util::Hand::Left,
};
player.open_book(hand);
Ok(())
}
async fn open_sign_editor(
&mut self,
player: Resource<Player>,
pos: pumpkin::plugin::common::BlockPos,
is_front_text: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.open_sign_editor(
pumpkin_util::math::position::BlockPos(pumpkin_util::math::vector3::Vector3::new(
pos.x, pos.y, pos.z,
)),
is_front_text,
);
Ok(())
}
async fn set_velocity(
&mut self,
player: Resource<Player>,
velocity: pumpkin::plugin::common::Position,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.set_velocity(pumpkin_util::math::vector3::Vector3::new(
velocity.0, velocity.1, velocity.2,
));
Ok(())
}
async fn apply_knockback(
&mut self,
player: Resource<Player>,
strength: f64,
x: f64,
z: f64,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.apply_knockback(strength, x, z);
Ok(())
}
async fn set_movement_locked(
&mut self,
player: Resource<Player>,
locked: bool,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.set_movement_locked(locked);
Ok(())
}
async fn is_movement_locked(&mut self, player: Resource<Player>) -> wasmtime::Result<bool> {
let player = player_from_resource(self, &player)?;
Ok(player.is_movement_locked())
}
async fn set_freeze_ticks(
&mut self,
player: Resource<Player>,
ticks: i32,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
player.set_freeze_ticks(ticks);
Ok(())
}
async fn get_freeze_ticks(&mut self, player: Resource<Player>) -> wasmtime::Result<i32> {
let player = player_from_resource(self, &player)?;
Ok(player.get_freeze_ticks())
}
async fn set_server_links(
&mut self,
player: Resource<Player>,
links: Vec<pumpkin::plugin::player::ServerLink>,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
let mut converted = Vec::new();
for link in links {
converted.push(from_wit_server_link(self, link)?);
}
let protocol_links: Vec<pumpkin_protocol::Link<'_>> = converted
.iter()
.map(|(label, url)| pumpkin_protocol::Link::new(label.clone(), url))
.collect();
player.set_server_links(&protocol_links);
Ok(())
}
async fn add_effect( async fn add_effect(
&mut self, &mut self,
player: Resource<Player>, player: Resource<Player>,
@ -3331,6 +3744,52 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
Ok(()) Ok(())
} }
async fn send_game_event(
&mut self,
player: Resource<pumpkin::plugin::player::JavaPlayer>,
event: pumpkin::plugin::player::ClientGameEvent,
value: f32,
) -> wasmtime::Result<()> {
let player = self
.resource_table
.get::<crate::plugin::loader::wasm::wasm_host::state::JavaPlayerResource>(
&Resource::new_own(player.rep()),
)
.map_err(|_| wasmtime::Error::msg("invalid java-player resource handle"))?
.provider
.clone();
let internal_event = from_wit_client_game_event(event);
player.send_game_event(internal_event, value);
Ok(())
}
async fn send_entity_status(
&mut self,
player: Resource<pumpkin::plugin::player::JavaPlayer>,
entity_id: i32,
status: pumpkin::plugin::entity_statuses::EntityStatus,
) -> wasmtime::Result<()> {
let player = self
.resource_table
.get::<crate::plugin::loader::wasm::wasm_host::state::JavaPlayerResource>(
&Resource::new_own(player.rep()),
)
.map_err(|_| wasmtime::Error::msg("invalid java-player resource handle"))?
.provider
.clone();
// SAFETY: The WIT enum variants and discriminants are 1:1 generated from entity_statuses.json
let internal_status: pumpkin_data::entity_status::EntityStatus =
unsafe { std::mem::transmute(status as u8) };
let packet = pumpkin_protocol::java::client::play::CEntityStatus::new(
entity_id,
internal_status as u8 as i8,
);
player.try_send_client_packet(&packet);
Ok(())
}
async fn drop( async fn drop(
&mut self, &mut self,
rep: Resource<pumpkin::plugin::player::JavaPlayer>, rep: Resource<pumpkin::plugin::player::JavaPlayer>,

View File

@ -304,6 +304,34 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
Ok(()) Ok(())
} }
async fn delete_message_by_signature(
&mut self,
_rep: Resource<Server>,
signature: Vec<u8>,
) -> wasmtime::Result<()> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
let packet = pumpkin_protocol::java::client::play::CDeleteChat::from_signature(&signature);
server.broadcast_packet_all(&packet);
Ok(())
}
async fn delete_message_by_id(
&mut self,
_rep: Resource<Server>,
signature_id: i32,
) -> wasmtime::Result<()> {
let server = self
.server
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
let packet = pumpkin_protocol::java::client::play::CDeleteChat::from_cache_id(signature_id);
server.broadcast_packet_all(&packet);
Ok(())
}
async fn broadcast_tab_list_header_footer( async fn broadcast_tab_list_header_footer(
&mut self, &mut self,
_rep: Resource<Server>, _rep: Resource<Server>,
@ -618,6 +646,27 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
self.add_datapack_manager(server.clone()) self.add_datapack_manager(server.clone())
} }
async fn set_server_links(
&mut self,
_rep: Resource<Server>,
links: Vec<pumpkin::plugin::player::ServerLink>,
) -> wasmtime::Result<()> {
let server = self
.server
.clone()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
let mut converted = Vec::new();
for link in links {
converted.push(super::player::from_wit_server_link(self, link)?);
}
let protocol_links: Vec<pumpkin_protocol::Link<'_>> = converted
.iter()
.map(|(label, url)| pumpkin_protocol::Link::new(label.clone(), url))
.collect();
server.broadcast_server_links(&protocol_links);
Ok(())
}
async fn drop(&mut self, rep: Resource<Server>) -> wasmtime::Result<()> { async fn drop(&mut self, rep: Resource<Server>) -> wasmtime::Result<()> {
self.resource_table self.resource_table
.delete::<ServerResource>(Resource::new_own(rep.rep())) .delete::<ServerResource>(Resource::new_own(rep.rep()))

View File

@ -864,7 +864,7 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
&mut self, &mut self,
world: Resource<World>, world: Resource<World>,
sound: pumpkin::plugin::sounds::Sound, sound: pumpkin::plugin::sounds::Sound,
category: pumpkin::plugin::world::SoundCategory, category: pumpkin::plugin::sounds::SoundCategory,
pos: pumpkin::plugin::common::Position, pos: pumpkin::plugin::common::Position,
volume: f32, volume: f32,
pitch: f32, pitch: f32,
@ -874,38 +874,7 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
let sound_data = pumpkin_data::sound::Sound::from_name(&sound_name) let sound_data = pumpkin_data::sound::Sound::from_name(&sound_name)
.ok_or_else(|| wasmtime::Error::msg(format!("Unknown sound: {sound_name}")))?; .ok_or_else(|| wasmtime::Error::msg(format!("Unknown sound: {sound_name}")))?;
let internal_category = match category { let internal_category = from_wit_sound_category(category);
pumpkin::plugin::world::SoundCategory::Master => {
pumpkin_data::sound::SoundCategory::Master
}
pumpkin::plugin::world::SoundCategory::Music => {
pumpkin_data::sound::SoundCategory::Music
}
pumpkin::plugin::world::SoundCategory::Records => {
pumpkin_data::sound::SoundCategory::Records
}
pumpkin::plugin::world::SoundCategory::Weather => {
pumpkin_data::sound::SoundCategory::Weather
}
pumpkin::plugin::world::SoundCategory::Blocks => {
pumpkin_data::sound::SoundCategory::Blocks
}
pumpkin::plugin::world::SoundCategory::Hostile => {
pumpkin_data::sound::SoundCategory::Hostile
}
pumpkin::plugin::world::SoundCategory::Neutral => {
pumpkin_data::sound::SoundCategory::Neutral
}
pumpkin::plugin::world::SoundCategory::Players => {
pumpkin_data::sound::SoundCategory::Players
}
pumpkin::plugin::world::SoundCategory::Ambient => {
pumpkin_data::sound::SoundCategory::Ambient
}
pumpkin::plugin::world::SoundCategory::Voice => {
pumpkin_data::sound::SoundCategory::Voice
}
};
world_ref.provider.play_sound_raw( world_ref.provider.play_sound_raw(
sound_data as u16, sound_data as u16,
@ -917,6 +886,27 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
Ok(()) Ok(())
} }
async fn play_custom_sound(
&mut self,
world: Resource<World>,
sound_name: String,
category: pumpkin::plugin::sounds::SoundCategory,
pos: pumpkin::plugin::common::Position,
volume: f32,
pitch: f32,
) -> wasmtime::Result<()> {
let world_ref = self.get_world_res(&world)?;
let internal_category = from_wit_sound_category(category);
world_ref.provider.play_custom_sound(
&sound_name,
internal_category,
&pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2),
volume,
pitch,
);
Ok(())
}
async fn spawn_particle( async fn spawn_particle(
&mut self, &mut self,
world: Resource<World>, world: Resource<World>,
@ -2307,6 +2297,41 @@ impl pumpkin_world::generation::generator::CustomChunkGenerator for WasmChunkGen
} }
} }
#[must_use]
pub const fn from_wit_sound_category(
category: pumpkin::plugin::sounds::SoundCategory,
) -> pumpkin_data::sound::SoundCategory {
match category {
pumpkin::plugin::sounds::SoundCategory::Master => {
pumpkin_data::sound::SoundCategory::Master
}
pumpkin::plugin::sounds::SoundCategory::Music => pumpkin_data::sound::SoundCategory::Music,
pumpkin::plugin::sounds::SoundCategory::Records => {
pumpkin_data::sound::SoundCategory::Records
}
pumpkin::plugin::sounds::SoundCategory::Weather => {
pumpkin_data::sound::SoundCategory::Weather
}
pumpkin::plugin::sounds::SoundCategory::Blocks => {
pumpkin_data::sound::SoundCategory::Blocks
}
pumpkin::plugin::sounds::SoundCategory::Hostile => {
pumpkin_data::sound::SoundCategory::Hostile
}
pumpkin::plugin::sounds::SoundCategory::Neutral => {
pumpkin_data::sound::SoundCategory::Neutral
}
pumpkin::plugin::sounds::SoundCategory::Players => {
pumpkin_data::sound::SoundCategory::Players
}
pumpkin::plugin::sounds::SoundCategory::Ambient => {
pumpkin_data::sound::SoundCategory::Ambient
}
pumpkin::plugin::sounds::SoundCategory::Voice => pumpkin_data::sound::SoundCategory::Voice,
pumpkin::plugin::sounds::SoundCategory::Ui => pumpkin_data::sound::SoundCategory::Ui,
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::pumpkin; use super::pumpkin;

View File

@ -748,6 +748,12 @@ impl Server {
} }
} }
/// Broadcasts custom server links to all players on the server.
pub fn broadcast_server_links(&self, links: &[pumpkin_protocol::Link<'_>]) {
let packet = pumpkin_protocol::java::client::play::CPlayServerLinks::new(links);
self.broadcast_packet_all(&packet);
}
pub fn broadcast_tab_list_header_footer(&self, header: &TextComponent, footer: &TextComponent) { pub fn broadcast_tab_list_header_footer(&self, header: &TextComponent, footer: &TextComponent) {
let packet = CTabList::new(header, footer); let packet = CTabList::new(header, footer);
for world in self.worlds.load().iter() { for world in self.worlds.load().iter() {

View File

@ -126,7 +126,9 @@ use pumpkin_protocol::{
}; };
use pumpkin_protocol::{ use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer, codec::item_stack_seralizer::ItemStackSerializer,
java::client::play::{CBlockEvent, CRemoveMobEffect, CSetEquipment, CUpdateMobEffect}, java::client::play::{
CBlockEvent, CParticle, CRemoveMobEffect, CSetEquipment, CUpdateMobEffect,
},
}; };
use pumpkin_util::resource_location::ResourceLocation; use pumpkin_util::resource_location::ResourceLocation;
use pumpkin_util::text::{TextComponent, color::NamedColor}; use pumpkin_util::text::{TextComponent, color::NamedColor};
@ -1207,6 +1209,52 @@ impl World {
self.play_sound_raw(sound as u16, category, position, volume, pitch); self.play_sound_raw(sound as u16, category, position, volume, pitch);
} }
/// Plays a custom sound event by identifier for all players in range.
pub fn play_custom_sound(
&self,
sound_name: &str,
category: SoundCategory,
position: &Vector3<f64>,
volume: f32,
pitch: f32,
) {
let seed = rand::random::<f64>();
let packet = CSoundEffect::new(
pumpkin_protocol::IdOr::Value(pumpkin_protocol::SoundEvent {
sound_name: sound_name.into(),
range: None,
}),
category,
position,
volume,
pitch,
seed,
);
self.broadcast_packet_all(&packet);
}
/// Spawns a cluster of particles in the world for all players in range.
pub fn spawn_particles(
&self,
particle: pumpkin_data::particle::Particle,
pos: Vector3<f64>,
count: u32,
offset: Vector3<f32>,
max_speed: f32,
) {
let packet = CParticle::new(
false,
false,
pos,
offset,
max_speed,
count as i32,
(particle.to_id() as i32).into(),
&[],
);
self.broadcast_packet_all(&packet);
}
/// Plays a Bedrock level sound for players close enough to hear it. /// Plays a Bedrock level sound for players close enough to hear it.
pub fn play_bedrock_level_sound( pub fn play_bedrock_level_sound(
&self, &self,

View File

@ -73,6 +73,7 @@ mod registry;
mod remap; mod remap;
mod scoreboard_slot; mod scoreboard_slot;
mod screen; mod screen;
mod sdk;
mod sound; mod sound;
mod sound_category; mod sound_category;
mod spawn_egg; mod spawn_egg;
@ -174,6 +175,7 @@ pub fn main() {
let filters: Vec<String> = std::env::args().skip(1).collect(); let filters: Vec<String> = std::env::args().skip(1).collect();
let build_functions: Vec<_> = if filters.is_empty() { let build_functions: Vec<_> = if filters.is_empty() {
wit::main(); wit::main();
sdk::main();
build_functions build_functions
} else { } else {
build_functions build_functions

View File

@ -0,0 +1,103 @@
use heck::ToPascalCase;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::fs;
#[derive(Deserialize)]
struct BlocksAsset {
blocks: Vec<BlockEntry>,
}
#[derive(Deserialize)]
struct BlockEntry {
name: String,
}
pub fn build() -> String {
let json: BlocksAsset =
serde_json::from_str(&fs::read_to_string("../../assets/blocks.json").unwrap())
.expect("Failed to parse blocks.json");
let mut names: BTreeSet<String> = BTreeSet::new();
for b in json.blocks {
names.insert(b.name);
}
let mut code = String::new();
code.push_str("// Generated by pumpkin-codegen. Do not edit.\n");
code.push_str("#![allow(missing_docs, clippy::all)]\n\n");
code.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n");
code.push_str("pub enum BlockType {\n");
for name in &names {
let pascal = name.to_pascal_case();
code.push_str(&format!(" {pascal},\n"));
}
code.push_str("}\n\n");
code.push_str("impl BlockType {\n");
code.push_str(
" /// Returns the plain block name (e.g. `\"stone\"` or `\"diamond_block\"`).\n",
);
code.push_str(" #[must_use]\n");
code.push_str(" pub const fn as_str(&self) -> &'static str {\n");
code.push_str(" match self {\n");
for name in &names {
let pascal = name.to_pascal_case();
code.push_str(&format!(" Self::{pascal} => \"{name}\",\n"));
}
code.push_str(" }\n");
code.push_str(" }\n\n");
code.push_str(
" /// Returns the namespaced resource location (e.g. `\"minecraft:stone\"`).\n",
);
code.push_str(" #[must_use]\n");
code.push_str(" pub const fn resource_location(&self) -> &'static str {\n");
code.push_str(" match self {\n");
for name in &names {
let pascal = name.to_pascal_case();
code.push_str(&format!(
" Self::{pascal} => \"minecraft:{name}\",\n"
));
}
code.push_str(" }\n");
code.push_str(" }\n\n");
code.push_str(" /// Resolves a typed `BlockType` from a name or namespaced identifier.\n");
code.push_str(" #[must_use]\n");
code.push_str(" pub fn from_name(name: &str) -> Option<Self> {\n");
code.push_str(" let clean = name.strip_prefix(\"minecraft:\").unwrap_or(name);\n");
code.push_str(" match clean {\n");
for name in &names {
let pascal = name.to_pascal_case();
code.push_str(&format!(
" \"{name}\" => Some(Self::{pascal}),\n"
));
}
code.push_str(" _ => None,\n");
code.push_str(" }\n");
code.push_str(" }\n\n");
code.push_str(" /// Resolves a typed `BlockType` from a registry key.\n");
code.push_str(" #[must_use]\n");
code.push_str(" pub fn from_registry_key(key: &str) -> Option<Self> {\n");
code.push_str(" Self::from_name(key)\n");
code.push_str(" }\n");
code.push_str("}\n\n");
code.push_str("impl std::fmt::Display for BlockType {\n");
code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
code.push_str(" write!(f, \"{}\", self.resource_location())\n");
code.push_str(" }\n");
code.push_str("}\n\n");
code.push_str("impl AsRef<str> for BlockType {\n");
code.push_str(" fn as_ref(&self) -> &str {\n");
code.push_str(" self.as_str()\n");
code.push_str(" }\n");
code.push_str("}\n");
code
}

View File

@ -0,0 +1,87 @@
use heck::ToPascalCase;
use std::collections::BTreeMap;
use std::fs;
pub fn build() -> String {
let json: BTreeMap<String, serde_json::Value> =
serde_json::from_str(&fs::read_to_string("../../assets/items.json").unwrap())
.expect("Failed to parse items.json");
let mut code = String::new();
code.push_str("// Generated by pumpkin-codegen. Do not edit.\n");
code.push_str("#![allow(missing_docs, clippy::all)]\n\n");
code.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n");
code.push_str("pub enum Item {\n");
for name in json.keys() {
let pascal = name.to_pascal_case();
code.push_str(&format!(" {pascal},\n"));
}
code.push_str("}\n\n");
code.push_str("impl Item {\n");
code.push_str(
" /// Returns the plain item name (e.g. `\"diamond\"` or `\"netherite_sword\"`).\n",
);
code.push_str(" #[must_use]\n");
code.push_str(" pub const fn as_str(&self) -> &'static str {\n");
code.push_str(" match self {\n");
for name in json.keys() {
let pascal = name.to_pascal_case();
code.push_str(&format!(" Self::{pascal} => \"{name}\",\n"));
}
code.push_str(" }\n");
code.push_str(" }\n\n");
code.push_str(
" /// Returns the namespaced resource location (e.g. `\"minecraft:diamond\"`).\n",
);
code.push_str(" #[must_use]\n");
code.push_str(" pub const fn resource_location(&self) -> &'static str {\n");
code.push_str(" match self {\n");
for name in json.keys() {
let pascal = name.to_pascal_case();
code.push_str(&format!(
" Self::{pascal} => \"minecraft:{name}\",\n"
));
}
code.push_str(" }\n");
code.push_str(" }\n\n");
code.push_str(" /// Resolves a typed `Item` from a name or namespaced identifier.\n");
code.push_str(" #[must_use]\n");
code.push_str(" pub fn from_name(name: &str) -> Option<Self> {\n");
code.push_str(" let clean = name.strip_prefix(\"minecraft:\").unwrap_or(name);\n");
code.push_str(" match clean {\n");
for name in json.keys() {
let pascal = name.to_pascal_case();
code.push_str(&format!(
" \"{name}\" => Some(Self::{pascal}),\n"
));
}
code.push_str(" _ => None,\n");
code.push_str(" }\n");
code.push_str(" }\n\n");
code.push_str(" /// Resolves a typed `Item` from a registry key.\n");
code.push_str(" #[must_use]\n");
code.push_str(" pub fn from_registry_key(key: &str) -> Option<Self> {\n");
code.push_str(" Self::from_name(key)\n");
code.push_str(" }\n");
code.push_str("}\n\n");
code.push_str("impl std::fmt::Display for Item {\n");
code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
code.push_str(" write!(f, \"{}\", self.resource_location())\n");
code.push_str(" }\n");
code.push_str("}\n\n");
code.push_str("impl AsRef<str> for Item {\n");
code.push_str(" fn as_ref(&self) -> &str {\n");
code.push_str(" self.as_str()\n");
code.push_str(" }\n");
code.push_str("}\n");
code
}

View File

@ -0,0 +1,21 @@
pub mod block;
pub mod item;
use std::fs;
use std::path::Path;
pub const SDK_OUT_DIR: &str = "../../crates/pumpkin-plugin-api/src/generated";
pub fn main() {
fs::create_dir_all(SDK_OUT_DIR).expect("Failed to create SDK generated directory");
let targets: Vec<(fn() -> String, &str)> =
vec![(item::build, "item.rs"), (block::build, "block.rs")];
for (build_fn, file) in targets {
println!("Generating SDK for {}", file);
let code = build_fn();
let path = Path::new(SDK_OUT_DIR).join(file);
fs::write(&path, code).unwrap_or_else(|_| panic!("Failed to write {file}"));
}
}

View File

@ -1,6 +1,7 @@
use crate::wit::utils::map_type; use crate::wit::utils::map_type_with_defined;
use heck::ToKebabCase; use heck::ToKebabCase;
use semver::Version; use semver::Version;
use std::collections::HashSet;
use std::{fs, path::Path}; use std::{fs, path::Path};
use syn::{Fields, Item}; use syn::{Fields, Item};
use wit_encoder::{ use wit_encoder::{
@ -20,18 +21,39 @@ pub fn build() -> String {
let mut serverbound_variant = Variant::empty(); let mut serverbound_variant = Variant::empty();
let mut clientbound_variant = Variant::empty(); let mut clientbound_variant = Variant::empty();
let mut defined_cases = HashSet::new();
let dirs = [
"../../crates/pumpkin-protocol/src/bedrock/server",
"../../crates/pumpkin-protocol/src/bedrock/client",
"../../crates/pumpkin-protocol/src/bedrock/network_item.rs",
];
let defined_types = collect_defined_types(&dirs);
// Process serverbound packets // Process serverbound packets
process_packets( process_packets(
"../../crates/pumpkin-protocol/src/bedrock/server", "../../crates/pumpkin-protocol/src/bedrock/server",
&mut interface, &mut interface,
&mut serverbound_variant, &mut serverbound_variant,
&mut defined_cases,
&defined_types,
); );
// Process clientbound packets // Process clientbound packets
process_packets( process_packets(
"../../crates/pumpkin-protocol/src/bedrock/client", "../../crates/pumpkin-protocol/src/bedrock/client",
&mut interface, &mut interface,
&mut clientbound_variant, &mut clientbound_variant,
&mut defined_cases,
&defined_types,
);
// Process network_item helper types
process_packets(
"../../crates/pumpkin-protocol/src/bedrock/network_item.rs",
&mut interface,
&mut clientbound_variant,
&mut defined_cases,
&defined_types,
); );
// Add an 'unknown' fallback variant (no payload) — raw payload is carried on the event record // Add an 'unknown' fallback variant (no payload) — raw payload is carried on the event record
@ -51,7 +73,75 @@ pub fn build() -> String {
package.to_string() package.to_string()
} }
fn process_packets(dir: &str, interface: &mut Interface, variant: &mut Variant) { fn collect_defined_types(dirs: &[&str]) -> HashSet<String> {
let mut defined = HashSet::new();
for &dir in dirs {
collect_defined_types_in_path(Path::new(dir), &mut defined);
}
defined
}
fn collect_defined_types_in_path(path: &Path, defined: &mut HashSet<String>) {
if path.is_file() {
if path.extension().is_some_and(|ext| ext == "rs")
&& path.file_name().is_some_and(|name| name != "mod.rs")
&& let Ok(content) = fs::read_to_string(path)
&& let Ok(file) = syn::parse_file(&content)
{
for item in file.items {
match item {
Item::Struct(s)
if has_static_packet_attr(&s.attrs) || is_valid_helper_struct(&s) =>
{
defined.insert(s.ident.to_string().to_kebab_case());
}
Item::Enum(e)
if has_static_packet_attr(&e.attrs) || is_valid_helper_enum(&e) =>
{
defined.insert(e.ident.to_string().to_kebab_case());
}
_ => {}
}
}
}
return;
}
let Ok(paths) = fs::read_dir(path) else {
return;
};
for entry in paths.flatten() {
let p = entry.path();
collect_defined_types_in_path(&p, defined);
}
}
fn is_valid_helper_struct(s: &syn::ItemStruct) -> bool {
matches!(s.vis, syn::Visibility::Public(_))
&& matches!(s.fields, Fields::Named(_))
&& s.generics.type_params().next().is_none()
&& s.generics.const_params().next().is_none()
}
fn is_valid_helper_enum(e: &syn::ItemEnum) -> bool {
matches!(e.vis, syn::Visibility::Public(_))
&& e.generics.type_params().next().is_none()
&& e.generics.const_params().next().is_none()
}
fn process_packets(
dir: &str,
interface: &mut Interface,
variant: &mut Variant,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let path = Path::new(dir);
if path.is_file() {
parse_packet_file(path, interface, variant, defined_cases, defined_types);
return;
}
let paths = fs::read_dir(dir).expect("Failed to read packet directory"); let paths = fs::read_dir(dir).expect("Failed to read packet directory");
let mut sorted_paths: Vec<_> = paths let mut sorted_paths: Vec<_> = paths
.map(|e| e.expect("Failed to read entry").path()) .map(|e| e.expect("Failed to read entry").path())
@ -60,13 +150,19 @@ fn process_packets(dir: &str, interface: &mut Interface, variant: &mut Variant)
for path in sorted_paths { for path in sorted_paths {
if path.is_dir() { if path.is_dir() {
process_packets(path.to_str().unwrap(), interface, variant); process_packets(
path.to_str().unwrap(),
interface,
variant,
defined_cases,
defined_types,
);
continue; continue;
} }
if path.extension().is_some_and(|ext| ext == "rs") if path.extension().is_some_and(|ext| ext == "rs")
&& path.file_name().is_some_and(|name| name != "mod.rs") && path.file_name().is_some_and(|name| name != "mod.rs")
{ {
parse_packet_file(&path, interface, variant); parse_packet_file(&path, interface, variant, defined_cases, defined_types);
} }
} }
} }
@ -104,39 +200,83 @@ fn has_static_packet_attr(attrs: &[syn::Attribute]) -> bool {
fn collect_types( fn collect_types(
fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<WitType> { ) -> Vec<WitType> {
fields fields
.into_iter() .into_iter()
.map(|field| map_type(&field.ty)) .map(|field| map_type_with_defined(&field.ty, Some(defined_types)))
.collect() .collect()
} }
fn collect_fields( fn collect_fields(
named_fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, named_fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<Field> { ) -> Vec<Field> {
named_fields named_fields
.into_iter() .into_iter()
.map(|field| { .map(|field| {
let field_name = field.ident.as_ref().unwrap().to_string().to_kebab_case(); let field_name = field.ident.as_ref().unwrap().to_string().to_kebab_case();
let field_type = map_type(&field.ty); let field_type = map_type_with_defined(&field.ty, Some(defined_types));
Field::new(field_name, field_type) Field::new(field_name, field_type)
}) })
.collect() .collect()
} }
fn process_struct(s: syn::ItemStruct, interface: &mut Interface, variant: &mut Variant) { fn process_struct(
s: syn::ItemStruct,
interface: &mut Interface,
variant: &mut Variant,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let wit_name = s.ident.to_string().to_kebab_case(); let wit_name = s.ident.to_string().to_kebab_case();
if !defined_cases.insert(wit_name.clone()) {
return;
}
let fields_list = match s.fields { let fields_list = match s.fields {
Fields::Named(fields) => collect_fields(fields.named), Fields::Named(fields) => collect_fields(fields.named, defined_types),
_ => Vec::new(), _ => Vec::new(),
}; };
register_wit_type(wit_name, fields_list, interface, variant, None); register_wit_type(wit_name, fields_list, interface, variant, None);
} }
fn process_enum(e: syn::ItemEnum, interface: &mut Interface, variant: &mut Variant) { fn process_helper_struct(
s: syn::ItemStruct,
interface: &mut Interface,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let wit_name = s.ident.to_string().to_kebab_case();
if !defined_cases.insert(wit_name.clone()) {
return;
}
let fields_list = match s.fields {
Fields::Named(fields) => collect_fields(fields.named, defined_types),
_ => Vec::new(),
};
if !fields_list.is_empty() {
interface.type_def(TypeDef::new(
wit_name,
TypeDefKind::Record(Record::new(fields_list)),
));
}
}
fn process_enum(
e: syn::ItemEnum,
interface: &mut Interface,
variant: &mut Variant,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let enum_wit_name = e.ident.to_string().to_kebab_case(); let enum_wit_name = e.ident.to_string().to_kebab_case();
if !defined_cases.insert(enum_wit_name.clone()) {
return;
}
let mut cases = Vec::new(); let mut cases = Vec::new();
for v in e.variants { for v in e.variants {
@ -144,18 +284,22 @@ fn process_enum(e: syn::ItemEnum, interface: &mut Interface, variant: &mut Varia
match v.fields { match v.fields {
Fields::Named(fields) => { Fields::Named(fields) => {
let fields_list = collect_fields(fields.named); let sub_record_name = format!("{}-{}", enum_wit_name, variant_wit_name);
register_wit_type( let fields_list = collect_fields(fields.named, defined_types);
if !fields_list.is_empty() && defined_cases.insert(sub_record_name.clone()) {
interface.type_def(TypeDef::new(
sub_record_name.clone(),
TypeDefKind::Record(Record::new(fields_list)),
));
}
cases.push(VariantCase::value(
variant_wit_name, variant_wit_name,
fields_list, WitType::named(sub_record_name),
interface, ));
variant,
Some(enum_wit_name.clone()),
);
} }
Fields::Unnamed(fields) => { Fields::Unnamed(fields) => {
let types = collect_types(fields.unnamed); let types = collect_types(fields.unnamed, defined_types);
match types.len() { match types.len() {
0 => cases.push(VariantCase::empty(variant_wit_name)), 0 => cases.push(VariantCase::empty(variant_wit_name)),
1 => cases.push(VariantCase::value( 1 => cases.push(VariantCase::value(
@ -177,7 +321,7 @@ fn process_enum(e: syn::ItemEnum, interface: &mut Interface, variant: &mut Varia
WitType::named(enum_wit_name.clone()), WitType::named(enum_wit_name.clone()),
)); ));
// define weather to use Enum or Variant // define whether to use Enum or Variant
let all_empty = cases.iter().all(|c| c.type_().is_none()); let all_empty = cases.iter().all(|c| c.type_().is_none());
if all_empty { if all_empty {
interface.type_def(TypeDef::new( interface.type_def(TypeDef::new(
@ -194,17 +338,94 @@ fn process_enum(e: syn::ItemEnum, interface: &mut Interface, variant: &mut Varia
} }
} }
fn parse_packet_file(path: &Path, interface: &mut Interface, variant: &mut Variant) { fn process_helper_enum(
e: syn::ItemEnum,
interface: &mut Interface,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let enum_wit_name = e.ident.to_string().to_kebab_case();
if !defined_cases.insert(enum_wit_name.clone()) {
return;
}
let mut cases = Vec::new();
for v in e.variants {
let variant_wit_name = v.ident.to_string().to_kebab_case();
match v.fields {
Fields::Named(fields) => {
let sub_record_name = format!("{}-{}", enum_wit_name, variant_wit_name);
let fields_list = collect_fields(fields.named, defined_types);
if !fields_list.is_empty() && defined_cases.insert(sub_record_name.clone()) {
interface.type_def(TypeDef::new(
sub_record_name.clone(),
TypeDefKind::Record(Record::new(fields_list)),
));
}
cases.push(VariantCase::value(
variant_wit_name,
WitType::named(sub_record_name),
));
}
Fields::Unnamed(fields) => {
let types = collect_types(fields.unnamed, defined_types);
match types.len() {
0 => cases.push(VariantCase::empty(variant_wit_name)),
1 => cases.push(VariantCase::value(
variant_wit_name,
types.into_iter().next().unwrap(),
)),
_ => cases.push(VariantCase::value(variant_wit_name, WitType::tuple(types))),
}
}
Fields::Unit => {
cases.push(VariantCase::empty(variant_wit_name));
}
}
}
let all_empty = cases.iter().all(|c| c.type_().is_none());
if all_empty {
interface.type_def(TypeDef::new(
enum_wit_name,
TypeDefKind::Enum(Enum::from_iter(
cases.into_iter().map(|c| EnumCase::new(c.name().clone())),
)),
));
} else {
interface.type_def(TypeDef::new(
enum_wit_name,
TypeDefKind::Variant(Variant::from(cases)),
));
}
}
fn parse_packet_file(
path: &Path,
interface: &mut Interface,
variant: &mut Variant,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let content = fs::read_to_string(path).expect("Failed to read file"); let content = fs::read_to_string(path).expect("Failed to read file");
let file = syn::parse_file(&content).expect("Failed to parse file"); let file = syn::parse_file(&content).expect("Failed to parse file");
for item in file.items { for item in file.items {
match item { match item {
Item::Struct(s) if has_static_packet_attr(&s.attrs) => { Item::Struct(s) if has_static_packet_attr(&s.attrs) => {
process_struct(s, interface, variant); process_struct(s, interface, variant, defined_cases, defined_types);
} }
Item::Enum(e) if has_static_packet_attr(&e.attrs) => { Item::Enum(e) if has_static_packet_attr(&e.attrs) => {
process_enum(e, interface, variant); process_enum(e, interface, variant, defined_cases, defined_types);
}
Item::Struct(s) if is_valid_helper_struct(&s) => {
process_helper_struct(s, interface, defined_cases, defined_types);
}
Item::Enum(e) if is_valid_helper_enum(&e) => {
process_helper_enum(e, interface, defined_cases, defined_types);
} }
_ => {} _ => {}
} }

View File

@ -0,0 +1,30 @@
use heck::ToKebabCase;
use semver::Version;
use std::{collections::BTreeMap, fs};
use wit_encoder::{Enum, Interface, Package, PackageName, TypeDef, TypeDefKind};
pub fn build() -> String {
let events: BTreeMap<String, u8> =
serde_json::from_str(&fs::read_to_string("../../assets/entity_statuses.json").unwrap())
.expect("Failed to parse entity_statuses.json");
let mut package = Package::new(PackageName::new(
"pumpkin",
"plugin",
Some(Version::new(0, 1, 0)),
));
let mut interface = Interface::new("entity-statuses");
let mut status_enum = Enum::empty();
for (event_name, _) in events {
status_enum.case(event_name.to_kebab_case());
}
interface.type_def(TypeDef::new(
"entity-status",
TypeDefKind::Enum(status_enum),
));
package.interface(interface);
package.to_string()
}

View File

@ -0,0 +1,33 @@
use semver::Version;
use std::fs;
use wit_encoder::{Enum, Interface, Package, PackageName, TypeDef, TypeDefKind};
pub fn build() -> String {
let game_events: Vec<String> =
serde_json::from_str(&fs::read_to_string("../../assets/game_event.json").unwrap())
.expect("Failed to parse game_event.json");
let mut package = Package::new(PackageName::new(
"pumpkin",
"plugin",
Some(Version::new(0, 1, 0)),
));
let mut interface = Interface::new("game-events");
let mut game_event_enum = Enum::empty();
for game_event in game_events {
let name = game_event
.strip_prefix("minecraft:")
.unwrap_or(&game_event)
.replace('_', "-");
game_event_enum.case(name);
}
interface.type_def(TypeDef::new(
"game-event",
TypeDefKind::Enum(game_event_enum),
));
package.interface(interface);
package.to_string()
}

View File

@ -1,4 +1,4 @@
use crate::wit::utils::map_type; use crate::wit::utils::map_type_with_defined;
use heck::ToKebabCase; use heck::ToKebabCase;
use semver::Version; use semver::Version;
use std::collections::HashSet; use std::collections::HashSet;
@ -22,29 +22,47 @@ pub fn build() -> String {
let mut serverbound_variant = Variant::empty(); let mut serverbound_variant = Variant::empty();
let mut clientbound_variant = Variant::empty(); let mut clientbound_variant = Variant::empty();
let mut serverbound_cases = HashSet::new(); let mut defined_cases = HashSet::new();
let mut clientbound_cases = HashSet::new();
let server_states = &["config", "handshake", "login", "play", "status"];
let client_states = &["config", "login", "play", "status"];
let mut dirs = Vec::new();
for state in server_states {
dirs.push((
format!("../../crates/pumpkin-protocol/src/java/server/{}", state),
*state,
));
}
for state in client_states {
dirs.push((
format!("../../crates/pumpkin-protocol/src/java/client/{}", state),
*state,
));
}
let defined_types = collect_defined_types(&dirs);
// Process serverbound packets // Process serverbound packets
let server_states = &["config", "handshake", "login", "play", "status"];
for state in server_states { for state in server_states {
process_packets( process_packets(
&format!("../../crates/pumpkin-protocol/src/java/server/{}", state), &format!("../../crates/pumpkin-protocol/src/java/server/{}", state),
state, state,
&mut interface, &mut interface,
&mut serverbound_variant, &mut serverbound_variant,
&mut serverbound_cases, &mut defined_cases,
&defined_types,
); );
} }
// Process clientbound packets // Process clientbound packets
let client_states = &["config", "login", "play", "status"];
for state in client_states { for state in client_states {
process_packets( process_packets(
&format!("../../crates/pumpkin-protocol/src/java/client/{}", state), &format!("../../crates/pumpkin-protocol/src/java/client/{}", state),
state, state,
&mut interface, &mut interface,
&mut clientbound_variant, &mut clientbound_variant,
&mut clientbound_cases, &mut defined_cases,
&defined_types,
); );
} }
@ -65,12 +83,64 @@ pub fn build() -> String {
package.to_string() package.to_string()
} }
fn collect_defined_types(dirs: &[(String, &str)]) -> HashSet<String> {
let mut defined = HashSet::new();
for (dir, state) in dirs {
let Ok(paths) = fs::read_dir(dir) else {
continue;
};
for entry in paths.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "rs")
&& path.file_name().is_some_and(|name| name != "mod.rs")
{
if let Ok(content) = fs::read_to_string(&path)
&& let Ok(file) = syn::parse_file(&content)
{
for item in file.items {
match item {
Item::Struct(s) if has_java_packet_attr(&s.attrs) => {
defined.insert(wit_name(s.ident.to_string(), state));
}
Item::Enum(e) if has_java_packet_attr(&e.attrs) => {
defined.insert(wit_name(e.ident.to_string(), state));
}
Item::Struct(s) if is_valid_helper_struct(&s) => {
defined.insert(s.ident.to_string().to_kebab_case());
}
Item::Enum(e) if is_valid_helper_enum(&e) => {
defined.insert(e.ident.to_string().to_kebab_case());
}
_ => {}
}
}
}
}
}
}
defined
}
fn is_valid_helper_struct(s: &syn::ItemStruct) -> bool {
matches!(s.vis, syn::Visibility::Public(_))
&& matches!(s.fields, Fields::Named(_))
&& s.generics.type_params().next().is_none()
&& s.generics.const_params().next().is_none()
}
fn is_valid_helper_enum(e: &syn::ItemEnum) -> bool {
matches!(e.vis, syn::Visibility::Public(_))
&& e.generics.type_params().next().is_none()
&& e.generics.const_params().next().is_none()
}
fn process_packets( fn process_packets(
dir: &str, dir: &str,
state: &str, state: &str,
interface: &mut Interface, interface: &mut Interface,
variant: &mut Variant, variant: &mut Variant,
defined_cases: &mut HashSet<String>, defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) { ) {
let paths = fs::read_dir(dir).expect("Failed to read packet directory"); let paths = fs::read_dir(dir).expect("Failed to read packet directory");
let mut sorted_paths: Vec<_> = paths let mut sorted_paths: Vec<_> = paths
@ -79,10 +149,28 @@ fn process_packets(
sorted_paths.sort(); sorted_paths.sort();
for path in sorted_paths { for path in sorted_paths {
if path.is_dir() {
process_packets(
path.to_str().unwrap(),
state,
interface,
variant,
defined_cases,
defined_types,
);
continue;
}
if path.extension().is_some_and(|ext| ext == "rs") if path.extension().is_some_and(|ext| ext == "rs")
&& path.file_name().is_some_and(|name| name != "mod.rs") && path.file_name().is_some_and(|name| name != "mod.rs")
{ {
parse_packet_file(&path, state, interface, variant, defined_cases); parse_packet_file(
&path,
state,
interface,
variant,
defined_cases,
defined_types,
);
} }
} }
} }
@ -93,6 +181,7 @@ fn parse_packet_file(
interface: &mut Interface, interface: &mut Interface,
variant: &mut Variant, variant: &mut Variant,
defined_cases: &mut HashSet<String>, defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) { ) {
let content = fs::read_to_string(path).expect("Failed to read file"); let content = fs::read_to_string(path).expect("Failed to read file");
let file = syn::parse_file(&content).expect("Failed to parse file"); let file = syn::parse_file(&content).expect("Failed to parse file");
@ -100,10 +189,16 @@ fn parse_packet_file(
for item in file.items { for item in file.items {
match item { match item {
Item::Struct(s) if has_java_packet_attr(&s.attrs) => { Item::Struct(s) if has_java_packet_attr(&s.attrs) => {
process_struct(s, state, interface, variant, defined_cases); process_struct(s, state, interface, variant, defined_cases, defined_types);
} }
Item::Enum(e) if has_java_packet_attr(&e.attrs) => { Item::Enum(e) if has_java_packet_attr(&e.attrs) => {
process_enum(e, state, interface, variant, defined_cases); process_enum(e, state, interface, variant, defined_cases, defined_types);
}
Item::Struct(s) if is_valid_helper_struct(&s) => {
process_helper_struct(s, interface, defined_cases, defined_types);
}
Item::Enum(e) if is_valid_helper_enum(&e) => {
process_helper_enum(e, interface, defined_cases, defined_types);
} }
_ => {} _ => {}
} }
@ -143,23 +238,25 @@ fn register_wit_type(
fn collect_types( fn collect_types(
fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<WitType> { ) -> Vec<WitType> {
fields fields
.into_iter() .into_iter()
.filter(|field| extract_type_name(&field.ty) != "DynamicRecipe") .filter(|field| extract_type_name(&field.ty) != "DynamicRecipe")
.map(|field| map_type(&field.ty)) .map(|field| map_type_with_defined(&field.ty, Some(defined_types)))
.collect() .collect()
} }
fn collect_fields( fn collect_fields(
named_fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, named_fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<Field> { ) -> Vec<Field> {
named_fields named_fields
.into_iter() .into_iter()
.filter(|field| extract_type_name(&field.ty) != "DynamicRecipe") .filter(|field| extract_type_name(&field.ty) != "DynamicRecipe")
.map(|field| { .map(|field| {
let field_name = field.ident.as_ref().unwrap().to_string().to_kebab_case(); let field_name = field.ident.as_ref().unwrap().to_string().to_kebab_case();
let field_type = map_type(&field.ty); let field_type = map_type_with_defined(&field.ty, Some(defined_types));
Field::new(field_name, field_type) Field::new(field_name, field_type)
}) })
.collect() .collect()
@ -179,25 +276,50 @@ fn process_struct(
interface: &mut Interface, interface: &mut Interface,
variant: &mut Variant, variant: &mut Variant,
defined_cases: &mut HashSet<String>, defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) { ) {
let wit_name = wit_name(s.ident.to_string(), state); let wit_name = wit_name(s.ident.to_string(), state);
if !defined_cases.insert(wit_name.clone()) { if !defined_cases.insert(wit_name.clone()) {
return; return;
} }
let fields_list = match s.fields { let fields_list = match s.fields {
Fields::Named(fields) => collect_fields(fields.named), Fields::Named(fields) => collect_fields(fields.named, defined_types),
_ => Vec::new(), _ => Vec::new(),
}; };
register_wit_type(wit_name, fields_list, interface, variant, None); register_wit_type(wit_name, fields_list, interface, variant, None);
} }
fn process_helper_struct(
s: syn::ItemStruct,
interface: &mut Interface,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let wit_name = s.ident.to_string().to_kebab_case();
if !defined_cases.insert(wit_name.clone()) {
return;
}
let fields_list = match s.fields {
Fields::Named(fields) => collect_fields(fields.named, defined_types),
_ => Vec::new(),
};
if !fields_list.is_empty() {
interface.type_def(TypeDef::new(
wit_name,
TypeDefKind::Record(Record::new(fields_list)),
));
}
}
fn process_enum( fn process_enum(
e: syn::ItemEnum, e: syn::ItemEnum,
state: &str, state: &str,
interface: &mut Interface, interface: &mut Interface,
variant: &mut Variant, variant: &mut Variant,
defined_cases: &mut HashSet<String>, defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) { ) {
let enum_wit_name = wit_name(e.ident.to_string(), state); let enum_wit_name = wit_name(e.ident.to_string(), state);
if !defined_cases.insert(enum_wit_name.clone()) { if !defined_cases.insert(enum_wit_name.clone()) {
@ -210,18 +332,22 @@ fn process_enum(
match v.fields { match v.fields {
Fields::Named(fields) => { Fields::Named(fields) => {
let fields_list = collect_fields(fields.named); let sub_record_name = format!("{}-{}", enum_wit_name, variant_wit_name);
register_wit_type( let fields_list = collect_fields(fields.named, defined_types);
if !fields_list.is_empty() && defined_cases.insert(sub_record_name.clone()) {
interface.type_def(TypeDef::new(
sub_record_name.clone(),
TypeDefKind::Record(Record::new(fields_list)),
));
}
cases.push(VariantCase::value(
variant_wit_name, variant_wit_name,
fields_list, WitType::named(sub_record_name),
interface, ));
variant,
Some(enum_wit_name.clone()),
);
} }
Fields::Unnamed(fields) => { Fields::Unnamed(fields) => {
let types = collect_types(fields.unnamed); let types = collect_types(fields.unnamed, defined_types);
match types.len() { match types.len() {
0 => cases.push(VariantCase::empty(variant_wit_name)), 0 => cases.push(VariantCase::empty(variant_wit_name)),
1 => cases.push(VariantCase::value( 1 => cases.push(VariantCase::value(
@ -243,7 +369,72 @@ fn process_enum(
WitType::named(enum_wit_name.clone()), WitType::named(enum_wit_name.clone()),
)); ));
// define weather to use Enum or Variant // define whether to use Enum or Variant
let all_empty = cases.iter().all(|c| c.type_().is_none());
if all_empty {
interface.type_def(TypeDef::new(
enum_wit_name,
TypeDefKind::Enum(Enum::from_iter(
cases.into_iter().map(|c| EnumCase::new(c.name().clone())),
)),
));
} else {
interface.type_def(TypeDef::new(
enum_wit_name,
TypeDefKind::Variant(Variant::from(cases)),
));
}
}
fn process_helper_enum(
e: syn::ItemEnum,
interface: &mut Interface,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let enum_wit_name = e.ident.to_string().to_kebab_case();
if !defined_cases.insert(enum_wit_name.clone()) {
return;
}
let mut cases = Vec::new();
for v in e.variants {
let variant_wit_name = v.ident.to_string().to_kebab_case();
match v.fields {
Fields::Named(fields) => {
let sub_record_name = format!("{}-{}", enum_wit_name, variant_wit_name);
let fields_list = collect_fields(fields.named, defined_types);
if !fields_list.is_empty() && defined_cases.insert(sub_record_name.clone()) {
interface.type_def(TypeDef::new(
sub_record_name.clone(),
TypeDefKind::Record(Record::new(fields_list)),
));
}
cases.push(VariantCase::value(
variant_wit_name,
WitType::named(sub_record_name),
));
}
Fields::Unnamed(fields) => {
let types = collect_types(fields.unnamed, defined_types);
match types.len() {
0 => cases.push(VariantCase::empty(variant_wit_name)),
1 => cases.push(VariantCase::value(
variant_wit_name,
types.into_iter().next().unwrap(),
)),
_ => cases.push(VariantCase::value(variant_wit_name, WitType::tuple(types))),
}
}
Fields::Unit => {
cases.push(VariantCase::empty(variant_wit_name));
}
}
}
let all_empty = cases.iter().all(|c| c.type_().is_none()); let all_empty = cases.iter().all(|c| c.type_().is_none());
if all_empty { if all_empty {
interface.type_def(TypeDef::new( interface.type_def(TypeDef::new(

View File

@ -4,11 +4,14 @@ pub mod biome;
pub mod damage_type; pub mod damage_type;
pub mod data_component; pub mod data_component;
pub mod enchantment; pub mod enchantment;
pub mod entity_status;
pub mod entity_type; pub mod entity_type;
pub mod game_event;
pub mod game_rules; pub mod game_rules;
pub mod java_packet; pub mod java_packet;
pub mod packet_mapping; pub mod packet_mapping;
pub mod particle; pub mod particle;
pub mod potion;
pub mod screen; pub mod screen;
pub mod sound; pub mod sound;
pub mod statistic; pub mod statistic;
@ -42,6 +45,9 @@ pub fn main() {
(screen::build, "screens.wit"), (screen::build, "screens.wit"),
(statistic::build, "statistics.wit"), (statistic::build, "statistics.wit"),
(game_rules::build, "game-rules.wit"), (game_rules::build, "game-rules.wit"),
(game_event::build, "game-events.wit"),
(potion::build, "potions.wit"),
(entity_status::build, "entity-statuses.wit"),
]; ];
for (build_fn, file) in build_functions { for (build_fn, file) in build_functions {

View File

@ -0,0 +1,36 @@
use semver::Version;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;
use wit_encoder::{Enum, Interface, Package, PackageName, TypeDef, TypeDefKind};
#[derive(Deserialize)]
struct PotionRaw {
id: u8,
}
pub fn build() -> String {
let raw_map: BTreeMap<String, PotionRaw> =
serde_json::from_str(&fs::read_to_string("../../assets/potion.json").unwrap())
.expect("Failed to parse potion.json");
let mut potions: Vec<(String, PotionRaw)> = raw_map.into_iter().collect();
potions.sort_by_key(|(_, raw)| raw.id);
let mut package = Package::new(PackageName::new(
"pumpkin",
"plugin",
Some(Version::new(0, 1, 0)),
));
let mut interface = Interface::new("potions");
let mut potion_enum = Enum::empty();
for (name, _) in potions {
potion_enum.case(name.replace('_', "-"));
}
interface.type_def(TypeDef::new("potion-type", TypeDefKind::Enum(potion_enum)));
package.interface(interface);
package.to_string()
}

View File

@ -14,6 +14,24 @@ pub fn build() -> String {
)); ));
let mut interface = Interface::new("sounds"); let mut interface = Interface::new("sounds");
let mut category_enum = Enum::empty();
category_enum.case("master");
category_enum.case("music");
category_enum.case("records");
category_enum.case("weather");
category_enum.case("blocks");
category_enum.case("hostile");
category_enum.case("neutral");
category_enum.case("players");
category_enum.case("ambient");
category_enum.case("voice");
category_enum.case("ui");
interface.type_def(TypeDef::new(
"sound-category",
TypeDefKind::Enum(category_enum),
));
let mut sound_enum = Enum::empty(); let mut sound_enum = Enum::empty();
for sound in sounds { for sound in sounds {
sound_enum.case(sound.replace(['_', '.'], "-")); sound_enum.case(sound.replace(['_', '.'], "-"));

View File

@ -1,48 +1,112 @@
use syn::{Type, TypePath}; use heck::ToKebabCase;
use std::collections::HashSet;
use syn::{GenericArgument, PathArguments, Type, TypePath};
use wit_encoder::Type as WitType; use wit_encoder::Type as WitType;
pub fn map_type(ty: &Type) -> WitType { pub fn map_type(ty: &Type) -> WitType {
map_type_with_defined(ty, None)
}
pub fn map_type_with_defined(ty: &Type, defined_types: Option<&HashSet<String>>) -> WitType {
match ty { match ty {
Type::Path(TypePath { path, .. }) => { Type::Path(TypePath { path, .. }) => {
let last_segment = path.segments.last().unwrap(); let last_segment = path.segments.last().unwrap();
let ident_str = last_segment.ident.to_string(); let ident_str = last_segment.ident.to_string();
match ident_str.as_str() { match ident_str.as_str() {
"String" | "TextComponent" => WitType::String, "String" | "TextComponent" | "str" | "Identifier" | "ResourceLocation"
| "LpVector3d" => WitType::String,
"Uuid" => WitType::Named("uuid".into()), "Uuid" => WitType::Named("uuid".into()),
"i32" | "VarInt" | "u32" | "VarUInt" | "usize" => WitType::S32, "i32" | "VarInt" | "u32" | "VarUInt" | "usize" | "u24" => WitType::S32,
"i64" | "u64" | "VarLong" | "VarULong" => WitType::S64, "i64" | "u64" | "VarLong" | "VarULong" => WitType::S64,
"bool" => WitType::Bool, "bool" => WitType::Bool,
"f32" => WitType::F32, "f32" => WitType::F32,
"f64" => WitType::F64, "f64" => WitType::F64,
"u8" | "i8" => WitType::U8, "u8" | "i8" => WitType::U8,
"u16" | "i16" => WitType::S32, "u16" | "i16" => WitType::S32,
"Bytes" | "BoxedU8Slice" | "ChunkData" => WitType::list(WitType::U8),
"BitSet" | "Bitset" => WitType::list(WitType::S64),
"NbtCompound" | "Nbt" | "DynamicRecipe" => WitType::String,
"Option" => { "Option" => {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() && let Some(GenericArgument::Type(inner_ty)) = args.args.first()
{ {
return WitType::option(map_type(inner_ty)); return WitType::option(map_type_with_defined(inner_ty, defined_types));
} }
WitType::String WitType::String
} }
"Box" => { "Box" => {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() && let Some(GenericArgument::Type(inner_ty)) = args.args.first()
{ {
return map_type(inner_ty); if let Type::Slice(ts) = inner_ty {
if let Type::Path(tp) = &*ts.elem
&& tp.path.segments.last().unwrap().ident == "u8"
{
return WitType::list(WitType::U8);
}
return WitType::list(map_type_with_defined(&ts.elem, defined_types));
}
if let Type::Path(tp) = inner_ty
&& tp.path.segments.last().unwrap().ident == "str"
{
return WitType::String;
}
return map_type_with_defined(inner_ty, defined_types);
} }
WitType::String WitType::String
} }
"Vec" => { "Vec" => {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() && let Some(GenericArgument::Type(inner_ty)) = args.args.first()
{ {
// Check if the inner type is u8
if let Type::Path(tp) = inner_ty if let Type::Path(tp) = inner_ty
&& tp.path.segments.last().unwrap().ident == "u8" && tp.path.segments.last().unwrap().ident == "u8"
{ {
return WitType::list(WitType::U8); return WitType::list(WitType::U8);
} }
return WitType::list(map_type(inner_ty)); return WitType::list(map_type_with_defined(inner_ty, defined_types));
}
WitType::String
}
"Cow" => {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments {
for arg in &args.args {
if let GenericArgument::Type(inner_ty) = arg {
if let Type::Path(tp) = inner_ty
&& tp.path.segments.last().unwrap().ident == "str"
{
return WitType::String;
}
return map_type_with_defined(inner_ty, defined_types);
}
}
}
WitType::String
}
"NonZero" | "EnumAsStr" | "IdOr" => {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(GenericArgument::Type(inner_ty)) = args.args.first()
{
return map_type_with_defined(inner_ty, defined_types);
}
WitType::String
}
"HashMap" => {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments {
let type_args: Vec<_> = args
.args
.iter()
.filter_map(|a| match a {
GenericArgument::Type(t) => Some(t),
_ => None,
})
.collect();
if type_args.len() == 2 {
return WitType::list(WitType::tuple(vec![
map_type_with_defined(type_args[0], defined_types),
map_type_with_defined(type_args[1], defined_types),
]));
}
} }
WitType::String WitType::String
} }
@ -53,19 +117,54 @@ pub fn map_type(ty: &Type) -> WitType {
WitType::tuple(vec![WitType::S32, WitType::S32, WitType::S32]) WitType::tuple(vec![WitType::S32, WitType::S32, WitType::S32])
} }
} }
_ => WitType::String, "Vector2" => WitType::tuple(vec![WitType::F64, WitType::F64]),
_ => {
let kebab = ident_str.to_kebab_case();
if let Some(defined) = defined_types {
if defined.contains(&kebab) {
WitType::named(kebab)
} else {
WitType::String
}
} else {
WitType::named(kebab)
} }
} }
Type::Reference(tr) => map_type(&tr.elem), }
}
Type::Reference(tr) => map_type_with_defined(&tr.elem, defined_types),
Type::Slice(ts) => { Type::Slice(ts) => {
if let Type::Path(tp) = &*ts.elem if let Type::Path(tp) = &*ts.elem
&& tp.path.segments.last().unwrap().ident == "u8" && tp.path.segments.last().unwrap().ident == "u8"
{ {
WitType::list(WitType::U8) WitType::list(WitType::U8)
} else { } else {
WitType::list(map_type(&ts.elem)) WitType::list(map_type_with_defined(&ts.elem, defined_types))
} }
} }
Type::Array(ta) => {
if let Type::Path(tp) = &*ta.elem
&& tp.path.segments.last().unwrap().ident == "u8"
{
WitType::list(WitType::U8)
} else {
WitType::list(map_type_with_defined(&ta.elem, defined_types))
}
}
Type::Tuple(tt) => {
if tt.elems.is_empty() {
WitType::String
} else {
WitType::tuple(
tt.elems
.iter()
.map(|e| map_type_with_defined(e, defined_types))
.collect::<Vec<_>>(),
)
}
}
Type::Paren(tp) => map_type_with_defined(&tp.elem, defined_types),
Type::Group(tg) => map_type_with_defined(&tg.elem, defined_types),
_ => WitType::String, _ => WitType::String,
} }
} }