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 .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
116 .map_or_else(Vec::new, |targets| {
117 context.source().user_permission_rule_suggestions(targets)
118 }),
119 PermissionSuggestionScope::GroupOwned => context
120 .argument("group")
121 .and_then(|value| value.downcast_ref::<PermissionGroupName>())
122 .map_or_else(Vec::new, |group| {
123 context
124 .source()
125 .group_permission_rule_suggestions(group.as_str())
126 }),
127 };
128 suggest_expression(builder, context.source(), expressions);
129 }
130
131 fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
132 permission_expression_argument()
133 }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub(super) struct PermissionMetadataParser {
138 suggestions: PermissionSuggestionScope,
139}
140
141impl PermissionMetadataParser {
142 pub(super) const fn all() -> Self {
143 Self {
144 suggestions: PermissionSuggestionScope::All,
145 }
146 }
147
148 pub(super) const fn user_owned() -> Self {
149 Self {
150 suggestions: PermissionSuggestionScope::UserOwned,
151 }
152 }
153
154 pub(super) const fn group_owned() -> Self {
155 Self {
156 suggestions: PermissionSuggestionScope::GroupOwned,
157 }
158 }
159}
160
161unsafe impl DowncastType for PermissionMetadataParser {
163 const TYPE_KEY: DowncastTypeKey =
164 DowncastTypeKey::new("steel:command/parser/permission_metadata");
165}
166
167impl SteelArgumentParser for PermissionMetadataParser {
168 type Value = PermissionMetadataExpression;
169
170 fn parse(
171 &self,
172 reader: &mut StringReader<'_>,
173 _source: &dyn CommandArgumentSource,
174 ) -> Result<Self::Value, CommandSyntaxError> {
175 let value = reader.read_unquoted_token();
176 PermissionMetadataExpression::parse(value).map_err(|error| {
177 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
178 error.to_string().into(),
179 )))
180 })
181 }
182
183 fn list_suggestions(
184 &self,
185 context: &dyn SteelArgumentSuggestionContext,
186 builder: &mut SuggestionsBuilder<'_>,
187 ) {
188 let expressions = match self.suggestions {
189 PermissionSuggestionScope::All => context.source().permission_metadata_suggestions(),
190 PermissionSuggestionScope::UserOwned => context
191 .argument("targets")
192 .and_then(|value| value.downcast_ref::<super::GameProfileArgument>())
193 .map_or_else(Vec::new, |targets| {
194 context
195 .source()
196 .user_permission_metadata_suggestions(targets)
197 }),
198 PermissionSuggestionScope::GroupOwned => context
199 .argument("group")
200 .and_then(|value| value.downcast_ref::<PermissionGroupName>())
201 .map_or_else(Vec::new, |group| {
202 context
203 .source()
204 .group_permission_metadata_suggestions(group.as_str())
205 }),
206 };
207 suggest_expression(builder, context.source(), expressions);
208 }
209
210 fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
211 permission_expression_argument()
212 }
213}
214
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216pub(super) struct PermissionGroupParser {
217 pub(super) require_existing: bool,
218}
219
220unsafe impl DowncastType for PermissionGroupParser {
222 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/parser/permission_group");
223}
224
225impl SteelArgumentParser for PermissionGroupParser {
226 type Value = PermissionGroupName;
227
228 fn parse(
229 &self,
230 reader: &mut StringReader<'_>,
231 source: &dyn CommandArgumentSource,
232 ) -> Result<Self::Value, CommandSyntaxError> {
233 let value = reader.read_unquoted_string();
234 PermissionSegment::parse(value).map_err(|error| {
235 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
236 error.to_string().into(),
237 )))
238 })?;
239 if self.require_existing
240 && !source
241 .permission_group_names()
242 .iter()
243 .any(|group| group == value)
244 {
245 return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
246 format!("Unknown permission group '{value}'").into(),
247 ))));
248 }
249 Ok(PermissionGroupName(value.into()))
250 }
251
252 fn list_suggestions(
253 &self,
254 context: &dyn SteelArgumentSuggestionContext,
255 builder: &mut SuggestionsBuilder<'_>,
256 ) {
257 let prefix = builder.remaining_lowercase().to_owned();
258 for group in context.source().permission_group_names() {
259 if group.to_lowercase().starts_with(&prefix) {
260 builder.suggest(group);
261 }
262 }
263 }
264
265 fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
266 permission_group_argument()
267 }
268}
269
270const fn permission_expression_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>)
271{
272 (
276 ProtocolArgumentType::String {
277 behavior: ArgumentStringTypeBehavior::GreedyPhrase,
278 },
279 Some(ProtocolSuggestionType::AskServer),
280 )
281}
282
283const fn permission_group_argument() -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
284 (
285 ProtocolArgumentType::String {
286 behavior: ArgumentStringTypeBehavior::SingleWord,
287 },
288 Some(ProtocolSuggestionType::AskServer),
289 )
290}
291
292fn suggest_expression(
293 builder: &mut SuggestionsBuilder<'_>,
294 source: &dyn CommandArgumentSource,
295 expressions: Vec<String>,
296) {
297 let prefix = builder.remaining();
298 for expression in &expressions {
299 if expression.starts_with(prefix) {
300 builder.suggest(expression.clone());
301 }
302 }
303
304 let Some((base, context_prefix)) = prefix.split_once('{') else {
305 return;
306 };
307 if base.is_empty() || context_prefix.ends_with('}') {
308 return;
309 }
310
311 let known_contexts = known_custom_contexts(
312 expressions
313 .iter()
314 .chain(source.permission_rule_suggestions().iter())
315 .chain(source.permission_metadata_suggestions().iter()),
316 );
317 suggest_context(builder, source, base, context_prefix, &known_contexts);
318}
319
320fn suggest_context(
321 builder: &mut SuggestionsBuilder<'_>,
322 source: &dyn CommandArgumentSource,
323 base: &str,
324 context_prefix: &str,
325 known_contexts: &BTreeMap<String, BTreeSet<String>>,
326) {
327 let (completed, current) = context_prefix
328 .rsplit_once(',')
329 .map_or(("", context_prefix), |(completed, current)| {
330 (completed, current)
331 });
332 let completed_keys = completed
333 .split(',')
334 .filter_map(|entry| entry.split_once('=').map(|(key, _)| key))
335 .collect::<BTreeSet<_>>();
336 let entry_prefix = if completed.is_empty() {
337 format!("{base}{{")
338 } else {
339 format!("{base}{{{completed},")
340 };
341
342 let Some((key, value_prefix)) = current.split_once('=') else {
343 for key in ["domain", "world"]
344 .into_iter()
345 .chain(known_contexts.keys().map(String::as_str))
346 {
347 if !completed_keys.contains(key) && key.starts_with(current) {
348 builder.suggest(format!("{entry_prefix}{key}="));
349 }
350 }
351 return;
352 };
353
354 let values = match key {
355 "domain" => source
356 .domain_names()
357 .into_iter()
358 .map(str::to_owned)
359 .collect(),
360 "world" => source.permission_context_world_names(),
361 custom => known_contexts
362 .get(custom)
363 .map_or_else(Vec::new, |values| values.iter().cloned().collect()),
364 };
365 for value in values {
366 if value.starts_with(value_prefix) {
367 builder.suggest(format!("{entry_prefix}{key}={value}}}"));
368 }
369 }
370}
371
372fn known_custom_contexts<'a>(
373 expressions: impl Iterator<Item = &'a String>,
374) -> BTreeMap<String, BTreeSet<String>> {
375 let mut contexts = BTreeMap::new();
376 for expression in expressions {
377 if let Ok(expression) = PermissionRuleExpression::parse(expression) {
378 collect_custom_contexts(expression.context(), &mut contexts);
379 } else if let Ok(expression) = PermissionMetadataExpression::parse(expression) {
380 collect_custom_contexts(expression.context(), &mut contexts);
381 }
382 }
383 contexts
384}
385
386fn collect_custom_contexts(
387 context: &PermissionRuleContext,
388 contexts: &mut BTreeMap<String, BTreeSet<String>>,
389) {
390 match context {
391 PermissionRuleContext::Custom { key, value } => {
392 contexts
393 .entry(key.as_str().to_owned())
394 .or_default()
395 .insert(value.as_str().to_owned());
396 }
397 PermissionRuleContext::All(nested) => {
398 for context in nested.iter() {
399 collect_custom_contexts(context, contexts);
400 }
401 }
402 PermissionRuleContext::Global
403 | PermissionRuleContext::Domain(_)
404 | PermissionRuleContext::World(_) => {}
405 }
406}