#!/usr/bin/env python3
argparse
os
importar
zlib
from pathlib import Path
Importar de coleções Contador
def parse_gfc(gfc_path: Path):
dados = gfc_path.read_bytes()
se len(dados) < 16:
raise ValueError("GFC muito pequeno")
magic, gob_size, num_clusters, num_files = struct.unpack(">IIII", data[:16])
se magic != 0x8008:
raise ValueError(f"GFC mágico inesperado: 0x{magic:08X}")
clusters_off = 0x10
checksum_off = clusters_off + num_clusters * 0x10
arquivos_desativados = checksum_desativado + num_clusters * 4
se files_off + num_files * 0x0C > len(data):
raise ValueError("Os tamanhos das tabelas GFC excedem o comprimento do arquivo")
clusters = []
para o intervalo(num_clusters):
comp_size, gob_off, nxt, comp = struct.unpack(">IIII", data[clusters_off + i*0x10 : clusters_off + (i+1)*0x10])
clusters.append({
"idx": eu,
"comp_size": comp_size,
"desligado": gob_off,
"próximo": próximo,
"comp": comp,
})
arquivos = []
para i em range(num_files):
crc, tamanho, primeiro = struct.unpack(">III", data[files_off + i*0x0C : files_off + (i+1)*0x0C])
arquivos.append({
"idx": eu,
"crc": crc,
"tamanho": tamanho,
"primeiro": primeiro,
})
adquirir {
"mágica": mágica,
"tamanho_da_gota": tamanho_da_gota,
"num_clusters": num_clusters,
"num_files": num_files,
"aglomerados": aglomerados,
"arquivos": arquivos,
}
def decompress_cluster(gob: bytes, cluster: dict) -> bytes:
raw = gob[cluster["off"] : cluster["off"] + cluster["comp_size"]]
comp_kind = (cluster["comp"] >> 24) & 0xFF
se comp_kind == 0x7A e len(raw) >= 6:
# Contêiner zlib, mas com o comportamento de checksum do jogo; descompactar carga útil DEFLATE bruto
# Layout bruto: cabeçalho zlib de 2 bytes, fluxo DEFLATE, Adler32 de 4 bytes
retornar zlib.decompress(raw[2:-4], wbits=-15)
Relatar bruto
def reconstruir_arquivo(gob: bytes, clusters: lista, entrada_arquivo: dict, cache: dict) -> bytes:
se file_entry["size"] == 0 ou file_entry["first"] == 0xFFFFFFFF:
representar b""
saída = bytearray()
idx = file_entry["first"]
visto = conjunto()
enquanto Verdadeiro:
se idx estiverem visto:
raise RuntimeError(f"Ciclo detectado na cadeia de clusters em {idx}")
se idx < 0 ou idx >= len(clusters):
raise RuntimeError(f"Índice do cluster fora do intervalo: {idx}")
visto.adicionar(idx)
Se o índice não estiver em cache:
cache[idx] = descomprimir_cluster(gob, clusters[idx])
saída += cache[idx]
nxt = clusters[idx]["next"]
se nxt == 0x7FFF:
quebrar
idx = nxt
retornar bytes(out[:file_entry["size"]])
def guess_ext(data: bytes) -> str:
se data.startswith(b"<"):
retornar ".xml"
se data.startswith(b"RIFF"):
retornar ".wav"
se data.startswith(b"OggS"):
retornar ".ogg"
se data.startswith(b"\x89PNG\r\n\x1a\n"):
retornar ".png"
se data.startswith(b"BINK") ou data.startswith(b"bink"):
retornar ".bik"
se data.startswith(b"\x10\x00"):
retornar ".lz77"
se b"<raiz" em data[:256]:
retornar ".xml"
retornar ".bin"
def main():
ap = argparse.ArgumentParser(description="Arquivos extras de arquivos VV .GFC/.GOB.")
ap.add_argument("gfc", help="Caminho para main.gfc")
ap.add_argument("gob", help="Caminho para main.gob")
ap.add_argument("-o", "--out", default="extraído", help="Diretório de saída")
ap.add_argument("--report", default="extract_report.txt", help="Caminho do arquivo de relatório")
args = ap.parse_args()
gfc_path = Path(args.gfc)
gob_path = Path(args.gob)
diretório_saída = Caminho(args.saída)
out_dir.mkdir(parents=True, exist_ok=True)
meta = parse_gfc(caminho_gfc)
gob = gob_path.read_bytes()
se len(gob) != meta["gob_size"]:
print(f"[aviso] Tamanho do GOB incompatível: o cabeçalho diz {meta['gob_size']}, o arquivo tem {len(gob)}")
cache = {}
linhas = []
linhas.append(f"GFC: {gfc_path}")
linhas.append(f"GOB: {caminho_gob}")
linhas.append(f"Clusters: {meta['num_clusters']}")
linhas.append(f"Arquivos: {meta['num_files']}")
linhas.append("")
para f em meta["arquivos"]:
dados = reconstruir_arquivo(gob, meta["clusters"], f, cache)
ext = guess_ext(dados)
nome = f"arquivo_{f['idx']:04d}_crc{f['crc']:08X}_tamanho{f['tamanho']:08X}{ext}"
caminho = diretório_de_saída /nome
caminho.escrever_bytes(dados)
linhas.append(f"{nome} primeiro_cluster={f['primeiro']} tamanho={f['tamanho']}")
(out_dir / args.report).write_text("\n".join(lines), encoding="utf-8")
print(f"Extraídos {len(meta['files'])} arquivos para {out_dir}")
se __name__ == "__main__":
principal()