#!/usr/bin/env python3
"""Locate FMinimalViewInfo::CalculateProjectionMatrixGivenView in GB4-Win64-Shipping.exe.

Fingerprint: the function computes  max(0.001f, FOV) * (PI/360)  — so it
references both float constants 0.001f and pi/360 within a short span.
We find all RIP-relative references to those constants and cluster them.
"""
import pefile, struct, math, sys

EXE = sys.argv[1] if len(sys.argv) > 1 else \
    "/media/ckoch/Data/steam/steamapps/common/GBBBB/GB4/Binaries/Win64/GB4-Win64-Shipping.exe"

pe = pefile.PE(EXE, fast_load=True)
base = pe.OPTIONAL_HEADER.ImageBase

sections = {}
for s in pe.sections:
    name = s.Name.rstrip(b"\0").decode()
    sections[name] = (s.VirtualAddress, s.SizeOfRawData, s.get_data())
    print(f"section {name:8s} va=0x{base+s.VirtualAddress:x} rawsize=0x{s.SizeOfRawData:x}")

text_rva, _, text = sections[".text"]

# --- find constant locations anywhere outside .text (rdata usually) ---
consts = {
    "0.001f": struct.pack("<f", 0.001),
    "pi/360": struct.pack("<f", math.pi / 360),
}
const_vas = {k: [] for k in consts}
for name, (rva, size, data) in sections.items():
    if name == ".text":
        continue
    for key, pat in consts.items():
        off = -1
        while True:
            off = data.find(pat, off + 1)
            if off < 0:
                break
            va = base + rva + off
            # only aligned-ish hits to cut noise
            const_vas[key].append(va)
for k, v in const_vas.items():
    print(f"{k}: {len(v)} occurrences in data sections")

# --- find rip-relative references in .text to any of those VAs ---
targets = {}
for k, vas in const_vas.items():
    for va in vas:
        targets.setdefault(va, k)

refs = []  # (text_off, va, key)
tbase = base + text_rva
n = len(text)
for i in range(n - 4):
    disp = struct.unpack_from("<i", text, i)[0]
    va = tbase + i + 4 + disp
    k = targets.get(va)
    if k:
        refs.append((i, va, k))
print(f"total rip refs to candidate consts: {len(refs)}")

# cluster: find 0.001f refs with a pi/360 ref within 0x100 bytes
refs.sort()
by_key = {}
for off, va, k in refs:
    by_key.setdefault(k, []).append(off)

import bisect
p360 = by_key.get("pi/360", [])
hits = []
for off in by_key.get("0.001f", []):
    j = bisect.bisect_left(p360, off - 0x100)
    while j < len(p360) and p360[j] < off + 0x100:
        hits.append((off, p360[j]))
        j += 1
print(f"\nclustered candidates (0.001f + pi/360 within 0x100):")
for a, b in hits:
    print(f"  0.001f@text+0x{a:x} (va 0x{tbase+a:x})  pi/360@text+0x{b:x} (va 0x{tbase+b:x})")
