morrigan Posted Saturday at 11:49 AM Posted Saturday at 11:49 AM (edited) Hello everyone, I'm currently looking into the files for the Sega game Panzer Dragoon Orta on the Original Xbox. I've come across files with the .txb extension. Some of these have the magic ID TXRB. I believe these are texture files, as I was able to view some images from them using 'image heat'. However, there seem to be different types of .txb files. Others have the magic ID PCMP. These are not viewable with 'image heat'. I suspect these might be containers or perhaps compressed TXB files. Is PCMP related to compressed textures on the Xbox? I've attached some example files. azel.txb and lzzdra_lv1.txb (both TXRB) are viewable with 'image heat'(BC2_DXT3 pixel format). Does anyone have experience with this format or game data? Any help analyzing these TXB variations would be greatly appreciated! Thanks! TXB_FILES.zip Edited Saturday at 11:50 AM by morrigan
DKDave Posted Saturday at 12:48 PM Posted Saturday at 12:48 PM 1 hour ago, morrigan said: Hello everyone, I'm currently looking into the files for the Sega game Panzer Dragoon Orta on the Original Xbox. I've come across files with the .txb extension. Some of these have the magic ID TXRB. I believe these are texture files, as I was able to view some images from them using 'image heat'. However, there seem to be different types of .txb files. Others have the magic ID PCMP. These are not viewable with 'image heat'. I suspect these might be containers or perhaps compressed TXB files. Is PCMP related to compressed textures on the Xbox? I've attached some example files. azel.txb and lzzdra_lv1.txb (both TXRB) are viewable with 'image heat'(BC2_DXT3 pixel format). Does anyone have experience with this format or game data? Any help analyzing these TXB variations would be greatly appreciated! Thanks! TXB_FILES.zip 2.4 MB · 1 download Yes, they are compressed. In the PCMP header, there is the compressed and decompressed sizes. For the title image, compressed size is 0x11e33a and decompressed is 0x500030. There are several images in that file when decompressed. The first image title one is in RGBA32 format with Morton swizzling. Should be possible to do a proper script to handle multiple images. The compression type is "Yakuza" from the QuickBMS compression scanner. 1
Engineers Solution Rabatini Posted Saturday at 05:57 PM Engineers Solution Posted Saturday at 05:57 PM (edited) it uses a rather simple LZSS compression. I haven't tested it myself because I don't have these new graphics tools, and I don't even know where to open the file! LOL if you can test the tool attached if works or not. """ Descompressor PCMP → LZSS -------------------------------------------------------------""" from __future__ import annotations import struct, argparse from pathlib import Path import tkinter as tk from tkinter import filedialog as fd, messagebox as mb DATA_OFFSET = 0x20 # início dos dados comprimidos # --------------------------------------------------------------------------- # SIMPLE HEADER # --------------------------------------------------------------------------- def _read_header(buf: bytes): if len(buf) < DATA_OFFSET: raise ValueError("Arquivo muito curto para cabeçalho PCMP.") if buf[:4] != b"PCMP": raise ValueError("Assinatura 'PCMP' não encontrada.") out_size, comp_size = struct.unpack_from("<II", buf, 0x14) remaining = len(buf) - DATA_OFFSET if comp_size == 0 or comp_size > remaining: comp_size = remaining # fallback: usa o resto do arquivo return out_size, comp_size # --------------------------------------------------------------------------- # LZSS # --------------------------------------------------------------------------- def _lzss_simple(comp: memoryview, out_size: int) -> bytes: out = bytearray() idx = 0 ilen = len(comp) if ilen == 0: raise EOFError("Fluxo comprimido vazio.") b = comp[idx]; idx += 1 bits = 8 while len(out) < out_size: if idx > ilen: raise EOFError("Fluxo comprimido terminou antes do previsto.") op = 1 if (b & 0x80) else 0 # 1 = cópia, 0 = literal b <<= 1 bits -= 1 if bits == 0: if idx >= ilen: break # não há mais flags b = comp[idx]; idx += 1 bits = 8 if op: if idx + 1 >= ilen: raise EOFError("Fim prematuro lendo bloco de cópia.") d = ((comp[idx] >> 4) | (comp[idx + 1] << 4)) + 1 # deslocamento cnt = (comp[idx] & 0x0F) + 3 # comprimento idx += 2 for _ in range(cnt): if len(out) >= out_size: break if d > len(out): out.append(0) else: out.append(out[-d]) else: # literal if idx >= ilen: raise EOFError("Fim prematuro lendo literal.") out.append(comp[idx]); idx += 1 return bytes(out) # --------------------------------------------------------------------------- # Função principal # --------------------------------------------------------------------------- def decompress_pcmp(data: bytes) -> bytes: out_sz, comp_sz = _read_header(data) return _lzss_simple(memoryview(data[DATA_OFFSET: DATA_OFFSET + comp_sz]), out_sz) # --------------------------------------------------------------------------- # CLI & GUI enxutos # --------------------------------------------------------------------------- def _decompress_file(src: Path, dst: Path): dst.write_bytes(decompress_pcmp(src.read_bytes())) print(f"Descomprimido → {dst}") def _cli(argv=None): ap = argparse.ArgumentParser(description="Descompressor PCMP (comtype YAKUZA)") ap.add_argument("input", nargs="?", help="Arquivo PCMP de entrada") ap.add_argument("output", nargs="?", help="Arquivo BIN de saída") ap.add_argument("--nogui", action="store_true", help="Forçar modo terminal") ar = ap.parse_args(argv) if ar.nogui or (ar.input and ar.output): if not (ar.input and ar.output): ap.error("Informe entrada e saída ou utilize a GUI.") _decompress_file(Path(ar.input), Path(ar.output)) else: _launch_gui() class _App(tk.Tk): def __init__(self): super().__init__() self.title("PCMP → BIN (lzss)") self.geometry("500x160") self.in_path = tk.StringVar(self); self.out_path = tk.StringVar(self) tk.Label(self, text="Arquivo PCMP:").grid(row=0, column=0, sticky="e", padx=10, pady=10) tk.Entry(self, textvariable=self.in_path, width=45).grid(row=0, column=1, padx=5) tk.Button(self, text="…", width=3, command=self._choose_in).grid(row=0, column=2, padx=5) tk.Label(self, text="Salvar como:").grid(row=1, column=0, sticky="e", padx=10, pady=10) tk.Entry(self, textvariable=self.out_path, width=45).grid(row=1, column=1, padx=5) tk.Button(self, text="…", width=3, command=self._choose_out).grid(row=1, column=2, padx=5) tk.Button(self, text="Descomprimir", width=15, command=self._run).grid(row=2, column=1, pady=20) def _choose_in(self): p = fd.askopenfilename(title="Selecione o arquivo PCMP", filetypes=[("TXB", "*.txb"), ("Todos", "*.*")]) if p: self.in_path.set(p) if not self.out_path.get(): self.out_path.set(Path(p).with_suffix(".dec")) def _choose_out(self): p = fd.asksaveasfilename(title="Salvar arquivo descomprimido", defaultextension=".bin", filetypes=[("DEC", "*.dec"), ("Todos", "*.*")]) if p: self.out_path.set(p) def _run(self): if not self.in_path.get(): mb.showwarning("Atenção", "Selecione um arquivo de entrada.") return if not self.out_path.get(): mb.showwarning("Atenção", "Selecione um local de saída.") return try: _decompress_file(Path(self.in_path.get()), Path(self.out_path.get())) mb.showinfo("Sucesso", "Descompressão concluída!") except Exception as exc: mb.showerror("Erro", str(exc)) def _launch_gui(): _App().mainloop() # --------------------------------------------------------------------------- if __name__ == "__main__": _cli() tool._TXB.py Edited Saturday at 05:58 PM by Rabatini 2
DKDave Posted Saturday at 11:28 PM Posted Saturday at 11:28 PM 5 hours ago, Rabatini said: it uses a rather simple LZSS compression. I haven't tested it myself because I don't have these new graphics tools, and I don't even know where to open the file! LOL if you can test the tool attached if works or not. """ Descompressor PCMP → LZSS -------------------------------------------------------------""" from __future__ import annotations import struct, argparse from pathlib import Path import tkinter as tk from tkinter import filedialog as fd, messagebox as mb DATA_OFFSET = 0x20 # início dos dados comprimidos # --------------------------------------------------------------------------- # SIMPLE HEADER # --------------------------------------------------------------------------- def _read_header(buf: bytes): if len(buf) < DATA_OFFSET: raise ValueError("Arquivo muito curto para cabeçalho PCMP.") if buf[:4] != b"PCMP": raise ValueError("Assinatura 'PCMP' não encontrada.") out_size, comp_size = struct.unpack_from("<II", buf, 0x14) remaining = len(buf) - DATA_OFFSET if comp_size == 0 or comp_size > remaining: comp_size = remaining # fallback: usa o resto do arquivo return out_size, comp_size # --------------------------------------------------------------------------- # LZSS # --------------------------------------------------------------------------- def _lzss_simple(comp: memoryview, out_size: int) -> bytes: out = bytearray() idx = 0 ilen = len(comp) if ilen == 0: raise EOFError("Fluxo comprimido vazio.") b = comp[idx]; idx += 1 bits = 8 while len(out) < out_size: if idx > ilen: raise EOFError("Fluxo comprimido terminou antes do previsto.") op = 1 if (b & 0x80) else 0 # 1 = cópia, 0 = literal b <<= 1 bits -= 1 if bits == 0: if idx >= ilen: break # não há mais flags b = comp[idx]; idx += 1 bits = 8 if op: if idx + 1 >= ilen: raise EOFError("Fim prematuro lendo bloco de cópia.") d = ((comp[idx] >> 4) | (comp[idx + 1] << 4)) + 1 # deslocamento cnt = (comp[idx] & 0x0F) + 3 # comprimento idx += 2 for _ in range(cnt): if len(out) >= out_size: break if d > len(out): out.append(0) else: out.append(out[-d]) else: # literal if idx >= ilen: raise EOFError("Fim prematuro lendo literal.") out.append(comp[idx]); idx += 1 return bytes(out) # --------------------------------------------------------------------------- # Função principal # --------------------------------------------------------------------------- def decompress_pcmp(data: bytes) -> bytes: out_sz, comp_sz = _read_header(data) return _lzss_simple(memoryview(data[DATA_OFFSET: DATA_OFFSET + comp_sz]), out_sz) # --------------------------------------------------------------------------- # CLI & GUI enxutos # --------------------------------------------------------------------------- def _decompress_file(src: Path, dst: Path): dst.write_bytes(decompress_pcmp(src.read_bytes())) print(f"Descomprimido → {dst}") def _cli(argv=None): ap = argparse.ArgumentParser(description="Descompressor PCMP (comtype YAKUZA)") ap.add_argument("input", nargs="?", help="Arquivo PCMP de entrada") ap.add_argument("output", nargs="?", help="Arquivo BIN de saída") ap.add_argument("--nogui", action="store_true", help="Forçar modo terminal") ar = ap.parse_args(argv) if ar.nogui or (ar.input and ar.output): if not (ar.input and ar.output): ap.error("Informe entrada e saída ou utilize a GUI.") _decompress_file(Path(ar.input), Path(ar.output)) else: _launch_gui() class _App(tk.Tk): def __init__(self): super().__init__() self.title("PCMP → BIN (lzss)") self.geometry("500x160") self.in_path = tk.StringVar(self); self.out_path = tk.StringVar(self) tk.Label(self, text="Arquivo PCMP:").grid(row=0, column=0, sticky="e", padx=10, pady=10) tk.Entry(self, textvariable=self.in_path, width=45).grid(row=0, column=1, padx=5) tk.Button(self, text="…", width=3, command=self._choose_in).grid(row=0, column=2, padx=5) tk.Label(self, text="Salvar como:").grid(row=1, column=0, sticky="e", padx=10, pady=10) tk.Entry(self, textvariable=self.out_path, width=45).grid(row=1, column=1, padx=5) tk.Button(self, text="…", width=3, command=self._choose_out).grid(row=1, column=2, padx=5) tk.Button(self, text="Descomprimir", width=15, command=self._run).grid(row=2, column=1, pady=20) def _choose_in(self): p = fd.askopenfilename(title="Selecione o arquivo PCMP", filetypes=[("TXB", "*.txb"), ("Todos", "*.*")]) if p: self.in_path.set(p) if not self.out_path.get(): self.out_path.set(Path(p).with_suffix(".dec")) def _choose_out(self): p = fd.asksaveasfilename(title="Salvar arquivo descomprimido", defaultextension=".bin", filetypes=[("DEC", "*.dec"), ("Todos", "*.*")]) if p: self.out_path.set(p) def _run(self): if not self.in_path.get(): mb.showwarning("Atenção", "Selecione um arquivo de entrada.") return if not self.out_path.get(): mb.showwarning("Atenção", "Selecione um local de saída.") return try: _decompress_file(Path(self.in_path.get()), Path(self.out_path.get())) mb.showinfo("Sucesso", "Descompressão concluída!") except Exception as exc: mb.showerror("Erro", str(exc)) def _launch_gui(): _App().mainloop() # --------------------------------------------------------------------------- if __name__ == "__main__": _cli() tool._TXB.py 5.83 kB · 0 downloads I can confirm your decompression works. It's a slightly strange compression format. 1
morrigan Posted Sunday at 06:17 PM Author Posted Sunday at 06:17 PM On 5/4/2025 at 1:57 AM, Rabatini said: it uses a rather simple LZSS compression. I haven't tested it myself because I don't have these new graphics tools, and I don't even know where to open the file! LOL if you can test the tool attached if works or not. """ Descompressor PCMP → LZSS -------------------------------------------------------------""" from __future__ import annotations import struct, argparse from pathlib import Path import tkinter as tk from tkinter import filedialog as fd, messagebox as mb DATA_OFFSET = 0x20 # início dos dados comprimidos # --------------------------------------------------------------------------- # SIMPLE HEADER # --------------------------------------------------------------------------- def _read_header(buf: bytes): if len(buf) < DATA_OFFSET: raise ValueError("Arquivo muito curto para cabeçalho PCMP.") if buf[:4] != b"PCMP": raise ValueError("Assinatura 'PCMP' não encontrada.") out_size, comp_size = struct.unpack_from("<II", buf, 0x14) remaining = len(buf) - DATA_OFFSET if comp_size == 0 or comp_size > remaining: comp_size = remaining # fallback: usa o resto do arquivo return out_size, comp_size # --------------------------------------------------------------------------- # LZSS # --------------------------------------------------------------------------- def _lzss_simple(comp: memoryview, out_size: int) -> bytes: out = bytearray() idx = 0 ilen = len(comp) if ilen == 0: raise EOFError("Fluxo comprimido vazio.") b = comp[idx]; idx += 1 bits = 8 while len(out) < out_size: if idx > ilen: raise EOFError("Fluxo comprimido terminou antes do previsto.") op = 1 if (b & 0x80) else 0 # 1 = cópia, 0 = literal b <<= 1 bits -= 1 if bits == 0: if idx >= ilen: break # não há mais flags b = comp[idx]; idx += 1 bits = 8 if op: if idx + 1 >= ilen: raise EOFError("Fim prematuro lendo bloco de cópia.") d = ((comp[idx] >> 4) | (comp[idx + 1] << 4)) + 1 # deslocamento cnt = (comp[idx] & 0x0F) + 3 # comprimento idx += 2 for _ in range(cnt): if len(out) >= out_size: break if d > len(out): out.append(0) else: out.append(out[-d]) else: # literal if idx >= ilen: raise EOFError("Fim prematuro lendo literal.") out.append(comp[idx]); idx += 1 return bytes(out) # --------------------------------------------------------------------------- # Função principal # --------------------------------------------------------------------------- def decompress_pcmp(data: bytes) -> bytes: out_sz, comp_sz = _read_header(data) return _lzss_simple(memoryview(data[DATA_OFFSET: DATA_OFFSET + comp_sz]), out_sz) # --------------------------------------------------------------------------- # CLI & GUI enxutos # --------------------------------------------------------------------------- def _decompress_file(src: Path, dst: Path): dst.write_bytes(decompress_pcmp(src.read_bytes())) print(f"Descomprimido → {dst}") def _cli(argv=None): ap = argparse.ArgumentParser(description="Descompressor PCMP (comtype YAKUZA)") ap.add_argument("input", nargs="?", help="Arquivo PCMP de entrada") ap.add_argument("output", nargs="?", help="Arquivo BIN de saída") ap.add_argument("--nogui", action="store_true", help="Forçar modo terminal") ar = ap.parse_args(argv) if ar.nogui or (ar.input and ar.output): if not (ar.input and ar.output): ap.error("Informe entrada e saída ou utilize a GUI.") _decompress_file(Path(ar.input), Path(ar.output)) else: _launch_gui() class _App(tk.Tk): def __init__(self): super().__init__() self.title("PCMP → BIN (lzss)") self.geometry("500x160") self.in_path = tk.StringVar(self); self.out_path = tk.StringVar(self) tk.Label(self, text="Arquivo PCMP:").grid(row=0, column=0, sticky="e", padx=10, pady=10) tk.Entry(self, textvariable=self.in_path, width=45).grid(row=0, column=1, padx=5) tk.Button(self, text="…", width=3, command=self._choose_in).grid(row=0, column=2, padx=5) tk.Label(self, text="Salvar como:").grid(row=1, column=0, sticky="e", padx=10, pady=10) tk.Entry(self, textvariable=self.out_path, width=45).grid(row=1, column=1, padx=5) tk.Button(self, text="…", width=3, command=self._choose_out).grid(row=1, column=2, padx=5) tk.Button(self, text="Descomprimir", width=15, command=self._run).grid(row=2, column=1, pady=20) def _choose_in(self): p = fd.askopenfilename(title="Selecione o arquivo PCMP", filetypes=[("TXB", "*.txb"), ("Todos", "*.*")]) if p: self.in_path.set(p) if not self.out_path.get(): self.out_path.set(Path(p).with_suffix(".dec")) def _choose_out(self): p = fd.asksaveasfilename(title="Salvar arquivo descomprimido", defaultextension=".bin", filetypes=[("DEC", "*.dec"), ("Todos", "*.*")]) if p: self.out_path.set(p) def _run(self): if not self.in_path.get(): mb.showwarning("Atenção", "Selecione um arquivo de entrada.") return if not self.out_path.get(): mb.showwarning("Atenção", "Selecione um local de saída.") return try: _decompress_file(Path(self.in_path.get()), Path(self.out_path.get())) mb.showinfo("Sucesso", "Descompressão concluída!") except Exception as exc: mb.showerror("Erro", str(exc)) def _launch_gui(): _App().mainloop() # --------------------------------------------------------------------------- if __name__ == "__main__": _cli() tool._TXB.py 5.83 kB · 1 download 18 hours ago, DKDave said: I can confirm your decompression works. It's a slightly strange compression format. Successfully viewed the correct image now, thanks for everyone's help! Since I could only accept one answer, I went with Rabatini's. The script even came with a GUI, though the interface seems to be in Portuguese, fortunately, it wasn't too hard to understand. DKDave also provided important hints, letting me know which color format and swizzle type to choose. Many thanks! 1
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now