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}