Skip to main content

steel_core/command/execution/
coordinates.rs

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