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)
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}