1use std::array::IntoIter;
28use std::fmt;
29use std::iter;
30use std::range::Range;
31use std::slice;
32use std::sync::atomic::{AtomicU64, Ordering};
33use std::sync::{Arc, OnceLock};
34use std::vec;
35
36use steel_registry::{item_stack::ItemStack, menu_type::MenuTypeRef};
37use steel_utils::locks::Shared;
38
39use crate::inventory::menu::Menu;
40use crate::inventory::menu::behavior::MenuBehavior;
41use crate::inventory::menu::kind::MenuKind;
42use crate::inventory::menu::layout::MenuLayout;
43use crate::inventory::{
44 lock::{ContainerId, ContainerLockGuard, ContainerRef},
45 slots::{NormalSlot, RestrictedRules, RestrictedSlot, ResultHandler, ResultSlot, Slot},
46};
47use crate::player::Player;
48use crate::player::player_inventory::PlayerInventory;
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub(crate) struct MenuInstanceId(u64);
56
57impl MenuInstanceId {
58 fn next() -> Self {
60 static NEXT: AtomicU64 = AtomicU64::new(0);
61 Self(NEXT.fetch_add(1, Ordering::Relaxed))
62 }
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct Section {
72 menu: MenuInstanceId,
73 range: Range<usize>,
74}
75
76impl Section {
77 pub(crate) fn new(menu: MenuInstanceId, range: impl Into<Range<usize>>) -> Self {
78 Self {
79 menu,
80 range: range.into(),
81 }
82 }
83
84 #[must_use]
86 pub const fn start(self) -> usize {
87 self.range.start
88 }
89
90 #[must_use]
92 pub const fn end(self) -> usize {
93 self.range.end
94 }
95
96 #[must_use]
98 pub const fn len(self) -> usize {
99 self.range.end - self.range.start
100 }
101
102 #[must_use]
104 pub const fn is_empty(self) -> bool {
105 self.range.start == self.range.end
106 }
107
108 #[must_use]
110 pub const fn contains(self, slot_index: usize) -> bool {
111 slot_index >= self.range.start && slot_index < self.range.end
112 }
113
114 #[must_use]
116 pub const fn range(self) -> Range<usize> {
117 self.range
118 }
119}
120
121pub trait IntoSections {
123 type Iter: Iterator<Item = Section>;
125
126 fn into_sections(self) -> Self::Iter;
128}
129
130impl IntoSections for Section {
131 type Iter = iter::Once<Section>;
132
133 fn into_sections(self) -> Self::Iter {
134 iter::once(self)
135 }
136}
137
138impl<const N: usize> IntoSections for [Section; N] {
139 type Iter = IntoIter<Section, N>;
140
141 fn into_sections(self) -> Self::Iter {
142 self.into_iter()
143 }
144}
145
146impl<'a> IntoSections for &'a [Section] {
147 type Iter = iter::Copied<slice::Iter<'a, Section>>;
148
149 fn into_sections(self) -> Self::Iter {
150 self.iter().copied()
151 }
152}
153
154impl IntoSections for Vec<Section> {
155 type Iter = vec::IntoIter<Section>;
156
157 fn into_sections(self) -> Self::Iter {
158 self.into_iter()
159 }
160}
161
162#[derive(Clone, Copy, Debug)]
166pub struct PlayerInventorySections {
167 all: Section,
169 main: Section,
171 hotbar: Section,
173}
174
175impl PlayerInventorySections {
176 #[must_use]
178 pub const fn all(&self) -> Section {
179 self.all
180 }
181
182 #[must_use]
184 pub const fn main(&self) -> Section {
185 self.main
186 }
187
188 #[must_use]
190 pub const fn hotbar(&self) -> Section {
191 self.hotbar
192 }
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub struct DataSlot {
199 menu: MenuInstanceId,
200 index: usize,
201}
202
203impl DataSlot {
204 #[must_use]
210 pub fn get(self, behavior: &MenuBehavior) -> i16 {
211 assert_eq!(
212 self.menu,
213 behavior.instance(),
214 "DataSlot used with a MenuBehavior it does not belong to"
215 );
216 behavior
217 .get_data(self.index)
218 .expect("DataSlot index is always valid for its own menu")
219 }
220
221 pub fn set(self, behavior: &mut MenuBehavior, value: i16) {
227 assert_eq!(
228 self.menu,
229 behavior.instance(),
230 "DataSlot used with a MenuBehavior it does not belong to"
231 );
232 behavior.set_data(self.index, value);
233 }
234
235 #[must_use]
237 pub const fn index(self) -> usize {
238 self.index
239 }
240}
241
242pub struct ContainerSlots {
248 container: ContainerRef,
250 next: usize,
252 size: usize,
255}
256
257impl fmt::Debug for ContainerSlots {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 f.debug_struct("ContainerSlots")
260 .field("next", &self.next)
261 .field("size", &self.size)
262 .finish_non_exhaustive()
263 }
264}
265
266pub trait SectionSource {
269 fn take(self, count: usize) -> (ContainerRef, Range<usize>);
271}
272
273impl<T: Into<ContainerRef>> SectionSource for T {
274 fn take(self, count: usize) -> (ContainerRef, Range<usize>) {
275 (self.into(), (0..count).into())
276 }
277}
278
279impl SectionSource for &mut ContainerSlots {
280 fn take(self, count: usize) -> (ContainerRef, Range<usize>) {
283 let start = self.next;
284 assert!(
285 start + count <= self.size,
286 "section takes container slots {}..{}, but the container only has {} slots",
287 start,
288 start + count,
289 self.size
290 );
291 self.next = start + count;
292 (self.container.clone(), (start..start + count).into())
293 }
294}
295
296pub type SlotFactory = Arc<dyn Fn(&ContainerRef, usize) -> Box<dyn Slot> + Send + Sync>;
298
299#[derive(Clone)]
306#[non_exhaustive]
307pub enum SectionKind {
308 Normal,
310 Restricted(Arc<RestrictedRules>),
314 Display,
317 Custom(SlotFactory),
319}
320
321impl SectionKind {
322 pub fn restricted(
325 may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
326 ) -> Self {
327 Self::Restricted(RestrictedRules::place_only(may_place))
328 }
329
330 pub fn guarded(
333 may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
334 may_pickup: impl Fn(usize, &ItemStack, &ContainerLockGuard, &Player) -> bool
335 + Send
336 + Sync
337 + 'static,
338 ) -> Self {
339 Self::Restricted(RestrictedRules::guarded(may_place, may_pickup))
340 }
341
342 pub fn custom(
345 factory: impl Fn(&ContainerRef, usize) -> Box<dyn Slot> + Send + Sync + 'static,
346 ) -> Self {
347 Self::Custom(Arc::new(factory))
348 }
349
350 #[must_use]
353 pub fn take_only() -> Self {
354 Self::Restricted(deny_place_rules())
355 }
356
357 pub(crate) fn make(&self, container: &ContainerRef, index: usize) -> Box<dyn Slot> {
358 match self {
359 Self::Normal => Box::new(NormalSlot::new(container.clone(), index)),
360 Self::Restricted(rules) => Box::new(RestrictedSlot::with_rules(
361 container.clone(),
362 index,
363 Arc::clone(rules),
364 )),
365 Self::Display => Box::new(RestrictedSlot::with_rules(
366 container.clone(),
367 index,
368 deny_all_rules(),
369 )),
370 Self::Custom(factory) => factory(container, index),
371 }
372 }
373}
374
375impl fmt::Debug for SectionKind {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 f.write_str(match self {
378 Self::Normal => "SectionKind::Normal",
379 Self::Restricted(_) => "SectionKind::Restricted(..)",
380 Self::Display => "SectionKind::Display",
381 Self::Custom(_) => "SectionKind::Custom(..)",
382 })
383 }
384}
385
386impl From<&Self> for SectionKind {
387 fn from(kind: &Self) -> Self {
388 kind.clone()
389 }
390}
391
392fn deny_place_rules() -> Arc<RestrictedRules> {
394 static DENY: OnceLock<Arc<RestrictedRules>> = OnceLock::new();
395 Arc::clone(DENY.get_or_init(|| RestrictedRules::place_only(|_, _| false)))
396}
397
398fn deny_all_rules() -> Arc<RestrictedRules> {
400 static DENY: OnceLock<Arc<RestrictedRules>> = OnceLock::new();
401 Arc::clone(DENY.get_or_init(|| RestrictedRules::guarded(|_, _| false, |_, _, _, _| false)))
402}
403
404#[derive(Clone, Copy, Debug, PartialEq, Eq)]
409pub enum FillDirection {
410 Forward,
412 Backward,
414}
415
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
418pub enum FakeResultRemainderPolicy {
419 Drop,
421 Discard,
423}
424
425pub(crate) struct Route {
427 pub(crate) from: Range<usize>,
428 pub(crate) targets: Vec<Range<usize>>,
429 pub(crate) direction: FillDirection,
430 pub(crate) fake_result_remainder: FakeResultRemainderPolicy,
431}
432
433pub struct MenuBuilder {
437 instance: MenuInstanceId,
438 menu_type: Option<MenuTypeRef>,
439 container_id: u8,
440 overrides_player_slots: bool,
441 slots: Vec<Box<dyn Slot>>,
442 container_refs: Vec<ContainerRef>,
443 data_slots: Vec<i16>,
444 routes: Vec<Route>,
445 drain_sections: Vec<Range<usize>>,
446 claimed: Vec<(ContainerId, Range<usize>)>,
449}
450
451impl fmt::Debug for MenuBuilder {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 f.debug_struct("MenuBuilder")
454 .field("instance", &self.instance)
455 .field("container_id", &self.container_id)
456 .field("slots", &self.slots.len())
457 .field("routes", &self.routes.len())
458 .finish_non_exhaustive()
459 }
460}
461
462impl MenuBuilder {
463 #[must_use]
468 pub fn new(menu_type: impl Into<Option<MenuTypeRef>>, container_id: u8) -> Self {
469 Self {
470 instance: MenuInstanceId::next(),
471 menu_type: menu_type.into(),
472 container_id,
473 overrides_player_slots: false,
474 slots: Vec::new(),
475 container_refs: Vec::new(),
476 data_slots: Vec::new(),
477 routes: Vec::new(),
478 drain_sections: Vec::new(),
479 claimed: Vec::new(),
480 }
481 }
482
483 #[must_use]
507 #[expect(
508 clippy::unused_self,
509 reason = "split is intentionally builder-scoped as part of the menu DSL"
510 )]
511 pub fn split(&mut self, container: impl Into<ContainerRef>) -> ContainerSlots {
512 let container = container.into();
513 let size = Self::container_size(&container);
514 ContainerSlots {
515 container,
516 next: 0,
517 size,
518 }
519 }
520
521 fn container_size(container: &ContainerRef) -> usize {
523 ContainerLockGuard::lock_all(slice::from_ref(container))
524 .get(container.container_id())
525 .expect("container was just locked")
526 .get_container_size()
527 }
528
529 pub fn section(&mut self, source: impl SectionSource, count: usize) -> Section {
541 self.section_with(source, count, SectionKind::Normal)
542 }
543
544 pub fn section_with(
566 &mut self,
567 source: impl SectionSource,
568 count: usize,
569 kind: impl Into<SectionKind>,
570 ) -> Section {
571 let kind = kind.into();
572 let (container, range) = source.take(count);
573 self.claim(&container, range);
574 let start = self.slots.len();
575 for index in range {
576 let slot = kind.make(&container, index);
577 self.push_section_slot(slot, &container, index);
578 }
579 self.section_from(start)
580 }
581
582 pub fn section_all(&mut self, container: impl Into<ContainerRef>) -> Section {
592 self.section_all_with(container, SectionKind::Normal)
593 }
594
595 pub fn section_all_with(
601 &mut self,
602 container: impl Into<ContainerRef>,
603 kind: impl Into<SectionKind>,
604 ) -> Section {
605 let container = container.into();
606 let size = Self::container_size(&container);
607 self.section_with(container, size, kind)
608 }
609
610 pub fn section_at(
620 &mut self,
621 container: impl Into<ContainerRef>,
622 indices: impl IntoIterator<Item = usize>,
623 kind: impl Into<SectionKind>,
624 ) -> Section {
625 let kind = kind.into();
626 let container = container.into();
627 let start = self.slots.len();
628 let mut run: Option<Range<usize>> = None;
629 for index in indices {
630 match &mut run {
631 Some(r) if index == r.end => r.end += 1,
632 _ => {
633 if let Some(r) = run.take() {
634 self.claim(&container, r);
635 }
636 run = Some((index..index + 1).into());
637 }
638 }
639 let slot = kind.make(&container, index);
640 self.push_section_slot(slot, &container, index);
641 }
642 if let Some(r) = run {
643 self.claim(&container, r);
644 }
645 self.section_from(start)
646 }
647
648 pub fn player_inventory(
650 &mut self,
651 inventory: &Shared<PlayerInventory>,
652 ) -> PlayerInventorySections {
653 self.player_inventory_with(inventory, SectionKind::Normal)
654 }
655
656 pub fn player_inventory_with(
664 &mut self,
665 inventory: &Shared<PlayerInventory>,
666 kind: impl Into<SectionKind>,
667 ) -> PlayerInventorySections {
668 let kind = kind.into();
669 let container = ContainerRef::from(Arc::clone(inventory));
670 let start = self.slots.len();
671 for index in PlayerInventory::MAIN.chain(PlayerInventory::HOTBAR) {
672 let slot = kind.make(&container, index);
673 self.push_section_slot(slot, &container, index);
674 }
675
676 let main = Section::new(self.instance, start..start + PlayerInventory::MAIN.len());
677 let hotbar = Section::new(
678 self.instance,
679 start + PlayerInventory::MAIN.len()..self.slots.len(),
680 );
681 let all = Section::new(self.instance, start..self.slots.len());
682 PlayerInventorySections { all, main, hotbar }
683 }
684
685 pub fn result_slot(&mut self, handler: impl ResultHandler + 'static) -> Section {
697 self.add_result_slot(ResultSlot::new(handler))
698 }
699
700 pub fn crafting_result_slot(&mut self, handler: impl ResultHandler + 'static) -> Section {
706 self.add_result_slot(ResultSlot::crafting(handler))
707 }
708
709 fn add_result_slot(&mut self, slot: ResultSlot) -> Section {
710 let container = slot.result_container().clone();
711 self.claim(&container, (0..1).into());
712 let start = self.slots.len();
713 self.push_section_slot(Box::new(slot), &container, 0);
714 self.section_from(start)
715 }
716
717 #[cfg(test)]
728 pub(crate) fn custom_section<S>(&mut self, slots: impl IntoIterator<Item = S>) -> Section
729 where
730 S: Slot + 'static,
731 {
732 self.custom_boxed_section(
733 slots
734 .into_iter()
735 .map(|slot| Box::new(slot) as Box<dyn Slot>),
736 )
737 }
738
739 #[cfg(test)]
741 pub(crate) fn custom_boxed_section(
742 &mut self,
743 slots: impl IntoIterator<Item = Box<dyn Slot>>,
744 ) -> Section {
745 let start = self.slots.len();
746 for slot in slots {
747 self.push_boxed_slot(slot);
748 }
749 self.section_from(start)
750 }
751
752 pub fn data_slot(&mut self, initial: i16) -> DataSlot {
754 let index = self.data_slots.len();
755 self.data_slots.push(initial);
756 DataSlot {
757 menu: self.instance,
758 index,
759 }
760 }
761
762 pub fn route(
778 &mut self,
779 from: impl IntoSections,
780 targets: impl IntoSections,
781 direction: FillDirection,
782 ) -> &mut Self {
783 self.route_with_remainder_policy(from, targets, direction, FakeResultRemainderPolicy::Drop)
784 }
785
786 pub fn route_with_remainder_policy(
793 &mut self,
794 from: impl IntoSections,
795 targets: impl IntoSections,
796 direction: FillDirection,
797 fake_result_remainder: FakeResultRemainderPolicy,
798 ) -> &mut Self {
799 let targets: Vec<Range<usize>> = targets.into_sections().map(|s| self.owned(s)).collect();
800 for from in from.into_sections() {
801 let from = self.owned(from);
802 assert!(
803 !self
804 .routes
805 .iter()
806 .any(|route| route.from.start < from.end && from.start < route.from.end),
807 "shift-click route source {from:?} overlaps an existing route source",
808 );
809 assert!(
810 !targets
811 .iter()
812 .any(|t| t.start < from.end && from.start < t.end),
813 "shift-click route target {targets:?} overlaps its own source {from:?}",
814 );
815 self.routes.push(Route {
816 from,
817 targets: targets.clone(),
818 direction,
819 fake_result_remainder,
820 });
821 }
822 self
823 }
824
825 pub fn drain(&mut self, sections: impl IntoSections) -> &mut Self {
861 let ranges: Vec<_> = sections.into_sections().map(|s| self.owned(s)).collect();
862 assert!(
863 ranges
864 .iter()
865 .flat_map(|range| *range)
866 .all(|slot| !self.slots[slot].is_fake()),
867 "drain sections cannot contain fake or result slots"
868 );
869 self.drain_sections.extend(ranges);
870 self
871 }
872
873 pub const fn override_player_slots(&mut self) -> &mut Self {
878 self.overrides_player_slots = true;
879 self
880 }
881
882 #[must_use]
888 pub fn build(self, kind: impl MenuKind + 'static) -> Menu {
889 self.build_boxed(Box::new(kind))
890 }
891
892 #[must_use]
901 pub fn build_boxed(self, kind: Box<dyn MenuKind>) -> Menu {
902 if let Some(menu_type) = self.menu_type {
903 assert_eq!(
904 self.slots.len(),
905 menu_type.slot_count,
906 "menu type {} expects {} slots, but the builder has {}",
907 menu_type.key,
908 menu_type.slot_count,
909 self.slots.len(),
910 );
911 }
912 Self::assert_no_fake_slot_aliases(&self.slots);
913
914 let mut behavior = MenuBehavior::new(
915 self.instance,
916 self.slots,
917 self.container_id,
918 self.menu_type,
919 self.container_refs,
920 );
921 for initial in self.data_slots {
922 behavior.add_data_slot(initial);
923 }
924
925 let layout = MenuLayout {
926 routes: self.routes,
927 drain_sections: self.drain_sections,
928 };
929 Menu::from_parts(behavior, layout, kind, self.overrides_player_slots)
930 }
931
932 fn assert_no_fake_slot_aliases(slots: &[Box<dyn Slot>]) {
935 use rustc_hash::FxHashMap;
936
937 let mut physical_slots: FxHashMap<(ContainerId, usize), (usize, bool)> =
938 FxHashMap::default();
939 for (slot_index, slot) in slots.iter().enumerate() {
940 let Some(key) = slot.storage().physical_key() else {
941 continue;
942 };
943 let is_fake = slot.is_fake();
944 if let Some(&(other_index, other_is_fake)) = physical_slots.get(&key) {
945 assert!(
946 !is_fake && !other_is_fake,
947 "menu slots {other_index} and {slot_index} alias physical container slot \
948 {key:?}, but fake slots require exclusive backing storage"
949 );
950 } else {
951 physical_slots.insert(key, (slot_index, is_fake));
952 }
953 }
954 }
955
956 pub(crate) const fn instance(&self) -> MenuInstanceId {
958 self.instance
959 }
960
961 #[must_use]
963 pub const fn slot_count(&self) -> usize {
964 self.slots.len()
965 }
966
967 pub(crate) fn push_boxed_slot(&mut self, slot: Box<dyn Slot>) {
969 for container in slot.storage().container_refs() {
970 self.register_container(container.clone());
971 }
972 self.slots.push(slot);
973 }
974
975 pub(crate) fn push_section_slot(
977 &mut self,
978 slot: Box<dyn Slot>,
979 source: &ContainerRef,
980 source_index: usize,
981 ) {
982 assert_eq!(
983 slot.storage().physical_key(),
984 Some((source.container_id(), source_index)),
985 "section slot backing must match its declared source container and index"
986 );
987 self.push_boxed_slot(slot);
988 }
989
990 pub(crate) fn claim(&mut self, container: &ContainerRef, range: Range<usize>) {
995 let id = container.container_id();
996 let size = {
997 let guard = ContainerLockGuard::lock_all(slice::from_ref(container));
998 let Some(container) = guard.get(id) else {
999 panic!("container was not locked while validating a menu section");
1000 };
1001 container.get_container_size()
1002 };
1003 assert!(
1004 range.end <= size,
1005 "section takes container slots {}..{}, but the container only has {size} slots",
1006 range.start,
1007 range.end,
1008 );
1009 for (other_id, other) in &self.claimed {
1010 assert!(
1011 *other_id != id || range.start >= other.end || other.start >= range.end,
1012 "two sections cover overlapping slots ({other:?} and {range:?}) of the same \
1013 container; carve shared containers with MenuBuilder::split"
1014 );
1015 }
1016 self.claimed.push((id, range));
1017 }
1018
1019 pub(crate) fn register_container(&mut self, container: impl Into<ContainerRef>) {
1021 let container_ref = container.into();
1022 let id = container_ref.container_id();
1023 if !self.container_refs.iter().any(|c| c.container_id() == id) {
1024 self.container_refs.push(container_ref);
1025 }
1026 }
1027
1028 fn owned(&self, section: Section) -> Range<usize> {
1030 assert_eq!(
1031 section.menu, self.instance,
1032 "Section was minted by a different MenuBuilder"
1033 );
1034 section.range()
1035 }
1036
1037 fn section_from(&self, start: usize) -> Section {
1039 Section::new(self.instance, start..self.slots.len())
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use steel_registry::{init_vanilla_registry, vanilla_items, vanilla_menu_types};
1046 use steel_utils::{Downcast as _, locks::IntoShared};
1047
1048 use super::*;
1049 use crate::inventory::container::SimpleContainer;
1050 use crate::inventory::menu::kinds::BasicKind;
1051
1052 struct NoopResultHandler(ContainerRef);
1053
1054 impl ResultHandler for NoopResultHandler {
1055 fn result_container(&self) -> ContainerRef {
1056 self.0.clone()
1057 }
1058
1059 fn dependencies(&self) -> Vec<ContainerRef> {
1060 Vec::new()
1061 }
1062
1063 fn update_result(&self, _guard: &mut ContainerLockGuard) {}
1064
1065 fn on_result_taken(
1066 &self,
1067 _guard: &mut ContainerLockGuard,
1068 _player: &Player,
1069 ) -> Option<ItemStack> {
1070 None
1071 }
1072
1073 fn is_result_valid(&self, _guard: &ContainerLockGuard, _player: &Player) -> bool {
1074 true
1075 }
1076 }
1077
1078 struct DependencyResultHandler {
1079 result: ContainerRef,
1080 dependency: ContainerRef,
1081 }
1082
1083 impl ResultHandler for DependencyResultHandler {
1084 fn result_container(&self) -> ContainerRef {
1085 self.result.clone()
1086 }
1087
1088 fn dependencies(&self) -> Vec<ContainerRef> {
1089 vec![self.dependency.clone()]
1090 }
1091
1092 fn update_result(&self, _guard: &mut ContainerLockGuard) {}
1093
1094 fn on_result_taken(
1095 &self,
1096 _guard: &mut ContainerLockGuard,
1097 _player: &Player,
1098 ) -> Option<ItemStack> {
1099 None
1100 }
1101
1102 fn is_result_valid(&self, _guard: &ContainerLockGuard, _player: &Player) -> bool {
1103 true
1104 }
1105 }
1106
1107 #[test]
1108 #[should_panic(
1109 expected = "menu type minecraft:generic_9x6 expects 90 slots, but the builder has 0"
1110 )]
1111 fn build_rejects_a_slot_count_that_disagrees_with_the_menu_type() {
1112 let _ = MenuBuilder::new(&vanilla_menu_types::GENERIC_9X6, 1).build(BasicKind);
1113 }
1114
1115 #[test]
1116 fn builds_with_an_erased_menu_kind() {
1117 let kind: Box<dyn MenuKind> = Box::new(BasicKind);
1118 let menu = MenuBuilder::new(None, 0).build_boxed(kind);
1119
1120 assert!(menu.kind().downcast_ref::<BasicKind>().is_some());
1121 }
1122
1123 #[test]
1124 #[should_panic(
1125 expected = "section takes container slots 0..2, but the container only has 1 slots"
1126 )]
1127 fn direct_section_rejects_a_range_past_container_capacity() {
1128 let mut builder = MenuBuilder::new(None, 0);
1129 builder.section(SimpleContainer::new(1).into_shared(), 2);
1130 }
1131
1132 #[test]
1133 #[should_panic(
1134 expected = "section takes container slots 0..1, but the container only has 0 slots"
1135 )]
1136 fn result_slot_rejects_a_container_without_slot_zero() {
1137 let container = ContainerRef::from(SimpleContainer::new(0).into_shared());
1138 let mut builder = MenuBuilder::new(None, 0);
1139
1140 let _ = builder.result_slot(NoopResultHandler(container));
1141 }
1142
1143 #[test]
1144 #[should_panic(expected = "two sections cover overlapping slots")]
1145 fn result_slot_claims_slot_zero_against_normal_sections() {
1146 let container = ContainerRef::from(SimpleContainer::new(1).into_shared());
1147 let mut builder = MenuBuilder::new(None, 0);
1148 let _ = builder.result_slot(NoopResultHandler(container.clone()));
1149
1150 let _ = builder.section_all(container);
1151 }
1152
1153 #[test]
1154 fn result_slot_registers_handler_dependencies() {
1155 let result = ContainerRef::from(SimpleContainer::new(1).into_shared());
1156 let dependency = ContainerRef::from(SimpleContainer::new(1).into_shared());
1157 let dependency_id = dependency.container_id();
1158 let mut builder = MenuBuilder::new(None, 0);
1159 let _ = builder.result_slot(DependencyResultHandler { result, dependency });
1160
1161 let menu = builder.build(BasicKind);
1162 let guard = menu.behavior().lock_all_containers();
1163
1164 assert!(guard.contains(dependency_id));
1165 }
1166
1167 #[test]
1168 #[should_panic(expected = "drain sections cannot contain fake or result slots")]
1169 fn drain_rejects_a_result_slot() {
1170 let result = ContainerRef::from(SimpleContainer::new(1).into_shared());
1171 let mut builder = MenuBuilder::new(None, 0);
1172 let section = builder.result_slot(NoopResultHandler(result));
1173
1174 builder.drain(section);
1175 }
1176
1177 #[test]
1178 #[should_panic(expected = "fake slots require exclusive backing storage")]
1179 fn build_rejects_a_result_alias_through_player_inventory() {
1180 let inventory = PlayerInventory::new().into_shared();
1181 let mut builder = MenuBuilder::new(None, 0);
1182 let _ = builder.result_slot(NoopResultHandler(ContainerRef::from(Arc::clone(
1183 &inventory,
1184 ))));
1185 let _ = builder.player_inventory_with(&inventory, SectionKind::Normal);
1186
1187 let _ = builder.build(BasicKind);
1188 }
1189
1190 #[test]
1191 fn build_allows_non_fake_player_inventory_aliases() {
1192 let inventory = PlayerInventory::new().into_shared();
1193 let mut builder = MenuBuilder::new(None, 0);
1194 let _ = builder.player_inventory(&inventory);
1195 let _ = builder.player_inventory_with(&inventory, SectionKind::Display);
1196
1197 let _ = builder.build(BasicKind);
1198 }
1199
1200 #[test]
1201 #[should_panic(expected = "shift-click route source 0..27 overlaps an existing route source")]
1202 fn route_rejects_overlapping_source_sections() {
1203 let inventory = PlayerInventory::new().into_shared();
1204 let mut builder = MenuBuilder::new(None, 0);
1205 let player = builder.player_inventory(&inventory);
1206 let target = builder.section(SimpleContainer::new(1).into_shared(), 1);
1207
1208 builder.route(player.all(), [target], FillDirection::Forward);
1209 builder.route(player.main(), [target], FillDirection::Forward);
1210 }
1211
1212 #[test]
1213 fn section_at_preserves_the_given_index_order() {
1214 let container = ContainerRef::from(SimpleContainer::new(5).into_shared());
1215 let mut b = MenuBuilder::new(None, 0);
1216 let section = b.section_at(container, [4, 3, 0, 1], SectionKind::Normal);
1217 let menu = b.build(BasicKind);
1218
1219 assert_eq!((section.start(), section.end()), (0, 4));
1220 let container_slots: Vec<usize> = menu
1221 .behavior()
1222 .slots()
1223 .iter()
1224 .map(|slot| slot.get_container_slot())
1225 .collect();
1226 assert_eq!(container_slots, vec![4, 3, 0, 1]);
1227 }
1228
1229 #[test]
1230 #[should_panic(expected = "two sections cover overlapping slots")]
1231 fn section_at_rejects_indices_claimed_by_another_section() {
1232 let container = SimpleContainer::new(4).into_shared();
1233 let mut b = MenuBuilder::new(None, 0);
1234 let _ = b.section(Arc::clone(&container), 2);
1235 let _ = b.section_at(container, [1], SectionKind::Normal);
1236 }
1237
1238 #[test]
1239 #[should_panic(expected = "two sections cover overlapping slots")]
1240 fn section_at_rejects_a_repeated_index() {
1241 let container = ContainerRef::from(SimpleContainer::new(4).into_shared());
1242 let mut b = MenuBuilder::new(None, 0);
1243 let _ = b.section_at(container, [0, 2, 0], SectionKind::Normal);
1244 }
1245
1246 #[test]
1247 fn display_kind_rejects_placement() {
1248 init_vanilla_registry();
1249 let container = ContainerRef::from(SimpleContainer::new(1).into_shared());
1250 let mut b = MenuBuilder::new(None, 0);
1251 let _ = b.section_at(container, [0], SectionKind::Display);
1252 let menu = b.build(BasicKind);
1253
1254 let stack = ItemStack::new(&vanilla_items::STONE);
1255 assert!(!menu.behavior().slots()[0].may_place(&stack));
1256 }
1257
1258 #[test]
1259 fn custom_kind_lowers_through_the_factory() {
1260 let container = ContainerRef::from(SimpleContainer::new(3).into_shared());
1261 let factory = SectionKind::custom(|container, index| {
1262 Box::new(NormalSlot::new(container.clone(), index))
1263 });
1264 let mut b = MenuBuilder::new(None, 0);
1265 let _ = b.section_at(container, [2, 0], factory);
1266 let menu = b.build(BasicKind);
1267
1268 let container_slots: Vec<usize> = menu
1269 .behavior()
1270 .slots()
1271 .iter()
1272 .map(|slot| slot.get_container_slot())
1273 .collect();
1274 assert_eq!(container_slots, vec![2, 0]);
1275 }
1276
1277 #[test]
1278 #[should_panic(expected = "section slot backing must match its declared source")]
1279 fn custom_kind_rejects_a_mismatched_physical_backing() {
1280 let source = ContainerRef::from(SimpleContainer::new(1).into_shared());
1281 let other = ContainerRef::from(SimpleContainer::new(1).into_shared());
1282 let kind =
1283 SectionKind::custom(move |_container, _index| Box::new(NormalSlot::new(&other, 0)));
1284 let mut builder = MenuBuilder::new(None, 0);
1285
1286 let _ = builder.section_with(source, 1, kind);
1287 }
1288}