azalea_inventory_macros/
menu_impl.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::Ident;
4
5use crate::{
6    parse_macro::{DeclareMenus, Menu},
7    utils::{to_pascal_case, to_snake_case},
8};
9
10pub fn generate(input: &DeclareMenus) -> TokenStream {
11    let mut slot_mut_match_variants = quote! {};
12    let mut slot_match_variants = quote! {};
13    let mut len_match_variants = quote! {};
14    let mut kind_match_variants = quote! {};
15    let mut slots_match_variants = quote! {};
16    let mut contents_match_variants = quote! {};
17    let mut location_match_variants = quote! {};
18    let mut player_slots_range_match_variants = quote! {};
19
20    let mut player_consts = quote! {};
21    let mut menu_consts = quote! {};
22
23    let mut hotbar_slots_start = 0;
24    let mut hotbar_slots_end = 0;
25    let mut inventory_without_hotbar_slots_start = 0;
26    let mut inventory_without_hotbar_slots_end = 0;
27
28    for menu in &input.menus {
29        slot_mut_match_variants.extend(generate_match_variant_for_slot_mut(menu, true));
30        slot_match_variants.extend(generate_match_variant_for_slot_mut(menu, false));
31        len_match_variants.extend(generate_match_variant_for_len(menu));
32        kind_match_variants.extend(generate_match_variant_for_kind(menu));
33        slots_match_variants.extend(generate_match_variant_for_slots(menu));
34        contents_match_variants.extend(generate_match_variant_for_contents(menu));
35        location_match_variants.extend(generate_match_variant_for_location(menu));
36        player_slots_range_match_variants
37            .extend(generate_match_variant_for_player_slots_range(menu));
38
39        // this part is only used to generate `Player::is_hotbar_slot`
40        if menu.name == "Player" {
41            let mut i = 0;
42            for field in &menu.fields {
43                let field_name = &field.name;
44                let start = i;
45                i += field.length;
46                let end = i - 1;
47
48                if field_name == "inventory" {
49                    // it only subtracts 8 here since it's inclusive (there's 9 total hotbar slots)
50                    hotbar_slots_start = end - 8;
51                    hotbar_slots_end = end;
52
53                    inventory_without_hotbar_slots_start = start;
54                    inventory_without_hotbar_slots_end = end - 9;
55                }
56
57                if start == end {
58                    let const_name = Ident::new(
59                        &format!("{}_SLOT", field_name.to_string().to_uppercase()),
60                        field_name.span(),
61                    );
62                    player_consts.extend(quote! {
63                        pub const #const_name: usize = #start;
64                    });
65                } else {
66                    let const_name = Ident::new(
67                        &format!("{}_SLOTS", field_name.to_string().to_uppercase()),
68                        field_name.span(),
69                    );
70                    player_consts.extend(quote! {
71                        pub const #const_name: RangeInclusive<usize> = #start..=#end;
72                    });
73                }
74            }
75        } else {
76            menu_consts.extend(generate_menu_consts(menu));
77        }
78    }
79
80    assert!(hotbar_slots_start != 0 && hotbar_slots_end != 0);
81    quote! {
82        impl Player {
83            pub const HOTBAR_SLOTS: RangeInclusive<usize> = #hotbar_slots_start..=#hotbar_slots_end;
84            pub const INVENTORY_WITHOUT_HOTBAR_SLOTS: RangeInclusive<usize> = #inventory_without_hotbar_slots_start..=#inventory_without_hotbar_slots_end;
85            #player_consts
86
87            /// Returns whether the given protocol index is in the player's hotbar.
88            ///
89            /// Equivalent to `Player::HOTBAR_SLOTS.contains(&i)`.
90            pub fn is_hotbar_slot(i: usize) -> bool {
91                Self::HOTBAR_SLOTS.contains(&i)
92            }
93        }
94
95        impl Menu {
96            #menu_consts
97
98            /// Get a mutable reference to the [`ItemStack`] at the given protocol index.
99            ///
100            /// If you're trying to get an item in a menu without caring about
101            /// protocol indexes, you should just `match` it and index the
102            /// [`ItemStack`] you get.
103            ///
104            /// Use [`Menu::slot`] if you don't need a mutable reference to the slot.
105            ///
106            /// # Errors
107            ///
108            /// Returns `None` if the index is out of bounds.
109            #[inline]
110            pub fn slot_mut(&mut self, i: usize) -> Option<&mut ItemStack> {
111                Some(match self {
112                    #slot_mut_match_variants
113                })
114            }
115
116            /// Get a reference to the [`ItemStack`] at the given protocol index.
117            ///
118            /// If you're trying to get an item in a menu without caring about
119            /// protocol indexes, you should just `match` it and index the
120            /// [`ItemStack`] you get.
121            ///
122            /// Use [`Menu::slot_mut`] if you need a mutable reference to the slot.
123            ///
124            /// # Errors
125            ///
126            /// Returns `None` if the index is out of bounds.
127            pub fn slot(&self, i: usize) -> Option<&ItemStack> {
128                Some(match self {
129                    #slot_match_variants
130                })
131            }
132
133            /// Returns the number of slots in the menu.
134            #[allow(clippy::len_without_is_empty)]
135            pub const fn len(&self) -> usize {
136                match self {
137                    #len_match_variants
138                }
139            }
140
141            pub fn from_kind(kind: azalea_registry::MenuKind) -> Self {
142                match kind {
143                    #kind_match_variants
144                }
145            }
146
147            /// Return the contents of the menu, including the player's inventory.
148            ///
149            /// The indexes in this will match up with [`Menu::slot_mut`].
150            ///
151            /// If you don't want to include the player's inventory, use [`Menu::contents`]
152            /// instead.
153            ///
154            /// If you *only* want to include the players inventory, then you can filter by only
155            /// using the slots in [`Self::player_slots_range`].
156            pub fn slots(&self) -> Vec<ItemStack> {
157                match self {
158                    #slots_match_variants
159                }
160            }
161
162            /// Return the contents of the menu, not including the player's inventory.
163            ///
164            /// If you want to include the player's inventory, use [`Menu::slots`] instead.
165            pub fn contents(&self) -> Vec<ItemStack> {
166                match self {
167                    #contents_match_variants
168                }
169            }
170
171            pub fn location_for_slot(&self, i: usize) -> Option<MenuLocation> {
172                Some(match self {
173                    #location_match_variants
174                })
175            }
176
177            /// Get the range of slot indexes that contain the player's inventory. This may be different for each menu.
178            pub fn player_slots_range(&self) -> RangeInclusive<usize> {
179                match self {
180                    #player_slots_range_match_variants
181                }
182            }
183
184            /// Get the range of slot indexes that contain the player's hotbar. This may be different for each menu.
185            ///
186            /// ```
187            /// # let inventory = azalea_inventory::Menu::Player(azalea_inventory::Player::default());
188            /// let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
189            /// ```
190            pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> {
191                // hotbar is always last 9 slots in the player's inventory
192                ((*self.player_slots_range().end() - 8)..=*self.player_slots_range().end())
193            }
194
195            /// Get the range of slot indexes that contain the player's inventory, not including the hotbar. This may be different for each menu.
196            pub fn player_slots_without_hotbar_range(&self) -> RangeInclusive<usize> {
197                (*self.player_slots_range().start()..=*self.player_slots_range().end() - 9)
198            }
199
200            /// Returns whether the given index would be in the player's hotbar.
201            ///
202            /// Equivalent to `self.hotbar_slots_range().contains(&i)`.
203            pub fn is_hotbar_slot(&self, i: usize) -> bool {
204                self.hotbar_slots_range().contains(&i)
205            }
206        }
207    }
208}
209
210/// Menu::Player {
211///     craft_result,
212///     craft,
213///     armor,
214///     inventory,
215///     offhand,
216/// } => {
217///     match i {
218///         0 => craft_result,
219///         1..=4 => craft,
220///         5..=8 => armor,
221///         // ...
222///         _ => return None,
223///     }
224/// } // ...
225pub fn generate_match_variant_for_slot_mut(menu: &Menu, mutable: bool) -> TokenStream {
226    let mut match_arms = quote! {};
227    let mut i = 0;
228    for field in &menu.fields {
229        let field_name = &field.name;
230        let start = i;
231        i += field.length;
232        let end = i - 1;
233        match_arms.extend(if start == end {
234            quote! { #start => #field_name, }
235        } else if start == 0 {
236            if mutable {
237                quote! { #start..=#end => &mut #field_name[i], }
238            } else {
239                quote! { #start..=#end => &#field_name[i], }
240            }
241        } else if mutable {
242            quote! { #start..=#end => &mut #field_name[i - #start], }
243        } else {
244            quote! { #start..=#end => &#field_name[i - #start], }
245        });
246    }
247
248    generate_matcher(
249        menu,
250        &quote! {
251            match i {
252                #match_arms
253                _ => return None
254            }
255        },
256        true,
257    )
258}
259
260pub fn generate_match_variant_for_len(menu: &Menu) -> TokenStream {
261    let length = menu.fields.iter().map(|f| f.length).sum::<usize>();
262    generate_matcher(
263        menu,
264        &quote! {
265            #length
266        },
267        false,
268    )
269}
270
271pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream {
272    // azalea_registry::MenuKind::Generic9x3 => Menu::Generic9x3 { contents:
273    // Default::default(), player: Default::default() },
274
275    let menu_name = &menu.name;
276    let menu_field_names = if menu.name == "Player" {
277        // player isn't in MenuKind
278        return quote! {};
279    } else {
280        let mut menu_field_names = quote! {};
281        for field in &menu.fields {
282            let field_name = &field.name;
283            menu_field_names.extend(quote! { #field_name: Default::default(), })
284        }
285        quote! { { #menu_field_names } }
286    };
287
288    quote! {
289        azalea_registry::MenuKind::#menu_name => Menu::#menu_name #menu_field_names,
290    }
291}
292
293pub fn generate_match_variant_for_slots(menu: &Menu) -> TokenStream {
294    let mut instructions = quote! {};
295    let mut length = 0;
296    for field in &menu.fields {
297        let field_name = &field.name;
298        instructions.extend(if field.length == 1 {
299            quote! { items.push(#field_name.clone()); }
300        } else {
301            quote! { items.extend(#field_name.iter().cloned()); }
302        });
303        length += field.length;
304    }
305
306    generate_matcher(
307        menu,
308        &quote! {
309            let mut items = Vec::with_capacity(#length);
310            #instructions
311            items
312        },
313        true,
314    )
315}
316
317pub fn generate_match_variant_for_contents(menu: &Menu) -> TokenStream {
318    let mut instructions = quote! {};
319    let mut length = 0;
320    for field in &menu.fields {
321        let field_name = &field.name;
322        if field_name == "player" {
323            continue;
324        }
325        instructions.extend(if field.length == 1 {
326            quote! { items.push(#field_name.clone()); }
327        } else {
328            quote! { items.extend(#field_name.iter().cloned()); }
329        });
330        length += field.length;
331    }
332
333    generate_matcher(
334        menu,
335        &quote! {
336            let mut items = Vec::with_capacity(#length);
337            #instructions
338            items
339        },
340        true,
341    )
342}
343
344pub fn generate_match_variant_for_location(menu: &Menu) -> TokenStream {
345    let mut match_arms = quote! {};
346    let mut i = 0;
347
348    let menu_name = Ident::new(&to_pascal_case(&menu.name.to_string()), menu.name.span());
349    let menu_enum_name = Ident::new(&format!("{menu_name}MenuLocation"), menu_name.span());
350
351    for field in &menu.fields {
352        let field_name = Ident::new(&to_pascal_case(&field.name.to_string()), field.name.span());
353        let start = i;
354        i += field.length;
355        let end = i - 1;
356        match_arms.extend(if start == end {
357            quote! { #start => #menu_enum_name::#field_name, }
358        } else {
359            quote! { #start..=#end => #menu_enum_name::#field_name, }
360        });
361    }
362
363    generate_matcher(
364        menu,
365        &quote! {
366            MenuLocation::#menu_name(match i {
367                #match_arms
368                _ => return None
369            })
370        },
371        false,
372    )
373}
374
375pub fn generate_match_variant_for_player_slots_range(menu: &Menu) -> TokenStream {
376    // Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS_RANGE,,
377    // Menu::Generic9x3 { .. } => Menu::GENERIC9X3_SLOTS_RANGE,
378    // ..
379
380    match menu.name.to_string().as_str() {
381        "Player" => {
382            quote! {
383                Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS,
384            }
385        }
386        _ => {
387            let menu_name = &menu.name;
388            let menu_slots_range_name = Ident::new(
389                &format!(
390                    "{}_PLAYER_SLOTS",
391                    to_snake_case(&menu.name.to_string()).to_uppercase()
392                ),
393                menu.name.span(),
394            );
395            quote! {
396                Menu::#menu_name { .. } => Menu::#menu_slots_range_name,
397            }
398        }
399    }
400}
401
402fn generate_menu_consts(menu: &Menu) -> TokenStream {
403    let mut menu_consts = quote! {};
404
405    let mut i = 0;
406
407    for field in &menu.fields {
408        let field_name_start = format!(
409            "{}_{}",
410            to_snake_case(&menu.name.to_string()).to_uppercase(),
411            to_snake_case(&field.name.to_string()).to_uppercase()
412        );
413        let field_index_start = i;
414        i += field.length;
415        let field_index_end = i - 1;
416
417        if field.length == 1 {
418            let field_name = Ident::new(
419                format!("{field_name_start}_SLOT").as_str(),
420                field.name.span(),
421            );
422            menu_consts.extend(quote! { pub const #field_name: usize = #field_index_start; });
423        } else {
424            let field_name = Ident::new(
425                format!("{field_name_start}_SLOTS").as_str(),
426                field.name.span(),
427            );
428            menu_consts.extend(quote! { pub const #field_name: RangeInclusive<usize> = #field_index_start..=#field_index_end; });
429        }
430    }
431
432    menu_consts
433}
434
435pub fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream {
436    let menu_name = &menu.name;
437    let menu_field_names = if needs_fields {
438        let mut menu_field_names = quote! {};
439        for field in &menu.fields {
440            let field_name = &field.name;
441            menu_field_names.extend(quote! { #field_name, })
442        }
443        menu_field_names
444    } else {
445        quote! { .. }
446    };
447
448    let matcher = if menu.name == "Player" {
449        quote! { (Player { #menu_field_names }) }
450    } else {
451        quote! { { #menu_field_names } }
452    };
453    quote! {
454        Menu::#menu_name #matcher => {
455            #match_arms
456        },
457    }
458}