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>
This commit is contained in:
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/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})")
|
||||
Reference in New Issue
Block a user