steel_core/command/builtins/
invsee.rs1use std::{
2 array,
3 ops::Range,
4 sync::{Arc, Weak},
5};
6
7use steel_registry::vanilla_menu_types;
8use steel_utils::Identifier;
9use text_components::TextComponent;
10
11use super::super::{
12 brigadier::{CommandNodeBuilder, CommandSyntaxError},
13 execution::{
14 CommandPermissionSource, CommandSource, SteelArgumentType, SteelCommandRuntime, argument,
15 literal,
16 },
17 registration::{CommandRegistration, CommandRegistrationError},
18};
19use crate::entity::Entity;
20use crate::inventory::menu::Menu;
21use crate::inventory::prelude::*;
22use crate::inventory::slots::CraftingHandler;
23use crate::permission::{PermissionExpr, PermissionKey, PermissionKeyError};
24use crate::player::player_inventory::{PlayerInventory, armor_equipment};
25use crate::player::{Player, connection::NetworkConnection};
26
27const INVSEE_PERMISSION: &str = "steel.command.invsee";
28const MODIFY_PERMISSION: &str = "steel.command.invsee.modify";
29
30pub(super) fn registration() -> Result<CommandRegistration<CommandSource>, CommandRegistrationError>
31{
32 let id = Identifier::from_steel("invsee");
33 let (access_permission, modify_permission) = invsee_permissions().map_err(|source| {
34 CommandRegistrationError::InvalidExplicitPermission {
35 id: id.clone(),
36 source,
37 }
38 })?;
39 let command_modify = modify_permission.clone();
40 Ok(
41 CommandRegistration::new(id, move |_| command(command_modify))
42 .permission(access_permission),
43 )
44}
45
46fn invsee_permissions() -> Result<(PermissionExpr, PermissionExpr), PermissionKeyError> {
47 let access = PermissionExpr::key(PermissionKey::parse(INVSEE_PERMISSION)?);
48 let modify = PermissionExpr::key(PermissionKey::parse(MODIFY_PERMISSION)?);
49 Ok((PermissionExpr::Any(vec![access, modify.clone()]), modify))
50}
51
52fn command(
53 modify_permission: PermissionExpr,
54) -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
55 literal("invsee").then(
56 argument("target", SteelArgumentType::player()).executes(move |ctx| {
57 let target = ctx.player("target")?;
58 let Some(source) = ctx.source().player() else {
59 return Err(CommandSyntaxError::dynamic(TextComponent::const_plain(
60 "you cannot use this command from the console",
61 )));
62 };
63 ensure_same_domain(source, &target)?;
64 let modify = ctx.source().has_permission(&modify_permission);
68 let opener = Arc::clone(source);
69 let menu_source = Arc::clone(source);
70 opener.open_menu(target.display_name(), move |context| {
71 invsee(context.container_id, &menu_source, &target, modify)
72 });
73 Ok(1)
74 }),
75 )
76}
77
78fn ensure_same_domain(source: &Player, target: &Player) -> Result<(), CommandSyntaxError> {
79 if source.is_domain_switching() || target.is_domain_switching() {
80 return Err(CommandSyntaxError::dynamic(
81 "Invsee is unavailable while a player is switching domains",
82 ));
83 }
84 let source_world = source.get_world();
85 let target_world = target.get_world();
86 if source_world.domain() == target_world.domain() {
87 return Ok(());
88 }
89 Err(CommandSyntaxError::dynamic(
90 "Invsee cannot open inventories across Steel domains",
91 ))
92}
93
94fn invsee(container_id: u8, source: &Arc<Player>, target: &Arc<Player>, modify: bool) -> Menu {
95 let mut b = MenuBuilder::new(&vanilla_menu_types::GENERIC_9X5, container_id);
96
97 let kind = if modify {
98 SectionKind::Normal
99 } else {
100 SectionKind::Display
101 };
102
103 let target_inventory = b.player_inventory_with(&target.inventory, &kind);
104
105 let armor_kind = if modify {
106 SectionKind::restricted(|index, item| item.is_equippable_in_slot(armor_equipment(index)))
107 } else {
108 SectionKind::Display
109 };
110 let armor = b.section_at(
111 &target.inventory,
112 PlayerInventory::ARMOR_TOP_DOWN,
113 armor_kind,
114 );
115 let offhand = b.section_at(&target.inventory, [PlayerInventory::SLOT_OFFHAND], kind);
116
117 let crafting_handler = target.inventory_crafting_handler();
118 let crafting_container = crafting_handler.crafting_container();
119 let crafting = if modify {
120 b.section_all_with(crafting_container, SectionKind::take_only())
121 } else {
122 b.section_all_with(crafting_container, SectionKind::Display)
123 };
124 b.register_container(crafting_handler.result_container());
125
126 let target_slots = 0..b.slot_count();
127 let viewer = b.player_inventory(&source.inventory);
128
129 if modify {
130 let inventories_alias = Arc::ptr_eq(&source.inventory, &target.inventory);
131 if !inventories_alias {
132 b.route(
133 target_inventory.all(),
134 viewer.all(),
135 FillDirection::Backward,
136 );
137 b.route(
138 viewer.all(),
139 [target_inventory.all(), armor, offhand],
140 FillDirection::Forward,
141 );
142 }
143 b.route(
144 [armor, offhand, crafting],
145 viewer.all(),
146 FillDirection::Backward,
147 );
148 }
149
150 b.build(InvseeMenuKind {
151 target: Arc::downgrade(target),
152 target_inventory_id: ContainerId::from_arc(&target.inventory),
153 domain: target.get_world().domain().into(),
154 modify,
155 target_slots,
156 crafting,
157 crafting_handler,
158 inventory_before_click: None,
159 })
160}
161
162struct InvseeMenuKind {
163 target: Weak<Player>,
164 target_inventory_id: ContainerId,
165 domain: Box<str>,
166 modify: bool,
167 target_slots: Range<usize>,
168 crafting: Section,
169 crafting_handler: CraftingHandler,
170 inventory_before_click: Option<[ItemStack; PlayerInventory::SLOT_OFFHAND + 1]>,
171}
172
173unsafe impl steel_utils::DowncastType for InvseeMenuKind {
176 const TYPE_KEY: steel_utils::DowncastTypeKey =
177 steel_utils::DowncastTypeKey::new("steel:menu/invsee");
178}
179
180impl MenuKind for InvseeMenuKind {
181 fn on_drag(
182 &mut self,
183 _behavior: &mut MenuBehavior,
184 guard: &mut ContainerLockGuard,
185 _action: QuickCraft,
186 _player: &Player,
187 ) -> ClickOutcome {
188 self.snapshot_inventory_before_click(guard);
189 ClickOutcome::Fallthrough
190 }
191
192 fn on_open(
193 &mut self,
194 _behavior: &mut MenuBehavior,
195 guard: &mut ContainerLockGuard,
196 _player: &Player,
197 ) {
198 self.crafting_handler.update_result(guard);
199 }
200
201 fn slots_changed(
202 &mut self,
203 _behavior: &mut MenuBehavior,
204 guard: &mut ContainerLockGuard,
205 _player: &Player,
206 ) {
207 self.crafting_handler.update_result(guard);
208 self.queue_changed_target_inventory(guard);
209 }
210
211 fn on_slot_clicked(
212 &mut self,
213 _behavior: &mut MenuBehavior,
214 guard: &mut ContainerLockGuard,
215 click: Click,
216 _player: &Player,
217 ) -> ClickOutcome {
218 let Some(slot) = click.slot() else {
219 return ClickOutcome::Fallthrough;
220 };
221 self.snapshot_inventory_before_click(guard);
222 if (!self.modify && self.target_slots.contains(&slot))
223 || (self.crafting.contains(slot) && matches!(click, Click::Clone { .. }))
224 {
225 ClickOutcome::Consume
226 } else {
227 ClickOutcome::Fallthrough
228 }
229 }
230
231 fn can_drag_to(&self, slot_index: usize) -> bool {
232 if self.modify {
233 !self.crafting.contains(slot_index)
234 } else {
235 !self.target_slots.contains(&slot_index)
236 }
237 }
238
239 fn can_take_item_for_pick_all(&self, _carried: &ItemStack, slot_index: usize) -> bool {
240 self.modify || !self.target_slots.contains(&slot_index)
241 }
242
243 fn still_valid(&self, _behavior: &MenuBehavior, player: &Player) -> bool {
244 let Some(target) = self.target.upgrade() else {
245 return false;
246 };
247 let player_world = player.get_world();
248 let target_world = target.get_world();
249 !player.is_domain_switching()
250 && !target.connection.closed()
251 && !target.is_domain_switching()
252 && player_world.domain() == self.domain.as_ref()
253 && target_world.domain() == self.domain.as_ref()
254 }
255}
256
257impl InvseeMenuKind {
258 fn snapshot_inventory_before_click(&mut self, guard: &ContainerLockGuard) {
259 if !self.modify {
260 return;
261 }
262 let Some(inventory) = guard.get(self.target_inventory_id) else {
263 unreachable!("invsee always locks the target inventory");
264 };
265 self.inventory_before_click = Some(array::from_fn(|slot| inventory.get_item(slot).clone()));
266 }
267
268 fn queue_changed_target_inventory(&mut self, guard: &ContainerLockGuard) {
269 let Some(previous) = self.inventory_before_click.take() else {
270 return;
271 };
272 let Some(inventory) = guard.get(self.target_inventory_id) else {
273 unreachable!("invsee always locks the target inventory");
274 };
275 let changed_slots = previous
276 .iter()
277 .enumerate()
278 .filter_map(|(slot, previous)| {
279 let current = inventory.get_item(slot);
280 (!ItemStack::matches(previous, current)).then_some(slot)
281 })
282 .collect::<Vec<_>>();
283 if changed_slots.is_empty() {
284 return;
285 }
286 let Some(target) = self.target.upgrade() else {
287 return;
288 };
289 target.request_inventory_resync(changed_slots);
290 }
291}
292
293#[cfg(test)]
294mod tests;