Jump to content

Help Analyzing Panzer Dragoon Orta (Xbox) TXB File Format


Go to solution Solved by Rabatini,

Recommended Posts

Posted (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'.

image.thumb.png.68a78b6d1bf5cec9c15c62960e4af396.png

image.png.0c32884744211e5e79e12fc7f377da3b.png

 

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?

image.png.2bef5da95fa5fb965a7d92d5e42df7d5.png
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 by morrigan
Posted
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'.

image.thumb.png.68a78b6d1bf5cec9c15c62960e4af396.png

image.png.0c32884744211e5e79e12fc7f377da3b.png

 

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?

image.png.2bef5da95fa5fb965a7d92d5e42df7d5.png
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.

 

image.png.03d12f0920dd0d857bc05bb7429ed9f3.png

 

 

  • Like 1
  • Engineers
  • Solution
Posted (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 by Rabatini
  • Like 2
Posted
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.

 

 

  • Like 1
Posted
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!image.png.6942ecbf6cab7cff952a280f8fc39783.png

  • Like 1

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 account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...