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:
parent
9d4967a461
commit
370c6e3133
4 changed files with 13982 additions and 0 deletions
13560
docs/research/acclient_decompiled.c
Normal file
13560
docs/research/acclient_decompiled.c
Normal file
File diff suppressed because it is too large
Load diff
150
tools/DecompileTargets.java
Normal file
150
tools/DecompileTargets.java
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// Ghidra headless Java script to decompile specific functions from acclient.exe
|
||||
// @category Analysis
|
||||
// @author acdream
|
||||
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.FunctionManager;
|
||||
import ghidra.program.model.listing.FunctionIterator;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public class DecompileTargets extends GhidraScript {
|
||||
|
||||
// Known function addresses from ACME ClientReference.cs
|
||||
private static final long[] KNOWN_ADDRS = {
|
||||
0x00531D10L, // CLandBlockStruct::ConstructPolygons
|
||||
0x00532170L, // CLandBlockStruct::GetCellRotation
|
||||
0x005328D0L, // CLandBlockStruct::ConstructVertices
|
||||
0x005A9980L, // LandDefs::get_vars
|
||||
};
|
||||
|
||||
private static final String[] KNOWN_NAMES = {
|
||||
"CLandBlockStruct::ConstructPolygons",
|
||||
"CLandBlockStruct::GetCellRotation",
|
||||
"CLandBlockStruct::ConstructVertices",
|
||||
"LandDefs::get_vars",
|
||||
};
|
||||
|
||||
// Name patterns to search for in the function list
|
||||
private static final String[] PATTERNS = {
|
||||
"physics", "motion", "moveto", "move_to", "jump",
|
||||
"transit", "portal", "envcell", "env_cell",
|
||||
"landblock", "terrain", "split", "step_up",
|
||||
"velocity", "autonomous", "collision", "cylinder",
|
||||
"sphere", "find_terrain", "interp",
|
||||
};
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String outPath = "C:/Users/erikn/source/repos/acdream/docs/research/acclient_decompiled.c";
|
||||
PrintWriter out = new PrintWriter(new FileWriter(outPath));
|
||||
|
||||
DecompInterface decomp = new DecompInterface();
|
||||
decomp.openProgram(currentProgram);
|
||||
|
||||
out.println("// Decompiled from acclient.exe using Ghidra 12.0.4 headless");
|
||||
out.println("// Target: Asheron's Call client physics/terrain/movement functions");
|
||||
out.println("// Binary: C:/Turbine/Asheron's Call/acclient.exe (4.7MB, 2016)");
|
||||
out.println("");
|
||||
|
||||
// 1. Decompile at known addresses
|
||||
out.println("// ============================================================");
|
||||
out.println("// KNOWN ADDRESSES (from ACME ClientReference.cs)");
|
||||
out.println("// ============================================================");
|
||||
out.println("");
|
||||
|
||||
for (int i = 0; i < KNOWN_ADDRS.length; i++) {
|
||||
Address addr = toAddr(KNOWN_ADDRS[i]);
|
||||
Function func = getFunctionAt(addr);
|
||||
if (func == null) {
|
||||
createFunction(addr, KNOWN_NAMES[i]);
|
||||
func = getFunctionAt(addr);
|
||||
}
|
||||
out.printf("// --- %s at 0x%08X ---\n", KNOWN_NAMES[i], KNOWN_ADDRS[i]);
|
||||
if (func != null) {
|
||||
DecompileResults results = decomp.decompileFunction(func, 60, monitor);
|
||||
if (results != null && results.depiledFunction() != null) {
|
||||
out.println(results.getDecompiledFunction().getC());
|
||||
} else {
|
||||
out.println("// DECOMPILATION FAILED");
|
||||
}
|
||||
} else {
|
||||
out.println("// FUNCTION NOT FOUND");
|
||||
}
|
||||
out.println("");
|
||||
}
|
||||
|
||||
// 2. Search for functions by name pattern
|
||||
out.println("// ============================================================");
|
||||
out.println("// PATTERN-MATCHED FUNCTIONS");
|
||||
out.println("// ============================================================");
|
||||
out.println("");
|
||||
|
||||
java.util.Set<Long> seen = new java.util.HashSet<>();
|
||||
for (long a : KNOWN_ADDRS) seen.add(a);
|
||||
|
||||
FunctionManager fm = currentProgram.getFunctionManager();
|
||||
int patternMatched = 0;
|
||||
|
||||
for (String pattern : PATTERNS) {
|
||||
FunctionIterator iter = fm.getFunctions(true);
|
||||
while (iter.hasNext()) {
|
||||
Function func = iter.next();
|
||||
String name = func.getName().toLowerCase();
|
||||
if (!name.contains(pattern)) continue;
|
||||
|
||||
long addr = func.getEntryPoint().getOffset();
|
||||
if (seen.contains(addr)) continue;
|
||||
seen.add(addr);
|
||||
|
||||
out.printf("// --- %s at 0x%08X (pattern: '%s') ---\n",
|
||||
func.getName(), addr, pattern);
|
||||
DecompileResults results = decomp.decompileFunction(func, 60, monitor);
|
||||
if (results != null && results.depiledFunction() != null) {
|
||||
out.println(results.getDecompiledFunction().getC());
|
||||
} else {
|
||||
out.println("// DECOMPILATION FAILED");
|
||||
}
|
||||
out.println("");
|
||||
patternMatched++;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Decompile ALL functions near the known CLandBlockStruct area
|
||||
// (0x00530000 - 0x00535000) to get the full terrain/physics module
|
||||
out.println("// ============================================================");
|
||||
out.println("// CLandBlockStruct REGION (0x00530000 - 0x00535000)");
|
||||
out.println("// ============================================================");
|
||||
out.println("");
|
||||
|
||||
int regionFuncs = 0;
|
||||
FunctionIterator allFuncs = fm.getFunctions(true);
|
||||
while (allFuncs.hasNext()) {
|
||||
Function func = allFuncs.next();
|
||||
long addr = func.getEntryPoint().getOffset();
|
||||
if (addr >= 0x00530000L && addr < 0x00535000L && !seen.contains(addr)) {
|
||||
seen.add(addr);
|
||||
out.printf("// --- %s at 0x%08X ---\n", func.getName(), addr);
|
||||
DecompileResults results = decomp.decompileFunction(func, 60, monitor);
|
||||
if (results != null && results.depiledFunction() != null) {
|
||||
out.println(results.getDecompiledFunction().getC());
|
||||
}
|
||||
out.println("");
|
||||
regionFuncs++;
|
||||
}
|
||||
}
|
||||
|
||||
out.close();
|
||||
decomp.dispose();
|
||||
|
||||
printf("Decompilation complete. Output: %s\n", outPath);
|
||||
printf("Known: %d, Pattern-matched: %d, Region: %d, Total: %d\n",
|
||||
KNOWN_ADDRS.length, patternMatched, regionFuncs,
|
||||
KNOWN_ADDRS.length + patternMatched + regionFuncs);
|
||||
}
|
||||
}
|
||||
145
tools/decompile_acclient.py
Normal file
145
tools/decompile_acclient.py
Normal 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()
|
||||
127
tools/ghidra_decompile.py
Normal file
127
tools/ghidra_decompile.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue