Skip to main content

steel_core/command/execution/selector/
mod.rs

1//! Vanilla-style entity selector parsing and resolution.
2//!
3//! Vanilla's server-wide candidate scope maps to one Steel domain. Selectors
4//! never expose players, entities, teams, or scores from another domain.
5
6use std::sync::Arc;
7
8use glam::DVec3;
9use rand::seq::SliceRandom;
10use simdnbt::owned::NbtCompound;
11use steel_registry::{
12    REGISTRY, RegistryExt as _, TaggedRegistryExt as _, entity_type::EntityTypeRef,
13    vanilla_entities,
14};
15use steel_utils::{
16    Identifier,
17    geometry::WorldAabb,
18    java,
19    nbt::{compare_nbt_compounds, parse_snbt_compound_argument},
20    translations,
21    types::GameType,
22};
23use text_components::TextComponent;
24use uuid::Uuid;
25
26use crate::{
27    command::brigadier::{
28        CommandSyntaxError, CommandSyntaxErrorKind, ReaderCursor, StringReader, SuggestionsBuilder,
29    },
30    entity::{Entity, SharedEntity},
31    player::Player,
32    scoreboard::{ScoreHolder, Scoreboard},
33    world::World,
34};
35
36use super::{CommandArgumentSource, CommandSource};
37
38const SORT_NEAREST: &str = "nearest";
39const SORT_FURTHEST: &str = "furthest";
40const SORT_RANDOM: &str = "random";
41const SORT_ARBITRARY: &str = "arbitrary";
42const SELECTOR_OPTION_KEYS: &[&str] = &[
43    "name",
44    "distance",
45    "level",
46    "x",
47    "y",
48    "z",
49    "dx",
50    "dy",
51    "dz",
52    "x_rotation",
53    "y_rotation",
54    "limit",
55    "sort",
56    "gamemode",
57    "type",
58    "tag",
59    "team",
60    "nbt",
61    "scores",
62    "advancements",
63    "predicate",
64];
65const UNSUPPORTED_SELECTOR_OPTION_KEYS: &[&str] = &[
66    // Needs player advancement progress before it can resolve faithfully.
67    "advancements",
68    // Needs a real reloadable/plugin predicate registry.
69    "predicate",
70];
71const SET_ONCE_SELECTOR_OPTIONS: &[&str] = &[
72    "distance",
73    "level",
74    "x",
75    "y",
76    "z",
77    "dx",
78    "dy",
79    "dz",
80    "x_rotation",
81    "y_rotation",
82    "limit",
83    "sort",
84    "scores",
85    "advancements",
86];
87const GAME_MODE_SUGGESTIONS: &[&str] = &["survival", "creative", "adventure", "spectator"];
88
89#[derive(Clone, Debug, PartialEq)]
90pub(crate) struct EntitySelector {
91    kind: SelectorKind,
92    max_results: usize,
93    includes_entities: bool,
94    current_entity: bool,
95    world_limited: bool,
96    order: SelectorOrder,
97    position: SelectorPosition,
98    delta: SelectorDelta,
99    distance: Option<DoubleRange>,
100    level: Option<IntRange>,
101    x_rotation: Option<FloatRange>,
102    y_rotation: Option<FloatRange>,
103    filters: Vec<SelectorFilter>,
104    uses_advanced_options: bool,
105}
106
107#[derive(Clone, Debug, PartialEq)]
108enum SelectorKind {
109    Selector(SelectorType),
110    PlayerName(String),
111    EntityUuid(Uuid),
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115enum SelectorType {
116    AllPlayers,
117    AllEntities,
118    NearestEntity,
119    NearestPlayer,
120    RandomPlayer,
121    SelfEntity,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125enum SelectorOrder {
126    Nearest,
127    Furthest,
128    Random,
129    Arbitrary,
130}
131
132#[derive(Clone, Debug, Default, PartialEq)]
133struct SelectorPosition {
134    x: Option<f64>,
135    y: Option<f64>,
136    z: Option<f64>,
137}
138
139impl SelectorPosition {
140    fn apply(&self, base: DVec3) -> DVec3 {
141        DVec3::new(
142            self.x.unwrap_or(base.x),
143            self.y.unwrap_or(base.y),
144            self.z.unwrap_or(base.z),
145        )
146    }
147}
148
149#[derive(Clone, Copy, Debug, Default, PartialEq)]
150struct SelectorDelta {
151    x: Option<f64>,
152    y: Option<f64>,
153    z: Option<f64>,
154}
155
156impl SelectorDelta {
157    const fn has_any(self) -> bool {
158        self.x.is_some() || self.y.is_some() || self.z.is_some()
159    }
160
161    fn aabb(self) -> WorldAabb {
162        create_delta_aabb(
163            self.x.unwrap_or(0.0),
164            self.y.unwrap_or(0.0),
165            self.z.unwrap_or(0.0),
166        )
167    }
168}
169
170#[derive(Clone, Debug, PartialEq)]
171enum SelectorFilter {
172    Alive,
173    Name {
174        value: String,
175        inverted: bool,
176    },
177    GameMode {
178        value: GameType,
179        inverted: bool,
180    },
181    EntityType {
182        value: EntityTypeRef,
183        inverted: bool,
184    },
185    EntityTypeTag {
186        value: Identifier,
187        inverted: bool,
188    },
189    Tag {
190        value: String,
191        inverted: bool,
192    },
193    Team {
194        value: String,
195        inverted: bool,
196    },
197    Nbt {
198        value: NbtCompound,
199        inverted: bool,
200    },
201    Scores(Vec<(String, IntRange)>),
202}
203
204#[derive(Clone, Copy, Debug, PartialEq)]
205struct DoubleRange {
206    min: Option<f64>,
207    max: Option<f64>,
208}
209
210impl DoubleRange {
211    fn matches_squared(self, value: f64) -> bool {
212        if let Some(min) = self.min
213            && value < min * min
214        {
215            return false;
216        }
217        if let Some(max) = self.max
218            && value > max * max
219        {
220            return false;
221        }
222        true
223    }
224}
225
226#[derive(Clone, Copy, Debug, PartialEq)]
227struct FloatRange {
228    min: Option<f32>,
229    max: Option<f32>,
230}
231
232impl FloatRange {
233    fn matches_rotation(self, value: f32) -> bool {
234        let min = wrap_degrees(self.min.unwrap_or(0.0));
235        let max = wrap_degrees(self.max.unwrap_or(359.0));
236        let value = wrap_degrees(value);
237        if min > max {
238            value >= min || value <= max
239        } else {
240            value >= min && value <= max
241        }
242    }
243}
244
245#[derive(Clone, Copy, Debug, PartialEq)]
246struct IntRange {
247    min: Option<i32>,
248    max: Option<i32>,
249}
250
251impl IntRange {
252    #[cfg(test)]
253    const fn exactly(value: i32) -> Self {
254        Self {
255            min: Some(value),
256            max: Some(value),
257        }
258    }
259
260    const fn matches(self, value: i32) -> bool {
261        if let Some(min) = self.min
262            && value < min
263        {
264            return false;
265        }
266        if let Some(max) = self.max
267            && value > max
268        {
269            return false;
270        }
271        true
272    }
273}
274
275#[derive(Clone, Debug, Default)]
276struct InvertableOptionState {
277    positive_seen: bool,
278    negative_seen: bool,
279}
280
281impl InvertableOptionState {
282    fn parse_element(&mut self, inverted: bool, option: &str) -> Result<(), SelectorParseError> {
283        if inverted {
284            if self.positive_seen {
285                return Err(SelectorParseError::invalid(format!(
286                    "option '{option}' cannot be repeated after a positive value"
287                )));
288            }
289            self.negative_seen = true;
290        } else {
291            if self.positive_seen || self.negative_seen {
292                return Err(SelectorParseError::invalid(format!(
293                    "option '{option}' cannot add a positive value after another value"
294                )));
295            }
296            self.positive_seen = true;
297        }
298        Ok(())
299    }
300
301    const fn suggestion_mode(&self) -> InvertableSuggestionMode {
302        if self.positive_seen {
303            InvertableSuggestionMode::None
304        } else if self.negative_seen {
305            InvertableSuggestionMode::NegativeOnly
306        } else {
307            InvertableSuggestionMode::Any
308        }
309    }
310}
311
312#[derive(Clone, Copy, Debug, PartialEq, Eq)]
313enum InvertableSuggestionMode {
314    Any,
315    NegativeOnly,
316    None,
317}
318
319impl InvertableSuggestionMode {
320    const fn allows_positive(self) -> bool {
321        matches!(self, Self::Any)
322    }
323
324    const fn allows_negative(self) -> bool {
325        matches!(self, Self::Any | Self::NegativeOnly)
326    }
327
328    const fn allows_any(self) -> bool {
329        !matches!(self, Self::None)
330    }
331}
332
333#[derive(Clone, Debug, Default)]
334struct EntityTypeOptionState {
335    invertible: InvertableOptionState,
336    tags_seen: Vec<Identifier>,
337}
338
339impl EntityTypeOptionState {
340    fn parse_element(&mut self, inverted: bool, option: &str) -> Result<(), SelectorParseError> {
341        self.invertible.parse_element(inverted, option)
342    }
343
344    fn parse_tag(&mut self, tag: &Identifier, option: &str) -> Result<(), SelectorParseError> {
345        if self.tags_seen.iter().any(|existing| existing == tag) {
346            return Err(SelectorParseError::invalid(format!(
347                "option '{option}' cannot repeat tag '#{tag}'"
348            )));
349        }
350        self.invertible.parse_element(true, option)?;
351        self.tags_seen.push(tag.clone());
352        Ok(())
353    }
354}
355
356#[derive(Clone, Debug, Default)]
357struct SelectorOptionState {
358    name: InvertableOptionState,
359    team: InvertableOptionState,
360    gamemode: InvertableOptionState,
361    entity_type: EntityTypeOptionState,
362    distance: bool,
363    level: bool,
364    x: bool,
365    y: bool,
366    z: bool,
367    dx: bool,
368    dy: bool,
369    dz: bool,
370    x_rotation: bool,
371    y_rotation: bool,
372    limit: bool,
373    sort: bool,
374    scores: bool,
375}
376
377#[derive(Clone, Debug)]
378struct SelectorParseError {
379    kind: SelectorParseErrorKind,
380    cursor: usize,
381}
382
383#[derive(Clone, Debug)]
384enum SelectorParseErrorKind {
385    NotAllowed,
386    AdvancedNotAllowed,
387    Invalid(Box<TextComponent>),
388    Unsupported(String),
389}
390
391impl SelectorParseError {
392    const fn not_allowed(cursor: usize) -> Self {
393        Self {
394            kind: SelectorParseErrorKind::NotAllowed,
395            cursor,
396        }
397    }
398
399    const fn advanced_not_allowed(cursor: usize) -> Self {
400        Self {
401            kind: SelectorParseErrorKind::AdvancedNotAllowed,
402            cursor,
403        }
404    }
405
406    fn invalid(message: impl Into<TextComponent>) -> Self {
407        Self {
408            kind: SelectorParseErrorKind::Invalid(Box::new(message.into())),
409            cursor: 0,
410        }
411    }
412
413    fn invalid_at(message: impl Into<TextComponent>, cursor: usize) -> Self {
414        Self {
415            kind: SelectorParseErrorKind::Invalid(Box::new(message.into())),
416            cursor,
417        }
418    }
419
420    fn unsupported(option: impl Into<String>, cursor: usize) -> Self {
421        Self {
422            kind: SelectorParseErrorKind::Unsupported(option.into()),
423            cursor,
424        }
425    }
426
427    fn message(self) -> TextComponent {
428        match self.kind {
429            SelectorParseErrorKind::NotAllowed => {
430                TextComponent::from(&translations::ARGUMENT_ENTITY_SELECTOR_NOT_ALLOWED)
431            }
432            SelectorParseErrorKind::AdvancedNotAllowed => {
433                TextComponent::from("Advanced entity selectors are not allowed")
434            }
435            SelectorParseErrorKind::Invalid(message) => *message,
436            SelectorParseErrorKind::Unsupported(option) => {
437                TextComponent::from(format!("Unsupported entity selector option: {option}"))
438            }
439        }
440    }
441}
442
443mod model;
444mod parser;
445mod suggestions;
446
447use model::create_delta_aabb;
448pub(crate) use parser::*;
449pub(crate) use suggestions::*;
450
451#[cfg(test)]
452mod tests;