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()
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] <== 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()
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".
Here's an example of the output: