Skip to main content

chunkedge_command/
scopes.rs

1//! Scope graph for the ChunkEdge Command system.
2//!
3//! ## Breakdown
4//! Each scope is a node in a graph. A path from one node to another indicates
5//! that the first scope implies the second. A dot in the scope name indicates
6//! a sub-scope. You can use this to create a hierarchy of scopes. For example,
7//! the scope "chunkedge.command" implies "chunkedge.command.tp". this means
8//! that if a player has the "chunkedge.command" scope, they can use the "tp"
9//! command.
10//!
11//! You may also link scopes together in the registry. This is useful for admin
12//! scope umbrellas. For example, if the scope "chunkedge.admin" is linked to
13//! "chunkedge.command", It means that if a player has the "chunkedge.admin"
14//! scope, they can use all commands under the command scope.
15//!
16//! # Example
17//! ```
18//! use chunkedge_command::scopes::CommandScopeRegistry;
19//!
20//! let mut registry = CommandScopeRegistry::new();
21//!
22//! // add a scope to the registry
23//! registry.add_scope("chunkedge.command.teleport");
24//!
25//! // we added 4 scopes to the registry. "chunkedge", "chunkedge.command", "chunkedge.command.teleport",
26//! // and the root scope.
27//! assert_eq!(registry.scope_count(), 4);
28//!
29//! registry.add_scope("chunkedge.admin");
30//!
31//! // add a scope to the registry with a link to another scope
32//! registry.link("chunkedge.admin", "chunkedge.command.teleport");
33//!
34//! // the "chunkedge.admin" scope implies the "chunkedge.command.teleport" scope
35//! assert_eq!(
36//!     registry.grants("chunkedge.admin", "chunkedge.command.teleport"),
37//!     true
38//! );
39//! ```
40
41use std::collections::{BTreeSet, HashMap};
42use std::fmt::{Debug, Formatter};
43
44use bevy_app::{App, Plugin, Update};
45use bevy_derive::{Deref, DerefMut};
46use bevy_ecs::prelude::{Component, ResMut, Resource};
47use bevy_ecs::query::Changed;
48use bevy_ecs::system::Query;
49use petgraph::dot;
50use petgraph::dot::Dot;
51use petgraph::prelude::*;
52
53pub struct CommandScopePlugin;
54
55impl Plugin for CommandScopePlugin {
56    fn build(&self, app: &mut App) {
57        app.init_resource::<CommandScopeRegistry>()
58            .add_systems(Update, add_new_scopes);
59    }
60}
61
62/// Command scope Component for players. This is a list of scopes that a player
63/// has. If a player has a scope, they can use any command that requires
64/// that scope.
65#[derive(
66    Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Component, Default, Deref, DerefMut,
67)]
68pub struct CommandScopes(pub BTreeSet<String>);
69
70/// This system makes it a bit easier to add new scopes to the registry without
71/// having to explicitly add them to the registry on app startup.
72fn add_new_scopes(
73    mut registry: ResMut<CommandScopeRegistry>,
74    scopes: Query<&CommandScopes, Changed<CommandScopes>>,
75) {
76    for scopes in scopes.iter() {
77        for scope in scopes.iter() {
78            if !registry.string_to_node.contains_key(scope) {
79                registry.add_scope(scope);
80            }
81        }
82    }
83}
84
85impl CommandScopes {
86    /// create a new scope component
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// add a scope to this component
92    pub fn add(&mut self, scope: &str) {
93        self.0.insert(scope.into());
94    }
95}
96
97/// Store the scope graph and provide methods for querying it.
98#[derive(Clone, Resource)]
99pub struct CommandScopeRegistry {
100    graph: Graph<String, ()>,
101    string_to_node: HashMap<String, NodeIndex>,
102    root: NodeIndex,
103}
104
105impl Default for CommandScopeRegistry {
106    fn default() -> Self {
107        let mut graph = Graph::new();
108        let root = graph.add_node("root".to_owned());
109        Self {
110            graph,
111            string_to_node: HashMap::from([("root".to_owned(), root)]),
112            root,
113        }
114    }
115}
116
117impl Debug for CommandScopeRegistry {
118    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
119        write!(
120            f,
121            "{:?}",
122            Dot::with_config(&self.graph, &[dot::Config::EdgeNoLabel])
123        )?;
124        Ok(())
125    }
126}
127
128impl CommandScopeRegistry {
129    /// Create a new scope registry.
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Add a scope to the registry.
135    ///
136    /// # Example
137    /// ```
138    /// use chunkedge_command::CommandScopeRegistry;
139    ///
140    /// let mut registry = CommandScopeRegistry::new();
141    ///
142    /// // creates two nodes. "chunkedge" and "command" with an edge from "chunkedge" to "command"
143    /// registry.add_scope("chunkedge.command");
144    /// // creates one node. "chunkedge.command.tp" with an edge from "chunkedge.command" to
145    /// // "chunkedge.command.tp"
146    /// registry.add_scope("chunkedge.command.tp");
147    ///
148    /// // the root node is always present
149    /// assert_eq!(registry.scope_count(), 4);
150    /// ```
151    pub fn add_scope<S: Into<String>>(&mut self, scope: S) {
152        let scope = scope.into();
153        if self.string_to_node.contains_key(&scope) {
154            return;
155        }
156        let mut current_node = self.root;
157        let mut prefix = String::new();
158        for part in scope.split('.') {
159            let node = self
160                .string_to_node
161                .entry(prefix.clone() + part)
162                .or_insert_with(|| {
163                    let node = self.graph.add_node(part.to_owned());
164                    self.graph.add_edge(current_node, node, ());
165                    node
166                });
167            current_node = *node;
168
169            prefix = prefix + part + ".";
170        }
171    }
172
173    /// Remove a scope from the registry.
174    ///
175    /// # Example
176    /// ```
177    /// use chunkedge_command::CommandScopeRegistry;
178    ///
179    /// let mut registry = CommandScopeRegistry::new();
180    ///
181    /// registry.add_scope("chunkedge.command");
182    /// registry.add_scope("chunkedge.command.tp");
183    ///
184    /// assert_eq!(registry.scope_count(), 4);
185    ///
186    /// registry.remove_scope("chunkedge.command.tp");
187    ///
188    /// assert_eq!(registry.scope_count(), 3);
189    /// ```
190    pub fn remove_scope(&mut self, scope: &str) {
191        if let Some(node) = self.string_to_node.remove(scope) {
192            self.graph.remove_node(node);
193        };
194    }
195
196    /// Check if a scope grants another scope.
197    ///
198    /// # Example
199    /// ```
200    /// use chunkedge_command::CommandScopeRegistry;
201    ///
202    /// let mut registry = CommandScopeRegistry::new();
203    ///
204    /// registry.add_scope("chunkedge.command");
205    /// registry.add_scope("chunkedge.command.tp");
206    ///
207    /// assert!(registry.grants("chunkedge.command", "chunkedge.command.tp")); // command implies tp
208    /// assert!(!registry.grants("chunkedge.command.tp", "chunkedge.command")); // tp does not imply command
209    /// ```
210    pub fn grants(&self, scope: &str, other: &str) -> bool {
211        if scope == other {
212            return true;
213        }
214
215        let scope_idx = match self.string_to_node.get(scope) {
216            None => {
217                return false;
218            }
219            Some(idx) => *idx,
220        };
221        let other_idx = match self.string_to_node.get(other) {
222            None => {
223                return false;
224            }
225            Some(idx) => *idx,
226        };
227
228        if scope_idx == self.root {
229            return true;
230        }
231
232        // if we can reach the other scope from the scope, then the scope
233        // grants the other scope
234        let mut dfs = Dfs::new(&self.graph, scope_idx);
235        while let Some(node) = dfs.next(&self.graph) {
236            if node == other_idx {
237                return true;
238            }
239        }
240        false
241    }
242
243    /// do any of the scopes in the list grant the other scope?
244    ///
245    /// # Example
246    /// ```
247    /// use chunkedge_command::CommandScopeRegistry;
248    ///
249    /// let mut registry = CommandScopeRegistry::new();
250    ///
251    /// registry.add_scope("chunkedge.command");
252    /// registry.add_scope("chunkedge.command.tp");
253    /// registry.add_scope("chunkedge.admin");
254    ///
255    /// assert!(registry.any_grants(
256    ///     &vec!["chunkedge.admin", "chunkedge.command"],
257    ///     "chunkedge.command.tp"
258    /// ));
259    /// ```
260    pub fn any_grants(&self, scopes: &Vec<&str>, other: &str) -> bool {
261        for scope in scopes {
262            if self.grants(scope, other) {
263                return true;
264            }
265        }
266        false
267    }
268
269    /// Create a link between two scopes so that one implies the other. It will
270    /// add them if they don't exist.
271    ///
272    /// # Example
273    /// ```
274    /// use chunkedge_command::CommandScopeRegistry;
275    ///
276    /// let mut registry = CommandScopeRegistry::new();
277    ///
278    /// registry.add_scope("chunkedge.command.tp");
279    ///
280    /// registry.link("chunkedge.admin", "chunkedge.command");
281    ///
282    /// assert!(registry.grants("chunkedge.admin", "chunkedge.command"));
283    /// assert!(registry.grants("chunkedge.admin", "chunkedge.command.tp"));
284    /// ```
285    pub fn link(&mut self, scope: &str, other: &str) {
286        self.add_scope(scope);
287        self.add_scope(other);
288
289        let scope_idx = self.string_to_node[scope];
290        let other_idx = self.string_to_node[other];
291
292        self.graph.add_edge(scope_idx, other_idx, ());
293    }
294
295    /// Get the number of scopes in the registry.
296    pub fn scope_count(&self) -> usize {
297        self.graph.node_count()
298    }
299}