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