diff --git a/.gitignore b/.gitignore index 1703750..9d3fd4b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ snapshots/ # Local HTML render of the wiki (regenerable via wikigen/render_wiki.py) /wiki_site/ +# RE method-index cache (regenerable from il2cpp/dump.cs) +/reverse/_method_index.pkl + # Python __pycache__/ *.py[cod] diff --git a/bundle/component_census.py b/bundle/component_census.py new file mode 100644 index 0000000..9a75188 --- /dev/null +++ b/bundle/component_census.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Census of ECS components across every EntityBlueprint in epb_assets_all.bundle. + +Decodes all 1446 blueprints' Odin payloads and tallies which $type components +appear and how often. Use it to answer "is component X authored in data at all?" +firsthand (Odin stores type names as UTF-16, so an ascii grep gives false +negatives — this decodes properly). Optional filter substring narrows output. + + python bundle/component_census.py # combat-ish components + python bundle/component_census.py Damage # only names containing 'Damage' + python bundle/component_census.py '' # everything +""" +import os, sys, json, UnityPy +from collections import Counter +from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import odin_read + +GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest" +META = os.path.join(GAME, "Sand_Data/il2cpp_data/Metadata/global-metadata.dat") +DLL = os.path.join(GAME, "GameAssembly.dll") +BUNDLES = "/home/downloadpizza/sand_tools/bundles" +UNITY = "6000.0.40f1" +COMBAT = ("Damage", "Health", "Weapon", "Attack", "Melee", "Shoot", "Projectile", + "Penetra", "Overheat", "Hit", "AoE", "Armor", "Resist") + + +def walk(n): + if isinstance(n, dict): + t = n.get("$type") + if t: + yield t + for k, v in n.items(): + if k != "$type": + yield from walk(v) + elif isinstance(n, list): + for v in n: + yield from walk(v) + + +def main(): + filt = sys.argv[1] if len(sys.argv) > 1 else None # None -> combat preset + gen = TypeTreeGenerator(UNITY) + gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read()) + env = UnityPy.load(os.path.join(BUNDLES, "epb_assets_all.bundle"), + os.path.join(BUNDLES, "sand_monoscripts.bundle")) + comp = Counter(); ndone = 0; nodecache = {} + for path, obj in env.container.items(): + if obj.type.name != "GameObject": + continue + eb = None + for c in obj.read().m_Components: + co = c.read(); r = co.object_reader + if r.type.name == "MonoBehaviour" and co.m_Script.read().m_ClassName == "EntityBlueprint": + eb = r; break + if eb is None: + continue + sc = eb.read().m_Script.read() + full = (sc.m_Namespace + "." if sc.m_Namespace else "") + sc.m_ClassName + if full not in nodecache: + nodecache[full] = json.loads(gen.get_nodes_as_json(sc.m_AssemblyName, full)) + try: + sb = eb.read_typetree(nodecache[full]).get("serializationData", {}).get("SerializedBytes") + except Exception: + continue + if not sb: + continue + try: + p = odin_read.parse(bytes(sb)) + except Exception: + continue + seen = set() + for root in ("roots", "items"): + for t in walk(p.get(root)): + seen.add(t) + for t in seen: + comp[t] += 1 + ndone += 1 + print("blueprints parsed:", ndone, " distinct components:", len(comp)) + for t, n in comp.most_common(): + short = t.split(".")[-1] + if filt is None: + if any(w in short for w in COMBAT): + print("%4d %s" % (n, t)) + elif filt == "" or filt in t: + print("%4d %s" % (n, t)) + + +if __name__ == "__main__": + main() diff --git a/bundle/dump_blueprint.py b/bundle/dump_blueprint.py new file mode 100644 index 0000000..db39a89 --- /dev/null +++ b/bundle/dump_blueprint.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Fully decode one or more EntityBlueprints (by base name) from epb_assets_all.bundle. + +Prints every Odin component ($type) and every scalar field, so we can see firsthand +whether a weapon blueprint carries any damage magnitude. Usage: + + python bundle/dump_blueprint.py item_revolverSmall_dusters [other_base ...] +""" +import os, sys, json, UnityPy +from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import odin_read + +GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest" +META = os.path.join(GAME, "Sand_Data/il2cpp_data/Metadata/global-metadata.dat") +DLL = os.path.join(GAME, "GameAssembly.dll") +BUNDLES = "/home/downloadpizza/sand_tools/bundles" +UNITY = "6000.0.40f1" + + +def entity_blueprint(go): + for c in go.m_Components: + co = c.read(); r = co.object_reader + if r.type.name == "MonoBehaviour" and co.m_Script.read().m_ClassName == "EntityBlueprint": + return r + return None + + +def walk(node, prefix=""): + """Yield (path, type_label, scalar) for every node; flags floats.""" + if isinstance(node, dict): + t = node.get("$type") + if t: + yield (prefix, t, None) + for k, v in node.items(): + if k == "$type": + continue + yield from walk(v, prefix + "." + str(k)) + elif isinstance(node, list): + for i, v in enumerate(node): + yield from walk(v, prefix + f"[{i}]") + else: + yield (prefix, None, node) + + +def main(): + targets = sys.argv[1:] or ["item_revolverSmall_dusters"] + gen = TypeTreeGenerator(UNITY) + gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read()) + env = UnityPy.load(os.path.join(BUNDLES, "epb_assets_all.bundle"), + os.path.join(BUNDLES, "sand_monoscripts.bundle")) + + want = {t: None for t in targets} + for path, obj in env.container.items(): + base = path.split("/")[-1].replace("_epb.prefab", "") + if base in want and obj.type.name == "GameObject": + want[base] = obj + + for base, obj in want.items(): + print("\n" + "=" * 70) + print("BLUEPRINT:", base, "" if obj else " *** NOT FOUND ***") + print("=" * 70) + if not obj: + continue + eb = entity_blueprint(obj.read()) + if eb is None: + print(" no EntityBlueprint component") + continue + script = eb.read().m_Script.read() + full = (script.m_Namespace + "." if script.m_Namespace else "") + script.m_ClassName + nodes = json.loads(gen.get_nodes_as_json(script.m_AssemblyName, full)) + sb = eb.read_typetree(nodes).get("serializationData", {}).get("SerializedBytes") + if not sb: + print(" no SerializedBytes") + continue + parsed = odin_read.parse(bytes(sb)) + # list components + comps = set() + floats = [] + for root in ("roots", "items"): + for p, t, sc in walk(parsed.get(root)): + if t: + comps.add(t) + if isinstance(sc, float) and sc not in (0.0,) : + floats.append((p, sc)) + print(" components (%d):" % len(comps)) + for c in sorted(comps): + print(" -", c) + print(" non-zero float fields (%d):" % len(floats)) + for p, v in floats: + print(" %-60s = %s" % (p, v)) + + +if __name__ == "__main__": + main() diff --git a/docs/WEAPON_DAMAGE.md b/docs/WEAPON_DAMAGE.md new file mode 100644 index 0000000..5521863 --- /dev/null +++ b/docs/WEAPON_DAMAGE.md @@ -0,0 +1,106 @@ +# Weapon damage — static map of the model (for tracking across updates) + +Goal: locate weapon damage (esp. the `_dusters` revolver's **melee** value) in the static +files, documented so it can be re-checked on patches. + +**Status:** the damage *model* is fully mapped statically (below). The per-weapon *base +numbers* are **not** present in any asset, and are **not reachable by static anchor** in +`GameAssembly.dll` because every ECS component is accessed through fully-generic Entitas +dispatch (runtime integer index, no static class/index reference in calling code) and the +damage *resolution* is server-authoritative. What you can diff across updates is the model +and the formula function (RVAs below); the literal constants need a different method (see +end). This corrects an earlier draft that wrongly concluded "no value exists" — the values +**are** live at runtime; they just aren't statically anchorable constants. + +## Damage model (all static, all in `il2cpp/dump.cs` + verified by disasm) + +Per-type damage lives as a `float value` (object offset **+0x10**) on 8 components on the +**item/ammo** entity: `Damage{Physical,Cold,Heat,Rad,Fire,Poison,Siege,True}DataComponent` +(`: BaseFloatValueComponent`). `MeleeDataComponent` (TypeDefIndex 5825) is a marker only. + +Read path — `Hologryph.Sand.Shared.Damage.HealthAndDamageExtensions`: + +| Method | RVA | Role | +|---|---|---| +| `GetDamage(ItemEntity, DamageType)` | `0x4BAC520` | jump-table over the 8 types → returns that type's `DamageXxxDataComponent.value` (`[comp+0x10]`), or `0` if absent | +| `GetDamageModifier(ItemEntity, DamageType)` | `0x4BAC340` | per-type `DamageModifierXxx` value | +| `GetAoEDamage(ItemEntity, DamageType)` | (via calc) | explosive AoE (`AoEDamageDataComponent.AoEDamage` struct: radius + 8 floats) | +| `IEnumerable GetDamage(weapon, ammo, distance, isHeadShot, isAoE)` | `0x4BAC460` | the per-shot calc factory (state machine `d__12`) | + +**The damage formula** = `d__12.MoveNext` (**RVA `0x4BB3DB0`**) — decompiled here: +per damage type it takes base `GetDamage(weapon,type)` (and ammo), then multiplies by + +- **range falloff**: `RangeDamageModifierDataComponent` → `GetModifierByDistance(distance)` + (RVA `0x4A4B870`); **skipped when `isMelee`** (iterator flag at `+0x4c`), +- **headshot**: `HeadShotMultiplierDataComponent.value` (`[+0x10]`) when `isHeadShot`, +- plus `GetDamageModifier` / `GetAoEDamage`. + +So **melee damage = base per-type value × headshot mult** (no distance falloff). The base is +the item's `DamagePhysical`/`DamageSiege` value. `WeaponDamage` struct = `{float damageAmount, +DamageType damageType}`. + +## Damage delivery / application (static) + +- A shot/attack carries **`PlainDamgeDealerComponent`** (game's typo "Damge"; TypeDefIndex + 7343, `IGameContextComponent`): `float damageAmount @+0x10`, `DamageType damageType @+0x14`, + `bool isMelee @+0x15`. Created at runtime per shot (not in any asset). +- On impact → **`HitEventInfo`** (`Effects`, TypeDefIndex 7208): `float damageAmount @+0x30`, + `ammoId`, `ProjectileType`, `HitType`; carried by `HitEventComponent` and snapshot + `PlainHitEventComponentData` (`damageAmount @+0x2C`). +- Applied to **`HealthDataComponent`** (TypeDefIndex 7319): `ushort count @+0x10`, + `DamageType[] damageMask @+0x18`, `float value @+0x20`. `GetTotal()` RVA `0x4BAC900`. +- Networked as **`DamageEvent`** (TypeDefIndex 7299): `int damageAmount @+0xC`, `targetId`, + `blueprintId`, `bool isLethal`; batched in `DamageEventMessage`. Client-side resolution + callers (`OnDamageAvatar(hpBeforeDamage)`, `ConsumeDamage`, armor `GetArmorAbsorbMax`/ + `IsBlocksDamage`/`GetDamageExtra`) are **display/feedback only** → the authoritative + computation is server-side. + +## The base numbers are not in any asset (verified, multiple ways) + +| Check | Result | +|---|---| +| All 1446 EntityBlueprints, Odin-decoded (`bundle/component_census.py`) | no `DamageXxx`/`PlainDamgeDealer`/`AoEDamage` component on any item, ammo, or projectile | +| `item_revolverSmall_dusters` full decode (`bundle/dump_blueprint.py`) | 8 components, all presentation/physics; actions only `WeaponPickup`/`WeaponSwap` | +| All 35 bundles, UTF-16 + ascii grep of the component type-names | 0 occurrences anywhere | +| `CheatItemDefinitionsData` (`ItemDefinition`) | only `{Name, Type, StorageStack}` — no damage field | + +## Why the literal constants aren't statically anchorable + +All of these were checked and eliminated as ways to reach the code that *sets* a weapon's +damage value: + +- **Typed Entitas setters are dead.** `AddDamagePhysical(entity,float)` etc. — 0 callers. + This is build-wide, not damage-specific (the `HealthDataComponent` setters are also + uncalled), so "setter has no callers" proves nothing — this build mutates components + in-place / via generic add, never the generated typed setters. +- **Component class & module globals** (`DamagePhysical` class `0x187CE73B8`, module + `0x187D6B778`; `PlainDamgeDealer` class `0x187CE3638`, module `0x187D82300`) are + referenced **only by the generated extension methods** — no producer/init references them. +- **Item-id strings have 0 code references.** Verified the string-xref method is sound + (60/60 sampled `ScriptString` slots resolve to ≥1 ref), then confirmed + `"item_revolverSmall_dusters"` (slot `0x1807CC1808`) has **0** — so damage is not keyed + by item-id string in code. +- **Constant scans** (rip-relative `movss`, and `mov dword[mem],imm32` float-immediates) + don't isolate weapon damage from UI/physics/geometry noise. + +Net: an item's damage component is created from a pooled factory (set up once at context +init) and added via `entity.AddComponent(component)` where the component's index is derived +from the object itself — there is **no static reference in the producing code** that an xref +can follow. That, plus server-authoritative resolution, is why the constant can't be reached +by static anchoring (in my tooling **or** Ghidra — Ghidra reads bodies but can't defeat the +dynamic dispatch without already knowing which system to read). + +## Re-deriving / tracking on updates + +Tooling (regenerates the map from a new `dump.cs` + bundles): +- `reverse/il2cpp_re.py` — PE map, dump.cs method index, `find_xrefs` (call rel32), + `find_rip_refs` / `find_rip_refs_batch` (RIP-relative data xrefs), `disasm_method` / + `analyze` (resolves calls, reads float consts), `scan_movss_consts`. +- `bundle/component_census.py`, `bundle/dump_blueprint.py`. + +To diff the *formula* across patches: re-locate `HealthAndDamageExtensions.GetDamage` and +`d__12.MoveNext` by signature and compare. To get the actual *numbers*: they are +runtime component values shown in the in-game weapon-inspect UI (`ShowWeaponInfo` / +`ShowPhysicalDamageInfo` call `GetDamage`), so a live read is the reliable source; static +extraction would require following the dynamic Entitas dispatch by hand through the (server- +side) shot/attack init — not achievable via xref/anchor. diff --git a/reverse/il2cpp_re.py b/reverse/il2cpp_re.py new file mode 100644 index 0000000..010be19 --- /dev/null +++ b/reverse/il2cpp_re.py @@ -0,0 +1,355 @@ +"""Core IL2CPP reverse-engineering helpers for SAND's GameAssembly.dll. + +dump.cs (Il2CppDumper output) gives every method's RVA, file Offset, VA and +signature. GameAssembly.dll holds the actual x86-64 bodies. This module: + + - maps VA <-> file offset via the PE section table, + - builds a sorted method index from dump.cs (cached to a pickle), + - attributes any VA to its containing method, + - finds call-rel32 / jmp-rel32 sites that target a given VA (xrefs), + - disassembles a method body and pulls out float constants it loads. + +No method bodies live in dump.cs — only signatures. The values we want +(weapon damage floats) are operands inside the bodies, recovered here. +""" +import os, re, pickle, bisect, struct + +GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest" +DLL = os.path.join(GAME, "GameAssembly.dll") +DUMP = "/home/downloadpizza/sand_tools/il2cpp/dump.cs" +CACHE = "/home/downloadpizza/sand_tools/reverse/_method_index.pkl" + +IMAGE_BASE = 0x180000000 # from PE optional header (VA = IMAGE_BASE + RVA) + + +class PE: + """Minimal PE map: VA<->file-offset, plus raw byte access.""" + def __init__(self, path=DLL): + import pefile + self.pe = pefile.PE(path, fast_load=True) + self.base = self.pe.OPTIONAL_HEADER.ImageBase + self.data = self.pe.__data__ # bytes-like of whole file + self.sections = [] + for s in self.pe.sections: + self.sections.append(( + s.VirtualAddress, s.Misc_VirtualSize, + s.PointerToRawData, s.SizeOfRawData, + s.Name.decode(errors="replace").strip("\x00"), + )) + + def rva_to_off(self, rva): + for va, vsz, praw, rsz, name in self.sections: + if va <= rva < va + max(vsz, rsz): + if rva - va < rsz: + return praw + (rva - va) + return None # in bss / uninitialized + return None + + def off_to_rva(self, off): + for va, vsz, praw, rsz, name in self.sections: + if praw <= off < praw + rsz: + return va + (off - praw) + return None + + def va_to_off(self, va): + return self.rva_to_off(va - self.base) + + def read_off(self, off, n): + return bytes(self.data[off:off + n]) + + def read_va(self, va, n): + off = self.va_to_off(va) + if off is None: + return None + return self.read_off(off, n) + + def text_range(self): + for va, vsz, praw, rsz, name in self.sections: + if name == ".text": + return va, vsz, praw, rsz + return None + + def code_ranges(self): + """Executable code sections. IL2CPP method bodies live in the + 'il2cpp' section; runtime/engine glue in '.text'.""" + out = [] + for va, vsz, praw, rsz, name in self.sections: + if name in (".text", "il2cpp"): + out.append((va, vsz, praw, rsz, name)) + return out + + +_RVA_RE = re.compile(r"// RVA: 0x([0-9A-Fa-f]+) Offset: 0x([0-9A-Fa-f]+) VA: 0x([0-9A-Fa-f]+)") + + +def build_index(force=False): + """Parse dump.cs -> list of methods sorted by VA. + + Each entry: dict(va, rva, off, sig). sig is the C#-ish declaration line. + Cached to CACHE. + """ + if not force and os.path.exists(CACHE): + with open(CACHE, "rb") as f: + return pickle.load(f) + methods = [] + with open(DUMP, "r", encoding="utf-8", errors="replace") as f: + prev = None + for line in f: + m = _RVA_RE.search(line) + if m: + prev = (int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)) + continue + if prev is not None: + sig = line.strip() + rva, off, va = prev + if rva != 0: + methods.append({"va": va, "rva": rva, "off": off, "sig": sig}) + prev = None + methods.sort(key=lambda d: d["va"]) + with open(CACHE, "wb") as f: + pickle.dump(methods, f) + return methods + + +class Index: + def __init__(self): + self.methods = build_index() + self.vas = [m["va"] for m in self.methods] + + def method_at(self, va): + """Return the method whose body contains `va` (largest va <= target).""" + i = bisect.bisect_right(self.vas, va) - 1 + if i < 0: + return None + return self.methods[i] + + def next_va(self, va): + i = bisect.bisect_right(self.vas, va) + return self.vas[i] if i < len(self.vas) else None + + def find_sig(self, needle): + return [m for m in self.methods if needle in m["sig"]] + + +def find_xrefs(pe, target_vas, call_only=True): + """Scan all code sections for E8 (call) / E9 (jmp) rel32 sites whose + target is in `target_vas` (int or set/iterable of ints). + + Returns list of (site_va, opcode_byte, target_va). Linear byte scan with + displacement check; a few false positives are possible but validated by + the disassembler downstream. + """ + if isinstance(target_vas, int): + target_vas = {target_vas} + else: + target_vas = set(target_vas) + out = [] + base = pe.base + data = pe.data + for sva, svsz, spraw, srsz, name in pe.code_ranges(): + raw = bytes(data[spraw:spraw + srsz]) + n = len(raw) + j = 0 + while True: + j = raw.find(b"\xe8", j) if call_only else _find_either(raw, j) + if j < 0 or j + 5 > n: + break + disp = struct.unpack_from(" disp + i == (target_va - base - sva - 4) where i is the in-section offset + of the disp field. We solve for i. + """ + import numpy as np + out = [] + base = pe.base + data = pe.data + for sva, svsz, spraw, srsz, name in pe.code_ranges(): + raw = np.frombuffer(bytes(data[spraw:spraw + srsz]), dtype=np.uint8) + n = len(raw) + # little-endian int32 at every byte offset 0..n-4 + disp = (raw[0:n-3].astype(np.int64) + | (raw[1:n-2].astype(np.int64) << 8) + | (raw[2:n-1].astype(np.int64) << 16) + | (raw[3:n].astype(np.int64) << 24)) + # sign-extend 32-bit + disp = disp - ((disp & 0x80000000) << 1) + idx = np.arange(n - 3, dtype=np.int64) + const = target_va - base - sva - 4 + hits = np.nonzero(disp + idx == const)[0] + for i in hits.tolist(): + out.append(base + sva + int(i)) # va of the disp field + return out + + +def find_rip_refs_batch(pe, targets): + """One pass over code: for a set of target VAs, return {target_va: [disp_field_va,...]}. + Assumes disp32 is the final 4 bytes of the instruction (movss/mov/lea loads).""" + import numpy as np + targets = np.array(sorted(set(int(t) for t in targets)), dtype=np.int64) + out = {int(t): [] for t in targets} + base = pe.base + data = pe.data + for sva, svsz, spraw, srsz, name in pe.code_ranges(): + raw = np.frombuffer(bytes(data[spraw:spraw + srsz]), dtype=np.uint8) + n = len(raw) + disp = (raw[0:n-3].astype(np.int64) | (raw[1:n-2].astype(np.int64) << 8) + | (raw[2:n-1].astype(np.int64) << 16) | (raw[3:n].astype(np.int64) << 24)) + disp = disp - ((disp & 0x80000000) << 1) + idx = np.arange(n - 3, dtype=np.int64) + tgt = base + sva + idx + 4 + disp # absolute target of a disp-final instruction + mask = np.isin(tgt, targets) + for i in np.nonzero(mask)[0]: + t = int(tgt[i]) + if t in out: + out[t].append(base + sva + int(i)) + return out + + +def disasm_around(pe, va, back=24, total=64): + """Disassemble a window [va-back, va-back+total) to recover the instruction + that contains/precedes `va` (e.g. a RIP-ref disp field).""" + from capstone import Cs, CS_ARCH_X86, CS_MODE_64 + start = va - back + off = pe.va_to_off(start) + code = pe.read_off(off, total) + md = Cs(CS_ARCH_X86, CS_MODE_64) + md.detail = True + return list(md.disasm(code, start)) + + +def _find_either(raw, start): + a = raw.find(b"\xe8", start) + b = raw.find(b"\xe9", start) + if a < 0: + return b + if b < 0: + return a + return min(a, b) + + +def disasm_method(pe, idx, va, max_bytes=0x4000): + """Disassemble one method body (until next method VA or max_bytes).""" + from capstone import Cs, CS_ARCH_X86, CS_MODE_64 + nxt = idx.next_va(va) + size = max_bytes + if nxt: + size = min(size, nxt - va) + off = pe.va_to_off(va) + code = pe.read_off(off, size) + md = Cs(CS_ARCH_X86, CS_MODE_64) + md.detail = True + return list(md.disasm(code, va)) + + +def rip_target(insn): + """If an instruction has a RIP-relative memory operand, return its absolute VA.""" + from capstone import x86 + for op in insn.operands: + if op.type == x86.X86_OP_MEM and op.mem.base == x86.X86_REG_RIP: + return insn.address + insn.size + op.mem.disp + return None + + +def read_f32(pe, va): + b = pe.read_va(va, 4) + return None if b is None else struct.unpack("= 0: + if j + 8 <= n: + modrm = raw[j + 3] + if modrm in rm_ok: + disp = _s.unpack_from("= lo) and (hi is None or av <= hi): + out.append((insn_va, v, kind)) + j = raw.find(pref, j + 1) + return out + + +def analyze(pe, idx, va, max_bytes=0x6000, show=True): + """Disassemble a method; resolve call targets to method sigs and read any + RIP-relative float/double constants loaded (movss/movsd/comiss/addss/...). + Returns (insns, calls, floats).""" + from capstone import x86 + ins = disasm_method(pe, idx, va, max_bytes) + calls, floats, lines = [], [], [] + for i in ins: + tgt = rip_target(i) + note = "" + mn = i.mnemonic + if mn in ("call", "jmp") and i.operands and i.operands[0].type == x86.X86_OP_IMM: + d = i.operands[0].imm + m = idx.method_at(d) + nm = m["sig"] if (m and abs(m["va"] - d) < 0x6000) else "?" + if mn == "call": + calls.append((i.address, d, nm)) + note = " -> %x %s" % (d, nm[:78]) + elif tgt is not None and ("ss" in mn or "sd" in mn or mn == "movss" or mn == "movsd"): + # float/double constant load from .rdata + if "sd" in mn: + v = read_f64(pe, tgt) + floats.append((i.address, tgt, v, "f64")) + note = " ; =%s (f64 @%x)" % (v, tgt) + else: + v = read_f32(pe, tgt) + floats.append((i.address, tgt, v, "f32")) + note = " ; =%s (f32 @%x)" % (v, tgt) + elif tgt is not None: + note = " ; [data %x]" % tgt + lines.append(" %x %-9s %s%s" % (i.address, mn, i.op_str, note)) + if show: + print("\n".join(lines)) + return ins, calls, floats + + +if __name__ == "__main__": + pe = PE() + idx = Index() + print("methods indexed:", len(idx.methods)) + print("text section:", [hex(x) for x in pe.text_range()]) + # sanity: the known DamagePhysical float setter + m = idx.method_at(0x1849EC5B0) + print("method at 0x1849EC5B0:", m["sig"] if m else None, hex(m["va"]) if m else "")