1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00
This commit is contained in:
mat 2023-03-13 21:10:21 -05:00
parent b44dc94274
commit 6286e953a6
17 changed files with 282 additions and 44 deletions

1
Cargo.lock generated
View file

@ -181,6 +181,7 @@ dependencies = [
"azalea-chat", "azalea-chat",
"azalea-client", "azalea-client",
"azalea-core", "azalea-core",
"azalea-inventory",
"azalea-physics", "azalea-physics",
"azalea-protocol", "azalea-protocol",
"azalea-registry", "azalea-registry",

View file

@ -3,7 +3,7 @@ use crate::{
disconnect::{DisconnectEvent, DisconnectPlugin}, disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents}, events::{Event, EventPlugin, LocalPlayerEvents},
interact::{CurrentSequenceNumber, InteractPlugin}, interact::{CurrentSequenceNumber, InteractPlugin},
inventory::{InventoryComponent, InventoryPlugin}, inventory_plugin::{InventoryComponent, InventoryPlugin},
local_player::{ local_player::{
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent, death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent, LocalPlayer, PhysicsState, SendPacketEvent,
@ -50,6 +50,7 @@ use bevy_ecs::{
entity::Entity, entity::Entity,
schedule::IntoSystemConfig, schedule::IntoSystemConfig,
schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel}, schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel},
system::{ResMut, Resource},
world::World, world::World,
}; };
use bevy_log::LogPlugin; use bevy_log::LogPlugin;
@ -59,7 +60,10 @@ use log::{debug, error};
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration}; use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration};
use thiserror::Error; use thiserror::Error;
use tokio::{sync::mpsc, time}; use tokio::{
sync::{broadcast, mpsc},
time,
};
use uuid::Uuid; use uuid::Uuid;
/// `Client` has the things that a user interacting with the library will want. /// `Client` has the things that a user interacting with the library will want.
@ -627,6 +631,36 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<(
} }
} }
/// A resource that contains a [`broadcast::Sender`] that will be sent every
/// time the ECS schedule is run.
///
/// This is useful for running code every schedule from async user code.
///
/// ```no_run
/// let mut receiver = {
/// let ecs = client.ecs.lock();
/// let schedule_broadcast = ecs.resource::<RanScheduleBroadcast>();
/// schedule_broadcast.subscribe()
/// };
/// while receiver.recv().await.is_ok() {
/// // do something
/// }
/// ```
#[derive(Resource, Deref)]
pub struct RanScheduleBroadcast(broadcast::Sender<()>);
fn send_ran_schedule_event(ran_schedule_broadcast: ResMut<RanScheduleBroadcast>) {
let _ = ran_schedule_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
pub struct RanSchedulePlugin;
impl Plugin for RanSchedulePlugin {
fn build(&self, app: &mut App) {
app.insert_resource(RanScheduleBroadcast(broadcast::channel(1).0))
.add_system(send_ran_schedule_event);
}
}
/// This plugin group will add all the default plugins necessary for Azalea to /// This plugin group will add all the default plugins necessary for Azalea to
/// work. /// work.
pub struct DefaultPlugins; pub struct DefaultPlugins;
@ -647,5 +681,6 @@ impl PluginGroup for DefaultPlugins {
.add(DisconnectPlugin) .add(DisconnectPlugin)
.add(PlayerMovePlugin) .add(PlayerMovePlugin)
.add(InteractPlugin) .add(InteractPlugin)
.add(RanSchedulePlugin)
} }
} }

View file

@ -30,10 +30,11 @@ impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<BlockInteractEvent>().add_systems( app.add_event::<BlockInteractEvent>().add_systems(
( (
handle_block_interact_event,
update_hit_result_component.after(clamp_look_direction), update_hit_result_component.after(clamp_look_direction),
handle_block_interact_event,
) )
.before(handle_send_packet_event), .before(handle_send_packet_event)
.chain(),
); );
} }
} }

View file

@ -1,12 +1,63 @@
use azalea_chat::FormattedText;
use azalea_core::BlockPos;
use azalea_inventory::{ItemSlot, Menu}; use azalea_inventory::{ItemSlot, Menu};
use azalea_registry::MenuKind;
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_ecs::{component::Component, entity::Entity, event::EventReader, system::Query}; use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
schedule::IntoSystemConfigs,
system::{Commands, Query},
};
use crate::{client::RanScheduleBroadcast, Client};
pub struct InventoryPlugin; pub struct InventoryPlugin;
impl Plugin for InventoryPlugin { impl Plugin for InventoryPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<ClientSideCloseContainerEvent>() app.add_event::<ClientSideCloseContainerEvent>()
.add_system(handle_client_side_close_container_event); .add_event::<MenuOpenedEvent>()
.add_systems(
(
handle_menu_opened_event,
handle_client_side_close_container_event,
)
.chain(),
);
}
}
#[derive(Component, Debug)]
pub struct WaitingForInventoryOpen;
impl Client {
pub async fn open_container(&mut self, pos: BlockPos) -> Option<Menu> {
self.ecs
.lock()
.entity_mut(self.entity)
.insert(WaitingForInventoryOpen);
self.block_interact(pos);
let mut receiver = {
let ecs = self.ecs.lock();
let schedule_broadcast = ecs.resource::<RanScheduleBroadcast>();
schedule_broadcast.subscribe()
};
while receiver.recv().await.is_ok() {
let ecs = self.ecs.lock();
if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() {
break;
}
}
let ecs = self.ecs.lock();
let inventory = ecs.get::<InventoryComponent>(self.entity);
if let Some(inventory) = inventory {
inventory.container_menu.clone()
} else {
None
}
} }
} }
@ -33,7 +84,7 @@ pub struct InventoryComponent {
pub carried: ItemSlot, pub carried: ItemSlot,
/// An identifier used by the server to track client inventory desyncs. /// An identifier used by the server to track client inventory desyncs.
pub state_id: u32, pub state_id: u32,
// minecraft also has these fields, but i don't need they're necessary?: // minecraft also has these fields, but i don't think they're necessary?:
// private final NonNullList<ItemStack> remoteSlots; // private final NonNullList<ItemStack> remoteSlots;
// private final IntList remoteDataSlots; // private final IntList remoteDataSlots;
// private ItemStack remoteCarried; // private ItemStack remoteCarried;
@ -70,9 +121,33 @@ impl Default for InventoryComponent {
} }
} }
/// Sent from the server when a menu (like a chest or crafting table) was
/// opened by the client.
pub struct MenuOpenedEvent {
pub entity: Entity,
pub window_id: u32,
pub menu_type: MenuKind,
pub title: FormattedText,
}
fn handle_menu_opened_event(
mut commands: Commands,
mut events: EventReader<MenuOpenedEvent>,
mut query: Query<&mut InventoryComponent>,
) {
for event in events.iter() {
commands
.entity(event.entity)
.remove::<WaitingForInventoryOpen>();
let mut inventory = query.get_mut(event.entity).unwrap();
inventory.id = event.window_id as i8;
inventory.container_menu = Some(Menu::from_kind(event.menu_type));
}
}
/// Close a container without notifying the server. /// Close a container without notifying the server.
/// ///
/// Note that this also gets fired from [`CloseContainerEvent`]. /// Note that this also gets fired when we get a [`CloseContainerEvent`].
pub struct ClientSideCloseContainerEvent { pub struct ClientSideCloseContainerEvent {
pub entity: Entity, pub entity: Entity,
} }

View file

@ -19,7 +19,7 @@ mod entity_query;
mod events; mod events;
mod get_mc_dir; mod get_mc_dir;
pub mod interact; pub mod interact;
pub mod inventory; pub mod inventory_plugin;
mod local_player; mod local_player;
mod movement; mod movement;
pub mod packet_handling; pub mod packet_handling;

View file

@ -39,7 +39,7 @@ use crate::{
chat::{ChatPacket, ChatReceivedEvent}, chat::{ChatPacket, ChatReceivedEvent},
client::TabList, client::TabList,
disconnect::DisconnectEvent, disconnect::DisconnectEvent,
inventory::{ClientSideCloseContainerEvent, InventoryComponent}, inventory_plugin::{ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent},
local_player::{GameProfileComponent, LocalGameMode, LocalPlayer}, local_player::{GameProfileComponent, LocalGameMode, LocalPlayer},
ClientInformation, PlayerInfo, ClientInformation, PlayerInfo,
}; };
@ -971,7 +971,17 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::MerchantOffers(_) => {} ClientboundGamePacket::MerchantOffers(_) => {}
ClientboundGamePacket::MoveVehicle(_) => {} ClientboundGamePacket::MoveVehicle(_) => {}
ClientboundGamePacket::OpenBook(_) => {} ClientboundGamePacket::OpenBook(_) => {}
ClientboundGamePacket::OpenScreen(_) => {} ClientboundGamePacket::OpenScreen(p) => {
let mut system_state: SystemState<EventWriter<MenuOpenedEvent>> =
SystemState::new(ecs);
let mut menu_opened_events = system_state.get_mut(ecs);
menu_opened_events.send(MenuOpenedEvent {
entity: player_entity,
window_id: p.container_id,
menu_type: p.menu_type,
title: p.title,
})
}
ClientboundGamePacket::OpenSignEditor(_) => {} ClientboundGamePacket::OpenSignEditor(_) => {}
ClientboundGamePacket::Ping(_) => {} ClientboundGamePacket::Ping(_) => {}
ClientboundGamePacket::PlaceGhostRecipe(_) => {} ClientboundGamePacket::PlaceGhostRecipe(_) => {}

View file

@ -5,6 +5,8 @@ use quote::quote;
pub fn generate(input: &DeclareMenus) -> TokenStream { pub fn generate(input: &DeclareMenus) -> TokenStream {
let mut slot_mut_match_variants = quote! {}; let mut slot_mut_match_variants = quote! {};
let mut len_match_variants = quote! {}; let mut len_match_variants = quote! {};
let mut kind_match_variants = quote! {};
let mut contents_match_variants = quote! {};
let mut hotbar_slot_start = 0; let mut hotbar_slot_start = 0;
let mut hotbar_slot_end = 0; let mut hotbar_slot_end = 0;
@ -12,6 +14,8 @@ pub fn generate(input: &DeclareMenus) -> TokenStream {
for menu in &input.menus { for menu in &input.menus {
slot_mut_match_variants.extend(generate_match_variant_for_slot_mut(menu)); slot_mut_match_variants.extend(generate_match_variant_for_slot_mut(menu));
len_match_variants.extend(generate_match_variant_for_len(menu)); len_match_variants.extend(generate_match_variant_for_len(menu));
kind_match_variants.extend(generate_match_variant_for_kind(menu));
contents_match_variants.extend(generate_match_variant_for_contents(menu));
// this part is only used to generate `Player::is_hotbar_slot` // this part is only used to generate `Player::is_hotbar_slot`
if menu.name == "Player" { if menu.name == "Player" {
@ -55,6 +59,19 @@ pub fn generate(input: &DeclareMenus) -> TokenStream {
#len_match_variants #len_match_variants
} }
} }
pub fn from_kind(kind: azalea_registry::MenuKind) -> Self {
match kind {
#kind_match_variants
}
}
/// Return the contents of the menu, not including the player's inventory.
pub fn contents(&self) -> Vec<ItemSlot> {
match self {
#contents_match_variants
}
}
} }
} }
} }
@ -114,6 +131,67 @@ pub fn generate_match_variant_for_len(menu: &Menu) -> TokenStream {
) )
} }
pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream {
// azalea_registry::MenuKind::Player => Menu::Player(Player::default()),
// 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" {
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_contents(menu: &Menu) -> TokenStream {
// Menu::Generic9x3(m) => {
// let mut contents = Vec::new();
// contents.extend(player.m.iter().copied());
// ...
// contents
// },
// Menu::Generic9x3(m) => {
// let mut contents = Vec::new();
// contents.extend(m.contents.iter().copied());
// contents
// },
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,
&quote! {
let mut items = Vec::with_capacity(#length);
#instructions
items
},
true,
)
}
fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream { fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream {
let menu_name = &menu.name; let menu_name = &menu.name;
let menu_field_names = if needs_fields { let menu_field_names = if needs_fields {

View file

@ -27,6 +27,21 @@ impl<const N: usize> Default for SlotList<N> {
} }
} }
impl Menu {
/// Get the [`Player`] from this [`Menu`].
///
/// # Panics
///
/// Will panic if the menu isn't `Menu::Player`.
pub fn as_player(&self) -> &Player {
if let Menu::Player(player) = &self {
player
} else {
unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.")
}
}
}
// the player inventory part is always the last 36 slots (except in the Player // the player inventory part is always the last 36 slots (except in the Player
// menu), so we don't have to explicitly specify it // menu), so we don't have to explicitly specify it
@ -142,18 +157,3 @@ declare_menus! {
result: 1, result: 1,
}, },
} }
impl Menu {
/// Get the [`Player`] from this [`Menu`].
///
/// # Panics
///
/// Will panic if the menu isn't `Menu::Player`.
pub fn as_player(&self) -> &Player {
if let Menu::Player(player) = &self {
player
} else {
unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.")
}
}
}

View file

@ -10,6 +10,15 @@ pub enum ItemSlot {
Present(ItemSlotData), Present(ItemSlotData),
} }
impl ItemSlot {
pub fn is_empty(&self) -> bool {
matches!(self, ItemSlot::Empty)
}
pub fn is_present(&self) -> bool {
matches!(self, ItemSlot::Present(_))
}
}
/// An item in an inventory, with a count and NBT. Usually you want [`ItemSlot`] /// An item in an inventory, with a count and NBT. Usually you want [`ItemSlot`]
/// or [`azalea_registry::Item`] instead. /// or [`azalea_registry::Item`] instead.
#[derive(Debug, Clone, McBuf)] #[derive(Debug, Clone, McBuf)]

View file

@ -6,6 +6,6 @@ use azalea_protocol_macros::ClientboundGamePacket;
pub struct ClientboundOpenScreenPacket { pub struct ClientboundOpenScreenPacket {
#[var] #[var]
pub container_id: u32, pub container_id: u32,
pub menu_type: azalea_registry::Menu, pub menu_type: azalea_registry::MenuKind,
pub title: FormattedText, pub title: FormattedText,
} }

View file

@ -3010,7 +3010,7 @@ enum MemoryModuleKind {
} }
registry! { registry! {
enum Menu { enum MenuKind {
Generic9x1 => "minecraft:generic_9x1", Generic9x1 => "minecraft:generic_9x1",
Generic9x2 => "minecraft:generic_9x2", Generic9x2 => "minecraft:generic_9x2",
Generic9x3 => "minecraft:generic_9x3", Generic9x3 => "minecraft:generic_9x3",

View file

@ -18,6 +18,7 @@ azalea-block = { version = "0.6.0", path = "../azalea-block" }
azalea-chat = { version = "0.6.0", path = "../azalea-chat" } azalea-chat = { version = "0.6.0", path = "../azalea-chat" }
azalea-client = { version = "0.6.0", path = "../azalea-client" } azalea-client = { version = "0.6.0", path = "../azalea-client" }
azalea-core = { version = "0.6.0", path = "../azalea-core" } azalea-core = { version = "0.6.0", path = "../azalea-core" }
azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-physics = { version = "0.6.0", path = "../azalea-physics" } azalea-physics = { version = "0.6.0", path = "../azalea-physics" }
azalea-protocol = { version = "0.6.0", path = "../azalea-protocol" } azalea-protocol = { version = "0.6.0", path = "../azalea-protocol" }
azalea-registry = { version = "0.6.0", path = "../azalea-registry" } azalea-registry = { version = "0.6.0", path = "../azalea-registry" }

View file

@ -48,7 +48,10 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
return Ok(()); return Ok(());
}; };
bot.goto(chest_block.into()); bot.goto(chest_block.into());
let chest = bot.open_container(&chest_block).await.unwrap(); let Some(chest) = bot.open_container(&chest_block).await else {
println!("Couldn't open chest");
return Ok(());
};
bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks") bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks")
.await; .await;
chest.close().await; chest.close().await;

View file

@ -6,7 +6,8 @@ use azalea::ecs::query::With;
use azalea::entity::metadata::Player; use azalea::entity::metadata::Player;
use azalea::entity::{EyeHeight, Position}; use azalea::entity::{EyeHeight, Position};
use azalea::interact::HitResultComponent; use azalea::interact::HitResultComponent;
use azalea::inventory::InventoryComponent; use azalea::inventory::ItemSlot;
use azalea::inventory_plugin::InventoryComponent;
use azalea::pathfinder::BlockPosGoal; use azalea::pathfinder::BlockPosGoal;
use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection}; use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
use azalea::{Account, Client, Event}; use azalea::{Account, Client, Event};
@ -149,17 +150,17 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
println!("inventory: {:?}", inventory.menu()); println!("inventory: {:?}", inventory.menu());
} }
"findblock" => { "findblock" => {
let target_pos = bot.world().read().find_block( let target_pos = bot
bot.position(), .world()
&azalea_registry::Block::DiamondBlock.into(), .read()
); .find_block(bot.position(), &azalea::Block::DiamondBlock.into());
bot.chat(&format!("target_pos: {target_pos:?}",)); bot.chat(&format!("target_pos: {target_pos:?}",));
} }
"gotoblock" => { "gotoblock" => {
let target_pos = bot.world().read().find_block( let target_pos = bot
bot.position(), .world()
&azalea_registry::Block::DiamondBlock.into(), .read()
); .find_block(bot.position(), &azalea::Block::DiamondBlock.into());
if let Some(target_pos) = target_pos { if let Some(target_pos) = target_pos {
// +1 to stand on top of the block // +1 to stand on top of the block
bot.goto(BlockPosGoal::from(target_pos.up(1))); bot.goto(BlockPosGoal::from(target_pos.up(1)));
@ -171,7 +172,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
let target_pos = bot let target_pos = bot
.world() .world()
.read() .read()
.find_block(bot.position(), &azalea_registry::Block::Lever.into()); .find_block(bot.position(), &azalea::Block::Lever.into());
let Some(target_pos) = target_pos else { let Some(target_pos) = target_pos else {
bot.chat("no lever found"); bot.chat("no lever found");
return Ok(()) return Ok(())
@ -184,6 +185,27 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
let hit_result = bot.get_component::<HitResultComponent>(); let hit_result = bot.get_component::<HitResultComponent>();
bot.chat(&format!("hit_result: {hit_result:?}",)); bot.chat(&format!("hit_result: {hit_result:?}",));
} }
"chest" => {
let target_pos = bot
.world()
.read()
.find_block(bot.position(), &azalea::Block::Chest.into());
let Some(target_pos) = target_pos else {
bot.chat("no chest found");
return Ok(())
};
bot.look_at(target_pos.center());
let container = bot.open_container(target_pos).await;
if let Some(container) = container {
for item in container.contents() {
if let ItemSlot::Present(item) = item {
println!("item: {:?}", item);
}
}
} else {
println!("no container found");
}
}
_ => {} _ => {}
} }
} }

View file

@ -38,7 +38,7 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
bot.goto(pathfinder::Goals::NearXZ(5, azalea::BlockXZ(0, 0))) bot.goto(pathfinder::Goals::NearXZ(5, azalea::BlockXZ(0, 0)))
.await; .await;
let chest = bot let chest = bot
.open_container(&bot.world().find_block(azalea_registry::Block::Chest)) .open_container(&bot.world().find_block(azalea::Block::Chest))
.await .await
.unwrap(); .unwrap();
bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks") bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks")
@ -46,9 +46,7 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
chest.close().await; chest.close().await;
let crafting_table = bot let crafting_table = bot
.open_crafting_table( .open_crafting_table(&bot.world.find_block(azalea::Block::CraftingTable))
&bot.world.find_block(azalea_registry::Block::CraftingTable),
)
.await .await
.unwrap(); .unwrap();
bot.craft(&crafting_table, &bot.recipe_for("minecraft:sticks")) bot.craft(&crafting_table, &bot.recipe_for("minecraft:sticks"))

View file

@ -10,6 +10,7 @@ use app::{App, Plugin, PluginGroup};
pub use azalea_block as blocks; pub use azalea_block as blocks;
pub use azalea_client::*; pub use azalea_client::*;
pub use azalea_core::{BlockPos, Vec3}; pub use azalea_core::{BlockPos, Vec3};
pub use azalea_inventory as inventory;
pub use azalea_protocol as protocol; pub use azalea_protocol as protocol;
pub use azalea_registry::{Block, EntityKind}; pub use azalea_registry::{Block, EntityKind};
pub use azalea_world::{entity, Instance}; pub use azalea_world::{entity, Instance};

View file

@ -16,12 +16,16 @@ def generate_registries(registries: dict):
# Stone => "minecraft:stone" # Stone => "minecraft:stone"
# }); # });
registry_name = registry_name.split(':')[1]
if registry_name.endswith('_type'): if registry_name.endswith('_type'):
# change _type to _kind because that's Rustier (and because _type # change _type to _kind because that's Rustier (and because _type
# is a reserved keyword) # is a reserved keyword)
registry_name = registry_name[:-5] + '_kind' registry_name = registry_name[:-5] + '_kind'
elif registry_name == 'menu':
registry_name = 'menu_kind'
registry_struct_name = to_camel_case(registry_name.split(':')[1]) registry_struct_name = to_camel_case(registry_name)
registry_code = [] registry_code = []
registry_code.append(f'enum {registry_struct_name} {{') registry_code.append(f'enum {registry_struct_name} {{')