From d112856ff6353592a50658b0ddd316f54dd97b87 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Sun, 6 Nov 2022 14:05:01 -0600 Subject: Entity metadata (#37) * add example generated metadata.rs * metadata.rs codegen * add the files * add comment to top of metadata.rs * avoid clone * metadata * defaults * defaults * fix metadata readers and writers * fix bad bitmasks and ignore some clippy warnings in generated code * add set_index function to entity metadatas * applying metadata --- codegen/lib/code/entity.py | 403 +++++++++++++++++++++++++++++++++++++++++++ codegen/lib/code/packet.py | 4 +- codegen/lib/code/registry.py | 2 +- codegen/lib/mappings.py | 2 +- 4 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 codegen/lib/code/entity.py (limited to 'codegen/lib') diff --git a/codegen/lib/code/entity.py b/codegen/lib/code/entity.py new file mode 100644 index 00000000..13f99022 --- /dev/null +++ b/codegen/lib/code/entity.py @@ -0,0 +1,403 @@ +from lib.utils import to_camel_case, to_snake_case, get_dir_location, upper_first_letter +from lib.mappings import Mappings +from typing import Optional +import re + +METADATA_RS_DIR = get_dir_location( + '../azalea-world/src/entity/metadata.rs') + + +def generate_entity_metadata(burger_entity_data: dict, mappings: Mappings): + # TODO: auto generate this and use it for generating the EntityDataValue enum + metadata_types = [ + {'name': 'Byte', 'type': 'u8'}, + {'name': 'Int', 'type': 'i32'}, + {'name': 'Float', 'type': 'f32'}, + {'name': 'String', 'type': 'String'}, + {'name': 'Component', 'type': 'Component'}, + {'name': 'OptionalComponent', 'type': 'Option'}, + {'name': 'ItemStack', 'type': 'Slot'}, + {'name': 'Boolean', 'type': 'bool'}, + {'name': 'Rotations', 'type': 'Rotations'}, + {'name': 'BlockPos', 'type': 'BlockPos'}, + {'name': 'OptionalBlockPos', 'type': 'Option'}, + {'name': 'Direction', 'type': 'Direction'}, + {'name': 'OptionalUuid', 'type': 'Option'}, + {'name': 'OptionalBlockState', 'type': 'Option'}, + {'name': 'CompoundTag', 'type': 'azalea_nbt::Tag'}, + {'name': 'Particle', 'type': 'Particle'}, + {'name': 'VillagerData', 'type': 'VillagerData'}, + {'name': 'OptionalUnsignedInt', 'type': 'Option'}, + {'name': 'Pose', 'type': 'Pose'}, + {'name': 'CatVariant', 'type': 'azalea_registry::CatVariant'}, + {'name': 'FrogVariant', 'type': 'azalea_registry::FrogVariant'}, + {'name': 'GlobalPos', 'type': 'GlobalPos'}, + {'name': 'PaintingVariant', 'type': 'azalea_registry::PaintingVariant'} + ] + + code = [] + code.append('// This file is generated from codegen/lib/code/entity.py.') + code.append("// Don't change it manually!") + code.append('') + code.append('#![allow(clippy::clone_on_copy, clippy::derivable_impls)]') + code.append('use super::{EntityDataValue, Rotations, VillagerData, Pose};') + code.append('use azalea_block::BlockState;') + code.append('use azalea_chat::Component;') + code.append('use azalea_core::{BlockPos, Direction, Particle, Slot};') + code.append('use std::{collections::VecDeque, ops::Deref};') + code.append('use uuid::Uuid;') + code.append('') + + entity_structs = [] + + parent_field_name = None + for entity_id in burger_entity_data: + entity_parents = get_entity_parents(entity_id, burger_entity_data) + entity_metadata = get_entity_metadata(entity_id, burger_entity_data) + entity_metadata_names = get_entity_metadata_names( + entity_id, burger_entity_data, mappings) + + struct_name: str = upper_first_letter( + to_camel_case(entity_parents[0].replace('~', ''))) + parent_struct_name: Optional[str] = upper_first_letter(to_camel_case( + entity_parents[1].replace('~', ''))) if (len(entity_parents) >= 2) else None + if parent_struct_name: + parent_field_name = to_snake_case(parent_struct_name) + if not entity_parents[0].startswith('~'): + entity_structs.append(struct_name) + + reader_code = [] + writer_code = [] + set_index_code = [] + field_names = [] + + code.append(f'#[derive(Debug, Clone)]') + code.append(f'pub struct {struct_name} {{') + + if parent_struct_name: + assert parent_field_name + code.append(f'pub {parent_field_name}: {parent_struct_name},') + reader_code.append( + f'let {parent_field_name} = {parent_struct_name}::read(metadata)?;') + writer_code.append( + f'metadata.extend(self.{parent_field_name}.write());') + for index, name_or_bitfield in entity_metadata_names.items(): + if isinstance(name_or_bitfield, str): + # normal field (can be any type) + name = name_or_bitfield + if name == 'type': + name = 'kind' + field_names.append(name) + type_id = next(filter(lambda i: i['index'] == index, entity_metadata))[ + 'type_id'] + metadata_type_data = metadata_types[type_id] + rust_type = metadata_type_data['type'] + type_name = metadata_type_data['name'] + code.append(f'pub {name}: {rust_type},') + + type_name_field = to_snake_case(type_name) + reader_code.append( + f'let {name} = metadata.pop_front()?.into_{type_name_field}().ok()?;') + writer_code.append( + f'metadata.push(EntityDataValue::{type_name}(self.{name}.clone()));') + + # 1 => self.dancing = value.into_boolean().ok()?, + set_index_code.append( + f'{index} => self.{name} = value.into_{type_name_field}().ok()?,' + ) + else: + # bitfield (sent as a byte, each bit in the byte is used as a boolean) + reader_code.append( + 'let bitfield = metadata.pop_front()?.into_byte().ok()?;') + writer_code.append('let mut bitfield = 0u8;') + set_index_code.append(f'{index} => {{') + set_index_code.append( + f'let bitfield = value.into_byte().ok()?;') + for mask, name in name_or_bitfield.items(): + if name == 'type': + name = 'kind' + + field_names.append(name) + code.append(f'pub {name}: bool,') + reader_code.append(f'let {name} = bitfield & {mask} != 0;') + writer_code.append( + f'if self.{name} {{ bitfield &= {mask}; }}') + set_index_code.append( + f'self.{name} = bitfield & {mask} != 0;') + writer_code.append( + 'metadata.push(EntityDataValue::Byte(bitfield));') + set_index_code.append('},') + + code.append('}') + code.append('') + + code.append(f'impl {struct_name} {{') + + code.append( + 'pub fn read(metadata: &mut VecDeque) -> Option {') + code.extend(reader_code) + + self_args = [] + if parent_struct_name: + self_args.append( + f'{parent_field_name}') + self_args.extend(field_names) + code.append(f'Some(Self {{ {",".join(self_args)} }})') + code.append('}') + code.append('') + + code.append('pub fn write(&self) -> Vec {') + code.append('let mut metadata = Vec::new();') + code.extend(writer_code) + code.append('metadata') + code.append('}') + + code.append('}') + code.append('') + + # default + code.append(f'impl Default for {struct_name} {{') + code.append('fn default() -> Self {') + default_fields_code = [] + if parent_struct_name: + assert parent_field_name + default_fields_code.append( + f'{parent_field_name}: Default::default()') + for index, name_or_bitfield in entity_metadata_names.items(): + default = next(filter(lambda i: i['index'] == index, entity_metadata)).get( + 'default', 'Default::default()') + if isinstance(name_or_bitfield, str): + type_id = next(filter(lambda i: i['index'] == index, entity_metadata))[ + 'type_id'] + metadata_type_data = metadata_types[type_id] + type_name = metadata_type_data['name'] + + # TODO: burger doesn't get the default if it's a complex type + # like `Rotations`, so entities like armor stands will have the + # wrong default metadatas. This should be added to Burger. + if default is None: + # some types don't have Default implemented + if type_name == 'CompoundTag': + default = 'azalea_nbt::Tag::Compound(Default::default())' + elif type_name == 'CatVariant': + default = 'azalea_registry::CatVariant::Tabby' + elif type_name == 'PaintingVariant': + default = 'azalea_registry::PaintingVariant::Kebab' + elif type_name == 'FrogVariant': + default = 'azalea_registry::FrogVariant::Temperate' + else: + default = 'Default::default()' + else: + if type_name == 'Boolean': + default = 'true' if default else 'false' + elif type_name == 'String': + string_escaped = default.replace('"', '\\"') + default = f'"{string_escaped}".to_string()' + elif type_name == 'BlockPos': + default = f'BlockPos::new{default}' + elif type_name == 'OptionalBlockPos': # Option + default = f'Some(BlockPos::new{default})' if default != 'Empty' else 'None' + elif type_name == 'OptionalUuid': + default = f'Some(uuid::uuid!({default}))' if default != 'Empty' else 'None' + elif type_name == 'OptionalUnsignedInt': + default = f'Some({default})' if default != 'Empty' else 'None' + elif type_name == 'ItemStack': + default = f'Slot::Present({default})' if default != 'Empty' else 'Slot::Empty' + elif type_name == 'OptionalBlockState': + default = f'Some({default})' if default != 'Empty' else 'None' + elif type_name == 'OptionalComponent': + default = f'Some({default})' if default != 'Empty' else 'None' + elif type_name == 'CompoundTag': + default = f'azalea_nbt::Tag::Compound({default})' if default != 'Empty' else 'azalea_nbt::Tag::Compound(Default::default())' + + print(default, name_or_bitfield, type_name) + name = name_or_bitfield + if name == 'type': + name = 'kind' + default_fields_code.append(f'{name}: {default}') + else: + # if it's a bitfield, we'll have to extract the default for + # each bool from each bit in the default + for mask, name in name_or_bitfield.items(): + if name == 'type': + name = 'kind' + mask = int(mask, 0) + field_names.append(name) + bit_default = 'true' if (default & mask != 0) else 'false' + default_fields_code.append(f'{name}: {bit_default}') + + # Self { abstract_creature: Default::default(), dancing: Default::default(), can_duplicate: Default::default() } + code.append(f'Self {{ {", ".join(default_fields_code)} }}') + code.append('}') + code.append('}') + code.append('') + + # impl Allay { + # pub fn set_index(&mut self, index: u8, value: EntityDataValue) -> Option<()> { + # match index { + # 0..=0 => self.abstract_creature.set_index(index, value), + # 1 => self.dancing = value.into_boolean().ok()?, + # 2 => self.can_duplicate = value.into_boolean().ok()?, + # _ => {} + # } + # Some(()) + # } + # } + code.append(f'impl {struct_name} {{') + code.append( + 'pub fn set_index(&mut self, index: u8, value: EntityDataValue) -> Option<()> {') + if len(entity_metadata_names) > 0: + code.append('match index {') + # get the smallest index for this entity + smallest_index = min(entity_metadata_names.keys()) + if parent_struct_name: + code.append( + f'0..={smallest_index-1} => self.{parent_field_name}.set_index(index, value)?,') + code.extend(set_index_code) + code.append('_ => {}') + code.append('}') + code.append('Some(())') + elif parent_struct_name: + code.append(f'self.{parent_field_name}.set_index(index, value)') + else: + code.append('Some(())') + code.append('}') + code.append('}') + + # deref + if parent_struct_name: + code.append(f'impl Deref for {struct_name} {{') + code.append(f'type Target = {parent_struct_name};') + code.append( + f'fn deref(&self) -> &Self::Target {{ &self.{parent_field_name} }}') + code.append('}') + code.append('') + + # make the EntityMetadata enum from entity_structs + code.append(f'#[derive(Debug, Clone)]') + code.append('pub enum EntityMetadata {') + for struct_name in entity_structs: + code.append(f'{struct_name}({struct_name}),') + code.append('}') + code.append('') + + # impl From for EntityMetadata { + code.append('impl From for EntityMetadata {') + code.append('fn from(value: azalea_registry::EntityType) -> Self {') + code.append('match value {') + # azalea_registry::EntityType::Allay => EntityMetadata::Allay(Allay::default()), + for struct_name in entity_structs: + code.append( + f'azalea_registry::EntityType::{struct_name} => EntityMetadata::{struct_name}({struct_name}::default()),') + code.append('}') + code.append('}') + code.append('}') + code.append('') + + # impl EntityMetadata + # pub fn set_index(&mut self, index: u8, value: EntityDataValue) + code.append('impl EntityMetadata {') + code.append( + 'pub fn set_index(&mut self, index: u8, value: EntityDataValue) -> Option<()> {') + code.append('match self {') + # EntityMetadata::Allay(allay) => allay.set_index(index, value), + for struct_name in entity_structs: + code.append( + f'EntityMetadata::{struct_name}(entity) => entity.set_index(index, value),') + code.append('}') + code.append('}') + code.append('}') + code.append('') + + with open(METADATA_RS_DIR, 'w') as f: + f.write('\n'.join(code)) + + +def get_entity_parents(entity_id: str, burger_entity_data: dict): + parents = [] + while entity_id: + parents.append(entity_id) + entity_id = get_entity_parent(entity_id, burger_entity_data) + return parents + + +def get_entity_parent(entity_id: str, burger_entity_data: dict): + entity_metadata = burger_entity_data[entity_id]['metadata'] + first_metadata = entity_metadata[0] + return first_metadata.get('entity') + + +def get_entity_metadata(entity_id: str, burger_entity_data: dict): + entity_metadata = burger_entity_data[entity_id]['metadata'] + entity_useful_metadata = [] + for metadata_item in entity_metadata: + if 'data' in metadata_item: + for metadata_attribute in metadata_item['data']: + entity_useful_metadata.append({ + 'index': metadata_attribute['index'], + 'type_id': metadata_attribute['serializer_id'], + 'default': metadata_attribute.get('default') + }) + return entity_useful_metadata + + +def get_entity_metadata_names(entity_id: str, burger_entity_data: dict, mappings: Mappings): + entity_metadata = burger_entity_data[entity_id]['metadata'] + mapped_metadata_names = {} + + for metadata_item in entity_metadata: + if 'data' in metadata_item: + obfuscated_class = metadata_item['class'] + mojang_class = mappings.get_class(obfuscated_class) + + first_byte_index = None + + for metadata_attribute in metadata_item['data']: + obfuscated_field = metadata_attribute['field'] + mojang_field = mappings.get_field( + obfuscated_class, obfuscated_field) + pretty_mojang_name = prettify_mojang_field(mojang_field) + mapped_metadata_names[metadata_attribute['index'] + ] = pretty_mojang_name + + if metadata_attribute['serializer'] == 'Byte' and first_byte_index is None: + first_byte_index = metadata_attribute['index'] + + if metadata_item['bitfields'] and first_byte_index is not None: + clean_bitfield = {} + for bitfield_item in metadata_item['bitfields']: + bitfield_item_obfuscated_class = bitfield_item.get( + 'class', obfuscated_class) + mojang_bitfield_item_name = mappings.get_method( + bitfield_item_obfuscated_class, bitfield_item['method'], '') + bitfield_item_name = prettify_mojang_method( + mojang_bitfield_item_name) + bitfield_hex_mask = hex(bitfield_item['mask']) + clean_bitfield[bitfield_hex_mask] = bitfield_item_name + mapped_metadata_names[first_byte_index] = clean_bitfield + return mapped_metadata_names + + +def prettify_mojang_field(mojang_field: str): + # mojang names are like "DATA_AIR_SUPPLY" and that's ugly + better_name = mojang_field + if better_name.startswith('DATA_'): + better_name = better_name[5:] + + # remove the weird "Id" from the end of names + if better_name.endswith('_ID'): + better_name = better_name[:-3] + # remove the weird "id" from the front of names + if better_name.startswith('ID_'): + better_name = better_name[3:] + + return better_name.lower() + + +def prettify_mojang_method(mojang_method: str): + better_name = mojang_method + if better_name.endswith('()'): + better_name = better_name[:-2] + if re.match(r'is[A-Z]', better_name): + better_name = better_name[2:] + return to_snake_case(better_name) diff --git a/codegen/lib/code/packet.py b/codegen/lib/code/packet.py index a7751c23..4beccd35 100644 --- a/codegen/lib/code/packet.py +++ b/codegen/lib/code/packet.py @@ -1,7 +1,7 @@ -from typing import Optional -from lib.code.utils import burger_type_to_rust_type, write_packet_file from lib.utils import padded_hex, to_snake_case, to_camel_case, get_dir_location +from lib.code.utils import burger_type_to_rust_type, write_packet_file from lib.mappings import Mappings +from typing import Optional import os import re diff --git a/codegen/lib/code/registry.py b/codegen/lib/code/registry.py index 68d8c481..c22eefe9 100644 --- a/codegen/lib/code/registry.py +++ b/codegen/lib/code/registry.py @@ -1,6 +1,6 @@ -from typing import Optional from lib.utils import to_snake_case, upper_first_letter, get_dir_location, to_camel_case from ..mappings import Mappings +from typing import Optional import re REGISTRIES_DIR = get_dir_location('../azalea-registry/src/lib.rs') diff --git a/codegen/lib/mappings.py b/codegen/lib/mappings.py index 6cf6273f..7cbb863a 100644 --- a/codegen/lib/mappings.py +++ b/codegen/lib/mappings.py @@ -71,7 +71,7 @@ class Mappings: return self.classes[obfuscated_class_name] def get_method(self, obfuscated_class_name, obfuscated_method_name, obfuscated_signature): - print(obfuscated_class_name, self.methods[obfuscated_class_name]) + # print(obfuscated_class_name, self.methods[obfuscated_class_name]) return self.methods[obfuscated_class_name][f'{obfuscated_method_name}({obfuscated_signature})'] def get_field_type(self, obfuscated_class_name, obfuscated_field_name) -> str: -- cgit v1.2.3