29/05/2026
Tibia 1.03 (Alfa): Sprites


Introdução

Tibia é um dos MMORPGs mais antigos ainda online e em desenvolvimento ativo. Criado em 1997 por estudantes de uma universidade alemã – que mais tarde fundaram a CipSoft , eventualmente se tornou muito famoso, especialmente no Brasil.

Algum tempo atrás, acabei esbarrando em uma cópia do cliente Alfa (versão 1.03), que, até onde eu sei, foi a primeira versão liberada publicamente. Você pode baixá-lo aqui .

Infelizmente, porém, não há nenhum servidor (oficial ou emulador) disponível para essa versão do jogo. O cliente foi escrito em C++ (compilado com Borland C++ 4.5 para sistemas Windows de 16 bits, mais especificamente Windows 3.1 e 95, mas é possível executá-lo até no Windows 2000.

Como gosto de velharias e de quebra-cabeças de programação, resolvi explorar o cliente para entender como ele funciona e, quem sabe um dia, escrever um emulador de servidor.

Como precisava começar por algum lugar, escolhi entender como os sprites são salvos e lidos nessa versão. Ao longo deste post, busco detalhar um pouco do meu entendimento obtido por meio de engenharia reversa .

Para isso, usei algumas ferramentas, em especial:

Infelizmente, como o executável é compilado para sistemas de 16 bits (NE), temos que lidar com far e near pointers . Ferramentas mais modernas, como o Ghidra (ou até mesmo versões mais novas do IDA), não têm suporte completo ou simplesmente não oferecem suporte algum.

MUDOBJ.SPR

Esse é o arquivo que contém os sprites do jogo. Sua estrutura é a seguinte:

Cabeçalho (Header)

  • Data Block Count (2 bytes): define a quantidade total de blocos de dados (data blocks) que vêm a seguir.

Bloco de Dados (Data Block)

  • Sprite ID (2 bytes): identificador único do sprite.
  • Block Size (2 bytes): tamanho total dos dados do sprite em bytes. Inclui os dois bytes do próprio campo de tamanho, mas não inclui o tamanho do Sprite ID.
  • Raw Pixel Data (Block Size - 2 bytes): o payload contendo os dados dos pixels de forma comprimida.

Compressão dos Pixels

Para compressão, é utilizada uma versão customizada de Run-Length Encoding . A lógica de descompressão interpreta o fluxo de bytes como sequências alternadas de blocos Transparentes e Coloridos, até que todo o sprite seja processado.

A lógica de descompressão segue o seguinte padrão:

  1. Bloco Transparente:
    • : 2 bytes.
    • Calcula:
      • transparentBytes = value % 0x5A0
      • pixelsToSkip = transparentBytes / 3
    • Ação:
      • Avança a posição atual em pixelsToSkip. Esses pixels permanecem transparentes (alpha = 0).
  2. Bloco Colorido:
    • : 2 bytes (representando a quantidade de bytes de pixels coloridos).
    • Calcula:
      • coloredPixels = value / 3
    • Ação:
      • Itera coloredPixels vezes.
      • Para cada iteração, lê 3 bytes.
      • Formato do Pixel: BGR (Blue, Green, Red), 1 byte por canal.
      • Escreve o pixel opaco (alpha = 255) na posição atual. Nota: os dados dos sprites são armazenados da esquerda para a direita, de baixo para cima (começam na esquerda inferior, terminam no topo direito).

Script de extração de sprites

Para executar o script de extração abaixo, você precisará do Python instalado e da biblioteca Pillow para a manipulação de imagens.

  1. Instale a biblioteca Pillow:

    pip install Pillow
    
  2. Execute o script: Certifique-se de que o arquivo MUDOBJ.SPR esteja no mesmo diretório do script (ou passe o caminho do arquivo como argumento) e execute:

    python extract_sprites.py
    

    (Caso queira passar um arquivo específico, execute python extract_sprites.py caminho/para/arquivo.SPR).

Os sprites extraídos serão salvos automaticamente em uma pasta chamada output/ no mesmo diretório.

A seguir, o código completo do script:

import struct  
import os  
import sys  
from PIL import Image  
  
  
def extract_sprites(filename, output_dir="output"):  
    # 1. Cria uma pasta de saída caso ainda não exista
    if not os.path.exists(output_dir):  
        os.makedirs(output_dir)  
        print(f"Diretório criado: {output_dir}")  
  
    try:  
        with open(filename, "rb") as f:  
            print(f"Processando {filename}...")  
  
            # 2. Lê a quantidade de sprites (uint16)  
            header_data = f.read(2)  
            if len(header_data) < 2:  
                print("Erro: Arquivo muito pequeno.")  
                return  
  
            sprite_count = struct.unpack("<H", header_data)[0]  
            print(f"{sprite_count} sprites encontrados. Extraindo...")  
  
            sprites_extracted = 0  
  
            # 3. Itera por todos os sprites
            for _ in range(sprite_count):  
                # Lê o ID (uint16) e o Tamanho (uint16)  
                header = f.read(4)  
                if len(header) < 4:  
                    break  
  
                sprite_id, sprite_size = struct.unpack("<HH", header)  
  
                # O campo 'tamanho' inclui os 2 bytes do campo tamanho em si,
                # então o tamanho real dos dados do payload é size - 2.  
                payload_size = sprite_size - 2  
  
                if payload_size <= 0:  
                    # Sprite vazio ou inválido - deve ser pulado.
                    continue  
  
                # Lê o payload RLE completo para o sprite.
                sprite_data = f.read(payload_size)  
                if len(sprite_data) != payload_size:  
                    print(f"Atenção: Fim de arquivo inesperado para o sprite {sprite_id}")  
                    break  
  
                # 4. Descomprime os dados RLE  
                # Cria uma imagem 32x32 transparente
                img = Image.new("RGBA", (32, 32), (0, 0, 0, 0))  
                pixels = img.load()  
  
                read_offset = 0  
                current_pixel = 0  
  
                while read_offset < payload_size and current_pixel < 1024:  
                    # A. Bloco Transparente
                    # Precisamos de pelo menos 2 bytes para a contagem de pixels transparentes
                    if read_offset + 2 > payload_size:  
                        break  
  
                    trans_raw = struct.unpack_from("<H", sprite_data, read_offset)[0]  
                    read_offset += 2  
  
                    # Aplica lógica de módulo
                    trans_val = trans_raw % 0x5A0  
  
                    # Converte a contagem de bytes para pixels
                    skip_pixels = trans_val // 3  
                    current_pixel += skip_pixels  
  
                    # B. Bloco colorido
                    # Precisamos de pelo menos 2 bytes para a contagem de pixels coloridos
                    if read_offset + 2 > payload_size:  
                        break  
  
                    colored_bytes = struct.unpack_from("<H", sprite_data, read_offset)[0]  
                    read_offset += 2  
  
                    # Lê os bytes de pixels coloridos (sequência BGR)
                    if read_offset + colored_bytes > payload_size:  
                        break  
  
                    # Extrai o bloco de pixels coloridos
                    color_chunk = sprite_data[read_offset: read_offset + colored_bytes]  
                    read_offset += colored_bytes  
  
                    # Processa o bloco em grupos de 3 bytes (BGR)
                    for i in range(0, len(color_chunk), 3):  
                        # Garante que temos o trio completo
                        if i + 2 < len(color_chunk):  
                            b = color_chunk[i]  
                            g = color_chunk[i + 1]  
                            r = color_chunk[i + 2]  
  
                            if current_pixel < 1024:  
                                # Calcula X, Y  
                                x = current_pixel % 32  
                                y = current_pixel // 32  
  
                                # Inverte o eixo Y
                                flipped_y = 31 - y  
  
                                # Escreve o pixel  
                                pixels[x, flipped_y] = (r, g, b, 255)  
                                current_pixel += 1  
  
                # 5. Salva o sprite. 
                output_path = os.path.join(output_dir, f"{sprite_id}.png")  
                img.save(output_path)  
                sprites_extracted += 1  
  
            print(f"Pronto! {sprites_extracted} extraídos para '{output_dir}/'.")  
  
    except FileNotFoundError:  
        print(f"Erro: não foi possível encontrar o arquivo '{filename}'")  
    except Exception as e:  
        print(f"Um erro inesperado ocorreu: {e}")  
  
  
if __name__ == "__main__":  
    # Caso necessário, altere o nome do arquivo aqui, ou passe como argumento
    file_to_process = "MUDOBJ.SPR"  
    if len(sys.argv) > 1:  
        file_to_process = sys.argv[1]  
  
    extract_sprites(file_to_process)