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