feat(command): implement Vec3 argument type (#1943)

* added `Vector3` dot and cross methods and added both coordinate types

* added parsing for coordinates and created `vec3` file

* finished `vec3` argument type and implemented parsing for local coordinates
This commit is contained in:
SomeYellowGuy
2026-04-04 00:17:38 +05:30
committed by GitHub
parent c7cbf37cc2
commit 73a5288aea
4 changed files with 537 additions and 1 deletions

View File

@@ -90,7 +90,7 @@ impl<T: Copy> Vector3<T> {
}
}
impl<T: Math + PartialOrd + Copy> Vector3<T> {
impl<T> Vector3<T> {
/// Creates a new `Vector3` with the given components.
///
/// # Arguments
@@ -104,7 +104,9 @@ impl<T: Math + PartialOrd + Copy> Vector3<T> {
pub const fn new(x: T, y: T, z: T) -> Self {
Self { x, y, z }
}
}
impl<T: Math + PartialOrd + Copy> Vector3<T> {
/// Calculates the squared length (magnitude) of the vector.
///
/// # Returns
@@ -343,6 +345,40 @@ impl<T: Math + PartialOrd + Copy> Vector3<T> {
&& self.z >= min_z
&& self.z <= max_z
}
/// Computes the dot product of this vector with the given coordinates.
///
/// # Arguments
/// - `x` The X coordinate to dot with.
/// - `y` The Y coordinate to dot with.
/// - `z` The Z coordinate to dot with.
///
/// # Returns
/// The dot product `self.x * x + self.y * y + self.z * z`.
#[inline]
#[must_use]
pub fn dot(&self, other: &Self) -> T {
self.x * other.x + self.y * other.y + self.z * other.z
}
/// Computes the cross product of this vector with the given coordinates.
///
/// # Arguments
/// - `x` The X coordinate to cross with.
/// - `y` The Y coordinate to cross with.
/// - `z` The Z coordinate to cross with.
///
/// # Returns
/// The cross product of both vectors.
#[inline]
#[must_use]
pub fn cross(&self, other: &Self) -> Self {
Self::new(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
)
}
}
impl<T: Math + Copy + Float> Vector3<T> {

View File

@@ -0,0 +1,337 @@
use crate::command::context::command_source::CommandSource;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::{
CommandErrorType, READER_EXPECTED_DOUBLE, READER_EXPECTED_INT,
};
use crate::command::string_reader::StringReader;
use pumpkin_data::translation;
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::math::vector3::{Axis, Vector3};
pub mod vec3;
pub const MIXED_TYPE_ERROR_TYPE: CommandErrorType<0> =
CommandErrorType::new(translation::ARGUMENT_POS_MIXED);
/// Represents a single world coordinate.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WorldCoordinate {
Absolute(f64),
Relative(f64),
}
impl WorldCoordinate {
/// Creates a new `WorldCoordinate`.
#[must_use]
pub const fn new(is_relative: bool, value: f64) -> Self {
if is_relative {
Self::Relative(value)
} else {
Self::Absolute(value)
}
}
/// Returns whether this coordinate is relative.
#[must_use]
pub const fn is_relative(&self) -> bool {
matches!(self, Self::Relative(_))
}
/// Returns the physical coordinate value this [`WorldCoordinate`] represents, given
/// an absolute coordinate origin.
#[must_use]
pub const fn resolve(&self, origin: f64) -> f64 {
match self {
Self::Absolute(absolute) => *absolute,
Self::Relative(relative) => origin + *relative,
}
}
/// Checks if a `StringReader` is about to describle a relative coordinate.
///
/// # Arguments
/// * `reader` - The `StringReader` to check.
///
/// # Returns
/// - `true` if a `~` (tilde) can be found. It is also skipped by this method.
/// - `false` if no `~` can be found.
pub fn consume_relative_start(reader: &mut StringReader) -> bool {
if reader.peek() == Some('~') {
reader.skip();
true
} else {
false
}
}
/// Tries to parse a [`WorldCoordinate`] from a single number.
///
/// # Arguments
/// * `reader` - The `StringReader` to parse the coordinate from.
/// * `center_integers` - Whether to correct integral coordinates by adding `+0.5` to them
/// (as mentioned by [`Vec3ArgumentType::Default`]).
///
/// # Returns
/// - The `WorldCoordinate` if it was correctly parsed, wrapped in an `Ok`.
/// - A [`CommandSyntaxError`] describing an error if it could not be correctly parsed,
/// wrapped in an `Err`.
///
/// [`Vec3ArgumentType::Default`]: Vec3ArgumentType::Default
pub fn parse(
reader: &mut StringReader,
center_integers: bool,
) -> Result<Self, CommandSyntaxError> {
if reader.peek() == Some('^') {
Err(MIXED_TYPE_ERROR_TYPE.create(reader))
} else if !reader.can_read_char() {
Err(READER_EXPECTED_DOUBLE.create(reader))
} else {
let is_relative = Self::consume_relative_start(reader);
let i = reader.cursor();
let mut value = if reader.can_read_char() && reader.peek() != Some(' ') {
reader.read_double()?
} else {
0.0
};
let slice = &reader.string()[i..reader.cursor()];
if is_relative && slice.is_empty() {
Ok(Self::Relative(0.0))
} else {
if !slice.contains('.') && !is_relative && center_integers {
value += 0.5;
}
Ok(Self::new(is_relative, value))
}
}
}
/// Tries to parse a [`WorldCoordinate`] from a single number, expecting an integral non-relative coordinate
/// or any relative coordinate.
///
/// # Arguments
/// * `reader` - The `StringReader` to parse the coordinate from.
///
/// # Returns
/// - The `WorldCoordinate` if it was correctly parsed, wrapped in an `Ok`.
/// - A [`CommandSyntaxError`] describing an error if it could not be correctly parsed,
/// wrapped in an `Err`.
pub fn parse_integer(reader: &mut StringReader) -> Result<Self, CommandSyntaxError> {
if reader.peek() == Some('^') {
Err(MIXED_TYPE_ERROR_TYPE.create(reader))
} else if !reader.can_read_char() {
Err(READER_EXPECTED_INT.create(reader))
} else {
let is_relative = Self::consume_relative_start(reader);
let value = if reader.can_read_char() && reader.peek() != Some(' ') {
if is_relative {
reader.read_double()?
} else {
reader.read_int()? as f64
}
} else {
0.0
};
Ok(Self::new(is_relative, value))
}
}
}
/// An object representing some command coordinates.
///
/// A set of [`Coordinates`] can be *resolved* via the [`Coordinates::resolve`] method.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Coordinates {
/// Normal coordinates (each coordinate can be *absolute* or *relative*.)
World(Vector3<WorldCoordinate>),
/// Local coordinates (can be different depending on the command source.)
Local { left: f64, up: f64, forward: f64 },
}
macro_rules! check_for_space_char {
($reader:ident, $i:ident, $value:ident) => {
if $reader.peek() == Some(' ') {
$reader.skip();
Ok($value)
} else {
$reader.set_cursor($i);
Err(vec3::INCOMPLETE_ERROR_TYPE.create($reader))
}
};
}
impl Coordinates {
/// Returns whether a coordinate (of the given [`Axis`]) of these [`Coordinates`] is relative.
///
/// This also returns `true` for a *local coordinate*.
#[must_use]
pub const fn is_relative(&self, axis: Axis) -> bool {
match self {
Self::World(vector) => vector.get_axis(axis).is_relative(),
Self::Local { .. } => true,
}
}
/// Returns the physical position that these [`Coordinates`] represent.
#[must_use]
pub fn resolve(&self, source: &CommandSource) -> Vector3<f64> {
match self {
Self::World(vector) => {
let pos = source.position;
Vector3::new(
vector.x.resolve(pos.x),
vector.y.resolve(pos.y),
vector.z.resolve(pos.z),
)
}
Self::Local { left, up, forward } => {
convert_local_coordinates(*left, *up, *forward, source.rotation)
}
}
}
/// Tries to parse a set of world [`Coordinates`], expecting coordinates, each either being
/// an integral non-relative coordinate or any relative coordinate.
///
/// # Arguments
/// * `reader` - The `StringReader` to parse the coordinates from.
/// * `center_integers` - Whether to correct integral coordinates by adding `+0.5` to them
/// (as mentioned by [`Vec3ArgumentType::Default`]).
///
/// # Returns
/// - The world `Coordinates` if they were correctly parsed, wrapped in an `Ok`.
/// - A [`CommandSyntaxError`] describing an error if they could not be correctly parsed,
/// wrapped in an `Err`.
///
/// [`Vec3ArgumentType::Default`]: Vec3ArgumentType::Default
pub fn parse_world(
reader: &mut StringReader,
center_integers: bool,
) -> Result<Self, CommandSyntaxError> {
let i = reader.cursor();
let coordinate_1 = Self::parse_world_single(i, reader, center_integers)?;
// The Y coordinate is never centered.
let coordinate_2 = Self::parse_world_single(i, reader, false)?;
let coordinate_3 = WorldCoordinate::parse(reader, center_integers)?;
Ok(Self::World(Vector3::new(
coordinate_1,
coordinate_2,
coordinate_3,
)))
}
fn parse_world_single(
i: usize,
reader: &mut StringReader,
center_integers: bool,
) -> Result<WorldCoordinate, CommandSyntaxError> {
let coordinate = WorldCoordinate::parse(reader, center_integers)?;
check_for_space_char!(reader, i, coordinate)
}
/// Tries to parse a set of world [`Coordinates`].
///
/// # Arguments
/// * `reader` - The `StringReader` to parse the coordinate from.
///
/// # Returns
/// - The world `Coordinates` if they were correctly parsed, wrapped in an `Ok`.
/// - A [`CommandSyntaxError`] describing an error if they could not be correctly parsed,
/// wrapped in an `Err`.
pub fn parse_world_integers(reader: &mut StringReader) -> Result<Self, CommandSyntaxError> {
let i = reader.cursor();
let coordinate_1 = Self::parse_world_single_integer(i, reader)?;
let coordinate_2 = Self::parse_world_single_integer(i, reader)?;
let coordinate_3 = WorldCoordinate::parse_integer(reader)?;
Ok(Self::World(Vector3::new(
coordinate_1,
coordinate_2,
coordinate_3,
)))
}
fn parse_world_single_integer(
i: usize,
reader: &mut StringReader,
) -> Result<WorldCoordinate, CommandSyntaxError> {
let coordinate = WorldCoordinate::parse_integer(reader)?;
check_for_space_char!(reader, i, coordinate)
}
/// Tries to parse a set of local [`Coordinates`].
///
/// # Arguments
/// * `reader` - The `StringReader` to parse the coordinate from.
///
/// # Returns
/// - The local `Coordinates` if they were correctly parsed, wrapped in an `Ok`.
/// - A [`CommandSyntaxError`] describing an error if they could not be correctly parsed,
/// wrapped in an `Err`.
pub fn parse_local(reader: &mut StringReader) -> Result<Self, CommandSyntaxError> {
let i = reader.cursor();
let left = Self::parse_local_single(i, reader)?;
let up = Self::parse_local_single(i, reader)?;
let forward = Self::parse_local_number(i, reader)?;
Ok(Self::Local { left, up, forward })
}
fn parse_local_single(i: usize, reader: &mut StringReader) -> Result<f64, CommandSyntaxError> {
let number = Self::parse_local_number(i, reader)?;
check_for_space_char!(reader, i, number)
}
fn parse_local_number(i: usize, reader: &mut StringReader) -> Result<f64, CommandSyntaxError> {
if !reader.can_read_char() {
Err(READER_EXPECTED_DOUBLE.create(reader))
} else if reader.peek() != Some('^') {
reader.set_cursor(i);
Err(MIXED_TYPE_ERROR_TYPE.create(reader))
} else {
reader.skip();
let number = if reader.can_read_char() && reader.peek() != Some(' ') {
reader.read_double()?
} else {
0.0
};
Ok(number)
}
}
}
/// Converts a set of local coordinates to their physical [`Vector3`] form.
///
/// # Arguments
/// * `left` - The left component of the coordinates.
/// * `up` - The up component of the coordinates.
/// * `forward` - The forward component of the coordinates.
/// * `rotation` - The rotation to use to calculate the physical coordinates.
/// Both coordinates must be in *degrees*.
///
/// # Returns
/// The physical position represented by the local coordinates.
#[must_use]
fn convert_local_coordinates(
left: f64,
up: f64,
forward: f64,
rotation: Vector2<f32>,
) -> Vector3<f64> {
let y = (rotation.y + 90.0).to_radians() as f64;
let y_cos = y.cos();
let y_sin = y.sin();
let x = (-rotation.x).to_radians() as f64;
let x_cos = x.cos();
let x_sin = x.sin();
let x_up = (-rotation.x + 90.0).to_radians() as f64;
let x_up_cos = x_up.cos();
let x_up_sin = x_up.sin();
let forward_vector = Vector3::new(y_cos * x_cos, x_sin, y_sin * x_cos);
let up_vector = Vector3::new(y_cos * x_up_cos, x_up_sin, y_sin * x_up_cos);
let left_vector = forward_vector.cross(&up_vector) * -1.0;
Vector3::new(
forward_vector.x * forward + up_vector.x * up + left_vector.x * left,
forward_vector.y * forward + up_vector.y * up + left_vector.y * left,
forward_vector.z * forward + up_vector.z * up + left_vector.z * left,
)
}

View File

@@ -0,0 +1,162 @@
use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType};
use crate::command::argument_types::coordinates::Coordinates;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::string_reader::StringReader;
use pumpkin_data::translation;
pub const INCOMPLETE_ERROR_TYPE: CommandErrorType<0> =
CommandErrorType::new(translation::ARGUMENT_POS3D_INCOMPLETE);
/// An argument type for a 3-dimensional vector.
pub enum Vec3ArgumentType {
/// The default `Vec3ArgumentType` variant.
///
/// To represent some position in the world,
/// you'll almost always want to use this.
///
/// For each coordinate, if it does not use the decimal (`.`) sign
/// (the coordinate is integral) and it is not relative,
/// a `+0.5` offset is added to it.
///
Default,
/// No center correction occurs for this `Vec3ArgumentType` variant.
Uncorrected,
}
impl Vec3ArgumentType {
/// Returns whether this argument type centers integers.\
#[must_use]
pub const fn centers_integers(&self) -> bool {
matches!(self, Self::Default)
}
}
impl ArgumentType for Vec3ArgumentType {
type Item = Coordinates;
fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError> {
if reader.peek() == Some('^') {
Coordinates::parse_local(reader)
} else {
Coordinates::parse_world(reader, self.centers_integers())
}
}
fn client_side_parser(&'_ self) -> JavaClientArgumentType<'_> {
JavaClientArgumentType::Vec3
}
fn examples(&self) -> Vec<String> {
examples!("1 1 1", "3 ~34 ~-2", "40 50 60", "^ ^4 ^3")
}
}
#[cfg(test)]
mod test {
use crate::command::argument_types::argument_type::ArgumentType;
use crate::command::argument_types::coordinates::vec3::{
INCOMPLETE_ERROR_TYPE, Vec3ArgumentType,
};
use crate::command::argument_types::coordinates::{
Coordinates, MIXED_TYPE_ERROR_TYPE, WorldCoordinate,
};
use crate::command::string_reader::StringReader;
use pumpkin_util::math::vector3::Vector3;
macro_rules! world_coordinate {
($( ($variant:ident, $value:expr) ),+) => {
Coordinates::World(Vector3::new(
$( WorldCoordinate::$variant($value), )+
))
};
}
#[test]
fn parse_test() {
let mut reader = StringReader::new("0 0 0");
// The default type centers both the X and Z coordinates.
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Default,
world_coordinate!((Absolute, 0.5), (Absolute, 0.0), (Absolute, 0.5))
);
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Uncorrected,
world_coordinate!((Absolute, 0.0), (Absolute, 0.0), (Absolute, 0.0))
);
let mut reader = StringReader::new("~ ~4 8");
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Default,
world_coordinate!(
(Relative, 0.0),
(Relative, 4.0),
// Only the Z coordinate is centered.
(Absolute, 8.5)
)
);
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Uncorrected,
world_coordinate!((Relative, 0.0), (Relative, 4.0), (Absolute, 8.0))
);
let mut reader = StringReader::new("~-1 ~-2.5 ~-3");
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Default,
world_coordinate!((Relative, -1.0), (Relative, -2.5), (Relative, -3.0))
);
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Uncorrected,
world_coordinate!((Relative, -1.0), (Relative, -2.5), (Relative, -3.0))
);
let mut reader = StringReader::new("-3 4 5");
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Default,
world_coordinate!((Absolute, -2.5), (Absolute, 4.0), (Absolute, 5.5))
);
let mut reader = StringReader::new("1000 2000");
assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &INCOMPLETE_ERROR_TYPE);
}
#[test]
fn parse_local_coordinates() {
let mut reader = StringReader::new("^1 ^10 ^30");
assert_parse_ok_reset!(
reader,
Vec3ArgumentType::Default,
Coordinates::Local {
left: 1.0,
up: 10.0,
forward: 30.0
}
);
// We can't mix world & local coordinates.
let mut reader = StringReader::new("^1 ^10 ~20");
assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &MIXED_TYPE_ERROR_TYPE);
let mut reader = StringReader::new("^-3 ^-7 10");
assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &MIXED_TYPE_ERROR_TYPE);
let mut reader = StringReader::new("^1 ^2");
assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &INCOMPLETE_ERROR_TYPE);
}
}

View File

@@ -174,6 +174,7 @@ impl FromStringReader for FloatDegreeBounds {
}
pub mod argument_type;
pub mod coordinates;
pub mod core;
pub mod entity;
pub mod entity_selector;