acdream/tools/decompile_full.py
Erik 4d36756b91 research: full acclient.exe decompilation — 22,225 functions, 688K lines
Complete decompilation of the retail Asheron's Call client using
Ghidra 12.0.4 + pyghidra headless. 22,225 of 22,226 functions
successfully decompiled in 75 seconds.

Output: docs/research/decompiled/ (54 files, 688,567 lines of C)

Key findings already identified:
- CLandBlockStruct::ConstructPolygons at chunk_00530000.c:2270
  (split direction formula with 0x0CCAC033 constants)
- Motion command handlers at chunk_00510000.c (0x45000005 etc)
- Motion interpreter at chunk_00520000.c
- Portal space UI at chunk_004D0000.c and chunk_00560000.c

Next: identify CPhysicsObj, CMotionInterp, collision, and movement
functions by cross-referencing against ACE's C# port.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:25:51 +02:00

114 lines
4.5 KiB
Python

"""
Decompile the ENTIRE acclient.exe binary using pyghidra.
Outputs all functions organized by address range.
"""
import pyghidra
import time
GHIDRA_PATH = "C:/tools/ghidra_12.0.4_PUBLIC"
BINARY_PATH = "C:/Turbine/Asheron's Call/acclient.exe"
OUTPUT_DIR = "C:/Users/erikn/source/repos/acdream/docs/research/decompiled"
PROJECT_DIR = "C:/Users/erikn/source/repos/acdream/tools/ghidra_project"
def main():
import os
os.makedirs(OUTPUT_DIR, exist_ok=True)
pyghidra.start(install_dir=GHIDRA_PATH, verbose=False)
from ghidra.app.decompiler import DecompInterface
from ghidra.util.task import ConsoleTaskMonitor
with pyghidra.open_program(BINARY_PATH, project_location=PROJECT_DIR,
project_name="acclient_full2") as flat_api:
program = flat_api.getCurrentProgram()
print(f"Opened: {program.getName()}")
decomp = DecompInterface()
decomp.openProgram(program)
monitor = ConsoleTaskMonitor()
fm = program.getFunctionManager()
# Count total functions
total = 0
func_iter = fm.getFunctions(True)
while func_iter.hasNext():
func_iter.next()
total += 1
print(f"Total functions found: {total}")
# Decompile ALL functions, split into files by address range (64KB chunks)
CHUNK_SIZE = 0x10000 # 64KB chunks
current_chunk = -1
current_file = None
decompiled = 0
failed = 0
start_time = time.time()
func_iter = fm.getFunctions(True)
while func_iter.hasNext():
func = func_iter.next()
faddr = func.getEntryPoint().getOffset()
chunk = faddr // CHUNK_SIZE
# New chunk = new file
if chunk != current_chunk:
if current_file:
current_file.close()
current_chunk = chunk
chunk_start = chunk * CHUNK_SIZE
filename = f"chunk_{chunk_start:08X}.c"
filepath = os.path.join(OUTPUT_DIR, filename)
current_file = open(filepath, "w")
current_file.write(f"// Decompiled from acclient.exe — chunk 0x{chunk_start:08X}\n")
current_file.write(f"// Ghidra 12.0.4 + pyghidra headless\n\n")
try:
results = decomp.decompileFunction(func, 30, monitor)
if results and results.decompiledFunction:
code = results.decompiledFunction.getC()
current_file.write(f"// --- {func.getName()} at 0x{faddr:08X} (size: {func.getBody().getNumAddresses()}) ---\n")
current_file.write(code)
current_file.write("\n\n")
decompiled += 1
else:
current_file.write(f"// --- {func.getName()} at 0x{faddr:08X} --- DECOMPILATION FAILED\n\n")
failed += 1
except Exception as e:
current_file.write(f"// --- {func.getName()} at 0x{faddr:08X} --- ERROR: {e}\n\n")
failed += 1
if (decompiled + failed) % 500 == 0:
elapsed = time.time() - start_time
rate = (decompiled + failed) / elapsed if elapsed > 0 else 0
print(f" Progress: {decompiled + failed}/{total} ({decompiled} ok, {failed} failed) "
f"[{rate:.0f} funcs/sec, {elapsed:.0f}s elapsed]")
if current_file:
current_file.close()
decomp.dispose()
elapsed = time.time() - start_time
print(f"\nDone! Decompiled {decompiled}/{total} functions ({failed} failed)")
print(f"Time: {elapsed:.0f}s ({decompiled/elapsed:.0f} funcs/sec)")
print(f"Output: {OUTPUT_DIR}/")
# Write a summary index
index_path = os.path.join(OUTPUT_DIR, "INDEX.md")
with open(index_path, "w") as idx:
idx.write("# Decompiled acclient.exe — Full Index\n\n")
idx.write(f"- Total functions: {total}\n")
idx.write(f"- Successfully decompiled: {decompiled}\n")
idx.write(f"- Failed: {failed}\n")
idx.write(f"- Time: {elapsed:.0f}s\n\n")
idx.write("## Files by address chunk\n\n")
for f in sorted(os.listdir(OUTPUT_DIR)):
if f.endswith(".c"):
fpath = os.path.join(OUTPUT_DIR, f)
lines = sum(1 for _ in open(fpath))
idx.write(f"- `{f}` — {lines} lines\n")
if __name__ == "__main__":
main()