diff --git a/scripts/build.py b/scripts/build.py index de648ac1..c35ef7d0 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -10,7 +10,7 @@ def main(): parser = argparse.ArgumentParser("th06-build") parser.add_argument( - "--build-type", choices=["normal", "diffbuild"], default="normal" + "--build-type", choices=["normal", "diffbuild", "dllbuild"], default="normal" ) args = parser.parse_args() @@ -19,13 +19,22 @@ def main(): build_type = BuildType.NORMAL elif args.build_type == "diffbuild": build_type = BuildType.DIFFBUILD + elif args.build_type == "dllbuild": + build_type = BuildType.DLLBUILD configure(build_type) + ninja_args = [] + + if args.build_type == "dllbuild": + ninja_args += ["build/th06e.dll"] + else: + ninja_args += ["build/th06e.exe"] + # Then, run the build. We use run_windows_program to automatically go through # wine if running on linux/macos. scripts/th06run.bat will setup PATH and other # environment variables for the MSVC toolchain to work before calling ninja. run_windows_program( - [str(SCRIPTS_DIR / "th06run.bat"), "ninja", "build/th06e.exe"], + [str(SCRIPTS_DIR / "th06run.bat"), "ninja"] + ninja_args, cwd=str(SCRIPTS_DIR.parent), ) diff --git a/scripts/configure.py b/scripts/configure.py index d3793b30..a05a915d 100644 --- a/scripts/configure.py +++ b/scripts/configure.py @@ -9,6 +9,7 @@ class BuildType(Enum): NORMAL = 1 DIFFBUILD = 2 + DLLBUILD = 3 def configure(build_type): @@ -21,7 +22,7 @@ def configure(build_type): writer.variable("cl", "cl.exe") writer.variable( "cl_common_flags", - "/MT /EHsc /G5 /Gs /DNDEBUG /Zi /I $builddir/autogenerated /I src /I src/pbg3", + "/MT /EHsc /G5 /Gs /DNDEBUG /Zi /I $builddir/autogenerated /I src /I src/pbg3 /I Detours/src", ) writer.variable("cl_flags", "$cl_common_flags /Od /Oi /Ob1 /Op") writer.variable("cl_flags_pbg3", "$cl_common_flags /O2") @@ -29,7 +30,7 @@ def configure(build_type): "cl_flags_detours", "/W4 /WX /we4777 /we4800 /Zi /MT /Gy /Gm- /Zl /Od /DDETOUR_DEBUG=0 /DWIN32_LEAN_AND_MEAN /D_WIN32_WINNT=0x501", ) - if build_type == BuildType.DIFFBUILD: + if build_type in [BuildType.DIFFBUILD, BuildType.DLLBUILD]: writer.variable("cl_flags", "$cl_flags /DDIFFBUILD") writer.variable("cl_flags_pbg3", "$cl_flags_pbg3 /DDIFFBUILD") writer.variable("gas", "as.exe") @@ -74,6 +75,10 @@ def configure(build_type): "genstubs", "python scripts/generate_stubs.py --output $out", ) + writer.rule( + "gendetours", + "python scripts/generate_detours.py --input-def $builddir/th06.def --output $out", + ) writer.rule( "gendef", "python scripts/gendef.py --output $out $in", @@ -156,6 +161,25 @@ def configure(build_type): "$builddir/autogenerated/i18n.hpp", ], ) + writer.build( + "$builddir/autogenerated/detouring.cpp", + "gendetours", + implicit=[ + "config/implemented.csv", + "config/mapping.csv", + "scripts/generate_detours.py", + "$builddir/th06.def", + ], + ) + writer.build( + "$builddir/dllbuild.obj", + "cc", + "src/dllbuild.cpp", + implicit=[ + "$builddir/autogenerated/detouring.cpp", + "$builddir/autogenerated/i18n.hpp", + ], + ) writer.build("$builddir/globals.obj", "as", inputs="src/globals.asm") writer.build("$builddir/autogenerated/i18n.hpp", "geni18n", "src/i18n.tpl") @@ -180,8 +204,9 @@ def configure(build_type): + ["$builddir/" + src + ".obj" for src in pbg3_sources] + ["$builddir/th06.res", "$builddir/stubs.obj"] ) - if build_type == BuildType.DIFFBUILD: + if build_type in [BuildType.DIFFBUILD, BuildType.DLLBUILD]: objfiles += ["$builddir/globals.obj"] + th06_link_libs = "dxguid.lib d3dx8.lib d3d8.lib winmm.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib" writer.build( "$builddir/th06e.exe", @@ -192,6 +217,18 @@ def configure(build_type): "link_flags": "$th06_link_flags /debug /pdb:$builddir/th06e.pdb", }, ) + + writer.build( + "$builddir/th06e.dll", + "link", + inputs=objfiles + ["$builddir/dllbuild.obj", "$builddir/detours.lib"], + implicit=["$builddir/th06.def"], + variables={ + "link_libs": th06_link_libs, + "link_flags": "/DLL /debug /pdb:$builddir/th06e.pdb /export:DetourFinishHelperProcess,@1,NONAME /def:$builddir/th06.def /export:Direct3DCreate8 /export:malloc /export:calloc /export:realloc /export:??2@YAPAXI@Z /export:free /export:_msize", + }, + ) + writer.build( "$builddir/detours.lib", "link", diff --git a/scripts/generate_detours.py b/scripts/generate_detours.py new file mode 100644 index 00000000..cf52bbd9 --- /dev/null +++ b/scripts/generate_detours.py @@ -0,0 +1,127 @@ +import argparse +import csv +import sys + +parser = argparse.ArgumentParser( + prog="generate_detours", description="Generate stubs based on the stubs.csv file." +) +parser.add_argument( + "-o", "--output", action="store", help="File to store the generated stubs in" +) +parser.add_argument( + "-i", "--input-def", action="store", help="Def file to find mangled symbols in" +) +args = parser.parse_args() + + +def get_path_of_mangled_symbol(symbol): + if symbol[0] == "?": + cpp_symbol = symbol[1:] + path = cpp_symbol.split("@") + last = next((idx for idx, x in enumerate(path) if x == ""), None) + path = path[0:last] + + first_elem = path[0] + if first_elem[0] == "?": + if first_elem[1] == "0": + cls = first_elem[2:] + path[0] = cls + "::" + cls + elif first_elem[1] == "1": + cls = first_elem[2:] + path[0] = cls + "::~" + cls + elif first_elem[1:3] == "_H": + return None + else: + print("WARNING: Unknown special symbol " + symbol) + + return "::".join(reversed(path)) + elif symbol[0] == "_": + return symbol[1:].split("@", 1)[0] + else: + raise Exception("Unknown symbol kind " + symbol) + + +output = sys.stdout +if args.output: + output = open(args.output, "w") + +fun_to_mangled_map = {} +with open(args.input_def) as f: + for line in f: + if len(line.strip()) == 0: + continue + + if line.strip() == "EXPORTS": + continue + + mangled_symbol = line.rsplit(" ", 1)[0].strip() + fun_path = get_path_of_mangled_symbol(mangled_symbol) + if fun_path is None: + continue + print(fun_path) + if fun_path in fun_to_mangled_map: + raise Exception("Overload detected, two functions patch " + fun_path) + fun_to_mangled_map[fun_path] = mangled_symbol + +fun_to_mangled_map["operator_new"] = "??2@YAPAXI@Z" +fun_to_mangled_map["_malloc"] = "malloc" +fun_to_mangled_map["_calloc"] = "calloc" +fun_to_mangled_map["_realloc"] = "realloc" +fun_to_mangled_map["_free"] = "free" +fun_to_mangled_map["__msize"] = "_msize" + +with open("config/mapping.csv") as f: + mapping_csv = csv.reader(f) + mapping_obj = {} + for func in mapping_csv: + fun_name = func[0] + fun_addr = int(func[1], 16) + mapping_obj[fun_name] = fun_addr + +f = open("config/implemented.csv") +implemented_csv = csv.reader(f) + +print("Detouring detours[] = {", file=output) +first = True +for implemented in implemented_csv: + if not first: + print(",", file=output) + + fun_name = implemented[0] + fun_mangled_name = fun_to_mangled_map[fun_name] + fun_addr = mapping_obj[fun_name] + print( + " { " + hex(fun_addr) + ', "' + fun_mangled_name + '", FALSE }', + end="", + file=output, + ) + first = False + +f = open("config/stubbed.csv") +stubbed_csv = csv.reader(f) +first = True +for implemented in stubbed_csv: + print(",", file=output) + + fun_name = implemented[0] + fun_mangled_name = fun_to_mangled_map[fun_name] + fun_addr = mapping_obj[fun_name] + print( + " { " + hex(fun_addr) + ', "' + fun_mangled_name + '", TRUE }', + end="", + file=output, + ) + +# Add some necessary detouring to share MSVCRT heap with the main executable +for fun_name in ["_malloc", "_calloc", "_realloc", "operator_new", "_free", "__msize"]: + print(",", file=output) + + fun_mangled_name = fun_to_mangled_map[fun_name] + fun_addr = mapping_obj[fun_name] + print( + " { " + hex(fun_addr) + ', "' + fun_mangled_name + '", FALSE }', + end="", + file=output, + ) + +print("\n};", file=output) diff --git a/src/dllbuild.cpp b/src/dllbuild.cpp new file mode 100644 index 00000000..18492264 --- /dev/null +++ b/src/dllbuild.cpp @@ -0,0 +1,127 @@ +// DLLBUILD support +// +// The DLLBUILD is a special build of th06 meant to be injected into th06 1.02h, +// replacing the official functions with our own reimplementation. The primary +// purpose here is to validate that our reimplementations work even when they +// are not bit-accurate, but can also serve as a mechanism to enable modding +// before the reimplementation work is complete. +// +// The way it works is rather simple: We use the Detours library to hijack all +// the functions we have reimplemented from the original binary. Similarly, for +// the functions we want to call but have not yet reimplemented, we generate +// stub functions that simply forward the argument. Finally, to ensure the data +// is shared, we integrate the DIFFBUILD mechanism to store data at fixed +// locations in memory. +// +// The stubs are generated automatically by a script in +// scripts/generate_stubs.py, based on the information found in +// config/stubbed.csv. This generates a stubbed.cpp file that will be +// automatically compiled and linked. +// +// Meanwhile, the list of functions to detour is auto-generated by another +// script, scripts/generate_detours.py, based on the information found in +// config/implemented.csv. + +#include "AnmManager.hpp" +#include "AsciiManager.hpp" +#include "Chain.hpp" +#include "detours.h" +#include +#include + +struct Detouring +{ + size_t addressInOriginalBinary; + char *nameInDllReplacement; + BOOL stub; + + void *addrToReplace; + void *replaceWith; +}; + +#include "detouring.cpp" + +// For now, always use D3D_WRAPPER. In the future, we may want to swap it out +// with another "host" dll. +#define D3D_WRAPPER 1 +#ifdef D3D_WRAPPER +typedef IDirect3D8 *(WINAPI *Direct3DCreate8Proc)(UINT); +extern "C" IDirect3D8 *__stdcall Direct3DCreate8(UINT sdk_version) +{ + char path[MAX_PATH + 1]; + GetSystemDirectoryA(path, MAX_PATH); + strncat(path, "\\d3d8.dll", MAX_PATH - strlen(path)); + HMODULE d3d8dll = LoadLibraryA(path); + if (d3d8dll == NULL) + { + return NULL; + } + Direct3DCreate8Proc realproc = (Direct3DCreate8Proc)GetProcAddress(d3d8dll, "Direct3DCreate8"); + if (!realproc) + { + return NULL; + } + + return realproc(sdk_version); +} +#endif + +BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved) +{ + if (DetourIsHelperProcess()) + { + return TRUE; + } + + if (dwReason == DLL_PROCESS_ATTACH) + { + DetourRestoreAfterWith(); + + if (DetourTransactionBegin() != 0) + { + // TODO: Show an error. + return TRUE; + } + if (DetourUpdateThread(GetCurrentThread()) != 0) + { + // TODO: Show an error + DetourTransactionAbort(); + return TRUE; + } + for (size_t i = 0; i < sizeof(detours) / sizeof(detours[0]); i++) + { + if (!detours[i].stub) + { + detours[i].addrToReplace = (void *)detours[i].addressInOriginalBinary; + detours[i].replaceWith = (void *)GetProcAddress(hinst, detours[i].nameInDllReplacement); + } + else + { + detours[i].addrToReplace = (void *)GetProcAddress(hinst, detours[i].nameInDllReplacement); + detours[i].replaceWith = (void *)detours[i].addressInOriginalBinary; + } + if (DetourAttach(&detours[i].addrToReplace, detours[i].replaceWith) != 0) + { + // TODO: Show an error + DetourTransactionAbort(); + return TRUE; + } + } + if (DetourTransactionCommit() != 0) + { + // TODO: Show an error + return TRUE; + } + } + else if (dwReason == DLL_PROCESS_DETACH) + { + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + for (size_t i = 0; i < sizeof(detours) / sizeof(detours[0]); i++) + { + DetourDetach(&detours[i].addrToReplace, detours[i].replaceWith); + } + DetourTransactionCommit(); + } + return TRUE; +}