Skip to main content

steel_core/command/execution/
profile.rs

1//! Vanilla game-profile command arguments.
2
3use steel_protocol::packets::game::{
4    ArgumentType as ProtocolArgumentType, SuggestionType as ProtocolSuggestionType,
5};
6use steel_utils::{DowncastType, DowncastTypeKey, translations};
7use text_components::TextComponent;
8use uuid::Uuid;
9
10use crate::command::brigadier::{
11    CommandSyntaxError, CommandSyntaxErrorKind, StringReader, SuggestionsBuilder,
12};
13
14use super::{
15    CommandArgumentSource, CommandSource,
16    argument::{SteelArgumentParser, SteelArgumentSuggestionContext},
17    selector::{EntitySelector, parse_entity_selector, suggest_entity_selector},
18};
19
20/// Resolved game profile used by permission administration commands.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub(crate) struct ResolvedGameProfile {
23    pub(crate) uuid: Uuid,
24    pub(crate) name: String,
25}
26
27/// Parsed vanilla game-profile argument.
28#[derive(Clone, Debug, PartialEq)]
29pub(crate) enum GameProfileArgument {
30    /// Online players selected through an entity selector.
31    Selector(Box<EntitySelector>),
32    /// A direct profile name or UUID string.
33    Direct(Box<str>),
34}
35
36impl GameProfileArgument {
37    pub(crate) async fn resolve(
38        self,
39        source: &CommandSource,
40    ) -> Result<Vec<ResolvedGameProfile>, CommandSyntaxError> {
41        match self {
42            Self::Selector(selector) => {
43                let players = selector.find_online_profile_players(source)?;
44                if players.is_empty() {
45                    return Err(CommandSyntaxError::dynamic(TextComponent::from(
46                        &translations::ARGUMENT_ENTITY_NOTFOUND_PLAYER,
47                    )));
48                }
49                Ok(players
50                    .into_iter()
51                    .map(|player| ResolvedGameProfile {
52                        uuid: player.gameprofile.id,
53                        name: player.gameprofile.name.clone(),
54                    })
55                    .collect())
56            }
57            Self::Direct(name) => {
58                let profile = source
59                    .server()
60                    .resolve_player_profile(&name)
61                    .await
62                    .map_err(|error| CommandSyntaxError::dynamic(error.to_string()))?;
63                Ok(vec![ResolvedGameProfile {
64                    uuid: profile.uuid(),
65                    name: profile.last_known_name().to_owned(),
66                }])
67            }
68        }
69    }
70}
71
72// SAFETY: This Steel-owned key uniquely identifies the concrete parsed value.
73unsafe impl DowncastType for GameProfileArgument {
74    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/value/game_profile");
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub(super) enum GameProfileSuggestionMode {
79    All,
80    NonOperators,
81    Operators,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub(super) struct GameProfileParser {
86    suggestion_mode: GameProfileSuggestionMode,
87}
88
89impl GameProfileParser {
90    pub(super) const fn new(suggestion_mode: GameProfileSuggestionMode) -> Self {
91        Self { suggestion_mode }
92    }
93}
94
95// SAFETY: This Steel-owned key uniquely identifies the concrete parser.
96unsafe impl DowncastType for GameProfileParser {
97    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:command/parser/game_profile");
98}
99
100impl SteelArgumentParser for GameProfileParser {
101    type Value = GameProfileArgument;
102
103    fn parse(
104        &self,
105        reader: &mut StringReader<'_>,
106        source: &dyn CommandArgumentSource,
107    ) -> Result<Self::Value, CommandSyntaxError> {
108        if reader.peek() == Some('@') {
109            return parse_entity_selector(reader, source, false, true)
110                .map(Box::new)
111                .map(GameProfileArgument::Selector);
112        }
113
114        let value = reader.read_unquoted_string();
115        if value.is_empty() {
116            return Err(reader.error(CommandSyntaxErrorKind::UnknownArgument));
117        }
118        Ok(GameProfileArgument::Direct(value.into()))
119    }
120
121    fn list_suggestions(
122        &self,
123        context: &dyn SteelArgumentSuggestionContext,
124        builder: &mut SuggestionsBuilder<'_>,
125    ) {
126        suggest_entity_selector(builder, context.source(), false, true);
127        let names = match self.suggestion_mode {
128            GameProfileSuggestionMode::All => context.source().all_profile_names(),
129            GameProfileSuggestionMode::NonOperators => {
130                context.source().non_operator_profile_names()
131            }
132            GameProfileSuggestionMode::Operators => context.source().operator_profile_names(),
133        };
134        let prefix = builder.remaining_lowercase().to_owned();
135        for name in names {
136            if name.to_lowercase().starts_with(&prefix) {
137                builder.suggest(name);
138            }
139        }
140    }
141
142    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
143        (
144            ProtocolArgumentType::GameProfile,
145            Some(ProtocolSuggestionType::AskServer),
146        )
147    }
148}