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 DENY.get_or_init(|| RestrictedRules::place_only(|_, _| false))
396 .clone()
397}
398
399fn deny_all_rules() -> Arc<RestrictedRules> {
401 static DENY: OnceLock<Arc<RestrictedRules>> = OnceLock::new();
402 DENY.get_or_init(|| RestrictedRules::guarded(|_, _| false, |_, _, _, _| false))
403 .clone()
404}
405
406#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum FillDirection {
412 Forward,
414 Backward,
416}
417
418#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420pub enum FakeResultRemainderPolicy {
421 Drop,
423 Discard,
425}
426
427pub(crate) struct Route {
429 pub(crate) from: Range<usize>,
430 pub(crate) targets: Vec<Range<usize>>,
431 pub(crate) direction: FillDirection,
432 pub(crate) fake_result_remainder: FakeResultRemainderPolicy,
433}
434
435pub struct MenuBuilder {
439 instance: MenuInstanceId,
440 menu_type: Option<MenuTypeRef>,
441 container_id: u8,
442 overrides_player_slots: bool,
443 slots: Vec<Box<dyn Slot>>,
444 container_refs: Vec<ContainerRef>,
445 data_slots: Vec<i16>,
446 routes: Vec<Route>,
447 drain_sections: Vec<Range<usize>>,
448 claimed: Vec<(ContainerId, Range<usize>)>,
451}
452
453impl fmt::Debug for MenuBuilder {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455 f.debug_struct("MenuBuilder")
456 .field("instance", &self.instance)
457 .field("container_id", &self.container_id)
458 .field("slots", &self.slots.len())
459 .field("routes", &self.routes.len())
460 .finish_non_exhaustive()
461 }
462}
463
464impl MenuBuilder {
465 #[must_use]
470 pub fn new(menu_type: impl Into<Option<MenuTypeRef>>, container_id: u8) -> Self {
471 Self {
472 instance: MenuInstanceId::next(),
473 menu_type: menu_type.into(),
474 container_id,
475 overrides_player_slots: false,
476 slots: Vec::new(),
477 container_refs: Vec::new(),
478 data_slots: Vec::new(),
479 routes: Vec::new(),
480 drain_sections: Vec::new(),
481 claimed: Vec::new(),
482 }
483 }
484
485 #[must_use]
509 #[expect(
510 clippy::unused_self,
511 reason = "split is intentionally builder-scoped as part of the menu DSL"
512 )]
513 pub fn split(&mut self, container: impl Into<ContainerRef>) -> ContainerSlots {
514 let container = container.into();
515 let size = Self::container_size(&container);
516 ContainerSlots {
517 container,
518 next: 0,
519 size,
520 }
521 }
522
523 fn container_size(container: &ContainerRef) -> usize {
525 ContainerLockGuard::lock_all(slice::from_ref(container))
526 .get(container.container_id())
527 .expect("container was just locked")
528 .get_container_size()
529 }
530
531 pub fn section(&mut self, source: impl SectionSource, count: usize) -> Section {
543 self.section_with(source, count, SectionKind::Normal)
544 }
545
546 pub fn section_with(
568 &mut self,
569 source: impl SectionSource,
570 count: usize,
571 kind: impl Into<SectionKind>,
572 ) -> Section {
573 let kind = kind.into();
574 let (container, range) = source.take(count);
575 self.claim(&container, range);
576 let start = self.slots.len();
577 for index in range {
578 let slot = kind.make(&container, index);
579 self.push_section_slot(slot, &container, index);
580 }
581 self.section_from(start)
582 }
583
584 pub fn section_all(&mut self, container: impl Into<ContainerRef>) -> Section {
594 self.section_all_with(container, SectionKind::Normal)
595 }
596
597 pub fn section_all_with(
603 &mut self,
604 container: impl Into<ContainerRef>,
605 kind: impl Into<SectionKind>,
606 ) -> Section {
607 let container = container.into();
608 let size = Self::container_size(&container);
609 self.section_with(container, size, kind)
610 }
611
612 pub fn section_at(
622 &mut self,
623 container: impl Into<ContainerRef>,
624 indices: impl IntoIterator<Item = usize>,
625 kind: impl Into<SectionKind>,
626 ) -> Section {
627 let kind = kind.into();
628 let container = container.into();
629 let start = self.slots.len();
630 let mut run: Option<Range<usize>> = None;
631 for index in indices {
632 match &mut run {
633 Some(r) if index == r.end => r.end += 1,
634 _ => {
635 if let Some(r) = run.take() {
636 self.claim(&container, r);
637 }
638 run = Some((index..index + 1).into());
639 }
640 }
641 let slot = kind.make(&container, index);
642 self.push_section_slot(slot, &container, index);
643 }
644 if let Some(r) = run {
645 self.claim(&container, r);
646 }
647 self.section_from(start)
648 }
649
650 pub fn player_inventory(
652 &mut self,
653 inventory: &Shared<PlayerInventory>,
654 ) -> PlayerInventorySections {
655 self.player_inventory_with(inventory, SectionKind::Normal)
656 }
657
658 pub fn player_inventory_with(
666 &mut self,
667 inventory: &Shared<PlayerInventory>,
668 kind: impl Into<SectionKind>,
669 ) -> PlayerInventorySections {
670 let kind = kind.into();
671 let container = ContainerRef::from(inventory.clone());
672 let start = self.slots.len();
673 for index in PlayerInventory::MAIN.chain(PlayerInventory::HOTBAR) {
674 let slot = kind.make(&container, index);
675 self.push_section_slot(slot, &container, index);
676 }
677
678 let main = Section::new(self.instance, start..start + PlayerInventory::MAIN.len());
679 let hotbar = Section::new(
680 self.instance,
681 start + PlayerInventory::MAIN.len()..self.slots.len(),
682 );
683 let all = Section::new(self.instance, start..self.slots.len());
684 PlayerInventorySections { all, main, hotbar }
685 }
686
687 pub fn result_slot(&mut self, handler: impl ResultHandler + 'static) -> Section {
696 let slot = ResultSlot::new(handler);
697 let container = slot.result_container().clone();
698 self.claim(&container, (0..1).into());
699 let start = self.slots.len();
700 self.push_section_slot(Box::new(slot), &container, 0);
701 self.section_from(start)
702 }
703
704 #[cfg(test)]
715 pub(crate) fn custom_section<S>(&mut self, slots: impl IntoIterator<Item = S>) -> Section
716 where
717 S: Slot + 'static,
718 {
719 self.custom_boxed_section(
720 slots
721 .into_iter()
722 .map(|slot| Box::new(slot) as Box<dyn Slot>),
723 )
724 }
725
726 #[cfg(test)]
728 pub(crate) fn custom_boxed_section(
729 &mut self,
730 slots: impl IntoIterator<Item = Box<dyn Slot>>,
731 ) -> Section {
732 let start = self.slots.len();
733 for slot in slots {
734 self.push_boxed_slot(slot);
735 }
736 self.section_from(start)
737 }
738
739 pub fn data_slot(&mut self, initial: i16) -> DataSlot {
741 let index = self.data_slots.len();
742 self.data_slots.push(initial);
743 DataSlot {
744 menu: self.instance,
745 index,
746 }
747 }
748
749 pub fn route(
765 &mut self,
766 from: impl IntoSections,
767 targets: impl IntoSections,
768 direction: FillDirection,
769 ) -> &mut Self {
770 self.route_with_remainder_policy(from, targets, direction, FakeResultRemainderPolicy::Drop)
771 }
772
773 pub fn route_with_remainder_policy(
780 &mut self,
781 from: impl IntoSections,
782 targets: impl IntoSections,
783 direction: FillDirection,
784 fake_result_remainder: FakeResultRemainderPolicy,
785 ) -> &mut Self {
786 let targets: Vec<Range<usize>> = targets.into_sections().map(|s| self.owned(s)).collect();
787 for from in from.into_sections() {
788 let from = self.owned(from);
789 assert!(
790 !self
791 .routes
792 .iter()
793 .any(|route| route.from.start < from.end && from.start < route.from.end),
794 "shift-click route source {from:?} overlaps an existing route source",
795 );
796 assert!(
797 !targets
798 .iter()
799 .any(|t| t.start < from.end && from.start < t.end),
800 "shift-click route target {targets:?} overlaps its own source {from:?}",
801 );
802 self.routes.push(Route {
803 from,
804 targets: targets.clone(),
805 direction,
806 fake_result_remainder,
807 });
808 }
809 self
810 }
811
812 pub fn drain(&mut self, sections: impl IntoSections) -> &mut Self {
848 let ranges: Vec<_> = sections.into_sections().map(|s| self.owned(s)).collect();
849 assert!(
850 ranges
851 .iter()
852 .flat_map(|range| *range)
853 .all(|slot| !self.slots[slot].is_fake()),
854 "drain sections cannot contain fake or result slots"
855 );
856 self.drain_sections.extend(ranges);
857 self
858 }
859
860 pub const fn override_player_slots(&mut self) -> &mut Self {
865 self.overrides_player_slots = true;
866 self
867 }
868
869 #[must_use]
875 pub fn build(self, kind: impl MenuKind + 'static) -> Menu {
876 self.build_boxed(Box::new(kind))
877 }
878
879 #[must_use]
888 pub fn build_boxed(self, kind: Box<dyn MenuKind>) -> Menu {
889 if let Some(menu_type) = self.menu_type {
890 assert_eq!(
891 self.slots.len(),
892 menu_type.slot_count,
893 "menu type {} expects {} slots, but the builder has {}",
894 menu_type.key,
895 menu_type.slot_count,
896 self.slots.len(),
897 );
898 }
899 Self::assert_no_fake_slot_aliases(&self.slots);
900
901 let mut behavior = MenuBehavior::new(
902 self.instance,
903 self.slots,
904 self.container_id,
905 self.menu_type,
906 self.container_refs,
907 );
908 for initial in self.data_slots {
909 behavior.add_data_slot(initial);
910 }
911
912 let layout = MenuLayout {
913 routes: self.routes,
914 drain_sections: self.drain_sections,
915 };
916 Menu::from_parts(behavior, layout, kind, self.overrides_player_slots)
917 }
918
919 fn assert_no_fake_slot_aliases(slots: &[Box<dyn Slot>]) {
922 use rustc_hash::FxHashMap;
923
924 let mut physical_slots: FxHashMap<(ContainerId, usize), (usize, bool)> =
925 FxHashMap::default();
926 for (slot_index, slot) in slots.iter().enumerate() {
927 let Some(key) = slot.storage().physical_key() else {
928 continue;
929 };
930 let is_fake = slot.is_fake();
931 if let Some(&(other_index, other_is_fake)) = physical_slots.get(&key) {
932 assert!(
933 !is_fake && !other_is_fake,
934 "menu slots {other_index} and {slot_index} alias physical container slot \
935 {key:?}, but fake slots require exclusive backing storage"
936 );
937 } else {
938 physical_slots.insert(key, (slot_index, is_fake));
939 }
940 }
941 }
942
943 pub(crate) const fn instance(&self) -> MenuInstanceId {
945 self.instance
946 }
947
948 #[must_use]
950 pub const fn slot_count(&self) -> usize {
951 self.slots.len()
952 }
953
954 pub(crate) fn push_boxed_slot(&mut self, slot: Box<dyn Slot>) {
956 for container in slot.storage().container_refs() {
957 self.register_container(container.clone());
958 }
959 self.slots.push(slot);
960 }
961
962 pub(crate) fn push_section_slot(
964 &mut self,
965 slot: Box<dyn Slot>,
966 source: &ContainerRef,
967 source_index: usize,
968 ) {
969 assert_eq!(
970 slot.storage().physical_key(),
971 Some((source.container_id(), source_index)),
972 "section slot backing must match its declared source container and index"
973 );
974 self.push_boxed_slot(slot);
975 }
976
977 pub(crate) fn claim(&mut self, container: &ContainerRef, range: Range<usize>) {
982 let id = container.container_id();
983 let size = {
984 let guard = ContainerLockGuard::lock_all(slice::from_ref(container));
985 let Some(container) = guard.get(id) else {
986 panic!("container was not locked while validating a menu section");
987 };
988 container.get_container_size()
989 };
990 assert!(
991 range.end <= size,
992 "section takes container slots {}..{}, but the container only has {size} slots",
993 range.start,
994 range.end,
995 );
996 for (other_id, other) in &self.claimed {
997 assert!(
998 *other_id != id || range.start >= other.end || other.start >= range.end,
999 "two sections cover overlapping slots ({other:?} and {range:?}) of the same \
1000 container; carve shared containers with MenuBuilder::split"
1001 );
1002 }
1003 self.claimed.push((id, range));
1004 }
1005
1006 pub(crate) fn register_container(&mut self, container: impl Into<ContainerRef>) {
1008 let container_ref = container.into();
1009 let id = container_ref.container_id();
1010 if !self.container_refs.iter().any(|c| c.container_id() == id) {
1011 self.container_refs.push(container_ref);
1012 }
1013 }
1014
1015 fn owned(&self, section: Section) -> Range<usize> {
1017 assert_eq!(
1018 section.menu, self.instance,
1019 "Section was minted by a different MenuBuilder"
1020 );
1021 section.range()
1022 }
1023
1024 fn section_from(&self, start: usize) -> Section {
1026 Section::new(self.instance, start..self.slots.len())
1027 }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use steel_registry::{init_vanilla_registry, vanilla_items, vanilla_menu_types};
1033 use steel_utils::{Downcast as _, locks::IntoShared};
1034
1035 use super::*;
1036 use crate::inventory::container::SimpleContainer;
1037 use crate::inventory::menu::kinds::BasicKind;
1038
1039 struct NoopResultHandler(ContainerRef);
1040
1041 impl ResultHandler for NoopResultHandler {
1042 fn result_container(&self) -> ContainerRef {
1043 self.0.clone()
1044 }
1045
1046 fn dependencies(&self) -> Vec<ContainerRef> {
1047 Vec::new()
1048 }
1049
1050 fn update_result(&self, _guard: &mut ContainerLockGuard) {}
1051
1052 fn on_result_taken(
1053 &self,
1054 _guard: &mut ContainerLockGuard,
1055 _player: &Player,
1056 ) -> Option<ItemStack> {
1057 None
1058 }
1059
1060 fn is_result_valid(&self, _guard: &ContainerLockGuard, _player: &Player) -> bool {
1061 true
1062 }
1063 }
1064
1065 struct DependencyResultHandler {
1066 result: ContainerRef,
1067 dependency: ContainerRef,
1068 }
1069
1070 impl ResultHandler for DependencyResultHandler {
1071 fn result_container(&self) -> ContainerRef {
1072 self.result.clone()
1073 }
1074
1075 fn dependencies(&self) -> Vec<ContainerRef> {
1076 vec![self.dependency.clone()]
1077 }
1078
1079 fn update_result(&self, _guard: &mut ContainerLockGuard) {}
1080
1081 fn on_result_taken(
1082 &self,
1083 _guard: &mut ContainerLockGuard,
1084 _player: &Player,
1085 ) -> Option<ItemStack> {
1086 None
1087 }
1088
1089 fn is_result_valid(&self, _guard: &ContainerLockGuard, _player: &Player) -> bool {
1090 true
1091 }
1092 }
1093
1094 #[test]
1095 #[should_panic(
1096 expected = "menu type minecraft:generic_9x6 expects 90 slots, but the builder has 0"
1097 )]
1098 fn build_rejects_a_slot_count_that_disagrees_with_the_menu_type() {
1099 let _ = MenuBuilder::new(&vanilla_menu_types::GENERIC_9X6, 1).build(BasicKind);
1100 }
1101
1102 #[test]
1103 fn builds_with_an_erased_menu_kind() {
1104 let kind: Box<dyn MenuKind> = Box::new(BasicKind);
1105 let menu = MenuBuilder::new(None, 0).build_boxed(kind);
1106
1107 assert!(menu.kind().downcast_ref::<BasicKind>().is_some());
1108 }
1109
1110 #[test]
1111 #[should_panic(
1112 expected = "section takes container slots 0..2, but the container only has 1 slots"
1113 )]
1114 fn direct_section_rejects_a_range_past_container_capacity() {
1115 let mut builder = MenuBuilder::new(None, 0);
1116 builder.section(SimpleContainer::new(1).into_shared(), 2);
1117 }
1118
1119 #[test]
1120 #[should_panic(
1121 expected = "section takes container slots 0..1, but the container only has 0 slots"
1122 )]
1123 fn result_slot_rejects_a_container_without_slot_zero() {
1124 let container = ContainerRef::from(SimpleContainer::new(0).into_shared());
1125 let mut builder = MenuBuilder::new(None, 0);
1126
1127 let _ = builder.result_slot(NoopResultHandler(container));
1128 }
1129
1130 #[test]
1131 #[should_panic(expected = "two sections cover overlapping slots")]
1132 fn result_slot_claims_slot_zero_against_normal_sections() {
1133 let container = ContainerRef::from(SimpleContainer::new(1).into_shared());
1134 let mut builder = MenuBuilder::new(None, 0);
1135 let _ = builder.result_slot(NoopResultHandler(container.clone()));
1136
1137 let _ = builder.section_all(container);
1138 }
1139
1140 #[test]
1141 fn result_slot_registers_handler_dependencies() {
1142 let result = ContainerRef::from(SimpleContainer::new(1).into_shared());
1143 let dependency = ContainerRef::from(SimpleContainer::new(1).into_shared());
1144 let dependency_id = dependency.container_id();
1145 let mut builder = MenuBuilder::new(None, 0);
1146 let _ = builder.result_slot(DependencyResultHandler { result, dependency });
1147
1148 let menu = builder.build(BasicKind);
1149 let guard = menu.behavior().lock_all_containers();
1150
1151 assert!(guard.contains(dependency_id));
1152 }
1153
1154 #[test]
1155 #[should_panic(expected = "drain sections cannot contain fake or result slots")]
1156 fn drain_rejects_a_result_slot() {
1157 let result = ContainerRef::from(SimpleContainer::new(1).into_shared());
1158 let mut builder = MenuBuilder::new(None, 0);
1159 let section = builder.result_slot(NoopResultHandler(result));
1160
1161 builder.drain(section);
1162 }
1163
1164 #[test]
1165 #[should_panic(expected = "fake slots require exclusive backing storage")]
1166 fn build_rejects_a_result_alias_through_player_inventory() {
1167 let inventory = PlayerInventory::new().into_shared();
1168 let mut builder = MenuBuilder::new(None, 0);
1169 let _ = builder.result_slot(NoopResultHandler(ContainerRef::from(inventory.clone())));
1170 let _ = builder.player_inventory_with(&inventory, SectionKind::Normal);
1171
1172 let _ = builder.build(BasicKind);
1173 }
1174
1175 #[test]
1176 fn build_allows_non_fake_player_inventory_aliases() {
1177 let inventory = PlayerInventory::new().into_shared();
1178 let mut builder = MenuBuilder::new(None, 0);
1179 let _ = builder.player_inventory(&inventory);
1180 let _ = builder.player_inventory_with(&inventory, SectionKind::Display);
1181
1182 let _ = builder.build(BasicKind);
1183 }
1184
1185 #[test]
1186 #[should_panic(expected = "shift-click route source 0..27 overlaps an existing route source")]
1187 fn route_rejects_overlapping_source_sections() {
1188 let inventory = PlayerInventory::new().into_shared();
1189 let mut builder = MenuBuilder::new(None, 0);
1190 let player = builder.player_inventory(&inventory);
1191 let target = builder.section(SimpleContainer::new(1).into_shared(), 1);
1192
1193 builder.route(player.all(), [target], FillDirection::Forward);
1194 builder.route(player.main(), [target], FillDirection::Forward);
1195 }
1196
1197 #[test]
1198 fn section_at_preserves_the_given_index_order() {
1199 let container = ContainerRef::from(SimpleContainer::new(5).into_shared());
1200 let mut b = MenuBuilder::new(None, 0);
1201 let section = b.section_at(container, [4, 3, 0, 1], SectionKind::Normal);
1202 let menu = b.build(BasicKind);
1203
1204 assert_eq!((section.start(), section.end()), (0, 4));
1205 let container_slots: Vec<usize> = menu
1206 .behavior()
1207 .slots()
1208 .iter()
1209 .map(|slot| slot.get_container_slot())
1210 .collect();
1211 assert_eq!(container_slots, vec![4, 3, 0, 1]);
1212 }
1213
1214 #[test]
1215 #[should_panic(expected = "two sections cover overlapping slots")]
1216 fn section_at_rejects_indices_claimed_by_another_section() {
1217 let container = SimpleContainer::new(4).into_shared();
1218 let mut b = MenuBuilder::new(None, 0);
1219 let _ = b.section(container.clone(), 2);
1220 let _ = b.section_at(container, [1], SectionKind::Normal);
1221 }
1222
1223 #[test]
1224 #[should_panic(expected = "two sections cover overlapping slots")]
1225 fn section_at_rejects_a_repeated_index() {
1226 let container = ContainerRef::from(SimpleContainer::new(4).into_shared());
1227 let mut b = MenuBuilder::new(None, 0);
1228 let _ = b.section_at(container, [0, 2, 0], SectionKind::Normal);
1229 }
1230
1231 #[test]
1232 fn display_kind_rejects_placement() {
1233 init_vanilla_registry();
1234 let container = ContainerRef::from(SimpleContainer::new(1).into_shared());
1235 let mut b = MenuBuilder::new(None, 0);
1236 let _ = b.section_at(container, [0], SectionKind::Display);
1237 let menu = b.build(BasicKind);
1238
1239 let stack = ItemStack::new(&vanilla_items::STONE);
1240 assert!(!menu.behavior().slots()[0].may_place(&stack));
1241 }
1242
1243 #[test]
1244 fn custom_kind_lowers_through_the_factory() {
1245 let container = ContainerRef::from(SimpleContainer::new(3).into_shared());
1246 let factory = SectionKind::custom(|container, index| {
1247 Box::new(NormalSlot::new(container.clone(), index))
1248 });
1249 let mut b = MenuBuilder::new(None, 0);
1250 let _ = b.section_at(container, [2, 0], factory);
1251 let menu = b.build(BasicKind);
1252
1253 let container_slots: Vec<usize> = menu
1254 .behavior()
1255 .slots()
1256 .iter()
1257 .map(|slot| slot.get_container_slot())
1258 .collect();
1259 assert_eq!(container_slots, vec![2, 0]);
1260 }
1261
1262 #[test]
1263 #[should_panic(expected = "section slot backing must match its declared source")]
1264 fn custom_kind_rejects_a_mismatched_physical_backing() {
1265 let source = ContainerRef::from(SimpleContainer::new(1).into_shared());
1266 let other = ContainerRef::from(SimpleContainer::new(1).into_shared());
1267 let kind =
1268 SectionKind::custom(move |_container, _index| Box::new(NormalSlot::new(&other, 0)));
1269 let mut builder = MenuBuilder::new(None, 0);
1270
1271 let _ = builder.section_with(source, 1, kind);
1272 }
1273}