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