Files
openttd-cmclient/gen_commands.py

354 lines
13 KiB
Python

import glob
import json
import re
from pathlib import Path
from pprint import pprint
RX_COMMAND = re.compile(r'(?P<returns>CommandCost|std::tuple<CommandCost, [^>]*>) (?P<name>Cmd\w*)\((?P<args>[^)]*)\);')
RX_DEF_TRAIT = re.compile(r'DEF_CMD_TRAIT\((?P<constant>\w+),\s+(?P<function>\w+),\s+(?P<flags>[^,()]+(?:\([^)]+\))?),\s+(?P<category>[\w:]+)\)')
RX_ARG = re.compile(r'(?P<type>(:?const |)[\w:]* &?)(?P<name>\w*)')
RX_CALLBACK = re.compile(r'void\s+(?P<name>Cc\w+)\(Commands')
RX_CALLBACK_REF = re.compile(r'CommandCallback(?:Data|)\s+(?P<name>Cc\w+);')
RX_CAMEL_TO_SNAKE = re.compile(r'(?<!^)(?=[A-Z])')
RX_CMD_CONSTANT = re.compile(r'CMD_[\w_]+')
FILES = [
'misc_cmd.h',
'object_cmd.h',
'order_cmd.h',
'rail_cmd.h',
'road_cmd.h',
'station_cmd.h',
'town_cmd.h',
'tunnelbridge_cmd.h',
'script/script_cmd.h',
]
BASE_CLASS = {
'BuildDock': 'StationBuildCommand',
'BuildRailStation': 'StationBuildCommand',
'BuildAirport': 'StationBuildCommand',
'BuildRoadStop': 'StationBuildCommand',
}
BASE_FIELDS = {
'StationBuildCommand': ['station_to_join', 'adjacent'],
}
BASE_DIR = Path(__file__).parent
OUTPUT = BASE_DIR / 'src/citymania/generated/cm_gen_commands'
GLOBAL_TYPES = set(('GoalType', 'GoalTypeID', 'GoalID'))
TYPE_CONVERT = {
'const std::string &': 'std::string',
}
AREA_CODE = {
('CMD_BUILD_RAIL_STATION',): (
'if (this->axis == AXIS_X) return CommandArea(this->tile, this->plat_len, this->numtracks);\n'
'return CommandArea(this->tile, this->numtracks, this->plat_len);\n'
),
('CMD_BUILD_AIRPORT',): (
'const AirportSpec *as = AirportSpec::Get(this->airport_type);\n'
'if (!as->IsAvailable()) return CommandArea(this->tile);\n'
'return CommandArea(this->tile, as->size_x, as->size_y);\n'
),
('CMD_BUILD_ROAD_STOP',):
'return CommandArea(this->tile, this->width, this->length);\n',
#TODO diagonal areas
(
'CMD_PLANT_TREE',
'CMD_BUILD_RAILROAD_TRACK',
'CMD_REMOVE_RAILROAD_TRACK',
'CMD_BUILD_LONG_ROAD',
'CMD_REMOVE_LONG_ROAD',
'CMD_CLEAR_AREA',
'CMD_BUILD_CANAL',
'CMD_LEVEL_LAND',
'CMD_BUILD_BRIDGE',
): 'return CommandArea(this->start_tile, this->tile);\n',
(
'CMD_BUILD_BRIDGE',
): 'return CommandArea(this->tile_start, this->tile);\n',
(
'CMD_BUILD_SINGLE_RAIL',
'CMD_BUILD_TRAIN_DEPOT',
'CMD_BUILD_BUOY',
'CMD_BUILD_ROAD',
'CMD_BUILD_ROAD_DEPOT',
'CMD_PLACE_SIGN',
'CMD_LANDSCAPE_CLEAR',
'CMD_TERRAFORM_LAND',
'CMD_FOUND_TOWN', # TODO
'CMD_BUILD_DOCK', # TODO
'CMD_BUILD_SHIP_DEPOT', # TODO
'CMD_BUILD_LOCK', # TODO
'CMD_BUILD_OBJECT', # TODO
'CMD_BUILD_INDUSTRY', # TODO
'CMD_BUILD_TUNNEL', # TODO find other end
): 'return CommandArea(this->tile);\n'
}
DEFAULT_AREA_CODE = 'return CommandArea();\n'
def parse_commands():
res = []
includes = []
callbacks = []
command_ids = {}
cid = 0
for l in open(BASE_DIR / 'src' / 'command_type.h'):
cl = RX_CMD_CONSTANT.findall(l)
if not cl:
continue
cmd = cl[0]
if cmd == 'CMD_END':
break
command_ids[cmd] = cid
cid += 1
for f in glob.glob(str(BASE_DIR / 'src' / '*_cmd.h')) + glob.glob(str(BASE_DIR / 'src' / '*' / '*_cmd.h')):
# for f in glob.glob(str(BASE_DIR / 'src' / 'group_cmd.h')):
includes.append(Path(f).name)
data = open(f).read()
traits = {}
for constant, name, flags, category in RX_DEF_TRAIT.findall(data):
traits[name] = constant, flags, category
callbacks.extend(RX_CALLBACK.findall(data))
callbacks.extend(RX_CALLBACK_REF.findall(data))
for returns, name, args_str in RX_COMMAND.findall(data):
trait = traits.get(name)
if not trait:
print(f'Not a command: {name}')
continue
print(f, name, end=' ', flush=True)
constant, flags, category = trait
cid = command_ids[constant]
if returns.startswith('std::tuple'):
result_type = returns[24: -1]
if result_type in GLOBAL_TYPES:
result_type = '::' + result_type
else:
result_type = None
args = [RX_ARG.fullmatch(x).group('type', 'name') for x in args_str.split(', ')]
args = args[1:] # flags
for i, (at, an) in enumerate(args):
at = at.strip()
if at in GLOBAL_TYPES:
at = '::' + at
args[i] = (at, an)
do_args = args[:]
if 'Location' in flags:
args = [('TileIndex', 'location')] + args
print(cid, constant, category, args)
callback_args = 'CommandCost' if result_type is None else f'CommandCost, {result_type}'
callback_type = f'std::function<void ({callback_args})>'
area_code = DEFAULT_AREA_CODE
for cl, cc in AREA_CODE.items():
if constant in cl:
area_code = cc
default_run_as = 'INVALID_COMPANY'
if 'Deity' in flags:
default_run_as = 'OWNER_DEITY'
if 'Server' in flags or 'CMD_SPECTATOR' in flags:
default_run_as = 'COMPANY_SPECTATOR' # same as INVALID though
res.append({
'name': name[3:],
'id': cid,
'constant': constant,
'category': category,
'flags': flags,
'default_run_as': default_run_as,
'args': args,
'do_args': do_args,
'returns': returns,
'result_type': result_type,
'callback_type': callback_type,
'area_code': area_code,
})
return res, includes, callbacks
CPP_TEMPLATES = '''\
inline constexpr size_t _callback_tuple_size = std::tuple_size_v<decltype(_callback_tuple)>;
#ifdef SILENCE_GCC_FUNCTION_POINTER_CAST
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
template <size_t... i>
inline auto MakeCallbackTable(std::index_sequence<i...>) noexcept {
return std::array<::CommandCallback *, sizeof...(i)>{{ reinterpret_cast<::CommandCallback *>(reinterpret_cast<void(*)()>(std::get<i>(_callback_tuple)))... }}; // MingW64 fails linking when casting a pointer to its own type. To work around, cast it to some other type first.
}
/** Type-erased table of callbacks. */
static auto _callback_table = MakeCallbackTable(std::make_index_sequence<_callback_tuple_size>{});\n
template <typename T> struct CallbackArgsHelper;
template <typename... Targs>
struct CallbackArgsHelper<void(*const)(Commands, const CommandCost &, Targs...)> {
using Args = std::tuple<std::decay_t<Targs>...>;
};
#ifdef SILENCE_GCC_FUNCTION_POINTER_CAST
# pragma GCC diagnostic pop
#endif
static size_t FindCallbackIndex(::CommandCallback *callback) {
if (auto it = std::ranges::find(_callback_table, callback); it != std::end(_callback_table)) {
return static_cast<size_t>(std::distance(std::begin(_callback_table), it));
}
return std::numeric_limits<size_t>::max();
}
template <Commands Tcmd, size_t Tcb, typename... Targs>
bool _DoPost(StringID err_msg, Targs... args) {
return ::Command<Tcmd>::Post(err_msg, std::get<Tcb>(_callback_tuple), std::forward<Targs>(args)...);
}
template <Commands Tcmd, size_t Tcb, typename... Targs>
constexpr auto MakeCallback() noexcept {
/* Check if the callback matches with the command arguments. If not, don''t generate an Unpack proc. */
using Tcallback = std::tuple_element_t<Tcb, decltype(_callback_tuple)>;
if constexpr (std::is_same_v<Tcallback, ::CommandCallback * const> ||
std::is_same_v<Tcallback, CommandCallbackData * const> ||
std::is_same_v<typename CommandTraits<Tcmd>::CbArgs, typename CallbackArgsHelper<Tcallback>::Args> ||
(!std::is_void_v<typename CommandTraits<Tcmd>::RetTypes> && std::is_same_v<typename CallbackArgsHelper<typename CommandTraits<Tcmd>::RetCallbackProc const>::Args, typename CallbackArgsHelper<Tcallback>::Args>)) {
return &_DoPost<Tcmd, Tcb, Targs...>;
} else {
return nullptr;
}
}
template <Commands Tcmd, typename... Targs, size_t... i>
inline constexpr auto MakeDispatchTableHelper(std::index_sequence<i...>) noexcept
{
return std::array<bool (*)(StringID err_msg, Targs...), sizeof...(i)>{MakeCallback<Tcmd, i, Targs...>()... };
}
template <Commands Tcmd, typename... Targs>
inline constexpr auto MakeDispatchTable() noexcept
{
return MakeDispatchTableHelper<Tcmd, Targs...>(std::make_index_sequence<_callback_tuple_size>{});
}
'''
def run():
commands, includes, callbacks = parse_commands()
json.dump({
'commands': commands,
'includes': includes,
'callbacks': callbacks,
}, open('commands.json', 'w'))
with open(OUTPUT.with_suffix('.hpp'), 'w') as f:
f.write(
'// This file is generated by gen_commands.py, do not edit\n\n'
'#ifndef CM_GEN_COMMANDS_HPP\n'
'#define CM_GEN_COMMANDS_HPP\n'
'#include "../cm_command_type.hpp"\n'
)
for i in includes:
f.write(f'#include "../../{i}"\n')
f.write('\n')
f.write(
'namespace citymania {\n'
'namespace cmd {\n\n'
)
for cmd in commands:
name = cmd['name']
base_class = BASE_CLASS.get(name, 'Command')
base_fields = BASE_FIELDS.get(base_class, [])
f.write(
f'class {name}: public {base_class} {{\n'
f'public:\n'
)
args_list = ', '.join(f'{at} {an}' for at, an in cmd['args'])
args_init = ', '.join(f'{an}{{{an}}}' for _, an in cmd['args'] if an not in base_fields)
if base_fields:
base_joined = ', '.join(base_fields)
args_init = f'{base_class}{{{base_joined}}}, ' + args_init
for at, an in cmd['args']:
if an in base_fields:
continue
f.write(f' {at} {an};\n')
f.write(f'\n')
if args_init:
f.write(
f' {name}({args_list})\n'
f' :{args_init} {{}}\n'
)
else:
f.write(f' {name}({args_list}) {{}}\n')
f.write(
f' ~{name}() override {{}}\n'
f'\n'
f' bool _post(::CommandCallback * callback) override;\n'
f' CommandCost _do(DoCommandFlags flags) override;\n'
f' Commands get_command() override;\n'
f'}};\n\n'
)
f.write(
'} // namespace cmd\n'
'} // namespace citymania\n'
'#endif\n'
)
with open(OUTPUT.with_suffix('.cpp'), 'w') as f:
f.write(
'// This file is generated by gen_commands.py, do not edit\n\n'
'#include "../../stdafx.h"\n'
'#include "cm_gen_commands.hpp"\n'
)
for fn in FILES:
f.write(f'#include "../../{fn}"\n')
f.write(
'namespace citymania {\n'
'namespace cmd {\n\n'
)
f.write(
'/*\n'
' * The code is mostly copied from network_command.cpp\n'
' * but the table is not the same.\n'
' */\n'
'static constexpr auto _callback_tuple = std::make_tuple(\n'
' (::CommandCallback *)nullptr, // Make sure this is actually a pointer-to-function.\n'
)
for i, cb in enumerate(callbacks):
comma = ',' if i != len(callbacks) - 1 else ''
f.write(f' &{cb}{comma}\n')
f.write(');\n\n')
f.write(CPP_TEMPLATES)
for cmd in commands:
name = cmd['name']
constant = cmd['constant']
this_args_list = ', '.join(f'this->{an}' for _, an in cmd['args'])
args_list = ', '.join(f'{an}' for _, an in cmd['args'])
args_type_list = ', '.join(f'{at}' for at, an in cmd['args'])
test_args_list = ', '.join(f'{an}' for _, an in cmd['do_args'])
cost_getter = '' if cmd['result_type'] is None else 'std::get<0>'
sep_args_list = sep_args_type_list = sep_this_args_list = ''
if args_list:
sep_args_list = ', ' + args_list
sep_args_type_list = ', ' + args_type_list
sep_this_args_list = ', ' + this_args_list
f.write(
f'Commands {name}::get_command() {{ return {constant}; }}\n'
f'static constexpr auto _{name}_dispatch = MakeDispatchTable<{constant}{sep_args_type_list}>();\n'
f'bool {name}::_post(::CommandCallback *callback) {{\n'
f' return _{name}_dispatch[FindCallbackIndex(callback)](this->error{sep_this_args_list});\n'
'}\n'
f'CommandCost {name}::_do(DoCommandFlags flags) {{\n'
f' return {cost_getter}(::Command<{constant}>::Do(flags, {test_args_list}));\n'
'}\n'
)
f.write('\n')
f.write(
'} // namespace cmd\n'
'} // namespace citymania\n'
)
if __name__ == "__main__":
run()