diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2023-05-03 20:57:27 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-03 20:57:27 -0500 |
| commit | 634cb8d72c6608512aedba19e5cd669104bc35ea (patch) | |
| tree | f8e76ce9eb43403d29cc0cbcf9a4f51522419dc2 /azalea-inventory/azalea-inventory-macros/src | |
| parent | 1fb4418f2c9cbd004c64c2f23d2d0352ee12c0e5 (diff) | |
| download | azalea-drasl-634cb8d72c6608512aedba19e5cd669104bc35ea.tar.xz | |
Inventory (#48)
* start adding azalea-inventory
* design more of how inventories are defined
* start working on az-inv-macros
* inventory macro works
* start adding inventory codegen
* update some deps
* add inventory codegen
* manually write inventory menus
* put the inventories in Client
* start on containersetcontent
* inventory menu should hopefully work
* checks in containersetcontent
* format a comment
* move some variant matches
* inventory.rs
* inventory stuff
* more inventory stuff
* inventory/container tracking works
* start adding interact function
* sequence number
* start adding HitResultComponent
* implement traverse_blocks
* start adding clip
* add clip function
* update_hit_result_component
* start trying to fix
* fix
* make some stuff simpler
* clippy
* lever
* chest
* container handle
* fix ambiguity
* fix some doc tests
* move some container stuff from az-client to azalea
* clicking container
* start implementing simulate_click
* keep working on simulate click
* implement more of simulate_click
this is really boring
* inventory fixes
* start implementing shift clicking
* fix panic in azalea-chat i hope
* shift clicking implemented
* more inventory stuff
* fix items not showing in containers sometimes
* fix test
* fix all warnings
* remove a println
---------
Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea-inventory/azalea-inventory-macros/src')
6 files changed, 745 insertions, 0 deletions
diff --git a/azalea-inventory/azalea-inventory-macros/src/lib.rs b/azalea-inventory/azalea-inventory-macros/src/lib.rs new file mode 100644 index 00000000..d3faa091 --- /dev/null +++ b/azalea-inventory/azalea-inventory-macros/src/lib.rs @@ -0,0 +1,45 @@ +mod location_enum; +mod menu_enum; +mod menu_impl; +mod parse_macro; +mod utils; + +use parse_macro::{DeclareMenus, Field}; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{self, parse_macro_input, Ident}; + +#[proc_macro] +pub fn declare_menus(input: TokenStream) -> TokenStream { + let mut input = parse_macro_input!(input as DeclareMenus); + + // implicitly add a `player` field at the end unless an `inventory` field + // is present + for menu in &mut input.menus { + let mut inventory_field_missing = true; + for field in &menu.fields { + if matches!(field.name.to_string().as_str(), "inventory" | "player") { + inventory_field_missing = false; + } + } + if inventory_field_missing { + menu.fields.push(Field { + name: Ident::new("player", Span::call_site()), + length: 36, + }) + } + } + + let menu_enum = menu_enum::generate(&input); + let menu_impl = menu_impl::generate(&input); + let location_enum = location_enum::generate(&input); + + quote! { + #menu_enum + #menu_impl + + #location_enum + } + .into() +} diff --git a/azalea-inventory/azalea-inventory-macros/src/location_enum.rs b/azalea-inventory/azalea-inventory-macros/src/location_enum.rs new file mode 100644 index 00000000..6cec88e6 --- /dev/null +++ b/azalea-inventory/azalea-inventory-macros/src/location_enum.rs @@ -0,0 +1,59 @@ +use crate::{parse_macro::DeclareMenus, utils::to_pascal_case}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Ident; + +pub fn generate(input: &DeclareMenus) -> TokenStream { + // pub enum MenuLocation { + // Player(PlayerMenuLocation), + // ... + // } + // pub enum PlayerMenuLocation { + // CraftResult, + // Craft, + // Armor, + // Inventory, + // Offhand, + // } + // ... + + let mut menu_location_variants = quote! {}; + let mut enums = quote! {}; + for menu in &input.menus { + let name_snake_case = &menu.name; + let variant_name = Ident::new( + &to_pascal_case(&name_snake_case.to_string()), + name_snake_case.span(), + ); + let enum_name = Ident::new( + &format!("{}MenuLocation", variant_name), + variant_name.span(), + ); + menu_location_variants.extend(quote! { + #variant_name(#enum_name), + }); + let mut individual_menu_location_variants = quote! {}; + for field in &menu.fields { + let field_name = &field.name; + let variant_name = + Ident::new(&to_pascal_case(&field_name.to_string()), field_name.span()); + individual_menu_location_variants.extend(quote! { + #variant_name, + }); + } + enums.extend(quote! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum #enum_name { + #individual_menu_location_variants + } + }); + } + + quote! { + pub enum MenuLocation { + #menu_location_variants + } + + #enums + } +} diff --git a/azalea-inventory/azalea-inventory-macros/src/menu_enum.rs b/azalea-inventory/azalea-inventory-macros/src/menu_enum.rs new file mode 100644 index 00000000..a9e4f430 --- /dev/null +++ b/azalea-inventory/azalea-inventory-macros/src/menu_enum.rs @@ -0,0 +1,70 @@ +//! Generate the `enum menu` and nothing else. Implementations are in +//! impl_menu.rs + +use crate::parse_macro::{DeclareMenus, Field, Menu}; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn generate(input: &DeclareMenus) -> TokenStream { + let mut variants = quote! {}; + let mut player_fields = None; + for menu in &input.menus { + if menu.name == "Player" { + player_fields = Some(generate_fields(&menu.fields, true)); + } else { + variants.extend(generate_variant_for_menu(menu)); + } + } + let player_fields = player_fields.expect("Player variant must be present"); + + quote! { + #[derive(Clone, Debug, Default)] + pub struct Player { + #player_fields + } + + /// A menu, which is a fixed collection of slots. + #[derive(Clone, Debug)] + pub enum Menu { + Player(Player), + #variants + } + } +} + +/// Player { +/// craft_result: ItemSlot, +/// craft: [ItemSlot; 4], +/// armor: [ItemSlot; 4], +/// inventory: [ItemSlot; 36], +/// offhand: ItemSlot, +/// }, +fn generate_variant_for_menu(menu: &Menu) -> TokenStream { + let name = &menu.name; + let fields = generate_fields(&menu.fields, false); + + quote! { + #name { + #fields + }, + } +} + +fn generate_fields(fields: &[Field], public: bool) -> TokenStream { + let mut generated_fields = quote! {}; + for field in fields { + let field_length = field.length; + let field_type = if field.length == 1 { + quote! { ItemSlot } + } else { + quote! { SlotList<#field_length> } + }; + let field_name = &field.name; + if public { + generated_fields.extend(quote! { pub #field_name: #field_type, }) + } else { + generated_fields.extend(quote! { #field_name: #field_type, }) + } + } + generated_fields +} diff --git a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs new file mode 100644 index 00000000..804f69f2 --- /dev/null +++ b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs @@ -0,0 +1,448 @@ +use crate::{ + parse_macro::{DeclareMenus, Menu}, + utils::{to_pascal_case, to_snake_case}, +}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Ident; + +pub fn generate(input: &DeclareMenus) -> TokenStream { + let mut slot_mut_match_variants = quote! {}; + let mut slot_match_variants = quote! {}; + let mut len_match_variants = quote! {}; + let mut kind_match_variants = quote! {}; + let mut slots_match_variants = quote! {}; + let mut contents_match_variants = quote! {}; + let mut location_match_variants = quote! {}; + let mut player_slots_range_match_variants = quote! {}; + + let mut player_consts = quote! {}; + let mut menu_consts = quote! {}; + + let mut hotbar_slots_start = 0; + let mut hotbar_slots_end = 0; + let mut inventory_without_hotbar_slots_start = 0; + let mut inventory_without_hotbar_slots_end = 0; + + for menu in &input.menus { + slot_mut_match_variants.extend(generate_match_variant_for_slot_mut(menu, true)); + slot_match_variants.extend(generate_match_variant_for_slot_mut(menu, false)); + len_match_variants.extend(generate_match_variant_for_len(menu)); + kind_match_variants.extend(generate_match_variant_for_kind(menu)); + slots_match_variants.extend(generate_match_variant_for_slots(menu)); + contents_match_variants.extend(generate_match_variant_for_contents(menu)); + location_match_variants.extend(generate_match_variant_for_location(menu)); + player_slots_range_match_variants + .extend(generate_match_variant_for_player_slots_range(menu)); + + // this part is only used to generate `Player::is_hotbar_slot` + if menu.name == "Player" { + let mut i = 0; + for field in &menu.fields { + let field_name = &field.name; + let start = i; + i += field.length; + let end = i - 1; + + if field_name == "inventory" { + // it only subtracts 8 here since it's inclusive (there's 9 total hotbar slots) + hotbar_slots_start = end - 8; + hotbar_slots_end = end; + + inventory_without_hotbar_slots_start = start; + inventory_without_hotbar_slots_end = end - 9; + } + + if start == end { + let const_name = Ident::new( + &format!("{}_SLOT", field_name.to_string().to_uppercase()), + field_name.span(), + ); + player_consts.extend(quote! { + pub const #const_name: usize = #start; + }); + } else { + let const_name = Ident::new( + &format!("{}_SLOTS", field_name.to_string().to_uppercase()), + field_name.span(), + ); + player_consts.extend(quote! { + pub const #const_name: RangeInclusive<usize> = #start..=#end; + }); + } + } + } else { + menu_consts.extend(generate_menu_consts(menu)); + } + } + + assert!(hotbar_slots_start != 0 && hotbar_slots_end != 0); + quote! { + impl Player { + pub const HOTBAR_SLOTS: RangeInclusive<usize> = #hotbar_slots_start..=#hotbar_slots_end; + pub const INVENTORY_WITHOUT_HOTBAR_SLOTS: RangeInclusive<usize> = #inventory_without_hotbar_slots_start..=#inventory_without_hotbar_slots_end; + #player_consts + + /// Returns whether the given protocol index is in the player's hotbar. + /// + /// Equivalent to `Player::HOTBAR_SLOTS.contains(&i)`. + pub fn is_hotbar_slot(i: usize) -> bool { + Self::HOTBAR_SLOTS.contains(&i) + } + } + + impl Menu { + #menu_consts + + /// Get a mutable reference to the [`ItemSlot`] at the given protocol index. + /// + /// If you're trying to get an item in a menu without caring about + /// protocol indexes, you should just `match` it and index the + /// [`ItemSlot`] you get. + /// + /// Use [`Menu::slot`] if you don't need a mutable reference to the slot. + /// + /// # Errors + /// + /// Returns `None` if the index is out of bounds. + #[inline] + pub fn slot_mut(&mut self, i: usize) -> Option<&mut ItemSlot> { + Some(match self { + #slot_mut_match_variants + }) + } + + /// Get a reference to the [`ItemSlot`] at the given protocol index. + /// + /// If you're trying to get an item in a menu without caring about + /// protocol indexes, you should just `match` it and index the + /// [`ItemSlot`] you get. + /// + /// Use [`Menu::slot_mut`] if you need a mutable reference to the slot. + /// + /// # Errors + /// + /// Returns `None` if the index is out of bounds. + pub fn slot(&self, i: usize) -> Option<&ItemSlot> { + Some(match self { + #slot_match_variants + }) + } + + /// Returns the number of slots in the menu. + #[allow(clippy::len_without_is_empty)] + pub const fn len(&self) -> usize { + match self { + #len_match_variants + } + } + + pub fn from_kind(kind: azalea_registry::MenuKind) -> Self { + match kind { + #kind_match_variants + } + } + + /// Return the contents of the menu, including the player's inventory. + /// + /// The indexes in this will match up with [`Menu::slot_mut`]. + /// + /// If you don't want to include the player's inventory, use [`Menu::contents`] instead. + pub fn slots(&self) -> Vec<ItemSlot> { + match self { + #slots_match_variants + } + } + + /// Return the contents of the menu, not including the player's inventory. + /// + /// If you want to include the player's inventory, use [`Menu::slots`] instead. + pub fn contents(&self) -> Vec<ItemSlot> { + match self { + #contents_match_variants + } + } + + pub fn location_for_slot(&self, i: usize) -> Option<MenuLocation> { + Some(match self { + #location_match_variants + }) + } + + /// Get the range of slot indexes that contain the player's inventory. This may be different for each menu. + pub fn player_slots_range(&self) -> RangeInclusive<usize> { + match self { + #player_slots_range_match_variants + } + } + + /// Get the range of slot indexes that contain the player's hotbar. This may be different for each menu. + pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> { + // hotbar is always last 9 slots in the player's inventory + ((*self.player_slots_range().end() - 8)..=*self.player_slots_range().end()) + } + + /// Get the range of slot indexes that contain the player's inventory, not including the hotbar. This may be different for each menu. + pub fn player_slots_without_hotbar_range(&self) -> RangeInclusive<usize> { + (*self.player_slots_range().start()..=*self.player_slots_range().end() - 9) + } + + /// Returns whether the given index would be in the player's hotbar. + /// + /// Equivalent to `self.hotbar_slots_range().contains(&i)`. + pub fn is_hotbar_slot(&self, i: usize) -> bool { + self.hotbar_slots_range().contains(&i) + } + } + } +} + +/// Menu::Player { +/// craft_result, +/// craft, +/// armor, +/// inventory, +/// offhand, +/// } => { +/// match i { +/// 0 => craft_result, +/// 1..=4 => craft, +/// 5..=8 => armor, +/// // ... +/// _ => return None, +/// } +/// } // ... +pub fn generate_match_variant_for_slot_mut(menu: &Menu, mutable: bool) -> TokenStream { + let mut match_arms = quote! {}; + let mut i = 0; + for field in &menu.fields { + let field_name = &field.name; + let start = i; + i += field.length; + let end = i - 1; + match_arms.extend(if start == end { + quote! { #start => #field_name, } + } else if start == 0 { + if mutable { + quote! { #start..=#end => &mut #field_name[i], } + } else { + quote! { #start..=#end => &#field_name[i], } + } + } else if mutable { + quote! { #start..=#end => &mut #field_name[i - #start], } + } else { + quote! { #start..=#end => &#field_name[i - #start], } + }); + } + + generate_matcher( + menu, + "e! { + match i { + #match_arms + _ => return None + } + }, + true, + ) +} + +pub fn generate_match_variant_for_len(menu: &Menu) -> TokenStream { + let length = menu.fields.iter().map(|f| f.length).sum::<usize>(); + generate_matcher( + menu, + "e! { + #length + }, + false, + ) +} + +pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream { + // azalea_registry::MenuKind::Generic9x3 => Menu::Generic9x3 { contents: + // Default::default(), player: Default::default() }, + + let menu_name = &menu.name; + let menu_field_names = if menu.name == "Player" { + // player isn't in MenuKind + return quote! {}; + } else { + let mut menu_field_names = quote! {}; + for field in &menu.fields { + let field_name = &field.name; + menu_field_names.extend(quote! { #field_name: Default::default(), }) + } + quote! { { #menu_field_names } } + }; + + quote! { + azalea_registry::MenuKind::#menu_name => Menu::#menu_name #menu_field_names, + } +} + +pub fn generate_match_variant_for_slots(menu: &Menu) -> TokenStream { + let mut instructions = quote! {}; + let mut length = 0; + for field in &menu.fields { + let field_name = &field.name; + instructions.extend(if field.length == 1 { + quote! { items.push(#field_name.clone()); } + } else { + quote! { items.extend(#field_name.iter().cloned()); } + }); + length += field.length; + } + + generate_matcher( + menu, + "e! { + let mut items = Vec::with_capacity(#length); + #instructions + items + }, + true, + ) +} + +pub fn generate_match_variant_for_contents(menu: &Menu) -> TokenStream { + let mut instructions = quote! {}; + let mut length = 0; + for field in &menu.fields { + let field_name = &field.name; + if field_name == "player" { + continue; + } + instructions.extend(if field.length == 1 { + quote! { items.push(#field_name.clone()); } + } else { + quote! { items.extend(#field_name.iter().cloned()); } + }); + length += field.length; + } + + generate_matcher( + menu, + "e! { + let mut items = Vec::with_capacity(#length); + #instructions + items + }, + true, + ) +} + +pub fn generate_match_variant_for_location(menu: &Menu) -> TokenStream { + let mut match_arms = quote! {}; + let mut i = 0; + + let menu_name = Ident::new(&to_pascal_case(&menu.name.to_string()), menu.name.span()); + let menu_enum_name = Ident::new(&format!("{menu_name}MenuLocation"), menu_name.span()); + + for field in &menu.fields { + let field_name = Ident::new(&to_pascal_case(&field.name.to_string()), field.name.span()); + let start = i; + i += field.length; + let end = i - 1; + match_arms.extend(if start == end { + quote! { #start => #menu_enum_name::#field_name, } + } else { + quote! { #start..=#end => #menu_enum_name::#field_name, } + }); + } + + generate_matcher( + menu, + "e! { + MenuLocation::#menu_name(match i { + #match_arms + _ => return None + }) + }, + false, + ) +} + +pub fn generate_match_variant_for_player_slots_range(menu: &Menu) -> TokenStream { + // Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS_RANGE,, + // Menu::Generic9x3 { .. } => Menu::GENERIC9X3_SLOTS_RANGE, + // .. + + match menu.name.to_string().as_str() { + "Player" => { + quote! { + Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS, + } + } + _ => { + let menu_name = &menu.name; + let menu_slots_range_name = Ident::new( + &format!( + "{}_PLAYER_SLOTS", + to_snake_case(&menu.name.to_string()).to_uppercase() + ), + menu.name.span(), + ); + quote! { + Menu::#menu_name { .. } => Menu::#menu_slots_range_name, + } + } + } +} + +fn generate_menu_consts(menu: &Menu) -> TokenStream { + let mut menu_consts = quote! {}; + + let mut i = 0; + + for field in &menu.fields { + let field_name_start = format!( + "{}_{}", + to_snake_case(&menu.name.to_string()).to_uppercase(), + to_snake_case(&field.name.to_string()).to_uppercase() + ); + let field_index_start = i; + i += field.length; + let field_index_end = i - 1; + + if field.length == 1 { + let field_name = Ident::new( + format!("{}_SLOT", field_name_start).as_str(), + field.name.span(), + ); + menu_consts.extend(quote! { pub const #field_name: usize = #field_index_start; }); + } else { + let field_name = Ident::new( + format!("{}_SLOTS", field_name_start).as_str(), + field.name.span(), + ); + menu_consts.extend(quote! { pub const #field_name: RangeInclusive<usize> = #field_index_start..=#field_index_end; }); + } + } + + menu_consts +} + +pub fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream { + let menu_name = &menu.name; + let menu_field_names = if needs_fields { + let mut menu_field_names = quote! {}; + for field in &menu.fields { + let field_name = &field.name; + menu_field_names.extend(quote! { #field_name, }) + } + menu_field_names + } else { + quote! { .. } + }; + + let matcher = if menu.name == "Player" { + quote! { (Player { #menu_field_names }) } + } else { + quote! { { #menu_field_names } } + }; + quote! { + Menu::#menu_name #matcher => { + #match_arms + }, + } +} diff --git a/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs b/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs new file mode 100644 index 00000000..8eada4ec --- /dev/null +++ b/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs @@ -0,0 +1,69 @@ +use syn::{ + self, braced, + parse::{Parse, ParseStream, Result}, + Ident, LitInt, Token, +}; + +/// An identifier, colon, and number +/// `craft_result: 1` +pub struct Field { + pub name: Ident, + pub length: usize, +} +impl Parse for Field { + fn parse(input: ParseStream) -> Result<Self> { + let name = input.parse::<Ident>()?; + let _ = input.parse::<Token![:]>()?; + let length = input.parse::<LitInt>()?.base10_parse()?; + Ok(Self { name, length }) + } +} + +/// An identifier and a list of `Field` in curly brackets +/// ```rust,ignore +/// Player { +/// craft_result: 1, +/// ... +/// } +/// ``` +pub struct Menu { + /// The menu name, e.g. `Player` + pub name: Ident, + pub fields: Vec<Field>, +} + +impl Parse for Menu { + fn parse(input: ParseStream) -> Result<Self> { + let name = input.parse::<Ident>()?; + + let content; + braced!(content in input); + let fields = content + .parse_terminated::<Field, Token![,]>(Field::parse)? + .into_iter() + .collect(); + + Ok(Self { name, fields }) + } +} + +/// A list of `Menu`s +/// ```rust,ignore +/// Player { +/// craft_result: 1, +/// ... +/// }, +/// ... +/// ``` +pub struct DeclareMenus { + pub menus: Vec<Menu>, +} +impl Parse for DeclareMenus { + fn parse(input: ParseStream) -> Result<Self> { + let menus = input + .parse_terminated::<Menu, Token![,]>(Menu::parse)? + .into_iter() + .collect(); + Ok(Self { menus }) + } +} diff --git a/azalea-inventory/azalea-inventory-macros/src/utils.rs b/azalea-inventory/azalea-inventory-macros/src/utils.rs new file mode 100644 index 00000000..568c9f71 --- /dev/null +++ b/azalea-inventory/azalea-inventory-macros/src/utils.rs @@ -0,0 +1,54 @@ +pub fn to_pascal_case(s: &str) -> String { + // we get the first item later so this is to make it impossible for that + // to error + if s.is_empty() { + return String::new(); + } + + let mut result = String::new(); + let mut prev_was_underscore = true; // set to true by default so the first character is capitalized + if s.chars().next().unwrap().is_numeric() { + result.push('_'); + } + for c in s.chars() { + if c == '_' { + prev_was_underscore = true; + } else if prev_was_underscore { + result.push(c.to_ascii_uppercase()); + prev_was_underscore = false; + } else { + result.push(c); + } + } + result +} + +pub fn to_snake_case(s: &str) -> String { + let mut result = String::new(); + let mut prev_was_uppercase = true; + for c in s.chars() { + if c.is_ascii_uppercase() { + if !prev_was_uppercase { + result.push('_'); + } + result.push(c.to_ascii_lowercase()); + prev_was_uppercase = true; + } else { + result.push(c); + prev_was_uppercase = false; + } + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_snake_case() { + assert_eq!(to_snake_case("HelloWorld"), "hello_world"); + assert_eq!(to_snake_case("helloWorld"), "hello_world"); + assert_eq!(to_snake_case("hello_world"), "hello_world"); + } +} |
