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#[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
72pub(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
159pub(crate) trait ExecutionCommandSource:
161 CommandArgumentSource + Sized + Send + Sync + 'static
162{
163 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
175pub(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#[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#[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 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}