Skip to main content

steel_core/command/execution/selector/
model.rs

1use super::*;
2use crate::server::Server;
3
4#[derive(Clone, Copy)]
5enum PlayerSelection {
6    Gameplay,
7    OnlineProfile,
8}
9
10impl EntitySelector {
11    pub(super) fn new(
12        kind: SelectorKind,
13        max_results: usize,
14        includes_entities: bool,
15        current_entity: bool,
16        order: SelectorOrder,
17    ) -> Self {
18        Self {
19            kind,
20            max_results,
21            includes_entities,
22            current_entity,
23            world_limited: false,
24            order,
25            position: SelectorPosition::default(),
26            delta: SelectorDelta::default(),
27            distance: None,
28            level: None,
29            x_rotation: None,
30            y_rotation: None,
31            filters: Vec::new(),
32            uses_advanced_options: false,
33        }
34    }
35
36    pub(super) fn for_selector_type(selector_type: SelectorType) -> Self {
37        let (max_results, includes_entities, current_entity, order) = match selector_type {
38            SelectorType::AllPlayers => (usize::MAX, false, false, SelectorOrder::Arbitrary),
39            SelectorType::AllEntities => (usize::MAX, true, false, SelectorOrder::Arbitrary),
40            SelectorType::NearestEntity => (1, true, false, SelectorOrder::Nearest),
41            SelectorType::NearestPlayer => (1, false, false, SelectorOrder::Nearest),
42            SelectorType::RandomPlayer => (1, false, false, SelectorOrder::Random),
43            SelectorType::SelfEntity => (1, true, true, SelectorOrder::Arbitrary),
44        };
45        let mut selector = Self::new(
46            SelectorKind::Selector(selector_type),
47            max_results,
48            includes_entities,
49            current_entity,
50            order,
51        );
52        if matches!(
53            selector_type,
54            SelectorType::AllEntities | SelectorType::NearestEntity
55        ) {
56            selector.filters.push(SelectorFilter::Alive);
57        }
58        selector
59    }
60
61    pub(super) fn validate_for_argument(
62        &self,
63        single: bool,
64        players_only: bool,
65    ) -> Result<(), SelectorParseError> {
66        if single && self.max_results > 1 {
67            let message = if players_only {
68                TextComponent::from(&translations::ARGUMENT_PLAYER_TOOMANY).to_string()
69            } else {
70                TextComponent::from(&translations::ARGUMENT_ENTITY_TOOMANY).to_string()
71            };
72            return Err(SelectorParseError::invalid(message));
73        }
74        if players_only && self.includes_entities && !self.current_entity {
75            return Err(SelectorParseError::invalid(
76                TextComponent::from(&translations::ARGUMENT_PLAYER_ENTITIES).to_string(),
77            ));
78        }
79        Ok(())
80    }
81
82    pub(crate) fn find_players(
83        &self,
84        source: &CommandSource,
85    ) -> Result<Vec<Arc<Player>>, CommandSyntaxError> {
86        self.find_players_with(source, PlayerSelection::Gameplay)
87    }
88
89    pub(crate) fn find_online_profile_players(
90        &self,
91        source: &CommandSource,
92    ) -> Result<Vec<Arc<Player>>, CommandSyntaxError> {
93        // Profile administration is server-global. Explicit spatial selector
94        // options still limit candidates to the source world.
95        self.find_players_with(source, PlayerSelection::OnlineProfile)
96    }
97
98    fn find_players_with(
99        &self,
100        source: &CommandSource,
101        selection: PlayerSelection,
102    ) -> Result<Vec<Arc<Player>>, CommandSyntaxError> {
103        self.check_selector_permission(source)?;
104        let server = source.server();
105        let position = selector_position(self, source);
106        let aabb = self.absolute_aabb(position);
107        let mut players = match &self.kind {
108            SelectorKind::PlayerName(name) => server
109                .get_players()
110                .into_iter()
111                .filter(|player| match selection {
112                    PlayerSelection::Gameplay => selected_player_world(server, player, selection)
113                        .is_some_and(|world| world.domain() == source.world().domain()),
114                    PlayerSelection::OnlineProfile => true,
115                })
116                .filter(|player| player_name_matches(&player.gameprofile.name, name))
117                .collect::<Vec<_>>(),
118            SelectorKind::EntityUuid(uuid) => server
119                .get_players()
120                .into_iter()
121                .filter(|player| match selection {
122                    PlayerSelection::Gameplay => selected_player_world(server, player, selection)
123                        .is_some_and(|world| world.domain() == source.world().domain()),
124                    PlayerSelection::OnlineProfile => true,
125                })
126                .filter(|player| player.uuid() == *uuid)
127                .collect::<Vec<_>>(),
128            SelectorKind::Selector(SelectorType::SelfEntity) => {
129                let Some(player) = source.player() else {
130                    return Ok(Vec::new());
131                };
132                if selected_player_world(server, player, selection).is_some()
133                    && self.matches_entity(player.as_ref(), position, aabb, source)?
134                {
135                    vec![Arc::clone(player)]
136                } else {
137                    Vec::new()
138                }
139            }
140            SelectorKind::Selector(_) => self.candidate_players(source, selection),
141        };
142
143        if !matches!(self.kind, SelectorKind::Selector(SelectorType::SelfEntity)) {
144            let mut filtered = Vec::new();
145            for player in players {
146                if self.matches_entity(player.as_ref(), position, aabb, source)? {
147                    filtered.push(player);
148                    if self.stops_filtering_after_match_count(filtered.len()) {
149                        break;
150                    }
151                }
152            }
153            players = filtered;
154        }
155        self.sort_and_limit_players(position, &mut players);
156        Ok(players)
157    }
158
159    pub(crate) fn find_entities(
160        &self,
161        source: &CommandSource,
162    ) -> Result<Vec<SharedEntity>, CommandSyntaxError> {
163        self.check_selector_permission(source)?;
164        if !self.includes_entities {
165            return Ok(self
166                .find_players(source)?
167                .into_iter()
168                .map(|player| player as SharedEntity)
169                .collect());
170        }
171        let server = source.server();
172        let position = selector_position(self, source);
173        let aabb = self.absolute_aabb(position);
174        let mut entities = match &self.kind {
175            SelectorKind::PlayerName(name) => server
176                .get_players()
177                .into_iter()
178                .filter(|player| player.get_world().domain() == source.world().domain())
179                .filter(|player| player_name_matches(&player.gameprofile.name, name))
180                .map(|player| player as SharedEntity)
181                .collect::<Vec<_>>(),
182            SelectorKind::EntityUuid(uuid) => find_entity_by_uuid(source, uuid)
183                .into_iter()
184                .collect::<Vec<_>>(),
185            SelectorKind::Selector(SelectorType::SelfEntity) => {
186                let Some(entity) = source.entity() else {
187                    return Ok(Vec::new());
188                };
189                if self.matches_entity(entity.as_ref(), position, aabb, source)? {
190                    vec![Arc::clone(entity)]
191                } else {
192                    Vec::new()
193                }
194            }
195            SelectorKind::Selector(_) => self.candidate_entities(source, aabb),
196        };
197        entities.retain(|entity| {
198            entity
199                .as_player()
200                .is_none_or(|player| source.server().command_world_for_player(player).is_some())
201        });
202
203        if !matches!(self.kind, SelectorKind::Selector(SelectorType::SelfEntity)) {
204            let mut filtered = Vec::new();
205            for entity in entities {
206                if self.matches_entity(entity.as_ref(), position, aabb, source)? {
207                    filtered.push(entity);
208                    if self.stops_filtering_after_match_count(filtered.len()) {
209                        break;
210                    }
211                }
212            }
213            entities = filtered;
214        }
215        self.sort_and_limit_entities(position, &mut entities);
216        Ok(entities)
217    }
218
219    fn check_selector_permission(&self, source: &CommandSource) -> Result<(), CommandSyntaxError> {
220        if !matches!(self.kind, SelectorKind::Selector(_)) {
221            return Ok(());
222        }
223        if !allow_selectors(source) {
224            return Err(CommandSyntaxError::dynamic(TextComponent::from(
225                &translations::ARGUMENT_ENTITY_SELECTOR_NOT_ALLOWED,
226            )));
227        }
228        if self.uses_advanced_options && !allow_advanced_selectors(source) {
229            return Err(CommandSyntaxError::dynamic(
230                "Advanced entity selectors are not allowed",
231            ));
232        }
233        Ok(())
234    }
235
236    fn candidate_players(
237        &self,
238        source: &CommandSource,
239        selection: PlayerSelection,
240    ) -> Vec<Arc<Player>> {
241        let mut players = source.server().get_players();
242        if self.world_limited {
243            players.retain(|player| {
244                selected_player_world(source.server(), player, selection)
245                    .is_some_and(|world| Arc::ptr_eq(&world, source.world()))
246            });
247        } else if matches!(selection, PlayerSelection::Gameplay) {
248            let domain = source.world().domain();
249            players.retain(|player| {
250                selected_player_world(source.server(), player, selection)
251                    .is_some_and(|world| world.domain() == domain)
252            });
253        }
254        players
255    }
256
257    fn candidate_entities(
258        &self,
259        source: &CommandSource,
260        aabb: Option<WorldAabb>,
261    ) -> Vec<SharedEntity> {
262        if self.world_limited {
263            return world_candidates(source.world(), aabb);
264        }
265
266        source
267            .server()
268            .worlds
269            .worlds_in_domain(source.world().domain())
270            .into_iter()
271            .flat_map(|world| world_candidates(&world, aabb))
272            .collect()
273    }
274
275    fn absolute_aabb(&self, position: DVec3) -> Option<WorldAabb> {
276        if self.delta.has_any() {
277            return Some(self.delta.aabb().translate(position));
278        }
279        let max_distance = self.distance.and_then(|distance| distance.max)?;
280        Some(
281            WorldAabb::from_min_max(
282                DVec3::splat(-max_distance),
283                DVec3::splat(max_distance + 1.0),
284            )
285            .translate(position),
286        )
287    }
288
289    fn requires_position(&self) -> bool {
290        self.distance.is_some()
291            || self.delta.has_any()
292            || self
293                .position
294                .x
295                .is_some_and(|_| self.position.y.is_none() || self.position.z.is_none())
296            || self
297                .position
298                .y
299                .is_some_and(|_| self.position.x.is_none() || self.position.z.is_none())
300            || self
301                .position
302                .z
303                .is_some_and(|_| self.position.x.is_none() || self.position.y.is_none())
304            || matches!(self.order, SelectorOrder::Nearest | SelectorOrder::Furthest)
305    }
306
307    fn matches_entity(
308        &self,
309        entity: &dyn Entity,
310        position: DVec3,
311        aabb: Option<WorldAabb>,
312        source: &CommandSource,
313    ) -> Result<bool, CommandSyntaxError> {
314        if let Some(aabb) = aabb
315            && !aabb.intersects(entity.bounding_box())
316        {
317            return Ok(false);
318        }
319        if let Some(distance) = self.distance
320            && !distance.matches_squared(entity.position().distance_squared(position))
321        {
322            return Ok(false);
323        }
324        if let Some(level) = self.level {
325            let Some(player) = entity.as_player() else {
326                return Ok(false);
327            };
328            if !level.matches(player.experience.lock().level()) {
329                return Ok(false);
330            }
331        }
332        if let Some(range) = self.x_rotation
333            && !range.matches_rotation(entity.rotation().1)
334        {
335            return Ok(false);
336        }
337        if let Some(range) = self.y_rotation
338            && !range.matches_rotation(entity.rotation().0)
339        {
340            return Ok(false);
341        }
342        for filter in &self.filters {
343            if !filter.matches(entity, source)? {
344                return Ok(false);
345            }
346        }
347        Ok(true)
348    }
349
350    const fn stops_filtering_after_match_count(&self, count: usize) -> bool {
351        matches!(self.order, SelectorOrder::Arbitrary) && count >= self.max_results
352    }
353
354    fn sort_and_limit_players(&self, position: DVec3, players: &mut Vec<Arc<Player>>) {
355        match self.order {
356            SelectorOrder::Nearest => players.sort_by(|left, right| {
357                left.position()
358                    .distance_squared(position)
359                    .total_cmp(&right.position().distance_squared(position))
360            }),
361            SelectorOrder::Furthest => players.sort_by(|left, right| {
362                right
363                    .position()
364                    .distance_squared(position)
365                    .total_cmp(&left.position().distance_squared(position))
366            }),
367            SelectorOrder::Random => players.shuffle(&mut rand::rng()),
368            SelectorOrder::Arbitrary => {}
369        }
370        players.truncate(self.max_results);
371    }
372
373    fn sort_and_limit_entities(&self, position: DVec3, entities: &mut Vec<SharedEntity>) {
374        match self.order {
375            SelectorOrder::Nearest => entities.sort_by(|left, right| {
376                left.position()
377                    .distance_squared(position)
378                    .total_cmp(&right.position().distance_squared(position))
379            }),
380            SelectorOrder::Furthest => entities.sort_by(|left, right| {
381                right
382                    .position()
383                    .distance_squared(position)
384                    .total_cmp(&left.position().distance_squared(position))
385            }),
386            SelectorOrder::Random => entities.shuffle(&mut rand::rng()),
387            SelectorOrder::Arbitrary => {}
388        }
389        entities.truncate(self.max_results);
390    }
391}
392
393fn selected_player_world(
394    server: &Server,
395    player: &Player,
396    selection: PlayerSelection,
397) -> Option<Arc<World>> {
398    match selection {
399        PlayerSelection::Gameplay => server.command_world_for_player(player),
400        PlayerSelection::OnlineProfile => server.live_world_for_player(player),
401    }
402}
403
404impl SelectorFilter {
405    fn matches(
406        &self,
407        entity: &dyn Entity,
408        source: &CommandSource,
409    ) -> Result<bool, CommandSyntaxError> {
410        match self {
411            Self::Alive => Ok(entity.is_alive()),
412            Self::Name { value, inverted } => {
413                Ok(entity_name_filter_matches(value, *inverted, entity))
414            }
415            Self::GameMode { value, inverted } => {
416                Ok(game_mode_filter_matches(*value, *inverted, entity))
417            }
418            Self::EntityType { value, inverted } => {
419                let matches = entity.entity_type() == *value;
420                Ok(matches != *inverted)
421            }
422            Self::EntityTypeTag { value, inverted } => {
423                let matches = REGISTRY.entity_types.is_in_tag(entity.entity_type(), value);
424                Ok(matches != *inverted)
425            }
426            Self::Tag { value, inverted } => {
427                let tags = entity.tags();
428                let matches = if value.is_empty() {
429                    tags.is_empty()
430                } else {
431                    tags.iter().any(|tag| tag == value)
432                };
433                Ok(matches != *inverted)
434            }
435            Self::Team { value, inverted } => {
436                let holder_name = entity.scoreboard_name();
437                let scoreboard = source_scoreboard(source)?;
438                Ok(team_filter_matches(
439                    value,
440                    *inverted,
441                    &holder_name,
442                    scoreboard,
443                ))
444            }
445            Self::Nbt { value, inverted } => {
446                Ok(entity_nbt_filter_matches(value, *inverted, entity))
447            }
448            Self::Scores(scores) => {
449                let holder_name = entity.scoreboard_name();
450                let scoreboard = source_scoreboard(source)?;
451                Ok(score_filter_matches(scores, &holder_name, scoreboard))
452            }
453        }
454    }
455}
456
457pub(super) fn entity_nbt_filter_matches(
458    expected: &NbtCompound,
459    inverted: bool,
460    entity: &dyn Entity,
461) -> bool {
462    let actual = entity.nbt_for_data_compare();
463    compare_nbt_compounds(expected, &actual, true) != inverted
464}
465
466pub(super) fn entity_name_filter_matches(value: &str, inverted: bool, entity: &dyn Entity) -> bool {
467    (entity.plain_text_name() == value) != inverted
468}
469
470pub(super) fn game_mode_filter_matches(
471    value: GameType,
472    inverted: bool,
473    entity: &dyn Entity,
474) -> bool {
475    let Some(player) = entity.as_player() else {
476        return false;
477    };
478    (player.game_mode() == value) != inverted
479}
480
481pub(super) fn team_filter_matches(
482    expected: &str,
483    inverted: bool,
484    holder_name: &str,
485    scoreboard: &Scoreboard,
486) -> bool {
487    let holder = ScoreHolder::new(holder_name.to_owned());
488    let current = scoreboard.holder_team_name(&holder).unwrap_or_default();
489    (current == expected) != inverted
490}
491
492const fn player_name_matches(actual: &str, expected: &str) -> bool {
493    actual.eq_ignore_ascii_case(expected)
494}
495
496pub(super) fn score_filter_matches(
497    scores: &[(String, IntRange)],
498    holder_name: &str,
499    scoreboard: &Scoreboard,
500) -> bool {
501    let holder = ScoreHolder::new(holder_name.to_owned());
502    scores.iter().all(|(objective_name, range)| {
503        let Some(objective) = scoreboard.objective(objective_name) else {
504            return false;
505        };
506        scoreboard
507            .score(&holder, &objective)
508            .is_some_and(|score| range.matches(score))
509    })
510}
511
512fn source_scoreboard(source: &CommandSource) -> Result<&Scoreboard, CommandSyntaxError> {
513    source
514        .server()
515        .scoreboards
516        .get(source.world().domain())
517        .ok_or_else(|| {
518            CommandSyntaxError::dynamic(format!(
519                "Domain '{}' has no command scoreboard",
520                source.world().domain()
521            ))
522        })
523}
524
525fn world_candidates(world: &World, aabb: Option<WorldAabb>) -> Vec<SharedEntity> {
526    aabb.map_or_else(
527        || world.entity_manager().get_accessible_entities(),
528        |aabb| world.entity_manager().get_entities_in_aabb(&aabb),
529    )
530}
531
532pub(super) fn selector_position(selector: &EntitySelector, source: &CommandSource) -> DVec3 {
533    let base = if selector.requires_position() {
534        source.position()
535    } else {
536        DVec3::ZERO
537    };
538    selector.position.apply(base)
539}
540
541pub(super) fn find_entity_by_uuid(source: &CommandSource, uuid: &Uuid) -> Option<SharedEntity> {
542    source
543        .server()
544        .worlds
545        .worlds_in_domain(source.world().domain())
546        .into_iter()
547        .find_map(|world| {
548            let entity = world.get_entity_by_uuid(uuid)?;
549            world.get_accessible_entity_by_id(entity.id())
550        })
551}
552
553pub(super) fn create_delta_aabb(x: f64, y: f64, z: f64) -> WorldAabb {
554    let min = DVec3::new(
555        if x < 0.0 { x } else { 0.0 },
556        if y < 0.0 { y } else { 0.0 },
557        if z < 0.0 { z } else { 0.0 },
558    );
559    let max = DVec3::new(
560        if x < 0.0 { 0.0 } else { x } + 1.0,
561        if y < 0.0 { 0.0 } else { y } + 1.0,
562        if z < 0.0 { 0.0 } else { z } + 1.0,
563    );
564    WorldAabb::from_min_max(min, max)
565}