mirror of
https://github.com/sal063/AC6_recomp
synced 2026-06-25 02:02:10 -04:00
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
import zlib
|
|
from functools import lru_cache
|
|
|
|
|
|
# Number of distinct pads. The keygen masks the index with 0xFF, so there are
|
|
# exactly 256 possible pads; entry indices beyond 255 reuse them mod 256.
|
|
_PAD_COUNT = 256
|
|
|
|
# We need pi fractional words up to 2*(255)+2 = 512, plus slack.
|
|
_PI_WORDS_NEEDED = 2 * (_PAD_COUNT - 1) + 3
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _pi_fractional_words(nwords: int) -> tuple[int, ...]:
|
|
"""First `nwords` base-2^32 words of frac(pi), most-significant word first.
|
|
|
|
Pure-integer Machin formula: pi = 16*arctan(1/5) - 4*arctan(1/239).
|
|
"""
|
|
bits = 32 * nwords + 64
|
|
|
|
def arctan_inv(x: int) -> int:
|
|
total = 0
|
|
term = (1 << bits) // x
|
|
x2 = x * x
|
|
k = 0
|
|
sign = 1
|
|
while term // (2 * k + 1):
|
|
total += sign * (term // (2 * k + 1))
|
|
term //= x2
|
|
k += 1
|
|
sign = -sign
|
|
return total
|
|
|
|
pi_scaled = 16 * arctan_inv(5) - 4 * arctan_inv(239)
|
|
frac = pi_scaled - (3 << bits) # drop the integer part "3"
|
|
return tuple(
|
|
(frac >> (bits - 32 * (i + 1))) & 0xFFFFFFFF for i in range(nwords)
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=_PAD_COUNT)
|
|
def pad_for_index(index: int) -> bytes:
|
|
"""Return the 8-byte descramble pad for a DATA.TBL entry index."""
|
|
words = _pi_fractional_words(_PI_WORDS_NEEDED)
|
|
w0 = 2 * (index & 0xFF) + 1
|
|
return struct.pack(">II", words[w0], words[w0 + 1])
|
|
|
|
|
|
def descramble(data: bytes, index: int) -> bytes:
|
|
"""Undo the XOR scrambling for a compressed entry at the given table index."""
|
|
pad = pad_for_index(index)
|
|
return bytes(b ^ pad[i & 7] for i, b in enumerate(data))
|
|
|
|
|
|
def decompress_entry(data: bytes, index: int, expected_size: int | None = None) -> bytes:
|
|
"""Descramble + raw-inflate a compressed PAC entry.
|
|
|
|
Args:
|
|
data: the on-disk (scrambled, compressed) entry bytes from DATA00/01.PAC.
|
|
index: the entry's DATA.TBL row index (drives the descramble pad).
|
|
expected_size: optional decompressed size from DATA.TBL for validation.
|
|
|
|
Returns the decompressed payload (typically an FHM container).
|
|
Raises zlib.error on a decode failure or ValueError on a size mismatch.
|
|
"""
|
|
raw = descramble(data, index)
|
|
out = zlib.decompress(raw, wbits=-15)
|
|
if expected_size is not None and len(out) != expected_size:
|
|
raise ValueError(
|
|
f"decompressed size mismatch: got {len(out)}, expected {expected_size}"
|
|
)
|
|
return out
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Smoke test: print the first few pads so the table can be eyeballed.
|
|
for i in range(4):
|
|
print(f"pad[{i}] = {pad_for_index(i).hex()}")
|