1
0
mirror of https://github.com/rwengine/openrw.git synced 2024-11-07 11:22:45 +01:00
openrw/scripts/generate_scripts_functions.py

353 lines
12 KiB
Python
Executable File

#!/usr/bin/env python2
# This script combines interface description XML files, with the opcode ini
# to generate two files:
# binding.cpp: Defines a module that binds functions to opcodes.
# functions.cpp: Stubs for every opcode that unpack all the known arguments
#
# Input:
# SCM.ini
# SCM.xml
#
# SCM.ini is used to document the stub with expected behaviour
# SCM.xml is used to determine the correct types for each argument
import re
import lxml.etree as etree
instruction_file = "SCM.ini"
args_file = "SCM.xml"
output_functions = "functions.cpp"
output_binding = "binding.cpp"
arg_regex = "%(\d+)([-:\w]*/?%?[-_\w]*%?)"
d = etree.parse(open(args_file))
functions_file = open(output_functions, "w")
binding_file = open(output_binding, "w")
function_template = """/**
\t@brief {5}
\topcode {0:04x}{1}
*/
{2} {3} {{
\tRW_UNIMPLEMENTED_OPCODE(0x{0:04x});{4}
}}
"""
binding_template = """\tbind(0x{0:04x}, {1}, opcode_{0:04x});\n"""
type_names = {
'ENTITY': {
'PLAYER': 'ScriptPlayer',
'OBJECT': 'ScriptObject',
'CHAR': 'ScriptCharacter',
'CAR': 'ScriptVehicle',
'CAR_GENERATOR': 'ScriptVehicleGenerator',
'PICKUP': 'ScriptPickup',
'BLIP': 'ScriptBlip',
'GARAGE': 'ScriptGarage',
'PHONE': 'ScriptPhone',
'SCRIPT_FIRE': 'ScriptFire',
'SPHERE': 'ScriptSphere',
'SOUND': 'ScriptSound',
}
,
'TYPE': {
'INT': 'ScriptInt',
'FLOAT': 'ScriptFloat',
'TEXT_LABEL': 'ScriptString',
'LABEL': 'ScriptLabel',
'ANY': '_TODO_ANY',
}
,
'MISC': {
'CARPEDMODEL': 'ScriptModelID',
'PEDTYPE': 'ScriptPedType',
'DRIVINGMODE': 'ScriptDrivingMode',
'MISSION': 'ScriptMission',
'PAD': 'ScriptPad',
'BUTTON': 'ScriptButton',
'MODEL': 'ScriptModel',
'WEAPONTYPE': 'ScriptWeaponType',
'THREAT': 'ScriptThreat',
'CARLOCK': 'ScriptCarLock',
'CARCOLOUR': 'ScriptCarColour',
'CAMMODE': 'ScriptCamMode',
'CHANGECAMMODE': 'ScriptChangeCamMode',
'BLIPCOLOUR': 'ScriptBlipColour',
'BLIP_DISPLAY': 'ScriptBlipDisplay',
'FADE': 'ScriptFade',
'SHADOW': 'ScriptShadow',
'CONTACT': 'ScriptContact',
'WEATHER': 'ScriptWeather',
'FOLLOW_ROUTE': 'ScriptFollowRoute',
'EXPLOSION': 'ScriptExplosion',
'CARBOMB': 'ScriptCarBomb',
'GANG': 'ScriptGang',
'PEDSTAT': 'ScriptPedStat',
'ANIM': 'ScriptAnim',
'CORONATYPE': 'ScriptCoronaType',
'FLARETYPE': 'ScriptFlareType',
'POBJECT': 'ScriptPObject',
'RADAR_SPRITE': 'ScriptRadarSprite',
'PEDGRP': 'ScriptPedGrp',
'CAM_ZOOM': 'ScriptCamZoom',
'FONT': 'ScriptFont',
'WAITSTATE': 'ScriptWaitState',
'MOTION_BLUR': 'ScriptMotionBlur',
'STATUS': 'ScriptStatus',
'TIMER': 'ScriptTimer',
'COUNTER_DISPLAY': 'ScriptCounterDisplay',
'LEVEL': 'ScriptLevel',
'HUD_FLASH': 'ScriptHudFlash',
'DOOR': 'ScriptDoor',
'RADIO': 'ScriptRadio',
'PARTICLE': 'ScriptParticle',
'TEMPACT': 'ScriptTempact',
'SOUND': 'ScriptSoundType',
'PICKUP': 'ScriptPickupType',
'GARAGE': 'ScriptGarageType',
}
}
var_names = {
'GXT Entry': 'gxtEntry',
'X Coord': 'xCoord',
'Y Coord': 'yCoord',
'Z Coord': 'zCoord',
'Player': 'player',
'Character/ped': 'character',
'Car/vehicle': 'vehicle',
'Object': 'object',
'Pickup': 'pickup',
'Model ID': 'model',
'Radius': 'radius',
'X Radius': 'xRadius',
'Y Radius': 'yRadius',
'Z Radius': 'zRadius',
'Time (ms)': 'time',
#'Boolean true/false': 'boolean',
'GXT entry': 'gxtEntry',
'Angle': 'angle',
'X offset': 'xOffset',
'Y offset': 'yOffset',
'Z offset': 'zOffset',
'Blip': 'blip',
'Red (0-255)': 'rColour',
'Green (0-255)': 'gColour',
'Blue (0-255)': 'bColour',
'Alpha (0-255)': 'aColour',
'Car colour ID': 'carColour',
'Weapon ID': 'weaponID',
'X Rotation': 'xRot',
'Y Rotation': 'yRot',
'Z Rotation': 'zRot',
'Area name': 'areaName',
'Fire': 'fire',
'Car generator': 'carGen',
'Blip sprite ID': 'blipSprite',
'Ped type': 'pedType',
'2D pixel X': 'pixelX',
'2D pixel Y': 'pixelY',
'Gang ID': 'gangID',
'Explosion ID': 'explosionID',
'Vehicle action ID': 'vehicleActionID',
'Camera mode ID': 'cameraModeID',
'Button ID': 'buttonID',
'Pad ID': 'padID',
'Weather ID': 'weatherID',
'Sound ID': 'soundID',
'Stat ID': 'statID',
'Set script name': 'name',
'CHAR': 'character',
'GARAGE': 'garage',
'CAR': 'vehicle',
'PLAYER': 'player',
'OBJECT': 'object',
'PICKUP': 'pickup',
'PHONE': 'phone',
'SPHERE': 'sphere',
'BLIP': 'blip',
'SOUND': 'sound',
'SCRIPT_FIRE': 'fire',
'CAR_GENERATOR': 'carGen',
'CARPEDMODEL': 'model',
'PEDTYPE': 'pedType',
'FADE': 'scriptFade',
}
def arg_type(x, el):
outType = ''
outAccess = 'const '
outRef = ''
outName = ''
outComment = ''
isOut = False
if 'Out' in el.attrib:
outAccess = '' if el.attrib['Out'] == 'true' else 'const '
outRef = '&'
if 'AllowConst' in el.attrib:
outAccess = '' if el.attrib['AllowConst'] == 'false' else 'const '
if 'Entity' in el.attrib:
entity = el.attrib['Entity']
outType = type_names['ENTITY'][entity]
if entity in var_names:
outName = var_names[entity]
else:
print("Unhandled entity type: {}".format(entity))
elif 'Enum' in el.attrib:
enum = el.attrib['Enum']
outType = type_names['MISC'][enum]
if enum in var_names:
outName = var_names[enum]
else:
type_ = el.attrib['Type']
outType = type_names['TYPE'][type_]
# Determine if this is always a local / global
allowLocal = True
allowGlobal = True
if 'AllowLocalVar' in el.attrib:
allowLocal = True if el.attrib['AllowLocalVar'] == 'true' else False
if 'AllowGlobalVar' in el.attrib:
allowGlobal = True if el.attrib['AllowGlobalVar'] == 'true' else False
if allowLocal == False and allowGlobal == True:
outRef = '&'
outName = 'arg' + str(x) + 'G'
elif allowLocal == True and allowGlobal == False:
outRef = '&'
outName = 'arg' + str(x) + 'L'
else:
# Can be anything, assume immediate
pass
if 'Desc' in el.attrib:
outComment = el.attrib['Desc']
if outComment == 'Boolean true/false':
outType = "ScriptBoolean"
return (outAccess + outType + outRef, outName, outComment, x)
def impl_sig(opcode, sig):
outType = 'void'
if sig[0:2] == ' ':
outType = 'bool'
sig = sig.strip()
return (outType, 'opcode_{:04x}'.format(opcode))
def adjust_args(args):
for x in range(len(args)):
arg = args[x]
if len(arg[1]) == 0:
if arg[2] in var_names:
args[x] = (arg[0], var_names[arg[2]], arg[2], arg[3])
arg = args[x]
else:
args[x] = (arg[0], 'arg' + str(arg[3]), arg[2], arg[3])
arg = args[x]
args[x] = arg
return args
coordinates = ['x', 'y', 'z']
colours = ['r', 'g', 'b', 'a']
def check_vector(typeName, argName, doc, args, x):
coords = 0
for y in range(len(coordinates)):
if x+y >= len(args):
break
if '{}{}'.format(coordinates[y], typeName) == args[x+y][1]:
coords += 1
if coords > 1:
args[x] = ('ScriptVec{}'.format(coords), argName, doc, x)
for y in range(coords-1):
args.pop(x+1)
def check_colour(typeName, argName, doc, args, x):
coords = 0
for y in range(len(colours)):
if x+y >= len(args):
break
if '{}{}'.format(colours[y], typeName) == args[x+y][1]:
coords += 1
if coords > 2:
if coords == 4:
args[x] = ('ScriptRGBA', argName, doc, x)
if coords == 3:
args[x] = ('ScriptRGB', argName, doc, x)
for y in range(coords-1):
args.pop(x+1)
def process_args(args):
names = []
x = 0
while x < len(args):
arg = args[x]
if '&' not in arg[0]:
check_vector('Coord', 'coord', 'Coordinates', args, x)
check_vector('Rot', 'rotation', 'Rotation', args, x)
check_vector('Offset', 'offset', 'Offset', args, x)
check_vector('Radius', 'radius', 'Radius', args, x)
check_colour('Colour', 'colour', 'Colour (0-255)', args, x)
x += 1
return args
def finalize_args(args):
names = []
# Accumulate counts
name_counts = {}
for arg in args:
name_counts[arg[1]] = (name_counts[arg[1]] if arg[1] in name_counts else 0) + 1
for x in range(len(args)):
arg = args[x]
orgname = arg[1]
if name_counts[orgname] > 1:
args[x] = (arg[0], arg[1] + str(names.count(orgname)), arg[2], arg[3])
names.append(orgname)
return args
with open(instruction_file) as f:
for line in f:
m = re.match("([0-9A-Za-z]+)=(-?\d+),(.*)$", line)
if m is not None:
opcode = int(m.group(1), 16)
argc = int(m.group(2))
func = m.group(3)
am = re.findall(arg_regex, line)
sig = impl_sig(opcode, func)
xpath = "//Command[@ID=\"0x{:x}\"]".format(opcode)
res = d.xpath(xpath)
args = []
if len(res) != 0:
res = res[0]
sa_name = res.attrib['Name']
if sig != sa_name and False:
print("Name mismatch: {} v {}".format(sig, sa_name))
eargs = res.xpath("Args/Arg")
for x in range(len(eargs)):
args.append(arg_type(x+1, eargs[x]))
args = adjust_args(args)
args = process_args(args)
args = finalize_args(args)
postfix = ''
if argc >= 0:
signame = sig[1] + "(const ScriptArguments& args" + ''.join([", {} {}".format(x[0], x[1]) for x in args]) + ")"
for a in args:
postfix += "\n\tRW_UNUSED({});".format(a[1])
else:
signame = sig[1] + "(const ScriptArguments& args)"
postfix += "\n\tRW_UNUSED(args);"
argsdoc = ""
if len(args) > 0:
argsdoc = "\n" + "\n".join(["\t@arg {} {}".format(x[1], x[2]) for x in args])
if sig[0] == 'bool':
postfix += '\n\treturn false;'
functions_file.write(function_template.format(opcode, argsdoc, sig[0], signame, postfix, func))
parameters = ['args.getAs<{}>()'.format(type_) for type_, _, _, _ in args]
binding_file.write(binding_template.format(opcode, argc, ", ".join(parameters)))
if len(am) != abs(argc) and False:
raise RuntimeError("Mismatched argument count: {} v {}".format(len(am), argc))