Skip to main content

steel_core/command/execution/
source.rs

1use std::{collections::BTreeSet, ptr, sync::Arc};
2
3use crate::{
4    command::{
5        brigadier::CommandSyntaxError,
6        registration::{entity_selector_advanced_permission_expr, entity_selector_permission_expr},
7        sender::CommandSender,
8    },
9    entity::{Entity, EntityAnchor, SharedEntity},
10    permission::{
11        PermissionContext, PermissionExpr, PermissionKey, PermissionMetadataExpression,
12        PermissionRuleExpression, PermissionSet, PermissionState,
13    },
14    player::{DomainResidenceToken, KnownPlayer, Player},
15    scoreboard::Scoreboard,
16    server::Server,
17    world::World,
18};
19use glam::DVec3;
20use steel_math::{DEGREE_90, wrap_degrees};
21use steel_registry::{
22    vanilla_game_rules::{
23        LOG_ADMIN_COMMANDS, MAX_COMMAND_FORKS, MAX_COMMAND_SEQUENCE_LENGTH, SEND_COMMAND_FEEDBACK,
24    },
25    world_clock::WorldClockRef,
26};
27use steel_utils::translations;
28use text_components::{Modifier, TextComponent, format::Color};
29
30use super::{CommandExecutionContext, GameProfileArgument, selector::EntitySelector};
31
32type CommandResultCallbackFn = dyn Fn(bool, i32) + Send + Sync;
33
34/// A callback invoked after a terminal command returns or fails.
35#[derive(Clone, Default)]
36pub(crate) struct CommandResultCallback {
37    callback: Option<Arc<CommandResultCallbackFn>>,
38}
39
40impl CommandResultCallback {
41    pub(crate) fn new(callback: impl Fn(bool, i32) + Send + Sync + 'static) -> Self {
42        Self {
43            callback: Some(Arc::new(callback)),
44        }
45    }
46
47    pub(crate) const fn empty() -> Self {
48        Self { callback: None }
49    }
50
51    pub(crate) fn chain(first: Self, second: Self) -> Self {
52        match (first.callback, second.callback) {
53            (None, None) => Self::empty(),
54            (Some(callback), None) | (None, Some(callback)) => Self {
55                callback: Some(callback),
56            },
57            (Some(first), Some(second)) => Self::new(move |success, result| {
58                first(success, result);
59                second(success, result);
60            }),
61        }
62    }
63
64    pub(crate) fn on_result(&self, success: bool, result: i32) {
65        if let Some(callback) = &self.callback {
66            callback(success, result);
67        }
68    }
69}
70
71/// Read-only source data exposed to command argument parsers and suggestion providers.
72pub(crate) trait CommandArgumentSource: Send + Sync {
73    fn default_world_clock(&self) -> Option<WorldClockRef> {
74        None
75    }
76
77    fn domain_exists(&self, _domain: &str) -> bool {
78        false
79    }
80
81    fn domain_names(&self) -> Vec<&str> {
82        Vec::new()
83    }
84
85    fn command_world_names(&self) -> Vec<String> {
86        Vec::new()
87    }
88
89    fn permission_context_world_names(&self) -> Vec<String> {
90        Vec::new()
91    }
92
93    fn command_storage_keys(&self) -> Vec<String> {
94        Vec::new()
95    }
96
97    fn permission_rule_suggestions(&self) -> Vec<String> {
98        Vec::new()
99    }
100
101    fn permission_metadata_suggestions(&self) -> Vec<String> {
102        Vec::new()
103    }
104
105    fn user_permission_rule_suggestions(&self, _targets: &GameProfileArgument) -> Vec<String> {
106        Vec::new()
107    }
108
109    fn user_permission_metadata_suggestions(&self, _targets: &GameProfileArgument) -> Vec<String> {
110        Vec::new()
111    }
112
113    fn group_permission_rule_suggestions(&self, _group: &str) -> Vec<String> {
114        Vec::new()
115    }
116
117    fn group_permission_metadata_suggestions(&self, _group: &str) -> Vec<String> {
118        Vec::new()
119    }
120
121    fn entity_tag_suggestions(&self, _targets: &EntitySelector) -> Vec<String> {
122        Vec::new()
123    }
124
125    fn permission_group_names(&self) -> Vec<String> {
126        Vec::new()
127    }
128
129    fn non_operator_profile_names(&self) -> Vec<String> {
130        Vec::new()
131    }
132
133    fn all_profile_names(&self) -> Vec<String> {
134        Vec::new()
135    }
136
137    fn operator_profile_names(&self) -> Vec<String> {
138        Vec::new()
139    }
140
141    fn selector_player_names(&self) -> Vec<String> {
142        Vec::new()
143    }
144
145    fn selector_team_names(&self) -> Vec<String> {
146        Vec::new()
147    }
148
149    fn scoreboard_objective_names(&self) -> Vec<String> {
150        Vec::new()
151    }
152
153    fn allows_entity_selectors(&self) -> bool {
154        false
155    }
156
157    fn allows_advanced_entity_selectors(&self) -> bool {
158        false
159    }
160}
161
162/// Source behavior required by the Steel command scheduler.
163pub(crate) trait ExecutionCommandSource:
164    CommandArgumentSource + Sized + Send + Sync + 'static
165{
166    /// Returns whether delayed work may still execute for this exact source.
167    fn execution_is_current(&self) -> bool {
168        true
169    }
170
171    fn with_callback(&self, callback: CommandResultCallback) -> Self;
172
173    fn callback(&self) -> CommandResultCallback;
174
175    fn handle_error(&self, error: &CommandSyntaxError, forked: bool);
176}
177
178/// Permission lookup required while constructing and traversing Steel command trees.
179pub(crate) trait CommandPermissionSource: ExecutionCommandSource {
180    fn permission_state(&self, permission: &PermissionExpr) -> Option<PermissionState>;
181
182    fn has_permission(&self, permission: &PermissionExpr) -> bool {
183        self.permission_state(permission) == Some(PermissionState::Allow)
184    }
185}
186
187/// Authorization state captured when command execution starts.
188///
189/// Vanilla keeps the initiating permission state when `/execute` changes the
190/// execution entity, position, or world. Keeping this separate from the
191/// mutable execution fields prevents source transforms from changing which
192/// contextual Steel permissions apply. Player permissions come from the
193/// persistence-published server state so delayed client refreshes cannot extend
194/// revoked command access.
195#[derive(Clone, Debug, PartialEq, Eq)]
196struct CommandAuthorizationContext {
197    permission_context: PermissionContext,
198    player_permissions: Option<PermissionSet>,
199    world_scope: CommandWorldScope,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
203enum CommandWorldScope {
204    Global,
205    Domain(Box<str>),
206}
207
208impl CommandAuthorizationContext {
209    fn for_player(world: steel_utils::Identifier, permissions: PermissionSet) -> Self {
210        let domain = world.namespace.to_string().into_boxed_str();
211        Self {
212            permission_context: PermissionContext::for_world(world),
213            player_permissions: Some(permissions),
214            world_scope: CommandWorldScope::Domain(domain),
215        }
216    }
217
218    fn unrestricted(world: steel_utils::Identifier) -> Self {
219        Self {
220            permission_context: PermissionContext::for_world(world),
221            player_permissions: None,
222            world_scope: CommandWorldScope::Global,
223        }
224    }
225
226    const fn permission_context(&self) -> &PermissionContext {
227        &self.permission_context
228    }
229
230    fn permission_state(&self, permission: &PermissionExpr) -> Option<PermissionState> {
231        let Some(permissions) = &self.player_permissions else {
232            return Some(PermissionState::Allow);
233        };
234        permissions.resolve_in(permission, self.permission_context())
235    }
236
237    fn allows_execution_domain(&self, domain: &str) -> bool {
238        match &self.world_scope {
239            CommandWorldScope::Global => true,
240            CommandWorldScope::Domain(execution_domain) => execution_domain.as_ref() == domain,
241        }
242    }
243}
244
245/// Immutable Minecraft command execution source.
246#[derive(Clone)]
247pub(crate) struct CommandSource {
248    sender: CommandSender,
249    player: Option<Arc<Player>>,
250    entity: Option<SharedEntity>,
251    world: Arc<World>,
252    server: Arc<Server>,
253    position: DVec3,
254    rotation: (f32, f32),
255    anchor: EntityAnchor,
256    authorization: CommandAuthorizationContext,
257    sender_residence: Option<DomainResidenceToken>,
258    effective_player_residence: Option<DomainResidenceToken>,
259    callback: CommandResultCallback,
260    silent: bool,
261}
262
263impl CommandSource {
264    pub(crate) fn new(sender: CommandSender, server: Arc<Server>) -> Self {
265        let player = sender.get_player().cloned();
266        let world = player.as_ref().map_or_else(
267            || Arc::clone(server.overworld()),
268            |player| player.get_world(),
269        );
270        let entity = player
271            .as_ref()
272            .map(|player| Arc::clone(player) as SharedEntity);
273        let position = entity.as_ref().map_or_else(
274            || {
275                let level_data = world.level_data.read();
276                let spawn = &level_data.data().spawn;
277                DVec3::new(f64::from(spawn.x), f64::from(spawn.y), f64::from(spawn.z))
278            },
279            |entity| entity.position(),
280        );
281        let rotation = entity
282            .as_ref()
283            .map_or((0.0, 0.0), |entity| entity.rotation());
284        let sender_residence = sender
285            .get_player()
286            .map(|player| player.domain_residence_token());
287        let effective_player_residence = entity
288            .as_ref()
289            .and_then(|entity| entity.as_player())
290            .map(Player::domain_residence_token);
291        let authorization = match &sender {
292            CommandSender::Player(player) => CommandAuthorizationContext::for_player(
293                world.key.clone(),
294                server.command_permission_snapshot(player.gameprofile.id()),
295            ),
296            CommandSender::Console | CommandSender::Rcon => {
297                CommandAuthorizationContext::unrestricted(world.key.clone())
298            }
299        };
300
301        Self {
302            sender,
303            player,
304            entity,
305            world,
306            server,
307            position,
308            rotation,
309            anchor: EntityAnchor::default(),
310            authorization,
311            sender_residence,
312            effective_player_residence,
313            callback: CommandResultCallback::empty(),
314            silent: false,
315        }
316    }
317
318    #[expect(
319        dead_code,
320        reason = "source-aware runtime extensions need access to the original sender"
321    )]
322    pub(crate) const fn sender(&self) -> &CommandSender {
323        &self.sender
324    }
325
326    pub(crate) const fn player(&self) -> Option<&Arc<Player>> {
327        self.player.as_ref()
328    }
329
330    pub(crate) const fn entity(&self) -> Option<&SharedEntity> {
331        self.entity.as_ref()
332    }
333
334    pub(crate) const fn world(&self) -> &Arc<World> {
335        &self.world
336    }
337
338    pub(crate) const fn server(&self) -> &Arc<Server> {
339        &self.server
340    }
341
342    /// Returns whether `/execute` may project this source into `domain`.
343    ///
344    /// Player-originated command chains remain confined to their initial
345    /// domain. Console and Rcon sources are unrestricted.
346    pub(crate) fn allows_execution_domain(&self, domain: &str) -> bool {
347        self.authorization.allows_execution_domain(domain)
348    }
349
350    pub(crate) const fn position(&self) -> DVec3 {
351        self.position
352    }
353
354    pub(crate) const fn rotation(&self) -> (f32, f32) {
355        self.rotation
356    }
357
358    pub(crate) const fn anchor(&self) -> EntityAnchor {
359        self.anchor
360    }
361
362    pub(crate) fn with_entity(&self, entity: SharedEntity) -> Self {
363        let mut source = self.clone();
364        source.player = entity.as_player().and_then(|entity_player| {
365            self.server
366                .get_players()
367                .into_iter()
368                .find(|player| ptr::eq(player.as_ref(), entity_player))
369        });
370        source.effective_player_residence = entity.as_player().map(Player::domain_residence_token);
371        source.entity = Some(entity);
372        source
373    }
374
375    pub(crate) fn with_world(&self, world: Arc<World>) -> Result<Self, CommandSyntaxError> {
376        if !self.allows_execution_domain(world.domain()) {
377            return Err(CommandSyntaxError::dynamic(
378                "Players cannot execute commands across Steel domains",
379            ));
380        }
381        let mut source = self.clone();
382        if self.world.key != world.key {
383            let scale =
384                self.world.dimension_type.coordinate_scale / world.dimension_type.coordinate_scale;
385            source.position.x *= scale;
386            source.position.z *= scale;
387        }
388        source.world = world;
389        Ok(source)
390    }
391
392    pub(crate) fn with_position(&self, position: DVec3) -> Self {
393        let mut source = self.clone();
394        source.position = position;
395        source
396    }
397
398    pub(crate) fn with_rotation(&self, rotation: (f32, f32)) -> Self {
399        let mut source = self.clone();
400        source.rotation = normalize_rotation(rotation);
401        source
402    }
403
404    pub(crate) fn with_anchor(&self, anchor: EntityAnchor) -> Self {
405        let mut source = self.clone();
406        source.anchor = anchor;
407        source
408    }
409
410    pub(crate) fn facing_position(&self, target: DVec3) -> Self {
411        let delta = target - self.anchor_position();
412        let horizontal = delta.x.hypot(delta.z);
413        let pitch = -delta.y.atan2(horizontal).to_degrees() as f32;
414        let yaw = delta.z.atan2(delta.x).to_degrees() as f32 - DEGREE_90;
415        self.with_rotation((yaw, pitch))
416    }
417
418    pub(crate) fn anchor_position(&self) -> DVec3 {
419        if self.anchor == EntityAnchor::Eyes
420            && let Some(entity) = &self.entity
421        {
422            return DVec3::new(
423                self.position.x,
424                self.position.y + entity.get_eye_height(),
425                self.position.z,
426            );
427        }
428        self.position
429    }
430
431    #[expect(
432        dead_code,
433        reason = "silent command-source derivation is retained for future vanilla command paths"
434    )]
435    pub(crate) fn with_suppressed_output(&self) -> Self {
436        let mut source = self.clone();
437        source.silent = true;
438        source
439    }
440
441    #[expect(
442        dead_code,
443        reason = "custom executors need to inspect silent command-source state"
444    )]
445    pub(crate) const fn is_silent(&self) -> bool {
446        self.silent
447    }
448
449    /// Sends informational command output straight to the sender.
450    ///
451    /// Unlike [`Self::send_success`], this ignores the `sendCommandFeedback`
452    /// game rule and never broadcasts to admins - it's for output the sender
453    /// explicitly asked for (e.g. `/version`), not command-result feedback.
454    pub(crate) fn send_system_message(&self, message: &TextComponent) {
455        if !self.silent {
456            self.sender.send_message(message);
457        }
458    }
459
460    pub(crate) fn send_success(&self, message: &TextComponent, broadcast_to_admins: bool) {
461        if self.silent {
462            return;
463        }
464
465        let accepts_success = self
466            .sender
467            .get_player()
468            .is_none_or(|player| player.get_world().get_game_rule(&SEND_COMMAND_FEEDBACK));
469        if accepts_success {
470            self.sender.send_message(message);
471        }
472        if broadcast_to_admins {
473            self.broadcast_to_admins(message);
474        }
475    }
476
477    pub(crate) fn send_failure(&self, message: TextComponent) {
478        if !self.silent {
479            self.sender.send_message(&message.color(Color::Red));
480        }
481    }
482
483    fn sequence_limit(&self) -> usize {
484        let value = self.world.get_game_rule(&MAX_COMMAND_SEQUENCE_LENGTH);
485        value.max(1) as usize
486    }
487
488    fn fork_limit(&self) -> usize {
489        let value = self.world.get_game_rule(&MAX_COMMAND_FORKS);
490        value.max(0) as usize
491    }
492
493    fn broadcast_to_admins(&self, message: &TextComponent) {
494        let sender_name = admin_broadcast_source_name(self.entity.as_deref(), &self.sender);
495        let broadcast = translations::CHAT_TYPE_ADMIN
496            .message([sender_name, message.clone()])
497            .component()
498            .color(Color::Gray)
499            .italic(true);
500
501        if self.world.get_game_rule(&SEND_COMMAND_FEEDBACK) {
502            let sender_uuid = self
503                .sender
504                .get_player()
505                .map(|player| player.gameprofile.id());
506            for player in self.server.get_players() {
507                if Some(player.gameprofile.id()) != sender_uuid
508                    && self.server.is_operator(player.gameprofile.id())
509                {
510                    player.send_message(&broadcast);
511                }
512            }
513        }
514
515        if !matches!(self.sender, CommandSender::Console)
516            && self.world.get_game_rule(&LOG_ADMIN_COMMANDS)
517        {
518            CommandSender::Console.send_message(&broadcast);
519        }
520    }
521}
522
523impl ExecutionCommandSource for CommandSource {
524    fn execution_is_current(&self) -> bool {
525        let sender_is_current = self.sender.get_player().is_none_or(|player| {
526            self.sender_residence.is_some_and(|residence| {
527                self.server.command_world_for_player(player).is_some()
528                    && player.is_domain_residence_current(residence)
529            })
530        });
531        if !sender_is_current {
532            return false;
533        }
534
535        self.entity
536            .as_ref()
537            .and_then(|entity| entity.as_player())
538            .is_none_or(|player| {
539                self.effective_player_residence.is_some_and(|residence| {
540                    self.server.command_world_for_player(player).is_some()
541                        && player.is_domain_residence_current(residence)
542                })
543            })
544    }
545
546    fn with_callback(&self, callback: CommandResultCallback) -> Self {
547        let mut source = self.clone();
548        source.callback = callback;
549        source
550    }
551
552    fn callback(&self) -> CommandResultCallback {
553        self.callback.clone()
554    }
555
556    fn handle_error(&self, error: &CommandSyntaxError, forked: bool) {
557        if forked || self.silent {
558            return;
559        }
560        self.send_failure(error.message_component());
561        if let Some(context) = error.context_component() {
562            self.sender.send_message(&context);
563        }
564    }
565}
566
567impl CommandArgumentSource for CommandSource {
568    fn entity_tag_suggestions(&self, targets: &EntitySelector) -> Vec<String> {
569        targets
570            .find_entities(self)
571            .unwrap_or_default()
572            .into_iter()
573            .flat_map(|entity| entity.tags())
574            .collect::<BTreeSet<_>>()
575            .into_iter()
576            .collect()
577    }
578
579    fn default_world_clock(&self) -> Option<WorldClockRef> {
580        self.world.dimension_type.default_clock
581    }
582
583    fn domain_exists(&self, domain: &str) -> bool {
584        self.server.worlds.has_domain(domain)
585    }
586
587    fn domain_names(&self) -> Vec<&str> {
588        self.server.worlds.domain_names().collect()
589    }
590
591    fn command_world_names(&self) -> Vec<String> {
592        let mut names = Vec::new();
593        for key in self.server.worlds.keys() {
594            names.push(key.to_string());
595        }
596        names
597    }
598
599    fn permission_context_world_names(&self) -> Vec<String> {
600        self.server.worlds.keys().map(ToString::to_string).collect()
601    }
602
603    fn command_storage_keys(&self) -> Vec<String> {
604        self.server
605            .command_storage
606            .get(self.world.domain())
607            .map_or_else(Vec::new, |storage| {
608                storage
609                    .keys()
610                    .into_iter()
611                    .map(|key| key.to_string())
612                    .collect()
613            })
614    }
615
616    fn permission_rule_suggestions(&self) -> Vec<String> {
617        self.server.permission_rule_suggestions()
618    }
619
620    fn permission_metadata_suggestions(&self) -> Vec<String> {
621        self.server.permission_metadata_suggestions()
622    }
623
624    fn user_permission_rule_suggestions(&self, targets: &GameProfileArgument) -> Vec<String> {
625        let mut suggestions = BTreeSet::new();
626        for uuid in profile_argument_uuids(self, targets) {
627            let Some(state) = self.server.player_permission_state(uuid) else {
628                continue;
629            };
630            suggestions.extend(
631                state
632                    .overrides()
633                    .entries()
634                    .iter()
635                    .filter(|entry| can_manage_permission(self, entry.key()))
636                    .map(|entry| {
637                        PermissionRuleExpression::new(entry.key().clone(), entry.context().clone())
638                            .to_string()
639                    }),
640            );
641        }
642        suggestions.into_iter().collect()
643    }
644
645    fn user_permission_metadata_suggestions(&self, targets: &GameProfileArgument) -> Vec<String> {
646        if !has_permission_key(self, "steel.permission.metadata") {
647            return Vec::new();
648        }
649        let mut suggestions = BTreeSet::new();
650        for uuid in profile_argument_uuids(self, targets) {
651            let Some(state) = self.server.player_permission_state(uuid) else {
652                continue;
653            };
654            suggestions.extend(state.metadata_overrides().entries().iter().map(|entry| {
655                PermissionMetadataExpression::new(entry.key().clone(), entry.context().clone())
656                    .to_string()
657            }));
658        }
659        suggestions.into_iter().collect()
660    }
661
662    fn group_permission_rule_suggestions(&self, group: &str) -> Vec<String> {
663        if !can_manage_group(self, group) {
664            return Vec::new();
665        }
666        self.server
667            .permission_groups
668            .config_snapshot()
669            .groups
670            .get(group)
671            .into_iter()
672            .flat_map(|group| group.allow.iter().chain(&group.deny))
673            .filter(|expression| {
674                PermissionRuleExpression::parse(expression.as_str())
675                    .is_ok_and(|expression| can_manage_permission(self, expression.key()))
676            })
677            .cloned()
678            .collect::<BTreeSet<_>>()
679            .into_iter()
680            .collect()
681    }
682
683    fn group_permission_metadata_suggestions(&self, group: &str) -> Vec<String> {
684        if !has_permission_key(self, "steel.permission.metadata") || !can_manage_group(self, group)
685        {
686            return Vec::new();
687        }
688        self.server
689            .permission_groups
690            .config_snapshot()
691            .groups
692            .get(group)
693            .into_iter()
694            .flat_map(|group| group.metadata.iter().map(|rule| rule.key.clone()))
695            .collect::<BTreeSet<_>>()
696            .into_iter()
697            .collect()
698    }
699
700    fn permission_group_names(&self) -> Vec<String> {
701        self.server.permission_groups.group_names()
702    }
703
704    fn non_operator_profile_names(&self) -> Vec<String> {
705        profile_names(self.server(), false, false)
706    }
707
708    fn all_profile_names(&self) -> Vec<String> {
709        all_profile_names(self.server())
710    }
711
712    fn operator_profile_names(&self) -> Vec<String> {
713        profile_names(self.server(), true, true)
714    }
715
716    fn selector_player_names(&self) -> Vec<String> {
717        let domain = self.world.domain();
718        self.server
719            .get_players()
720            .into_iter()
721            .filter(|player| {
722                self.server
723                    .command_world_for_player(player)
724                    .is_some_and(|world| world.domain() == domain)
725            })
726            .map(|player| player.gameprofile.name().to_owned())
727            .collect()
728    }
729
730    fn selector_team_names(&self) -> Vec<String> {
731        self.server
732            .scoreboards
733            .get(self.world.domain())
734            .map_or_else(Vec::new, Scoreboard::team_names)
735    }
736
737    fn scoreboard_objective_names(&self) -> Vec<String> {
738        self.server
739            .scoreboards
740            .get(self.world.domain())
741            .map_or_else(Vec::new, Scoreboard::objective_names)
742    }
743
744    fn allows_entity_selectors(&self) -> bool {
745        let Ok(permission) = entity_selector_permission_expr() else {
746            tracing::error!("built-in entity selector permission key is invalid");
747            return false;
748        };
749        CommandPermissionSource::has_permission(self, &permission)
750    }
751
752    fn allows_advanced_entity_selectors(&self) -> bool {
753        let Ok(permission) = entity_selector_advanced_permission_expr() else {
754            tracing::error!("built-in advanced entity selector permission key is invalid");
755            return false;
756        };
757        CommandPermissionSource::has_permission(self, &permission)
758    }
759}
760
761fn profile_argument_uuids(
762    source: &CommandSource,
763    argument: &GameProfileArgument,
764) -> BTreeSet<uuid::Uuid> {
765    match argument {
766        GameProfileArgument::Selector(selector) => {
767            selector.find_online_profile_players(source).map_or_else(
768                |_| BTreeSet::new(),
769                |players| {
770                    players
771                        .into_iter()
772                        .map(|player| player.gameprofile.id())
773                        .collect()
774                },
775            )
776        }
777        GameProfileArgument::Direct(value) => {
778            let known = source.server.known_players();
779            let uuid = uuid::Uuid::parse_str(value)
780                .ok()
781                .or_else(|| known.by_name(value).map(KnownPlayer::uuid))
782                .or_else(|| {
783                    source
784                        .server
785                        .get_players()
786                        .into_iter()
787                        .find(|player| player.gameprofile.name().eq_ignore_ascii_case(value))
788                        .map(|player| player.gameprofile.id())
789                });
790            uuid.into_iter().collect()
791        }
792    }
793}
794
795fn can_manage_permission(source: &CommandSource, permission: &PermissionKey) -> bool {
796    has_permission_key(
797        source,
798        &format!("steel.permission.manage.{}", permission.as_str()),
799    )
800}
801
802fn can_manage_group(source: &CommandSource, group: &str) -> bool {
803    has_permission_key(source, &format!("steel.permission.group.{group}"))
804}
805
806fn has_permission_key(source: &CommandSource, value: &str) -> bool {
807    PermissionKey::parse(value)
808        .is_ok_and(|key| CommandPermissionSource::has_permission(source, &PermissionExpr::key(key)))
809}
810
811fn profile_names(server: &Server, operator: bool, include_known: bool) -> Vec<String> {
812    let players = server.get_players();
813    let mut names = Vec::new();
814    for player in &players {
815        if operator == server.is_operator(player.gameprofile.id()) {
816            names.push(player.gameprofile.name().to_owned());
817        }
818    }
819
820    if !include_known {
821        return names;
822    }
823
824    for known in server.known_players().entries() {
825        if names
826            .iter()
827            .any(|name| name.eq_ignore_ascii_case(known.last_known_name()))
828            || players.iter().any(|player| {
829                player
830                    .gameprofile
831                    .name()
832                    .eq_ignore_ascii_case(known.last_known_name())
833            })
834        {
835            continue;
836        }
837        let is_operator = server.is_operator(known.uuid());
838        if operator == is_operator {
839            names.push(known.last_known_name().to_owned());
840        }
841    }
842    names
843}
844
845fn all_profile_names(server: &Server) -> Vec<String> {
846    let players = server.get_players();
847    let mut names = players
848        .iter()
849        .map(|player| player.gameprofile.name().to_owned())
850        .collect::<Vec<_>>();
851    for known in server.known_players().entries() {
852        if names
853            .iter()
854            .any(|name| name.eq_ignore_ascii_case(known.last_known_name()))
855        {
856            continue;
857        }
858        names.push(known.last_known_name().to_owned());
859    }
860    names
861}
862
863impl CommandPermissionSource for CommandSource {
864    fn permission_state(&self, permission: &PermissionExpr) -> Option<PermissionState> {
865        self.authorization.permission_state(permission)
866    }
867}
868
869impl CommandExecutionContext<CommandSource> {
870    pub(crate) fn for_source(source: &CommandSource) -> Self {
871        Self::new(source.sequence_limit(), source.fork_limit())
872    }
873}
874
875fn admin_broadcast_source_name(
876    entity: Option<&dyn Entity>,
877    sender: &CommandSender,
878) -> TextComponent {
879    entity.map_or_else(
880        || TextComponent::plain(sender.to_string()),
881        |entity| TextComponent::plain(entity.plain_text_name()),
882    )
883}
884
885fn normalize_rotation((yaw, pitch): (f32, f32)) -> (f32, f32) {
886    (wrap_degrees(yaw), wrap_degrees(pitch))
887}
888
889#[cfg(test)]
890mod tests {
891    use std::sync::Weak;
892
893    use glam::DVec3;
894    use steel_registry::{entity_type::EntityTypeRef, vanilla_entities};
895    use steel_utils::Identifier;
896    use text_components::TextComponent;
897
898    use crate::command::sender::CommandSender;
899    use crate::entity::{Entity, EntityBase};
900    use crate::permission::{
901        PermissionContext, PermissionEntry, PermissionExpr, PermissionKey, PermissionSet,
902        PermissionState,
903    };
904
905    use super::{CommandAuthorizationContext, admin_broadcast_source_name, normalize_rotation};
906
907    struct NamedTestEntity {
908        base: EntityBase,
909    }
910
911    crate::entity::impl_test_downcast_type!(NamedTestEntity);
912
913    impl Entity for NamedTestEntity {
914        fn base(&self) -> &EntityBase {
915            &self.base
916        }
917
918        fn entity_type(&self) -> EntityTypeRef {
919            &vanilla_entities::ITEM
920        }
921
922        fn plain_text_name(&self) -> String {
923            "Executor".to_owned()
924        }
925    }
926
927    fn permission_key(value: &str) -> PermissionKey {
928        match PermissionKey::parse(value) {
929            Ok(key) => key,
930            Err(error) => panic!("test permission key should parse: {error}"),
931        }
932    }
933
934    #[test]
935    fn rotation_normalization_matches_command_source_stack() {
936        assert_eq!(normalize_rotation((540.0, -540.0)), (-180.0, -180.0));
937        assert_eq!(normalize_rotation((-181.0, 181.0)), (179.0, -179.0));
938    }
939
940    #[test]
941    fn admin_broadcast_uses_current_execution_entity_name() {
942        let entity = NamedTestEntity {
943            base: EntityBase::new(
944                1,
945                DVec3::ZERO,
946                vanilla_entities::ITEM.dimensions,
947                Weak::new(),
948            ),
949        };
950
951        assert_eq!(
952            admin_broadcast_source_name(Some(&entity), &CommandSender::Console),
953            TextComponent::plain("Executor")
954        );
955        assert_eq!(
956            admin_broadcast_source_name(None, &CommandSender::Console),
957            TextComponent::plain("Server")
958        );
959    }
960
961    #[test]
962    fn authorization_context_captures_initial_world_scope() {
963        let world = Identifier::new("lobby", "spawn");
964        let authorization =
965            CommandAuthorizationContext::for_player(world.clone(), PermissionSet::new());
966
967        assert_eq!(
968            authorization.permission_context(),
969            &PermissionContext::for_world(world)
970        );
971        assert!(authorization.allows_execution_domain("lobby"));
972        assert!(!authorization.allows_execution_domain("survival"));
973    }
974
975    #[test]
976    fn authorization_context_uses_published_snapshot_instead_of_stale_player_permissions() {
977        let world = Identifier::new("lobby", "spawn");
978        let permission = permission_key("minecraft.command.stop");
979        let stale_player_permissions =
980            PermissionSet::from_entries([PermissionEntry::allow(permission.clone())]);
981        assert!(stale_player_permissions.allows_key(&permission));
982
983        let authorization = CommandAuthorizationContext::for_player(world, PermissionSet::new());
984
985        assert_eq!(
986            authorization.permission_state(&PermissionExpr::key(permission)),
987            None
988        );
989    }
990
991    #[test]
992    fn unrestricted_authorization_allows_console_permissions() {
993        let authorization =
994            CommandAuthorizationContext::unrestricted(Identifier::new("default", "overworld"));
995        let permission = PermissionExpr::key(permission_key("minecraft.command.stop"));
996
997        assert_eq!(
998            authorization.permission_state(&permission),
999            Some(PermissionState::Allow)
1000        );
1001        assert!(authorization.allows_execution_domain("default"));
1002        assert!(authorization.allows_execution_domain("survival"));
1003    }
1004}