steel_core/command/execution/
permission.rs1use std::collections::{BTreeMap, BTreeSet};
4
5use steel_protocol::packets::game::{
6 ArgumentStringTypeBehavior, ArgumentType as ProtocolArgumentType,
7 SuggestionType as ProtocolSuggestionType,
8};
9use steel_utils::{DowncastType, DowncastTypeKey};
10
11use crate::{
12 command::brigadier::{
13 CommandSyntaxError, CommandSyntaxErrorKind, StringReader, SuggestionsBuilder,
14 },
15 permission::{
16 PermissionMetadataExpression, PermissionRuleContext, PermissionRuleExpression,
17 PermissionSegment,
18 },
19};
20
21use super::{
22 CommandArgumentSource,
23 argument::{SteelArgumentParser, SteelArgumentSuggestionContext},
24};
25
26unsafe impl DowncastType for PermissionRuleExpression {
28 const TYPE_KEY: DowncastTypeKey =
29 DowncastTypeKey::new("steel:command/value/permission_rule_expression");
30}
31
32unsafe impl DowncastType for PermissionMetadataExpression {
34 const TYPE_KEY: DowncastTypeKey =
35 DowncastTypeKey::new("steel:command/value/permission_metadata_expression");
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
40pub(crate) struct PermissionGroupName(Box<str>);
41
42impl PermissionGroupName {
43 pub(crate) fn as_str(&self) -> &str {
44 &self.0
45 }
46}
47
48unsafe impl DowncastType for PermissionGroupName {
50 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/value/permission_group");
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54enum PermissionSuggestionScope {
55 All,
56 UserOwned,
57 GroupOwned,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub(super) struct PermissionRuleParser {
62 suggestions: PermissionSuggestionScope,
63}
64
65impl PermissionRuleParser {
66 pub(super) const fn all() -> Self {
67 Self {
68 suggestions: PermissionSuggestionScope::All,
69 }
70 }
71
72 pub(super) const fn user_owned() -> Self {
73 Self {
74 suggestions: PermissionSuggestionScope::UserOwned,
75 }
76 }
77
78 pub(super) const fn group_owned() -> Self {
79 Self {
80 suggestions: PermissionSuggestionScope::GroupOwned,
81 }
82 }
83}
84
85unsafe impl DowncastType for PermissionRuleParser {
87 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/parser/permission_rule");
88}
89
90impl SteelArgumentParser for PermissionRuleParser {
91 type Value = PermissionRuleExpression;
92
93 fn parse(
94 &self,
95 reader: &mut StringReader<'_>,
96 _source: &dyn CommandArgumentSource,
97 ) -> Result<Self::Value, CommandSyntaxError> {
98 let value = reader.read_unquoted_token();
99 PermissionRuleExpression::parse(value).map_err(|error| {
100 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
101 error.to_string().into(),
102 )))
103 })
104 }
105
106 fn list_suggestions(
107 &self,
108 context: &dyn SteelArgumentSuggestionContext,
109 builder: &mut SuggestionsBuilder<'_>,
110 ) {
111 let expressions = match self.suggestions {
112 PermissionSuggestionScope::All => context.source().permission_rule_suggestions(),
113 PermissionSuggestionScope::UserOwned => context
114 .argument("targets")
115 .ok()
116 .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
117 .map_or_else(Vec::new, |targets| {
118 context.source().user_permission_rule_suggestions(targets)
119 }),
120 PermissionSuggestionScope::GroupOwned => context
121 .argument("group")
122 .ok()
123 .and_then(|value| value.downcast_ref::<PermissionGroupName>())
124 .map_or_else(Vec::new, |group| {
125 context
126 .source()
127 .group_permission_rule_suggestions(group.as_str())
128 }),
129 };
130 suggest_expression(builder, context.source(), expressions);
131 }
132
133 fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
134 permission_expression_argument()
135 }
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub(super) struct PermissionMetadataParser {
140 suggestions: PermissionSuggestionScope,
141}
142
143impl PermissionMetadataParser {
144 pub(super) const fn all() -> Self {
145 Self {
146 suggestions: PermissionSuggestionScope::All,
147 }
148 }
149
150 pub(super) const fn user_owned() -> Self {
151 Self {
152 suggestions: PermissionSuggestionScope::UserOwned,
153 }
154 }
155
156 pub(super) const fn group_owned() -> Self {
157 Self {
158 suggestions: PermissionSuggestionScope::GroupOwned,
159 }
160 }
161}
162
163unsafe impl DowncastType for PermissionMetadataParser {
165 const TYPE_KEY: DowncastTypeKey =
166 DowncastTypeKey::new("steel:command/parser/permission_metadata");
167}
168
169impl SteelArgumentParser for PermissionMetadataParser {
170 type Value = PermissionMetadataExpression;
171
172 fn parse(
173 &self,
174 reader: &mut StringReader<'_>,
175 _source: &dyn CommandArgumentSource,
176 ) -> Result<Self::Value, CommandSyntaxError> {
177 let value = reader.read_unquoted_token();
178 PermissionMetadataExpression::parse(value).map_err(|error| {
179 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
180 error.to_string().into(),
181 )))
182 })
183 }
184
185 fn list_suggestions(
186 &self,
187 context: &dyn SteelArgumentSuggestionContext,
188 builder: &mut SuggestionsBuilder<'_>,
189 ) {
190 let expressions = match self.suggestions {
191 PermissionSuggestionScope::All => context.source().permission_metadata_suggestions(),
192 PermissionSuggestionScope::UserOwned => context
193 .argument("targets")
194 .ok()
195 .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
196 .map_or_else(Vec::new, |targets| {
197 context
198 .source()
199 .user_permission_metadata_suggestions(targets)
200 }),
201 PermissionSuggestionScope::GroupOwned => context
202 .argument("group")
203 .ok()
204 .and_then(|value| value.downcast_ref::<PermissionGroupName>())
205 .map_or_else(Vec::new, |group| {
206 context
207 .source()
208 .group_permission_metadata_suggestions(group.as_str())
209 }),
210 };
211 suggest_expression(builder, context.source(), expressions);
212 }
213
214 fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
215 permission_expression_argument()
216 }
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub(super) struct PermissionGroupParser {
221 pub(super) require_existing: bool,
222}
223
224unsafe impl DowncastType for PermissionGroupParser {
226 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/parser/permission_group");
227}
228
229impl SteelArgumentParser for PermissionGroupParser {
230 type Value = PermissionGroupName;
231
232 fn parse(
233 &self,
234 reader: &mut StringReader<'_>,
235 source: &dyn CommandArgumentSource,
236 ) -> Result<Self::Value, CommandSyntaxError> {
237 let value = reader.read_unquoted_string();
238 PermissionSegment::parse(value).map_err(|error| {
239 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
240 error.to_string().into(),
241 )))
242 })?;
243 if self.require_existing
244 && !source
245 .permission_group_names()
246 .iter()
247 .any(|group| group == value)
248 {
249 return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
250 format!("Unknown permission group '{value}'").into(),
251 ))));
252 }
253 Ok(PermissionGroupName(value.into()))
254 }
255
256 fn list_suggestions(
257 &self,
258 context: &dyn SteelArgumentSuggestionContext,
259 builder: &mut SuggestionsBuilder<'_>,
260 ) {
261 let prefix = builder.remaining_lowercase().to_owned();
262 for group in context.source().permission_group_names() {
263 if group.to_lowercase().starts_with(&prefix) {
264 builder.suggest(group);
265 }
266 }
267 }
268
269 fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
270 permission_group_argument()
271 }
272}
273
274const fn permission_expression_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>)
275{
276 (
280 ProtocolArgumentType::String {
281 behavior: ArgumentStringTypeBehavior::GreedyPhrase,
282 },
283 Some(ProtocolSuggestionType::AskServer),
284 )
285}
286
287const fn permission_group_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
288 (
289 ProtocolArgumentType::String {
290 behavior: ArgumentStringTypeBehavior::SingleWord,
291 },
292 Some(ProtocolSuggestionType::AskServer),
293 )
294}
295
296fn suggest_expression(
297 builder: &mut SuggestionsBuilder<'_>,
298 source: &dyn CommandArgumentSource,
299 expressions: Vec<String>,
300) {
301 let prefix = builder.remaining();
302 for expression in &expressions {
303 if expression.starts_with(prefix) {
304 builder.suggest(expression.clone());
305 }
306 }
307
308 let Some((base, context_prefix)) = prefix.split_once('{') else {
309 return;
310 };
311 if base.is_empty() || context_prefix.ends_with('}') {
312 return;
313 }
314
315 let known_contexts = known_custom_contexts(
316 expressions
317 .iter()
318 .chain(source.permission_rule_suggestions().iter())
319 .chain(source.permission_metadata_suggestions().iter()),
320 );
321 suggest_context(builder, source, base, context_prefix, &known_contexts);
322}
323
324fn suggest_context(
325 builder: &mut SuggestionsBuilder<'_>,
326 source: &dyn CommandArgumentSource,
327 base: &str,
328 context_prefix: &str,
329 known_contexts: &BTreeMap<String, BTreeSet<String>>,
330) {
331 let (completed, current) = context_prefix
332 .rsplit_once(',')
333 .map_or(("", context_prefix), |(completed, current)| {
334 (completed, current)
335 });
336 let completed_keys = completed
337 .split(',')
338 .filter_map(|entry| entry.split_once('=').map(|(key, _)| key))
339 .collect::<BTreeSet<_>>();
340 let entry_prefix = if completed.is_empty() {
341 format!("{base}{{")
342 } else {
343 format!("{base}{{{completed},")
344 };
345
346 let Some((key, value_prefix)) = current.split_once('=') else {
347 for key in ["domain", "world"]
348 .into_iter()
349 .chain(known_contexts.keys().map(String::as_str))
350 {
351 if !completed_keys.contains(key) && key.starts_with(current) {
352 builder.suggest(format!("{entry_prefix}{key}="));
353 }
354 }
355 return;
356 };
357
358 let values = match key {
359 "domain" => source
360 .domain_names()
361 .into_iter()
362 .map(str::to_owned)
363 .collect(),
364 "world" => source.permission_context_world_names(),
365 custom => known_contexts
366 .get(custom)
367 .map_or_else(Vec::new, |values| values.iter().cloned().collect()),
368 };
369 for value in values {
370 if value.starts_with(value_prefix) {
371 builder.suggest(format!("{entry_prefix}{key}={value}}}"));
372 }
373 }
374}
375
376fn known_custom_contexts<'a>(
377 expressions: impl Iterator<Item = &'a String>,
378) -> BTreeMap<String, BTreeSet<String>> {
379 let mut contexts = BTreeMap::new();
380 for expression in expressions {
381 if let Ok(expression) = PermissionRuleExpression::parse(expression) {
382 collect_custom_contexts(expression.context(), &mut contexts);
383 } else if let Ok(expression) = PermissionMetadataExpression::parse(expression) {
384 collect_custom_contexts(expression.context(), &mut contexts);
385 }
386 }
387 contexts
388}
389
390fn collect_custom_contexts(
391 context: &PermissionRuleContext,
392 contexts: &mut BTreeMap<String, BTreeSet<String>>,
393) {
394 match context {
395 PermissionRuleContext::Custom { key, value } => {
396 contexts
397 .entry(key.as_str().to_owned())
398 .or_default()
399 .insert(value.as_str().to_owned());
400 }
401 PermissionRuleContext::All(nested) => {
402 for context in nested.iter() {
403 collect_custom_contexts(context, contexts);
404 }
405 }
406 PermissionRuleContext::Global
407 | PermissionRuleContext::Domain(_)
408 | PermissionRuleContext::World(_) => {}
409 }
410}