Skip to main content

steel_core/command/execution/
source.rs

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