diff options
Diffstat (limited to 'data-code-generator')
| -rw-r--r-- | data-code-generator/README.md | 4 | ||||
| -rw-r--r-- | data-code-generator/main.py | 41 | ||||
| -rw-r--r-- | data-code-generator/mappings.py | 60 | ||||
| -rw-r--r-- | data-code-generator/packetcodegen.py | 100 | ||||
| -rw-r--r-- | data-code-generator/utils.py | 15 |
5 files changed, 220 insertions, 0 deletions
diff --git a/data-code-generator/README.md b/data-code-generator/README.md new file mode 100644 index 00000000..1deeee4d --- /dev/null +++ b/data-code-generator/README.md @@ -0,0 +1,4 @@ +Generate code for reading/writing packets from [Burger](https://github.com/pokechu22/Burger). The only dependency is `requests`. + +The directory name doesn't start with `azalea-` because it's not a Rust crate. + diff --git a/data-code-generator/main.py b/data-code-generator/main.py new file mode 100644 index 00000000..50550d76 --- /dev/null +++ b/data-code-generator/main.py @@ -0,0 +1,41 @@ +from mappings import Mappings +import packetcodegen +import requests +import json +import os + +# enable this if you already have the burger.json and don't want to wait +SKIP_BURGER = True + +print( + f'\033[92mFinding Minecraft version...\033[m') +version_manifest_data = requests.get( + 'https://launchermeta.mojang.com/mc/game/version_manifest.json').json() +minecraft_version = version_manifest_data['latest']['snapshot'] +print( + f'\033[92mUsing \033[1m{minecraft_version}..\033[m') +package_url = next( + filter(lambda v: v['id'] == minecraft_version, version_manifest_data['versions']))['url'] +package_data = requests.get(package_url).json() +client_jar_url = package_data['downloads']['client']['url'] + +if not SKIP_BURGER: + print('\033[92mDownloading Burger...\033[m') + r = os.system('git clone https://github.com/pokechu22/Burger') + os.system('git pull') + print('\033[92mDownloading client jar...\033[m') + with open('client.jar', 'wb') as f: + f.write(requests.get(client_jar_url).content) + + print(f'\033[92mExtracting data with Burger...\033[m') + os.system('cd Burger && python munch.py ../client.jar --output ../burger.json') + +client_mappings_url = package_data['downloads']['client_mappings']['url'] +mappings = Mappings.parse(requests.get(client_mappings_url).text) + + +with open('burger.json', 'r') as f: + burger_data = json.load(f) + +burger_packets_data = burger_data[0]['packets']['packet'] +packetcodegen.generate(burger_packets_data, mappings) diff --git a/data-code-generator/mappings.py b/data-code-generator/mappings.py new file mode 100644 index 00000000..fb3e8bda --- /dev/null +++ b/data-code-generator/mappings.py @@ -0,0 +1,60 @@ +class Mappings: + __slots__ = ('classes', 'fields', 'methods') + + def __init__(self, classes, fields, methods): + self.classes = classes + self.fields = fields + self.methods = methods + + @staticmethod + def parse(mappings_txt): + classes = {} + fields = {} + methods = {} + + current_obfuscated_class_name = None + + for line in mappings_txt.splitlines(): + if line.startswith('#') or line == '': + continue + + if line.startswith(' '): + # if a line starts with 4 spaces, that means it's a method or a field + if '(' in line: + # if it has an opening parenthesis, it's a method + real_name_with_parameters_and_line, obfuscated_name = line.strip().split(' -> ') + real_name_with_parameters = real_name_with_parameters_and_line.split( + ':')[-1] + + real_name = real_name_with_parameters.split('(')[0] + parameters = real_name_with_parameters.split('(')[1] + + if current_obfuscated_class_name not in methods: + methods[current_obfuscated_class_name] = {} + methods[current_obfuscated_class_name][ + f'{obfuscated_name}({parameters})'] = real_name + else: + # otherwise, it's a field + real_name_with_type, obfuscated_name = line.strip().split(' -> ') + real_name = real_name_with_type.split(' ')[1] + + if current_obfuscated_class_name not in fields: + fields[current_obfuscated_class_name] = {} + fields[current_obfuscated_class_name][obfuscated_name] = real_name + else: + # otherwise it's a class + real_name, obfuscated_name = line.strip(':').split(' -> ') + current_obfuscated_class_name = obfuscated_name + + classes[obfuscated_name] = real_name + + return Mappings(classes, fields, methods) + + def get_field(self, obfuscated_class_name, obfuscated_field_name): + return self.fields.get(obfuscated_class_name, {}).get(obfuscated_field_name) + + def get_class(self, obfuscated_class_name): + return self.classes[obfuscated_class_name] + + def get_method(self, obfuscated_class_name, obfuscated_method_name, obfuscated_signature): + return self.methods[obfuscated_class_name][f'{obfuscated_method_name}({obfuscated_signature})'] diff --git a/data-code-generator/packetcodegen.py b/data-code-generator/packetcodegen.py new file mode 100644 index 00000000..674f97c2 --- /dev/null +++ b/data-code-generator/packetcodegen.py @@ -0,0 +1,100 @@ +from utils import to_snake_case, to_camel_case +from mappings import Mappings + + +def burger_type_to_rust_type(burger_type): + is_var = False + uses = set() + + if burger_type == 'byte': + field_type_rs = 'i8' + elif burger_type == 'short': + field_type_rs = 'i16' + elif burger_type == 'int': + field_type_rs = 'i32' + elif burger_type == 'long': + field_type_rs = 'i64' + elif burger_type == 'float': + field_type_rs = 'f32' + elif burger_type == 'double': + field_type_rs = 'f64' + + elif burger_type == 'varint': + is_var = True + field_type_rs = 'i32' + elif burger_type == 'varlong': + is_var = True + field_type_rs = 'i64' + + elif burger_type == 'boolean': + field_type_rs = 'bool' + elif burger_type == 'string': + field_type_rs = 'String' + + elif burger_type == 'chatcomponent': + field_type_rs = 'Component' + elif burger_type == 'identifier': + field_type_rs = 'ResourceLocation' + elif burger_type == 'uuid': + field_type_rs = 'Uuid' + elif burger_type == 'position': + field_type_rs = 'BlockPos' + elif burger_type == 'nbtcompound': + field_type_rs = 'azalea_nbt::Tag' + elif burger_type == 'itemstack': + field_type_rs = 'Slot' + elif burger_type == 'metadata': + field_type_rs = 'EntityMetadata' + elif burger_type == 'enum': + # enums are too complicated, leave those to the user + field_type_rs = 'todo!()' + elif burger_type.endswith('[]'): + field_type_rs, is_var, uses = burger_type_to_rust_type( + burger_type[:-2]) + field_type_rs = f'Vec<{field_type_rs}>' + else: + print('Unknown field type:', burger_type) + exit() + return field_type_rs, is_var, uses + + +def generate(burger_packets, mappings: Mappings): + for packet in burger_packets.values(): + direction = packet['direction'].lower() # serverbound or clientbound + state = packet['state'].lower() + + generated_packet_code = [] + generated_packet_code.append( + f'#[derive(Clone, Debug, {to_camel_case(state)}Packet)]') + + obfuscated_class_name = packet['class'].split('.')[0] + class_name = mappings.get_class(obfuscated_class_name).split('.')[-1] + + generated_packet_code.append( + f'pub struct {to_camel_case(class_name)} {{') + + for instruction in packet.get('instructions', []): + if instruction['operation'] == 'write': + obfuscated_field_name = instruction['field'] + if '.' in obfuscated_field_name or ' ' in obfuscated_field_name or '(' in obfuscated_field_name: + continue + field_name = mappings.get_field( + obfuscated_class_name, obfuscated_field_name) + if not field_name: + generated_packet_code.append(f'// TODO: {instruction}') + continue + + field_type = instruction['type'] + field_type_rs, is_var, uses = burger_type_to_rust_type( + field_type) + if is_var: + generated_packet_code.append('#[var]') + generated_packet_code.append( + f'pub {to_snake_case(field_name)}: {field_type_rs},') + else: + generated_packet_code.append(f'// TODO: {instruction}') + continue + + generated_packet_code.append('}') + print(generated_packet_code) + print() diff --git a/data-code-generator/utils.py b/data-code-generator/utils.py new file mode 100644 index 00000000..1052bb87 --- /dev/null +++ b/data-code-generator/utils.py @@ -0,0 +1,15 @@ +import urllib.request +import gzip +import json +import re +import io + + +def to_snake_case(name): + s = re.sub('([A-Z])', r'_\1', name) + return s.lower() + + +def to_camel_case(name): + s = re.sub('_([a-z])', lambda m: m.group(1).upper(), name) + return s[0].upper() + s[1:] |
