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> {
179 match self {
180 #player_slots_range_match_variants
181 }
182 }
183
184 pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> {
191 ((*self.player_slots_range().end() - 8)..=*self.player_slots_range().end())
193 }
194
195 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 pub fn is_hotbar_slot(&self, i: usize) -> bool {
204 self.hotbar_slots_range().contains(&i)
205 }
206 }
207 }
208}
209
210pub 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 "e! {
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 "e! {
265 #length
266 },
267 false,
268 )
269}
270
271pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream {
272 let menu_name = &menu.name;
276 let menu_field_names = if menu.name == "Player" {
277 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 "e! {
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 "e! {
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 "e! {
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 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}