Skip to main content

steel_core/command/execution/
source.rs

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