use std::collections::BTreeMap; use anyhow::Context; use heck::{ToPascalCase, ToShoutySnakeCase, ToSnakeCase}; use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use valence_build_utils::{ident, rerun_if_changed, write_generated_file}; #[derive(Deserialize, Clone, Debug)] struct Entity { #[serde(rename = "type")] typ: Option, translation_key: Option, fields: Vec, parent: Option, } #[derive(Deserialize, Clone, Debug)] struct EntityTypes { entity_type: BTreeMap, } #[derive(Deserialize, Clone, Debug)] struct Field { name: String, index: u8, #[serde(flatten)] default_value: Value, } #[derive(Deserialize, Clone, Debug)] #[serde(tag = "type", content = "default_value", rename_all = "snake_case")] enum Value { Byte(i8), Integer(i32), Long(i64), Float(f32), String(String), TextComponent(String), OptionalTextComponent(Option), ItemStack(String), Boolean(bool), Rotation { pitch: f32, yaw: f32, roll: f32, }, BlockPos(BlockPos), OptionalBlockPos(Option), Facing(String), OptionalUuid(Option), BlockState(String), OptionalBlockState(Option), NbtCompound(String), Particle(String), VillagerData { #[serde(rename = "type")] typ: String, profession: String, level: i32, }, OptionalInt(Option), EntityPose(String), CatVariant(String), FrogVariant(String), OptionalGlobalPos(Option<()>), // TODO PaintingVariant(String), SnifferState(String), Vector3f { x: f32, y: f32, z: f32, }, Quaternionf { x: f32, y: f32, z: f32, w: f32, }, } #[derive(Deserialize, Debug, Clone, Copy)] struct BlockPos { x: i32, y: i32, z: i32, } impl Value { pub fn type_id(&self) -> u8 { match self { Value::Byte(_) => 0, Value::Integer(_) => 1, Value::Long(_) => 2, Value::Float(_) => 3, Value::String(_) => 4, Value::TextComponent(_) => 5, Value::OptionalTextComponent(_) => 6, Value::ItemStack(_) => 7, Value::Boolean(_) => 8, Value::Rotation { .. } => 9, Value::BlockPos(_) => 10, Value::OptionalBlockPos(_) => 11, Value::Facing(_) => 12, Value::OptionalUuid(_) => 13, Value::BlockState(_) => 14, Value::OptionalBlockState(_) => 15, Value::NbtCompound(_) => 16, Value::Particle(_) => 17, Value::VillagerData { .. } => 18, Value::OptionalInt(_) => 19, Value::EntityPose(_) => 20, Value::CatVariant(_) => 21, Value::FrogVariant(_) => 22, Value::OptionalGlobalPos(_) => 23, Value::PaintingVariant(_) => 24, Value::SnifferState(_) => 25, Value::Vector3f { .. } => 26, Value::Quaternionf { .. } => 27, } } pub fn field_type(&self) -> TokenStream { match self { Value::Byte(_) => quote!(i8), Value::Integer(_) => quote!(i32), Value::Long(_) => quote!(i64), Value::Float(_) => quote!(f32), Value::String(_) => quote!(String), Value::TextComponent(_) => quote!(valence_core::text::Text), Value::OptionalTextComponent(_) => quote!(Option), Value::ItemStack(_) => quote!(valence_core::item::ItemStack), Value::Boolean(_) => quote!(bool), Value::Rotation { .. } => quote!(crate::EulerAngle), Value::BlockPos(_) => quote!(valence_core::block_pos::BlockPos), Value::OptionalBlockPos(_) => quote!(Option), Value::Facing(_) => quote!(valence_core::direction::Direction), Value::OptionalUuid(_) => quote!(Option<::uuid::Uuid>), Value::BlockState(_) => quote!(valence_block::BlockState), Value::OptionalBlockState(_) => quote!(valence_block::BlockState), Value::NbtCompound(_) => quote!(valence_nbt::Compound), Value::Particle(_) => quote!(valence_core::packet::s2c::play::particle::Particle), Value::VillagerData { .. } => quote!(crate::VillagerData), Value::OptionalInt(_) => quote!(Option), Value::EntityPose(_) => quote!(crate::Pose), Value::CatVariant(_) => quote!(crate::CatKind), Value::FrogVariant(_) => quote!(crate::FrogKind), Value::OptionalGlobalPos(_) => quote!(()), // TODO Value::PaintingVariant(_) => quote!(crate::PaintingKind), Value::SnifferState(_) => quote!(crate::SnifferState), Value::Vector3f { .. } => quote!(glam::f32::Vec3), Value::Quaternionf { .. } => quote!(glam::f32::Quat), } } pub fn default_expr(&self) -> TokenStream { match self { Value::Byte(b) => quote!(#b), Value::Integer(i) => quote!(#i), Value::Long(l) => quote!(#l), Value::Float(f) => quote!(#f), Value::String(s) => quote!(#s.to_owned()), Value::TextComponent(txt) => { assert!(txt.is_empty()); quote!(valence_core::text::Text::default()) } Value::OptionalTextComponent(t) => { assert!(t.is_none()); quote!(None) } Value::ItemStack(stack) => { assert_eq!(stack, "1 air"); quote!(valence_core::item::ItemStack::default()) } Value::Boolean(b) => quote!(#b), Value::Rotation { pitch, yaw, roll } => quote! { crate::EulerAngle { pitch: #pitch, yaw: #yaw, roll: #roll, } }, Value::BlockPos(BlockPos { x, y, z }) => { quote!(valence_core::block_pos::BlockPos { x: #x, y: #y, z: #z }) } Value::OptionalBlockPos(pos) => { assert!(pos.is_none()); quote!(None) } Value::Facing(f) => { let variant = ident(f.replace('.', "_").to_pascal_case()); quote!(valence_core::direction::Direction::#variant) } Value::OptionalUuid(uuid) => { assert!(uuid.is_none()); quote!(None) } Value::BlockState(_) => { quote!(valence_block::BlockState::default()) } Value::OptionalBlockState(bs) => { assert!(bs.is_none()); quote!(valence_block::BlockState::default()) } Value::NbtCompound(s) => { assert_eq!(s, "{}"); quote!(valence_nbt::Compound::default()) } Value::Particle(p) => { let variant = ident(p.replace('.', "_").to_pascal_case()); quote!(valence_core::packet::s2c::play::particle::Particle::#variant) } Value::VillagerData { typ, profession, level, } => { let typ = ident(typ.replace('.', "_").to_pascal_case()); let profession = ident(profession.replace('.', "_").to_pascal_case()); quote! { crate::VillagerData { kind: crate::VillagerKind::#typ, profession: crate::VillagerProfession::#profession, level: #level, } } } Value::OptionalInt(i) => { assert!(i.is_none()); quote!(None) } Value::EntityPose(p) => { let variant = ident(p.replace('.', "_").to_pascal_case()); quote!(crate::Pose::#variant) } Value::CatVariant(c) => { let variant = ident(c.replace('.', "_").to_pascal_case()); quote!(crate::CatKind::#variant) } Value::FrogVariant(f) => { let variant = ident(f.replace('.', "_").to_pascal_case()); quote!(crate::FrogKind::#variant) } Value::OptionalGlobalPos(_) => quote!(()), Value::PaintingVariant(p) => { let variant = ident(p.replace('.', "_").to_pascal_case()); quote!(crate::PaintingKind::#variant) } Value::SnifferState(s) => { let state = ident(s.replace('.', "_").to_pascal_case()); quote!(crate::SnifferState::#state) } Value::Vector3f { x, y, z } => quote!(glam::f32::Vec3::new(#x, #y, #z)), Value::Quaternionf { x, y, z, w } => quote! { glam::f32::Quat::from_xyzw(#x, #y, #z, #w) }, } } pub fn encodable_expr(&self, self_lvalue: TokenStream) -> TokenStream { match self { Value::Integer(_) => quote!(VarInt(#self_lvalue)), Value::OptionalInt(_) => quote!(OptionalInt(#self_lvalue)), Value::ItemStack(_) => quote!(Some(&#self_lvalue)), _ => quote!(&#self_lvalue), } } } type Entities = BTreeMap; pub fn main() -> anyhow::Result<()> { rerun_if_changed(["../../extracted/misc.json", "../../extracted/entities.json"]); write_generated_file(build()?, "entity.rs") } fn build() -> anyhow::Result { let entity_types = serde_json::from_str::(include_str!("../../extracted/misc.json")) .context("failed to deserialize misc.json")? .entity_type; let entities: Entities = serde_json::from_str::(include_str!("../../extracted/entities.json")) .context("failed to deserialize entities.json")? .into_iter() .collect(); let mut entity_kind_consts = TokenStream::new(); let mut entity_kind_fmt_args = TokenStream::new(); let mut translation_key_arms = TokenStream::new(); let mut modules = TokenStream::new(); let mut systems = TokenStream::new(); let mut system_names = vec![]; for (entity_name, entity) in entities.clone() { let entity_name_ident = ident(&entity_name); let stripped_shouty_entity_name = strip_entity_suffix(&entity_name) .replace('.', "_") .to_shouty_snake_case(); let stripped_shouty_entity_name_ident = ident(&stripped_shouty_entity_name); let stripped_snake_entity_name = strip_entity_suffix(&entity_name).to_snake_case(); let stripped_snake_entity_name_ident = ident(&stripped_snake_entity_name); let mut module_body = TokenStream::new(); if let Some(parent_name) = entity.parent { let stripped_snake_parent_name = strip_entity_suffix(&parent_name).to_snake_case(); let module_doc = format!( "Parent class: \ [`{stripped_snake_parent_name}`][super::{stripped_snake_parent_name}]." ); module_body.extend([quote! { #![doc = #module_doc] }]); } // Is this a concrete entity type? if let Some(entity_type) = entity.typ { let entity_type_id = entity_types[&entity_type]; entity_kind_consts.extend([quote! { pub const #stripped_shouty_entity_name_ident: EntityKind = EntityKind(#entity_type_id); }]); entity_kind_fmt_args.extend([quote! { EntityKind::#stripped_shouty_entity_name_ident => write!(f, "{} ({})", #entity_type_id, #stripped_shouty_entity_name), }]); let translation_key_expr = if let Some(key) = entity.translation_key { quote!(Some(#key)) } else { quote!(None) }; translation_key_arms.extend([quote! { EntityKind::#stripped_shouty_entity_name_ident => #translation_key_expr, }]); // Create bundle type. let mut bundle_fields = TokenStream::new(); let mut bundle_init_fields = TokenStream::new(); for marker_or_field in collect_bundle_fields(&entity_name, &entities) { match marker_or_field { MarkerOrField::Marker { entity_name } => { let stripped_entity_name = strip_entity_suffix(entity_name); let snake_entity_name_ident = ident(entity_name.to_snake_case()); let stripped_snake_entity_name_ident = ident(stripped_entity_name.to_snake_case()); let pascal_entity_name_ident = ident(entity_name.replace('.', "_").to_pascal_case()); bundle_fields.extend([quote! { pub #snake_entity_name_ident: super::#stripped_snake_entity_name_ident::#pascal_entity_name_ident, }]); bundle_init_fields.extend([quote! { #snake_entity_name_ident: Default::default(), }]); } MarkerOrField::Field { entity_name, field } => { let snake_field_name = field.name.to_snake_case(); let pascal_field_name = field.name.replace('.', "_").to_pascal_case(); let pascal_field_name_ident = ident(&pascal_field_name); let stripped_entity_name = strip_entity_suffix(entity_name); let stripped_snake_entity_name = stripped_entity_name.to_snake_case(); let stripped_snake_entity_name_ident = ident(&stripped_snake_entity_name); let field_name_ident = ident(format!("{stripped_snake_entity_name}_{snake_field_name}")); bundle_fields.extend([quote! { pub #field_name_ident: super::#stripped_snake_entity_name_ident::#pascal_field_name_ident, }]); bundle_init_fields.extend([quote! { #field_name_ident: Default::default(), }]); } } } bundle_fields.extend([quote! { pub kind: super::EntityKind, pub id: super::EntityId, pub uuid: super::UniqueId, pub location: super::Location, pub old_location: super::OldLocation, pub position: super::Position, pub old_position: super::OldPosition, pub look: super::Look, pub head_yaw: super::HeadYaw, pub on_ground: super::OnGround, pub velocity: super::Velocity, pub statuses: super::EntityStatuses, pub animations: super::EntityAnimations, pub object_data: super::ObjectData, pub tracked_data: super::TrackedData, pub packet_byte_range: super::PacketByteRange, }]); bundle_init_fields.extend([quote! { kind: super::EntityKind::#stripped_shouty_entity_name_ident, id: Default::default(), uuid: Default::default(), location: Default::default(), old_location: Default::default(), position: Default::default(), old_position: Default::default(), look: Default::default(), head_yaw: Default::default(), on_ground: Default::default(), velocity: Default::default(), statuses: Default::default(), animations: Default::default(), object_data: Default::default(), tracked_data: Default::default(), packet_byte_range: Default::default(), }]); let bundle_name_ident = ident(format!("{entity_name}Bundle")); let bundle_doc = format!( "The bundle of components for spawning `{stripped_snake_entity_name}` entities." ); module_body.extend([quote! { #[doc = #bundle_doc] #[derive(bevy_ecs::bundle::Bundle, Debug)] pub struct #bundle_name_ident { #bundle_fields } impl Default for #bundle_name_ident { fn default() -> Self { Self { #bundle_init_fields } } } }]); } for field in &entity.fields { let pascal_field_name_ident = ident(field.name.replace('.', "_").to_pascal_case()); let snake_field_name = field.name.to_snake_case(); let inner_type = field.default_value.field_type(); let default_expr = field.default_value.default_expr(); module_body.extend([quote! { #[derive(bevy_ecs::component::Component, PartialEq, Clone, Debug)] pub struct #pascal_field_name_ident(pub #inner_type); #[allow(clippy::derivable_impls)] impl Default for #pascal_field_name_ident { fn default() -> Self { Self(#default_expr) } } }]); let system_name_ident = ident(format!( "update_{stripped_snake_entity_name}_{snake_field_name}" )); let component_path = quote!(#stripped_snake_entity_name_ident::#pascal_field_name_ident); system_names.push(quote!(#system_name_ident)); let data_index = field.index; let data_type = field.default_value.type_id(); let encodable_expr = field.default_value.encodable_expr(quote!(value.0)); systems.extend([quote! { #[allow(clippy::needless_borrow)] fn #system_name_ident( mut query: Query<(&#component_path, &mut TrackedData), Changed<#component_path>> ) { for (value, mut tracked_data) in &mut query { if *value == Default::default() { tracked_data.remove_init_value(#data_index); } else { tracked_data.insert_init_value(#data_index, #data_type, #encodable_expr); } if !tracked_data.is_added() { tracked_data.append_update_value(#data_index, #data_type, #encodable_expr); } } } }]); } let marker_doc = format!("Marker component for `{stripped_snake_entity_name}` entities."); module_body.extend([quote! { #[doc = #marker_doc] #[derive(bevy_ecs::component::Component, Copy, Clone, Default, Debug)] pub struct #entity_name_ident; }]); modules.extend([quote! { #[allow(clippy::module_inception)] pub mod #stripped_snake_entity_name_ident { #module_body } }]); } #[derive(Deserialize, Debug)] struct MiscEntityData { entity_status: BTreeMap, entity_animation: BTreeMap, } let misc_entity_data: MiscEntityData = serde_json::from_str(include_str!("../../extracted/misc.json"))?; let entity_status_variants = misc_entity_data .entity_status .into_iter() .map(|(name, code)| { let name = ident(name.replace('.', "_").to_pascal_case()); let code = code as isize; quote! { #name = #code, } }); let entity_animation_variants = misc_entity_data .entity_animation .into_iter() .map(|(name, code)| { let name = ident(name.replace('.', "_").to_pascal_case()); let code = code as isize; quote! { #name = #code, } }); Ok(quote! { #modules /// Identifies the type of an entity. /// As a component, the entity kind should not be modified. #[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct EntityKind(i32); impl EntityKind { #entity_kind_consts pub const fn new(inner: i32) -> Self { Self(inner) } pub const fn get(self) -> i32 { self.0 } pub const fn translation_key(self) -> Option<&'static str> { match self { #translation_key_arms _ => None, } } } impl std::fmt::Debug for EntityKind { #[allow(clippy::write_literal)] fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { #entity_kind_fmt_args EntityKind(other) => write!(f, "{other}"), } } } #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum EntityStatus { #(#entity_status_variants)* } #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum EntityAnimation { #(#entity_animation_variants)* } fn add_tracked_data_systems(app: &mut App) { #systems #( app.add_system( #system_names .in_set(UpdateTrackedDataSet) .ambiguous_with(UpdateTrackedDataSet) ); )* } }) } enum MarkerOrField<'a> { Marker { entity_name: &'a str, }, Field { entity_name: &'a str, field: &'a Field, }, } fn collect_bundle_fields<'a>( mut entity_name: &'a str, entities: &'a Entities, ) -> Vec> { let mut res = vec![]; loop { let e = &entities[entity_name]; res.push(MarkerOrField::Marker { entity_name }); res.extend( e.fields .iter() .map(|field| MarkerOrField::Field { entity_name, field }), ); if let Some(parent) = &e.parent { entity_name = parent; } else { break; } } res } fn strip_entity_suffix(string: &str) -> String { let stripped = string.strip_suffix("Entity").unwrap_or(string); if stripped.is_empty() { string } else { stripped } .to_owned() }