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")]
#[rustfmt::skip]
#[path = "generated/entity_status.rs"]
mod entity_status;
pub mod entity_status;
#[cfg(feature = "entity_type")]
#[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)
}
}
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)]
mod tests {
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,
};
/// Block definitions and block type helpers.
pub mod block;
/// Plugin command registration and handling utilities.
pub mod commands;
/// Datapack management and query utilities.
@ -87,8 +89,11 @@ pub mod events;
mod ext;
/// Bedrock UI form builders.
pub mod forms;
pub(crate) mod generated;
/// Unified inventory and container management utilities.
pub mod inventory;
/// Typed item definitions and `ItemStack` construction helpers.
pub mod item;
/// Specialized mob entity wrappers and helpers.
pub mod mobs;
/// Constants for plugin permissions.
@ -118,15 +123,18 @@ pub use wit::pumpkin::plugin::{
context::{self, Context, MarketplaceMetadata, Server},
damage_types as damage_types_wit, data_components, datapack as datapack_wit,
display as display_wit, enchantments as enchantments_wit, entity,
entity_statuses as entity_statuses_wit,
entity_types::EntityType,
event::{self as events_wit, EventType},
gui, i18n, inventory as inventory_wit, ipc, item_stack, java_dialogs, java_packets, particles,
permission, player, recipe as recipe_wit, scoreboard, screens as screens_wit, server,
statistics as statistics_wit, text, uuid, world,
game_events as game_events_wit, gui, i18n, inventory as inventory_wit, ipc, item_stack,
java_dialogs, java_packets, particles, permission, player, potions as potions_wit,
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
// 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 datapack::{DatapackInfo, DatapackManager, EnablePosition};
pub use display::{
@ -139,15 +147,19 @@ pub use enchantment::{
AttributeModifierSlot, CustomEnchantment, CustomEnchantmentValue, Enchantment,
EnchantmentBuilder, EnchantmentError, EnchantmentManager, RegistrableEnchantment,
};
pub use entity_statuses_wit::EntityStatus;
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 item::{IntoItemKey, Item, ItemStackExt};
pub use mobs::{
Ageable, AgeableData, Cat, CatData, Creeper, CreeperData, DyeColor, Enderman, EndermanData,
EntityCastExt, Fox, FoxData, IronGolem, IronGolemData, MobCast, MobData, Sheep, SheepData,
Shulker, ShulkerData, Slime, SlimeData, Villager, VillagerData, VillagerProfession, Wolf,
WolfData, Zombie, ZombieData,
};
pub use potions_wit::PotionType;
pub use recipe::{
CookingRecipeBuilder, Ingredient, RecipeCategory, RecipeError, RecipeManager,
RegistrableRecipe, ShapedRecipeBuilder, ShapelessRecipeBuilder,
@ -155,7 +167,8 @@ pub use recipe::{
pub use screens_wit::Screen;
pub use statistics_wit::{CustomStatistic, StatisticCategory};
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::scoreboard::{CollisionRule, NametagVisibility, TeamSettings};
pub use wit::pumpkin::plugin::server::Dimension;

View File

@ -11,6 +11,7 @@ pub struct CDeleteChat<'a> {
}
impl<'a> CDeleteChat<'a> {
/// Creates a new `CDeleteChat` packet using a raw signature ID `VarInt`.
#[must_use]
pub const fn from_id(signature_id: VarInt) -> 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]
pub const fn from_signature(signature: &'a [u8]) -> Self {
Self {

View File

@ -1,6 +1,9 @@
use crate::ClientPacket;
use crate::Link;
use crate::ser::NetworkWriteExt;
use pumpkin_data::packet::clientbound::play::SERVER_LINKS;
use pumpkin_macros::java_packet;
use pumpkin_util::version::JavaMinecraftVersion;
#[java_packet(SERVER_LINKS)]
pub struct CPlayServerLinks<'a> {
@ -13,3 +16,17 @@ impl<'a> CPlayServerLinks<'a> {
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::version::JavaMinecraftVersion;
use rand::RngExt;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::{AtomicI32, AtomicU8, Ordering};
@ -1305,15 +1304,13 @@ pub trait PathAwareEntity: Mob + Send + Sync {
) >= 0.0
}
fn is_navigation<'a>(&'a self) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
Box::pin(async {
fn is_navigation(&self) -> bool {
let navigator = self
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
!navigator.is_idle()
})
}
// 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
pub fn set_rotation(&self, yaw: f32, pitch: f32) {
// 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_ulong::VarULong;
use pumpkin_protocol::java::client::play::{
Animation, CAcknowledgeBlockChange, CActionBar, CAwardStats, CChangeDifficulty,
Animation, CAcknowledgeBlockChange, CActionBar, CAwardStats, CBlockUpdate, CChangeDifficulty,
CCloseContainer, CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation,
CEntityPositionSync, CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle,
CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn,
CSetCamera, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem,
CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound,
CSubtitle, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk,
CUpdateMobEffect, CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction,
PlayerInfoFlags, PlayerSpawnData, PreviousMessage, Statistic,
CEntityPositionSync, CEntityVelocity, CGameEvent, CHurtAnimation, CItemCooldown, CMapItemData,
COpenBook, COpenScreen, COpenSignEditor, CParticle, CPlayServerLinks, CPlayerAbilities,
CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, CSetCamera,
CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetExperience,
CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle,
CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect,
CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction, PlayerInfoFlags,
PlayerSpawnData, PreviousMessage, Statistic,
};
use pumpkin_protocol::java::server::play::{
SClickSlot, SContainerButtonClick, SRenameItem, SlotActionType,
@ -462,6 +463,8 @@ pub struct Player {
/// Whether the client has reported that it has loaded.
pub client_loaded: 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.
pub client_loaded_timeout: AtomicU32,
/// Counter for tracking chat and command spam. Decays each server tick.
@ -733,6 +736,7 @@ impl Player {
gamemode: AtomicCell::new(gamemode),
previous_gamemode: 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
respawn_point: std::sync::Mutex::new(None),
sleeping_since: AtomicCell::new(None),
@ -2188,6 +2192,126 @@ impl Player {
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) {
const MAX_PACKETS_PER_TICK: usize = 64;
@ -2662,6 +2786,32 @@ impl Player {
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> {
self.living_entity.entity.world.load_full()
}

View File

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

View File

@ -41,7 +41,12 @@ impl JavaClient {
send_cancellable! {{
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: {
info!("<chat> {}: {}", gameprofile.name, event.message);

View File

@ -64,6 +64,10 @@ impl JavaClient {
{
return;
}
if player.is_movement_locked.load(Ordering::Relaxed) {
self.force_tp(player, player.get_entity().pos.load());
return;
}
// y = feet Y
let position = packet.position;
if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() {
@ -197,6 +201,12 @@ impl JavaClient {
{
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
let position = packet.position;
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.
pub recipients: Vec<Arc<Player>>,
/// The optional 256-byte cryptographic signature of the message.
pub signature: Option<Vec<u8>>,
}
impl PlayerChatEvent {
@ -28,14 +31,21 @@ impl PlayerChatEvent {
/// - `player`: A reference to the player sending the message.
/// - `message`: The message being sent.
/// - `recipients`: The recipients of the message. If empty, the message is broadcasted to all players.
/// - `signature`: The optional cryptographic signature of the message.
///
/// # Returns
/// 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 {
player,
message,
recipients,
signature,
cancelled: false,
}
}

View File

@ -322,6 +322,7 @@ impl ToFromWasmEvent for PlayerChatEvent {
player,
message: self.message.clone(),
recipients,
signature: self.signature.clone(),
cancelled: self.cancelled,
})
}
@ -336,6 +337,7 @@ impl ToFromWasmEvent for PlayerChatEvent {
.into_iter()
.map(|recipient| consume_player(state, &recipient))
.collect(),
signature: data.signature,
cancelled: data.cancelled,
},
_ => 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::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::enchantments::Enchantment as WitEnchantment;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::item_stack::{
CustomEnchantmentValue as WitCustomEnchantmentValue,
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 std::sync::Arc;
@ -14,8 +19,12 @@ use wasmtime::component::Resource;
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 pumpkin_data::Enchantment;
use pumpkin_data::attributes::Attributes;
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_protocol::codec::data_component::{deserialize, serialize};
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")
}
#[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 {
pub fn get_item_stack(
&self,
@ -380,6 +413,101 @@ impl HostItemStack for PluginHostState {
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(
&mut self,
res: Resource<ItemStackHandle>,

View File

@ -980,6 +980,21 @@ impl HostMob for PluginHostState {
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<()> {
let _ = self
.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::statistics::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<()> {
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_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(
version: JavaMinecraftVersion,
) -> pumpkin::plugin::player::JavaMinecraftVersion {
@ -1512,6 +1568,363 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
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(
&mut self,
player: Resource<Player>,
@ -3331,6 +3744,52 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
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(
&mut self,
rep: Resource<pumpkin::plugin::player::JavaPlayer>,

View File

@ -304,6 +304,34 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
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(
&mut self,
_rep: Resource<Server>,
@ -618,6 +646,27 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
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<()> {
self.resource_table
.delete::<ServerResource>(Resource::new_own(rep.rep()))

View File

@ -864,7 +864,7 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
&mut self,
world: Resource<World>,
sound: pumpkin::plugin::sounds::Sound,
category: pumpkin::plugin::world::SoundCategory,
category: pumpkin::plugin::sounds::SoundCategory,
pos: pumpkin::plugin::common::Position,
volume: 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)
.ok_or_else(|| wasmtime::Error::msg(format!("Unknown sound: {sound_name}")))?;
let internal_category = match 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
}
};
let internal_category = from_wit_sound_category(category);
world_ref.provider.play_sound_raw(
sound_data as u16,
@ -917,6 +886,27 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
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(
&mut self,
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)]
mod tests {
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) {
let packet = CTabList::new(header, footer);
for world in self.worlds.load().iter() {

View File

@ -126,7 +126,9 @@ use pumpkin_protocol::{
};
use pumpkin_protocol::{
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::text::{TextComponent, color::NamedColor};
@ -1207,6 +1209,52 @@ impl World {
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.
pub fn play_bedrock_level_sound(
&self,

View File

@ -73,6 +73,7 @@ mod registry;
mod remap;
mod scoreboard_slot;
mod screen;
mod sdk;
mod sound;
mod sound_category;
mod spawn_egg;
@ -174,6 +175,7 @@ pub fn main() {
let filters: Vec<String> = std::env::args().skip(1).collect();
let build_functions: Vec<_> = if filters.is_empty() {
wit::main();
sdk::main();
build_functions
} else {
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 semver::Version;
use std::collections::HashSet;
use std::{fs, path::Path};
use syn::{Fields, Item};
use wit_encoder::{
@ -20,18 +21,39 @@ pub fn build() -> String {
let mut serverbound_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_packets(
"../../crates/pumpkin-protocol/src/bedrock/server",
&mut interface,
&mut serverbound_variant,
&mut defined_cases,
&defined_types,
);
// Process clientbound packets
process_packets(
"../../crates/pumpkin-protocol/src/bedrock/client",
&mut interface,
&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
@ -51,7 +73,75 @@ pub fn build() -> 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 mut sorted_paths: Vec<_> = paths
.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 {
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;
}
if path.extension().is_some_and(|ext| ext == "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(
fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<WitType> {
fields
.into_iter()
.map(|field| map_type(&field.ty))
.map(|field| map_type_with_defined(&field.ty, Some(defined_types)))
.collect()
}
fn collect_fields(
named_fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<Field> {
named_fields
.into_iter()
.map(|field| {
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)
})
.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();
if !defined_cases.insert(wit_name.clone()) {
return;
}
let fields_list = match s.fields {
Fields::Named(fields) => collect_fields(fields.named),
Fields::Named(fields) => collect_fields(fields.named, defined_types),
_ => Vec::new(),
};
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();
if !defined_cases.insert(enum_wit_name.clone()) {
return;
}
let mut cases = Vec::new();
for v in e.variants {
@ -144,18 +284,22 @@ fn process_enum(e: syn::ItemEnum, interface: &mut Interface, variant: &mut Varia
match v.fields {
Fields::Named(fields) => {
let fields_list = collect_fields(fields.named);
register_wit_type(
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,
fields_list,
interface,
variant,
Some(enum_wit_name.clone()),
);
WitType::named(sub_record_name),
));
}
Fields::Unnamed(fields) => {
let types = collect_types(fields.unnamed);
let types = collect_types(fields.unnamed, defined_types);
match types.len() {
0 => cases.push(VariantCase::empty(variant_wit_name)),
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()),
));
// 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(
@ -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 file = syn::parse_file(&content).expect("Failed to parse file");
for item in file.items {
match item {
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) => {
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 semver::Version;
use std::collections::HashSet;
@ -22,29 +22,47 @@ pub fn build() -> String {
let mut serverbound_variant = Variant::empty();
let mut clientbound_variant = Variant::empty();
let mut serverbound_cases = HashSet::new();
let mut clientbound_cases = HashSet::new();
let mut defined_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
let server_states = &["config", "handshake", "login", "play", "status"];
for state in server_states {
process_packets(
&format!("../../crates/pumpkin-protocol/src/java/server/{}", state),
state,
&mut interface,
&mut serverbound_variant,
&mut serverbound_cases,
&mut defined_cases,
&defined_types,
);
}
// Process clientbound packets
let client_states = &["config", "login", "play", "status"];
for state in client_states {
process_packets(
&format!("../../crates/pumpkin-protocol/src/java/client/{}", state),
state,
&mut interface,
&mut clientbound_variant,
&mut clientbound_cases,
&mut defined_cases,
&defined_types,
);
}
@ -65,12 +83,64 @@ pub fn build() -> 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(
dir: &str,
state: &str,
interface: &mut Interface,
variant: &mut Variant,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let paths = fs::read_dir(dir).expect("Failed to read packet directory");
let mut sorted_paths: Vec<_> = paths
@ -79,10 +149,28 @@ fn process_packets(
sorted_paths.sort();
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")
&& 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,
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 file = syn::parse_file(&content).expect("Failed to parse file");
@ -100,10 +189,16 @@ fn parse_packet_file(
for item in file.items {
match item {
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) => {
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(
fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<WitType> {
fields
.into_iter()
.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()
}
fn collect_fields(
named_fields: syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
defined_types: &HashSet<String>,
) -> Vec<Field> {
named_fields
.into_iter()
.filter(|field| extract_type_name(&field.ty) != "DynamicRecipe")
.map(|field| {
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)
})
.collect()
@ -179,25 +276,50 @@ fn process_struct(
interface: &mut Interface,
variant: &mut Variant,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let wit_name = wit_name(s.ident.to_string(), state);
if !defined_cases.insert(wit_name.clone()) {
return;
}
let fields_list = match s.fields {
Fields::Named(fields) => collect_fields(fields.named),
Fields::Named(fields) => collect_fields(fields.named, defined_types),
_ => Vec::new(),
};
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(
e: syn::ItemEnum,
state: &str,
interface: &mut Interface,
variant: &mut Variant,
defined_cases: &mut HashSet<String>,
defined_types: &HashSet<String>,
) {
let enum_wit_name = wit_name(e.ident.to_string(), state);
if !defined_cases.insert(enum_wit_name.clone()) {
@ -210,18 +332,22 @@ fn process_enum(
match v.fields {
Fields::Named(fields) => {
let fields_list = collect_fields(fields.named);
register_wit_type(
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,
fields_list,
interface,
variant,
Some(enum_wit_name.clone()),
);
WitType::named(sub_record_name),
));
}
Fields::Unnamed(fields) => {
let types = collect_types(fields.unnamed);
let types = collect_types(fields.unnamed, defined_types);
match types.len() {
0 => cases.push(VariantCase::empty(variant_wit_name)),
1 => cases.push(VariantCase::value(
@ -243,7 +369,72 @@ fn process_enum(
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());
if all_empty {
interface.type_def(TypeDef::new(

View File

@ -4,11 +4,14 @@ pub mod biome;
pub mod damage_type;
pub mod data_component;
pub mod enchantment;
pub mod entity_status;
pub mod entity_type;
pub mod game_event;
pub mod game_rules;
pub mod java_packet;
pub mod packet_mapping;
pub mod particle;
pub mod potion;
pub mod screen;
pub mod sound;
pub mod statistic;
@ -42,6 +45,9 @@ pub fn main() {
(screen::build, "screens.wit"),
(statistic::build, "statistics.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 {

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 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();
for sound in sounds {
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;
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 {
Type::Path(TypePath { path, .. }) => {
let last_segment = path.segments.last().unwrap();
let ident_str = last_segment.ident.to_string();
match ident_str.as_str() {
"String" | "TextComponent" => WitType::String,
"String" | "TextComponent" | "str" | "Identifier" | "ResourceLocation"
| "LpVector3d" => WitType::String,
"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,
"bool" => WitType::Bool,
"f32" => WitType::F32,
"f64" => WitType::F64,
"u8" | "i8" => WitType::U8,
"u16" | "i16" => WitType::S32,
"Bytes" | "BoxedU8Slice" | "ChunkData" => WitType::list(WitType::U8),
"BitSet" | "Bitset" => WitType::list(WitType::S64),
"NbtCompound" | "Nbt" | "DynamicRecipe" => WitType::String,
"Option" => {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& 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
}
"Box" => {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& 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
}
"Vec" => {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
if let PathArguments::AngleBracketed(args) = &last_segment.arguments
&& let Some(GenericArgument::Type(inner_ty)) = args.args.first()
{
// Check if the inner type is u8
if let Type::Path(tp) = inner_ty
&& tp.path.segments.last().unwrap().ident == "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
}
@ -53,19 +117,54 @@ pub fn map_type(ty: &Type) -> WitType {
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) => {
if let Type::Path(tp) = &*ts.elem
&& tp.path.segments.last().unwrap().ident == "u8"
{
WitType::list(WitType::U8)
} 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,
}
}