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