acdream/tools/ghidra_decompile.py
Erik 370c6e3133 research: decompile acclient.exe terrain/physics via Ghidra headless
Used Ghidra 12.0.4 + pyghidra to decompile 368 functions from the
retail AC client binary (acclient.exe, 4.7MB, 2016).

Output: docs/research/acclient_decompiled.c (13,560 lines)

Confirmed the decompiled code matches ACME's ClientReference.cs:
- ConstructPolygons split formula at ~0x00532610 with constants
  0x0CCAC033, 0x6C1AC587, -0x421BE3BD, -0x519B8F25
- Same 2.3283064e-10 float comparison for split direction

Regions decompiled:
- 0x530000-0x536000: CLandBlockStruct + terrain (85 functions)
- 0x536000-0x540000: nearby functions (168 functions)
- 0x5A9000-0x5AB000: LandDefs region (111 functions)

Tools: tools/decompile_acclient.py (pyghidra headless script)

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

127 lines
4.6 KiB
Python

# Ghidra headless script to decompile specific functions from acclient.exe
# Run with: analyzeHeadless <project_dir> <project_name> -import <exe> -postScript ghidra_decompile.py
#
# Targets the key physics/movement/rendering functions identified from
# ACME's ClientReference.cs annotations.
from ghidra.app.decompiler import DecompInterface
from ghidra.util.task import ConsoleTaskMonitor
import java.io
# Known function addresses from ACME ClientReference.cs and ACViewer
# These are virtual addresses in the loaded PE image.
TARGET_FUNCTIONS = {
# Terrain (from ClientReference.cs)
0x00531D10: "CLandBlockStruct::ConstructPolygons",
0x00532170: "CLandBlockStruct::GetCellRotation",
0x005328D0: "CLandBlockStruct::ConstructVertices",
0x005A9980: "LandDefs::get_vars",
# Physics / movement (high priority targets)
# These are educated guesses based on ACE's C# port class names.
# Ghidra will find the actual functions at these addresses.
}
# Additional search patterns - we'll grep the decompiled output for these
SEARCH_PATTERNS = [
"CPhysicsObj",
"CMotionInterp",
"MoveTo",
"MoveToObject",
"MoveToPosition",
"find_transit_cells",
"CLandBlock",
"CEnvCell",
"step_up",
"jump",
"get_jump",
"set_velocity",
"autonomous",
"terrain_poly",
"find_terrain",
"split",
"NESW",
]
def decompile_at_address(decompiler, program, address, name):
"""Decompile the function at the given address."""
func = getFunctionAt(toAddr(address))
if func is None:
# Try to create a function at this address
createFunction(toAddr(address), name)
func = getFunctionAt(toAddr(address))
if func is None:
print("WARNING: No function found at 0x{:08X} ({})".format(address, name))
return None
results = decompiler.decompileFunction(func, 60, ConsoleTaskMonitor())
if results is None or not results.depiledFunction():
print("WARNING: Decompilation failed at 0x{:08X} ({})".format(address, name))
return None
return results.getDecompiledFunction().getC()
def find_functions_by_name_pattern(program, pattern):
"""Find functions whose name or decompiled body contains the pattern."""
results = []
fm = program.getFunctionManager()
for func in fm.getFunctions(True):
if pattern.lower() in func.getName().lower():
results.append(func)
return results
def main():
program = getCurrentProgram()
decompiler = DecompInterface()
decompiler.openProgram(program)
output_path = "C:/Users/erikn/source/repos/acdream/docs/research/acclient_decompiled.c"
f = java.io.PrintWriter(java.io.FileWriter(output_path))
f.println("// Decompiled from acclient.exe using Ghidra 12.0.4")
f.println("// Target: Asheron's Call client physics/terrain/movement functions")
f.println("// Source binary: C:/Turbine/Asheron's Call/acclient.exe (4.7MB, 2016)")
f.println("")
# 1. Decompile known addresses
f.println("// ============================================================")
f.println("// KNOWN ADDRESSES (from ACME ClientReference.cs)")
f.println("// ============================================================")
f.println("")
for addr, name in sorted(TARGET_FUNCTIONS.items()):
f.println("// --- {} at 0x{:08X} ---".format(name, addr))
code = decompile_at_address(decompiler, program, addr, name)
if code:
f.println(code)
f.println("")
# 2. Search for physics/movement functions by name pattern
f.println("// ============================================================")
f.println("// PATTERN-MATCHED FUNCTIONS")
f.println("// ============================================================")
f.println("")
seen_addrs = set(TARGET_FUNCTIONS.keys())
for pattern in SEARCH_PATTERNS:
funcs = find_functions_by_name_pattern(program, pattern)
for func in funcs:
addr = func.getEntryPoint().getOffset()
if addr in seen_addrs:
continue
seen_addrs.add(addr)
f.println("// --- {} at 0x{:08X} (matched pattern '{}') ---".format(
func.getName(), addr, pattern))
results = decompiler.decompileFunction(func, 60, ConsoleTaskMonitor())
if results and results.depiledFunction():
f.println(results.getDecompiledFunction().getC())
f.println("")
f.close()
decompiler.dispose()
print("Decompilation complete. Output: " + output_path)
print("Decompiled {} known + {} pattern-matched functions".format(
len(TARGET_FUNCTIONS), len(seen_addrs) - len(TARGET_FUNCTIONS)))
main()