From 04aea6afaf6819a66181d5d544356764d118b1bb Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 24 May 2022 19:50:19 -0500 Subject: code-generator/lib --- code-generator/.gitignore | 3 + code-generator/lib/mappings.py | 60 +++++++++++++ code-generator/lib/packetcodegen.py | 171 ++++++++++++++++++++++++++++++++++++ code-generator/lib/utils.py | 15 ++++ code-generator/lib/version.py | 80 +++++++++++++++++ code-generator/main.py | 49 +---------- code-generator/mappings.py | 60 ------------- code-generator/packetcodegen.py | 171 ------------------------------------ code-generator/utils.py | 15 ---- 9 files changed, 332 insertions(+), 292 deletions(-) create mode 100644 code-generator/.gitignore create mode 100644 code-generator/lib/mappings.py create mode 100644 code-generator/lib/packetcodegen.py create mode 100644 code-generator/lib/utils.py create mode 100644 code-generator/lib/version.py delete mode 100644 code-generator/mappings.py delete mode 100644 code-generator/packetcodegen.py delete mode 100644 code-generator/utils.py (limited to 'code-generator') diff --git a/code-generator/.gitignore b/code-generator/.gitignore new file mode 100644 index 00000000..2ef6e1be --- /dev/null +++ b/code-generator/.gitignore @@ -0,0 +1,3 @@ +downloads +__pycache__ +*.tmp diff --git a/code-generator/lib/mappings.py b/code-generator/lib/mappings.py new file mode 100644 index 00000000..fb3e8bda --- /dev/null +++ b/code-generator/lib/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/lib/packetcodegen.py b/code-generator/lib/packetcodegen.py new file mode 100644 index 00000000..3d453a4e --- /dev/null +++ b/code-generator/lib/packetcodegen.py @@ -0,0 +1,171 @@ +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, McBuf, {to_camel_case(state)}Packet)]') + uses.add(f'packet_macros::{{{to_camel_case(state)}Packet, McBuf}}') + + obfuscated_class_name = packet['class'].split('.')[0].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: + generated_packet_code.append(f'// TODO: {instruction}') + continue + field_name = mappings.get_field( + obfuscated_class_name, obfuscated_field_name) + if not field_name: + generated_packet_code.append( + f'// TODO: unknown field {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/lib/utils.py b/code-generator/lib/utils.py new file mode 100644 index 00000000..5336d574 --- /dev/null +++ b/code-generator/lib/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:] diff --git a/code-generator/lib/version.py b/code-generator/lib/version.py new file mode 100644 index 00000000..f50ab461 --- /dev/null +++ b/code-generator/lib/version.py @@ -0,0 +1,80 @@ +from mappings import Mappings +import requests +import json +import os + + +def download_burger(): + print('\033[92mDownloading Burger...\033[m') + os.system( + 'cd downloads && git clone https://github.com/pokechu22/Burger && cd Burger && git pull') + + print('\033[92mInstalling dependencies...\033[m') + os.system('cd downloads/Burger && pip install six jawa') + + +def get_version_manifest(): + if not os.path.exists(f'downloads/version_manifest.json'): + print( + f'\033[92mDownloading version manifest...\033[m') + version_manifest_data = requests.get( + 'https://launchermeta.mojang.com/mc/game/version_manifest.json').json() + with open(f'downloads/version_manifest.json', 'w') as f: + json.dump(version_manifest_data, f) + else: + with open(f'downloads/version_manifest.json', 'r') as f: + version_manifest_data = json.load(f) + return version_manifest_data + + +def get_version_data(version_id: str): + if not os.path.exists(f'downloads/{version_id}.json'): + version_manifest_data = get_version_manifest() + + print( + f'\033[92mGetting data for \033[1m{version_id}..\033[m') + package_url = next( + filter(lambda v: v['id'] == version_id, version_manifest_data['versions']))['url'] + package_data = requests.get(package_url).json() + with open(f'downloads/{version_id}.json', 'w') as f: + json.dump(package_data, f) + else: + with open(f'downloads/{version_id}.json', 'r') as f: + package_data = json.load(f) + return package_data + + +def get_client_jar(version_id: str): + if not os.path.exists(f'downloads/client-{version_id}.jar'): + package_data = get_version_data(version_id) + print('\033[92mDownloading client jar...\033[m') + client_jar_url = package_data['downloads']['client']['url'] + with open('client.jar', 'wb') as f: + f.write(requests.get(client_jar_url).content) + + +def get_burger_data_for_version(version_id: str): + if not os.path.exists(f'downloads/burger-{version_id}.json'): + get_client_jar(version_id) + + os.system( + f'cd downloads/Burger && python munch.py ../client-{version_id}.jar --output ../burger-{version_id}.json' + ) + with open(f'burger-{version_id}.json', 'r') as f: + return json.load(f) + + +def get_mappings_for_version(version_id: str): + if not os.path.exists(f'downloads/mappings-{version_id}.json'): + package_data = get_version_data(version_id) + + client_mappings_url = package_data['downloads']['client_mappings']['url'] + + mappings_text = requests.get(client_mappings_url).text + + with open(f'downloads/mappings-{version_id}.json', 'w') as f: + f.write(mappings_text) + else: + with open(f'downloads/mappings-{version_id}.json', 'r') as f: + mappings_text = f.read() + return Mappings.parse(mappings_text) diff --git a/code-generator/main.py b/code-generator/main.py index 78d307b7..54ad3946 100644 --- a/code-generator/main.py +++ b/code-generator/main.py @@ -1,54 +1,11 @@ -from mappings import Mappings -import packetcodegen +from .lib import version, 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[92mInstalling dependencies...\033[m') - # os.system('cd Burger && pip install six jawa') - - 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) +mappings = version.get_mappings_for_version('1.18.2') +burger_data = version.get_burger_data_for_version('1.18.2') burger_packets_data = burger_data[0]['packets']['packet'] packet_id, direction, state = int(sys.argv[1]), sys.argv[2], sys.argv[3] diff --git a/code-generator/mappings.py b/code-generator/mappings.py deleted file mode 100644 index fb3e8bda..00000000 --- a/code-generator/mappings.py +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 3d453a4e..00000000 --- a/code-generator/packetcodegen.py +++ /dev/null @@ -1,171 +0,0 @@ -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, McBuf, {to_camel_case(state)}Packet)]') - uses.add(f'packet_macros::{{{to_camel_case(state)}Packet, McBuf}}') - - obfuscated_class_name = packet['class'].split('.')[0].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: - generated_packet_code.append(f'// TODO: {instruction}') - continue - field_name = mappings.get_field( - obfuscated_class_name, obfuscated_field_name) - if not field_name: - generated_packet_code.append( - f'// TODO: unknown field {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 deleted file mode 100644 index 5336d574..00000000 --- a/code-generator/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -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:] -- cgit v1.2.3