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