COB / BOS — Bytecode di scripting delle unità
Ogni unità di Total Annihilation esegue uno script che anima i suoi pezzi (piece), emette i lampi delle armi, riproduce effetti sonori e risponde agli eventi di gioco. Il linguaggio sorgente è BOS ("Basic Object Script"); il bytecode compilato che il motore esegue davvero è COB ("Cobble", il gioco di parole di Cavedog per "compiled BOS"). Un file per unità, chiamato come l'
ObjectNamedell'unità nel suo FBI.
Tip
Provalo tu stesso.
# Decompile a unit's bytecode back to readable BOS
kbot cob decompile scripts/ARMCOM.cob
# Disassemble to opcode-level assembly with control-flow arrows
kbot cob disassemble scripts/ARMCOM.cob -a
# Lint a whole directory for common script issues
kbot cob lint scripts/ -q
# Round-trip the entire scripts/ tree to verify byte-perfect compile
kbot cob roundtrip scripts/
Consulta il riferimento CLI di kbot cob
per tutte le opzioni (e la tabella completa delle regole di lint).
Da Go. Usa formats/scripting di kbot-io:
import "github.com/coreprime/kbot-io/formats/scripting"
cob, _ := scripting.LoadFromFile("scripts/ARMCOM.cob")
fmt.Printf("%d scripts, %d pieces\n", cob.NumScripts, cob.NumPieces)
for i, name := range cob.ScriptNames {
fmt.Printf(" [%d] %s\n", i, name)
}
Due linguaggi, un solo formato di file
| Fase | Formato | Strumenti |
|---|---|---|
| Sorgente | BOS — simile al C, strutturato | Scritto a mano; Scriptor di Switeck; kbot cob decompile lo ricostruisce dal COB in round-trip |
| Compilato | COB — bytecode basato su stack | Caricato dal motore; prodotto da Scriptor o da kbot cob compile |
Il motore vede soltanto il COB. Il BOS è una comodità per gli autori umani:
quando "modifichi lo script di un'unità" in realtà modifichi il sorgente BOS,
lo ricompili in COB e distribuisci il COB nel .ufo aggiuntivo dell'unità.
Un esempio minimo:
// armcom.bos — fragment from the Arm Commander
piece base, pelvis, lthigh, lleg;
#define SIG_WALK 2
Create() {
hide pelvis;
set-signal-mask SIG_WALK;
}
Walk() {
set-signal-mask SIG_WALK;
while (1) {
turn lthigh to x-axis [-25] speed [50];
turn lleg to x-axis [50] speed [100];
wait-for-turn lthigh around x-axis;
sleep 100;
}
}
kbot cob compile traduce tutto questo in un blob binario. kbot cob disassemble -a mostra il flusso di opcode affiancato da frecce del flusso di
controllo; kbot cob decompile fa l'operazione inversa e recupera un BOS che
quasi sempre supera il round-trip identico byte per byte (kbot cob roundtrip lo verifica sull'intero catalogo del gioco).
Un sorgente viene compilato come script di TA (COB versione 4), a meno che
non inizi con .version 6. Il compilatore rifiuta ciò che TA 3.1c eseguirebbe
in modo errato invece di scrivere un segnaposto: i costrutti di TA: Kingdoms
(play-sound, Mission-Command, __tak_math_*, .sound_name) e % sono
errori in uno script di TA (TA non ha un'istruzione di modulo, e
0x10037000, la parola in cui veniva tradotto %, è il suo XOR bit a bit),
così come gli identificatori sconosciuti e le funzioni che richiedono più dei
32 slot di stack del gioco. Ciò che compila comunque ma merita un'occhiata
(una funzione definita due volte: ogni chiamata si lega alla prima
definizione, come fa la ricerca per nome del gioco) viene stampato come
avviso, insieme a ogni segnalazione delle
regole di lint di compatibilità con TA; --strict fa fallire il
comando in presenza di avvisi. kbot cob assemble applica gli stessi
controlli ai listati scritti a mano.
Struttura su disco del COB
Important
Due dialetti. Questa sezione descrive COB v4, il dialetto
distribuito con Total Annihilation. TA: Kingdoms usa COB v6
(VersionSignature == 6), che mantiene tutto quanto segue ma allunga
l'intestazione di 8 byte e inserisce un'ulteriore tabella di offset delle
stringhe di comando dopo l'array dei nomi dei pezzi. Vedi
TA: Kingdoms — differenze di COB v6 per i
dettagli di v6; l'elenco degli opcode vale per entrambi, con gli opcode
aggiuntivi esclusivi di TAK documentati
nell'Appendice C.
typedef struct { // 44-byte header (11 × uint32)
uint32 VersionSignature; // 4 for TA, 6 for TA: Kingdoms
uint32 NumScripts; // Number of functions (Create, Killed, …)
uint32 NumPieces; // Number of named pieces
uint32 LengthOfScripts; // Total code size, in DWORDs
uint32 NumberOfStaticVars; // Static-variable count
uint32 UKZero; // Always 0 in retail bytecode; purpose unknown
uint32 OffsetToScriptCodeIndexArray; // → uint32 × NumScripts: code-index per script
uint32 OffsetToScriptNameOffsetArray; // → uint32 × NumScripts: absolute offsets into string pool
uint32 OffsetToPieceNameOffsetArray; // → uint32 × NumPieces: absolute offsets into string pool
uint32 OffsetToScriptCode; // → packed uint32 opcode stream
uint32 OffsetToNameArray; // Byte just past the piece-name offset array (= sound-name offset table start for TAK v6, or string-pool start for TA v4)
} COBHeader;
Note
I nomi canonici dei campi corrispondono alla struct HEADER_COB di
Scriptor. Le revisioni precedenti di kbot chiamavano LengthOfScripts /
NumberOfStaticVars / UKZero / OffsetToNameArray "Unknown0/1/2/3":
stessi offset, solo con le identità documentate ora applicate.
Seguono poi, in questa disposizione:
┌─ Script code ─────────────────────────────┐ uint32 opcodes/operands, packed
├─ Script code-index table ─────────────────┤ uint32[NumScripts] — *not* byte offsets
├─ Script name offset table ────────────────┤ uint32[NumScripts]
├─ Piece name offset table ─────────────────┤ uint32[NumPieces]
└─ String pool ─────────────────────────────┘ NUL-terminated names, in order
Risolvere uno script
La tabella degli indici del codice contiene offset in parole uint32,
non offset in byte. Per trovare il bytecode dello script numero i:
uint32 first_instr_word = ScriptCodeIndexArray[i];
uint32 byte_offset = OffsetToScriptCode + first_instr_word * 4;
Lo script finisce dove comincia il successivo (oppure all'inizio della tabella degli indici, per l'ultimo script).
Important
Gli offset delle stringhe nelle tabelle dei nomi sono offset assoluti nel file, non relativi al pool di stringhe. Gli strumenti di Cavedog non spostavano mai il pool di stringhe, quindi in pratica la cosa non aveva importanza; se riorganizzi un COB a mano, devi aggiornare ogni offset.
Il flusso di opcode
COB è una macchina virtuale basata su stack. La maggior parte degli opcode estrae gli argomenti da uno stack di dati proprio di ciascuno script, fa il suo lavoro e vi inserisce un risultato. Alcuni opcode hanno operandi inline (destinazioni dei salti, numeri di pezzo).
Ogni opcode è un uint32 con questa struttura:
MSB LSB
┌─────────┬─────────┬─────────┬─────────┐
│ 0x10 │ family │ op │ flags │
└─────────┴─────────┴─────────┴─────────┘
Il byte alto è sempre 0x10; famiglia, operazione e flag variano da opcode a
opcode. Il gioco non confronta l'intera parola: smista in base a
word & 0x100FF000, quindi i bit fuori da quella maschera vengono ignorati e
0x10064001 viene eseguito come JUMP (il disassemblatore di kbot scrive una
parola del genere come JUMP@0x10064001, così si riassembla identica). PUSH
(0x10021000) e POP (0x10023000) leggono poi la loro sorgente o
destinazione dai tre bit bassi. Il catalogo è
scripting/opcodes.go di kbot-io;
scripting.Opcodes() elenca ogni istruzione con i suoi operandi inline e il
suo effetto sullo stack.
Famiglie di opcode
| Famiglia | Esempi | Note |
|---|---|---|
0x100x |
MOVE, TURN, SPIN, SHOW, HIDE, EMIT_SFX |
Animazione dei pezzi. Il numero del pezzo e quello dell'asse seguono inline. |
0x101x |
WAIT_FOR_TURN, WAIT_FOR_MOVE, SLEEP |
Barriere asincrone: lo script cede il controllo finché la condizione non è soddisfatta. |
0x102x |
PUSH_CONST, PUSH_LOCAL, PUSH_STATIC, POP_* |
Manipolazione dello stack. |
0x103x |
ADD, SUB, MUL, DIV, BITWISE_AND, BITWISE_OR, XOR, NOT |
Aritmetica e operazioni bit a bit. Non c'è il modulo. |
0x104x |
RAND, GET_UNIT_VALUE, GET, IS_CARRYING_UNIT, CARRIER_UNIT_ID |
Interrogazioni al motore. |
0x105x |
<, <=, >, >=, ==, !=, &&, ||, XOR_ALT, ! |
Confronti e operazioni logiche. XOR_ALT è uno XOR bit a bit, non logico. |
0x106x |
START_SCRIPT, CALL_SCRIPT, DISCARD_CALL, JUMP, RETURN, JUMP_IF_FALSE, SIGNAL, SET_SIGNAL_MASK |
Flusso di controllo e concorrenza. |
0x107x |
EXPLODE, PLAY_SOUND |
Effetti speciali. PLAY_SOUND esiste solo in TA: Kingdoms. |
0x108x |
SET_VALUE, ATTACH_UNIT, DROP_UNIT |
Modifiche allo stato del motore. |
Un campionario piccolo ma rappresentativo:
0x10021001 PUSH_CONST <value> ( -- v )
0x10021002 PUSH_LOCAL <var#> ( -- v )
0x10023002 POP_LOCAL <var#> ( v -- )
0x10033000 MUL ( a b -- a*b )
0x10051000 LESS_THAN ( a b -- a<b )
0x10066000 JUMP_IF_FALSE <byteOffset> ( v -- ) pops; falls through if v != 0
0x10064000 JUMP <byteOffset> ( -- )
0x10065000 RETURN ( v -- ) pops the return value
0x10001000 MOVE <piece> <axis> ( speed distance -- )
0x10003000 SPIN <piece> <axis> ( speed -- )
0x1000F000 EMIT_SFX <piece> ( flags -- )
0x10068000 SET_SIGNAL_MASK ( mask -- )
Note
Gli offset dei salti sono offset in byte relativi all'inizio della
sezione del codice degli script (non all'istruzione corrente). JUMP 0x4C
significa "salta a OffsetToScriptCode + 0x4C". L'opzione -a del
disassemblatore disegna frecce tra le istruzioni corrispondenti per
renderlo evidente.
Modello di concorrenza
Un'unità può avere molti script in esecuzione contemporaneamente: Walk, Aim, SmokeUnit, MoveWatcher, ecc. Sono schedulati in modo cooperativo:
sleep <ticks>cede il controllo per N tick (1/30 s ciascuno).wait-for-turn <piece> around <axis>cede il controllo finché quel pezzo non completa l'animazioneturnin corso. Lo stesso vale perwait-for-move.signal <n>termina ogni script per cuisignal_mask & n != 0.set-signal-mask <n>dichiara a quali segnali è interessato lo script corrente.
La maschera dei segnali (signal mask) è il modo in cui gli script realizzano
la prelazione (preemption): MoveWatcher imposta la maschera SIG_MOVE e
sorveglia la velocità; quando l'unità smette di muoversi invia SIG_MOVE,
terminando in modo pulito le animazioni di camminata prima di avviare la
posa di riposo.
Warning
SET_SIGNAL_MASK registra soltanto un interesse: non termina nulla. È
l'invio di signal X che ferma gli script con la maschera X impostata. La
regola di lint signal-never-signalled di kbot cob lint individua gli
script che impostano una maschera che nessuno segnala mai (il che è quasi
sempre un bug).
Come si svolge davvero la prelazione
Uno schema comune: MoveUnit avvia uno script di animazione Walk; quando il
movimento si ferma, il motore invia SIG_MOVE per fare pulizia prima che
parta Idle. La sequenza:
Engine MoveUnit script Walk script Idle script
│ │ │ │
│ unit ordered to move │ │ │
├─ start-script MoveUnit──▶│ │ │
│ ├─ set-signal-mask │ │
│ │ SIG_MOVE │ │
│ ├─ start-script Walk─▶│ │
│ │ ├─ set-signal-mask │
│ │ │ SIG_MOVE │
│ │ │ (loops: │
│ │ │ turn pieces, │
│ │ │ sleep, repeat) │
│ │ │ │
│ movement completes │ │ │
├─ engine sets velocity=0 │ │ │
│ ├─ signal SIG_MOVE ──┐│ │
│ │ ││ │
│ ◀─── KILLED ────────────────────────────── ◀┤ │
│ ◀─── KILLED ──────────┤ │ │
│ │
├─ start-script Idle ─────────────────────────────────────────────▶│
│ │ (loops)
Tre cose da notare:
- Entrambi gli script che impostano
SIG_MOVEvengono terminati quando il segnale scatta, compresoMoveUnitstesso (non importa: il suo lavoro è finito). - Lo script che invia il segnale (
MoveUnit) sopravvive abbastanza a lungo da inviare il segnale prima di essere terminato; il runtime controlla le maschere dopo che l'opcodesignalha restituito il controllo. Idlenon impostaSIG_MOVE, quindi il successivosignal SIG_MOVEnon lo tocca.
Esempio svolto — cortruck.cob (Core Construction Truck)
kbot cob decompile scripts/cortruck.cob:
piece base;
SmokeUnit() {
var local_0, local_1, local_2;
while (get BUILD_PERCENT_LEFT) { sleep 400; }
while (1) {
local_0 = get HEALTH;
if (local_0 < 66) {
local_2 = (256 | 2);
if (rand(1, 66) < local_0) { local_2 = (256 | 1); }
emit-sfx local_2 from base;
}
local_1 = (local_0 * 50);
if (local_1 < 200) { local_1 = 200; }
sleep local_1;
}
return 0;
}
Create() { return 0; }
Killed() {
var severity;
if (severity <= 25) { explode base type (32 | 256); return 0; }
if (severity <= 50) { explode base type (32 | 256); return 0; }
if (severity <= 99) { explode base type (32 | 256); return 0; }
explode base type (32 | 256);
}
Il disassemblato corrispondente (kbot cob disassemble -a -s SmokeUnit)
mostra il flusso di opcode effettivo, con le frecce di JUMP_IF_FALSE/JUMP
che attraversano i corpi dei while/if: utile quando l'output del
decompilatore è ambiguo.
Linting
kbot cob lint (e la scheda Lint dell'esploratore di asset) esegue l'insieme
di regole predefinito di kbot-io: regole di stile ricavate da bug reali negli
script di Cavedog stessi e da patch di mod inviate dalla comunità, due
controlli a livello di file e, per ogni COB che non dichiara la versione 6, le
regole di compatibilità con TA 3.1c. Il catalogo delle regole di stile:
| Regola | Gravità | Individua |
|---|---|---|
unused-piece |
warning | Pezzo dichiarato ma mai usato in alcuno script. |
unused-static / unused-local |
warning | Variabile allocata ma mai letta. |
always-true / dead-code |
info / warning | if(1) / while(0). |
long-function / high-complexity |
warning | > 100 istruzioni o complessità ciclomatica > 15. |
invalid-call |
error | call-script verso una funzione inesistente. |
speed-zero |
warning | move/turn con speed <0>: l'animazione non termina mai. |
empty-function |
info | Il corpo della funzione è solo return 0. |
duplicate-animation |
warning | Due comandi di animazione identici uno dopo l'altro. |
sleep-only-guard |
info | Il blocco if non contiene altro che sleep. |
duplicate-if |
info | Due if di seguito con la stessa condizione. |
raw-signal |
info | signal 4 invece di signal SIG_AIM. |
unnamed-global |
info | global_3 invece di una variabile statica con nome. |
signal-never-signalled |
warning | Lo script maschera un segnale che nessuno emette. |
recursive-call |
warning | Ciclo di call-script. |
duplicate-function |
warning | Due script hanno lo stesso nome; il gioco chiama solo il primo. |
malformed-cob |
error / warning | Il file non si carica, o il codice di uno script è troncato (errore); tabelle dei nomi che il lettore ha tollerato (avviso). |
Le regole di compatibilità con TA segnalano ciò su cui TA 3.1c va in errore o
che esegue in modo diverso. Sono errori, compaiono nell'output SARIF di --ci
come le altre e saltano i file di TA: Kingdoms (versione 6):
| Regola | Individua |
|---|---|
ta-kingdoms-opcode |
PLAY_SOUND, MISSION_COMMAND o un operatore TAK_MATH_* in uno script di TA. TA non ha un gestore per questi, e lo script va in errore quando ne raggiunge uno. |
ta-unknown-opcode |
Una parola di opcode che nessun gioco esegue. |
ta-push-flags |
Un PUSH i cui bit bassi non valgono 1 (costante), 2 (locale) o 4 (statica), o un POP i cui bit bassi non valgono 2 o 4, compresi i 0x10021000 e 0x10021008 senza flag scritti da alcuni strumenti. |
ta-stack-limit |
Più di 32 STACK_ALLOC, oppure variabili locali più valori in sospeso oltre i 32 slot di stack del gioco; il gioco non controlla e sovrascrive la memoria che segue lo script. |
ta-get-arguments |
Un GET raggiunto con meno di cinque valori in sospeso. GET estrae sempre una porta e quattro argomenti, quindi un get con un solo argomento prende il resto dalle variabili locali della funzione e le sovrascrive. |
ta-stack-underflow |
Qualsiasi altra istruzione che estrae più valori di quelli in sospeso, o percorsi che si ricongiungono con profondità di stack diverse. |
ta-discard-call |
DISCARD_CALL con più di quattro argomenti. |
Le descrizioni complete delle regole sono nel README di kbot. Il decompilatore conserva di proposito la numerazione originale di variabili e pezzi, così un ciclo decompilazione→lint→correzione→compilazione mette in evidenza le mod che hanno bisogno di manutenzione.
Insidie
Warning
call-script è 0x10062000. 0x10063000 è un'istruzione diversa,
DISCARD_CALL: ha le stesse due parole inline (numero dello script e
numero di argomenti), ma estrae gli argomenti e non esegue nulla. Nessuno
script originale la usa; il decompilatore di kbot la scrive come
__discard_call(word, args…).
Warning
Diverse tabelle di opcode pubblicate danno nomi sbagliati alle parole bit
a bit. TA esegue 0x10037000 come XOR bit a bit (non come modulo: TA
non ha un'istruzione di modulo), 0x10038000 come NOT bit a bit unario
(non come XOR) e 0x10059000 come un secondo XOR bit a bit (non uno XOR
logico: 6 XOR_ALT 3 vale 5). 0x1003A000 è un operatore di TA: Kingdoms su
cui TA va in errore. I listati scritti con i vecchi nomi (MOD,
BITWISE_XOR, BITWISE_NOT, LOGICAL_XOR) si assemblano ancora nelle
parole da cui sono stati ricavati.
- Tutti gli offset nell'intestazione del COB sono offset assoluti nel
file, ma le voci di
ScriptCodeIndexArraysono offset in parole all'interno della sezione del codice. È facile confonderli. - I numeri di pezzo negli opcode sono indici nella tabella dei nomi dei pezzi, non stringhe. Il COB contiene i nomi in un pool di stringhe; il runtime usa gli indici per velocità.
- Gli opcode
get UNIT_VALUEdifferiscono in modo sottile tra TA e TA: Kingdoms. TA: Kingdoms ha aggiunto ID di valore per la rigenerazione della magia, i punti comando, ecc.; la tabella dei valori di TA è più compatta. Il decompilatore di kbot attualmente etichetta l'insieme di TA; i numeri di porta di TAK ≥ 21 vengono emessi come letterali numerici (che superano comunque il round-trip attraverso l'assemblatore). Vedi TA: Kingdoms — differenze di COB v6 per tutte le differenze. - Le variabili sono di due tipi:
local_N(slot di stack locale alla funzione) eglobal_N(statica per unità che persiste tra le chiamate). Il decompilatore emettelocal_N/global_Nper impostazione predefinita; rinominale nel sorgente per renderlo più leggibile. - Lo strumento Scriptor di Cavedog a volte rende inline
CURRENT_SPEEDcome letterale29, rompendo gli script in stileMoveWatcherdopo un round-trip. Il decompilatore di kbot ripristina il nome simbolico; se devi per forza passare da Scriptor, usa le parentesi:(get CURRENT_SPEED) > 5.
Dimensioni tipiche
| Elemento | Intervallo osservato negli scripts/*.cob di Cavedog |
|---|---|
| Dimensione del file | 0,5 KB – 25 KB |
| Overhead dell'intestazione | sempre 44 byte |
| Script per unità | 3 – 30 (unità tipiche: 5–10) |
| Pezzi per unità | 3 – 80 (Krogoth ≈ 80) |
| Dimensione della sezione del codice | 300 B – 20 KB (varia moltissimo con la complessità) |
armcom.cob (comandante) |
~14 KB, 30 script |
cortruck.cob (unità semplice) |
~750 B, 3 script |
TA: Kingdoms — differenze di COB v6
Note
Questa sezione riguarda solo le differenze di TA:K. Tutto ciò che precede descrive COB v4 (TA). TA: Kingdoms mantiene la stessa VM a stack, lo stesso pool di stringhe, lo stesso schema di codifica degli opcode e lo stesso meccanismo dei nomi di script e di pezzi; le differenze sono un'intestazione estesa, una tabella di stringhe aggiuntiva accodata dopo l'array dei nomi dei pezzi e quattro nuovi opcode.
Struttura del file v6
┌─ 44-byte v4 header ──────────────────────────────────┐
│ VersionSignature == 6, NumScripts, NumPieces, … │
├─ Sub-header (8 bytes, TAK-only) ─────────────────────┤ (soundNameOffsetTableStart, soundNameCount)
├─ Script code (uint32 opcode stream) ─────────────────┤ same encoding as v4
├─ ScriptCodeIndexArray (uint32 × NumScripts) ─────────┤ unchanged
├─ ScriptNameOffsetArray (uint32 × NumScripts) ────────┤ unchanged
├─ PieceNameOffsetArray (uint32 × NumPieces) ──────────┤ unchanged
├─ SoundNameOffsetArray (uint32 × Count) ──────────────┤ TAK-only — count from sub-header[1]
└─ String pool ────────────────────────────────────────┘ script names → piece names → sound names (in that order)
La sotto-intestazione di 8 byte si trova tra OffsetToNameArray (fine
dell'intestazione canonica di 11 DWORD, al byte 44) e l'inizio della sezione
del codice (OffsetToScriptCode, di solito 0x34 = 52). Contiene due uint32
little-endian:
| Campo | Significato |
|---|---|
[0] |
Offset assoluto nel file della tabella degli offset dei nomi dei suoni. Sempre uguale a OffsetToPieceNameOffsetArray + NumPieces × 4, cioè il byte appena dopo l'array degli offset dei nomi dei pezzi. Ridondante rispetto alla struttura canonica; lo scrittore lo ricostruisce. |
[1] |
Numero di nomi di suoni nella tabella. Conteggio autorevole. |
Ogni voce della tabella degli offset dei nomi dei suoni è l'offset
assoluto nel file di una stringa terminata da NUL nel pool di stringhe: cose
come "SetMission o 1, s", "create ARASWORD", "ARROW10". Il bytecode le
richiama per indice (non per offset) tramite l'opcode MISSION_COMMAND
(vedi l'Appendice C). La tabella si chiama
array dei nomi dei suoni perché il compilatore canonico Scriptor vi registra
le stringhe tramite l'azione ADDSOUND; il contenuto effettivo, però, sono
stringhe di comando del motore e non nomi di file audio.
La rappresentazione di kbot
scripting.COB conserva i
nomi dei suoni come campo strutturato []string, senza preservare byte
opachi. Tutto il resto (i valori della sotto-intestazione, la tabella degli
offset, gli offset di ogni stringa nel pool) viene ricostruito da questo
elenco in scrittura:
type COB struct {
// …canonical TA fields…
SoundNames []string // TAK-only; nil for v4
}
Per i file v4 la slice è sempre nil, e sia la sotto-intestazione sia la tabella degli offset dei nomi dei suoni vengono omesse dall'output.
Il disassemblatore e il decompilatore emettono ogni stringa come direttiva
.sound_name, così sopravvivono a un round-trip attraverso .coba e .bos:
.version 6
.statics 6
.sound_name "create NPCEMEN"
.sound_name "SetMission o 1, s"
.sound_name "ARROW10"
.script Start
…
Nuovi opcode
I file .cob di TAK usano istruzioni per cui TA 3.1c non ha un gestore
(PLAY_SOUND, MISSION_COMMAND e gli operatori TAK_MATH_*), più
DONT_SHADOW, che TA esegue come no-op. La loro semantica esatta lato gioco
non è documentata, ma la loro struttura binaria (dimensione + numero di
parametri inline) è nota da ogni .cob originale di TAK, e questo basta
perché il round-trip disassemblaggio→assemblaggio recuperi i byte originali
alla lettera. Vedi l'Appendice C per il
riferimento completo.
Stato del round-trip
| Pipeline | TA | TA: Kingdoms |
|---|---|---|
disassemble → assemble (identico byte per byte) |
278 / 278 ✅ | 267 / 267 ✅ |
decompile → compile (identico byte per byte) |
278 / 278 ✅ | Gli script delle unità semplici e molti COB delle missioni (≈ 70 / 267) superano il round-trip identici byte per byte. I fallimenti rimanenti sono schemi di flusso di controllo in cui la ricostruzione strutturata di if/while del decompilatore sceglie una disposizione dei JUMP che il compilatore rigenera in modo leggermente diverso: il bytecode è semanticamente equivalente (ogni opcode di TAK, stringa di comando, sotto-intestazione e la parola di versione v6 sopravvivono), solo non identico byte per byte. Lo stesso tipo di discrepanza colpirebbe qualsiasi ciclo decompilazione→compilazione su uno script non banale. |
Sintassi BOS per le estensioni di TAK
Il decompilatore emette queste pseudo-chiamate perché i COB di TAK superino il
round-trip attraverso il BOS. Il compilatore accetta la stessa sintassi, ma
solo dopo .version 6 (eccetto dont-shadow: TA lo esegue come no-op).
dont-shadow e Mission-Command usano le parole chiave canoniche di TAK di
Scriptor; gli opcode matematici non hanno un nome documentato in Scriptor (li
etichetta ?? e ????), quindi kbot mantiene i segnaposto __tak_math_*:
// Statement-level (emitted on their own line):
dont-shadow(<piece>); // DONT_SHADOW — disables shadow casting for one piece
Mission-Command("sound name", arg1, arg2, …); // MISSION_COMMAND — engine call (statement form drops the return value)
return_val = Mission-Command("sound name", args…); // assignment form keeps the pushed result
// Expression-level (binary operators: two values popped, one pushed):
local_x = __tak_math_09(a, b); // TAK_MATH_09
local_x = __tak_math_0a(a, b); // TAK_MATH_0A
local_x = __tak_math_0b(a, b); // TAK_MATH_0B
Il primo argomento di Mission-Command è il nome del suono come stringa
tra virgolette: il compilatore lo cerca nella tabella dei nomi dei suoni
(quindi la direttiva .sound_name corrispondente deve comparire prima nel
file). Gli argomenti rimanenti vengono inseriti nello stack e il motore li
consuma.
Due direttive in cima al file portano il resto dei metadati v6:
.version 6
.sound_name "create NPCEMEN"
.sound_name "SetMission o 1, s"
.sound_name "ARROW10"
.version viene sempre emessa (così i file v4 di TA superano il round-trip
identici attraverso la stessa pipeline). Ogni .sound_name è una voce della
tabella dei nomi dei suoni del COB; lo scrittore ricostruisce la
sotto-intestazione di 8 byte e la tabella degli offset dei nomi dei suoni a
partire da len(SoundNames) e dall'ordine in cui compaiono le voci.
L'epilogo STACK_ALLOC ; RETURN di TAK, che ogni return; senza valore emette
in v6, è gestito dal compilatore: return; diventa STACK_ALLOC ; RETURN
quando c'è .version 6, mentre return <expr>; diventa <expr> ; RETURN
senza prefisso (come fa il compilatore di Cavedog).
Riproducilo sulla tua installazione:
kbot ctx use kingdoms # or wherever you registered TAK
kbot cob roundtrip
Simboli delle porte oltre la porta 20
Gli script di TA: Kingdoms usano le porte di get_unit_value fino alla 46
(CURRENT_SPEED, VETERAN_LEVEL, campi della riserva di magia, ecc.). Il
decompilatore di kbot attualmente etichetta in modo simbolico solo le porte
1–20 (vedi l'Appendice B); le porte
di TAK ≥ 21 vengono emesse come letterali numerici. Il bytecode le memorizza
comunque come interi, quindi i round-trip non ne risentono: ne soffre solo la
leggibilità del BOS. L'aggiunta dei nomi delle porte di TAK è seguita a parte.
Appendice A — Riferimento completo degli opcode
Ogni istruzione eseguita da TA o da TA: Kingdoms. Fa fede
formats/scripting/opcodes.go
di kbot-io. La notazione dello stack usa lo stile Forth
( before -- after ). La colonna Inline indica quante parole uint32
seguono la parola dell'opcode stesso (consumate durante la decodifica, non
dallo stack).
Animazione — manipolazione dei pezzi (0x100x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10001000 |
MOVE |
2 (piece, axis) |
( speed dist -- ) |
move <piece> to <axis> <dist> speed <speed>; |
0x10002000 |
TURN |
2 (piece, axis) |
( speed dir -- ) |
turn <piece> to <axis> <dir> speed <speed>; |
0x10003000 |
SPIN |
2 (piece, axis) |
( speed -- ) |
spin <piece> around <axis> speed <speed>; |
0x10004000 |
STOP_SPIN |
2 (piece, axis) |
( decel -- ) |
stop-spin <piece> around <axis> [decel <d>]; |
0x10005000 |
SHOW |
1 (piece) |
( -- ) |
show <piece>; |
0x10006000 |
HIDE |
1 (piece) |
( -- ) |
hide <piece>; |
0x10007000 |
CACHE |
1 (piece) |
( -- ) |
cache <piece>; |
0x10008000 |
DONT_CACHE |
1 (piece) |
( -- ) |
dont-cache <piece>; |
0x10009000 |
PIECE_OP_09 |
1 (piece) |
( a b -- ) |
__piece_op_09(<piece>, a, b); — TA lo esegue, ma i suoi script delle unità non ne fanno nulla; nessuno script originale lo usa. |
0x1000A000 |
DONT_SHADOW |
1 (piece) |
( -- ) |
dont-shadow <piece>; — TA lo esegue come no-op; vedi l'Appendice C. |
0x1000B000 |
MOVE_NOW |
2 (piece, axis) |
( pos -- ) |
move <piece> to <axis> <pos> now; |
0x1000C000 |
TURN_NOW |
2 (piece, axis) |
( angle -- ) |
turn <piece> to <axis> <angle> now; |
0x1000D000 |
SHADE |
1 (piece) |
( -- ) |
shade <piece>; |
0x1000E000 |
DONT_SHADE |
1 (piece) |
( -- ) |
dont-shade <piece>; |
0x1000F000 |
EMIT_SFX |
1 (piece) |
( type -- ) |
emit-sfx <type> from <piece>; |
Operazioni di attesa (0x101x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10011000 |
WAIT_FOR_TURN |
2 (piece, axis) |
( -- ) |
wait-for-turn <piece> around <axis>; |
0x10012000 |
WAIT_FOR_MOVE |
2 (piece, axis) |
( -- ) |
wait-for-move <piece> along <axis>; |
0x10013000 |
SLEEP |
0 | ( ticks -- ) |
sleep <ticks>; |
Manipolazione dello stack (0x102x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10021000 |
PUSH_IMM |
1 (value) |
— | PUSH senza flag: il gioco va in errore (ta-push-flags). Il compilatore non lo scrive mai. |
0x10021001 |
PUSH_CONST |
1 (value) |
( -- v ) |
<literal> in qualsiasi espressione |
0x10021002 |
PUSH_LOCAL |
1 (var#) |
( -- v ) |
local_N in qualsiasi espressione |
0x10021004 |
PUSH_STATIC |
1 (var#) |
( -- v ) |
global_N in qualsiasi espressione |
0x10022000 |
STACK_ALLOC |
0 | ( -- ) |
(implicito: uno per ogni var e parametro) |
0x10023002 |
POP_LOCAL |
1 (var#) |
( v -- ) |
local_N = expr; |
0x10023004 |
POP_STATIC |
1 (var#) |
( v -- ) |
global_N = expr; |
0x10024000 |
POP_STACK |
0 | ( v -- ) |
(scarta il risultato di un'espressione) |
Aritmetica (0x103x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10031000 |
ADD |
0 | ( a b -- a+b ) |
a + b |
0x10032000 |
SUB |
0 | ( a b -- a-b ) |
a - b |
0x10033000 |
MUL |
0 | ( a b -- a*b ) |
a * b |
0x10034000 |
DIV |
0 | ( a b -- a/b ) |
a / b |
0x10035000 |
BITWISE_AND |
0 | ( a b -- a&b ) |
a & b |
0x10036000 |
BITWISE_OR |
0 | ( a b -- a|b ) |
a | b |
0x10037000 |
XOR |
0 | ( a b -- a^b ) |
a ^ b |
0x10038000 |
NOT |
0 | ( a -- ~a ) |
~a |
TA non ha un'istruzione di modulo: % è un errore di compilazione in uno
script di TA. Da 0x10039000 a 0x1003B000 ci sono gli operatori
TAK_MATH_* di TA: Kingdoms
(Appendice C); TA va in errore su di essi.
Interrogazioni al motore (0x104x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10041000 |
RAND |
0 | ( low high -- rnd ) |
rand(low, high) |
0x10042000 |
GET_UNIT_VALUE |
0 | ( port -- value ) |
get <PORT> |
0x10043000 |
GET |
0 | ( port a b c d -- value ) |
get <PORT>(a, b, …) / get(port, …) — estrae sempre una porta e quattro argomenti; il compilatore inserisce zeri per quelli mancanti |
0x10044000 |
IS_CARRYING_UNIT |
0 | ( unit -- bool ) |
__is_carrying_unit(unit) — 1 quando quell'unità viaggia su questa |
0x10045000 |
CARRIER_UNIT_ID |
0 | ( -- id ) |
__carrier_unit_id() — l'unità che trasporta questa, oppure 0 |
Vedi l'Appendice B per il catalogo delle porte.
Confronti e operazioni logiche (0x105x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10051000 |
LESS_THAN |
0 | ( a b -- a<b ) |
a < b |
0x10052000 |
LESS_OR_EQUAL |
0 | ( a b -- a<=b ) |
a <= b |
0x10053000 |
GREATER_THAN |
0 | ( a b -- a>b ) |
a > b |
0x10054000 |
GREATER_EQUAL |
0 | ( a b -- a>=b ) |
a >= b |
0x10055000 |
EQUAL |
0 | ( a b -- a==b ) |
a == b |
0x10056000 |
NOT_EQUAL |
0 | ( a b -- a!=b ) |
a != b |
0x10057000 |
LOGICAL_AND |
0 | ( a b -- a&&b ) |
a AND b |
0x10058000 |
LOGICAL_OR |
0 | ( a b -- a||b ) |
a OR b |
0x10059000 |
XOR_ALT |
0 | ( a b -- a^b ) |
a XOR b — uno XOR bit a bit come 0x10037000 (6 XOR 3 vale 5), non logico |
0x1005A000 |
LOGICAL_NOT |
0 | ( a -- !a ) |
NOT a |
Flusso di controllo e concorrenza (0x106x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10061000 |
START_SCRIPT |
2 (script#, argc) |
( args… -- ) |
start-script Foo(a, b); |
0x10062000 |
CALL_SCRIPT |
2 (script#, argc) |
( args… -- ) |
call-script Foo(a, b); |
0x10063000 |
DISCARD_CALL |
2 (word, argc) |
( args… -- ) |
__discard_call(word, args…); — estrae gli argomenti (più di quattro sforano il buffer del gioco) e non esegue nulla |
0x10064000 |
JUMP |
1 (offset) |
( -- ) |
(compilatore: goto, cicli) |
0x10065000 |
RETURN |
0 | ( v -- ) |
return v; |
0x10066000 |
JUMP_IF_FALSE |
1 (offset) |
( v -- ) |
(compilatore: if/while) |
0x10067000 |
SIGNAL |
0 | ( mask -- ) |
signal <mask>; |
0x10068000 |
SET_SIGNAL_MASK |
0 | ( mask -- ) |
set-signal-mask <mask>; |
L'offset di JUMP / JUMP_IF_FALSE è un offset in byte relativo a
OffsetToScriptCode, non all'istruzione corrente.
Effetti (0x107x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10071000 |
EXPLODE |
1 (piece) |
( type -- ) |
explode <piece> type <type>; |
0x10072000 |
PLAY_SOUND |
1 (vol) |
( sound -- result ) |
play-sound(<sound>, <vol>); — solo TA: Kingdoms (.version 6); TA va in errore |
Operazioni di impostazione (0x108x)
| Opcode | Mnemonico | Inline | Effetto sullo stack | Sintassi BOS |
|---|---|---|---|---|
0x10082000 |
SET_VALUE |
0 | ( port value -- ) |
set <PORT_NAME> to <value>; |
0x10083000 |
ATTACH_UNIT |
1 (piece) |
( unit -- ) |
attach-unit <unit> to <piece>; |
0x10084000 |
DROP_UNIT |
0 | ( unit -- ) |
drop-unit <unit>; |
Codifica degli opcode
Ogni parola di opcode è 0x10CCSSFF, dove:
CC= il byte di categoria (animazione, matematica, flusso di controllo, …).SS= il nibble del sotto-comando, nel nibble alto del secondo byte dall'alto.FF= il byte dei flag.
Il gioco smista in base a word & 0x100FF000 e ignora gli altri bit, quindi
una parola come 0x10064001 viene eseguita come JUMP; il disassemblatore la
scrive JUMP@0x10064001 e l'assemblatore la rilegge nella stessa parola.
PUSH e POP leggono la loro sorgente o destinazione dai tre bit bassi:
PUSH accetta 1 (costante), 2 (locale) e 4 (statica), POP accetta 2 e 4, e
qualsiasi altro valore manda in errore lo script.
Le parole che nessun gioco esegue vengono rese dal disassemblatore come
UNKNOWN_0x10xxxxxx (e segnalate da ta-unknown-opcode); se ne trovi una in
un file reale, apri una issue allegando il COB di esempio.
Appendice B — Tabella delle porte di get_unit_value
Le 20 porte che il decompilatore di kbot attualmente etichetta in modo simbolico. Il vero TA ne ha ~80; TA: Kingdoms ne aggiunge altre ~15 per le grandezze legate alla magia. Oggi tutto ciò che supera la porta 20 viene reso come letterale numerico nell'output del decompilatore.
| Porta | Nome simbolico | Restituisce | Note |
|---|---|---|---|
| 1 | ACTIVATION |
bool | L'interruttore "attivo" dell'unità è acceso (radar, jammer, fortificazioni)? |
| 2 | STANDINGMOVEORDERS |
enum | 0 = mantieni la posizione, 1 = manovra, 2 = vaga. |
| 3 | STANDINGFIREORDERS |
enum | 0 = non sparare, 1 = rispondi al fuoco, 2 = fuoco a volontà. |
| 4 | HEALTH |
0–100 | Percentuale di MaxDamage rimanente. |
| 5 | INBUILDSTANCE |
bool | L'unità sta estendendo i nano-lathe in questo momento? |
| 6 | BUSY |
bool | L'unità è attualmente occupata con un ordine? |
| 7 | PIECE_XZ |
packed | X/Z combinate di un pezzo (codificate). Usato per mirare i proiettili. |
| 8 | PIECE_Y |
int | Coordinata Y di un pezzo relativa all'origine dell'unità. |
| 9 | UNIT_XZ |
packed | X/Z combinate dell'unità. |
| 10 | UNIT_Y |
int | Coordinata Y dell'unità. |
| 11 | UNIT_HEIGHT |
int | Altezza sopra il terreno. |
| 12 | XZ_ATAN |
int | atan2 tra due valori codificati XZ (per le torrette che mirano). |
| 13 | XZ_HYPOT |
int | Ipotenusa tra due valori codificati XZ. |
| 14 | ATAN |
int | atan2(y, x) di due valori inseriti nello stack. |
| 15 | HYPOT |
int | hypot(x, y). |
| 16 | GROUND_HEIGHT |
int | Elevazione del terreno nella posizione dell'unità. |
| 17 | BUILD_PERCENT_LEFT |
0–100 | Usato da SmokeUnit per conoscere l'avanzamento della costruzione. 0 una volta costruita. |
| 18 | YARD_OPEN |
bool | Per gli edifici con yard map: le porte sono aperte? |
| 19 | BUGGER_OFF |
bool | Impostato dal motore quando l'unità deve togliersi di mezzo. |
| 20 | ARMORED |
bool | La postura corazzata dell'unità è attiva (Bulldog, Goliath, ecc.)? |
Le porte più alte includono CURRENT_SPEED (29, notoriamente reso inline
come letterale dallo Scriptor di Cavedog; vedi Insidie più sopra), MAX_ID,
UNIT_TEAM, UNIT_BUILD_PERCENT, BUILDER, UNIT_ALLIED e molte altre.
L'elenco completo si trova nella documentazione interna di Cavedog; il wiki
della comunità sui forum di TAUniverse ha il catalogo pubblico più completo.
TA: Kingdoms estende l'intervallo delle porte. Le porte esclusive di TAK che
kbot riconosce (il decompilatore rende il nome simbolico; il compilatore lo
accetta in ingresso) sono prese dalla tabella [UNITVLAUES] di Scriptor:
| Porta | Simbolo | Note |
|---|---|---|
| 21 | WEAPON_AIM_ABORTED |
Impostato quando la soluzione di mira di un'arma viene annullata. |
| 22 | WEAPON_READY |
L'arma si è ricaricata ed è pronta a sparare. |
| 23 | WEAPON_LAUNCH_NOW |
Il motore ha fatto fuoco con l'arma. |
| 26 | FINISHED_DYING |
L'animazione di morte è terminata. |
| 27 | ORIENTATION |
Direzione verso cui è rivolta l'unità. |
| 28 | IN_WATER |
L'unità è in acqua. |
| 29 | CURRENT_SPEED |
Velocità di movimento attuale (condivisa con TA: Scriptor la numera allo stesso modo anche qui). |
| 31 | MAGIC_DEATH |
L'unità è morta per danno magico. |
| 32 | VETERAN_LEVEL |
Livello di veterania. |
| 34 | ON_ROAD |
L'unità è su un tile di strada. |
Appendice C — Opcode di TA: Kingdoms
Note
Opcode esclusivi di TA:K. Nessuno di questi compare nei file .cob
originali di TA. DONT_SHADOW e MISSION_COMMAND usano i mnemonici
canonici di Scriptor (il compilatore/decompilatore di Switeck che conosce
TAK); le operazioni matematiche non hanno una semantica documentata:
Scriptor le etichetta ?? e ????, quindi kbot mantiene i segnaposto
TAK_MATH_09 / TAK_MATH_0A / TAK_MATH_0B. Gli script originali di TA:
Kingdoms usano ciascuno di essi come operatore binario (due valori estratti,
uno inserito) quando impacchettano e spacchettano campi di bit. TA 3.1c non
ha un gestore per PLAY_SOUND, MISSION_COMMAND o gli operatori
matematici; DONT_SHADOW lo esegue come no-op.
| Opcode | Mnemonico | Inline | Occorrenze in TAK originale | Note |
|---|---|---|---|---|
0x1000A000 |
DONT_SHADOW |
1 (piece) |
66 | Disattiva la proiezione dell'ombra per un singolo pezzo. Si trova nella categoria dell'animazione accanto a DONT_SHADE (0x1000E000); in passato kbot lo chiamava TAK_ANIM_0A. Stessa forma su disco degli altri opcode ANIM_*: opcode di 4 byte + una DWORD piece. Parola chiave BOS: dont-shadow <piece>;. |
0x10039000 |
TAK_MATH_09 |
0 | 264 | Operatore binario della categoria matematica, 4 byte in tutto: ( a b -- r ). BOS: __tak_math_09(a, b). |
0x1003A000 |
TAK_MATH_0A |
0 | — | Stessa forma. Alcune tabelle di opcode lo chiamano BITWISE_NOT; il NOT bit a bit di TA è 0x10038000. BOS: __tak_math_0a(a, b). |
0x1003B000 |
TAK_MATH_0B |
0 | 66 | Stessa forma di TAK_MATH_09. BOS: __tak_math_0b(a, b). |
0x10073000 |
MISSION_COMMAND |
2 (soundNameIdx, argCount) |
3.885 | Chiamata di un comando del motore. La prima DWORD inline è un indice nella tabella SoundNames del COB; la seconda è il numero di valori da estrarre dallo stack come argomenti. L'opcode inserisce un unico risultato nello stack (di solito scartato con un POP_STACK successivo o salvato con POP_STATIC/POP_LOCAL). È di gran lunga l'opcode esclusivo di TAK più comune. Parola chiave BOS: Mission-Command("name", args…), che corrisponde alla sintassi canonica di Scriptor. |
Codifica (riepilogo)
Ogni parola di opcode COB è 0x10CCSSFF, dove CC è la categoria e SS il
sotto-comando: questi opcode di TAK occupano semplicemente slot CC:SS prima
inutilizzati all'interno delle categorie esistenti, invece di introdurre un
nuovo byte di categoria. DONT_SHADOW sta nella categoria dell'animazione
accanto a DONT_CACHE/DONT_SHADE, e MISSION_COMMAND nella categoria degli
effetti accanto a EXPLODE e PLAY_SOUND.
Tip
Confronta con il bytecode reale.
kbot ctx use tak-30bb-flat
kbot cob disassemble scripts/araat.cob -a | grep -E 'DONT_SHADOW|MISSION|TAK_MATH|PLAY_SOUND'
Esempi reali nel catalogo di riferimento
Unità reali che spingono il formato ai suoi estremi: aprile per vedere la complessità effettiva degli script, invece degli esempi costruiti nel testo:
CORKROG— Krogoth (Core) — uno dei COB più grandi del gioco (~25 KB, più di 30 script, animazioni complesse con molti pezzi).ARMCOM— Arm Commander — unità "ricca" rappresentativa: cicli di camminata/mira/riposo/costruzione, più armi, gestione del D-gun.CORTRUCK— Construction Truck — COB quasi minimo, usato come esempio guidato in questa pagina (3 script:SmokeUnit,Create,Killed).ARMSAM— Samson — buon esempio di unità con torretta (rotazione diturret1per mirare, animazione di rinculo, sincronizzazione conwait-for-turn).
Decompila qualsiasi di questi in locale con
kbot cob decompile scripts/<name>.cob, una volta impostato un contesto kbot.
Vedi anche
- 3DO — i nomi dei pezzi a cui fanno riferimento gli script COB
stanno nel
.3dodell'unità. - FBI — il file dei metadati dell'unità. Lì
ObjectNamelega il COB a un 3DO;Soundsdefinisce il banco audio che lo script può riprodurre. - La tabella delle regole di
cob lintdella CLI di kbot. - Glossario — pezzo (piece), asse (axis), maschera dei segnali (signal mask), tick.