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>
This commit is contained in:
Erik 2026-04-12 23:18:27 +02:00
parent 9d4967a461
commit 370c6e3133
4 changed files with 13982 additions and 0 deletions

145
tools/decompile_acclient.py Normal file
View file

@ -0,0 +1,145 @@
"""
Decompile targeted functions from acclient.exe using pyghidra.
Run with: python decompile_acclient.py
"""
import pyghidra
GHIDRA_PATH = "C:/tools/ghidra_12.0.4_PUBLIC"
BINARY_PATH = "C:/Turbine/Asheron's Call/acclient.exe"
OUTPUT_PATH = "C:/Users/erikn/source/repos/acdream/docs/research/acclient_decompiled.c"
PROJECT_DIR = "C:/Users/erikn/source/repos/acdream/tools/ghidra_project"
# Known function addresses from ACME ClientReference.cs
KNOWN_FUNCTIONS = {
0x00531D10: "CLandBlockStruct_ConstructPolygons",
0x00532170: "CLandBlockStruct_GetCellRotation",
0x005328D0: "CLandBlockStruct_ConstructVertices",
0x005A9980: "LandDefs_get_vars",
}
# Name patterns to search for
PATTERNS = [
"physics", "motion", "moveto", "move_to", "jump",
"transit", "portal", "envcell", "env_cell",
"landblock", "terrain", "split", "step_up",
"velocity", "autonomous", "collision", "cylinder",
"sphere", "interp", "heading", "position",
]
def main():
pyghidra.start(install_dir=GHIDRA_PATH, verbose=True)
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_pyghidra") as flat_api:
program = flat_api.getCurrentProgram()
print(f"Opened: {program.getName()}, {program.getLanguage()}")
decomp = DecompInterface()
decomp.openProgram(program)
monitor = ConsoleTaskMonitor()
with open(OUTPUT_PATH, "w") as out:
out.write("// Decompiled from acclient.exe using Ghidra 12.0.4 + pyghidra\n")
out.write("// Target: AC client physics/terrain/movement functions\n")
out.write(f"// Binary: {BINARY_PATH}\n\n")
# 1. Known addresses
out.write("// " + "=" * 60 + "\n")
out.write("// KNOWN ADDRESSES (from ACME ClientReference.cs)\n")
out.write("// " + "=" * 60 + "\n\n")
for addr_int, name in sorted(KNOWN_FUNCTIONS.items()):
addr = flat_api.toAddr(addr_int)
func = flat_api.getFunctionAt(addr)
if func is None:
# Try to disassemble at this address first
try:
flat_api.disassemble(addr)
except:
pass
try:
flat_api.createFunction(addr, name)
except:
pass
func = flat_api.getFunctionAt(addr)
if func is None:
# Maybe it's inside another function — get the containing function
func = flat_api.getFunctionContaining(addr)
if func:
out.write(f"// NOTE: 0x{addr_int:08X} is inside {func.getName()} at 0x{func.getEntryPoint().getOffset():08X}\n")
out.write(f"// --- {name} at 0x{addr_int:08X} ---\n")
if func:
results = decomp.decompileFunction(func, 60, monitor)
if results and results.decompiledFunction:
out.write(results.getDecompiledFunction().getC())
else:
out.write("// DECOMPILATION FAILED\n")
else:
out.write("// FUNCTION NOT FOUND\n")
out.write("\n")
# 2. Pattern search
out.write("// " + "=" * 60 + "\n")
out.write("// PATTERN-MATCHED FUNCTIONS\n")
out.write("// " + "=" * 60 + "\n\n")
seen = set(KNOWN_FUNCTIONS.keys())
fm = program.getFunctionManager()
pattern_count = 0
for pattern in PATTERNS:
func_iter = fm.getFunctions(True)
while func_iter.hasNext():
func = func_iter.next()
fname = func.getName().lower()
if pattern not in fname:
continue
faddr = func.getEntryPoint().getOffset()
if faddr in seen:
continue
seen.add(faddr)
out.write(f"// --- {func.getName()} at 0x{faddr:08X} (pattern: '{pattern}') ---\n")
results = decomp.decompileFunction(func, 60, monitor)
if results and results.decompiledFunction:
out.write(results.getDecompiledFunction().getC())
else:
out.write("// DECOMPILATION FAILED\n")
out.write("\n")
pattern_count += 1
# 3. Expanded region scan
REGIONS = [
(0x00530000, 0x00536000, "CLandBlockStruct + terrain"),
(0x00536000, 0x00540000, "Nearby functions"),
(0x005A9000, 0x005AB000, "LandDefs region"),
]
region_count = 0
for rstart, rend, rname in REGIONS:
out.write("// " + "=" * 60 + "\n")
out.write(f"// REGION: {rname} (0x{rstart:08X} - 0x{rend:08X})\n")
out.write("// " + "=" * 60 + "\n\n")
func_iter = fm.getFunctions(True)
while func_iter.hasNext():
func = func_iter.next()
faddr = func.getEntryPoint().getOffset()
if rstart <= faddr < rend and faddr not in seen:
seen.add(faddr)
out.write(f"// --- {func.getName()} at 0x{faddr:08X} ---\n")
results = decomp.decompileFunction(func, 60, monitor)
if results and results.decompiledFunction:
out.write(results.decompiledFunction.getC())
out.write("\n")
region_count += 1
decomp.dispose()
print(f"Done! Known: {len(KNOWN_FUNCTIONS)}, Pattern: {pattern_count}, Region: {region_count}")
print(f"Output: {OUTPUT_PATH}")
if __name__ == "__main__":
main()