Skip to main content

steel_core/command/
sender.rs

1//! Module defining the sender of a command.
2use std::{fmt, sync::Arc};
3use text_components::TextComponent;
4use uuid::Uuid;
5
6use crate::{
7    player::{DomainResidenceToken, Player},
8    server::Server,
9};
10
11/// The sender of a command.
12#[derive(Clone)]
13pub enum CommandSender {
14    /// The command was sent by a player via the chat.
15    Player(Arc<Player>),
16    /// The command was sent via the server's console.
17    Console,
18    /// The command was sent via Rcon.
19    Rcon,
20}
21
22/// Stable identity used to preserve top-level command ordering while work is suspended.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub(crate) enum CommandSenderKey {
25    Player(Uuid),
26    Console,
27    Rcon,
28}
29
30/// Exact key used to coalesce suggestions from one live player residence.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub(crate) enum CommandSuggestionKey {
33    Player {
34        uuid: Uuid,
35        session_address: usize,
36        residence: Option<DomainResidenceToken>,
37    },
38    Console,
39    Rcon,
40}
41
42/// Exact runtime owner of queued command work.
43///
44/// UUIDs remain the ordering key, while the player `Arc` and residence token
45/// prevent an old session or an earlier domain stay from resuming work.
46#[derive(Clone)]
47pub(crate) struct CommandExecutionOwner {
48    sender: CommandSender,
49    player_residence: Option<DomainResidenceToken>,
50}
51
52impl CommandExecutionOwner {
53    pub(crate) fn capture(sender: CommandSender, server: &Server) -> Self {
54        let player_residence = sender.get_player().and_then(|player| {
55            // Snapshot first so a concurrent detach cannot pair source
56            // availability with the next domain residence.
57            let residence = player.domain_residence_token();
58            server.command_world_for_player(player).map(|_| residence)
59        });
60        Self {
61            sender,
62            player_residence,
63        }
64    }
65
66    #[cfg(test)]
67    pub(crate) fn non_player_for_test(sender: CommandSender) -> Self {
68        assert!(
69            sender.get_player().is_none(),
70            "test helper only constructs non-player command owners"
71        );
72        Self {
73            sender,
74            player_residence: None,
75        }
76    }
77
78    pub(crate) fn key(&self) -> CommandSenderKey {
79        self.sender.key()
80    }
81
82    pub(crate) fn suggestion_key(&self) -> CommandSuggestionKey {
83        match &self.sender {
84            CommandSender::Player(player) => CommandSuggestionKey::Player {
85                uuid: player.gameprofile.id,
86                // The owner retains this Arc while queued, so its allocation
87                // address cannot be reused by a replacement session.
88                session_address: Arc::as_ptr(player) as usize,
89                residence: self.player_residence,
90            },
91            CommandSender::Console => CommandSuggestionKey::Console,
92            CommandSender::Rcon => CommandSuggestionKey::Rcon,
93        }
94    }
95
96    pub(crate) const fn sender(&self) -> &CommandSender {
97        &self.sender
98    }
99
100    pub(crate) fn is_current(&self, server: &Server) -> bool {
101        let CommandSender::Player(player) = &self.sender else {
102            return true;
103        };
104        let Some(residence) = self.player_residence else {
105            return false;
106        };
107        server.command_world_for_player(player).is_some()
108            && player.is_domain_residence_current(residence)
109    }
110}
111
112impl CommandSender {
113    pub(crate) fn key(&self) -> CommandSenderKey {
114        match self {
115            Self::Player(player) => CommandSenderKey::Player(player.gameprofile.id),
116            Self::Console => CommandSenderKey::Console,
117            Self::Rcon => CommandSenderKey::Rcon,
118        }
119    }
120
121    /// Returns the player if the sender is a player.
122    #[must_use]
123    pub const fn get_player(&self) -> Option<&Arc<Player>> {
124        match self {
125            Self::Player(player) => Some(player),
126            _ => None,
127        }
128    }
129
130    /// Sends a system message to the command sender.
131    pub fn send_message(&self, text: &TextComponent) {
132        match self {
133            Self::Player(player) => player.send_message(text),
134            Self::Console => log::info!("{text}"),
135            // TODO: Implement Rcon message sending
136            Self::Rcon => log::warn!("Dropping Rcon command message until Rcon output is wired"),
137        }
138    }
139}
140
141impl fmt::Display for CommandSender {
142    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
143        write!(
144            f,
145            "{}",
146            match self {
147                Self::Player(p) => &p.gameprofile.name,
148                Self::Console => "Server",
149                Self::Rcon => "Rcon",
150            }
151        )
152    }
153}