Bulk-decompiled ~17.2k combat/system functions (Ghidra 11.1.2 headless) and confirmed via a second toolchain what the capstone analysis found: - GetDamage is a pure component read (returns *(comp+0x10), 0 if absent), no constants. - PlainDamgeDealerComponent has CopyTo -> it's network-replicated (snapshot from server); the client receives damage, never computes it. - No client producer writes a damage value (GetDamage has no real combat caller; the rich calc factory has 0 callers). Conclusion documented: per-weapon damage numbers are server-authoritative runtime Entitas component values, assigned via fully-generic dispatch with no static class/index/string anchor -> not statically extractable from GameAssembly.dll. Trackable static artifacts are the model + formula RVAs. Adds reusable pipeline: reverse/ghidra_decomp_targets.py, reverse/find_damage_writes.py.
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Scan ghidra/decomp.c (Ghidra decompiled C) for the PlainDamgeDealerComponent
|
|
populate fingerprint: the same pointer var written at +0x10 (float, damageAmount),
|
|
+0x14 (damageType) and +0x15 (isMelee). Also reports HitEventInfo (+0x30 damageAmount)
|
|
and any function assigning a float constant to a +0x10 offset near those.
|
|
"""
|
|
import re, sys
|
|
|
|
BLOCK = re.compile(r"^// ==== (.+?) @ \+0x([0-9a-f]+) ====$")
|
|
|
|
|
|
def blocks(path):
|
|
name = None
|
|
addr = None
|
|
buf = []
|
|
for line in open(path, errors="replace"):
|
|
m = BLOCK.match(line.rstrip("\n"))
|
|
if m:
|
|
if name is not None:
|
|
yield name, addr, "".join(buf)
|
|
name, addr, buf = m.group(1), m.group(2), []
|
|
else:
|
|
buf.append(line)
|
|
if name is not None:
|
|
yield name, addr, "".join(buf)
|
|
|
|
|
|
# var written at a given byte offset: e.g. *(float *)(lVar5 + 0x10) = ...
|
|
def vars_writing_offset(code, off):
|
|
pat = re.compile(r"\*\([^)]*\)\(([A-Za-z_][A-Za-z0-9_]*) \+ 0x%x\) =" % off)
|
|
return set(pat.findall(code))
|
|
|
|
|
|
def main():
|
|
path = sys.argv[1] if len(sys.argv) > 1 else "/home/downloadpizza/sand_tools/ghidra/decomp.c"
|
|
hits = []
|
|
for name, addr, code in blocks(path):
|
|
v10 = vars_writing_offset(code, 0x10)
|
|
v14 = vars_writing_offset(code, 0x14)
|
|
v15 = vars_writing_offset(code, 0x15)
|
|
common = v10 & v14 & v15
|
|
if common:
|
|
# confirm the +0x10 write is a float
|
|
for v in common:
|
|
if re.search(r"\*\(float \*\)\(%s \+ 0x10\) =" % re.escape(v), code):
|
|
hits.append((name, addr, v))
|
|
break
|
|
print("=== STRICT: +0x10 float, +0x14, +0x15 same ptr ===")
|
|
for name, addr, v in hits:
|
|
print(" +0x%s var %s %s" % (addr, v, name))
|
|
print("strict total:", len(hits))
|
|
|
|
# LOOSE: float@+0x10 and byte@+0x15 on same ptr (isMelee + damageAmount)
|
|
loose = []
|
|
for name, addr, code in blocks(path):
|
|
common = vars_writing_offset(code, 0x10) & vars_writing_offset(code, 0x15)
|
|
for v in common:
|
|
if re.search(r"\*\(float \*\)\(%s \+ 0x10\) =" % re.escape(v), code):
|
|
loose.append((name, addr, v)); break
|
|
print("\n=== LOOSE: +0x10 float & +0x15 same ptr ===")
|
|
for name, addr, v in loose:
|
|
print(" +0x%s var %s %s" % (addr, v, name))
|
|
print("loose total:", len(loose))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|