aboutsummaryrefslogtreecommitdiff
path: root/code-generator
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2022-05-07 23:38:16 +0000
committerGitHub <noreply@github.com>2022-05-07 23:38:16 +0000
commitd9e388d8b0d8b3bdcf7a2e21b0cd2daa78d295e7 (patch)
tree8165f83b45e06c17d9114f1672ac73f202ba4b68 /code-generator
parent71acb5b3363fe8a7a9ac09691de01d7342db3ad1 (diff)
parentf198bdf777d4c68b11e6aeabeb96b5035e1ec5e1 (diff)
downloadazalea-drasl-d9e388d8b0d8b3bdcf7a2e21b0cd2daa78d295e7.tar.xz
Merge pull request #5 from mat-1/code-gen
Code gen
Diffstat (limited to 'code-generator')
-rw-r--r--code-generator/README.md8
-rw-r--r--code-generator/main.py58
-rw-r--r--code-generator/mappings.py60
-rw-r--r--code-generator/packetcodegen.py169
-rw-r--r--code-generator/utils.py15
5 files changed, 310 insertions, 0 deletions
diff --git a/code-generator/README.md b/code-generator/README.md
new file mode 100644
index 00000000..5b9a6139
--- /dev/null
+++ b/code-generator/README.md
@@ -0,0 +1,8 @@
+Tools for generating code.
+
+The directory name doesn't start with `azalea-` because it's not a Rust crate.
+
+## Usage
+
+Generate packet:\
+`python main.py [packet id] [clientbound or serverbound] \[game/handshake/login/status\]`
diff --git a/code-generator/main.py b/code-generator/main.py
new file mode 100644
index 00000000..5ab3219f
--- /dev/null
+++ b/code-generator/main.py
@@ -0,0 +1,58 @@
+from mappings import Mappings
+import packetcodegen
+import requests
+import json
+import sys
+import os
+
+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']['release']
+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']
+
+skipping_burger = False
+try:
+ with open('burger.json', 'r') as f:
+ burger_data = json.load(f)[0]
+ if burger_data['version']['id'] == minecraft_version:
+ skipping_burger = True
+ print(
+ f'\033[92mSkipping Burger step because the burger.json is up-to-date.\033[m')
+except FileNotFoundError:
+ pass
+
+if not skipping_burger:
+ print('\033[92mDownloading Burger...\033[m')
+ r = os.system('git clone https://github.com/pokechu22/Burger')
+ os.system('cd Burger && 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']
+packet_id, direction, state = int(sys.argv[1]), sys.argv[2], sys.argv[3]
+print(
+ f'Generating code for packet id: {packet_id} with direction {direction} and state {state}')
+packetcodegen.generate(burger_packets_data, mappings,
+ packet_id, direction, state)
+
+os.system('cd .. && cargo fmt')
+
+print('Done!')
diff --git a/code-generator/mappings.py b/code-generator/mappings.py
new file mode 100644
index 00000000..fb3e8bda
--- /dev/null
+++ b/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/code-generator/packetcodegen.py b/code-generator/packetcodegen.py
new file mode 100644
index 00000000..59a48535
--- /dev/null
+++ b/code-generator/packetcodegen.py
@@ -0,0 +1,169 @@
+from utils import to_snake_case, to_camel_case
+from mappings import Mappings
+import os
+
+
+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'
+ uses.add('azalea_chat::component::Component')
+ elif burger_type == 'identifier':
+ field_type_rs = 'ResourceLocation'
+ uses.add('azalea_core::resource_location::ResourceLocation')
+ elif burger_type == 'uuid':
+ field_type_rs = 'Uuid'
+ uses.add('uuid::Uuid')
+ elif burger_type == 'position':
+ field_type_rs = 'BlockPos'
+ uses.add('azalea_core::BlockPos')
+ elif burger_type == 'nbtcompound':
+ field_type_rs = 'azalea_nbt::Tag'
+ elif burger_type == 'itemstack':
+ field_type_rs = 'Slot'
+ uses.add('azalea_core::Slot')
+ elif burger_type == 'metadata':
+ field_type_rs = 'EntityMetadata'
+ uses.add('crate::mc_buf::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 write_packet_file(state, packet_name_snake_case, code):
+ with open(f'../azalea-protocol/src/packets/{state}/{packet_name_snake_case}.rs', 'w') as f:
+ f.write(code)
+
+
+def generate(burger_packets, mappings: Mappings, target_packet_id, target_packet_direction, target_packet_state):
+ for packet in burger_packets.values():
+ if packet['id'] != target_packet_id:
+ continue
+
+ direction = packet['direction'].lower() # serverbound or clientbound
+ state = {'PLAY': 'game'}.get(packet['state'], packet['state'].lower())
+
+ if state != target_packet_state or direction != target_packet_direction:
+ continue
+
+ generated_packet_code = []
+ uses = set()
+ generated_packet_code.append(
+ f'#[derive(Clone, Debug, {to_camel_case(state)}Packet)]')
+ uses.add(f'packet_macros::{to_camel_case(state)}Packet')
+
+ obfuscated_class_name = packet['class'].split('.')[0]
+ class_name = mappings.get_class(
+ obfuscated_class_name).split('.')[-1].split('$')[0]
+
+ 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, instruction_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},')
+ uses.update(instruction_uses)
+ else:
+ generated_packet_code.append(f'// TODO: {instruction}')
+ continue
+
+ generated_packet_code.append('}')
+
+ if uses:
+ # empty line before the `use` statements
+ generated_packet_code.insert(0, '')
+ for use in uses:
+ generated_packet_code.insert(0, f'use {use};')
+
+ print(generated_packet_code)
+ write_packet_file(state, to_snake_case(class_name),
+ '\n'.join(generated_packet_code))
+ print()
+
+ mod_rs_dir = f'../azalea-protocol/src/packets/{state}/mod.rs'
+ with open(mod_rs_dir, 'r') as f:
+ mod_rs = f.read().splitlines()
+
+ pub_mod_line = f'pub mod {to_snake_case(class_name)};'
+ if pub_mod_line not in mod_rs:
+ mod_rs.insert(0, pub_mod_line)
+ packet_mod_rs_line = f' {hex(packet["id"])}: {to_snake_case(class_name)}::{to_camel_case(class_name)},'
+
+ in_serverbound = False
+ in_clientbound = False
+ for i, line in enumerate(mod_rs):
+ if line.strip() == 'Serverbound => {':
+ in_serverbound = True
+ continue
+ elif line.strip() == 'Clientbound => {':
+ in_clientbound = True
+ continue
+ elif line.strip() in ('}', '},'):
+ if (in_serverbound and direction == 'serverbound') or (in_clientbound and direction == 'clientbound'):
+ mod_rs.insert(i, packet_mod_rs_line)
+ break
+ in_serverbound = in_clientbound = False
+ continue
+
+ if line.strip() == '' or line.strip().startswith('//') or (not in_serverbound and direction == 'serverbound') or (not in_clientbound and direction == 'clientbound'):
+ continue
+
+ line_packet_id_hex = line.strip().split(':')[0]
+ assert line_packet_id_hex.startswith('0x')
+ line_packet_id = int(line_packet_id_hex[2:], 16)
+ if line_packet_id > packet['id']:
+ mod_rs.insert(i, packet_mod_rs_line)
+ break
+
+ with open(mod_rs_dir, 'w') as f:
+ f.write('\n'.join(mod_rs))
diff --git a/code-generator/utils.py b/code-generator/utils.py
new file mode 100644
index 00000000..5336d574
--- /dev/null
+++ b/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().strip('_')
+
+
+def to_camel_case(name):
+ s = re.sub('_([a-z])', lambda m: m.group(1).upper(), name)
+ return s[0].upper() + s[1:]