diff --git a/.gitignore b/.gitignore index 14ad2717..49744f46 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ Release/ ISLE.EXE LEGO1.DLL build/ +original/ +compiler/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..c4adb083 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Configure decomp with standard setup", + "type": "shell", + "group": "build", + "command": "configure.py" + }, + { + "label": "Build decomp", + "type": "shell", + "group": "build", + "command": "build.py" + }, + { + "label": "Build decomp and print overall status", + "type": "shell", + "group": "build", + "command": "build.py --status" + }, + { + "label": "Build decomp and inspect function by cursor", + "type": "shell", + "group": "build", + "command": "build.py --inspect-shim ${file} ${lineNumber}" + } + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e7b584d9..309b0371 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,6 +173,13 @@ add_library(lego1 SHARED # Link libraries target_link_libraries(lego1 PRIVATE ddraw dsound winmm) +# Includes +target_include_directories(lego1 PRIVATE "${CMAKE_SOURCE_DIR}/LEGO1") + +# Add VC420 directories +target_include_directories(lego1 PRIVATE "$ENV{INCLUDE}") +target_link_directories(lego1 PRIVATE "$ENV{LIB}") + # Make sure filenames are ALL CAPS set_property(TARGET lego1 PROPERTY OUTPUT_NAME LEGO1) set_property(TARGET lego1 PROPERTY SUFFIX ".DLL") @@ -187,6 +194,10 @@ if (ISLE_BUILD_APP) # Include LEGO1 headers in ISLE target_include_directories(isle PRIVATE "${CMAKE_SOURCE_DIR}/LEGO1") + # Add VC420 directories + target_include_directories(isle PRIVATE "$ENV{INCLUDE}") + target_link_directories(isle PRIVATE "$ENV{LIB}") + # Link DSOUND, WINMM, and LEGO1 target_link_libraries(isle PRIVATE dsound winmm lego1) @@ -220,4 +231,4 @@ if (MSVC) set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "/incremental:no") set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "/incremental:no /debug") set(CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL "/incremental:no") -endif() +endif() \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 00000000..b3c50f24 --- /dev/null +++ b/build.py @@ -0,0 +1,78 @@ + +import pathlib +import subprocess +import argparse +import re +import requests + +parser = argparse.ArgumentParser() +parser.add_argument("-i", "--inspect", metavar='', + help="Get an assembly diff for the function at a given offset after building. " + "When decomping you will typically run this command over and over again as " + "you refine your code towards a 100% match.") +parser.add_argument("-s", "--status", action="store_true", + help="Get a general status report on the progress decompiling all functions.") +parser.add_argument("--inspect-shim", nargs=2, metavar=('', ''), + help="Inspect the assembly diff of the function spanning in . " + "Intended to be invoked by IDE commands.") +args = parser.parse_args() + +# Figure out what offset to pass to the reccmp tool if any +def get_inspect_offset(): + if inspect_offset := args.inspect: + if inspect_offset.startswith("0x"): + return inspect_offset + else: + return "0x" + inspect_offset + elif args.inspect_shim: + print("Inspect: ", args.inspect_shim[0], ":", args.inspect_shim[1]) + with open(args.inspect_shim[0], 'r') as f: + # get the lines in an array + lines = f.readlines() + OFFSET_PATTERN = re.compile(r'OFFSET:\s*LEGO1\s*(0x[0-9a-f]+)') + for i in range(int(args.inspect_shim[1]), 0, -1): + if i > 0 and i < len(lines): + if match := OFFSET_PATTERN.search(lines[i]): + inspect_offset = match.group(1) + break + if inspect_offset: + return inspect_offset + else: + print("Could not find any function to inspect near cursor.") + exit(1) + return None + +# If the build directory doesn't exist yet, run configure.py +if not pathlib.Path("build").exists(): + print("Not configured yet, please run configure.py first.") + exit(1) + +# Run cmake, with no parallel build because that does not play nice the +# MSVC420 compiler the original game was built with thanks to the different +# threads contending over the pdb file. +result = subprocess.run(["cmake", "--build", ".", "-j", "1"], cwd="build") + +def require_original(): + if not pathlib.Path("original/LEGO1.DLL").exists(): + print("Could not find original/LEGO1.DLL to compare results against. " + "Please obtain a copy the game and place its LEGO1.dll into the `original` folder.") + exit(1) + +# If the build succeeded, run the decompiler +if result.returncode == 0: + if inspect_offset := get_inspect_offset(): + require_original() + subprocess.run(["python", "tools/reccmp/reccmp.py", + "original/LEGO1.DLL", "build/LEGO1.DLL", + "build/LEGO1.PDB", ".", + "-v", inspect_offset]) + + elif args.status: + require_original() + subprocess.run(["python", "tools/reccmp/reccmp.py", + "original/LEGO1.DLL", "build/LEGO1.DLL", + "build/LEGO1.PDB", "."]) + +else: + print("Build failed.") + exit(1) \ No newline at end of file diff --git a/configure.py b/configure.py new file mode 100644 index 00000000..232e3546 --- /dev/null +++ b/configure.py @@ -0,0 +1,75 @@ + +import pathlib +import subprocess +import argparse +import shutil +import platform +import requests +import zipfile +import io +import os + +parser = argparse.ArgumentParser() +parser.add_argument("-m", "--modern-compiler", action="store_true", + help="Use a contemporary compiler instead of Microsoft Visual C++ 4.2 even though this will result in non-matching code") +args = parser.parse_args() + +# Do we have a builtin ninja? +specific_ninja = None +if shutil.which("ninja") is None: + sys = platform.system() + # Use the bundled ninja + if sys == "Windows": + specific_ninja = "./tools/ninja/ninja-win.exe" + elif sys == "Linux": + specific_ninja = "./tools/ninja/ninja-linux" + elif sys == "Darwin": + specific_ninja = "./tools/ninja/ninja-mac" + else: + print("No bundled ninja for this platform, please install ninja and add it to your path.") + exit(1) + specific_ninja = str(pathlib.Path(specific_ninja).absolute()) + +# Create a build folder in the current directory if it does not exist +pathlib.Path("build").mkdir(parents=True, exist_ok=True) + +# Create a folder for the compiler +pathlib.Path("compiler").mkdir(parents=True, exist_ok=True) + +# Check if MSVC420 is in the INCLUDE environment variable +set_compiler_paths = False +if not args.modern_compiler: + if not pathlib.Path("compiler/MSVC420-master").exists(): + print("MSVC420 not found in compiler/...") + url = "https://github.com/itsmattkc/MSVC420/archive/refs/heads/master.zip" + print("Downloading MSVC420...") + r = requests.get(url, stream=True) + print("Unzipping to compiler/...") + z = zipfile.ZipFile(io.BytesIO(r.content)) + z.extractall(path="compiler") + set_compiler_paths = True + +# Run cmake in the build folder +cmake_args = [] +cmake_args += ["-G", "Ninja"] +cmake_args += ["-DCMAKE_BUILD_TYPE=RelWithDebInfo"] +if specific_ninja: + cmake_args += ["-DCMAKE_MAKE_PROGRAM="+specific_ninja] + print("Using specific ninja:", specific_ninja) +if set_compiler_paths: + full_compiler_path = pathlib.Path.cwd() / "compiler" / "MSVC420-master/" + cmake_args += ["-DCMAKE_CXX_COMPILER=" + (full_compiler_path / "bin" / "CL.EXE").as_posix()] + cmake_args += ["-DCMAKE_C_COMPILER=" + (full_compiler_path / "bin" / "CL.EXE").as_posix()] + cmake_args += ["-DCMAKE_LINKER=" + (full_compiler_path / "bin" / "LINK.EXE").as_posix()] + cmake_args += ["-DCMAKE_RC_COMPILER=" + (full_compiler_path / "bin" / "RC.EXE").as_posix()] + os.environ["INCLUDE"] = (full_compiler_path / "include").as_posix() + os.environ["LIB"] = (full_compiler_path / "lib").as_posix() +cmake_args += [".."] +subprocess.run(["cmake"] + cmake_args, cwd="build") + +# Create a folder for the original binaries +if not pathlib.Path("original/LEGO1.DLL").exists(): + pathlib.Path("original").mkdir(parents=True, exist_ok=True) + print("Please obtain a copy of the original game and place its " + "ISLE.EXE and LEGO1.DLL into the `original` folder if you plan " + "on contributing so that the assembly diff tools can compare against them.") \ No newline at end of file diff --git a/tools/ninja/ninja-linux b/tools/ninja/ninja-linux new file mode 100644 index 00000000..3aaf2a9d Binary files /dev/null and b/tools/ninja/ninja-linux differ diff --git a/tools/ninja/ninja-mac b/tools/ninja/ninja-mac new file mode 100644 index 00000000..07dee2f5 Binary files /dev/null and b/tools/ninja/ninja-mac differ diff --git a/tools/ninja/ninja-win.exe b/tools/ninja/ninja-win.exe new file mode 100644 index 00000000..e4fafa04 Binary files /dev/null and b/tools/ninja/ninja-win.exe differ diff --git a/tools/reccmp/reccmp.py b/tools/reccmp/reccmp.py index abfe71a7..cc7aafd1 100755 --- a/tools/reccmp/reccmp.py +++ b/tools/reccmp/reccmp.py @@ -351,7 +351,8 @@ def parse_asm(file, addr, size): # If verbose, print the diff for that funciton to the output if verbose: if ratio == 1.0: - print("%s: %s 100%% match.\n\nOK!" % (hex(addr), recinfo.name)) + ok_text = "OK!" if plain else (colorama.Fore.GREEN + "✨ OK! ✨" + colorama.Style.RESET_ALL) + print("%s: %s 100%% match.\n\n%s\n\n" % (hex(addr), recinfo.name, ok_text)) else: for line in udiff: if line.startswith("++") or line.startswith("@@") or line.startswith("--"):