diff --git a/crates/pumpkin-plugin-api/Cargo.toml b/crates/pumpkin-plugin-api/Cargo.toml index dd1982a42..20fd292df 100644 --- a/crates/pumpkin-plugin-api/Cargo.toml +++ b/crates/pumpkin-plugin-api/Cargo.toml @@ -3,7 +3,7 @@ name = "pumpkin-plugin-api" # This should be different from the pumpkin server version. version = "0.1.0" edition.workspace = true -license.workspace = true +license = "MIT OR Apache-2.0" [dependencies] wit-bindgen = { workspace = true } diff --git a/crates/pumpkin-plugin-api/LICENSE-APACHE b/crates/pumpkin-plugin-api/LICENSE-APACHE new file mode 100644 index 000000000..c6e5ba771 --- /dev/null +++ b/crates/pumpkin-plugin-api/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or exemplary damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/pumpkin-plugin-api/LICENSE-MIT b/crates/pumpkin-plugin-api/LICENSE-MIT new file mode 100644 index 000000000..36a056ecd --- /dev/null +++ b/crates/pumpkin-plugin-api/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-2026 Pumpkin Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/pumpkin-plugin-api/src/ext/attributes.rs b/crates/pumpkin-plugin-api/src/ext/attributes.rs new file mode 100644 index 000000000..7da96559f --- /dev/null +++ b/crates/pumpkin-plugin-api/src/ext/attributes.rs @@ -0,0 +1,31 @@ +use crate::wit::pumpkin::plugin::attributes::{AttributeModifier, ModifierOperation}; + +impl AttributeModifier { + /// Creates a new attribute modifier. + #[must_use] + pub fn new(id: impl Into, amount: f64, operation: ModifierOperation) -> Self { + Self { + id: id.into(), + amount, + operation, + } + } + + /// Creates an additive attribute modifier. + #[must_use] + pub fn add(id: impl Into, amount: f64) -> Self { + Self::new(id, amount, ModifierOperation::Add) + } + + /// Creates a multiplicative base attribute modifier (`base * (1 + amount)`). + #[must_use] + pub fn multiply_base(id: impl Into, amount: f64) -> Self { + Self::new(id, amount, ModifierOperation::MultiplyBase) + } + + /// Creates a multiplicative total attribute modifier (`total * (1 + amount)`). + #[must_use] + pub fn multiply_total(id: impl Into, amount: f64) -> Self { + Self::new(id, amount, ModifierOperation::MultiplyTotal) + } +} diff --git a/crates/pumpkin-plugin-api/src/ext/mod.rs b/crates/pumpkin-plugin-api/src/ext/mod.rs index 8c4c7bcab..eee441fde 100644 --- a/crates/pumpkin-plugin-api/src/ext/mod.rs +++ b/crates/pumpkin-plugin-api/src/ext/mod.rs @@ -1,4 +1,6 @@ //! WIT-generated plugin type extensions belong in here. For example [Display](std::fmt::Display) implementations. +mod attributes; +mod player; mod server_list_ping; mod uuid; diff --git a/crates/pumpkin-plugin-api/src/ext/player.rs b/crates/pumpkin-plugin-api/src/ext/player.rs new file mode 100644 index 000000000..3dd179c17 --- /dev/null +++ b/crates/pumpkin-plugin-api/src/ext/player.rs @@ -0,0 +1,110 @@ +use crate::wit::pumpkin::plugin::player::{ + BanIpOptions, BanPlayerOptions, BedrockDisconnectReason, BedrockKickOptions, JavaKickOptions, + SocketTeardownPolicy, +}; +use crate::wit::pumpkin::plugin::text::TextComponent; + +impl JavaKickOptions { + /// Creates a new `JavaKickOptions` with the given reason and default settings. + #[must_use] + pub fn new(reason: TextComponent) -> Self { + Self { + reason, + log_to_console: true, + teardown_policy: SocketTeardownPolicy::Graceful, + } + } +} + +impl Default for BedrockKickOptions { + fn default() -> Self { + Self { + reason: BedrockDisconnectReason::Kicked, + message: String::new(), + skip_message: false, + filtered_message: String::new(), + log_to_console: true, + teardown_policy: SocketTeardownPolicy::Graceful, + } + } +} + +impl BedrockKickOptions { + /// Creates a new `BedrockKickOptions` with the given reason, message, and default settings. + #[must_use] + pub fn new(reason: BedrockDisconnectReason, message: impl Into) -> Self { + Self { + reason, + message: message.into(), + ..Default::default() + } + } +} + +impl Default for BanPlayerOptions { + fn default() -> Self { + Self { + reason: None, + source: None, + expires_at_utc: None, + duration_seconds: None, + kick_if_online: true, + log_to_console: true, + } + } +} + +impl BanPlayerOptions { + /// Creates a new permanent ban with an optional reason and default settings. + #[must_use] + pub fn new(reason: Option) -> Self { + Self { + reason, + ..Default::default() + } + } + + /// Creates a temporary ban with a specific duration in seconds. + #[must_use] + pub fn temporary(reason: Option, duration_seconds: u64) -> Self { + Self { + reason, + duration_seconds: Some(duration_seconds), + ..Default::default() + } + } +} + +impl Default for BanIpOptions { + fn default() -> Self { + Self { + reason: None, + source: None, + expires_at_utc: None, + duration_seconds: None, + kick_matching_players: true, + log_to_console: true, + } + } +} + +impl BanIpOptions { + /// Creates a new permanent IP ban with an optional reason and default settings. + #[must_use] + pub fn new(reason: Option) -> Self { + Self { + reason, + ..Default::default() + } + } + + /// Creates a temporary IP ban with a specific duration in seconds. + #[must_use] + pub fn temporary(reason: Option, duration_seconds: u64) -> Self { + Self { + reason, + duration_seconds: Some(duration_seconds), + ..Default::default() + } + } +} diff --git a/crates/pumpkin-plugin-wit b/crates/pumpkin-plugin-wit index 2f91c555a..d6a49a245 160000 --- a/crates/pumpkin-plugin-wit +++ b/crates/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit 2f91c555a4e902bf3e69d5cd0a60835232b2a92d +Subproject commit d6a49a2451c6e813d79cd965196dd0ec325444cc diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index d7495ee8b..3d55b2a82 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -3654,25 +3654,45 @@ impl Player { } pub async fn ban(&self, server: &Server, reason: Option) { - let mut banned_players = server.data.banned_player_list.write().await; + self.ban_explicit(server, reason, None, None, true, true) + .await; + } + + pub async fn ban_explicit( + &self, + server: &Server, + reason: Option, + source: Option, + expires: Option, + kick_if_online: bool, + log_to_console: bool, + ) { let string_reason = reason.clone().map_or_else( || "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text, ); + let source_str = source.unwrap_or_else(|| "Plugin".to_string()); - if banned_players - .banned_players - .iter() - .any(|entry| entry.uuid == self.gameprofile.id) - { - return; + if log_to_console { + tracing::info!( + "Banning player {} ({}) by {}: {}", + self.gameprofile.name, + self.gameprofile.id, + source_str, + string_reason + ); } + let mut banned_players = server.data.banned_player_list.write().await; + banned_players + .banned_players + .retain(|entry| entry.uuid != self.gameprofile.id); + banned_players.banned_players.push( crate::data::banlist_serializer::BannedPlayerEntry::new( &self.gameprofile, - "Plugin".to_string(), - None, + source_str, + expires, string_reason, ), ); @@ -3680,54 +3700,79 @@ impl Player { banned_players.save(); drop(banned_players); - let kick_reason = reason.unwrap_or_else(|| { - TextComponent::translate_cross( - translation::java::MULTIPLAYER_DISCONNECT_BANNED, - translation::bedrock::DISCONNECTIONSCREEN_TITLE_BANNEDBYHOST, - [], - ) - }); + if kick_if_online { + let kick_reason = reason.unwrap_or_else(|| { + TextComponent::translate_cross( + translation::java::MULTIPLAYER_DISCONNECT_BANNED, + translation::bedrock::DISCONNECTIONSCREEN_TITLE_BANNEDBYHOST, + [], + ) + }); - self.kick(DisconnectReason::Kicked, kick_reason).await; + self.kick(DisconnectReason::Kicked, kick_reason).await; + } } pub async fn ban_ip(&self, server: &Server, reason: Option) { - let mut banned_ips = server.data.banned_ip_list.write().await; + self.ban_ip_explicit(server, reason, None, None, true, true) + .await; + } + + pub async fn ban_ip_explicit( + &self, + server: &Server, + reason: Option, + source: Option, + expires: Option, + kick_matching_players: bool, + log_to_console: bool, + ) { let string_reason = reason.clone().map_or_else( || "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text, ); + let source_str = source.unwrap_or_else(|| "Plugin".to_string()); let target_ip = self.client.address().ip(); - if banned_ips.get_entry(&target_ip).is_some() { - return; + if log_to_console { + tracing::info!( + "Banning IP {} by {}: {}", + target_ip, + source_str, + string_reason + ); } + let mut banned_ips = server.data.banned_ip_list.write().await; + banned_ips.banned_ips.retain(|entry| entry.ip != target_ip); + banned_ips .banned_ips .push(crate::data::banlist_serializer::BannedIpEntry::new( target_ip, - "Plugin".to_string(), - None, + source_str, + expires, string_reason, )); banned_ips.save(); drop(banned_ips); - let kick_reason = reason.unwrap_or_else(|| { - TextComponent::translate_cross( - translation::java::MULTIPLAYER_DISCONNECT_IP_BANNED, - translation::java::MULTIPLAYER_DISCONNECT_IP_BANNED, - [], - ) - }); + if kick_matching_players { + let kick_reason = reason.unwrap_or_else(|| { + TextComponent::translate_cross( + translation::java::MULTIPLAYER_DISCONNECT_IP_BANNED, + translation::java::MULTIPLAYER_DISCONNECT_IP_BANNED, + [], + ) + }); - let affected = server.get_players_by_ip(target_ip); - for target in affected { - target - .kick(DisconnectReason::Kicked, kick_reason.clone()) - .await; + let affected = server.get_players_by_ip(target_ip); + for target in affected { + target + .kick(DisconnectReason::Kicked, kick_reason.clone()) + .await; + } } } diff --git a/crates/pumpkin/src/net/bedrock/mod.rs b/crates/pumpkin/src/net/bedrock/mod.rs index a268f70f5..f0a4bfbe2 100644 --- a/crates/pumpkin/src/net/bedrock/mod.rs +++ b/crates/pumpkin/src/net/bedrock/mod.rs @@ -271,6 +271,26 @@ impl BedrockClient { self.close().await; } + pub async fn kick_explicit( + &self, + reason: DisconnectReason, + message: String, + skip_message: bool, + filtered_message: String, + send_packet: bool, + ) { + if send_packet { + self.send_game_packet(&CDisconnectPlayer { + reason: pumpkin_protocol::codec::var_int::VarInt(reason as i32), + skip_message, + message, + filtered_message, + }) + .await; + } + self.close().await; + } + pub async fn send_chunks(&self, chunks: &[SyncChunk]) { let player = self.player.load_full(); let Some(player) = player.as_ref() else { diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs index b31a0291c..00868998a 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs @@ -425,20 +425,26 @@ impl JavaClient { } pub async fn kick(&self, reason: TextComponent) { - match self.connection_state.load() { - ConnectionState::Login => { - // TextComponent implements Serialize and writes in bytes instead of String, that's the reasib we only use content - self.send_packet_now(&CLoginDisconnect::new( - serde_json::to_string(&reason.0).unwrap_or_else(|_| String::new()), - )) - .await; - } - ConnectionState::Config => { - self.send_packet_now(&CConfigDisconnect::new(&reason.get_text())) + self.kick_explicit(&reason, true).await; + } + + pub async fn kick_explicit(&self, reason: &TextComponent, send_packet: bool) { + if send_packet { + match self.connection_state.load() { + ConnectionState::Login => { + // TextComponent implements Serialize and writes in bytes instead of String, that's the reason we only use content + self.send_packet_now(&CLoginDisconnect::new( + serde_json::to_string(&reason.0).unwrap_or_else(|_| String::new()), + )) .await; + } + ConnectionState::Config => { + self.send_packet_now(&CConfigDisconnect::new(&reason.clone().get_text())) + .await; + } + ConnectionState::Play => self.send_packet_now(&CPlayDisconnect::new(reason)).await, + _ => {} } - ConnectionState::Play => self.send_packet_now(&CPlayDisconnect::new(&reason)).await, - _ => {} } debug!("Closing connection for {}", self.id); self.close(); diff --git a/crates/pumpkin/src/net/mod.rs b/crates/pumpkin/src/net/mod.rs index 52a9194ff..0f323e86b 100644 --- a/crates/pumpkin/src/net/mod.rs +++ b/crates/pumpkin/src/net/mod.rs @@ -387,7 +387,7 @@ fn is_valid_player_name(name: &str) -> bool { !name.chars().any(|c| c.is_control() || c == ' ') } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum DisconnectReason { Unknown = 0, CantConnectNoInternet = 1, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs index 6e816ce92..aee3fcfa2 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs @@ -65,6 +65,9 @@ pub type ConsumedArgsResource = WasmResource; pub type CommandNodeResource = WasmResource; pub type ItemStackResource = WasmResource>>; pub type RecipeManagerResource = WasmResource>; +pub type OpManagerResource = WasmResource>; +pub type BanManagerResource = WasmResource>; +pub type WhitelistManagerResource = WasmResource>; pub type BlockEntityResource = WasmResource>; #[derive(Clone)] @@ -294,6 +297,32 @@ impl PluginHostState { Ok(wasmtime::component::Resource::new_own(resource.rep())) } + pub fn add_op_manager( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self.resource_table.push(OpManagerResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + + pub fn add_ban_manager( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self.resource_table.push(BanManagerResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + + pub fn add_whitelist_manager( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self + .resource_table + .push(WhitelistManagerResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + pub fn add_block_entity( &mut self, provider: Arc, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs index 36d303d3d..2e9010722 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs @@ -10,14 +10,20 @@ use crate::plugin::loader::wasm::wasm_host::{ state::{EntityResource, PluginHostState}, wit::v0_1::events::to_wasm_position, wit::v0_1::pumpkin::plugin::{ + attributes::{ + Attribute, AttributeModifier as WitAttributeModifier, + ModifierOperation as WitModifierOperation, + }, common::{EntityPose, Position}, entity::Host, entity_types, + item_stack::ItemStack as WitHostItemStack, text::TextComponent, uuid::Uuid, world::{ - BlockPos as WitBlockPos, BoundingBox as WitBoundingBox, Entity, HostEntity, - RaycastResult as WitRaycastResult, World, + BlockPos as WitBlockPos, BoundingBox as WitBoundingBox, Entity, + EquipmentSlot as WitEquipmentSlot, HostEntity, RaycastResult as WitRaycastResult, + World, }, }, wit::v0_1::uuid::UuidExt, @@ -62,6 +68,100 @@ const fn map_entity_pose(pose: InternalEntityPose) -> EntityPose { } } +#[must_use] +pub const fn from_wit_attribute(attr: Attribute) -> &'static pumpkin_data::attributes::Attributes { + use pumpkin_data::attributes::Attributes; + match attr { + Attribute::AirDragModifier => &Attributes::AIR_DRAG_MODIFIER, + Attribute::Armor => &Attributes::ARMOR, + Attribute::ArmorToughness => &Attributes::ARMOR_TOUGHNESS, + Attribute::AttackDamage => &Attributes::ATTACK_DAMAGE, + Attribute::AttackKnockback => &Attributes::ATTACK_KNOCKBACK, + Attribute::AttackSpeed => &Attributes::ATTACK_SPEED, + Attribute::BelowNameDistance => &Attributes::BELOW_NAME_DISTANCE, + Attribute::BlockBreakSpeed => &Attributes::BLOCK_BREAK_SPEED, + Attribute::BlockInteractionRange => &Attributes::BLOCK_INTERACTION_RANGE, + Attribute::Bounciness => &Attributes::BOUNCINESS, + Attribute::BurningTime => &Attributes::BURNING_TIME, + Attribute::CameraDistance => &Attributes::CAMERA_DISTANCE, + Attribute::ExplosionKnockbackResistance => &Attributes::EXPLOSION_KNOCKBACK_RESISTANCE, + Attribute::EntityInteractionRange => &Attributes::ENTITY_INTERACTION_RANGE, + Attribute::FallDamageMultiplier => &Attributes::FALL_DAMAGE_MULTIPLIER, + Attribute::FlyingSpeed => &Attributes::FLYING_SPEED, + Attribute::FollowRange => &Attributes::FOLLOW_RANGE, + Attribute::FrictionModifier => &Attributes::FRICTION_MODIFIER, + Attribute::Gravity => &Attributes::GRAVITY, + Attribute::JumpStrength => &Attributes::JUMP_STRENGTH, + Attribute::KnockbackResistance => &Attributes::KNOCKBACK_RESISTANCE, + Attribute::Luck => &Attributes::LUCK, + Attribute::MaxAbsorption => &Attributes::MAX_ABSORPTION, + Attribute::MaxHealth => &Attributes::MAX_HEALTH, + Attribute::MiningEfficiency => &Attributes::MINING_EFFICIENCY, + Attribute::MovementEfficiency => &Attributes::MOVEMENT_EFFICIENCY, + Attribute::MovementSpeed => &Attributes::MOVEMENT_SPEED, + Attribute::NameTagDistance => &Attributes::NAME_TAG_DISTANCE, + Attribute::OxygenBonus => &Attributes::OXYGEN_BONUS, + Attribute::SafeFallDistance => &Attributes::SAFE_FALL_DISTANCE, + Attribute::Scale => &Attributes::SCALE, + Attribute::SneakingSpeed => &Attributes::SNEAKING_SPEED, + Attribute::SpawnReinforcements => &Attributes::SPAWN_REINFORCEMENTS, + Attribute::StepHeight => &Attributes::STEP_HEIGHT, + Attribute::SubmergedMiningSpeed => &Attributes::SUBMERGED_MINING_SPEED, + Attribute::SweepingDamageRatio => &Attributes::SWEEPING_DAMAGE_RATIO, + Attribute::TemptRange => &Attributes::TEMPT_RANGE, + Attribute::WaterMovementEfficiency => &Attributes::WATER_MOVEMENT_EFFICIENCY, + Attribute::WaypointTransmitRange => &Attributes::WAYPOINT_TRANSMIT_RANGE, + Attribute::WaypointReceiveRange => &Attributes::WAYPOINT_RECEIVE_RANGE, + } +} + +#[must_use] +pub const fn from_wit_modifier_op( + op: WitModifierOperation, +) -> crate::entity::attributes::ModifierOperation { + match op { + WitModifierOperation::Add => crate::entity::attributes::ModifierOperation::Add, + WitModifierOperation::MultiplyBase => { + crate::entity::attributes::ModifierOperation::MultiplyBase + } + WitModifierOperation::MultiplyTotal => { + crate::entity::attributes::ModifierOperation::MultiplyTotal + } + } +} + +#[must_use] +pub const fn to_wit_modifier_op( + op: crate::entity::attributes::ModifierOperation, +) -> WitModifierOperation { + match op { + crate::entity::attributes::ModifierOperation::Add => WitModifierOperation::Add, + crate::entity::attributes::ModifierOperation::MultiplyBase => { + WitModifierOperation::MultiplyBase + } + crate::entity::attributes::ModifierOperation::MultiplyTotal => { + WitModifierOperation::MultiplyTotal + } + } +} + +#[must_use] +pub const fn from_wit_equipment_slot( + slot: WitEquipmentSlot, +) -> pumpkin_data::data_component_impl::EquipmentSlot { + use pumpkin_data::data_component_impl::EquipmentSlot; + match slot { + WitEquipmentSlot::MainHand => EquipmentSlot::MAIN_HAND, + WitEquipmentSlot::OffHand => EquipmentSlot::OFF_HAND, + WitEquipmentSlot::Feet => EquipmentSlot::FEET, + WitEquipmentSlot::Legs => EquipmentSlot::LEGS, + WitEquipmentSlot::Chest => EquipmentSlot::CHEST, + WitEquipmentSlot::Head => EquipmentSlot::HEAD, + WitEquipmentSlot::Body => EquipmentSlot::BODY, + WitEquipmentSlot::Saddle => EquipmentSlot::SADDLE, + } +} + impl HostEntity for PluginHostState { async fn get_id(&mut self, entity: Resource) -> wasmtime::Result { let entity = entity_from_resource(self, &entity)?; @@ -468,6 +568,212 @@ impl HostEntity for PluginHostState { Ok(()) } + async fn get_attribute_value( + &mut self, + entity: Resource, + attr: Attribute, + ) -> wasmtime::Result { + let entity = entity_from_resource(self, &entity)?; + let attribute = from_wit_attribute(attr); + Ok(entity + .get_living_entity() + .map_or(attribute.default_value, |living| { + living.get_attribute_value(attribute) + })) + } + + async fn get_attribute_base( + &mut self, + entity: Resource, + attr: Attribute, + ) -> wasmtime::Result { + let entity = entity_from_resource(self, &entity)?; + let attribute = from_wit_attribute(attr); + Ok(entity + .get_living_entity() + .map_or(attribute.default_value, |living| { + living.get_attribute_base(attribute) + })) + } + + async fn set_attribute_base( + &mut self, + entity: Resource, + attr: Attribute, + value: f64, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + living.set_attribute_base(attribute, value); + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ) + .await; + } + Ok(()) + } + + async fn add_attribute_modifier( + &mut self, + entity: Resource, + attr: Attribute, + modifier: WitAttributeModifier, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + let internal_mod = crate::entity::attributes::Modifier { + id: modifier.id, + amount: modifier.amount, + operation: from_wit_modifier_op(modifier.operation), + }; + living.update_attribute(attribute, |inst| inst.add_or_replace_modifier(internal_mod)); + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ) + .await; + } + Ok(()) + } + + async fn remove_attribute_modifier( + &mut self, + entity: Resource, + attr: Attribute, + id: String, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + living.update_attribute(attribute, |inst| inst.remove_modifier(&id)); + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ) + .await; + } + Ok(()) + } + + async fn get_attribute_modifiers( + &mut self, + entity: Resource, + attr: Attribute, + ) -> wasmtime::Result> { + let entity = entity_from_resource(self, &entity)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + let map = living.attributes.read().unwrap(); + if let Some(inst) = map.get(&attribute.id) { + return Ok(inst + .modifiers + .iter() + .map(|m| WitAttributeModifier { + id: m.id.clone(), + amount: m.amount, + operation: to_wit_modifier_op(m.operation), + }) + .collect()); + } + } + Ok(Vec::new()) + } + + async fn reset_attribute( + &mut self, + entity: Resource, + attr: Attribute, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + let attribute = from_wit_attribute(attr); + if let Some(living) = entity.get_living_entity() { + { + let mut map = living.attributes.write().unwrap(); + map.remove(&attribute.id); + }; + crate::entity::attributes::send_attribute_updates_for_living( + living, + vec![attribute.clone()], + ) + .await; + } + Ok(()) + } + + async fn reset_all_attributes(&mut self, entity: Resource) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + if let Some(living) = entity.get_living_entity() { + living.reset_effects_and_attributes().await; + } + Ok(()) + } + + async fn get_equipment( + &mut self, + entity: Resource, + slot: WitEquipmentSlot, + ) -> wasmtime::Result>> { + let entity = entity_from_resource(self, &entity)?; + if let Some(living) = entity.get_living_entity() { + let slot = from_wit_equipment_slot(slot); + let equipment = living.entity_equipment.lock().await; + let stack = equipment.get(&slot); + if !stack.is_empty() { + return Ok(Some( + self.add_item_stack(Arc::new(tokio::sync::Mutex::new(stack)))?, + )); + } + } + Ok(None) + } + + async fn set_equipment( + &mut self, + entity: Resource, + slot: WitEquipmentSlot, + stack: Option>, + ) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + if let Some(living) = entity.get_living_entity() { + let slot = from_wit_equipment_slot(slot); + let item_stack = if let Some(stack_res) = stack { + self.get_item_stack(&stack_res)?.lock().await.clone() + } else { + pumpkin_data::item_stack::ItemStack::EMPTY.clone() + }; + + { + let mut equipment = living.entity_equipment.lock().await; + equipment.put(&slot, item_stack.clone()); + }; + + living.send_equipment_changes(&[(slot, item_stack)]); + } + Ok(()) + } + + async fn clear_equipment(&mut self, entity: Resource) -> wasmtime::Result<()> { + let entity = entity_from_resource(self, &entity)?; + if let Some(living) = entity.get_living_entity() { + let mut equipment = living.entity_equipment.lock().await; + let slots_to_clear: Vec<( + pumpkin_data::data_component_impl::EquipmentSlot, + pumpkin_data::item_stack::ItemStack, + )> = equipment + .equipment + .drain() + .map(|(slot, _)| (slot, pumpkin_data::item_stack::ItemStack::EMPTY.clone())) + .collect(); + drop(equipment); + + living.send_equipment_changes(&slots_to_clear); + } + Ok(()) + } + async fn get_age(&mut self, entity: Resource) -> wasmtime::Result { let entity = entity_from_resource(self, &entity)?; Ok(entity diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs index 52d96892c..b5485dcce 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mod.rs @@ -46,6 +46,7 @@ impl pumpkin::plugin::bedrock_packets::Host for PluginHostState {} impl pumpkin::plugin::data_components::Host for PluginHostState {} impl pumpkin::plugin::enchantments::Host for PluginHostState {} impl pumpkin::plugin::biomes::Host for PluginHostState {} +impl pumpkin::plugin::attributes::Host for PluginHostState {} pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { Plugin::add_to_linker::<_, HasSelf<_>>(linker, |state: &mut PluginHostState| state)?; diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 7eb919e79..7c2bc1cc4 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -480,7 +480,7 @@ fn world_from_resource( .clone() } -const fn to_wit_permission_level( +pub(crate) const fn to_wit_permission_level( level: PermissionLvl, ) -> pumpkin::plugin::permission::PermissionLevel { match level { @@ -492,7 +492,7 @@ const fn to_wit_permission_level( } } -const fn from_wit_permission_level( +pub(crate) const fn from_wit_permission_level( level: pumpkin::plugin::permission::PermissionLevel, ) -> PermissionLvl { match level { @@ -504,6 +504,469 @@ const fn from_wit_permission_level( } } +pub(crate) fn parse_ban_expiry( + expires_at_utc: Option, + duration_seconds: Option, +) -> Option { + if let Some(dur) = duration_seconds { + let seconds = i64::try_from(dur).unwrap_or(i64::MAX); + return Some(time::OffsetDateTime::now_utc() + time::Duration::seconds(seconds)); + } + if let Some(s) = expires_at_utc { + if s.eq_ignore_ascii_case("forever") || s.is_empty() { + return None; + } + if let Ok(parsed) = + time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339) + { + return Some(parsed); + } + if let Ok(parsed) = time::OffsetDateTime::parse( + &s, + time::macros::format_description!( + "[year]-[month]-[day] [hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]" + ), + ) { + return Some(parsed); + } + } + None +} + +#[allow(clippy::too_many_lines)] +const fn from_wasm_bedrock_disconnect_reason( + reason: pumpkin::plugin::player::BedrockDisconnectReason, +) -> DisconnectReason { + match reason { + pumpkin::plugin::player::BedrockDisconnectReason::Unknown => DisconnectReason::Unknown, + pumpkin::plugin::player::BedrockDisconnectReason::CantConnectNoInternet => { + DisconnectReason::CantConnectNoInternet + } + pumpkin::plugin::player::BedrockDisconnectReason::NoPermissions => { + DisconnectReason::NoPermissions + } + pumpkin::plugin::player::BedrockDisconnectReason::UnrecoverableError => { + DisconnectReason::UnrecoverableError + } + pumpkin::plugin::player::BedrockDisconnectReason::ThirdPartyBlocked => { + DisconnectReason::ThirdPartyBlocked + } + pumpkin::plugin::player::BedrockDisconnectReason::ThirdPartyNoInternet => { + DisconnectReason::ThirdPartyNoInternet + } + pumpkin::plugin::player::BedrockDisconnectReason::ThirdPartyBadIp => { + DisconnectReason::ThirdPartyBadIP + } + pumpkin::plugin::player::BedrockDisconnectReason::ThirdPartyNoServerOrServerLocked => { + DisconnectReason::ThirdPartyNoServerOrServerLocked + } + pumpkin::plugin::player::BedrockDisconnectReason::VersionMismatch => { + DisconnectReason::VersionMismatch + } + pumpkin::plugin::player::BedrockDisconnectReason::SkinIssue => DisconnectReason::SkinIssue, + pumpkin::plugin::player::BedrockDisconnectReason::InviteSessionNotFound => { + DisconnectReason::InviteSessionNotFound + } + pumpkin::plugin::player::BedrockDisconnectReason::EduLevelSettingsMissing => { + DisconnectReason::EduLevelSettingsMissing + } + pumpkin::plugin::player::BedrockDisconnectReason::LocalServerNotFound => { + DisconnectReason::LocalServerNotFound + } + pumpkin::plugin::player::BedrockDisconnectReason::LegacyDisconnect => { + DisconnectReason::LegacyDisconnect + } + pumpkin::plugin::player::BedrockDisconnectReason::UserLeaveGameAttempted => { + DisconnectReason::UserLeaveGameAttempted + } + pumpkin::plugin::player::BedrockDisconnectReason::PlatformLockedSkinsError => { + DisconnectReason::PlatformLockedSkinsError + } + pumpkin::plugin::player::BedrockDisconnectReason::RealmsWorldUnassigned => { + DisconnectReason::RealmsWorldUnassigned + } + pumpkin::plugin::player::BedrockDisconnectReason::RealmsServerCantConnect => { + DisconnectReason::RealmsServerCantConnect + } + pumpkin::plugin::player::BedrockDisconnectReason::RealmsServerHidden => { + DisconnectReason::RealmsServerHidden + } + pumpkin::plugin::player::BedrockDisconnectReason::RealmsServerDisabledBeta => { + DisconnectReason::RealmsServerDisabledBeta + } + pumpkin::plugin::player::BedrockDisconnectReason::RealmsServerDisabled => { + DisconnectReason::RealmsServerDisabled + } + pumpkin::plugin::player::BedrockDisconnectReason::CrossPlatformDisabled => { + DisconnectReason::CrossPlatformDisabled + } + pumpkin::plugin::player::BedrockDisconnectReason::CantConnect => { + DisconnectReason::CantConnect + } + pumpkin::plugin::player::BedrockDisconnectReason::SessionNotFound => { + DisconnectReason::SessionNotFound + } + pumpkin::plugin::player::BedrockDisconnectReason::ClientSettingsIncompatibleWithServer => { + DisconnectReason::ClientSettingsIncompatibleWithServer + } + pumpkin::plugin::player::BedrockDisconnectReason::ServerFull => { + DisconnectReason::ServerFull + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidPlatformSkin => { + DisconnectReason::InvalidPlatformSkin + } + pumpkin::plugin::player::BedrockDisconnectReason::EditionVersionMismatch => { + DisconnectReason::EditionVersionMismatch + } + pumpkin::plugin::player::BedrockDisconnectReason::EditionMismatch => { + DisconnectReason::EditionMismatch + } + pumpkin::plugin::player::BedrockDisconnectReason::LevelNewerThanExeVersion => { + DisconnectReason::LevelNewerThanExeVersion + } + pumpkin::plugin::player::BedrockDisconnectReason::NoFailOccurred => { + DisconnectReason::NoFailOccurred + } + pumpkin::plugin::player::BedrockDisconnectReason::BannedSkin => { + DisconnectReason::BannedSkin + } + pumpkin::plugin::player::BedrockDisconnectReason::Timeout => DisconnectReason::Timeout, + pumpkin::plugin::player::BedrockDisconnectReason::ServerNotFound => { + DisconnectReason::ServerNotFound + } + pumpkin::plugin::player::BedrockDisconnectReason::OutdatedServer => { + DisconnectReason::OutdatedServer + } + pumpkin::plugin::player::BedrockDisconnectReason::OutdatedClient => { + DisconnectReason::OutdatedClient + } + pumpkin::plugin::player::BedrockDisconnectReason::NoPremiumPlatform => { + DisconnectReason::NoPremiumPlatform + } + pumpkin::plugin::player::BedrockDisconnectReason::MultiplayerDisabled => { + DisconnectReason::MultiplayerDisabled + } + pumpkin::plugin::player::BedrockDisconnectReason::NoWifi => DisconnectReason::NoWiFi, + pumpkin::plugin::player::BedrockDisconnectReason::WorldCorruption => { + DisconnectReason::WorldCorruption + } + pumpkin::plugin::player::BedrockDisconnectReason::NoReason => DisconnectReason::NoReason, + pumpkin::plugin::player::BedrockDisconnectReason::Disconnected => { + DisconnectReason::Disconnected + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidPlayer => { + DisconnectReason::InvalidPlayer + } + pumpkin::plugin::player::BedrockDisconnectReason::LoggedInOtherLocation => { + DisconnectReason::LoggedInOtherLocation + } + pumpkin::plugin::player::BedrockDisconnectReason::ServerIdConflict => { + DisconnectReason::ServerIdConflict + } + pumpkin::plugin::player::BedrockDisconnectReason::NotAllowed => { + DisconnectReason::NotAllowed + } + pumpkin::plugin::player::BedrockDisconnectReason::NotAuthenticated => { + DisconnectReason::NotAuthenticated + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidTenant => { + DisconnectReason::InvalidTenant + } + pumpkin::plugin::player::BedrockDisconnectReason::UnknownPacket => { + DisconnectReason::UnknownPacket + } + pumpkin::plugin::player::BedrockDisconnectReason::UnexpectedPacket => { + DisconnectReason::UnexpectedPacket + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidCommandRequestPacket => { + DisconnectReason::InvalidCommandRequestPacket + } + pumpkin::plugin::player::BedrockDisconnectReason::HostSuspended => { + DisconnectReason::HostSuspended + } + pumpkin::plugin::player::BedrockDisconnectReason::LoginPacketNoRequest => { + DisconnectReason::LoginPacketNoRequest + } + pumpkin::plugin::player::BedrockDisconnectReason::LoginPacketNoCert => { + DisconnectReason::LoginPacketNoCert + } + pumpkin::plugin::player::BedrockDisconnectReason::MissingClient => { + DisconnectReason::MissingClient + } + pumpkin::plugin::player::BedrockDisconnectReason::Kicked => DisconnectReason::Kicked, + pumpkin::plugin::player::BedrockDisconnectReason::KickedForExploit => { + DisconnectReason::KickedForExploit + } + pumpkin::plugin::player::BedrockDisconnectReason::KickedForIdle => { + DisconnectReason::KickedForIdle + } + pumpkin::plugin::player::BedrockDisconnectReason::ResourcePackProblem => { + DisconnectReason::ResourcePackProblem + } + pumpkin::plugin::player::BedrockDisconnectReason::IncompatiblePack => { + DisconnectReason::IncompatiblePack + } + pumpkin::plugin::player::BedrockDisconnectReason::OutOfStorage => { + DisconnectReason::OutOfStorage + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidLevel => { + DisconnectReason::InvalidLevel + } + pumpkin::plugin::player::BedrockDisconnectReason::DisconnectPacket => { + DisconnectReason::DisconnectPacket + } + pumpkin::plugin::player::BedrockDisconnectReason::BlockMismatch => { + DisconnectReason::BlockMismatch + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidHeights => { + DisconnectReason::InvalidHeights + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidWidths => { + DisconnectReason::InvalidWidths + } + pumpkin::plugin::player::BedrockDisconnectReason::ConnectionLost => { + DisconnectReason::ConnectionLost + } + pumpkin::plugin::player::BedrockDisconnectReason::ZombieConnection => { + DisconnectReason::ZombieConnection + } + pumpkin::plugin::player::BedrockDisconnectReason::Shutdown => DisconnectReason::Shutdown, + pumpkin::plugin::player::BedrockDisconnectReason::ReasonNotSet => { + DisconnectReason::ReasonNotSet + } + pumpkin::plugin::player::BedrockDisconnectReason::LoadingStateTimeout => { + DisconnectReason::LoadingStateTimeout + } + pumpkin::plugin::player::BedrockDisconnectReason::ResourcePackLoadingFailed => { + DisconnectReason::ResourcePackLoadingFailed + } + pumpkin::plugin::player::BedrockDisconnectReason::SearchingForSessionLoadingScreenFailed => { + DisconnectReason::SearchingForSessionLoadingScreenFailed + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetProtocolVersion => { + DisconnectReason::NetherNetProtocolVersion + } + pumpkin::plugin::player::BedrockDisconnectReason::SubsystemStatusError => { + DisconnectReason::SubsystemStatusError + } + pumpkin::plugin::player::BedrockDisconnectReason::EmptyAuthFromDiscovery => { + DisconnectReason::EmptyAuthFromDiscovery + } + pumpkin::plugin::player::BedrockDisconnectReason::EmptyUrlFromDiscovery => { + DisconnectReason::EmptyUrlFromDiscovery + } + pumpkin::plugin::player::BedrockDisconnectReason::ExpiredAuthFromDiscovery => { + DisconnectReason::ExpiredAuthFromDiscovery + } + pumpkin::plugin::player::BedrockDisconnectReason::UnknownSignalServiceSignInFailure => { + DisconnectReason::UnknownSignalServiceSignInFailure + } + pumpkin::plugin::player::BedrockDisconnectReason::XblJoinLobbyFailure => { + DisconnectReason::XBLJoinLobbyFailure + } + pumpkin::plugin::player::BedrockDisconnectReason::UnspecifiedClientInstanceDisconnection => { + DisconnectReason::UnspecifiedClientInstanceDisconnection + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetSessionNotFound => { + DisconnectReason::NetherNetSessionNotFound + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetCreatePeerConnection => { + DisconnectReason::NetherNetCreatePeerConnection + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetIce => { + DisconnectReason::NetherNetICE + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetConnectRequest => { + DisconnectReason::NetherNetConnectRequest + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetConnectResponse => { + DisconnectReason::NetherNetConnectResponse + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetNegotiationTimeout => { + DisconnectReason::NetherNetNegotiationTimeout + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetInactivityTimeout => { + DisconnectReason::NetherNetInactivityTimeout + } + pumpkin::plugin::player::BedrockDisconnectReason::StaleConnectionBeingReplaced => { + DisconnectReason::StaleConnectionBeingReplaced + } + pumpkin::plugin::player::BedrockDisconnectReason::RealmsSessionNotFound => { + DisconnectReason::RealmsSessionNotFound + } + pumpkin::plugin::player::BedrockDisconnectReason::BadPacket => DisconnectReason::BadPacket, + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetFailedToCreateOffer => { + DisconnectReason::NetherNetFailedToCreateOffer + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetFailedToCreateAnswer => { + DisconnectReason::NetherNetFailedToCreateAnswer + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetFailedToSetLocalDescription => { + DisconnectReason::NetherNetFailedToSetLocalDescription + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetFailedToSetRemoteDescription => { + DisconnectReason::NetherNetFailedToSetRemoteDescription + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetNegotiationTimeoutWaitingForResponse => { + DisconnectReason::NetherNetNegotiationTimeoutWaitingForResponse + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetNegotiationTimeoutWaitingForAccept => { + DisconnectReason::NetherNetNegotiationTimeoutWaitingForAccept + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetIncomingConnectionIgnored => { + DisconnectReason::NetherNetIncomingConnectionIgnored + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetSignalingParsingFailure => { + DisconnectReason::NetherNetSignalingParsingFailure + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetSignalingUnknownError => { + DisconnectReason::NetherNetSignalingUnknownError + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetSignalingUnicastDeliveryFailed => { + DisconnectReason::NetherNetSignalingUnicastDeliveryFailed + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetSignalingBroadcastDeliveryFailed => { + DisconnectReason::NetherNetSignalingBroadcastDeliveryFailed + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetSignalingGenericDeliveryFailed => { + DisconnectReason::NetherNetSignalingGenericDeliveryFailed + } + pumpkin::plugin::player::BedrockDisconnectReason::EditorMismatchEditorWorld => { + DisconnectReason::EditorMismatchEditorWorld + } + pumpkin::plugin::player::BedrockDisconnectReason::EditorMismatchVanillaWorld => { + DisconnectReason::EditorMismatchVanillaWorld + } + pumpkin::plugin::player::BedrockDisconnectReason::WorldTransferNotPrimaryClient => { + DisconnectReason::WorldTransferNotPrimaryClient + } + pumpkin::plugin::player::BedrockDisconnectReason::RequestServerShutdown => { + DisconnectReason::RequestServerShutdown + } + pumpkin::plugin::player::BedrockDisconnectReason::ClientGameSetupCancelled => { + DisconnectReason::ClientGameSetupCancelled + } + pumpkin::plugin::player::BedrockDisconnectReason::ClientGameSetupFailed => { + DisconnectReason::ClientGameSetupFailed + } + pumpkin::plugin::player::BedrockDisconnectReason::NoVenue => DisconnectReason::NoVenue, + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetSignalingSigninFailed => { + DisconnectReason::NetherNetSignalingSigninFailed + } + pumpkin::plugin::player::BedrockDisconnectReason::SessionAccessDenied => { + DisconnectReason::SessionAccessDenied + } + pumpkin::plugin::player::BedrockDisconnectReason::ServiceSigninIssue => { + DisconnectReason::ServiceSigninIssue + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetNoSignalingChannel => { + DisconnectReason::NetherNetNoSignalingChannel + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetNotLoggedIn => { + DisconnectReason::NetherNetNotLoggedIn + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetClientSignalingError => { + DisconnectReason::NetherNetClientSignalingError + } + pumpkin::plugin::player::BedrockDisconnectReason::SubClientLoginDisabled => { + DisconnectReason::SubClientLoginDisabled + } + pumpkin::plugin::player::BedrockDisconnectReason::DeepLinkTryingToOpenDemoWorldWhileSignedIn => { + DisconnectReason::DeepLinkTryingToOpenDemoWorldWhileSignedIn + } + pumpkin::plugin::player::BedrockDisconnectReason::AsyncJoinTaskDenied => { + DisconnectReason::AsyncJoinTaskDenied + } + pumpkin::plugin::player::BedrockDisconnectReason::RealmsTimelineRequired => { + DisconnectReason::RealmsTimelineRequired + } + pumpkin::plugin::player::BedrockDisconnectReason::GuestWithoutHost => { + DisconnectReason::GuestWithoutHost + } + pumpkin::plugin::player::BedrockDisconnectReason::FailedToJoinExperience => { + DisconnectReason::FailedToJoinExperience + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetDataChannelClosed => { + DisconnectReason::NetherNetDataChannelClosed + } + pumpkin::plugin::player::BedrockDisconnectReason::DiscoveryEnvironmentMismatch => { + DisconnectReason::DiscoveryEnvironmentMismatch + } + pumpkin::plugin::player::BedrockDisconnectReason::HostWithoutKeys => { + DisconnectReason::HostWithoutKeys + } + pumpkin::plugin::player::BedrockDisconnectReason::HostSignedOut => { + DisconnectReason::HostSignedOut + } + pumpkin::plugin::player::BedrockDisconnectReason::ScriptWatchdogException => { + DisconnectReason::ScriptWatchdogException + } + pumpkin::plugin::player::BedrockDisconnectReason::ScriptMemoryLimitExceeded => { + DisconnectReason::ScriptMemoryLimitExceeded + } + pumpkin::plugin::player::BedrockDisconnectReason::StorageLowDuringGameplay => { + DisconnectReason::StorageLowDuringGameplay + } + pumpkin::plugin::player::BedrockDisconnectReason::StorageFullDuringGameplay => { + DisconnectReason::StorageFullDuringGameplay + } + pumpkin::plugin::player::BedrockDisconnectReason::LevelStorageCorruption => { + DisconnectReason::LevelStorageCorruption + } + pumpkin::plugin::player::BedrockDisconnectReason::EditionMismatchVanillaToEdu => { + DisconnectReason::EditionMismatchVanillaToEdu + } + pumpkin::plugin::player::BedrockDisconnectReason::EditionMismatchEduToVanilla => { + DisconnectReason::EditionMismatchEduToVanilla + } + pumpkin::plugin::player::BedrockDisconnectReason::EditorMismatchEditorToVanilla => { + DisconnectReason::EditorMismatchEditorToVanilla + } + pumpkin::plugin::player::BedrockDisconnectReason::EditorMismatchVanillaToEditor => { + DisconnectReason::EditorMismatchVanillaToEditor + } + pumpkin::plugin::player::BedrockDisconnectReason::DenyListed => { + DisconnectReason::DenyListed + } + pumpkin::plugin::player::BedrockDisconnectReason::NonceMissing => { + DisconnectReason::NonceMissing + } + pumpkin::plugin::player::BedrockDisconnectReason::NonceNotFound => { + DisconnectReason::NonceNotFound + } + pumpkin::plugin::player::BedrockDisconnectReason::NonceExpired => { + DisconnectReason::NonceExpired + } + pumpkin::plugin::player::BedrockDisconnectReason::NonceNotValid => { + DisconnectReason::NonceNotValid + } + pumpkin::plugin::player::BedrockDisconnectReason::HostDisconnected => { + DisconnectReason::HostDisconnected + } + pumpkin::plugin::player::BedrockDisconnectReason::EditorJoinIntentPolicyFailure => { + DisconnectReason::EditorJoinIntentPolicyFailure + } + pumpkin::plugin::player::BedrockDisconnectReason::NetherNetIdentityNotAllowed => { + DisconnectReason::NetherNetIdentityNotAllowed + } + pumpkin::plugin::player::BedrockDisconnectReason::InvalidName => { + DisconnectReason::InvalidName + } + pumpkin::plugin::player::BedrockDisconnectReason::ExpiredToken => { + DisconnectReason::ExpiredToken + } + pumpkin::plugin::player::BedrockDisconnectReason::HostAcceptsNoTypeOfAuth => { + DisconnectReason::HostAcceptsNoTypeOfAuth + } + pumpkin::plugin::player::BedrockDisconnectReason::NotAuthenticatedFastFail => { + DisconnectReason::NotAuthenticatedFastFail + } + pumpkin::plugin::player::BedrockDisconnectReason::EditorNotAllowed => { + DisconnectReason::EditorNotAllowed + } + } +} + impl DowncastResourceExt for Resource { fn downcast_ref<'a>(&'a self, state: &'a mut PluginHostState) -> &'a PlayerResource { state @@ -1423,17 +1886,6 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(()) } - async fn kick( - &mut self, - player: Resource, - message: wasmtime::component::Resource, - ) -> wasmtime::Result<()> { - let component = text_component_from_resource(self, &message); - let player = player_from_resource(self, &player)?; - player.kick(DisconnectReason::Kicked, component).await; - Ok(()) - } - async fn respawn(&mut self, player: Resource) -> wasmtime::Result<()> { let player = player_from_resource(self, &player)?; player.respawn().await; @@ -1471,24 +1923,50 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { async fn ban( &mut self, player: Resource, - reason: Option>, + options: pumpkin::plugin::player::BanPlayerOptions, ) -> wasmtime::Result<()> { + let reason = options + .reason + .as_ref() + .map(|r| text_component_from_resource(self, r)); + let expires = parse_ban_expiry(options.expires_at_utc, options.duration_seconds); let player = player_from_resource(self, &player)?; let server = self.server.as_ref().expect("server not available"); - let reason = reason.map(|r| text_component_from_resource(self, &r)); - player.ban(server, reason).await; + player + .ban_explicit( + server, + reason, + options.source, + expires, + options.kick_if_online, + options.log_to_console, + ) + .await; Ok(()) } async fn ban_ip( &mut self, player: Resource, - reason: Option>, + options: pumpkin::plugin::player::BanIpOptions, ) -> wasmtime::Result<()> { + let reason = options + .reason + .as_ref() + .map(|r| text_component_from_resource(self, r)); + let expires = parse_ban_expiry(options.expires_at_utc, options.duration_seconds); let player = player_from_resource(self, &player)?; let server = self.server.as_ref().expect("server not available"); - let reason = reason.map(|r| text_component_from_resource(self, &r)); - player.ban_ip(server, reason).await; + player + .ban_ip_explicit( + server, + reason, + options.source, + expires, + options.kick_matching_players, + options.log_to_console, + ) + .await; Ok(()) } @@ -2357,6 +2835,52 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { Ok(()) } + async fn kick( + &mut self, + player: Resource, + options: pumpkin::plugin::player::JavaKickOptions, + ) -> wasmtime::Result<()> { + let player = self + .resource_table + .get::( + &Resource::new_own(player.rep()), + ) + .map_err(|_| wasmtime::Error::msg("invalid java-player resource handle"))? + .provider + .clone(); + + let reason = text_component_from_resource(self, &options.reason); + if options.log_to_console { + tracing::info!( + "Kicking Java player {} ({}): {}", + player.gameprofile.name, + player.gameprofile.id, + reason.clone().to_pretty_console() + ); + } + + if let Some(server) = player.world().server.upgrade() + && let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) + { + let mut event = crate::plugin::api::events::player::player_kick::PlayerKickEvent::new( + player_arc, + reason.clone().to_pretty_console(), + ); + server.plugin_manager.fire(&server, &mut event).await; + if event.cancelled { + return Ok(()); + } + } + + let send_packet = options.teardown_policy + != pumpkin::plugin::player::SocketTeardownPolicy::DropConnection; + if let crate::net::ClientPlatform::Java(java) = player.client.as_ref() { + java.kick_explicit(&reason, send_packet).await; + } + + Ok(()) + } + async fn drop( &mut self, rep: Resource, @@ -2787,6 +3311,61 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState { Ok(()) } + async fn kick( + &mut self, + player: Resource, + options: pumpkin::plugin::player::BedrockKickOptions, + ) -> wasmtime::Result<()> { + let player = self + .resource_table + .get::( + &Resource::new_own(player.rep()), + ) + .map_err(|_| wasmtime::Error::msg("invalid bedrock-player resource handle"))? + .provider + .clone(); + + let disconnect_reason = from_wasm_bedrock_disconnect_reason(options.reason); + if options.log_to_console { + tracing::info!( + "Kicking Bedrock player {} ({}) [Reason: {:?}]: {}", + player.gameprofile.name, + player.gameprofile.id, + disconnect_reason, + options.message + ); + } + + if let Some(server) = player.world().server.upgrade() + && let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) + { + let mut event = crate::plugin::api::events::player::player_kick::PlayerKickEvent::new( + player_arc, + options.message.clone(), + ); + server.plugin_manager.fire(&server, &mut event).await; + if event.cancelled { + return Ok(()); + } + } + + let send_packet = options.teardown_policy + != pumpkin::plugin::player::SocketTeardownPolicy::DropConnection; + if let crate::net::ClientPlatform::Bedrock(bedrock) = player.client.as_ref() { + bedrock + .kick_explicit( + disconnect_reason, + options.message, + options.skip_message, + options.filtered_message, + send_packet, + ) + .await; + } + + Ok(()) + } + async fn drop( &mut self, rep: Resource, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs index 40fb57d67..5dce984fd 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs @@ -5,15 +5,23 @@ use crate::command::CommandSender; use crate::plugin::loader::wasm::wasm_host::wit::v0_1::pumpkin::plugin::recipe::RecipeManager as WitRecipeManager; use pumpkin::plugin::server::CommandSender as WasmCommandSender; -use super::player::text_component_from_resource; +use super::player::{ + from_wit_permission_level, parse_ban_expiry, text_component_from_resource, + to_wit_permission_level, +}; +use crate::data::SaveJSONConfiguration; use crate::plugin::{ loader::wasm::wasm_host::{ state::{PluginHostState, ServerResource}, wit::v0_1::pumpkin::{ self, plugin::{ - player::Player, - server::{Difficulty, Dimension, Server, SysInfo}, + player::{BanIpOptions, BanPlayerOptions, Player}, + server::{ + BanManager as WitBanManager, BannedIpEntry, BannedPlayerEntry, Difficulty, + Dimension, OpEntry, OpManager as WitOpManager, Server, SysInfo, + WhitelistEntry as WitWhitelistEntry, WhitelistManager as WitWhitelistManager, + }, uuid::Uuid as WitUuid, }, }, @@ -397,6 +405,39 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { self.add_recipe_manager(server.recipe_manager.clone()) } + async fn get_op_manager( + &mut self, + _rep: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + self.add_op_manager(server.clone()) + } + + async fn get_ban_manager( + &mut self, + _rep: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + self.add_ban_manager(server.clone()) + } + + async fn get_whitelist_manager( + &mut self, + _rep: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + self.add_whitelist_manager(server.clone()) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { self.resource_table .delete::(Resource::new_own(rep.rep())) @@ -404,3 +445,634 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { Ok(()) } } + +impl pumpkin::plugin::server::HostOpManager for PluginHostState { + async fn is_op(&mut self, _res: Resource, id: WitUuid) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let ops = server.data.operator_config.read().await; + Ok(ops.get_entry(&uuid).is_some()) + } + + async fn get_op( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let ops = server.data.operator_config.read().await; + Ok(ops.get_entry(&uuid).map(|entry| OpEntry { + uuid: WitUuid::to_wit(&entry.uuid), + name: entry.name.clone(), + level: to_wit_permission_level(entry.level), + bypasses_player_limit: entry.bypasses_player_limit, + })) + } + + async fn get_permission_level( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let ops = server.data.operator_config.read().await; + Ok(ops.get_entry(&uuid).map_or( + pumpkin::plugin::permission::PermissionLevel::Zero, + |entry| to_wit_permission_level(entry.level), + )) + } + + async fn op_player( + &mut self, + _res: Resource, + name: String, + id: WitUuid, + level: pumpkin::plugin::permission::PermissionLevel, + bypasses_player_limit: bool, + ) -> wasmtime::Result<()> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let internal_level = from_wit_permission_level(level); + + let mut config = server.data.operator_config.write().await; + if let Some(existing) = config.ops.iter_mut().find(|o| o.uuid == uuid) { + existing.level = internal_level; + existing.name.clone_from(&name); + existing.bypasses_player_limit = bypasses_player_limit; + } else { + let op_entry = pumpkin_config::op::Op::new( + uuid, + name.clone(), + internal_level, + bypasses_player_limit, + ); + config.ops.push(op_entry); + } + config.save(); + drop(config); + + if let Some(player) = server.get_player_by_uuid(uuid) { + let command_dispatcher = server.command_dispatcher.read().await; + player + .set_permission_lvl(server, internal_level, &command_dispatcher) + .await; + } + + Ok(()) + } + + async fn deop_player( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + + let mut config = server.data.operator_config.write().await; + if let Some(op_index) = config.ops.iter().position(|o| o.uuid == uuid) { + config.ops.remove(op_index); + config.save(); + drop(config); + + if let Some(player) = server.get_player_by_uuid(uuid) { + let command_dispatcher = server.command_dispatcher.read().await; + player + .set_permission_lvl( + server, + pumpkin_util::PermissionLvl::Zero, + &command_dispatcher, + ) + .await; + } + + Ok(true) + } else { + Ok(false) + } + } + + async fn list_ops(&mut self, _res: Resource) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let config = server.data.operator_config.read().await; + Ok(config + .ops + .iter() + .map(|entry| OpEntry { + uuid: WitUuid::to_wit(&entry.uuid), + name: entry.name.clone(), + level: to_wit_permission_level(entry.level), + bypasses_player_limit: entry.bypasses_player_limit, + }) + .collect()) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::( + Resource::new_own(rep.rep()), + ); + Ok(()) + } +} + +impl pumpkin::plugin::server::HostBanManager for PluginHostState { + async fn is_player_banned( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let now = time::OffsetDateTime::now_utc(); + let mut list = server.data.banned_player_list.write().await; + list.banned_players + .retain(|entry| entry.expires.is_none_or(|expires| expires > now)); + list.save(); + Ok(list.banned_players.iter().any(|e| e.uuid == uuid)) + } + + async fn get_player_ban( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let now = time::OffsetDateTime::now_utc(); + let mut list = server.data.banned_player_list.write().await; + list.banned_players + .retain(|entry| entry.expires.is_none_or(|expires| expires > now)); + list.save(); + Ok(list + .banned_players + .iter() + .find(|e| e.uuid == uuid) + .map(|e| BannedPlayerEntry { + uuid: WitUuid::to_wit(&e.uuid), + name: e.name.clone(), + created: e + .created + .format(&time::format_description::well_known::Rfc3339) + .unwrap_or_default(), + source: e.source.clone(), + expires: e.expires.and_then(|exp| { + exp.format(&time::format_description::well_known::Rfc3339) + .ok() + }), + reason: e.reason.clone(), + })) + } + + async fn ban_player( + &mut self, + _res: Resource, + name: String, + id: WitUuid, + options: BanPlayerOptions, + ) -> wasmtime::Result<()> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let reason_text = options + .reason + .as_ref() + .map(|res| text_component_from_resource(self, res)) + .map_or_else( + || "Banned by plugin.".to_string(), + pumpkin_util::text::TextComponent::to_pretty_console, + ); + let source_name = options.source.unwrap_or_else(|| "Plugin".to_string()); + let expires = parse_ban_expiry(options.expires_at_utc, options.duration_seconds); + + let mut list = server.data.banned_player_list.write().await; + if let Some(existing) = list.banned_players.iter_mut().find(|e| e.uuid == uuid) { + existing.name.clone_from(&name); + existing.source = source_name; + existing.expires = expires; + existing.reason.clone_from(&reason_text); + } else { + let entry = crate::data::banlist_serializer::BannedPlayerEntry { + uuid, + name: name.clone(), + created: time::OffsetDateTime::now_utc(), + source: source_name, + expires, + reason: reason_text.clone(), + }; + list.banned_players.push(entry); + } + list.save(); + drop(list); + + if options.kick_if_online + && let Some(player) = server.get_player_by_uuid(uuid) + { + player + .kick( + crate::net::DisconnectReason::Kicked, + pumpkin_util::text::TextComponent::text(reason_text.clone()), + ) + .await; + } + + if options.log_to_console { + tracing::info!("Banned player {} ({}): {}", name, uuid, reason_text); + } + Ok(()) + } + + async fn unban_player( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let mut list = server.data.banned_player_list.write().await; + Ok(list + .banned_players + .iter() + .position(|e| e.uuid == uuid) + .is_some_and(|pos| { + list.banned_players.remove(pos); + list.save(); + true + })) + } + + async fn list_player_bans( + &mut self, + _res: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let now = time::OffsetDateTime::now_utc(); + let mut list = server.data.banned_player_list.write().await; + list.banned_players + .retain(|entry| entry.expires.is_none_or(|expires| expires > now)); + list.save(); + Ok(list + .banned_players + .iter() + .map(|e| BannedPlayerEntry { + uuid: WitUuid::to_wit(&e.uuid), + name: e.name.clone(), + created: e + .created + .format(&time::format_description::well_known::Rfc3339) + .unwrap_or_default(), + source: e.source.clone(), + expires: e.expires.and_then(|exp| { + exp.format(&time::format_description::well_known::Rfc3339) + .ok() + }), + reason: e.reason.clone(), + }) + .collect()) + } + + async fn is_ip_banned( + &mut self, + _res: Resource, + ip: String, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let ip_addr: std::net::IpAddr = ip + .parse() + .map_err(|_| wasmtime::Error::msg("Invalid IP address"))?; + let now = time::OffsetDateTime::now_utc(); + let mut list = server.data.banned_ip_list.write().await; + list.banned_ips + .retain(|entry| entry.expires.is_none_or(|expires| expires > now)); + list.save(); + Ok(list.banned_ips.iter().any(|e| e.ip == ip_addr)) + } + + async fn get_ip_ban( + &mut self, + _res: Resource, + ip: String, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let ip_addr: std::net::IpAddr = ip + .parse() + .map_err(|_| wasmtime::Error::msg("Invalid IP address"))?; + let now = time::OffsetDateTime::now_utc(); + let mut list = server.data.banned_ip_list.write().await; + list.banned_ips + .retain(|entry| entry.expires.is_none_or(|expires| expires > now)); + list.save(); + Ok(list + .banned_ips + .iter() + .find(|e| e.ip == ip_addr) + .map(|e| BannedIpEntry { + ip: e.ip.to_string(), + created: e + .created + .format(&time::format_description::well_known::Rfc3339) + .unwrap_or_default(), + source: e.source.clone(), + expires: e.expires.and_then(|exp| { + exp.format(&time::format_description::well_known::Rfc3339) + .ok() + }), + reason: e.reason.clone(), + })) + } + + async fn ban_ip( + &mut self, + _res: Resource, + ip: String, + options: BanIpOptions, + ) -> wasmtime::Result<()> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let ip_addr: std::net::IpAddr = ip + .parse() + .map_err(|_| wasmtime::Error::msg("Invalid IP address"))?; + let reason_text = options + .reason + .as_ref() + .map(|res| text_component_from_resource(self, res)) + .map_or_else( + || "Banned by plugin.".to_string(), + pumpkin_util::text::TextComponent::to_pretty_console, + ); + let source_name = options.source.unwrap_or_else(|| "Plugin".to_string()); + let expires = parse_ban_expiry(options.expires_at_utc, options.duration_seconds); + + let mut list = server.data.banned_ip_list.write().await; + if let Some(existing) = list.banned_ips.iter_mut().find(|e| e.ip == ip_addr) { + existing.source = source_name; + existing.expires = expires; + existing.reason.clone_from(&reason_text); + } else { + let entry = crate::data::banlist_serializer::BannedIpEntry { + ip: ip_addr, + created: time::OffsetDateTime::now_utc(), + source: source_name, + expires, + reason: reason_text.clone(), + }; + list.banned_ips.push(entry); + } + list.save(); + drop(list); + + if options.kick_matching_players { + for player in server.get_all_players() { + if player.client.address().ip() == ip_addr { + player + .kick( + crate::net::DisconnectReason::Kicked, + pumpkin_util::text::TextComponent::text(reason_text.clone()), + ) + .await; + } + } + } + + if options.log_to_console { + tracing::info!("Banned IP {}: {}", ip_addr, reason_text); + } + Ok(()) + } + + async fn unban_ip( + &mut self, + _res: Resource, + ip: String, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let ip_addr: std::net::IpAddr = ip + .parse() + .map_err(|_| wasmtime::Error::msg("Invalid IP address"))?; + let mut list = server.data.banned_ip_list.write().await; + Ok(list + .banned_ips + .iter() + .position(|e| e.ip == ip_addr) + .is_some_and(|pos| { + list.banned_ips.remove(pos); + list.save(); + true + })) + } + + async fn list_ip_bans( + &mut self, + _res: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let now = time::OffsetDateTime::now_utc(); + let mut list = server.data.banned_ip_list.write().await; + list.banned_ips + .retain(|entry| entry.expires.is_none_or(|expires| expires > now)); + list.save(); + Ok(list + .banned_ips + .iter() + .map(|e| BannedIpEntry { + ip: e.ip.to_string(), + created: e + .created + .format(&time::format_description::well_known::Rfc3339) + .unwrap_or_default(), + source: e.source.clone(), + expires: e.expires.and_then(|exp| { + exp.format(&time::format_description::well_known::Rfc3339) + .ok() + }), + reason: e.reason.clone(), + }) + .collect()) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::( + Resource::new_own(rep.rep()), + ); + Ok(()) + } +} + +impl pumpkin::plugin::server::HostWhitelistManager for PluginHostState { + async fn is_enabled(&mut self, _res: Resource) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + Ok(server.white_list.load(std::sync::atomic::Ordering::Relaxed)) + } + + async fn set_enabled( + &mut self, + _res: Resource, + enabled: bool, + ) -> wasmtime::Result<()> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + server + .white_list + .store(enabled, std::sync::atomic::Ordering::Relaxed); + if enabled && server.basic_config.enforce_whitelist { + let whitelist = server.data.whitelist_config.read().await; + for player in server.get_all_players() { + if !whitelist.is_whitelisted(&player.gameprofile) { + player + .kick( + crate::net::DisconnectReason::Kicked, + pumpkin_macros::translate_cross!( + pumpkin_data::translation::java::MULTIPLAYER_DISCONNECT_NOT_WHITELISTED, + pumpkin_data::translation::bedrock::DISCONNECT_KICKED + ), + ) + .await; + } + } + } + Ok(()) + } + + async fn is_whitelisted( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let whitelist = server.data.whitelist_config.read().await; + Ok(whitelist.whitelist.iter().any(|e| e.uuid == uuid)) + } + + async fn add_player( + &mut self, + _res: Resource, + name: String, + id: WitUuid, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let mut config = server.data.whitelist_config.write().await; + if config.whitelist.iter().any(|e| e.uuid == uuid) { + Ok(false) + } else { + config + .whitelist + .push(pumpkin_config::whitelist::WhitelistEntry::new(uuid, name)); + config.save(); + Ok(true) + } + } + + async fn remove_player( + &mut self, + _res: Resource, + id: WitUuid, + ) -> wasmtime::Result { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let uuid = WitUuid::from_wit(&id); + let mut config = server.data.whitelist_config.write().await; + Ok(config + .whitelist + .iter() + .position(|e| e.uuid == uuid) + .is_some_and(|pos| { + config.whitelist.remove(pos); + config.save(); + true + })) + } + + async fn list_entries( + &mut self, + _res: Resource, + ) -> wasmtime::Result> { + let server = self + .server + .as_ref() + .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; + let config = server.data.whitelist_config.read().await; + Ok(config + .whitelist + .iter() + .map(|e| WitWhitelistEntry { + uuid: WitUuid::to_wit(&e.uuid), + name: e.name.clone(), + }) + .collect()) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::( + Resource::new_own(rep.rep()), + ); + Ok(()) + } +} diff --git a/tools/pumpkin-codegen/src/wit/attribute.rs b/tools/pumpkin-codegen/src/wit/attribute.rs new file mode 100644 index 000000000..9b686ccb2 --- /dev/null +++ b/tools/pumpkin-codegen/src/wit/attribute.rs @@ -0,0 +1,66 @@ +use semver::Version; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::fs; +use wit_encoder::{Enum, Interface, Package, PackageName, Record, TypeDef, TypeDefKind}; + +#[derive(Deserialize)] +struct AttributeEntry { + id: u8, + default_value: f64, +} + +pub fn build() -> String { + let attributes: BTreeMap = + serde_json::from_str(&fs::read_to_string("../../assets/attributes.json").unwrap()) + .expect("Failed to parse attributes.json"); + + let mut package = Package::new(PackageName::new( + "pumpkin", + "plugin", + Some(Version::new(0, 1, 0)), + )); + let mut interface = Interface::new("attributes"); + + let mut sorted_attributes: Vec<(String, AttributeEntry)> = attributes.into_iter().collect(); + sorted_attributes.sort_by_key(|(_, raw)| raw.id); + + let mut attribute_enum = Enum::empty(); + for (raw_name, _) in sorted_attributes { + let name = raw_name + .strip_prefix("minecraft:") + .unwrap_or(&raw_name) + .replace('_', "-"); + attribute_enum.case(name); + } + + interface.type_def(TypeDef::new("attribute", TypeDefKind::Enum(attribute_enum))); + + let mut modifier_op_enum = Enum::empty(); + modifier_op_enum.case("add"); + modifier_op_enum.case("multiply-base"); + modifier_op_enum.case("multiply-total"); + + interface.type_def(TypeDef::new( + "modifier-operation", + TypeDefKind::Enum(modifier_op_enum), + )); + + let modifier_record = Record::new([ + wit_encoder::Field::new("id", wit_encoder::Type::String), + wit_encoder::Field::new("amount", wit_encoder::Type::F64), + wit_encoder::Field::new( + "operation", + wit_encoder::Type::Named(wit_encoder::Ident::new("modifier-operation")), + ), + ]); + + interface.type_def(TypeDef::new( + "attribute-modifier", + TypeDefKind::Record(modifier_record), + )); + + package.interface(interface); + + package.to_string() +} diff --git a/tools/pumpkin-codegen/src/wit/mod.rs b/tools/pumpkin-codegen/src/wit/mod.rs index 633ec5778..cc6493374 100644 --- a/tools/pumpkin-codegen/src/wit/mod.rs +++ b/tools/pumpkin-codegen/src/wit/mod.rs @@ -1,3 +1,4 @@ +pub mod attribute; pub mod bedrock_packet; pub mod biome; pub mod data_component; @@ -32,6 +33,7 @@ pub fn main() { (data_component::build, "data-components.wit"), (enchantment::build, "enchantments.wit"), (biome::build, "biomes.wit"), + (attribute::build, "attributes.wit"), ]; for (build_fn, file) in build_functions {