feat: add more plugin stuff
This commit is contained in:
parent
f8961d1fe0
commit
1ffb0d855b
@ -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]
|
||||
|
||||
160
crates/pumpkin-plugin-api/src/block.rs
Normal file
160
crates/pumpkin-plugin-api/src/block.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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};
|
||||
|
||||
4835
crates/pumpkin-plugin-api/src/generated/block.rs
Normal file
4835
crates/pumpkin-plugin-api/src/generated/block.rs
Normal file
File diff suppressed because it is too large
Load Diff
6213
crates/pumpkin-plugin-api/src/generated/item.rs
Normal file
6213
crates/pumpkin-plugin-api/src/generated/item.rs
Normal file
File diff suppressed because it is too large
Load Diff
4
crates/pumpkin-plugin-api/src/generated/mod.rs
Normal file
4
crates/pumpkin-plugin-api/src/generated/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub mod block;
|
||||
pub mod item;
|
||||
126
crates/pumpkin-plugin-api/src/item.rs
Normal file
126
crates/pumpkin-plugin-api/src/item.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)?;
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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()))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
103
tools/pumpkin-codegen/src/sdk/block.rs
Normal file
103
tools/pumpkin-codegen/src/sdk/block.rs
Normal 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
|
||||
}
|
||||
87
tools/pumpkin-codegen/src/sdk/item.rs
Normal file
87
tools/pumpkin-codegen/src/sdk/item.rs
Normal 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
|
||||
}
|
||||
21
tools/pumpkin-codegen/src/sdk/mod.rs
Normal file
21
tools/pumpkin-codegen/src/sdk/mod.rs
Normal 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}"));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
30
tools/pumpkin-codegen/src/wit/entity_status.rs
Normal file
30
tools/pumpkin-codegen/src/wit/entity_status.rs
Normal 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()
|
||||
}
|
||||
33
tools/pumpkin-codegen/src/wit/game_event.rs
Normal file
33
tools/pumpkin-codegen/src/wit/game_event.rs
Normal 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()
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
36
tools/pumpkin-codegen/src/wit/potion.rs
Normal file
36
tools/pumpkin-codegen/src/wit/potion.rs
Normal 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()
|
||||
}
|
||||
@ -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(['_', '.'], "-"));
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user