Tutte le pagine dei formati ↓
Unità, script, configurazioni

COB / BOS — Bytecode di scripting delle unità

Fonte: coreprime/kbot @ 0ab97bf · Modifica su GitHub

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'ObjectName dell'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'animazione turn in corso. Lo stesso vale per wait-for-move.
  • signal <n> termina ogni script per cui signal_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:

  1. Entrambi gli script che impostano SIG_MOVE vengono terminati quando il segnale scatta, compreso MoveUnit stesso (non importa: il suo lavoro è finito).
  2. 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'opcode signal ha restituito il controllo.
  3. Idle non imposta SIG_MOVE, quindi il successivo signal SIG_MOVE non 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 ScriptCodeIndexArray sono 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_VALUE differiscono 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) e global_N (statica per unità che persiste tra le chiamate). Il decompilatore emette local_N / global_N per impostazione predefinita; rinominale nel sorgente per renderlo più leggibile.
  • Lo strumento Scriptor di Cavedog a volte rende inline CURRENT_SPEED come letterale 29, rompendo gli script in stile MoveWatcher dopo 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 di turret1 per mirare, animazione di rinculo, sincronizzazione con wait-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 .3do dell'unità.
  • FBI — il file dei metadati dell'unità. Lì ObjectName lega il COB a un 3DO; Sounds definisce il banco audio che lo script può riprodurre.
  • La tabella delle regole di cob lint della CLI di kbot.
  • Glossario — pezzo (piece), asse (axis), maschera dei segnali (signal mask), tick.