Files
ckochandClaude Opus 4.8 dc28d60392 Gundam Breaker 4 UltraWide Fix v1.0.0
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>
2026-07-20 20:21:23 -04:00

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})")