Skip to main content

steel_core/command/execution/
coordinates.rs

1//! Vanilla command coordinate expressions.
2
3use std::f32::consts::PI;
4
5use glam::DVec3;
6use steel_math::trig;
7use steel_utils::{BlockPos, translations};
8use text_components::{TextComponent, translation::Translation};
9
10use super::CommandSource;
11use crate::command::brigadier::{
12    CommandSyntaxError, CommandSyntaxErrorKind, ReaderCursor, StringReader, SuggestionsBuilder,
13};
14
15/// A position or rotation expressed in world-relative or source-local coordinates.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub(crate) enum Coordinates {
18    World(WorldCoordinates),
19    Local(LocalCoordinates),
20}
21
22impl Coordinates {
23    /// Resolves this expression against the current command source.
24    pub(crate) fn position(self, source: &CommandSource) -> DVec3 {
25        match self {
26            Self::World(coordinates) => coordinates.position(source.position()),
27            Self::Local(coordinates) => {
28                coordinates.position(source.anchor_position(), source.rotation())
29            }
30        }
31    }
32
33    /// Resolves this expression as a `(yaw, pitch)` rotation.
34    pub(crate) fn rotation(self, source: &CommandSource) -> (f32, f32) {
35        match self {
36            Self::World(coordinates) => coordinates.rotation(source.rotation()),
37            Self::Local(_) => (0.0, 0.0),
38        }
39    }
40
41    /// Resolves this expression to the containing block position.
42    pub(crate) fn block_pos(self, source: &CommandSource) -> BlockPos {
43        BlockPos::from(self.position(source))
44    }
45
46    /// Returns whether the X component used relative or local syntax.
47    pub(crate) const fn is_x_relative(self) -> bool {
48        match self {
49            Self::World(coordinates) => coordinates.x.is_relative(),
50            Self::Local(_) => true,
51        }
52    }
53
54    /// Returns whether the Y component used relative or local syntax.
55    pub(crate) const fn is_y_relative(self) -> bool {
56        match self {
57            Self::World(coordinates) => coordinates.y.is_relative(),
58            Self::Local(_) => true,
59        }
60    }
61
62    /// Returns whether the Z component used relative or local syntax.
63    pub(crate) const fn is_z_relative(self) -> bool {
64        match self {
65            Self::World(coordinates) => coordinates.z.is_relative(),
66            Self::Local(_) => true,
67        }
68    }
69}
70
71/// One absolute or source-relative world coordinate.
72#[derive(Clone, Copy, Debug, PartialEq)]
73pub(super) struct WorldCoordinate {
74    relative: bool,
75    value: f64,
76}
77
78impl WorldCoordinate {
79    pub(super) const fn new(relative: bool, value: f64) -> Self {
80        Self { relative, value }
81    }
82
83    fn resolve(self, origin: f64) -> f64 {
84        if self.relative {
85            self.value + origin
86        } else {
87            self.value
88        }
89    }
90
91    const fn is_relative(self) -> bool {
92        self.relative
93    }
94}
95
96/// Three world-coordinate components retained until command execution.
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub(crate) struct WorldCoordinates {
99    x: WorldCoordinate,
100    y: WorldCoordinate,
101    z: WorldCoordinate,
102}
103
104impl WorldCoordinates {
105    pub(super) const fn new(x: WorldCoordinate, y: WorldCoordinate, z: WorldCoordinate) -> Self {
106        Self { x, y, z }
107    }
108
109    fn position(self, origin: DVec3) -> DVec3 {
110        DVec3::new(
111            self.x.resolve(origin.x),
112            self.y.resolve(origin.y),
113            self.z.resolve(origin.z),
114        )
115    }
116
117    fn rotation(self, (origin_yaw, origin_pitch): (f32, f32)) -> (f32, f32) {
118        (
119            self.y.resolve(f64::from(origin_yaw)) as f32,
120            self.x.resolve(f64::from(origin_pitch)) as f32,
121        )
122    }
123}
124
125/// Three source-local components retained until command execution.
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub(crate) struct LocalCoordinates {
128    left: f64,
129    up: f64,
130    forwards: f64,
131}
132
133impl LocalCoordinates {
134    pub(super) const fn new(left: f64, up: f64, forwards: f64) -> Self {
135        Self { left, up, forwards }
136    }
137
138    fn position(self, anchor: DVec3, (yaw, pitch): (f32, f32)) -> DVec3 {
139        let radians_per_degree = PI / 180.0;
140        let y_rotation = (yaw + 90.0) * radians_per_degree;
141        let x_rotation = -pitch * radians_per_degree;
142        let x_up_rotation = (-pitch + 90.0) * radians_per_degree;
143        let y_cos = f64::from(trig::cos(f64::from(y_rotation)));
144        let y_sin = f64::from(trig::sin(f64::from(y_rotation)));
145        let x_cos = f64::from(trig::cos(f64::from(x_rotation)));
146        let x_sin = f64::from(trig::sin(f64::from(x_rotation)));
147        let x_cos_up = f64::from(trig::cos(f64::from(x_up_rotation)));
148        let x_sin_up = f64::from(trig::sin(f64::from(x_up_rotation)));
149        let forwards_axis = DVec3::new(y_cos * x_cos, x_sin, y_sin * x_cos);
150        let up_axis = DVec3::new(y_cos * x_cos_up, x_sin_up, y_sin * x_cos_up);
151        let left_axis = -forwards_axis.cross(up_axis);
152
153        let offset = forwards_axis * self.forwards + up_axis * self.up + left_axis * self.left;
154        offset + anchor
155    }
156}
157
158pub(super) fn parse_block_pos(
159    reader: &mut StringReader<'_>,
160) -> Result<Coordinates, CommandSyntaxError> {
161    if reader.peek() == Some('^') {
162        parse_local_coordinates(reader)
163    } else {
164        parse_world_coordinates_int(reader)
165    }
166}
167
168pub(super) fn parse_vec3(
169    reader: &mut StringReader<'_>,
170    center_integers: bool,
171) -> Result<Coordinates, CommandSyntaxError> {
172    if reader.peek() == Some('^') {
173        parse_local_coordinates(reader)
174    } else {
175        parse_world_coordinates_double(reader, center_integers)
176    }
177}
178
179fn parse_world_coordinates_int(
180    reader: &mut StringReader<'_>,
181) -> Result<Coordinates, CommandSyntaxError> {
182    let start = reader.checkpoint();
183    let x = parse_world_coordinate_int(reader)?;
184    if reader.peek() != Some(' ') {
185        reader.restore(start);
186        return Err(translated_error(
187            reader,
188            &translations::ARGUMENT_POS3D_INCOMPLETE,
189        ));
190    }
191    reader.skip();
192    let y = parse_world_coordinate_int(reader)?;
193    if reader.peek() != Some(' ') {
194        reader.restore(start);
195        return Err(translated_error(
196            reader,
197            &translations::ARGUMENT_POS3D_INCOMPLETE,
198        ));
199    }
200    reader.skip();
201    let z = parse_world_coordinate_int(reader)?;
202    Ok(Coordinates::World(WorldCoordinates::new(x, y, z)))
203}
204
205fn parse_world_coordinates_double(
206    reader: &mut StringReader<'_>,
207    center_integers: bool,
208) -> Result<Coordinates, CommandSyntaxError> {
209    let start = reader.checkpoint();
210    let x = parse_world_coordinate_double(reader, center_integers)?;
211    if reader.peek() != Some(' ') {
212        reader.restore(start);
213        return Err(translated_error(
214            reader,
215            &translations::ARGUMENT_POS3D_INCOMPLETE,
216        ));
217    }
218    reader.skip();
219    let y = parse_world_coordinate_double(reader, false)?;
220    if reader.peek() != Some(' ') {
221        reader.restore(start);
222        return Err(translated_error(
223            reader,
224            &translations::ARGUMENT_POS3D_INCOMPLETE,
225        ));
226    }
227    reader.skip();
228    let z = parse_world_coordinate_double(reader, center_integers)?;
229    Ok(Coordinates::World(WorldCoordinates::new(x, y, z)))
230}
231
232fn parse_local_coordinates(
233    reader: &mut StringReader<'_>,
234) -> Result<Coordinates, CommandSyntaxError> {
235    let start = reader.checkpoint();
236    let left = parse_local_coordinate_or_restore(reader, start)?;
237    if reader.peek() != Some(' ') {
238        reader.restore(start);
239        return Err(translated_error(
240            reader,
241            &translations::ARGUMENT_POS3D_INCOMPLETE,
242        ));
243    }
244    reader.skip();
245    let up = parse_local_coordinate_or_restore(reader, start)?;
246    if reader.peek() != Some(' ') {
247        reader.restore(start);
248        return Err(translated_error(
249            reader,
250            &translations::ARGUMENT_POS3D_INCOMPLETE,
251        ));
252    }
253    reader.skip();
254    let forwards = parse_local_coordinate_or_restore(reader, start)?;
255    Ok(Coordinates::Local(LocalCoordinates::new(
256        left, up, forwards,
257    )))
258}
259
260fn parse_local_coordinate_or_restore(
261    reader: &mut StringReader<'_>,
262    argument_start: ReaderCursor,
263) -> Result<f64, CommandSyntaxError> {
264    match parse_local_coordinate(reader) {
265        Ok(value) => Ok(value),
266        Err(LocalCoordinateError::MissingDouble) => Err(translated_error(
267            reader,
268            &translations::ARGUMENT_POS_MISSING_DOUBLE,
269        )),
270        Err(LocalCoordinateError::Mixed) => {
271            reader.restore(argument_start);
272            Err(translated_error(reader, &translations::ARGUMENT_POS_MIXED))
273        }
274        Err(LocalCoordinateError::Syntax(error)) => Err(error),
275    }
276}
277
278enum LocalCoordinateError {
279    MissingDouble,
280    Mixed,
281    Syntax(CommandSyntaxError),
282}
283
284fn parse_local_coordinate(reader: &mut StringReader<'_>) -> Result<f64, LocalCoordinateError> {
285    if !reader.can_read() {
286        return Err(LocalCoordinateError::MissingDouble);
287    }
288    if reader.peek() != Some('^') {
289        return Err(LocalCoordinateError::Mixed);
290    }
291    reader.skip();
292    if !reader.can_read() || reader.peek() == Some(' ') {
293        return Ok(0.0);
294    }
295    reader.read_double().map_err(LocalCoordinateError::Syntax)
296}
297
298fn parse_world_coordinate_int(
299    reader: &mut StringReader<'_>,
300) -> Result<WorldCoordinate, CommandSyntaxError> {
301    if reader.peek() == Some('^') {
302        return Err(translated_error(reader, &translations::ARGUMENT_POS_MIXED));
303    }
304    if !reader.can_read() {
305        return Err(translated_error(
306            reader,
307            &translations::ARGUMENT_POS_MISSING_INT,
308        ));
309    }
310    let relative = read_relative_prefix(reader);
311    let value = if reader.can_read() && reader.peek() != Some(' ') {
312        if relative {
313            reader.read_double()?
314        } else {
315            f64::from(reader.read_int()?)
316        }
317    } else {
318        0.0
319    };
320    Ok(WorldCoordinate::new(relative, value))
321}
322
323fn parse_world_coordinate_double(
324    reader: &mut StringReader<'_>,
325    center_integer: bool,
326) -> Result<WorldCoordinate, CommandSyntaxError> {
327    if reader.peek() == Some('^') {
328        return Err(translated_error(reader, &translations::ARGUMENT_POS_MIXED));
329    }
330    if !reader.can_read() {
331        return Err(translated_error(
332            reader,
333            &translations::ARGUMENT_POS_MISSING_DOUBLE,
334        ));
335    }
336    let relative = read_relative_prefix(reader);
337    let number_start = reader.read_so_far().len();
338    let mut value = if reader.can_read() && reader.peek() != Some(' ') {
339        reader.read_double()?
340    } else {
341        0.0
342    };
343    let number = &reader.read_so_far()[number_start..];
344    if !relative && center_integer && !number.contains('.') {
345        value += 0.5;
346    }
347    Ok(WorldCoordinate::new(relative, value))
348}
349
350pub(super) fn parse_rotation(
351    reader: &mut StringReader<'_>,
352) -> Result<Coordinates, CommandSyntaxError> {
353    let start = reader.checkpoint();
354    if !reader.can_read() {
355        return Err(translated_error(
356            reader,
357            &translations::ARGUMENT_ROTATION_INCOMPLETE,
358        ));
359    }
360    let yaw = parse_world_coordinate_double(reader, false)?;
361    if reader.peek() != Some(' ') {
362        reader.restore(start);
363        return Err(translated_error(
364            reader,
365            &translations::ARGUMENT_ROTATION_INCOMPLETE,
366        ));
367    }
368    reader.skip();
369    let pitch = parse_world_coordinate_double(reader, false)?;
370    Ok(Coordinates::World(WorldCoordinates::new(
371        pitch,
372        yaw,
373        WorldCoordinate::new(true, 0.0),
374    )))
375}
376
377fn read_relative_prefix(reader: &mut StringReader<'_>) -> bool {
378    if reader.peek() != Some('~') {
379        return false;
380    }
381    reader.skip();
382    true
383}
384
385fn translated_error(reader: &StringReader<'_>, translation: &Translation<0>) -> CommandSyntaxError {
386    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
387        TextComponent::from(translation),
388    )))
389}
390
391pub(super) fn suggest_coordinates(
392    builder: &mut SuggestionsBuilder<'_>,
393    parser: impl Fn(&mut StringReader<'_>) -> Result<Coordinates, CommandSyntaxError>,
394) {
395    let input = builder.remaining();
396    let coordinate = if input.starts_with('^') { "^" } else { "~" };
397    if input.is_empty() {
398        let two_coordinates = format!("{coordinate} {coordinate}");
399        let full = format!("{two_coordinates} {coordinate}");
400        if valid_coordinates(&full, &parser) {
401            builder.suggest(coordinate);
402            builder.suggest(two_coordinates);
403            builder.suggest(full);
404        }
405        return;
406    }
407
408    let mut fields = input.split(' ').collect::<Vec<_>>();
409    while fields.last() == Some(&"") {
410        fields.pop();
411    }
412    match fields.as_slice() {
413        [x] => {
414            let two_coordinates = format!("{x} {coordinate}");
415            let full = format!("{two_coordinates} {coordinate}");
416            if valid_coordinates(&full, &parser) {
417                builder.suggest(two_coordinates);
418                builder.suggest(full);
419            }
420        }
421        [x, y] => {
422            let full = format!("{x} {y} {coordinate}");
423            if valid_coordinates(&full, &parser) {
424                builder.suggest(full);
425            }
426        }
427        _ => {}
428    }
429}
430
431fn valid_coordinates(
432    input: &str,
433    parser: &impl Fn(&mut StringReader<'_>) -> Result<Coordinates, CommandSyntaxError>,
434) -> bool {
435    parser(&mut StringReader::new(input)).is_ok()
436}
437
438#[cfg(test)]
439mod tests {
440    use glam::DVec3;
441
442    use super::{LocalCoordinates, WorldCoordinate, WorldCoordinates};
443
444    #[test]
445    fn world_coordinates_resolve_relative_components_at_execution() {
446        let coordinates = WorldCoordinates::new(
447            WorldCoordinate::new(true, 2.5),
448            WorldCoordinate::new(false, 64.0),
449            WorldCoordinate::new(true, -3.0),
450        );
451
452        assert_eq!(
453            coordinates.position(DVec3::new(10.0, 20.0, 30.0)),
454            DVec3::new(12.5, 64.0, 27.0)
455        );
456    }
457
458    #[test]
459    fn rotation_coordinates_keep_vanilla_pitch_yaw_component_order() {
460        let coordinates = WorldCoordinates::new(
461            WorldCoordinate::new(true, 5.0),
462            WorldCoordinate::new(false, 90.0),
463            WorldCoordinate::new(true, 0.0),
464        );
465
466        assert_eq!(coordinates.rotation((45.0, 10.0)), (90.0, 15.0));
467    }
468
469    #[test]
470    fn local_coordinates_follow_source_rotation() {
471        let coordinates = LocalCoordinates::new(0.0, 0.0, 1.0);
472        let position = coordinates.position(DVec3::ZERO, (0.0, 0.0));
473
474        assert!(position.x.abs() < f64::from(f32::EPSILON));
475        assert!(position.y.abs() < f64::from(f32::EPSILON));
476        assert!((position.z - 1.0).abs() < f64::from(f32::EPSILON));
477    }
478
479    #[test]
480    fn coordinates_retain_axis_relative_metadata() {
481        let coordinates = super::Coordinates::World(WorldCoordinates::new(
482            WorldCoordinate::new(true, 0.0),
483            WorldCoordinate::new(false, 64.0),
484            WorldCoordinate::new(true, 2.0),
485        ));
486        assert!(coordinates.is_x_relative());
487        assert!(!coordinates.is_y_relative());
488        assert!(coordinates.is_z_relative());
489
490        let local = super::Coordinates::Local(LocalCoordinates::new(0.0, 0.0, 0.0));
491        assert!(local.is_x_relative());
492        assert!(local.is_y_relative());
493        assert!(local.is_z_relative());
494    }
495}