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, start)?;
149    let maximum = if reader.can_read_length(2)
150        && reader.peek() == Some('.')
151        && peek_next(reader) == Some('.')
152    {
153        reader.skip();
154        reader.skip();
155        read_optional_integer(reader, start)?
156    } else {
157        minimum
158    };
159
160    if minimum.is_none() && maximum.is_none() {
161        return Err(range_error(
162            reader,
163            start,
164            &translations::ARGUMENT_RANGE_EMPTY,
165        ));
166    }
167    if minimum
168        .zip(maximum)
169        .is_some_and(|(minimum, maximum)| minimum > maximum)
170    {
171        return Err(range_error(
172            reader,
173            start,
174            &translations::ARGUMENT_RANGE_SWAPPED,
175        ));
176    }
177    Ok(IntRange::new(minimum, maximum))
178}
179
180fn resolve_name(name: &str, source: &CommandSource) -> ScoreHolder {
181    if name.starts_with('#') {
182        return ScoreHolder::new(name.to_owned());
183    }
184    source
185        .server()
186        .get_players()
187        .into_iter()
188        .filter(|player| player.get_world().domain() == source.world().domain())
189        .find(|player| player.gameprofile.name.eq_ignore_ascii_case(name))
190        .map_or_else(
191            || ScoreHolder::new(name.to_owned()),
192            |player| ScoreHolder::new(player.scoreboard_name()),
193        )
194}
195
196fn resolve_uuid(uuid: &Uuid, raw: &str, source: &CommandSource) -> Vec<ScoreHolder> {
197    let holders = source
198        .server()
199        .worlds
200        .worlds_in_domain(source.world().domain())
201        .into_iter()
202        .filter_map(|world| world.get_entity_by_uuid(uuid))
203        .map(|entity| ScoreHolder::new(entity.scoreboard_name()))
204        .collect::<Vec<_>>();
205    if holders.is_empty() {
206        vec![ScoreHolder::new(raw.to_owned())]
207    } else {
208        holders
209    }
210}
211
212fn read_optional_integer(
213    reader: &mut StringReader<'_>,
214    range_start: ReaderCursor,
215) -> Result<Option<i32>, CommandSyntaxError> {
216    let start = reader.read_so_far().len();
217    while reader
218        .peek()
219        .is_some_and(|character| is_range_number_character(character, peek_next(reader)))
220    {
221        reader.skip();
222    }
223    let raw = &reader.input()[start..reader.read_so_far().len()];
224    if raw.is_empty() {
225        return Ok(None);
226    }
227    let Ok(value) = raw.parse() else {
228        let raw = raw.into();
229        reader.restore(range_start);
230        return Err(reader.error(CommandSyntaxErrorKind::InvalidInt(raw)));
231    };
232    Ok(Some(value))
233}
234
235fn peek_next(reader: &StringReader<'_>) -> Option<char> {
236    reader.remaining().chars().nth(1)
237}
238
239fn is_range_number_character(character: char, next: Option<char>) -> bool {
240    character.is_ascii_digit() || character == '-' || (character == '.' && next != Some('.'))
241}
242
243fn range_error(
244    reader: &mut StringReader<'_>,
245    start: ReaderCursor,
246    translation: &'static Translation<0>,
247) -> CommandSyntaxError {
248    reader.restore(start);
249    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
250        TextComponent::from(translation),
251    )))
252}