steel_core/command/execution/
score.rs1use 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#[derive(Clone, Debug, PartialEq)]
22pub(crate) enum ScoreHolderArgument {
23 Name(Box<str>),
25 Uuid { uuid: Uuid, raw: Box<str> },
27 Selector(Box<EntitySelector>),
29 Wildcard,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub(crate) enum ScoreHolderWildcard {
36 Empty,
38 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#[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 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}