Last active
April 21, 2024 11:16
-
-
Save nitori/75b4e110b01f6658c3460c08f683f792 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from dataclasses import dataclass, field | |
import struct | |
from typing import BinaryIO | |
import zlib | |
import typing | |
""" | |
TFG1 Data files desired to be extracted (data directory) | |
https://gitea.xanax.lol/root/tfg-client | |
TFG4 Source Code (partially. important parts seem to be missing) | |
https://gitea.xanax.lol/root/tfg-source | |
""" | |
@dataclass | |
class Bitmap: | |
width: int | |
height: int | |
bitmap_width: int | |
bitmap_height: int | |
left_border: int | |
upper_border: int | |
transparent_color: tuple[int, int, int] | |
bytes_per_pixel: int | |
group: int | |
id: int | |
name: bytes | |
compression_method: int | |
compression_params: int | |
data_position: int | |
data_size: int | |
crc32: str | |
data: bytes = field(repr=False) | |
def read_string(f: BinaryIO): | |
length = f.read(1)[0] | |
return f.read(length) | |
class Stream: | |
def __init__(self, file_or_fp: str | typing.BinaryIO): | |
if isinstance(file_or_fp, str): | |
self._f = open(file_or_fp, 'rb') | |
self.filename = file_or_fp | |
else: | |
self._f = file_or_fp | |
self.filename = file_or_fp.name | |
self.number_of_bitmaps = None | |
self.header_size = None | |
self.max_tell = 0 | |
self.min_tell = float('inf') | |
def tell(self) -> int: | |
return self._f.tell() | |
def __enter__(self): | |
assert self._f.read(4) == b'GFXC' | |
chump = self._f.read(5) | |
self.number_of_bitmaps, a, b, c = struct.unpack('<HBBB', chump) | |
self.header_size = (c << 16) | (b << 8) | a | |
return self | |
def __exit__(self, exc_type, exc_val, exc_tb): | |
self._f.close() | |
def __iter__(self): | |
for i in range(self.number_of_bitmaps): | |
bitmap = self.unserialize() | |
yield bitmap | |
def unserialize(self) -> Bitmap: | |
f = self._f | |
# C0 00 00 01 3B 00 21 00 5E 00 7B 00 43 79 99 20 13 00 3C 0C 00 01 01 88 C6 DA 0C AF 04 00 00 9E 02 00 00 | |
# it seems to be little endian in all cases | |
# C0 00 00 01 | |
width, height = struct.unpack('<HH', f.read(4)) | |
# 3B 00 21 00 | |
bitmap_width, bitmap_height = struct.unpack('<HH', f.read(4)) | |
# 5E 00 7B 00 | |
left_border, upper_border = struct.unpack('<HH', f.read(4)) | |
# 43 79 99 | |
r, g, b = struct.unpack('BBB', f.read(3)) | |
# 20 | |
bytes_per_pixel = struct.unpack('B', f.read(1))[0] | |
assert bytes_per_pixel % 8 == 0, 'bytes_per_pixel must be a multiple of 8' | |
bytes_per_pixel = bytes_per_pixel // 8 | |
# 13 00 3C 0C | |
group, id_ = struct.unpack('<HH', f.read(4)) | |
# 00 | |
name = read_string(f) | |
# 01 01 | |
compression_method, compression_params = struct.unpack('BB', f.read(2)) | |
# 88 C6 DA 0C AF 04 00 00 9E 02 00 00 | |
crc32_checksum, data_position, data_size = struct.unpack('<III', f.read(12)) | |
pos = f.tell() | |
f.seek(data_position) | |
data = f.read(data_size) | |
self.max_tell = max(f.tell(), self.max_tell) | |
self.min_tell = min(data_position, self.min_tell) | |
f.seek(pos) | |
verify = zlib.crc32(data) | |
assert verify == crc32_checksum | |
assert len(data) == data_size | |
return Bitmap( | |
width, height, bitmap_width, bitmap_height, | |
left_border, upper_border, (r, g, b), | |
bytes_per_pixel, group, id_, | |
name, compression_method, compression_params, | |
data_position, data_size, | |
f'{crc32_checksum:08x}', | |
data | |
) | |
def main() -> None: | |
import os | |
os.makedirs('outputs', exist_ok=True) | |
for fn in os.listdir('outputs'): | |
os.unlink(os.path.join('outputs', fn)) | |
saved = set() | |
duplicates = {} | |
filename = 'tfg-client/data/data' | |
file_size = os.stat(filename).st_size | |
with Stream(filename) as stream: | |
print(f'File size: {file_size}') | |
print(f'Number of bitmaps: {stream.number_of_bitmaps}') | |
print(f'Header size: {stream.header_size}') | |
print() | |
for i, bitmap in enumerate(stream): | |
duplicates.setdefault(bitmap.crc32, 0) | |
duplicates[bitmap.crc32] += 1 | |
if bitmap.crc32 in saved: | |
continue | |
saved.add(bitmap.crc32) | |
# if len(bitmap.data) > 100: | |
# continue | |
if bitmap.crc32 == 'ceb1b544': | |
print(bitmap) | |
# key = f'{bitmap.group:06d}_{bitmap.id:06d}_{bitmap.crc32}' | |
# if bitmap.name: | |
# key = f'{key}_{bitmap.name.decode()}' | |
# if key in maps: | |
# assert maps[key] == bitmap, 'Bitmap already exists with different data!' | |
# else: | |
# maps[key] = bitmap | |
prefix = f'{bitmap.bitmap_width}x{bitmap.bitmap_height}x{bitmap.bytes_per_pixel}' | |
# with open(f'outputs/bitmap_{bitmap.name.decode()}_{bitmap.crc32}_{prefix}.raw', 'wb') as f: | |
# f.write(bitmap.data) | |
print() | |
print('Unique files:', len([v for v in duplicates.values() if v == 1])) | |
print('Files that appear twice:', len([v for v in duplicates.values() if v == 2])) | |
print('Files that appear 3 times:', len([v for v in duplicates.values() if v == 3])) | |
print('Files that appear 4 times:', len([v for v in duplicates.values() if v == 4])) | |
print('Files that appear 5 times:', len([v for v in duplicates.values() if v == 5])) | |
print('Files that appear more than 5 times:', len([v for v in duplicates.values() if v >= 6])) | |
print('Most common file:', max(duplicates.items(), key=lambda item: item[1])) | |
print('Min .tell():', stream.min_tell, '(should match Header size)') | |
print('Max .tell():', stream.max_tell) | |
with open(filename, 'rb') as f: | |
f.seek(stream.max_tell) | |
with open('outputs/rest.raw', 'wb') as fo: | |
fo.write(f.read()) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment