GAF: Graphics Animation Format
Fonte: coreprime/kbot @ 0ab97bf · Editar no GitHub
O GAF guarda animações de sprites em cores indexadas. Quase todo elemento 2D animado do Total Annihilation é um GAF: cursores, ponteiros do mouse, explosões, gadgets de unidades, o balanço das árvores, ícones de armas, as bolhas de lava, tudo. O TA: Kingdoms reaproveita o mesmo contêiner com a extensão
.tafpara suas animações com paleta (a paleta vem de um arquivo acompanhante). O TA:K também traz uma variante truecolor de.taf, que guarda pixels ARGB de 16 bits em vez de índices; essa tem página própria, TAF / TSF.
Tip
Experimente você mesmo.
kbot gaf list anims/cursors.gaf # sequences, frame counts, loop flag
kbot gaf export anims/cursors.gaf --format gif --sequence 0 --target cursor.gif
kbot gaf dump anims/cursors.gaf --target ./cursors # one PNG per frame
Todas as opções estão na referência do kbot gaf
da linha de comando.
Em Go. Use formats/gaf:
import "github.com/coreprime/kbot/formats/gaf"
r, _ := gaf.LoadFromFile("anims/cursors.gaf")
defer r.Close()
sequences, _ := r.ReadSequences()
for _, seq := range sequences {
fmt.Println(seq.Name, len(seq.Frames))
}
Visão geral
┌─ Header (12B) ─┐
│ ver N pad │
└────────┬───────┘
▼
┌─ Sequence offset table ─┐ N × uint32 absolute offsets
└───┬─────────────────────┘
▼
┌─ SequenceHeader (40B) ─┐
│ frameCount, loop word, │
│ +4 word, name[32] │
└─────────┬──────────────┘
▼
┌─ FrameListItem (8B) × frameCount ─┐ ptr + tick duration
└───┬───────────────────────────────┘
▼
┌─ FrameInfo (24B) ─┐
│ size, origin, │
│ transparency, │
│ compressed?, │
│ layers, +11 byte │
│ ptrPixelData │
└─────┬─────────────┘
▼
┌─ Pixel data: raw or run-encoded ─┐
└──────────────────────────────────┘
Um GAF é um array simples de sequências. Cada sequência é um array de quadros, e cada quadro é um bitmap de índices da paleta, de largura × altura, com um ponto de origem e (opcionalmente) trechos de transparência no estilo RLE. Os quadros podem ter "subquadros", de modo que um passo lógico da animação seja a composição de várias camadas sobrepostas.
Cabeçalho (12 bytes)
typedef struct {
uint32 Version; // 0x00010100 (or 0 for a handful of stock GAFs)
uint32 SequenceCount; // Number of animations in the file
uint32 Unknown1; // Always 0
} GAFHeader;
| Campo | Observações |
|---|---|
Version |
0x00010100 em todas as ferramentas de terceiros. Os arquivos anims/terrain.gaf e anims/vismasks.gaf da Cavedog vêm com Version == 0. O jogo não verifica esse valor. O kbot aceita qualquer valor (um desconhecido gera um aviso) e grava 0x00010100. |
SequenceCount |
Logo em seguida vêm SequenceCount × uint32 offsets absolutos no arquivo, cada um apontando para um SequenceHeader. O jogo usa só os 16 bits baixos, lidos com sinal: de 1 a 32.767 sequências, e nenhuma para zero ou um valor negativo. |
Unknown1 |
Sempre zero. Grave zero. |
Sequência (40 bytes)
typedef struct {
uint16 FrameCount;
uint16 LoopFlags; // +2: low byte non-zero = the sequence loops
uint32 Unknown4; // +4: not used by the game; stock files store 0
char Name[32]; // NUL-padded, case-preserved
} GAFSequence;
O jogo repete a sequência em loop quando o byte baixo de LoopFlags está
definido e a reproduz uma única vez (parando no último quadro) quando ele
vale 0. Todas as sequências originais guardam 1. O kbot preserva os
dois valores ao regravar um arquivo, e kbot gaf build dá o valor
original 1 a uma sequência sem sequence.csv.
Em seguida vêm FrameCount × FrameListItem:
typedef struct {
uint32 PtrFrameInfo; // → FrameInfo
uint32 Duration; // Game ticks (30 ticks = 1 second); low 16 bits used
} FrameListItem;
O jogo lê só os 16 bits baixos de Duration e mostra um quadro por
max(Duration, 1) ticks do seu relógio de animação de 30 Hz.
Os nomes das sequências importam: o jogo procura as animações pelo nome
(por exemplo, UnitInfo referencia corpyro_smoke do script
corpyro.gaf). Compare-os como ASCII sem diferenciar maiúsculas de
minúsculas.
Quadro (informações de 24 bytes + dados)
typedef struct {
uint16 Width;
uint16 Height;
int16 OriginX; // Render anchor, may be negative
int16 OriginY;
uint8 TransparencyIndex; // Key: transparent in raw frames
uint8 Compressed; // 0 = raw bitmap, non-zero = run-encoded
uint8 LayerCount; // +10: 0 = simple frame, >0 = composite of layers
uint8 Blend; // +11: set on a layer = drawn translucently
uint32 Unknown2; // +12: editor scratch, not used by the game
uint32 PtrFrameData; // → pixel bytes OR layer pointer table
uint32 Unknown3; // Always 0
} FrameInfo;
OriginX / OriginY definem o ponto de ancoragem usado ao copiar o
quadro para o mundo. Num sprite de explosão, a origem costuma ser o
centro da explosão; num cursor, é o pixel do hotspot.
Quadros em camadas
Quando LayerCount > 0, PtrFrameData é um array de LayerCount × uint32
ponteiros para FrameInfos aninhados, e não dados de pixels. Cada
camada é desenhada na ordem do array (de trás para a frente), com o seu
hotspot sobre o hotspot do quadro externo. Uma camada com o byte +11
definido é desenhada pela tabela de translucidez em vez de ser copiada
(nenhum arquivo original o define). O byte +10 e o byte +11 são campos
separados: lê-los como uma única contagem de 16 bits transforma um byte
+11 definido em 256 camadas.
Dados de pixels
TransparencyIndex vale para cada quadro, não para o arquivo todo: o
GAF dos cursores, por exemplo, usa o índice 9 (o rosa do sistema) em
alguns quadros e o índice 0 em outros.
Sem compressão (Compressed == 0)
Width × Height bytes, de cima para baixo e da esquerda para a direita.
O jogo desenha todos os pixels, exceto os iguais a TransparencyIndex. As
texturas das unidades (textures/*.gaf) e as máscaras de visão
(anims/vismasks.gaf) são lidas como arrays simples de pixels, por isso
os seus quadros precisam estar sem compressão. Todos os quadros originais
ali estão.
Com compressão (Compressed != 0): a codificação RLE do TA
Os dados de pixels são uma sequência de Height linhas independentes. Cada
linha começa com um uint16 lineLength, seguido de um fluxo de bytes
codificado em RLE com exatamente lineLength bytes. Dentro desse
fluxo, cada chunk começa com uma máscara de 1 byte cujos dois bits
baixos escolhem o tipo de chunk:
| Teste da máscara | Tipo de chunk | Comprimento (pixels) | Bytes seguintes |
|---|---|---|---|
mask & 0x01 |
Trecho transparente | count = mask >> 1 |
(nenhum: os pixels são pulados) |
mask & 0x02 |
Trecho repetido | count = (mask >> 2) + 1 |
1 byte: o índice de cor a repetir |
| nenhum dos dois | Cópia literal | count = (mask >> 2) + 1 |
count bytes: os pixels literais |
Assim, os trechos têm no máximo 127 pixels transparentes, 64 pixels
repetidos e 64 pixels literais por chunk. Só os pixels pulados são
transparentes: um pixel repetido ou literal é desenhado mesmo quando o
seu valor é igual a TransparencyIndex, e o índice 0 da paleta é um preto
opaco comum. Se a linha codificada terminar antes de Width, o kbot a
completa com pixels transparentes e avisa. O jogo continuaria lendo os
bytes seguintes. As camadas podem ser menores que o quadro externo e são
posicionadas pelos seus próprios OriginX/Y.
Referência: formats/gaf/gaf.go readCompressed
e formats/gaf/writer.go compressRow.
Important
O GAF não tem paleta. O arquivo guarda só índices de 8 bits. Para
renderizar em cores, é preciso a paleta global do TA
(palettes/palette.pal) ou, no TA: Kingdoms, uma paleta própria de cada
recurso, que fica fora do GAF. Veja [Resolução da paleta no TA: Kingdoms]
(#ta-kingdoms--palette-resolution) e A peculiaridade da transparência
no TA: Kingdoms para o mecanismo
específico do TAK. Por padrão, kbot gaf export usa a paleta do TA
embutida e a regra de transparência do jogo. As duas podem ser trocadas.
Resolução da paleta no TA: Kingdoms
Note
Esta seção trata só do que muda no TA:K. O renderizador do TA sempre
usa palettes/palette.pal. A disposição dos bytes do GAF é idêntica no
TA e no TAK, mas o TAK passou a usar uma paleta própria para cada recurso.
O TA: Kingdoms traz o mesmo contêiner .gaf (às vezes .taf) do TA, mas
tira a paleta de um arquivo acompanhante de cada recurso. A instalação da
versão comercial do TAK segue três convenções documentadas, em ordem de
prioridade:
Um
.pcxcom o mesmo nome ao lado do.gaf. Os recursos de interface e de animação trazem um arquivo PCX de 1×1 com a paleta desejada embutida no chunk PLTE do PCX.anims/actionbuttons.gafé renderizado comanims/actionbuttons.pcx. Cerca de 38% dos arquivosanims/*.gafdo TAK têm um acompanhante assim.Um
.palcom o mesmo nome-base empalettes/. Raro. Cobre uns poucos recursos originais para os quais a Cavedog exportou uma paleta bruta de 1.024 bytes.Uma paleta de lado, pelo prefixo do nome do arquivo. Os GAFs de unidades e de elementos têm um prefixo de lado de 3 letras e usam a paleta do lado correspondente:
Prefixo Lado Arquivo de paleta ara*Aramon palettes/aramon.pcxtar*Taros palettes/taros.pcxver*Veruna palettes/veruna.pcxzon*Zhon palettes/zhon.pcxaid*Aiden (Iron Plague) palettes/aiden.pcxcre*Creon (Iron Plague) palettes/creon.pcx
Se nenhuma delas resolver, o resolvedor recorre a palettes/palette.pal
e, por fim, à paleta do TA embutida. As duas são idênticas byte a byte no
TAK comercial e iguais à do TA, por isso, nos GAFs de unidades e elementos
que não batem com um prefixo de lado, as cores saem erradas sem uma
substituição explícita.
O resolvedor do kbot
internal/palettepick
implementa a cadeia acima. Todos os pontos de entrada que renderizam um
GAF passam por ele:
- Explorador web. As rotas
/png/…e/apng/…detectam a paleta automaticamente por padrão. O visualizador de GAF mostra uma lista suspensa de paletas com todos os candidatos que o resolvedor encontra, mais uma entradaauto. Passe?palette=<vfs-path>para forçar uma delas (a lista suspensa faz isso por você). kbot gaf export/kbot gaf dump. Passe--palette <path.pcx|path.pal>para substituir. Caso contrário, é usada a paleta do TA embutida (a linha de comando não roda o resolvedor do VFS, porque não tem contexto de VFS, a não ser que você a envolva comkbot mount).- Ferramenta MCP
gaf_export. Aceita um argumentopaletteopcional (um caminho dentro da pastagame-dataconfigurada). Sem ele, o resolvedor completo é executado.
Portar um GAF do TA para o TA: Kingdoms
A disposição dos bytes dos GAFs do TA e do TAK é idêntica: só a origem da
paleta muda. Para portar um .gaf do TA:
- Crie um PCX de 1×1 com paleta, com a paleta do TA embutida.
- Salve-o ao lado do GAF com o mesmo nome-base (por exemplo,
MyGaf.gaf+MyGaf.pcx).
Isso basta para o TAK renderizá-lo com as cores originais do TA.
A peculiaridade da transparência no TA: Kingdoms
Note
Esta seção trata só do que muda no TA:K. O byte TransparencyIndex
da seção Quadro é a referência para o
renderizador do TA, mas os autores do conteúdo do TAK não o mantiveram
de acordo com os pixels de fato nos quadros sem compressão.
Muitos GAFs de atlas de texturas do TAK (tudo em textures/*.gaf e uma
parte de anims/*.gaf) têm um quadro sem compressão cujo
TransparencyIndex nos metadados indica uma constante (em geral 9) que
nunca aparece nos pixels. Enquanto isso, o artista preencheu a borda
"transparente" com outro índice da paleta (muitas vezes 5). Uma leitura
literal dos metadados renderiza um fundo colorido opaco onde o
renderizador do TA teria produzido transparência.
O renderizador do TA nunca tenta adivinhar: usa a chave guardada nos
quadros sem compressão e os comandos de pulo nos comprimidos. Para os
atlas do TAK, o kbot oferece uma heurística de detecção pelos cantos
opcional (TransparencyModeHeuristic, veja EffectiveTransparencyIndex
em formats/gaf do kbot-io), que vale só para quadros sem compressão:
- Se
TransparencyIndexaparecer em qualquer ponto do buffer de pixels, confia nos metadados. - Senão, examina os quatro cantos. Se os quatro tiverem o mesmo valor, usa esse valor como índice transparente efetivo. Isso salva os quadros sem compressão do TAK em que o artista desenhou uma borda uniforme.
- Senão, recorre ao
TransparencyIndexgravado no disco (o que preserva a renderização de texturas que ocupam o quadro inteiro, comodungwormB1, cujos cantos são pixels da unidade; a detecção automática corretamente não substitui nada).
Os quadros comprimidos e os compostos sempre usam os seus pixels pulados. O byte no disco nunca é sobrescrito: quem regrava o arquivo em ida e volta vê o valor original. Um quadro do TA totalmente opaco e com borda uniforme perderia essa borda com a heurística, por isso as renderizações do TA nunca a usam.
Substituir a transparência na hora de renderizar
gaf.RenderOptions oferece estes modos:
| Modo | Comportamento | Uso |
|---|---|---|
TransparencyModeAuto / TransparencyModeMetadata |
A regra do jogo: os pixels-chave de um quadro sem compressão e os pixels pulados de um quadro comprimido são transparentes; o índice 0 é preto opaco (o valor zero, e o padrão em todo o kbot para o TA). | Exibição, extrações, idas e voltas. |
TransparencyModeHeuristic |
Palpite pelos cantos nos quadros sem compressão (acima), e a regra do jogo nos demais. É o que o studio usa para os sprites do TA: Kingdoms. | Atlas de texturas do TAK. |
TransparencyModeNone |
Todos os pixels são opacos. | Inspecionar a cor-chave. |
TransparencyModeIndex |
Usa um índice fornecido por quem chama. | Substituição pela interface, depuração. |
Toda entrada de paleta exportada é opaca, exceto uma posição transparente por exportação, normalmente a chave do quadro. Quando um quadro desenha pixels cujo valor é igual à sua chave (ou quando os quadros de uma mesma animação usam as chaves uns dos outros como cores), um índice que nenhum pixel desenhado usa passa a ser essa posição, e assim a imagem preserva todos os índices da paleta.
O visualizador de GAF do explorador de recursos oferece Default / Game rule / Corner guess / None numa lista suspensa ao lado do seletor de
paleta e repassa a escolha como ?transparency=game|heuristic|none|<N> em
toda requisição de PNG/GIF/APNG. O padrão é a regra do jogo nas
instalações do TA e o palpite pelos cantos nas do TA: Kingdoms. A chave do
cache inclui a marca de transparência, por isso trocar de modo não serve
renderizações antigas.
A ferramenta MCP gaf_export segue o mesmo padrão: a regra do jogo,
exceto quando a pasta game-data está registrada como TA: Kingdoms (um
contexto do kbot takingdoms), que recebe o palpite pelos cantos. O
resultado informa o modo usado. kbot gaf export lê um arquivo avulso sem
contexto de jogo, por isso o padrão é a regra do jogo. Passe
--transparency heuristic para atlas do TA: Kingdoms. kbot gaf dump
sempre usa a regra do jogo, já que kbot gaf build lê as chaves guardadas
de volta a partir das imagens.
Exemplo comentado: cursors.gaf
$ kbot gaf list anims/cursors.gaf
GAF: 22 sequence(s), version 0x00010100
# Name Frames Loops Duration (ticks) Duration (sec)
─ ──── ────── ───── ──────────────── ──────────────
0 cursormove 8 yes 64 2.13
1 cursorgrn 1 yes 10 0.33
2 cursorselect 2 yes 12 0.40
...
Um quadro típico de cursormove:
Width: 32
Height: 32
OriginX: 16 ← hotspot in the middle of the cursor
OriginY: 16
Transparency: 0 ← this frame's key (compressed, so only skips are transparent)
Compressed: 1 ← run-encoded
LayerCount: 0 ← flat (no sub-frames)
Montar um GAF
kbot gaf dump grava uma estrutura de pastas que kbot gaf build lê de
volta:
cursors/
├── cursormove/
│ ├── sequence.csv ← index,name,loop_flags,unknown4
│ ├── frames.csv ← frame,width,height,origin_x,origin_y,transparency,
│ │ duration_ticks,duration_sec,storage,blend
│ ├── animated.png ← preview only; not read back
│ ├── 0.png
│ ├── 1.png
│ └── ...
└── ...
kbot gaf dump anims/cursors.gaf --target ./cursors
# edit some PNGs / tweak frames.csv or sequence.csv
kbot gaf build ./cursors --target ./cursors-rebuilt.gaf
kbot gaf roundtrip anims/ # check dump/build keeps what the game reads
As imagens dos quadros são PNGs indexados (ou GIFs com --format gif)
desenhados com a regra do jogo, de modo que cada pixel mantém o seu índice
da paleta e só os pixels transparentes são marcados. A montagem copia
esses índices de volta e transforma os pixels transparentes na chave do
quadro. Imagens RGBA são associadas à cor mais próxima da paleta, evitando
a chave.
sequence.csvrestaura a ordem da sequência, o seu nome exato, o seu valor de loop e o seu valor em +4. Sem ele, o nome da pasta vira o nome da sequência e a sequência repete em loop, como todas as originais.storage(rawoucompressed) eblend(o byte +11) restauram o cabeçalho de cada quadro. Os quadros semstoragesão comprimidos, exceto que todo quadro gravado emtextures/*.gafouanims/vismasks.gaffica sem compressão, porque o jogo os lê como arrays simples de pixels.--storage raw|compressedse sobrepõe aos dois.- Os quadros compostos (em camadas) são extraídos como uma única imagem
achatada e montados de volta como quadros simples.
kbot gaf roundtripinforma quantos. - Um quadro sem pixels (largura ou altura 0) não tem imagem. Só a sua
linha em
frames.csvbasta para remontá-lo. - Uma subpasta sem
frames.csvnemsequence.csvé ignorada com um aviso. Qualquer outra subpasta que não possa ser montada (uma imagem ausente ou ilegível, um valor inválido num CSV) interrompe a montagem, para que nenhuma sequência fique de fora sem aviso.
Note
Os GAFs do TA e do TA:K são idênticos byte a byte. O contêiner é idêntico; só o tratamento da paleta muda. Veja Resolução da paleta no TA: Kingdoms acima para a cadeia completa e a receita comentada para portar um GAF para o TAK.
Armadilhas
Warning
Uma sequência cujo byte de loop vale 0 toca uma vez e para. Ferramentas que gravam o valor em +2 como zero congelam todos os elementos animados em que tocam. O kbot preserva o valor, e as novas sequências montadas pelo kbot repetem em loop.
- Algumas sequências vêm com quadros de duração
0. O jogo mostra esse quadro por um tick, como se a duração fosse 1. Não as "corrija" para um valor padrão. - Linhas de pixels comprimidas podem ser mais curtas que
Width. O jogo continua lendo os bytes seguintes como comandos. O kbot completa a linha com pixels transparentes e avisa. Nenhum arquivo original tem linhas assim. - As origens das camadas são hotspots: cada camada é posicionada de modo que a sua origem caia sobre a origem do quadro externo.
- O
.tafdo TA: Kingdoms vem em duas variantes. O kbot lê um.tafcom paleta como um GAF comum, mas o TA:K também traz arquivos.taftruecolor (pixels ARGB de 16 bits, sem paleta). Confira o byte de formato do quadro para distingui-los, e veja TAF / TSF para a variante truecolor. anims/terrain.gafeanims/vismasks.gaftêmVersion == 0. O jogo ignora o valor da versão.- O número de quadros por sequência não tem limite:
cursorteleporttem 46 quadros. Não suponha um teto fixo ao dimensionar buffers.
Tamanhos típicos
| Recurso | Faixa observada nos GAFs da Cavedog |
|---|---|
anims/cursors.gaf |
~30 KB, 22 sequências, de 1 a 46 quadros cada |
| Gadget de unidade (por exemplo, clarão do disparo) | 5–20 KB, 1 sequência, 4–12 quadros |
Explosão (família anims/explosions.gaf) |
50–250 KB por arquivo |
anims/textures.gaf (a folha mestra de texturas) |
~12 MB (o maior GAF do TA) |
anims/terrain.gaf |
~1,5 MB |
| Dados por quadro | Em geral <1 KB; quadros grandes (explosões de 256×256) passam de 10 KB |
Exemplos reais no catálogo de referência
GAFs notáveis da instalação do TA. Extraia qualquer um com
kbot hpi extract e depois use kbot gaf list / kbot gaf export:
anims/cursors.gaf: 22 sequências, cada uma a animação de um cursor da interface. As menores sequências (cursorgrn,cursorred) têm um único quadro. A maior (cursorteleport) tem 46 quadros em 460 ticks. Aparece no exemplo comentado acima.anims/textures.gaf: ~12 MB, a folha mestra de blocos referenciada pelas stringsOffsetToTextureNamede todos os modelos 3DO. Navegue comkbot gaf list anims/textures.gaf | head.- Família
anims/explosions.gaf: GAFs de efeitos típicos,lasersht.gaf,acid.gaf,bigexpls.gaf, cada um com várias sequências de explosão com nome. anims/armcom.gaf/corcom.gaf: gadgets do Comandante de cada lado (círculo de seleção, laser de construção).anims/terrain.gaf+anims/vismasks.gaf: os únicos GAFs originais comVersion == 0no cabeçalho (bom saber ao escrever um analisador).
Veja também
- TAF / TSF: a variante truecolor
.tafdo TA: Kingdoms, construída sobre esse mesmo contêiner, e a sua forma em texto editável. - PAL: a tabela de cores necessária para renderizar os quadros de um GAF.
- PCX: como o TA:K fornece a paleta a um
.tafcom paleta. - 3DO: quando um "sprite" é na verdade um modelo 3D texturizado (a maioria das explosões é GAF; a maioria dos projéteis é 3DO + clarão do disparo em GAF).
- Glossário: sub-frame (subquadro), origin (origem), tick.