1use 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#[derive(Clone, Copy, Debug, PartialEq)]
15pub(crate) enum Coordinates {
16 World(WorldCoordinates),
17 Local(LocalCoordinates),
18}
19
20impl Coordinates {
21 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 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 pub(crate) fn block_pos(self, source: &CommandSource) -> BlockPos {
41 BlockPos::from(self.position(source))
42 }
43
44 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 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 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#[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#[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#[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}