From e5e60a5cef6a8c8e06c9f87d1ecb65266716376c Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Jan 2026 22:40:14 +0300 Subject: [PATCH 01/17] add config support --- patcher/Config.py | 66 ++++++++++++++++++++++ patcher/PEData.py | 2 +- patcher/Patcher.py | 109 ++++++++++++++---------------------- patcher/config_example.json | 58 +++++++++++++++++++ 4 files changed, 168 insertions(+), 67 deletions(-) create mode 100644 patcher/Config.py create mode 100644 patcher/config_example.json diff --git a/patcher/Config.py b/patcher/Config.py new file mode 100644 index 0000000..dc6a83f --- /dev/null +++ b/patcher/Config.py @@ -0,0 +1,66 @@ +import json +from pathlib import Path +from dataclasses import dataclass, field +from typing import Self + + +@dataclass +class Config: + target_folder_path: Path + build_folder_path: Path + input_name: str = "ForgedAlliance_base.exe" + output_name: str = "ForgedAlliance_exxt.exe" + + clang_path: Path = "clang++" + gcc_path: Path = "g++" + linker_path: Path = "ld" + + clang_flags: tuple[str] = () + gcc_flags: tuple[str] = () + asm_flags: tuple[str] = () + + functions: dict[str, str] = field(default_factory=dict) + + @classmethod + def load_from_json(cls, path: Path) -> Self: + with open(path, 'r') as f: + config = json.load(f) + return cls( + target_folder_path=config.get("target_folder_path", path.parent), + build_folder_path=config.get("build_folder_path"), + input_name=config.get("input_name", Config.input_name), + output_name=config.get("output_name", Config.output_name), + clang_path=config.get("clang", Config.clang_path), + gcc_path=config.get("gcc", Config.gcc_path), + linker_path=config.get("linker", Config.linker_path), + clang_flags=config.get("clang_flags", Config.clang_flags), + gcc_flags=config.get("gcc_flags", Config.gcc_flags), + asm_flags=config.get("asm_flags", Config.asm_flags), + functions=config.get("functions", {}), + ) + + def __post_init__(self): + self.target_folder_path = Path(self.target_folder_path) + self.build_folder_path = Path(self.build_folder_path)\ + if self.build_folder_path \ + else self.target_folder_path / "build" + + self.clang_path = Path(self.clang_path) + self.gcc_path = Path(self.gcc_path) + self.linker_path = Path(self.linker_path) + + if not self.target_folder_path.is_absolute(): + raise ValueError( + "target_folder_path must be an absolute path to folder") + + if not self.build_folder_path.is_absolute(): + raise ValueError( + "build_folder_path must be an absolute path to folder") + + @property + def input_path(self) -> Path: + return self.target_folder_path / self.input_name + + @property + def output_path(self) -> Path: + return self.build_folder_path / self.output_name diff --git a/patcher/PEData.py b/patcher/PEData.py index d369937..6dcbb95 100644 --- a/patcher/PEData.py +++ b/patcher/PEData.py @@ -76,4 +76,4 @@ def find_sect(self, name: str) -> Optional[PESect]: for sect in self.sects: if sect.name == name: return sect - return None + raise Exception(f"Couldn't find section {name}") diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 6d760f5..9c8a086 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -3,19 +3,14 @@ import os from pathlib import Path import re +import json from typing import Optional import struct import itertools from patcher import Hook +from .Config import Config -CLANG_FLAGS = " ".join(["-pipe -m32 -Os -nostdlib -Werror -masm=intel -std=c++20 -march=core2 -c", - ]) - -GCC_FLAGS = " ".join(["-pipe -m32 -Os -fno-exceptions -nostdlib -nostartfiles -fpermissive -masm=intel -std=c++20 -march=core2 -mfpmath=both", - ]) -GCC_FLAGS_ASM = " ".join(["-pipe -m32 -Os -fno-exceptions -nostdlib -nostartfiles -w -fpermissive -masm=intel -std=c++20 -march=core2 -mfpmath=both", - ]) SECT_SIZE = 0x80000 ASM_RE = re.compile(r"(asm\(\"(0[xX][0-9a-fA-F]{1,8})\"\);)", re.IGNORECASE) @@ -92,17 +87,6 @@ def create_sections_file(path: Path, address_map: dict[str, str]): HEADER = """ OUTPUT_FORMAT(pei-i386) OUTPUT(section.pe) - """ - FUNC_NAMES = """ -_atexit = 0xA8211E; -__Znwj = 0xA825B9; -__ZdlPvj = 0x958C40; -"__imp__GetModuleHandleA@4" = 0xC0F378; -"__imp__GetProcAddress@8" = 0xC0F48C; -"___CxxFrameHandler3" = 0xA8958C; -"___std_terminate" = 0xA994FB; /*idk addr*/ -"??_7type_info@@6B@" = 0xD72A88; -"__CxxThrowException@8" = 0x00A89950; """ SECTIONS = """ SECTIONS { @@ -133,7 +117,6 @@ def create_sections_file(path: Path, address_map: dict[str, str]): with open(path, "w") as f: f.write(HEADER) - f.write(FUNC_NAMES) for name, address in address_map.items(): f.write(f"\"{name}\" = {address};\n") f.write(SECTIONS) @@ -154,7 +137,7 @@ def parse_sect_map(file_path: Path) -> dict[str, str]: line = f.readline() while not line.startswith(" *(.data*)"): - items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split("(")[0].split(" ") + items = SPACES_RE.sub(" ", line.strip()).split("(")[0].split(" ") if len(items) != 2 or items[1].startswith("?"): line = f.readline() continue @@ -171,7 +154,7 @@ def parse_sect_map(file_path: Path) -> dict[str, str]: line = f.readline() while not line.startswith(" *(.bss*)"): - items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split(" ") + items = SPACES_RE.sub(" ", line.strip()).split(" ") if len(items) != 2 or items[1].startswith("?"): line = f.readline() continue @@ -188,7 +171,7 @@ def parse_sect_map(file_path: Path) -> dict[str, str]: line = f.readline() while not line.startswith(" *(.rdata)"): - items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split(" ") + items = SPACES_RE.sub(" ", line.strip()).split(" ") if len(items) != 2 or items[1].startswith("?"): line = f.readline() continue @@ -308,10 +291,10 @@ def run_system(command: str) -> int: return os.system(command.replace("\n", " ")) -def patch(_, target_folder, clang_compiler_path, linker_path, gcc_compiler_path, * args): - target_path = Path(target_folder) +def patch(config_path): + config = Config.load_from_json(Path(config_path)) - base_pe = PEData(target_path / "ForgedAlliance_base.exe") + base_pe = PEData(config.input_path) new_v_offset = 0 new_f_offset = 0 @@ -324,50 +307,42 @@ def align(v, a): new_v_offset = align(new_v_offset, base_pe.sectalign) new_f_offset = align(new_f_offset, base_pe.filealign) - print(f"Image base: {base_pe.imgbase + new_v_offset - 0x1000:x}") + image_base = base_pe.imgbase + new_v_offset - 0x1000 + print(f"Image base: {image_base:x}") - section_folder_path = target_path / "section" - build_folder_path = target_path / "build" + section_folder_path = config.target_folder_path / "section" + include_folder_path = config.target_folder_path / "include" + hooks_folder_path = config.target_folder_path / "hooks" + build_folder_path = config.build_folder_path paths = find_patch_files(section_folder_path) - # files_contents = read_files_contents(f"{target_path}/section/", paths) - # files_contents, address_names = preprocess_lines(files_contents) - with open(section_folder_path / "main.cpp", "w") as main_file: for path in paths: main_file.writelines(f"#include \"{path}\"\n") - # main_file.writelines(files_contents) function_addresses = { - name: name for name in scan_header_files(target_path)} - - cxx_files_contents = read_files_contents(section_folder_path, list_files_at( - section_folder_path, "**/*.cxx", ["main.cxx"])) - cxx_files_contents, cxx_address_names = preprocess_lines( - cxx_files_contents) + name: name for name in scan_header_files(config.target_folder_path)} with open(section_folder_path / "main.cxx", "w") as main_file: - main_file.writelines(cxx_files_contents) - - folders = scan_for_headers_in_section(section_folder_path) - includes = " ".join((f"-I ../section/{folder}/" for folder in folders)) + for path in list_files_at(section_folder_path, "**/*.cxx", ["main.cxx"]): + main_file.writelines(f"#include \"{path}\"\n") if run_system( - f"""cd {build_folder_path} & - {clang_compiler_path} {CLANG_FLAGS} - -I ../include/ {includes} - ../section/main.cxx -o clangfile.o"""): + f"""{config.clang_path} + -c {" ".join(config.clang_flags)} + -I {include_folder_path} {section_folder_path / "main.cxx"} + -o {build_folder_path / "clangfile.o"}"""): raise Exception("Errors occurred during building of cxx files") - create_sections_file(target_path / "section.ld", - function_addresses | cxx_address_names) + create_sections_file(build_folder_path / "section.ld", + function_addresses | config.functions) if run_system( - f"""cd {build_folder_path} & - {gcc_compiler_path} {GCC_FLAGS} - -I ../include/ {includes} - -Wl,-T,../section.ld,--image-base,{base_pe.imgbase + new_v_offset - 0x1000},-s,-Map,sectmap.txt,-o,section.pe - ../section/main.cpp"""): + f""" cd {build_folder_path} & + {config.gcc_path} {" ".join(config.gcc_flags)} + -I {include_folder_path} + -Wl,-T,section.ld,--image-base,{image_base},-s,-Map,sectmap.txt,-o,section.pe + {section_folder_path / "main.cpp"}"""): raise Exception("Errors occurred during building of patch files") remove_files_at(build_folder_path, "**/*.o") @@ -382,26 +357,26 @@ def create_defines_file(path: Path, addresses: dict[str, str]): ]) for name, address in addresses.items(): f.write(f"#define {name} {address}\n") - create_defines_file(target_path / "define.h", addresses) + create_defines_file(config.target_folder_path / "define.h", addresses) def generate_hook_files(folder_path: Path): for file_path in list_files_at(folder_path, "**/*.hook"): - hook = Hook. load_hook(folder_path/file_path) + hook = Hook.load_hook(folder_path/file_path) hook_path = file_path.replace(os.sep, "_") + ".cpp" print(f"Generating {hook_path}") with open(folder_path/hook_path, "w") as f: f.write(hook.to_cpp()) - generate_hook_files(target_path/"hooks") + generate_hook_files(config.target_folder_path/"hooks") if run_system( f"""cd {build_folder_path} & - {gcc_compiler_path} -c {GCC_FLAGS_ASM} ../hooks/*.cpp"""): + {config.gcc_path} -c {" ".join(config.asm_flags)} {hooks_folder_path / "*.cpp"}"""): raise Exception("Errors occurred during building of hooks files") hooks: list[COFFData] = [] for path in list_files_at(build_folder_path, "**/*.o"): - coff_data = COFFData(build_folder_path / path, f"build/{path}") + coff_data = COFFData(build_folder_path / path, path) for sect in coff_data.sects: if len(sect.name) >= 8: raise Exception(f"sect name too long {sect.name}") @@ -416,10 +391,10 @@ def generate_hook_files(folder_path: Path): ssize = section_pe.sects[-1].v_offset + \ section_pe.sects[-1].v_size + section_pe.sects[0].v_offset - with open(target_path / "patch.ld", "w") as pld: + with open(build_folder_path / "patch.ld", "w") as pld: pld.writelines([ "OUTPUT_FORMAT(pei-i386)\n", - "OUTPUT(build/patch.pe)\n", + "OUTPUT(patch.pe)\n", ]) for name, address in addresses.items(): @@ -453,9 +428,10 @@ def generate_hook_files(folder_path: Path): " }\n", "}" ]) + if run_system( - f"""cd {target_path} & - {linker_path} -T patch.ld --image-base {base_pe.imgbase} -s -Map build/patchmap.txt"""): + f"""cd {build_folder_path} & + {config.linker_path} -T patch.ld --image-base {base_pe.imgbase} -s -Map patchmap.txt"""): raise Exception("Errors occurred during linking") base_file_data = bytearray(base_pe.data) @@ -489,7 +465,7 @@ def replace_data(new_data, offset): if SECT_SIZE > 0: if SECT_SIZE < exxt_sect.f_size: raise Exception( - f"Section size too small. Required: 0x{exxt_sect.f_size:x}") + f"Section size too small. Required: 0x{exxt_sect.f_size: x}") exxt_sect.v_size = SECT_SIZE exxt_sect.f_size = SECT_SIZE @@ -501,10 +477,11 @@ def replace_data(new_data, offset): replace_data(section_pe.data[s.f_offset:s.f_offset+s.f_size], nsect.f_offset+s.v_offset-section_pe.sects[0].v_offset) - apply_sig_patches(target_path / "SigPatches.txt", base_file_data) + apply_sig_patches(config.target_folder_path / + "SigPatches.txt", base_file_data) def save_new_base_data(data: bytearray): - with open(target_path / "ForgedAlliance_exxt.exe", "wb") as nf: + with open(config.output_path, "wb") as nf: sect_count = len(base_pe.sects) nf.write(data) nf.seek(base_pe.offset+0x6) @@ -521,4 +498,4 @@ def save_new_base_data(data: bytearray): save_new_base_data(base_file_data) remove_files_at(build_folder_path, "**/*.o") - remove_files_at(target_path/"hooks", "*.hook.cpp") + remove_files_at(hooks_folder_path, "*.hook.cpp") diff --git a/patcher/config_example.json b/patcher/config_example.json new file mode 100644 index 0000000..9ec199f --- /dev/null +++ b/patcher/config_example.json @@ -0,0 +1,58 @@ +{ + "target_folder_path": "", + "input_name": "ForgedAlliance_base.exe", + "output_name": "ForgedAlliance_exxt.exe", + "clang": "clang++.exe", + "gcc": "g++.exe", + "linker": "ld.exe", + "clang_flags": [ + "-pipe", + "-m32", + "-O3", + "-nostdlib", + "-Werror", + "-masm=intel", + "-std=c++20", + "-march=core2" + ], + "gcc_flags": [ + "-pipe", + "-m32", + "-Os", + "-fno-exceptions", + "-nostdlib", + "-nostartfiles", + "-fpermissive", + "-masm=intel", + "-std=c++20", + "-march=core2", + "-mfpmath=both" + ], + "asm_flags": [ + "-pipe", + "-m32", + "-Os", + "-fno-exceptions", + "-nostdlib", + "-nostartfiles", + "-w", + "-fpermissive", + "-masm=intel", + "-std=c++20", + "-march=core2", + "-mfpmath=both" + ], + "functions": { + "_atexit": "0xA8211E", + "__Znwj": "0xA825B9", + "__ZdlPvj": "0x958C40", + "__imp__GetModuleHandleA@4": "0xC0F378", + "__imp__GetProcAddress@8": "0xC0F48C", + "___CxxFrameHandler3": "0xA8958C", + "___std_terminate": "0xA994FB", + "??_7type_info@@6B@": "0xD72A88", + "__CxxThrowException@8": "0x00A89950", + "_memset": "0xA89110", + "__invoke_watson": "0x00A848E8" + } +} \ No newline at end of file From 265f825424b175f3af52d56f99049ede35fad3e3 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Jan 2026 22:43:49 +0300 Subject: [PATCH 02/17] revert changes --- patcher/Patcher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 9c8a086..de2a74d 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -137,7 +137,7 @@ def parse_sect_map(file_path: Path) -> dict[str, str]: line = f.readline() while not line.startswith(" *(.data*)"): - items = SPACES_RE.sub(" ", line.strip()).split("(")[0].split(" ") + items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split(" ") if len(items) != 2 or items[1].startswith("?"): line = f.readline() continue @@ -154,7 +154,7 @@ def parse_sect_map(file_path: Path) -> dict[str, str]: line = f.readline() while not line.startswith(" *(.bss*)"): - items = SPACES_RE.sub(" ", line.strip()).split(" ") + items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split(" ") if len(items) != 2 or items[1].startswith("?"): line = f.readline() continue @@ -171,7 +171,7 @@ def parse_sect_map(file_path: Path) -> dict[str, str]: line = f.readline() while not line.startswith(" *(.rdata)"): - items = SPACES_RE.sub(" ", line.strip()).split(" ") + items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split(" ") if len(items) != 2 or items[1].startswith("?"): line = f.readline() continue From f6d9d928065242ba246fc6ee98c2b66e6a0f3d95 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Jan 2026 22:44:53 +0300 Subject: [PATCH 03/17] F --- patcher/Patcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index de2a74d..084a6d2 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -137,7 +137,7 @@ def parse_sect_map(file_path: Path) -> dict[str, str]: line = f.readline() while not line.startswith(" *(.data*)"): - items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split(" ") + items = SPACES_RE.sub(" ", line.strip().replace("::", "__")).split("(")[0].split(" ") if len(items) != 2 or items[1].startswith("?"): line = f.readline() continue From 670cfcfe984009d223657b87508a6936087e07dd Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Jan 2026 22:47:55 +0300 Subject: [PATCH 04/17] pass args starting with second --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 398a860..fdd8f8d 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,6 @@ if __name__ == "__main__": start = time.time() - patcher.patch(*sys.argv) + patcher.patch(*sys.argv[1:]) end = time.time() print(f"Patched in {end-start:.2f}s") From 28d3b3fc8cb5443bf0e3f1501c1e3925539b5d7d Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Jan 2026 23:32:38 +0300 Subject: [PATCH 05/17] add relative path support --- patcher/config_example.json => config_example.json | 2 +- patcher/Config.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) rename patcher/config_example.json => config_example.json (94%) diff --git a/patcher/config_example.json b/config_example.json similarity index 94% rename from patcher/config_example.json rename to config_example.json index 9ec199f..b48e285 100644 --- a/patcher/config_example.json +++ b/config_example.json @@ -1,5 +1,5 @@ { - "target_folder_path": "", + "target_folder_path": "FA-Binary-Patches", "input_name": "ForgedAlliance_base.exe", "output_name": "ForgedAlliance_exxt.exe", "clang": "clang++.exe", diff --git a/patcher/Config.py b/patcher/Config.py index dc6a83f..f6ef500 100644 --- a/patcher/Config.py +++ b/patcher/Config.py @@ -6,6 +6,7 @@ @dataclass class Config: + path: Path target_folder_path: Path build_folder_path: Path input_name: str = "ForgedAlliance_base.exe" @@ -23,9 +24,12 @@ class Config: @classmethod def load_from_json(cls, path: Path) -> Self: + path = Path(path).resolve() with open(path, 'r') as f: config = json.load(f) + return cls( + path=path, target_folder_path=config.get("target_folder_path", path.parent), build_folder_path=config.get("build_folder_path"), input_name=config.get("input_name", Config.input_name), @@ -41,6 +45,10 @@ def load_from_json(cls, path: Path) -> Self: def __post_init__(self): self.target_folder_path = Path(self.target_folder_path) + + if not self.target_folder_path.is_absolute(): + self.target_folder_path = self.path.parent / self.target_folder_path + self.build_folder_path = Path(self.build_folder_path)\ if self.build_folder_path \ else self.target_folder_path / "build" @@ -49,10 +57,6 @@ def __post_init__(self): self.gcc_path = Path(self.gcc_path) self.linker_path = Path(self.linker_path) - if not self.target_folder_path.is_absolute(): - raise ValueError( - "target_folder_path must be an absolute path to folder") - if not self.build_folder_path.is_absolute(): raise ValueError( "build_folder_path must be an absolute path to folder") From 5466e6accc1b56fbf45e7a516e59e708dca2122e Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Jan 2026 23:49:59 +0300 Subject: [PATCH 06/17] handle paths in run_system --- patcher/Patcher.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 084a6d2..49b86c6 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -9,6 +9,7 @@ import itertools from patcher import Hook from .Config import Config +from string.templatelib import Template SECT_SIZE = 0x80000 @@ -286,7 +287,21 @@ def scan_for_headers_in_section(sections_path: Path): return folders -def run_system(command: str) -> int: +def run_system(template: Template) -> int: + if not isinstance(template, Template): + raise Exception("Not a template") + + command = [] + for part in template: + if isinstance(part, str): + command.append(part) + else: + value = part.value + if isinstance(value, Path) and ' ' in str(value): + command.append(f"\"{value}\"") + else: + command.append(str(value)) + command = "".join(command) print(command) return os.system(command.replace("\n", " ")) @@ -329,21 +344,15 @@ def align(v, a): main_file.writelines(f"#include \"{path}\"\n") if run_system( - f"""{config.clang_path} - -c {" ".join(config.clang_flags)} - -I {include_folder_path} {section_folder_path / "main.cxx"} - -o {build_folder_path / "clangfile.o"}"""): + t"""{config.clang_path} -c {" ".join(config.clang_flags)} -I {include_folder_path} {section_folder_path / "main.cxx"} -o {build_folder_path / "clangfile.o"}"""): raise Exception("Errors occurred during building of cxx files") create_sections_file(build_folder_path / "section.ld", function_addresses | config.functions) if run_system( - f""" cd {build_folder_path} & - {config.gcc_path} {" ".join(config.gcc_flags)} - -I {include_folder_path} - -Wl,-T,section.ld,--image-base,{image_base},-s,-Map,sectmap.txt,-o,section.pe - {section_folder_path / "main.cpp"}"""): - raise Exception("Errors occurred during building of patch files") + t""" cd {build_folder_path} & + {config.gcc_path} {" ".join(config.gcc_flags)} -I {include_folder_path} -Wl,-T,section.ld,--image-base,{image_base},-s,-Map,sectmap.txt,-o,section.pe {section_folder_path / "main.cpp"}"""): + raise Exception("Errors occurred during building of patch files") remove_files_at(build_folder_path, "**/*.o") @@ -370,9 +379,9 @@ def generate_hook_files(folder_path: Path): generate_hook_files(config.target_folder_path/"hooks") if run_system( - f"""cd {build_folder_path} & + t"""cd {build_folder_path} & {config.gcc_path} -c {" ".join(config.asm_flags)} {hooks_folder_path / "*.cpp"}"""): - raise Exception("Errors occurred during building of hooks files") + raise Exception("Errors occurred during building of hooks files") hooks: list[COFFData] = [] for path in list_files_at(build_folder_path, "**/*.o"): @@ -430,9 +439,9 @@ def generate_hook_files(folder_path: Path): ]) if run_system( - f"""cd {build_folder_path} & + t"""cd {build_folder_path} & {config.linker_path} -T patch.ld --image-base {base_pe.imgbase} -s -Map patchmap.txt"""): - raise Exception("Errors occurred during linking") + raise Exception("Errors occurred during linking") base_file_data = bytearray(base_pe.data) From 16e237cdb11b7e66df5ef518305646efb3e5ff0b Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 27 Jan 2026 18:16:20 +0300 Subject: [PATCH 07/17] make changes in test workflow --- .github/workflows/test.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d55479..ea9232c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,16 +43,16 @@ jobs: with: repository: FAForever/FA-Binary-Patches ref: 'master' - path: . + path: FA-Binary-Patches - uses: actions/checkout@v4 with: - path: fa-python-binary-patcher + path: . # install everything that we need - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.14" - name: Install MinGW i686 run: choco install mingw --x86 -y --no-progress @@ -60,17 +60,13 @@ jobs: - name: Update PATH run: echo C:/ProgramData/mingw64/mingw32/bin/ >> $env:GITHUB_PATH - - name: Install fa-python-binary-patcher - run: | - pip install -r ./fa-python-binary-patcher/requirements.txt - # download base executable - name: Download base executable run: | - curl -L "https://content.faforever.com/build/ForgedAlliance_base.exe" -o ForgedAlliance_base.exe + curl -L "https://content.faforever.com/build/ForgedAlliance_base.exe" -o FA-Binary-Patches/ForgedAlliance_base.exe # patch it, if it works then we're good - name: Patch base executable run: | - mkdir build - python ./fa-python-binary-patcher/main.py "$(pwd)" clang++ ld g++ + mkdir FA-Binary-Patches/build + python ./fa-python-binary-patcher/main.py "$(pwd)/config_example.json" From a20f44d185dddee7d56e020a3d50a10d9f66ca22 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 27 Jan 2026 18:27:14 +0300 Subject: [PATCH 08/17] fix paths --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea9232c..025c493 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,7 +47,7 @@ jobs: - uses: actions/checkout@v4 with: - path: . + path: fa-python-binary-patcher # install everything that we need - uses: actions/setup-python@v5 @@ -69,4 +69,4 @@ jobs: - name: Patch base executable run: | mkdir FA-Binary-Patches/build - python ./fa-python-binary-patcher/main.py "$(pwd)/config_example.json" + cd fa-python-binary-patcher & python main.py config_example.json From 78a57097dae323bd627626c994eea36f7a7a56d5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 27 Jan 2026 18:31:07 +0300 Subject: [PATCH 09/17] reorder repo clones --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 025c493..b306765 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,13 +41,13 @@ jobs: # clone everything that we need - uses: actions/checkout@v4 with: - repository: FAForever/FA-Binary-Patches - ref: 'master' - path: FA-Binary-Patches + path: . - uses: actions/checkout@v4 with: - path: fa-python-binary-patcher + repository: FAForever/FA-Binary-Patches + ref: 'master' + path: FA-Binary-Patches # install everything that we need - uses: actions/setup-python@v5 From f0cc0c00a715a0f1c9363c3e4b6a8b8da15e81fc Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 18:47:12 +0300 Subject: [PATCH 10/17] remove redundant code --- patcher/Patcher.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 49b86c6..937c47d 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -15,8 +15,6 @@ SECT_SIZE = 0x80000 ASM_RE = re.compile(r"(asm\(\"(0[xX][0-9a-fA-F]{1,8})\"\);)", re.IGNORECASE) -CALL_RE = re.compile( - r"((call|jmp|je|jne)\s+(0[xX][0-9a-fA-F]{1,8}))", re.IGNORECASE) SPACES_RE = re.compile(" +") @@ -57,32 +55,6 @@ def read_files_contents(dir_path: Path, paths: list[str]) -> dict[str, list[str] return files_contents -def preprocess_lines(files_contents: dict[str, list[str]]) -> tuple[list[str], dict[str, str]]: - new_lines = [] - address_names = {} - for file_name, contents in files_contents.items(): - file_lines = [] - file_addresses = {} - for line in contents: - matches = CALL_RE.finditer(line) - new_line = line - for match in matches: - full_s = match.group(0) - address = match.group(3) - address_name = "_" + address[1::] - s_start, s_end = match.span() - file_addresses[address_name] = address - new_line = new_line[:s_start] + \ - full_s.replace(address, address_name) + new_line[s_end:] - file_lines.append(new_line) - if len(file_addresses) == 0: - new_lines.append(f"#include \"{file_name}\"\n") - else: - new_lines.extend(file_lines) - address_names |= file_addresses - return new_lines, address_names - - def create_sections_file(path: Path, address_map: dict[str, str]): HEADER = """ @@ -123,12 +95,6 @@ def create_sections_file(path: Path, address_map: dict[str, str]): f.write(SECTIONS) -def create_cxx_sections_file(path, address_map): - with open(path, "w") as f: - for name, address in address_map.items(): - f.write(f"\"{name}\" = {address};\n") - - def parse_sect_map(file_path: Path) -> dict[str, str]: addresses: dict[str, str] = {} with open(file_path, "r") as f: From e919e7a2d67bfc0c64a48516963d17ff59878d1c Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 18:48:40 +0300 Subject: [PATCH 11/17] typo --- patcher/Patcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 937c47d..78da739 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -440,7 +440,7 @@ def replace_data(new_data, offset): if SECT_SIZE > 0: if SECT_SIZE < exxt_sect.f_size: raise Exception( - f"Section size too small. Required: 0x{exxt_sect.f_size: x}") + f"Section size too small. Required: 0x{exxt_sect.f_size:x}") exxt_sect.v_size = SECT_SIZE exxt_sect.f_size = SECT_SIZE From 40e53689b09b5fead3e520e13bfb069908f1c504 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 18:50:53 +0300 Subject: [PATCH 12/17] properly handle shell commands --- patcher/Patcher.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 78da739..15d450e 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -10,6 +10,8 @@ from patcher import Hook from .Config import Config from string.templatelib import Template +import shlex + SECT_SIZE = 0x80000 @@ -255,7 +257,8 @@ def scan_for_headers_in_section(sections_path: Path): def run_system(template: Template) -> int: if not isinstance(template, Template): - raise Exception("Not a template") + raise TypeError("Expected Template, got " + type(template).__name__) + command = [] for part in template: @@ -263,10 +266,7 @@ def run_system(template: Template) -> int: command.append(part) else: value = part.value - if isinstance(value, Path) and ' ' in str(value): - command.append(f"\"{value}\"") - else: - command.append(str(value)) + command.append(shlex.quote(str(value))) command = "".join(command) print(command) return os.system(command.replace("\n", " ")) From 251aab2eaa5ec61b51292497bbb861d609f7afbe Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 18:56:14 +0300 Subject: [PATCH 13/17] handle path separately --- patcher/Patcher.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 15d450e..f6b4c4e 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -266,7 +266,10 @@ def run_system(template: Template) -> int: command.append(part) else: value = part.value - command.append(shlex.quote(str(value))) + if isinstance(value, Path): + command.append(shlex.quote(str(value))) + else: + command.append(part) command = "".join(command) print(command) return os.system(command.replace("\n", " ")) From e1b26f2abd005e1b56e6d3afdd891c7d2222e7fd Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 18:56:39 +0300 Subject: [PATCH 14/17] pass value --- patcher/Patcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index f6b4c4e..16ef4bf 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -269,7 +269,7 @@ def run_system(template: Template) -> int: if isinstance(value, Path): command.append(shlex.quote(str(value))) else: - command.append(part) + command.append(value) command = "".join(command) print(command) return os.system(command.replace("\n", " ")) From fcc747b8e422278860cc021281194bd3d642aef7 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 19:08:07 +0300 Subject: [PATCH 15/17] f*ck shlex --- patcher/Patcher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 16ef4bf..9c71361 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -267,7 +267,8 @@ def run_system(template: Template) -> int: else: value = part.value if isinstance(value, Path): - command.append(shlex.quote(str(value))) + s_value = str(value) + command.append(f"\"{value}\"" if ' ' in value else value) else: command.append(value) command = "".join(command) From 72d226a665ae879e5bb78d3cb377c89cdeee2ebe Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 19:08:22 +0300 Subject: [PATCH 16/17] use s_value --- patcher/Patcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 9c71361..3b777cc 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -268,7 +268,7 @@ def run_system(template: Template) -> int: value = part.value if isinstance(value, Path): s_value = str(value) - command.append(f"\"{value}\"" if ' ' in value else value) + command.append(f"\"{s_value}\"" if ' ' in s_value else s_value) else: command.append(value) command = "".join(command) From 05f4dc40801ff8e739b6363118ca66869648b34a Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 28 Jan 2026 19:11:07 +0300 Subject: [PATCH 17/17] pain --- patcher/Patcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patcher/Patcher.py b/patcher/Patcher.py index 3b777cc..c749cab 100644 --- a/patcher/Patcher.py +++ b/patcher/Patcher.py @@ -266,11 +266,11 @@ def run_system(template: Template) -> int: command.append(part) else: value = part.value + s_value = str(value) if isinstance(value, Path): - s_value = str(value) command.append(f"\"{s_value}\"" if ' ' in s_value else s_value) else: - command.append(value) + command.append(s_value) command = "".join(command) print(command) return os.system(command.replace("\n", " "))