aboutsummaryrefslogtreecommitdiff
path: root/data-code-generator
diff options
context:
space:
mode:
Diffstat (limited to 'data-code-generator')
-rw-r--r--data-code-generator/README.md4
-rw-r--r--data-code-generator/main.py41
-rw-r--r--data-code-generator/mappings.py60
-rw-r--r--data-code-generator/packetcodegen.py100
-rw-r--r--data-code-generator/utils.py15
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:]