azalea_inventory_macros/
menu_impl.rs1use 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 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 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 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 #[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 pub fn slot(&self, i: usize) -> Option<&ItemStack> {
128 Some(match self {
129 #slot_match_variants
130 })
131 }
132
133 #[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 pub fn slots(&self) -> Vec<ItemStack> {
157 match self {
158 #slots_match_variants
159 }
160 }
161
162 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 pub fn player_slots_range(&self) -> RangeInclusive<usize> {
181 match self {
182 #player_slots_range_match_variants
183 }
184 }
185
186 pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> {
195 ((*self.player_slots_range().end() - 8)..=*self.player_slots_range().end())
197 }
198
199 pub fn player_slots_without_hotbar_range(&self) -> RangeInclusive<usize> {
203 (*self.player_slots_range().start()..=*self.player_slots_range().end() - 9)
204 }
205
206 pub fn is_hotbar_slot(&self, i: usize) -> bool {
210 self.hotbar_slots_range().contains(&i)
211 }
212 }
213 }
214}
215
216pub fn generate_match_variant_for_slot_mut(menu: &Menu, mutable: bool) -> TokenStream {
232 let mut match_arms = quote! {};
233 let mut i = 0;
234 for field in &menu.fields {
235 let field_name = &field.name;
236 let start = i;
237 i += field.length;
238 let end = i - 1;
239 match_arms.extend(if start == end {
240 quote! { #start => #field_name, }
241 } else if start == 0 {
242 if mutable {
243 quote! { #start..=#end => &mut #field_name[i], }
244 } else {
245 quote! { #start..=#end => &#field_name[i], }
246 }
247 } else if mutable {
248 quote! { #start..=#end => &mut #field_name[i - #start], }
249 } else {
250 quote! { #start..=#end => &#field_name[i - #start], }
251 });
252 }
253
254 generate_matcher(
255 menu,
256 "e! {
257 match i {
258 #match_arms
259 _ => return None
260 }
261 },
262 true,
263 )
264}
265
266pub fn generate_match_variant_for_len(menu: &Menu) -> TokenStream {
267 let length = menu.fields.iter().map(|f| f.length).sum::<usize>();
268 generate_matcher(
269 menu,
270 "e! {
271 #length
272 },
273 false,
274 )
275}
276
277pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream {
278 let menu_name = &menu.name;
282 let menu_field_names = if menu.name == "Player" {
283 return quote! {};
285 } else {
286 let mut menu_field_names = quote! {};
287 for field in &menu.fields {
288 let field_name = &field.name;
289 menu_field_names.extend(quote! { #field_name: Default::default(), })
290 }
291 quote! { { #menu_field_names } }
292 };
293
294 quote! {
295 azalea_registry::MenuKind::#menu_name => Menu::#menu_name #menu_field_names,
296 }
297}
298
299pub fn generate_match_variant_for_slots(menu: &Menu) -> TokenStream {
300 let mut instructions = quote! {};
301 let mut length = 0;
302 for field in &menu.fields {
303 let field_name = &field.name;
304 instructions.extend(if field.length == 1 {
305 quote! { items.push(#field_name.clone()); }
306 } else {
307 quote! { items.extend(#field_name.iter().cloned()); }
308 });
309 length += field.length;
310 }
311
312 generate_matcher(
313 menu,
314 "e! {
315 let mut items = Vec::with_capacity(#length);
316 #instructions
317 items
318 },
319 true,
320 )
321}
322
323pub fn generate_match_variant_for_contents(menu: &Menu) -> TokenStream {
324 let mut instructions = quote! {};
325 let mut length = 0;
326 for field in &menu.fields {
327 let field_name = &field.name;
328 if field_name == "player" {
329 continue;
330 }
331 instructions.extend(if field.length == 1 {
332 quote! { items.push(#field_name.clone()); }
333 } else {
334 quote! { items.extend(#field_name.iter().cloned()); }
335 });
336 length += field.length;
337 }
338
339 generate_matcher(
340 menu,
341 "e! {
342 let mut items = Vec::with_capacity(#length);
343 #instructions
344 items
345 },
346 true,
347 )
348}
349
350pub fn generate_match_variant_for_location(menu: &Menu) -> TokenStream {
351 let mut match_arms = quote! {};
352 let mut i = 0;
353
354 let menu_name = Ident::new(&to_pascal_case(&menu.name.to_string()), menu.name.span());
355 let menu_enum_name = Ident::new(&format!("{menu_name}MenuLocation"), menu_name.span());
356
357 for field in &menu.fields {
358 let field_name = Ident::new(&to_pascal_case(&field.name.to_string()), field.name.span());
359 let start = i;
360 i += field.length;
361 let end = i - 1;
362 match_arms.extend(if start == end {
363 quote! { #start => #menu_enum_name::#field_name, }
364 } else {
365 quote! { #start..=#end => #menu_enum_name::#field_name, }
366 });
367 }
368
369 generate_matcher(
370 menu,
371 "e! {
372 MenuLocation::#menu_name(match i {
373 #match_arms
374 _ => return None
375 })
376 },
377 false,
378 )
379}
380
381pub fn generate_match_variant_for_player_slots_range(menu: &Menu) -> TokenStream {
382 match menu.name.to_string().as_str() {
387 "Player" => {
388 quote! {
389 Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS,
390 }
391 }
392 _ => {
393 let menu_name = &menu.name;
394 let menu_slots_range_name = Ident::new(
395 &format!(
396 "{}_PLAYER_SLOTS",
397 to_snake_case(&menu.name.to_string()).to_uppercase()
398 ),
399 menu.name.span(),
400 );
401 quote! {
402 Menu::#menu_name { .. } => Menu::#menu_slots_range_name,
403 }
404 }
405 }
406}
407
408fn generate_menu_consts(menu: &Menu) -> TokenStream {
409 let mut menu_consts = quote! {};
410
411 let mut i = 0;
412
413 for field in &menu.fields {
414 let field_name_start = format!(
415 "{}_{}",
416 to_snake_case(&menu.name.to_string()).to_uppercase(),
417 to_snake_case(&field.name.to_string()).to_uppercase()
418 );
419 let field_index_start = i;
420 i += field.length;
421 let field_index_end = i - 1;
422
423 if field.length == 1 {
424 let field_name = Ident::new(
425 format!("{field_name_start}_SLOT").as_str(),
426 field.name.span(),
427 );
428 menu_consts.extend(quote! { pub const #field_name: usize = #field_index_start; });
429 } else {
430 let field_name = Ident::new(
431 format!("{field_name_start}_SLOTS").as_str(),
432 field.name.span(),
433 );
434 menu_consts.extend(quote! { pub const #field_name: RangeInclusive<usize> = #field_index_start..=#field_index_end; });
435 }
436 }
437
438 menu_consts
439}
440
441pub fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream {
442 let menu_name = &menu.name;
443 let menu_field_names = if needs_fields {
444 let mut menu_field_names = quote! {};
445 for field in &menu.fields {
446 let field_name = &field.name;
447 menu_field_names.extend(quote! { #field_name, })
448 }
449 menu_field_names
450 } else {
451 quote! { .. }
452 };
453
454 let matcher = if menu.name == "Player" {
455 quote! { (Player { #menu_field_names }) }
456 } else {
457 quote! { { #menu_field_names } }
458 };
459 quote! {
460 Menu::#menu_name #matcher => {
461 #match_arms
462 },
463 }
464}