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>
127 lines
4.6 KiB
Python
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()
|