GAF — Graphics Animation Format
GAF memorizza animazioni di sprite a colori indicizzati. Quasi ogni elemento 2D animato di Total Annihilation è un GAF: cursori, puntatori del mouse, esplosioni, parti delle unità, alberi che ondeggiano, icone delle armi, bolle di lava, e via dicendo. TA: Kingdoms riutilizza lo stesso contenitore con l'estensione
.tafper le sue animazioni con palette (la palette viene presa da un file di accompagnamento). TA:K distribuisce anche una variante.taftruecolor che memorizza pixel ARGB a 16 bit invece di indici: quella ha una pagina tutta sua, TAF / TSF.
Tip
Provalo tu stesso.
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
Consulta il riferimento CLI di kbot gaf
per tutte le opzioni.
Da Go. Usa 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))
}
In breve
┌─ 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 ─┐
└──────────────────────────────────┘
Un GAF è un array piatto di sequenze; ogni sequenza è un array di frame; ogni frame è una bitmap larghezza × altezza di indici della palette, con un punto di origine e (facoltativamente) run di trasparenza in stile RLE. I frame possono contenere dei "sotto-frame", così che un singolo passo logico dell'animazione sia la composizione di più livelli sovrapposti.
Intestazione (12 byte)
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 | Note |
|---|---|
Version |
0x00010100 per tutti gli strumenti di terze parti. anims/terrain.gaf e anims/vismasks.gaf di Cavedog sono distribuiti con Version == 0. Il gioco non controlla questa parola; kbot legge qualsiasi valore (uno non riconosciuto viene segnalato come avviso) e scrive 0x00010100. |
SequenceCount |
È seguito subito da SequenceCount × uint32 offset assoluti nel file, ciascuno dei quali punta a un SequenceHeader. Il gioco usa solo i 16 bit bassi, letti come valore con segno: da 1 a 32767 sequenze, nessuna per zero o per un valore negativo. |
Unknown1 |
Sempre zero; in scrittura usa zero. |
Sequenza (40 byte)
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;
Il gioco ripete in ciclo una sequenza quando il byte basso di LoopFlags è
impostato, e la riproduce una volta sola (fermandosi poi sull'ultimo frame)
quando vale 0. Tutte le sequenze originali contengono 1. kbot conserva
entrambe le parole quando riscrive un file, e kbot gaf build assegna a una
sequenza senza sequence.csv il valore originale 1.
Seguono FrameCount × FrameListItem:
typedef struct {
uint32 PtrFrameInfo; // → FrameInfo
uint32 Duration; // Game ticks (30 ticks = 1 second); low 16 bits used
} FrameListItem;
Il gioco legge solo i 16 bit bassi di Duration e mostra un frame per
max(Duration, 1) tick del suo orologio di animazione a 30 Hz.
I nomi delle sequenze contano: il gioco cerca le animazioni per nome (ad es.
UnitInfo fa riferimento a corpyro_smoke dallo script corpyro.gaf).
Quando li confronti, trattali come ASCII senza distinguere maiuscole e
minuscole.
Frame (info di 24 byte + payload)
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 descrivono il punto di ancoraggio usato quando il frame
viene copiato (blit) nel mondo. Per lo sprite di un'esplosione l'origine è di
solito il centro dello scoppio; per un cursore è il pixel del suo hotspot.
Frame a livelli
Quando LayerCount > 0, PtrFrameData è un array di LayerCount × uint32
puntatori a FrameInfo annidati, non a dati di pixel. Ogni livello viene
disegnato nell'ordine dell'array (dal fondo verso il primo piano) con il suo
hotspot sull'hotspot del frame esterno. Un livello con il byte +11
impostato viene disegnato attraverso la tabella di traslucenza invece di
essere copiato (nessun file originale lo imposta). Il byte +10 e il byte
+11 sono campi separati: leggerli come un unico conteggio a 16 bit
trasforma un byte +11 impostato in 256 livelli.
Dati dei pixel
TransparencyIndex vale per singolo frame, non per tutto il file: il GAF
dei cursori, per esempio, usa l'indice 9 (il rosa di sistema) in alcuni frame
e l'indice 0 in altri.
Non compresso (Compressed == 0)
Width × Height byte, dall'alto in basso, da sinistra a destra. Il gioco
disegna ogni pixel tranne quelli uguali a TransparencyIndex. Le texture
delle unità (textures/*.gaf) e le maschere di visibilità
(anims/vismasks.gaf) vengono lette come semplici array di pixel, quindi i
loro frame devono essere grezzi; lo sono tutti i frame originali di quei
file.
Compresso (Compressed != 0) — Run-Length Encoding di TA
I dati dei pixel sono una sequenza di Height righe indipendenti. Ogni riga
inizia con un uint16 lineLength seguito da un flusso di byte codificato
RLE, lungo esattamente lineLength byte. All'interno di quel flusso,
ogni blocco inizia con un byte di maschera i cui due bit bassi scelgono il
tipo di blocco:
| Test sulla maschera | Tipo di blocco | Lunghezza (pixel) | Byte che seguono |
|---|---|---|---|
mask & 0x01 |
Run trasparente | count = mask >> 1 |
(nessuno: i pixel vengono saltati) |
mask & 0x02 |
Run ripetuto | count = (mask >> 2) + 1 |
1 byte: l'indice di colore da ripetere |
| nessuno dei due | Copia letterale | count = (mask >> 2) + 1 |
count byte: i pixel letterali |
Le lunghezze massime per blocco sono quindi 127 pixel trasparenti, 64 pixel
ripetuti e 64 pixel letterali. Solo i pixel saltati sono trasparenti: un
pixel ripetuto o letterale viene disegnato anche quando il suo valore è
uguale a TransparencyIndex, e l'indice 0 della palette è un normale nero
opaco. Se la riga codificata finisce prima di Width, kbot la completa con
pixel trasparenti e lo segnala; il gioco continuerebbe a leggere nei byte
successivi. I livelli possono essere più piccoli del frame esterno e vengono
posizionati con il proprio OriginX/Y.
Riferimento: formats/gaf/gaf.go readCompressed
e formats/gaf/writer.go compressRow.
Important
Un GAF non contiene la palette. Il file memorizza solo indici a 8 bit. Per
visualizzarlo a colori ti serve la palette globale di TA
(palettes/palette.pal) oppure, per TA: Kingdoms, una palette per singolo
asset che si trova fuori dal GAF. Vedi [Risoluzione della palette in TA: Kingdoms]
(#ta-kingdoms--palette-resolution) e Stranezza della trasparenza in TA:
Kingdoms per i meccanismi specifici di
TAK. kbot gaf export usa per impostazione predefinita la palette di TA
incorporata e la regola di trasparenza del gioco; entrambe si possono
sostituire.
TA: Kingdoms — risoluzione della palette
Note
Questa sezione riguarda solo le differenze di TA:K. Il renderer di TA
usa sempre palettes/palette.pal; la struttura dei byte di un GAF è
identica tra TA e TAK, ma TAK ha introdotto una pipeline di palette per
singolo asset.
TA: Kingdoms distribuisce lo stesso contenitore .gaf (a volte .taf) di
TA, ma prende la palette da un file di accompagnamento proprio di ciascun
asset. L'installazione originale di TAK segue tre convenzioni documentate, in
ordine di priorità:
Un
.pcxcon lo stesso nome accanto al.gaf. Gli asset dell'interfaccia e delle animazioni sono accompagnati da un file PCX 1×1 con la palette desiderata incorporata come chunk PLTE del PCX.anims/actionbuttons.gafviene visualizzato conanims/actionbuttons.pcx. Circa il 38% dei fileanims/*.gafdi TAK ha un file di accompagnamento del genere.Un
.palcon lo stesso nome di base inpalettes/. Raro; riguarda una manciata di asset originali per cui Cavedog ha esportato una palette grezza di 1024 byte.Palette della fazione in base al prefisso del nome del file. I GAF di unità ed elementi hanno un prefisso di fazione di 3 lettere e usano la palette della fazione corrispondente:
Prefisso Fazione File della palette 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 nessuna di queste regole trova una palette, il risolutore ripiega su
palettes/palette.pal e infine sulla palette di TA incorporata; entrambe sono
identiche byte per byte nel TAK originale e uguali a quella di TA, quindi per
i GAF di unità ed elementi che non corrispondono a un prefisso di fazione i
colori saranno sbagliati senza una sostituzione esplicita.
Il risolutore di kbot
internal/palettepick
implementa la catena qui sopra. Ogni punto d'ingresso che visualizza un GAF
la utilizza:
- Esploratore web. Le route
/png/…e/apng/…rilevano automaticamente la palette per impostazione predefinita; il visualizzatore GAF offre un menu a tendina con tutte le palette candidate trovate dal risolutore, più una voceauto. Passa?palette=<vfs-path>per imporne una specifica (il menu a tendina lo fa per te). kbot gaf export/kbot gaf dump. Passa--palette <path.pcx|path.pal>per sostituirla; altrimenti viene usata la palette di TA incorporata (la CLI non esegue il risolutore sul VFS perché non ha un contesto VFS, a meno che tu non la avvolga conkbot mount).- Strumento MCP
gaf_export. Accetta un argomento facoltativopalette(un percorso all'interno della cartellagame-dataconfigurata); se lo ometti, esegue il risolutore completo.
Portare un GAF di TA in TA: Kingdoms
Le strutture dei byte dei GAF di TA e di TAK sono identiche: cambia solo la
provenienza della palette. Per portare un .gaf di TA esistente:
- Crea un PCX 1×1 con palette in cui sia incorporata quella di TA.
- Salvalo accanto al GAF con lo stesso nome di base (ad es.
MyGaf.gaf+MyGaf.pcx).
Questo basta perché TAK lo visualizzi con i colori originali di TA.
TA: Kingdoms — stranezza della trasparenza
Note
Questa sezione riguarda solo le differenze di TA:K. Il byte
TransparencyIndex nella sezione Frame fa
fede per il renderer di TA, ma gli autori dei contenuti di TAK non lo hanno
mantenuto allineato ai dati dei pixel effettivi nei frame non compressi.
Molti GAF di TAK usati come atlanti di texture (tutto ciò che sta sotto
textures/*.gaf e una parte di anims/*.gaf) contengono un frame non
compresso in cui il metadato TransparencyIndex riporta una costante (di
solito 9) che non compare mai nei dati dei pixel, mentre l'artista
ha riempito il bordo "trasparente" con un altro indice della palette (spesso
5). Una lettura letterale del metadato produce uno sfondo colorato opaco
dove il renderer di TA avrebbe prodotto trasparenza.
Il renderer di TA non tira mai a indovinare: usa la chiave memorizzata per i
frame grezzi e i comandi di salto per quelli compressi. Per gli atlanti di
TAK, kbot offre un'euristica di rilevamento degli angoli da attivare
esplicitamente (TransparencyModeHeuristic, vedi
EffectiveTransparencyIndex in formats/gaf di kbot-io), che si applica
solo ai frame grezzi:
- Se
TransparencyIndexcompare in un punto qualsiasi del buffer dei pixel, fidati del metadato. - Altrimenti campiona i quattro angoli. Se tutti e quattro concordano su un valore, usa quel valore come indice trasparente effettivo. Così si recuperano i frame non compressi di TAK in cui l'artista ha disegnato un bordo uniforme.
- Altrimenti ripiega sul
TransparencyIndexmemorizzato su disco (il che preserva la visualizzazione delle texture che arrivano fino ai bordi, comedungwormB1, i cui angoli sono pixel dell'unità: il rilevamento automatico, giustamente, rinuncia a sostituirlo).
I frame compressi e quelli composti usano sempre i loro pixel saltati. Il byte su disco non viene mai sovrascritto: chi riscrive il file in round-trip ritrova il valore originale. Un frame di TA completamente opaco con un bordo uniforme perderebbe quel bordo con l'euristica, quindi il rendering per TA non la usa mai.
Sostituire la trasparenza al momento del rendering
gaf.RenderOptions espone queste modalità:
| Modalità | Comportamento | Caso d'uso |
|---|---|---|
TransparencyModeAuto / TransparencyModeMetadata |
La regola del gioco: i pixel chiave di un frame grezzo e i pixel saltati di un frame compresso sono trasparenti; l'indice 0 è nero opaco (il valore zero, e l'impostazione predefinita in tutto kbot per TA). | Visualizzazione, dump, round-trip. |
TransparencyModeHeuristic |
Ipotesi basata sugli angoli per i frame grezzi (vedi sopra), altrimenti la regola del gioco. È quella che lo studio usa per gli sprite di TA: Kingdoms. | Atlanti di texture di TAK. |
TransparencyModeNone |
Ogni pixel è opaco. | Esaminare il colore chiave. |
TransparencyModeIndex |
Usa un indice fornito da chi chiama. | Sostituzione dall'interfaccia; debug. |
Ogni voce della palette esportata è opaca tranne uno slot trasparente per esportazione, normalmente la chiave del frame. Quando un frame disegna pixel il cui valore è uguale alla sua chiave (o i frame di una stessa animazione usano come colori le chiavi l'uno dell'altro), lo slot diventa invece un indice che nessun pixel disegnato usa, così l'immagine conserva ogni indice della palette.
Il visualizzatore GAF dell'esploratore di asset offre Default / Game rule / Corner guess / None in un menu a tendina accanto al selettore della palette,
e passa la scelta come ?transparency=game|heuristic|none|<N> in ogni
richiesta PNG/GIF/APNG. Il valore predefinito è la regola del gioco per TA e
l'ipotesi basata sugli angoli per le installazioni di TA: Kingdoms.
La chiave della cache include l'etichetta della trasparenza, quindi
cambiando modalità non vengono serviti rendering obsoleti.
Lo strumento MCP gaf_export segue la stessa impostazione predefinita: la
regola del gioco, salvo per una cartella game-data registrata come TA:
Kingdoms (un contesto kbot takingdoms), che usa l'ipotesi basata sugli
angoli; il suo risultato indica la modalità usata. kbot gaf export legge un
semplice file senza contesto di gioco, quindi usa per impostazione
predefinita la regola del gioco; passa --transparency heuristic per gli
atlanti di TA: Kingdoms. kbot gaf dump usa sempre la regola del gioco,
perché kbot gaf build rilegge le chiavi memorizzate dalle immagini.
Esempio svolto — 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
...
Un tipico frame di 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)
Costruire un GAF
kbot gaf dump scrive una struttura di directory che kbot gaf build sa
rileggere:
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
Le immagini dei frame sono PNG indicizzati (o GIF con --format gif)
disegnati con la regola del gioco, così ogni pixel conserva il suo indice
della palette e solo i pixel trasparenti sono marcati. La fase di build
ricopia quegli indici e trasforma i pixel trasparenti nella chiave del frame;
le immagini RGBA vengono invece associate al colore più vicino della palette,
evitando la chiave.
sequence.csvripristina l'ordine delle sequenze, il nome esatto, la sua parola di loop e la sua parola +4. Senza questo file, il nome della cartella diventa il nome della sequenza e la sequenza va in loop, come ogni sequenza originale.storage(rawocompressed) eblend(il byte +11) ripristinano l'intestazione di ciascun frame. I frame senza valore distoragevengono compressi, tranne quelli scritti intextures/*.gafoanims/vismasks.gaf, che sono sempre grezzi perché il gioco legge quei file come semplici array di pixel.--storage raw|compressedsostituisce entrambi.- I frame composti (a livelli) vengono esportati come un'unica immagine
appiattita e ricostruiti come frame semplici;
kbot gaf roundtripriporta quanti sono. - Un frame senza pixel (larghezza o altezza 0) non ha un'immagine; basta la
sua riga in
frames.csvper ricostruirlo. - Una sottocartella senza
frames.csvnésequence.csvviene saltata con un avviso. Qualsiasi altra sottocartella che non si può costruire (un'immagine mancante o illeggibile, un valore CSV non valido) interrompe la build, così una sequenza non viene mai esclusa in silenzio.
Note
I GAF di TA e TA:K sono identici byte per byte. Il contenitore è identico; cambia solo la pipeline della palette. Vedi Risoluzione della palette in TA: Kingdoms più sopra per la catena completa e la procedura passo passo per portare un GAF in TAK.
Insidie
Warning
Una sequenza il cui byte di loop vale 0 viene riprodotta una volta e si ferma. Gli strumenti che scrivono la parola +2 a zero congelano ogni elemento animato che toccano. kbot conserva la parola, e le nuove sequenze costruite da kbot vanno in loop.
- Alcune sequenze sono distribuite con durate dei frame pari a
0. Il gioco mostra un frame del genere per un tick, come con una durata di 1. Non "correggerle" con un valore predefinito. - Le righe di pixel compresse possono essere più corte di
Width. Il gioco continua a leggere i byte successivi come comandi; kbot completa la riga con pixel trasparenti e lo segnala. Nessun file originale ha righe del genere. - Le origini dei livelli sono hotspot: ogni livello viene posizionato in modo che la sua origine cada sull'origine del frame esterno.
- Il
.tafdi TA: Kingdoms esiste in due varianti. kbot legge un.tafcon palette come un normale GAF, ma TA:K distribuisce anche file.taftruecolor (pixel ARGB a 16 bit, senza palette). Controlla il byte del formato del frame per distinguerli e vedi TAF / TSF per la variante truecolor. anims/terrain.gafeanims/vismasks.gafhannoVersion == 0. Il gioco ignora la parola di versione.- Il numero di frame per sequenza non è limitato:
cursorteleportha 46 frame. Non presupporre un tetto fisso quando dimensioni i buffer.
Dimensioni tipiche
| Elemento | Intervallo osservato nei GAF di Cavedog |
|---|---|
anims/cursors.gaf |
~30 KB, 22 sequenze, da 1 a 46 frame ciascuna |
| Parte di un'unità (ad es. lampo di sparo) | 5–20 KB, 1 sequenza, 4–12 frame |
Esplosione (famiglia di anims/explosions.gaf) |
50–250 KB per file |
anims/textures.gaf (il foglio di texture principale) |
~12 MB (il GAF più grande di TA) |
anims/terrain.gaf |
~1,5 MB |
| Dati per frame | Di solito <1 KB; i frame grandi (esplosioni 256×256) arrivano a oltre 10 KB |
Esempi reali nel catalogo di riferimento
GAF degni di nota nell'installazione di TA: estraine uno qualsiasi con
kbot hpi extract e poi usa kbot gaf list / kbot gaf export:
anims/cursors.gaf: 22 sequenze, ciascuna un'animazione di un cursore dell'interfaccia. Le sequenze più piccole (cursorgrn,cursorred) sono frame singoli; la più grande (cursorteleport) ha 46 frame in 460 tick. Mostrato nell'esempio svolto qui sopra.anims/textures.gaf: ~12 MB, il foglio di tile principale a cui fanno riferimento le stringheOffsetToTextureNamedi ogni modello 3DO. Sfoglialo conkbot gaf list anims/textures.gaf | head.- Famiglia di
anims/explosions.gaf: tipici GAF di effetti (lasersht.gaf,acid.gaf,bigexpls.gaf), ciascuno con più sequenze di esplosioni con nome. anims/armcom.gaf/corcom.gaf: parti grafiche del comandante di ciascuna fazione (cerchio di selezione, laser di costruzione).anims/terrain.gaf+anims/vismasks.gaf: gli unici GAF originali conVersion == 0nell'intestazione (utile saperlo quando scrivi un parser).
Vedi anche
- TAF / TSF: la variante
.taftruecolor di TA: Kingdoms, costruita su questo stesso contenitore, più la sua forma testuale modificabile. - PAL: la tabella dei colori necessaria per visualizzare i frame GAF.
- PCX: come TA:K fornisce una palette a un
.tafcon palette. - 3DO: quando uno "sprite" è in realtà un modello 3D con texture (la maggior parte delle esplosioni è GAF; la maggior parte dei proiettili è 3DO + lampo di sparo GAF).
- Glossario: sotto-frame (sub-frame), origine (origin), tick.