steel_core/command/builtins/
domain.rs1use std::sync::Arc;
4
5use steel_registry::{
6 data_components::vanilla_components::{CUSTOM_NAME, ENCHANTMENT_GLINT_OVERRIDE},
7 vanilla_dimension_types, vanilla_items, vanilla_menu_types,
8};
9use steel_utils::Identifier;
10use text_components::TextComponent;
11
12use crate::{inventory::prelude::*, server::Server, world::World};
13
14use super::super::{
15 brigadier::{CommandNodeBuilder, CommandSyntaxError},
16 execution::{
17 CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
18 literal,
19 },
20 registration::CommandRegistration,
21};
22
23pub(super) fn registration() -> CommandRegistration<CommandSource> {
24 CommandRegistration::new(Identifier::from_steel("domain"), |_| command())
25}
26
27fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
28 literal("domain")
29 .executes(|ctx: &SteelCommandContext<CommandSource>| {
30 let Some(player) = ctx.source().player() else {
31 return Err(CommandSyntaxError::dynamic(
32 "you cannot use this command from the console",
33 ));
34 };
35 let server = Arc::clone(ctx.source().server());
36 let player = Arc::clone(player);
37 let menu_player = Arc::clone(&player);
38
39 player.open_menu("Domains", move |context| {
40 domain_menu(context.container_id, menu_player, context.world, &server)
41 });
42
43 Ok(1)
44 })
45 .then(argument("world", SteelArgumentType::world()).executes(switch_world))
46}
47
48fn switch_world(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
49 let source = context.source();
50 let Some(player) = source.player() else {
51 return Err(CommandSyntaxError::dynamic(
52 "This command can only be used by a player",
53 ));
54 };
55 let Some(world) = context.world_argument("world") else {
56 return Err(CommandSyntaxError::dynamic(
57 "Parsed world is missing from the command context",
58 ));
59 };
60 let world = world.resolve(source)?;
61 source
62 .server()
63 .queue_player_world_selection(Arc::clone(player), Arc::clone(&world))
64 .map_err(CommandSyntaxError::dynamic)?;
65
66 source.send_success(
67 &TextComponent::plain(format!("Switching to world {}", world.key)),
68 true,
69 );
70 Ok(1)
71}
72
73fn domain_menu(
74 container_id: u8,
75 player: Arc<Player>,
76 current_world: &Arc<World>,
77 server: &Arc<Server>,
78) -> Menu {
79 let mut b = MenuBuilder::new(&vanilla_menu_types::GENERIC_9X6, container_id);
80
81 let domain_names: Vec<String> = server
82 .worlds
83 .domain_names()
84 .map(ToOwned::to_owned)
85 .collect();
86
87 let map: Vec<(Section, Vec<Arc<World>>)> = b.grid(6, |g| {
88 g.paint_all(ItemStack::empty());
89 g.paint(
90 Rect::cols(..).rows(0),
91 &vanilla_items::GRAY_STAINED_GLASS_PANE,
92 );
93 g.paint(
94 Rect::cols(..).rows(5),
95 &vanilla_items::GRAY_STAINED_GLASS_PANE,
96 );
97 g.paint(
98 Rect::cols(8).rows(..),
99 &vanilla_items::GRAY_STAINED_GLASS_PANE,
100 );
101 g.paint(
102 Rect::cols(0).rows(1..5),
103 &vanilla_items::GRAY_STAINED_GLASS_PANE,
104 );
105
106 domain_names
108 .iter()
109 .take(4)
110 .enumerate()
111 .map(|(i, domain_name)| {
112 g.subgrid(Rect::cols(..8).rows(i + 1), |g| {
113 g.paint_all(ItemStack::empty());
114
115 let mut sign = ItemStack::new(&vanilla_items::OAK_SIGN);
116 sign.set(CUSTOM_NAME, domain_name.clone().into());
117
118 g.paint(Rect::cell(0, 0), sign);
119
120 let worlds = server.worlds.worlds_in_domain(domain_name);
121
122 let icons: Vec<ItemStack> =
123 worlds.iter().map(|w| icon(w, current_world)).collect();
124
125 let len = icons.len();
126
127 let container = SimpleContainer::from_items(icons).into_shared();
128
129 (
130 g.place(Rect::cols(1..(len + 1).min(6)).rows(0), container)
131 .display()
132 .section(),
133 worlds,
134 )
135 })
136 })
137 .collect()
138 });
139 b.player_inventory(&player.inventory);
140
141 b.build(DomainMenuKind {
142 map,
143 server: server.clone(),
144 player,
145 })
146}
147
148fn icon(world: &Arc<World>, current_world: &Arc<World>) -> ItemStack {
149 let item = match world.dimension_type {
150 b if b == &vanilla_dimension_types::OVERWORLD
151 || b == &vanilla_dimension_types::OVERWORLD_CAVES =>
152 {
153 &vanilla_items::GRASS_BLOCK
154 }
155 b if b == &vanilla_dimension_types::THE_NETHER => &vanilla_items::NETHERRACK,
156 b if b == &vanilla_dimension_types::THE_END => &vanilla_items::END_STONE,
157 _ => &vanilla_items::BEDROCK,
158 };
159 let mut icon = ItemStack::new(item);
160 icon.set(CUSTOM_NAME, world.key.path.to_string().into());
161 if world.key == current_world.key {
162 icon.set(ENCHANTMENT_GLINT_OVERRIDE, true);
163 }
164 icon
165}
166
167struct DomainMenuKind {
168 map: Vec<(Section, Vec<Arc<World>>)>,
169 server: Arc<Server>,
170 player: Arc<Player>,
171}
172
173unsafe impl steel_utils::DowncastType for DomainMenuKind {
176 const TYPE_KEY: steel_utils::DowncastTypeKey =
177 steel_utils::DowncastTypeKey::new("steel:menu/domain");
178}
179
180impl MenuKind for DomainMenuKind {
181 fn on_slot_clicked(
182 &mut self,
183 _behavior: &mut MenuBehavior,
184 _guard: &mut ContainerLockGuard,
185 click: Click,
186 _player: &Player,
187 ) -> ClickOutcome {
188 let Some(index) = click.slot() else {
189 return ClickOutcome::Fallthrough;
190 };
191
192 let Some((section, worlds)) = self
193 .map
194 .iter()
195 .find(|(section, _worlds)| section.contains(index))
196 else {
197 return ClickOutcome::Fallthrough;
198 };
199
200 if worlds.is_empty() {
201 return ClickOutcome::Consume;
202 }
203
204 let Some(world) = worlds.get(index - section.start()) else {
205 return ClickOutcome::Consume;
206 };
207
208 if !matches!(click, Click::Pickup { .. }) {
209 return ClickOutcome::Consume;
210 }
211
212 if let Err(error) = self
213 .server
214 .queue_player_world_selection(Arc::clone(&self.player), Arc::clone(world))
215 {
216 tracing::debug!(%error, target_world = %world.key, "domain menu selection was rejected");
217 }
218 self.player.close_container();
219 ClickOutcome::Consume
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::super::create_dispatcher;
226 use crate::command::{
227 brigadier::{CommandDispatcher, NodeId},
228 execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
229 };
230 use steel_registry::init_vanilla_registry;
231
232 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
233
234 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
235 let Some(children) = dispatcher.children(parent) else {
236 panic!("parent node should exist");
237 };
238 let Some(child) = children.iter().copied().find(|child| {
239 dispatcher
240 .node(*child)
241 .is_some_and(|node| node.name() == name)
242 }) else {
243 panic!("child {name} should exist");
244 };
245 child
246 }
247
248 #[test]
249 fn domain_graph_uses_the_loaded_world_argument() {
250 init_vanilla_registry();
251 let Ok(dispatcher) = create_dispatcher() else {
252 panic!("built-in commands should register");
253 };
254 let root = child(&dispatcher, dispatcher.root(), "domain");
255 let world = child(&dispatcher, root, "world");
256 assert_eq!(
257 dispatcher.node(world).and_then(|node| node.argument_type()),
258 Some(&SteelArgumentType::world())
259 );
260 let Some(world) = dispatcher.node(world) else {
261 panic!("world argument should exist");
262 };
263 assert!(world.is_executable());
264 }
265}