[Requesting Fix for CT] [Steam] Hearts of Iron IV

Requesting Fix for an edited Hearts of Iron IV CT by "otakusquared"

A forum for requesting cheat tables, trainers, or tools


Moderator: Table Moderator

Post Reply
eddycatboy
Curious
Curious
Posts: 4
Joined: Fri Jun 06, 2025 11:49 pm
Answers: 0

[Requesting Fix for CT] [Steam] Hearts of Iron IV

Post by eddycatboy »

Game Name: Hearts of Iron IV
Game Engine: Clausewitz Engine
Game Version: 1.16.9
Game Vender: Steam
Options Required: Fix Cheat Table:
For God Mode, Mobilization in a day and Weak Foe to work on Volunteers

It's been 7-8 months since Recifense updated the CT for Hearts of Iron IV now; But recently a user named "otakusquared" made an edit from one of the the 1.13.x versions and updated it to version 1.16.9, it works, but for "God Mode", "Weak Foe" and "Movement to adjacent region in 1 Hour", it doesn't work on volunteers.
I'm making a request for a simple hotfix since the edited ct hasn't been updated again to fix that for over a month.
The edited CT by "otakusquared" can be found in Recifense's old FearLess Hoi4 thread (The newer version is on the bottom):
viewtopic.php?f=4&t=9636&start=840

Steam Website:


User avatar
Hallowedsoul08
Novice Hacker
Novice Hacker
Posts: 46
Joined: Sat Nov 26, 2022 7:38 pm
Answers: 0
x 9

Re: [Requesting Fix for CT] [Steam] Hearts of Iron IV

Post by Hallowedsoul08 »

There is a mod that you could use alternatively that allow you to basically have similar features that recifense put up.

https://steamcommunity.com/sharedfiles/ ... t=modifier


eddycatboy
Curious
Curious
Posts: 4
Joined: Fri Jun 06, 2025 11:49 pm
Answers: 0

Re: [Requesting Fix for CT] [Steam] Hearts of Iron IV

Post by eddycatboy »

Hallowedsoul08 wrote: Sat Aug 23, 2025 6:57 pm

There is a mod that you could use alternatively that allow you to basically have similar features that recifense put up.

https://steamcommunity.com/sharedfiles/ ... t=modifier

I tested the mod, and it doesn't look like it has a god mode or fast movement like with recifense's CT.
The mod also crashed on me, with the recifense ct.
I still prefer just a simple check box in comparison.

it technically does. army defense and army speed, if you increase the modifier to like 5000%, you have fast enough speed to mimic it. Same with the defense, office, etc.

There's no army speed, just planning and hot climate accumulation speed.
Found it, but i still prefer the CT, but thanks for the mod recommendation and the civil war tip, but it just crashed again after working for a bit.

Last edited by eddycatboy on Mon Aug 25, 2025 11:07 pm, edited 7 times in total.

User avatar
Hallowedsoul08
Novice Hacker
Novice Hacker
Posts: 46
Joined: Sat Nov 26, 2022 7:38 pm
Answers: 0
x 9

Re: [Requesting Fix for CT] [Steam] Hearts of Iron IV

Post by Hallowedsoul08 »

eddycatboy wrote: Mon Aug 25, 2025 6:50 am
Hallowedsoul08 wrote: Sat Aug 23, 2025 6:57 pm

There is a mod that you could use alternatively that allow you to basically have similar features that recifense put up.

https://steamcommunity.com/sharedfiles/ ... t=modifier

I tested the mod, and it doesn't look like it has a god mode or fast movement like with recifense's CT.

it technically does. army defense and army speed, if you increase the modifier to like 5000%, you have fast enough speed to mimic it. Same with the defense, office, etc.

The only time you ever lose troops somehow, is a civil war technically. Although those that switch to the opposite side of you, no longer have the modifers so you can easily run them over.


Plebs
Curious
Curious
Posts: 2
Joined: Sun Jun 01, 2025 11:23 am
Answers: 0
x 3

Re: [Requesting Fix for CT] [Steam] Hearts of Iron IV

Post by Plebs »

This probably could use some love.

Personally mainly after 1 day Focus completion, instant construction, 1 day production, intel operations and refitting in 1 day.
Using console commands for Focus skips results in skipping the trigger for event.


renorp
Curious
Curious
Posts: 1
Joined: Thu Jan 01, 2026 11:42 pm
Answers: 0
x 1

Re: [Requesting Fix for CT] [Steam] Hearts of Iron IV

Post by renorp »

So, no one can do this?


eddycatboy
Curious
Curious
Posts: 4
Joined: Fri Jun 06, 2025 11:49 pm
Answers: 0

Re: [Requesting Fix for CT] [Steam] Hearts of Iron IV

Post by eddycatboy »

I have now something that can help with finding AOB PATTERNS for updating Recifense's HOI4 .CT (Note: This was made by Machine Learning):

Code: Select all

import pymem
import pymem.process
import time
import re

# --- Original patterns from Recifense's v1.16.9 table ---
PATTERNS = {
    "MOHP": "41 8B 96 10 02 00 00 48 8D 4D F7 E8 ?? ?? ?? ?? 48 8B D0 48 8B CB ??",
    "MOCP": "8B 57 38 03 55 38 4C 89 74 24 38 41 BE FF FF FF FF 89 57 38 48 85 F6",
    "MOPP": "8B 57 38 03 54 24 40 48 8B 5F 28 89 57 38 45 85 C0 75 ?? 33 C9 8B F1",
    "MPP1": "8B 57 38 03 55 38 83 7D 30 00 89 57 38 75 ?? 33 D2 8B F2 E9 ?? ?? ??",
    "MPP2": "89 73 38 3B F7 7C ?? 8B C6 48 8D 8B F0 00 00 00 2B C7 89 43 38 E8 ??",
    "MORP": "89 86 9C 01 00 00 E8 ?? ?? ?? ?? 8B 8E 9C 01 00 00 3B D9 7C ?? 3B 4C 24 30",
    "MOFP": "8B 4F 40 48 8B 47 18 3B 88 20 05 00 00 0F 8C ?? ?? ?? ?? 80 3D ?? ??",
    "MOAM": "39 9E E8 02 00 00 0F 8C ?? ?? ?? ?? 39 BE 28 01 00 00 7E ?? 89 9E E8",
    "MAM1": "8B 8E E8 02 00 00 85 C9 79 ?? 44 89 B6 E8 02 00 00 41 8B CE 44 39 B6",
    "GDMD": "48 8B 81 50 04 00 00 45 0F B6 F1 49 63 F8 48 8B F1 48 63 DA 48 85 C0",
    "GMDS": "89 86 80 04 00 00 8B 86 84 04 00 00 85 C0 0F 4F C8 89 8E 84 04 00 00",
    "GDS2": "89 8D 84 04 00 00 44 38 3D ?? ?? ?? ?? 74 53 44 38 3D ?? ?? ?? ?? 75 ?? 45 85 F6",
    "MOSR": "8B 48 40 89 4D 58 4C 8D 4D 58 4C 8D 05 ?? ?? ?? ?? 48 8D 15 ?? ?? ?? ?? 48 8D 4C 24 30",
    "MOMM": "48 8B 8E E0 04 00 00 48 8B 0C D9 E8 ?? ?? ?? ?? 48 8B C8 E8 ?? ?? ?? ?? 03 F8",
    "MOAC": "8B 15 ?? ?? ?? ?? 48 8D 8C 24 80 00 00 00 E8 ?? ?? ?? ?? 8B 84 24 80 00 00 00 39 83 D4 00 00 00",
    "MOAU": "03 D0 48 8D 8C 24 80 00 00 00 E8 ?? ?? ?? ?? 8B 84 24 80 00 00 00 39 83 D4 00 00 00",
    "MOOR": "89 8B 00 01 00 00 3B C8 7C 0C 89 BB 00 01 00 00 FF 83 FC 00 00 00 8B 83 FC 00",
    "MODP": "03 C7 89 46 1C 33 FF 40 38 3D ?? ?? ?? ?? 74 ?? 40 38 3D ?? ?? ?? ?? 75 ?? 85 DB",
    "MONP": "48 8B 45 10 41 89 4C 06 20 48 FF C3 49 83 C6 38 48 3B DF 0F 85 ??",
    "MOOP": "8B 43 30 3B 43 34 0F 8D ?? ?? ?? ?? 49 8B 4F 10 E8 ?? ?? ?? ?? 48 8D 55 C7",
    "MOPH": "83 BA 80 00 00 00 00 0F 84 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 89 45 AB",
    "MORI": "FF 4E 04 83 7E 04 00 0F 8F ?? ?? ?? ?? C7 46 04 FF FF FF FF C6 46 08 00",
    "MUDP": "89 BB C0 00 00 00 3B D5 7E ?? 89 6B 68 48 8B 5C 24 58 48 83 C4 20 5F 5E 5D C3",
    "MORW": "4C 8B 46 28 4C 8B CF 8B 56 20 C6 44 24 28 00 48 8B 88 60 03 00 00 4D 8B 80 90"
}

# --- Helper functions ---
def pattern_to_bytes_mask(pattern_str):
    parts = pattern_str.replace(" ", "")
    if len(parts) % 2 != 0:
        raise ValueError("Pattern length must be even.")
    chunks = [parts[i:i+2] for i in range(0, len(parts), 2)]
    pattern = bytearray()
    mask = bytearray()
    for ch in chunks:
        if ch == "??":
            pattern.append(0x00)
            mask.append(0x00)
        else:
            pattern.append(int(ch, 16))
            mask.append(0xFF)
    return bytes(pattern), bytes(mask)

def aob_scan_in_bytes(data, pattern_str):
    pattern_bytes, mask_bytes = pattern_to_bytes_mask(pattern_str)
    pattern_len = len(pattern_bytes)
    if pattern_len == 0:
        return None
    for i in range(len(data) - pattern_len + 1):
        match = True
        for j in range(pattern_len):
            if mask_bytes[j] != 0:
                if data[i+j] != pattern_bytes[j]:
                    match = False
                    break
        if match:
            return i
    return None

def fuzzy_aob_scan(data, pattern_str, max_mismatches=2):
    pattern_bytes, mask_bytes = pattern_to_bytes_mask(pattern_str)
    pattern_len = len(pattern_bytes)
    if pattern_len == 0:
        return None
    for i in range(len(data) - pattern_len + 1):
        mismatches = 0
        for j in range(pattern_len):
            if mask_bytes[j] != 0:
                if data[i+j] != pattern_bytes[j]:
                    mismatches += 1
                    if mismatches > max_mismatches:
                        break
        if mismatches <= max_mismatches:
            return i
    return None

def get_hex_bytes(data, offset, length):
    return " ".join(f"{b:02X}" for b in data[offset:offset+length])

def generate_relaxed_patterns(pattern_str, steps=[4, 8, 12, 16, 20, 24]):
    chunks = pattern_str.replace(" ", "")
    if len(chunks) % 2 != 0:
        raise ValueError("Invalid pattern length")
    hex_pairs = [chunks[i:i+2] for i in range(0, len(chunks), 2)]
    results = []
    for step in steps:
        if step >= len(hex_pairs):
            continue
        new_pairs = hex_pairs[:]
        for i in range(step):
            new_pairs[i] = "??"
        results.append(" ".join(new_pairs))
    return results

def main():
    try:
        pm = pymem.Pymem("hoi4.exe")
    except pymem.exception.ProcessNotFound:
        print("[!] hoi4.exe not found. Make sure the game is running.")
        return

print(f"[+] Attached to process: {pm.process_id}")
module = pymem.process.module_from_name(pm.process_handle, "hoi4.exe")
if not module:
    print("[!] Could not find module.")
    return
print(f"[+] Module base: 0x{module.lpBaseOfDll:X}, size: 0x{module.SizeOfImage:X}")

try:
    module_data = pm.read_bytes(module.lpBaseOfDll, module.SizeOfImage)
except Exception as e:
    print(f"[!] Failed to read module: {e}")
    return

found_patterns = {}
failed = []

total = len(PATTERNS)
for idx, (name, orig_pattern) in enumerate(PATTERNS.items(), 1):
    print(f"[{idx}/{total}] Searching {name}...", end="")
    # Try exact
    addr_offset = aob_scan_in_bytes(module_data, orig_pattern)
    if addr_offset is not None:
        addr = module.lpBaseOfDll + addr_offset
        found_patterns[name] = (addr, orig_pattern)
        print(f" Found at 0x{addr:X}")
        continue

    # Try fuzzy with 2 mismatches (but for MOAC we'll use 4)
    max_mis = 4 if name == "MOAC" else 2
    fuzzy_offset = fuzzy_aob_scan(module_data, orig_pattern, max_mismatches=max_mis)
    if fuzzy_offset is not None:
        addr = module.lpBaseOfDll + fuzzy_offset
        pattern_len = len(orig_pattern.replace(" ", "")) // 2
        actual_bytes = get_hex_bytes(module_data, fuzzy_offset, pattern_len)
        found_patterns[name] = (addr, actual_bytes)
        print(f" Found (fuzzy) at 0x{addr:X} with {max_mis} mismatches. New pattern: {actual_bytes}")
        continue

    # For MOAC specifically, try a shorter suffix
    if name == "MOAC":
        # Try searching for the last unique part: "39 83 D4 00 00 00"
        suffix = "39 83 D4 00 00 00"
        offset = aob_scan_in_bytes(module_data, suffix)
        if offset is not None:
            # Extract a pattern of length 30 bytes (same as original) before and after
            start = offset - 20  # take some bytes before to capture the full pattern
            if start < 0:
                start = 0
            length = 30  # enough to cover the whole pattern
            actual = get_hex_bytes(module_data, start, length)
            addr = module.lpBaseOfDll + start
            found_patterns[name] = (addr, actual)
            print(f" Found (suffix) at 0x{addr:X} using suffix. New pattern: {actual}")
            continue
        # Also try searching for "8D 8C 24 80 00 00 00"
        suffix2 = "8D 8C 24 80 00 00 00"
        offset2 = aob_scan_in_bytes(module_data, suffix2)
        if offset2 is not None:
            start = offset2 - 10
            if start < 0:
                start = 0
            length = 30
            actual = get_hex_bytes(module_data, start, length)
            addr = module.lpBaseOfDll + start
            found_patterns[name] = (addr, actual)
            print(f" Found (suffix2) at 0x{addr:X}. New pattern: {actual}")
            continue

    # Try relaxed versions
    relaxed = generate_relaxed_patterns(orig_pattern, steps=[4, 8, 12, 16, 20, 24])
    new_pattern = None
    for relaxed_pat in relaxed:
        offset = aob_scan_in_bytes(module_data, relaxed_pat)
        if offset is not None:
            new_pattern = relaxed_pat
            addr = module.lpBaseOfDll + offset
            found_patterns[name] = (addr, relaxed_pat)
            print(f" Found (relaxed) at 0x{addr:X} using: {relaxed_pat}")
            break
    if new_pattern is None:
        failed.append(name)
        print(" Not found!")

    time.sleep(0.1)

if found_patterns:
    print("\n--- Updated Patterns (use these in your Lua script) ---")
    for name, (addr, pat) in found_patterns.items():
        print(f'    {name} = "{pat}"')
    with open("updated_patterns.txt", "w") as f:
        for name, (addr, pat) in found_patterns.items():
            f.write(f'{name} = "{pat}"  ; 0x{addr:X}\n')
    print("\n[+] Saved to 'updated_patterns.txt'")

if failed:
    print("\n--- Patterns that could NOT be found automatically ---")
    for name in failed:
        print(f"  {name}")
    print("\n📌 Manual steps to find one of these patterns:")
    print("1. Open Cheat Engine and attach to hoi4.exe.")
    print("2. Use the 'Memory View' and search for the function that handles that cheat.")
    print("3. Look for a unique byte sequence (around 16-20 bytes) that is unlikely to change.")
    print("4. Replace the old pattern in the script with your new one.\n")

if __name__ == "__main__":
    main()
  • Save as .py file for python

  • Install pymem with pip

  • Open HOI4 and wait for the game to load

  • Run the .py file with py (name).py

  • Afterwords their should be a updated_patterns.txt file

Alternative:

Code: Select all

import pymem
import pymem.process
import time
import re
import os
import xml.etree.ElementTree as ET

# --- Original patterns from Recifense's v1.16.9 table ---
PATTERNS = {
    "MOHP": "41 8B 96 10 02 00 00 48 8D 4D F7 E8 ?? ?? ?? ?? 48 8B D0 48 8B CB ??",
    "MOCP": "8B 57 38 03 55 38 4C 89 74 24 38 41 BE FF FF FF FF 89 57 38 48 85 F6",
    "MOPP": "8B 57 38 03 54 24 40 48 8B 5F 28 89 57 38 45 85 C0 75 ?? 33 C9 8B F1",
    "MPP1": "8B 57 38 03 55 38 83 7D 30 00 89 57 38 75 ?? 33 D2 8B F2 E9 ?? ?? ??",
    "MPP2": "89 73 38 3B F7 7C ?? 8B C6 48 8D 8B F0 00 00 00 2B C7 89 43 38 E8 ??",
    "MORP": "89 86 9C 01 00 00 E8 ?? ?? ?? ?? 8B 8E 9C 01 00 00 3B D9 7C ?? 3B 4C 24 30",
    "MOFP": "8B 4F 40 48 8B 47 18 3B 88 20 05 00 00 0F 8C ?? ?? ?? ?? 80 3D ?? ??",
    "MOAM": "39 9E E8 02 00 00 0F 8C ?? ?? ?? ?? 39 BE 28 01 00 00 7E ?? 89 9E E8",
    "MAM1": "8B 8E E8 02 00 00 85 C9 79 ?? 44 89 B6 E8 02 00 00 41 8B CE 44 39 B6",
    "GDMD": "48 8B 81 50 04 00 00 45 0F B6 F1 49 63 F8 48 8B F1 48 63 DA 48 85 C0",
    "GMDS": "89 86 80 04 00 00 8B 86 84 04 00 00 85 C0 0F 4F C8 89 8E 84 04 00 00",
    "GDS2": "89 8D 84 04 00 00 44 38 3D ?? ?? ?? ?? 74 53 44 38 3D ?? ?? ?? ?? 75 ?? 45 85 F6",
    "MOSR": "8B 48 40 89 4D 58 4C 8D 4D 58 4C 8D 05 ?? ?? ?? ?? 48 8D 15 ?? ?? ?? ?? 48 8D 4C 24 30",
    "MOMM": "48 8B 8E E0 04 00 00 48 8B 0C D9 E8 ?? ?? ?? ?? 48 8B C8 E8 ?? ?? ?? ?? 03 F8",
    "MOAC": "8B 15 ?? ?? ?? ?? 48 8D 8C 24 80 00 00 00 E8 ?? ?? ?? ?? 8B 84 24 80 00 00 00 39 83 D4 00 00 00",
    "MOAU": "03 D0 48 8D 8C 24 80 00 00 00 E8 ?? ?? ?? ?? 8B 84 24 80 00 00 00 39 83 D4 00 00 00",
    "MOOR": "89 8B 00 01 00 00 3B C8 7C 0C 89 BB 00 01 00 00 FF 83 FC 00 00 00 8B 83 FC 00",
    "MODP": "03 C7 89 46 1C 33 FF 40 38 3D ?? ?? ?? ?? 74 ?? 40 38 3D ?? ?? ?? ?? 75 ?? 85 DB",
    "MONP": "48 8B 45 10 41 89 4C 06 20 48 FF C3 49 83 C6 38 48 3B DF 0F 85 ??",
    "MOOP": "8B 43 30 3B 43 34 0F 8D ?? ?? ?? ?? 49 8B 4F 10 E8 ?? ?? ?? ?? 48 8D 55 C7",
    "MOPH": "83 BA 80 00 00 00 00 0F 84 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 89 45 AB",
    "MORI": "FF 4E 04 83 7E 04 00 0F 8F ?? ?? ?? ?? C7 46 04 FF FF FF FF C6 46 08 00",
    "MUDP": "89 BB C0 00 00 00 3B D5 7E ?? 89 6B 68 48 8B 5C 24 58 48 83 C4 20 5F 5E 5D C3",
    "MORW": "4C 8B 46 28 4C 8B CF 8B 56 20 C6 44 24 28 00 48 8B 88 60 03 00 00 4D 8B 80 90"
}

# --- Known template filenames ---
KNOWN_TEMPLATES = [
    "hoi4_HeartsOfIron_IV_x64_v1-15-3-0143_Steam_TheSavour_PF_CE74_S10-4-AOB_T92.CT",
    "hoi4_HeartsOfIron_IV_x64_v1-16-9-c09b_Steam_CE74_S9-7-AOB_T86.CT"
]

# --- Helper functions ---
def pattern_to_bytes_mask(pattern_str):
    parts = pattern_str.replace(" ", "")
    if len(parts) % 2 != 0:
        raise ValueError("Pattern length must be even.")
    chunks = [parts[i:i+2] for i in range(0, len(parts), 2)]
    pattern = bytearray()
    mask = bytearray()
    for ch in chunks:
        if ch == "??":
            pattern.append(0x00)
            mask.append(0x00)
        else:
            pattern.append(int(ch, 16))
            mask.append(0xFF)
    return bytes(pattern), bytes(mask)

def aob_scan_in_bytes(data, pattern_str):
    pattern_bytes, mask_bytes = pattern_to_bytes_mask(pattern_str)
    pattern_len = len(pattern_bytes)
    if pattern_len == 0:
        return None
    for i in range(len(data) - pattern_len + 1):
        match = True
        for j in range(pattern_len):
            if mask_bytes[j] != 0:
                if data[i+j] != pattern_bytes[j]:
                    match = False
                    break
        if match:
            return i
    return None

def fuzzy_aob_scan(data, pattern_str, max_mismatches=2):
    pattern_bytes, mask_bytes = pattern_to_bytes_mask(pattern_str)
    pattern_len = len(pattern_bytes)
    if pattern_len == 0:
        return None
    for i in range(len(data) - pattern_len + 1):
        mismatches = 0
        for j in range(pattern_len):
            if mask_bytes[j] != 0:
                if data[i+j] != pattern_bytes[j]:
                    mismatches += 1
                    if mismatches > max_mismatches:
                        break
        if mismatches <= max_mismatches:
            return i
    return None

def get_hex_bytes(data, offset, length):
    return " ".join(f"{b:02X}" for b in data[offset:offset+length])

def generate_relaxed_patterns(pattern_str, steps=[4, 8, 12, 16, 20, 24]):
    chunks = pattern_str.replace(" ", "")
    if len(chunks) % 2 != 0:
        raise ValueError("Invalid pattern length")
    hex_pairs = [chunks[i:i+2] for i in range(0, len(chunks), 2)]
    results = []
    for step in steps:
        if step >= len(hex_pairs):
            continue
        new_pairs = hex_pairs[:]
        for i in range(step):
            new_pairs[i] = "??"
        results.append(" ".join(new_pairs))
    return results

def parse_patterns_file(filename="updated_patterns.txt"):
    found = {}
    try:
        with open(filename, "r") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                match = re.match(r'^(\w+)\s*=\s*"([^"]+)"\s*;\s*(0x[0-9A-Fa-f]+)', line)
                if match:
                    name = match.group(1)
                    pattern = match.group(2)
                    addr_str = match.group(3)
                    addr = int(addr_str, 16)
                    found[name] = (addr, pattern)
        return found
    except Exception as e:
        print(f"[!] Error parsing {filename}: {e}")
        return None

def generate_ct_file(found_patterns, template_path=None, output_path="hoi4_updated.CT"):
    """
    Generate CT file with static defines and AOBScanModule lines commented out.
    """
    if template_path and os.path.exists(template_path):
        try:
            tree = ET.parse(template_path)
            root = tree.getroot()
            main_entry = None
            for ce in root.findall(".//CheatEntry"):
                id_elem = ce.find("ID")
                if id_elem is not None and id_elem.text == "5":
                    main_entry = ce
                    break
            if main_entry is None:
                print("[!] Could not find main CheatEntry (ID 5) in template CT.")
                return False
            asm_script = main_entry.find("AssemblerScript")
            if asm_script is None:
                print("[!] No AssemblerScript found in main CheatEntry.")
                return False
            script_text = asm_script.text

        # Replace defines and comment out AOBScanModule lines for each found pattern
        for name, (addr, pattern) in found_patterns.items():
            # Replace define line
            def_regex = r'^(\s*)define\s*\(\s*' + re.escape(name) + r'\s*,\s*[^)]+\)\s*(//.*)?$'
            replacement = f'define({name}, {hex(addr)})'
            new_text, count = re.subn(def_regex, replacement, script_text, flags=re.MULTILINE)
            if count == 0:
                # If define not found, insert it after LUDO/ctCE74 defines
                lines = script_text.splitlines()
                insert_index = 0
                for i, line in enumerate(lines):
                    stripped = line.strip()
                    if stripped.startswith("define(LUDO") or stripped.startswith("define(ctCE74"):
                        insert_index = i + 1
                    elif stripped and not stripped.startswith("//") and not stripped.startswith("define"):
                        break
                lines.insert(insert_index, replacement)
                script_text = "\n".join(lines)
            else:
                script_text = new_text

            # Comment out the corresponding AOBScanModule line for this name
            aob_pattern = r'^(\s*)AOBScanModule\(' + re.escape(name) + r'\s*,.*$'
            script_text = re.sub(aob_pattern, r'\1// AOBScanModule removed (static address)', script_text, flags=re.MULTILINE)

        # Comment out any remaining AOBScanModule lines (as a catch-all)
        script_text = re.sub(r'^(\s*)AOBScanModule\(', r'\1// AOBScanModule(', script_text, flags=re.MULTILINE)

        asm_script.text = script_text
        tree.write(output_path, encoding="utf-8", xml_declaration=True)
        print(f"[+] Generated CT file: {output_path} from template {os.path.basename(template_path)}")
        return True
    except Exception as e:
        print(f"[!] Error processing template CT: {e}")
        return False
else:
    # Minimal CT generation
    print("[!] Template CT not found. Generating a minimal CT with only the defines.")
    define_lines = "\n".join([f'define({name}, {hex(addr)})' for name, (addr, _) in found_patterns.items()])
    script_content = f"""<?xml version="1.0" encoding="utf-8"?>
<CheatTable CheatEngineTableVersion="42">
  <CheatEntries>
    <CheatEntry>
      <ID>5</ID>
      <Description>"[X] &lt;== Hearts of Iron IV x64 (Updated)"</Description>
      <Options moHideChildren="1" moDeactivateChildrenAsWell="1"/>
      <Color>FF8080</Color>
      <VariableType>Auto Assembler Script</VariableType>
      <AssemblerScript>
{{
// Generated by Python AOB scanner
// Static addresses for this game version

[ENABLE]

{define_lines}

// The rest of the script (MyCode, hacking points, etc.) must be copied manually.
// This is a minimal CT; for full functionality, use a template CT.

[DISABLE]
}}
</AssemblerScript>
    </CheatEntry>
  </CheatEntries>
</CheatTable>
"""
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(script_content)
        print(f"[+] Generated minimal CT file: {output_path}")
        return True

def get_template_choice():
    """Return the path of the selected template, or None if none found."""
    available = [f for f in KNOWN_TEMPLATES if os.path.exists(f)]
    if not available:
        # No known templates found, ask for manual path
        user_path = input("Enter path to original CT file (or press Enter to skip): ").strip()
        if user_path and os.path.exists(user_path):
            return user_path
        return None
    if len(available) == 1:
        print(f"[+] Using template: {available[0]}")
        return available[0]
    # Multiple templates found, let user choose
    print("Multiple template CT files found. Please choose one:")
    for i, f in enumerate(available, 1):
        # Show a short identifier
        if "1.15.3" in f:
            label = "v1.15.3 (Recifense original, Script v10.4)"
        elif "1.16.9" in f:
            label = "v1.16.9 (edited by otakusquared, Script v9.7)"
        else:
            label = f
        print(f"  {i}) {label}")
    print("  m) Enter a custom path manually")
    choice = input("Enter number or 'm' (default: 1): ").strip()
    if choice.lower() == 'm':
        user_path = input("Enter full path to CT file: ").strip()
        if user_path and os.path.exists(user_path):
            return user_path
        print("[!] Invalid path. Falling back to first available.")
        return available[0]
    try:
        idx = int(choice) - 1
        if 0 <= idx < len(available):
            return available[idx]
    except:
        pass
    return available[0]  # default to first

def main():
    # --- Check if updated_patterns.txt exists ---
    patterns_file = "updated_patterns.txt"
    skip_scan = False
    if os.path.exists(patterns_file):
        print(f"[!] Found existing '{patterns_file}' with previous scan results.")
        print("What would you like to do?")
        print("  1) Skip the scan and use these addresses to generate a CT file.")
        print("  2) Run the scan again (overwrites the file later).")
        choice = input("Enter 1 or 2 (default: 2): ").strip()
        if choice == "1":
            skip_scan = True
            print("[+] You chose to skip the scan.")
        else:
            print("[+] You chose to run the scan. The file will be overwritten.")
    else:
        print("[+] No existing 'updated_patterns.txt' found. Will perform a full scan.")

if skip_scan:
    found = parse_patterns_file(patterns_file)
    if found is None:
        print("[!] Failed to parse the existing patterns file. Falling back to scan.")
        skip_scan = False
    else:
        print(f"[+] Loaded {len(found)} patterns from file.")
        print("\nDo you want to generate a Cheat Table (.CT) file with these addresses?")
        response = input("Enter 'y' or 'yes' to generate (default: no): ").strip().lower()
        if response in ('y', 'yes'):
            template = get_template_choice()
            if template:
                output = input("Enter output CT filename (default: 'hoi4_updated.CT'): ").strip()
                if not output:
                    output = "hoi4_updated.CT"
                generate_ct_file(found, template, output)
            else:
                print("[!] No template selected. Skipping CT generation.")
        else:
            print("[+] No CT generation requested. Exiting.")
        return

# --- If not skipping, perform the full scan ---
try:
    pm = pymem.Pymem("hoi4.exe")
except pymem.exception.ProcessNotFound:
    print("[!] hoi4.exe not found. Make sure the game is running.")
    return

print(f"[+] Attached to process: {pm.process_id}")
module = pymem.process.module_from_name(pm.process_handle, "hoi4.exe")
if not module:
    print("[!] Could not find module.")
    return
print(f"[+] Module base: 0x{module.lpBaseOfDll:X}, size: 0x{module.SizeOfImage:X}")

try:
    module_data = pm.read_bytes(module.lpBaseOfDll, module.SizeOfImage)
except Exception as e:
    print(f"[!] Failed to read module: {e}")
    return

found_patterns = {}
failed = []

total = len(PATTERNS)
for idx, (name, orig_pattern) in enumerate(PATTERNS.items(), 1):
    print(f"[{idx}/{total}] Searching {name}...", end="")
    # Try exact
    addr_offset = aob_scan_in_bytes(module_data, orig_pattern)
    if addr_offset is not None:
        addr = module.lpBaseOfDll + addr_offset
        found_patterns[name] = (addr, orig_pattern)
        print(f" Found at 0x{addr:X}")
        continue

    # Try fuzzy with 2 mismatches (but for MOAC we'll use 4)
    max_mis = 4 if name == "MOAC" else 2
    fuzzy_offset = fuzzy_aob_scan(module_data, orig_pattern, max_mismatches=max_mis)
    if fuzzy_offset is not None:
        addr = module.lpBaseOfDll + fuzzy_offset
        pattern_len = len(orig_pattern.replace(" ", "")) // 2
        actual_bytes = get_hex_bytes(module_data, fuzzy_offset, pattern_len)
        found_patterns[name] = (addr, actual_bytes)
        print(f" Found (fuzzy) at 0x{addr:X} with {max_mis} mismatches. New pattern: {actual_bytes}")
        continue

    # For MOAC specifically, try a shorter suffix
    if name == "MOAC":
        suffix = "39 83 D4 00 00 00"
        offset = aob_scan_in_bytes(module_data, suffix)
        if offset is not None:
            start = offset - 20
            if start < 0:
                start = 0
            length = 30
            actual = get_hex_bytes(module_data, start, length)
            addr = module.lpBaseOfDll + start
            found_patterns[name] = (addr, actual)
            print(f" Found (suffix) at 0x{addr:X} using suffix. New pattern: {actual}")
            continue
        suffix2 = "8D 8C 24 80 00 00 00"
        offset2 = aob_scan_in_bytes(module_data, suffix2)
        if offset2 is not None:
            start = offset2 - 10
            if start < 0:
                start = 0
            length = 30
            actual = get_hex_bytes(module_data, start, length)
            addr = module.lpBaseOfDll + start
            found_patterns[name] = (addr, actual)
            print(f" Found (suffix2) at 0x{addr:X}. New pattern: {actual}")
            continue

    # Try relaxed versions
    relaxed = generate_relaxed_patterns(orig_pattern, steps=[4, 8, 12, 16, 20, 24])
    new_pattern = None
    for relaxed_pat in relaxed:
        offset = aob_scan_in_bytes(module_data, relaxed_pat)
        if offset is not None:
            new_pattern = relaxed_pat
            addr = module.lpBaseOfDll + offset
            found_patterns[name] = (addr, relaxed_pat)
            print(f" Found (relaxed) at 0x{addr:X} using: {relaxed_pat}")
            break
    if new_pattern is None:
        failed.append(name)
        print(" Not found!")

    time.sleep(0.1)

if found_patterns:
    print("\n--- Updated Patterns (use these in your Lua script) ---")
    for name, (addr, pat) in found_patterns.items():
        print(f'    {name} = "{pat}"')
    with open("updated_patterns.txt", "w") as f:
        for name, (addr, pat) in found_patterns.items():
            f.write(f'{name} = "{pat}"  ; 0x{addr:X}\n')
    print("\n[+] Saved to 'updated_patterns.txt'")

    # Ask if user wants to generate a CT file
    print("\nDo you want to generate a Cheat Table (.CT) file with the updated addresses?")
    response = input("Enter 'y' or 'yes' to generate (default: no): ").strip().lower()
    if response in ('y', 'yes'):
        template = get_template_choice()
        if template:
            output = input("Enter output CT filename (default: 'hoi4_updated.CT'): ").strip()
            if not output:
                output = "hoi4_updated.CT"
            generate_ct_file(found_patterns, template, output)
        else:
            print("[!] No template selected. Skipping CT generation.")
else:
    print("\n[!] No patterns found. The game may have changed significantly.")

if failed:
    print("\n--- Patterns that could NOT be found automatically ---")
    for name in failed:
        print(f"  {name}")
    print("\n📌 Manual steps to find one of these patterns:")
    print("1. Open Cheat Engine and attach to hoi4.exe.")
    print("2. Use the 'Memory View' and search for the function that handles that cheat.")
    print("3. Look for a unique byte sequence (around 16-20 bytes) that is unlikely to change.")
    print("4. Replace the old pattern in the script with your new one.\n")

if __name__ == "__main__":
    main()
  • Save as .py file for python

  • Install pymem with pip

  • Open HOI4 and wait for the game to load

  • Run the .py file with py (name).py

  • Afterwords their should be a updated_patterns.txt file

It's the same thing as before but you can generate .CT file that enables but the cheats won't work at all and get's ignored in HOI4.

  • You will need otakusquared's edited Recifense CT table for 1.16.9 (hoi4_HeartsOfIron_IV_x64_v1-16-9-c09b_Steam_CE74_S9-7-AOB_T86.CT) or Recifense's original HOI4 CT for 1.15.3 (hoi4_HeartsOfIron_IV_x64_v1-15-3-0143_Steam_TheSavour_PF_CE74_S10-4-AOB_T92.CT) on the same folder to generate a .CT file, from what the Machine Learning is saying, it's from "offset instructions in MyCode".

  • Also if you have the updated_patterns.txt output already, then the output to skip scanning and make a .ct from the updated_patterns.txt file in the same folder.

  • You should have a file like hoi4_updated.CT be in the same folder as the .py file

Here's an example of the output:

Attachments
hoi4_updated.CT
Failed Output From the .CT Generating Version of the .py script; it enables but that's it.
(50.72 KiB) Downloaded 4 times

Post Reply