32:9 / 21:9 ultrawide support for Gundam Breaker 4 (UE4.27) via SUWSF + Ultimate ASI Loader. Forces Hor+ FOV (AspectRatioAxisConstraint=MaintainYFOV) and disables pillarboxing, patched in decrypted memory at runtime so it holds through missions (the exe is SteamStub-encrypted; a static patch is impossible). Confirmed working in gameplay at 5120x1440 (Samsung Odyssey G93SC) on Proton. Requires WINEDLLOVERRIDES="dsound=n,b" on Proton so the loader injects. Includes: patch/SUWSF.ini, Proton installer with clean uninstall, release-zip builder, and runtime dump/verify tooling. Third-party bundles under MIT (see THIRD_PARTY.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.5 KiB
Python
Executable File
79 lines
2.5 KiB
Python
Executable File
#!/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})")
|