207 lines
9.2 KiB
Python
207 lines
9.2 KiB
Python
"""Generate mods/bwcompat/bwcompat.cpp: a FuBi extension DLL for Siege Editor 2 with Broken World's script functions.
|
|
|
|
usage: python -I tools/bwcompat_gen.py BROKEN_WORLD_EXE SIEGEEDITOR2_EXE
|
|
|
|
Broken World's skrits call ~40 C++ functions its exe added (GoMind::GetHomeLeashDistance, GoAttack::GetChanceToPenetrate,
|
|
...). Siege Editor 2 (2005, base game) cannot compile those skrits ("'GetHomeLeashDistance': undefined function", a
|
|
retry prompt per skrit). Siege Editor 2 loads %ds2dll_path%/*.ds2dll and FuBi imports each one's export table into its
|
|
function database (FuBi.cpp: "Imported library: '%s'"). This DLL exports, under the same decorated names, every function
|
|
Broken World's exe exports and Siege Editor 2 does not. Each export is a stub: it returns 0 (fldz for float/double;
|
|
a pointer to zeroed memory for references) and removes its arguments from the stack the way its calling convention
|
|
requires, computed from the decorated name. The editor does not run game logic, so the skrits only need to compile.
|
|
Names whose argument sizes cannot be derived (a struct or class passed by value) are left out and listed.
|
|
"""
|
|
import os, re, sys
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from ds2exports import exports
|
|
|
|
SIZE4 = set('CDEFGHIJKM') # char..long, float: one stack slot each
|
|
|
|
|
|
class Unsupported(Exception):
|
|
pass
|
|
|
|
|
|
def read_name(s, i):
|
|
"""Skip a qualified name starting at i: fragments 'name@', back-reference digits, or templates '?$name@<args>@',
|
|
up to the terminating '@'. Return the index after it."""
|
|
while True:
|
|
if s[i] == '@':
|
|
return i + 1
|
|
if s[i].isdigit():
|
|
i += 1
|
|
elif s.startswith('?$', i): # template: ?$name@ then template arguments up to '@'
|
|
i = s.index('@', i) + 1
|
|
while s[i] != '@':
|
|
i = skip_type(s, i)
|
|
i += 1
|
|
else:
|
|
i = s.index('@', i) + 1
|
|
|
|
|
|
def skip_type(s, i):
|
|
"""Index after one type at s[i] (template arguments: sizes do not matter)."""
|
|
c = s[i]
|
|
if c.isdigit() or c in SIZE4 or c in 'NX':
|
|
return i + 1
|
|
if c == '_':
|
|
return i + 2
|
|
if c in 'PQRSA':
|
|
j = i + 2 # pointer kind + pointee cv
|
|
return skip_type(s, j)
|
|
if c in 'UVTW':
|
|
return read_name(s, i + (2 if c == 'W' else 1))
|
|
if c == '$': # template value argument ($0<number>)
|
|
j = i + 2
|
|
while s[j] not in '@':
|
|
j += 1
|
|
return j + 1
|
|
if c == '?':
|
|
return skip_type(s, i + 2)
|
|
raise Unsupported('template arg ' + c)
|
|
|
|
|
|
def read_type(s, i, backrefs):
|
|
"""Parse one type at s[i]; return (stack bytes, kind, next index). kind: 'f' float/double, 'v' void, 'i' other."""
|
|
c = s[i]
|
|
if c.isdigit(): # back-reference to an earlier argument type
|
|
k = int(c)
|
|
if k >= len(backrefs):
|
|
raise Unsupported('backref')
|
|
return backrefs[k][0], backrefs[k][1], i + 1
|
|
if c == 'X':
|
|
return 0, 'v', i + 1
|
|
if c in SIZE4:
|
|
return 4, 'f' if c == 'M' else 'i', i + 1
|
|
if c == 'N':
|
|
return 8, 'f', i + 1
|
|
if c == '_':
|
|
c2 = s[i + 1]
|
|
if c2 in 'NDEFGHIW': # bool, __int8.. __int32, wchar_t
|
|
return 4, 'i', i + 2
|
|
if c2 in 'JK': # __int64
|
|
return 8, 'i', i + 2
|
|
raise Unsupported('_' + c2)
|
|
if c in 'PQRSA': # pointer / reference: one slot; skip the pointee
|
|
j = i + 1
|
|
if s[j] == 'E': # __ptr64 marker (not in x86 names, but harmless)
|
|
j += 1
|
|
j += 1 # cv qualifier of the pointee (A/B/C/D)
|
|
if s[j] == '6': # function pointer
|
|
raise Unsupported('function pointer')
|
|
return 4, 'p' if c == 'A' else 'i', skip_type(s, j) # any pointee: the pointer is one slot
|
|
if c == 'W': # enum: W4Name@@
|
|
return 4, 'i', read_name(s, i + 2)
|
|
if c in 'UVT': # struct/class/union by value: size unknown
|
|
raise Unsupported('by value')
|
|
if c == '?': # return type with storage class (?A / ?B ...)
|
|
return read_type(s, i + 2, backrefs)
|
|
raise Unsupported('type ' + c)
|
|
|
|
|
|
def signature(name):
|
|
"""(convention, argument bytes, return kind) of a decorated function name, or raise Unsupported."""
|
|
if not name.startswith('?') or name.startswith('??'):
|
|
raise Unsupported('special')
|
|
i = read_name(name, 1)
|
|
code = name[i]
|
|
i += 1
|
|
if code in 'QAIUEM': # member functions (public/private/protected, virtual): this-cv follows
|
|
member = True
|
|
i += 1
|
|
elif code in 'SCK': # static member functions
|
|
member = False
|
|
elif code == 'Y': # global function
|
|
member = False
|
|
else:
|
|
raise Unsupported('not a function (' + code + ')')
|
|
conv = name[i]
|
|
i += 1
|
|
if conv not in 'AEG':
|
|
raise Unsupported('convention ' + conv)
|
|
ret_size, ret_kind, i = read_type(name, i, [])
|
|
args, backrefs = 0, []
|
|
if name[i] == 'X':
|
|
i += 1
|
|
else:
|
|
while name[i] not in '@Z':
|
|
size, kind, j = read_type(name, i, backrefs)
|
|
if j - i > 1 and len(backrefs) < 10:
|
|
backrefs.append((size, kind))
|
|
args += size
|
|
i = j
|
|
callee_pops = conv in 'EG' # thiscall, stdcall
|
|
return ('thiscall' if conv == 'E' else 'stdcall' if conv == 'G' else 'cdecl'), (args if callee_pops else 0), ret_kind
|
|
|
|
|
|
def overload_key(name):
|
|
"""(qualified name, parameter list) of a decorated function name - what FuBi compares. FuBi refuses a whole DLL
|
|
("Critical error! Separate functions are nearly identical", "ImportBindings() failure") when an export differs from
|
|
an existing function only in return type or const-ness, e.g. Broken World's void WorldFXMgr::SetEffectTexture(...)
|
|
against Siege Editor 2's unsigned int version."""
|
|
try:
|
|
i = read_name(name, 1)
|
|
code = name[i]
|
|
i += 2 if code in 'QAIUEM' else 1
|
|
i += 1 # calling convention
|
|
_, _, i = read_type(name, i, []) # return type
|
|
return name[1:read_name(name, 1)], name[i:]
|
|
except (Unsupported, ValueError, IndexError):
|
|
return None
|
|
|
|
|
|
def main():
|
|
bw, se = exports(sys.argv[1]), exports(sys.argv[2])
|
|
missing = sorted(set(bw) - set(se))
|
|
stubs, exported, skipped = {}, [], []
|
|
se_keys = {k for k in (overload_key(n) for n in se) if k}
|
|
for n in missing:
|
|
if overload_key(n) in se_keys:
|
|
skipped.append((n, 'same function in Siege Editor 2 with another return type'))
|
|
continue
|
|
try:
|
|
conv, pops, ret = signature(n)
|
|
except (Unsupported, ValueError, IndexError) as e:
|
|
skipped.append((n, str(e)))
|
|
continue
|
|
key = 'Stub_%s_%d' % ({'f': 'float', 'p': 'ref'}.get(ret, 'int'), pops)
|
|
stubs[key] = (ret, pops)
|
|
exported.append((n, key))
|
|
out = ['// GENERATED by tools/bwcompat_gen.py - do not edit. Broken World script functions for Siege Editor 2 (stubs).',
|
|
'// %d exports (functions Broken World\'s exe exports and SiegeEditor2.exe does not); %d left out:' % (len(exported), len(skipped))]
|
|
out += ['// %s (%s)' % s for s in skipped]
|
|
out += ['#include <windows.h>', '', 'namespace {', 'alignas(16) char g_zero[4096]; // what reference-returning stubs point at', '}', '',
|
|
'extern "C" {']
|
|
for key, (ret, pops) in sorted(stubs.items()):
|
|
if ret == 'f':
|
|
body = 'fldz'
|
|
elif ret == 'p':
|
|
body = 'lea eax, g_zero'
|
|
else:
|
|
body = 'sub eax, eax' # not xor: an alternative C++ token under /permissive-
|
|
ret_insn = 'ret %d' % pops if pops else 'ret'
|
|
out.append('__declspec(naked) void %s() {\n __asm {\n %s\n %s\n }\n}' % (key, body, ret_insn))
|
|
out.append('}')
|
|
out.append('')
|
|
out += ['', 'BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; }', '']
|
|
dest = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'mods', 'bwcompat', 'bwcompat.cpp')
|
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
with open(dest, 'w', newline='\n') as f:
|
|
f.write('\n'.join(out))
|
|
# Exports: MSVC's linker cuts an aliased export name at its first '@', so the DLL is linked with placeholder names
|
|
# (E0000=Stub_...) and tools/pe_rename_exports.py then writes the decorated names (bwcompat.map: placeholder, name).
|
|
with open(dest[:-4] + '.def', 'w', newline='\n') as f:
|
|
f.write('; GENERATED by tools/bwcompat_gen.py - placeholder exports, renamed after linking (bwcompat.map)\n')
|
|
f.write('EXPORTS\n')
|
|
for i, (n, key) in enumerate(exported):
|
|
f.write(' E%04d=%s\n' % (i, key))
|
|
with open(dest[:-4] + '.map', 'w', newline='\n') as f:
|
|
for i, (n, key) in enumerate(exported):
|
|
f.write('E%04d\t%s\n' % (i, n))
|
|
print('%d missing exports: %d stubbed (%d stub kinds), %d left out' % (len(missing), len(exported), len(stubs), len(skipped)))
|
|
for n, why in skipped[:15]:
|
|
print(' left out %s (%s)' % (n, why))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|