392 lines
14 KiB
Rust
392 lines
14 KiB
Rust
use std::fmt;
|
|
use std::pin::Pin;
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
|
|
use crate::entity::player::Player;
|
|
use crate::server::Server;
|
|
use crate::world::World;
|
|
use args::ConsumedArgs;
|
|
|
|
use crate::block::entities::BlockEntity;
|
|
use crate::block::entities::command_block::CommandBlockEntity;
|
|
use crate::command::context::command_source::CommandSource;
|
|
use crate::entity::EntityBase;
|
|
use dispatcher::CommandError;
|
|
use pumpkin_data::{
|
|
Block,
|
|
block_properties::{BlockProperties, CommandBlockLikeProperties, Facing},
|
|
dimension::Dimension,
|
|
};
|
|
use pumpkin_util::math::vector2::Vector2;
|
|
use pumpkin_util::math::vector3::Vector3;
|
|
use pumpkin_util::permission::{PermissionDefault, PermissionLvl};
|
|
use pumpkin_util::text::TextComponent;
|
|
use pumpkin_util::translation::Locale;
|
|
|
|
pub mod args;
|
|
pub mod argument_builder;
|
|
pub mod argument_types;
|
|
pub mod client_suggestions;
|
|
pub mod commands;
|
|
pub mod context;
|
|
pub mod dispatcher;
|
|
pub mod errors;
|
|
pub mod node;
|
|
pub mod parser;
|
|
pub mod snbt;
|
|
pub mod string_reader;
|
|
pub mod suggestion;
|
|
pub mod tree;
|
|
|
|
/// Represents the source of a command execution.
|
|
///
|
|
/// Different senders have different permissions, output targets, and
|
|
/// positions in the world. This enum abstracts those differences for the
|
|
/// command dispatcher.
|
|
#[derive(Clone)]
|
|
pub enum CommandSender {
|
|
/// A remote console connection via the RCON protocol.
|
|
///
|
|
/// Stores an asynchronous buffer to capture command output
|
|
/// so it can be sent back over the network to the RCON client.
|
|
Rcon(Arc<tokio::sync::Mutex<Vec<String>>>),
|
|
/// The local server terminal/console.
|
|
///
|
|
/// This sender typically has absolute permissions (bypass) and
|
|
/// outputs directly to the server logs.
|
|
Console,
|
|
/// A player currently connected to the server.
|
|
///
|
|
/// Contains a reference to the [Player] struct to access their
|
|
/// location, permissions, and session.
|
|
Player(Arc<Player>),
|
|
/// A Command Block or Command Block Minecart.
|
|
///
|
|
/// Contains the block entity responsible for the command and the
|
|
/// world context it exists in for coordinate-relative execution (e.g., `~ ~ ~`).
|
|
CommandBlock(Arc<CommandBlockEntity>, Arc<World>),
|
|
/// Nothingness. Anything sent to this sender is void.
|
|
/// Has the same permissions as that of `CommandBlock`.
|
|
Dummy,
|
|
}
|
|
|
|
impl fmt::Display for CommandSender {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"{}",
|
|
match self {
|
|
Self::Console => "Server",
|
|
Self::Rcon(_) => "Rcon",
|
|
Self::Player(p) => &p.gameprofile.name,
|
|
Self::CommandBlock(..) => "@",
|
|
Self::Dummy => "",
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
impl CommandSender {
|
|
pub async fn send_message(&self, text: TextComponent) {
|
|
match self {
|
|
#[allow(clippy::print_stdout)]
|
|
Self::Console => println!("{}", text.to_pretty_console()),
|
|
Self::Player(c) => c.send_system_message(&text).await,
|
|
Self::Rcon(s) => s.lock().await.push(text.to_pretty_console()),
|
|
Self::CommandBlock(block_entity, _) => {
|
|
let mut last_output = block_entity.last_output.lock().await;
|
|
|
|
let now = time::OffsetDateTime::now_utc();
|
|
let format = time::macros::format_description!("[hour]:[minute]:[second]");
|
|
let timestamp = now.format(&format).unwrap();
|
|
|
|
*last_output = format!("[{}] {}", timestamp, text.get_text());
|
|
}
|
|
Self::Dummy => {}
|
|
}
|
|
}
|
|
|
|
pub fn set_success_count(&self, count: u32) {
|
|
if let Self::CommandBlock(c, _) = self {
|
|
c.success_count
|
|
.store(count, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn is_player(&self) -> bool {
|
|
matches!(self, Self::Player(_))
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn is_console(&self) -> bool {
|
|
matches!(self, Self::Console)
|
|
}
|
|
#[must_use]
|
|
pub fn as_player(&self) -> Option<Arc<Player>> {
|
|
match self {
|
|
Self::Player(player) => Some(player.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// prefer using `has_permission_lvl(lvl)`
|
|
#[must_use]
|
|
pub fn permission_lvl(&self) -> PermissionLvl {
|
|
match self {
|
|
Self::Console | Self::Rcon(_) => PermissionLvl::Four,
|
|
Self::Player(p) => p.permission_lvl.load(),
|
|
Self::CommandBlock(..) | Self::Dummy => PermissionLvl::Two,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn has_permission_lvl(&self, lvl: PermissionLvl) -> bool {
|
|
match self {
|
|
Self::Console | Self::Rcon(_) => true,
|
|
Self::Player(p) => p.permission_lvl.load().ge(&lvl),
|
|
Self::CommandBlock(..) | Self::Dummy => PermissionLvl::Two >= lvl,
|
|
}
|
|
}
|
|
|
|
/// Check if the sender has a specific permission
|
|
pub async fn has_permission(&self, server: &Server, node: &str) -> bool {
|
|
match self {
|
|
Self::Console | Self::Rcon(_) => true, // Console and RCON always have all permissions
|
|
Self::Player(p) => p.has_permission(server, node).await,
|
|
Self::CommandBlock(..) | Self::Dummy => {
|
|
let perm_reg = server.permission_registry.read().await;
|
|
let Some(p) = perm_reg.get_permission(node) else {
|
|
return false;
|
|
};
|
|
match p.default {
|
|
PermissionDefault::Allow => true,
|
|
PermissionDefault::Deny => false,
|
|
PermissionDefault::Op(o) => o <= PermissionLvl::Two,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn position(&self) -> Option<Vector3<f64>> {
|
|
match self {
|
|
Self::Console | Self::Rcon(..) | Self::Dummy => None,
|
|
Self::Player(p) => Some(p.living_entity.entity.pos.load()),
|
|
Self::CommandBlock(c, _) => Some(c.get_position().to_centered_f64()),
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn rotation(&self) -> Option<(f32, f32)> {
|
|
match self {
|
|
Self::Console | Self::Rcon(..) | Self::Dummy => None,
|
|
Self::Player(player) => Some(player.rotation()),
|
|
Self::CommandBlock(command_block, world) => {
|
|
let pos = command_block.get_position();
|
|
let (chunk_coordinate, relative) = pos.chunk_and_chunk_relative_position();
|
|
let chunk = world.level.try_get_chunk(&chunk_coordinate)?;
|
|
let state_id = chunk.section.get_block_absolute_y(
|
|
relative.x as usize,
|
|
relative.y,
|
|
relative.z as usize,
|
|
)?;
|
|
let block = Block::from_state_id(state_id);
|
|
if !CommandBlockLikeProperties::handles_block_id(block.id) {
|
|
return None;
|
|
}
|
|
|
|
let props = CommandBlockLikeProperties::from_state_id(state_id, block);
|
|
Some((0.0, command_block_y_rot(props.facing)))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn world(&self) -> Option<Arc<World>> {
|
|
match self {
|
|
// TODO: maybe return first world when console
|
|
Self::Console | Self::Rcon(..) | Self::Dummy => None,
|
|
Self::Player(p) => Some(p.living_entity.entity.world.load_full()),
|
|
Self::CommandBlock(_, w) => Some(w.clone()),
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn get_locale(&self) -> Locale {
|
|
match self {
|
|
Self::CommandBlock(..) | Self::Console | Self::Rcon(..) | Self::Dummy => Locale::EnUs, // Default locale for console and RCON
|
|
Self::Player(player) => {
|
|
Locale::from_str(&player.config.load().locale).unwrap_or(Locale::EnUs)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn should_receive_feedback(&self) -> bool {
|
|
match self {
|
|
Self::CommandBlock(_, world) => {
|
|
world.level_info.load().game_rules.send_command_feedback
|
|
}
|
|
Self::Player(player) => {
|
|
player
|
|
.world()
|
|
.level_info
|
|
.load()
|
|
.game_rules
|
|
.send_command_feedback
|
|
}
|
|
Self::Console | Self::Rcon(_) => true,
|
|
Self::Dummy => false,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn should_broadcast_console_to_ops(&self) -> bool {
|
|
match self {
|
|
Self::CommandBlock(_, world) => world.level_info.load().game_rules.command_block_output,
|
|
// TODO: should Console and Rcon be decided by server config?
|
|
Self::Player(..) | Self::Console | Self::Rcon(_) => true,
|
|
Self::Dummy => false,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn should_track_output(&self) -> bool {
|
|
match self {
|
|
Self::Dummy => false,
|
|
Self::Player(..) | Self::Console | Self::Rcon(_) | Self::CommandBlock(..) => true,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub async fn into_source(self, server: &Arc<Server>) -> CommandSource {
|
|
match self {
|
|
Self::Rcon(rcon) => {
|
|
let (world, spawn_point) = Self::get_world_and_spawn_point(server);
|
|
CommandSource::new(
|
|
Self::Rcon(rcon),
|
|
world,
|
|
None,
|
|
spawn_point,
|
|
Vector2::new(0.0, 0.0),
|
|
"Rcon".to_owned(),
|
|
TextComponent::text("Rcon"),
|
|
server.clone(),
|
|
)
|
|
}
|
|
Self::Console => {
|
|
let (world, spawn_point) = Self::get_world_and_spawn_point(server);
|
|
CommandSource::new(
|
|
Self::Console,
|
|
world,
|
|
None,
|
|
spawn_point,
|
|
Vector2::new(0.0, 0.0),
|
|
"Server".to_owned(),
|
|
TextComponent::text("Server"),
|
|
server.clone(),
|
|
)
|
|
}
|
|
Self::Player(player) => CommandSource::new(
|
|
Self::Player(player.clone()),
|
|
player.world(),
|
|
Some(player.clone()),
|
|
player.position(),
|
|
player.rotation().into(),
|
|
player.get_display_name().await.get_text(),
|
|
player.get_display_name().await,
|
|
server.clone(),
|
|
),
|
|
Self::CommandBlock(command_entity, world) => {
|
|
let pos = command_entity.position;
|
|
|
|
let (block, state_id) = world.get_block_and_state_id(&pos).await;
|
|
let command_block_props =
|
|
CommandBlockLikeProperties::from_state_id(state_id, block);
|
|
let facing = command_block_props.facing;
|
|
|
|
let horizontal_direction = match facing {
|
|
Facing::South => 0.0,
|
|
Facing::West => 90.0,
|
|
Facing::North => 180.0,
|
|
Facing::Up | Facing::Down | Facing::East => 270.0,
|
|
};
|
|
|
|
// TODO: when command blocks get custom names, add a check for it
|
|
let name = TextComponent::text("@");
|
|
|
|
CommandSource::new(
|
|
Self::CommandBlock(command_entity, world.clone()),
|
|
world,
|
|
None,
|
|
pos.to_centered_f64(),
|
|
Vector2::new(0.0, horizontal_direction),
|
|
name.clone().get_text(),
|
|
name,
|
|
server.clone(),
|
|
)
|
|
}
|
|
Self::Dummy => {
|
|
let (world, spawn_point) = Self::get_world_and_spawn_point(server);
|
|
CommandSource::new(
|
|
Self::Dummy,
|
|
world,
|
|
None,
|
|
spawn_point,
|
|
Vector2::new(0.0, 0.0),
|
|
String::new(),
|
|
TextComponent::empty(),
|
|
server.clone(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_world_and_spawn_point(server: &Arc<Server>) -> (Arc<World>, Vector3<f64>) {
|
|
let world = server.get_world_from_dimension(&Dimension::OVERWORLD);
|
|
let spawn_point = {
|
|
let level_data = world.level_info.load();
|
|
|
|
Vector3::new(level_data.spawn_x, level_data.spawn_y, level_data.spawn_z)
|
|
};
|
|
|
|
(world, spawn_point.to_f64())
|
|
}
|
|
}
|
|
|
|
const fn command_block_y_rot(facing: Facing) -> f32 {
|
|
match facing {
|
|
Facing::North => 180.0,
|
|
Facing::South => 0.0,
|
|
Facing::West => 90.0,
|
|
Facing::East | Facing::Up | Facing::Down => 270.0,
|
|
}
|
|
}
|
|
|
|
/// Represents the result of running a command after completion.
|
|
///
|
|
/// If the command **ran successfully**, an [`Ok`] is returned containing an [`i32`].
|
|
/// This represents the 'output value' of the command, which is *homologous* to the
|
|
/// `int` that command executors in vanilla return **upon success**.
|
|
///
|
|
/// **You should choose the successful result as `1` if**:
|
|
/// - you don't know what value to use for a success for your
|
|
/// own commands, or
|
|
/// - you don't understand what this value means, or
|
|
/// - you just simply don't care about this value at all
|
|
///
|
|
/// If the command **fails**, an [`Err`] is returned, containing the [`CommandError`]
|
|
/// that led to this result.
|
|
pub type CommandResult<'a> = Pin<Box<dyn Future<Output = Result<i32, CommandError>> + Send + 'a>>;
|
|
|
|
pub trait CommandExecutor: Sync + Send {
|
|
fn execute<'a>(
|
|
&'a self,
|
|
sender: &'a CommandSender,
|
|
server: &'a Server,
|
|
args: &'a ConsumedArgs<'a>,
|
|
) -> CommandResult<'a>;
|
|
}
|