twnet_parser/scripts/generate_messages.py
ChillerDragon e22530743c Fix build of generated game msgs
the vote status field `pass` conflicts
with the python keyword so rename it to `pass_`

also fix `get_string()` not being a function
2023-03-25 18:43:45 +01:00

209 lines
8.2 KiB
Python

import os
import json
from typing import TypedDict, Literal
class ConstantJson(TypedDict):
name: list[str]
type: str
value: int
class GameEnumValuesJson(TypedDict):
value: str
name: list[str]
class GameEnumJson(TypedDict):
name: list[str]
values: list[GameEnumValuesJson]
class GameMessageMemberTypeJson(TypedDict):
kind: str
disallow_cc: bool
class GameMessageMemberJson(TypedDict):
name: list[str]
type: GameMessageMemberTypeJson
class GameMessageJson(TypedDict):
id: int
name: list[str]
members: list[GameMessageMemberJson]
attributes: list[Literal['msg_encoding']]
class SpecJson(TypedDict):
constants: list[ConstantJson]
game_enumerations: list[GameEnumJson]
game_messages: list[GameMessageJson]
def fix_name_conflict(name: str) -> str:
# https://peps.python.org/pep-0008/#descriptive-naming-styles
if name == 'pass':
return 'pass_'
return name
def name_to_camel(name_list: list[str]) -> str:
name = ''.join([part.capitalize() for part in name_list])
return fix_name_conflict(name)
def name_to_snake(name_list: list[str]) -> str:
name = '_'.join(name_list)
return fix_name_conflict(name)
def generate_msg(msg: GameMessageJson) -> None:
name_snake = name_to_snake(msg['name'])
name_camel = name_to_camel(msg['name'])
dirname = os.path.dirname(__file__)
file_path= os.path.join(
dirname,
'../twnet_parser/messages7/game/',
f'{name_snake}.py')
# if os.path.exists(file_path):
# print(f"Warning: file already exists! {file_path}")
# return
with open(file_path, 'w') as out_file:
print(f"Generating {file_path} ...")
out_file.write('# generated by scripts/generate_messages.py\n')
out_file.write('\n')
out_file.write('from twnet_parser.pretty_print import PrettyPrint\n')
out_file.write('from twnet_parser.packer import Unpacker\n')
out_file.write('\n')
out_file.write(f'class {name_camel}(PrettyPrint):\n')
out_file.write(' def __init__(\n')
out_file.write(' self,\n')
args: list[str] = []
for member in msg['members']:
# {'name': ['message'], 'type': {'kind': 'string', 'disallow_cc': False}}
ftype = 'int'
default = '-1'
if member['type']['kind'] == 'string': # TODO: sanitize cc
ftype = 'str'
default = "'default'"
elif member['type']['kind'] == 'raw':
ftype = 'bytes'
default = "b'\\x00'"
# {"name": ["mode"], "type": {"kind": "enum", "enum": ["chat"]}},
elif member['type']['kind'] == 'enum':
ftype = 'int'
default = '0'
# TODO: use ENUM_NAME_SOME_VALUE as default here
elif member['type']['kind'] in ('int32', 'tick'):
ftype = 'int'
default = '0'
elif member['type']['kind'] == 'boolean':
ftype = 'bool'
default = 'False'
elif member['type']['kind'] == 'tune_param': # TODO: think about tune params
ftype = 'int'
default = '0'
elif member['type']['kind'] == 'snapshot_object':
# TODO: think about snapshot_object
ftype = 'int'
default = '0'
elif member['type']['kind'] == 'array': # TODO: think about array
ftype = 'int'
default = '0'
elif member['type']['kind'] == 'flags': # TODO: think about flags
ftype = 'int'
default = '0'
else:
print(f"Error: unknown type {member['type']}")
exit(1)
name = name_to_snake(member["name"])
args.append(f' {name}: {ftype} = {default}')
out_file.write(',\n'.join(args) + '\n')
out_file.write(' ) -> None:\n')
out_file.write(f" self.message_name = '{name_snake}'\n")
for member in msg['members']:
# {'name': ['message'], 'type': {'kind': 'string', 'disallow_cc': False}}
ftype = 'int'
if member['type']['kind'] == 'string':
ftype = 'str'
elif member['type']['kind'] == 'raw':
ftype = 'bytes'
# {"name": ["mode"], "type": {"kind": "enum", "enum": ["chat"]}},
elif member['type']['kind'] == 'enum':
ftype = 'int'
elif member['type']['kind'] in ('int32', 'tick'):
ftype = 'int'
elif member['type']['kind'] == 'boolean':
ftype = 'bool'
elif member['type']['kind'] == 'tune_param': # TODO: think about tune params
ftype = 'int'
elif member['type']['kind'] == 'snapshot_object':
# TODO: think about snapshot_object
ftype = 'int'
elif member['type']['kind'] == 'array': # TODO: think about array
ftype = 'int'
elif member['type']['kind'] == 'flags': # TODO: think about flags
ftype = 'int'
else:
print(f"Error: unknown type {member['type']}")
exit(1)
name = name_to_snake(member["name"])
out_file.write(f" self.{name}: {ftype} = {name}\n")
out_file.write('\n')
out_file.write(' # first byte of data\n')
out_file.write(' # has to be the first byte of the message payload\n')
out_file.write(' # NOT the chunk header and NOT the message id\n')
out_file.write(' def unpack(self, data: bytes) -> bool:\n')
out_file.write(' unpacker = Unpacker(data)\n')
for member in msg['members']:
# {'name': ['message'], 'type': {'kind': 'string', 'disallow_cc': False}}
unpacker = 'int()'
if member['type']['kind'] == 'string': # TODO: sanitize cc
unpacker = 'str()'
elif member['type']['kind'] == 'raw':
unpacker = 'raw()'
# {"name": ["mode"], "type": {"kind": "enum", "enum": ["chat"]}},
elif member['type']['kind'] == 'enum':
unpacker = 'int() # TODO: this is a enum'
elif member['type']['kind'] in ('int32', 'tick'):
unpacker = 'int()'
elif member['type']['kind'] == 'boolean':
unpacker = 'int() == 1'
elif member['type']['kind'] == 'tune_param': # TODO: think about tune params
unpacker = 'int() # TODO: this is a tune param'
elif member['type']['kind'] == 'snapshot_object':
# TODO: think about snapshot_object
unpacker = 'int() # TODO: this is a snapshot object'
elif member['type']['kind'] == 'array': # TODO: think about array
unpacker = 'int() # TODO: this is an array'
elif member['type']['kind'] == 'flags': # TODO: think about flags
unpacker = 'int() # TODO: this is a flag'
else:
print(f"Error: unknown type {member['type']}")
exit(1)
name = name_to_snake(member["name"])
out_file.write(f' self.{name} = unpacker.get_{unpacker}\n')
out_file.write(' return True\n')
out_file.write('\n')
out_file.write(' def pack(self) -> bytes:\n')
out_file.write(" return b'todo'\n")
def generate(spec: str) -> None:
print(f"generating classes from {spec} ...")
with open(spec) as spec_io:
spec_data: SpecJson = json.load(spec_io)
# for msg in [spec_data['game_messages'][1]]:
for msg in spec_data['game_messages']:
generate_msg(msg)
def main() -> None:
dirname = os.path.dirname(__file__)
spec_07 = os.path.join(
dirname,
'../../libtw2/gamenet/generate/spec/teeworlds-0.7.5.json')
if os.path.exists(spec_07):
generate(spec_07)
else:
print(f"Error: file not found {spec_07}")
print(" try running these commands")
print("")
print(" git clone git@github.com:heinrich5991/libtw2 ..")
if __name__ == '__main__':
main()