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>
114 lines
4.5 KiB
Python
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()
|