Skip to main content

steel_core/command/execution/
score.rs

1//! Deferred scoreboard command arguments.
2
3use uuid::Uuid;
4
5use super::{CommandArgumentSource, CommandSource};
6use crate::{
7    command::{
8        brigadier::{
9            CommandSyntaxError, CommandSyntaxErrorKind, ReaderCursor, StringReader,
10            SuggestionsBuilder,
11        },
12        execution::selector::{EntitySelector, parse_entity_selector, suggest_entity_selector},
13    },
14    entity::Entity as _,
15    scoreboard::{ScoreHolder, Scoreboard},
16};
17use steel_utils::translations;
18use text_components::{TextComponent, translation::Translation};
19
20/// A score holder expression retained until command execution.
21#[derive(Clone, Debug, PartialEq)]
22pub(crate) enum ScoreHolderArgument {
23    /// A direct holder name, resolved against online players at execution time.
24    Name(Box<str>),
25    /// A UUID with its original token retained for name-only fallback.
26    Uuid { uuid: Uuid, raw: Box<str> },
27    /// An entity selector resolved to entity scoreboard names at execution time.
28    Selector(Box<EntitySelector>),
29    /// The scoreboard's tracked-holder wildcard.
30    Wildcard,
31}
32
33/// Wildcard expansion used by a score-holder consumer.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub(crate) enum ScoreHolderWildcard {
36    /// Wildcards resolve to no holders.
37    Empty,
38    /// Wildcards resolve to every holder tracked by the current domain scoreboard.
39    Tracked,
40}
41
42impl ScoreHolderArgument {
43    pub(crate) fn resolve(
44        &self,
45        source: &CommandSource,
46        wildcard: ScoreHolderWildcard,
47    ) -> Result<Vec<ScoreHolder>, CommandSyntaxError> {
48        let holders = match self {
49            Self::Name(name) => vec![resolve_name(name, source)],
50            Self::Uuid { uuid, raw } => resolve_uuid(uuid, raw, source),
51            Self::Selector(selector) => selector
52                .find_entities(source)?
53                .into_iter()
54                .map(|entity| ScoreHolder::new(entity.scoreboard_name()))
55                .collect(),
56            Self::Wildcard => match wildcard {
57                ScoreHolderWildcard::Empty => Vec::new(),
58                ScoreHolderWildcard::Tracked => source
59                    .server()
60                    .scoreboards
61                    .get(source.world().domain())
62                    .map_or_else(Vec::new, Scoreboard::tracked_holders),
63            },
64        };
65        Ok(holders)
66    }
67}
68
69/// Inclusive integer bounds used by vanilla range arguments.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub(crate) struct IntRange {
72    minimum: Option<i32>,
73    maximum: Option<i32>,
74}
75
76impl IntRange {
77    const fn new(minimum: Option<i32>, maximum: Option<i32>) -> Self {
78        Self { minimum, maximum }
79    }
80
81    pub(crate) const fn matches(self, value: i32) -> bool {
82        if let Some(minimum) = self.minimum
83            && value < minimum
84        {
85            return false;
86        }
87        if let Some(maximum) = self.maximum
88            && value > maximum
89        {
90            return false;
91        }
92        true
93    }
94}
95
96pub(super) fn parse_score_holder<S>(
97    reader: &mut StringReader<'_>,
98    source: &S,
99    multiple: bool,
100) -> Result<ScoreHolderArgument, CommandSyntaxError>
101where
102    S: CommandArgumentSource + ?Sized,
103{
104    if reader.peek() == Some('@') {
105        return parse_entity_selector(reader, source, !multiple, false)
106            .map(Box::new)
107            .map(ScoreHolderArgument::Selector);
108    }
109
110    let start = reader.read_so_far().len();
111    while reader.can_read() && reader.peek() != Some(' ') {
112        reader.skip();
113    }
114    let raw = &reader.input()[start..reader.read_so_far().len()];
115    if raw == "*" {
116        return Ok(ScoreHolderArgument::Wildcard);
117    }
118    if let Ok(uuid) = Uuid::parse_str(raw) {
119        return Ok(ScoreHolderArgument::Uuid {
120            uuid,
121            raw: raw.into(),
122        });
123    }
124    Ok(ScoreHolderArgument::Name(raw.into()))
125}
126
127pub(super) fn suggest_score_holders<S>(builder: &mut SuggestionsBuilder<'_>, source: &S)
128where
129    S: CommandArgumentSource + ?Sized,
130{
131    // Vanilla's SUGGEST_SCORE_HOLDERS provider is selector-aware but does not
132    // filter suggestions to the single-result parser variant.
133    suggest_entity_selector(builder, source, false, false);
134}
135
136pub(super) fn parse_int_range(
137    reader: &mut StringReader<'_>,
138) -> Result<IntRange, CommandSyntaxError> {
139    let start = reader.checkpoint();
140    if !reader.can_read() {
141        return Err(range_error(
142            reader,
143            start,
144            &translations::ARGUMENT_RANGE_EMPTY,
145        ));
146    }
147
148    let minimum = read_optional_integer(reader)
149        .map_err(|()| range_error(reader, start, &translations::ARGUMENT_RANGE_INTS))?;
150    let maximum = if reader.can_read_length(2)
151        && reader.peek() == Some('.')
152        && peek_next(reader) == Some('.')
153    {
154        reader.skip();
155        reader.skip();
156        read_optional_integer(reader)
157            .map_err(|()| range_error(reader, start, &translations::ARGUMENT_RANGE_INTS))?
158    } else {
159        minimum
160    };
161
162    if minimum.is_none() && maximum.is_none() {
163        return Err(range_error(
164            reader,
165            start,
166            &translations::ARGUMENT_RANGE_EMPTY,
167        ));
168    }
169    if minimum
170        .zip(maximum)
171        .is_some_and(|(minimum, maximum)| minimum > maximum)
172    {
173        return Err(range_error(
174            reader,
175            start,
176            &translations::ARGUMENT_RANGE_SWAPPED,
177        ));
178    }
179    Ok(IntRange::new(minimum, maximum))
180}
181
182fn resolve_name(name: &str, source: &CommandSource) -> ScoreHolder {
183    if name.starts_with('#') {
184        return ScoreHolder::new(name.to_owned());
185    }
186    source
187        .server()
188        .get_players()
189        .into_iter()
190        .filter(|player| player.get_world().domain() == source.world().domain())
191        .find(|player| player.gameprofile.name.eq_ignore_ascii_case(name))
192        .map_or_else(
193            || ScoreHolder::new(name.to_owned()),
194            |player| ScoreHolder::new(player.scoreboard_name()),
195        )
196}
197
198fn resolve_uuid(uuid: &Uuid, raw: &str, source: &CommandSource) -> Vec<ScoreHolder> {
199    let holders = source
200        .server()
201        .worlds
202        .worlds_in_domain(source.world().domain())
203        .into_iter()
204        .filter_map(|world| world.get_entity_by_uuid(uuid))
205        .map(|entity| ScoreHolder::new(entity.scoreboard_name()))
206        .collect::<Vec<_>>();
207    if holders.is_empty() {
208        vec![ScoreHolder::new(raw.to_owned())]
209    } else {
210        holders
211    }
212}
213
214fn read_optional_integer(reader: &mut StringReader<'_>) -> Result<Option<i32>, ()> {
215    let start = reader.read_so_far().len();
216    while reader
217        .peek()
218        .is_some_and(|character| is_range_number_character(character, peek_next(reader)))
219    {
220        reader.skip();
221    }
222    let raw = &reader.input()[start..reader.read_so_far().len()];
223    if raw.is_empty() {
224        return Ok(None);
225    }
226    raw.parse().map(Some).map_err(|_| ())
227}
228
229fn peek_next(reader: &StringReader<'_>) -> Option<char> {
230    reader.remaining().chars().nth(1)
231}
232
233fn is_range_number_character(character: char, next: Option<char>) -> bool {
234    character.is_ascii_digit() || character == '-' || (character == '.' && next != Some('.'))
235}
236
237fn range_error(
238    reader: &mut StringReader<'_>,
239    start: ReaderCursor,
240    translation: &'static Translation<0>,
241) -> CommandSyntaxError {
242    reader.restore(start);
243    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
244        TextComponent::from(translation),
245    )))
246}