Todas as páginas de formatos ↓
Unidades, scripts e ficheiros de configuração

COB / BOS — bytecode dos scripts das unidades

Fonte: coreprime/kbot @ 0ab97bf · Editar no GitHub

Todas as unidades do Total Annihilation correm um script que anima as suas peças, emite os clarões das armas, toca efeitos sonoros e responde aos eventos do jogo. A linguagem de origem é o BOS ("Basic Object Script"); o bytecode compilado que o motor realmente executa é o COB ("Cobble", o trocadilho da Cavedog para "BOS compilado"). Um ficheiro por unidade, com o nome do ObjectName que a unidade tem no seu FBI.

Tip

Experimente por si.

# 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/

Todas as opções (e a tabela completa das regras de lint) estão na referência de kbot cob da linha de comandos.

A partir de Go. Use o formats/scripting do 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)
}

Duas linguagens, um formato de ficheiro

Fase Formato Ferramentas
Código-fonte BOS — semelhante a C, estruturado Escrito à mão; o Scriptor de Switeck; o kbot cob decompile faz a ida e volta a partir do COB
Compilado COB — bytecode baseado em pilha Carregado pelo motor; produzido pelo Scriptor ou pelo kbot cob compile

O motor só vê o COB. O BOS é uma comodidade para os autores humanos: quando "edita o script de uma unidade", está na verdade a editar o código-fonte BOS, a recompilá-lo para COB e a distribuir o COB no complemento .ufo da unidade.

Um exemplo mínimo:

// 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;
    }
}

O kbot cob compile traduz isto para um blob binário. O kbot cob disassemble -a mostra o fluxo de opcodes lado a lado com setas de fluxo de controlo; o kbot cob decompile faz o inverso e recupera um BOS que, quase sempre, faz a ida e volta idêntico byte a byte (o kbot cob roundtrip confirma-o em todo o catálogo do jogo).

Um código-fonte compila como script do TA (COB versão 4), a não ser que comece por .version 6. O compilador recusa o que o TA 3.1c executaria mal, em vez de pôr algo provisório no lugar: as construções do TA: Kingdoms (play-sound, Mission-Command, __tak_math_*, .sound_name) e % são erros num script do TA (o TA não tem instrução de módulo, e 0x10037000, a palavra em que % se convertia, é o seu XOR bit a bit), tal como os identificadores desconhecidos e as funções que precisam de mais do que as 32 posições da pilha do jogo. O que ainda compila mas merece atenção — uma função definida duas vezes (cada chamada liga-se à primeira definição, como faz a procura de nomes do jogo) — é mostrado como aviso, juntamente com qualquer resultado das regras de lint de compatibilidade com o TA; --strict faz com que os avisos façam falhar o comando. O kbot cob assemble aplica as mesmas verificações às listagens escritas à mão.


Estrutura do COB no disco

Important

Dois dialetos. Esta secção descreve o COB v4, o dialeto que o Total Annihilation distribui. O TA: Kingdoms usa o COB v6 (VersionSignature == 6), que mantém tudo o que está abaixo, mas aumenta o cabeçalho em 8 bytes e insere uma tabela adicional de deslocamentos de cadeias de comandos depois do vetor de nomes de peças. Os pormenores da v6 estão em TA: Kingdoms — diferenças do COB v6; a lista de opcodes aplica-se aos dois, e os opcodes adicionais exclusivos do TAK estão documentados no Apêndice 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

Os nomes canónicos dos campos correspondem à struct HEADER_COB do Scriptor. As revisões anteriores do kbot chamavam a LengthOfScripts / NumberOfStaticVars / UKZero / OffsetToNameArray "Unknown0/1/2/3": os deslocamentos são os mesmos, só que agora com as identidades documentadas.

Seguem-se, por esta disposição:

┌─ 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

Resolver um script

A tabela de índices de código contém deslocamentos em palavras uint32, e não em bytes. Para encontrar o bytecode do script número i:

uint32 first_instr_word = ScriptCodeIndexArray[i];
uint32 byte_offset      = OffsetToScriptCode + first_instr_word * 4;

O script termina onde começa o seguinte (ou, no caso do último script, no início da tabela de índices).

Important

Os deslocamentos das cadeias nas tabelas de nomes são deslocamentos absolutos no ficheiro, e não relativos ao conjunto de nomes. As ferramentas da Cavedog nunca mudaram o conjunto de nomes de sítio, por isso na prática isto não fazia diferença; se reorganizar um COB à mão, tem de atualizar todos os deslocamentos.


O fluxo de opcodes

O COB é uma máquina virtual baseada em pilha. A maioria dos opcodes retira argumentos de uma pilha de dados própria de cada script, faz o seu trabalho e volta a pôr um resultado na pilha. Alguns opcodes têm operandos em linha (destinos de salto, números de peças).

Cada opcode é um uint32 com esta disposição:

       MSB                          LSB
  ┌─────────┬─────────┬─────────┬─────────┐
  │   0x10  │ family  │ op      │ flags   │
  └─────────┴─────────┴─────────┴─────────┘

O byte alto é sempre 0x10; a família, a operação e as flags variam de opcode para opcode. O jogo não compara a palavra inteira: escolhe a instrução por word & 0x100FF000, por isso os bits fora dessa máscara são ignorados e 0x10064001 é executado como JUMP (o desmontador do kbot escreve uma palavra destas como JUMP@0x10064001, para que volte a ser montada sem alterações). O PUSH (0x10021000) e o POP (0x10023000) leem depois a sua origem ou destino nos três bits baixos. O scripting/opcodes.go do kbot-io é o catálogo; scripting.Opcodes() lista todas as instruções com os seus operandos em linha e o seu efeito na pilha.

Famílias de opcodes

Família Exemplos Notas
0x100x MOVE, TURN, SPIN, SHOW, HIDE, EMIT_SFX Animação das peças. O número da peça e o do eixo vêm em linha.
0x101x WAIT_FOR_TURN, WAIT_FOR_MOVE, SLEEP Barreiras assíncronas: o script cede a vez até a condição se cumprir.
0x102x PUSH_CONST, PUSH_LOCAL, PUSH_STATIC, POP_* Manipulação da pilha.
0x103x ADD, SUB, MUL, DIV, BITWISE_AND, BITWISE_OR, XOR, NOT Aritmética e operações bit a bit. Não há módulo.
0x104x RAND, GET_UNIT_VALUE, GET, IS_CARRYING_UNIT, CARRIER_UNIT_ID Consultas ao motor.
0x105x <, <=, >, >=, ==, !=, &&, ||, XOR_ALT, ! Comparações e operações lógicas. XOR_ALT é um XOR bit a bit, e não um lógico.
0x106x START_SCRIPT, CALL_SCRIPT, DISCARD_CALL, JUMP, RETURN, JUMP_IF_FALSE, SIGNAL, SET_SIGNAL_MASK Fluxo de controlo e concorrência.
0x107x EXPLODE, PLAY_SOUND Efeitos especiais. PLAY_SOUND é exclusivo do TA: Kingdoms.
0x108x SET_VALUE, ATTACH_UNIT, DROP_UNIT Alterações ao estado do motor.

Uma amostra pequena, mas representativa:

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

Os deslocamentos dos saltos são deslocamentos em bytes relativos ao início da secção de código dos scripts (e não à instrução atual). JUMP 0x4C significa "saltar para OffsetToScriptCode + 0x4C". A opção -a do desmontador desenha setas entre as instruções correspondentes para que isto se torne evidente.


Modelo de concorrência

Uma unidade pode ter muitos scripts a correr em simultâneo — Walk, Aim, SmokeUnit, MoveWatcher, etc. São escalonados de forma cooperativa:

  • sleep <ticks> cede a vez durante N ticks (1/30 s cada).
  • wait-for-turn <piece> around <axis> cede a vez até essa peça terminar a sua animação turn atual. O mesmo para wait-for-move.
  • signal <n> mata todos os scripts cujo signal_mask & n != 0.
  • set-signal-mask <n> declara em que sinais o script atual está interessado.

A máscara de sinais é a forma como os scripts implementam a preempção: o MoveWatcher define a máscara SIG_MOVE e vigia a velocidade; quando a unidade para, envia SIG_MOVE, o que mata as animações de marcha de forma limpa antes de começar a pose de repouso.

Warning

O SET_SIGNAL_MASK só regista interesse: não mata nada. O que para os scripts com a máscara X definida é enviar signal X. A regra de lint signal-never-signalled do kbot cob lint apanha os scripts que definem uma máscara que ninguém sinaliza (o que é quase sempre um erro).

Como a preempção funciona na prática

Um padrão comum: o MoveUnit lança um script de animação Walk; quando o movimento para, o motor envia SIG_MOVE para limpar tudo antes de o Idle começar. A sequência:

   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)

Três coisas a notar:

  1. Os dois scripts que definem SIG_MOVE são mortos quando o sinal é disparado, incluindo o próprio MoveUnit (não faz mal; o seu trabalho está feito).
  2. O script que sinaliza (MoveUnit) sobrevive o tempo suficiente para enviar o sinal antes de ser morto; o runtime verifica as máscaras depois de o opcode signal regressar.
  3. O Idle não define SIG_MOVE, por isso o próximo signal SIG_MOVE não lhe toca.

Exemplo comentado — cortruck.cob (Camião de Construção CORE)

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);
}

A desmontagem correspondente (kbot cob disassemble -a -s SmokeUnit) mostra o fluxo real de opcodes, com as setas JUMP_IF_FALSE/JUMP a atravessar os corpos dos while/if: útil quando o resultado do descompilador é ambíguo.


Lint

O kbot cob lint (e o separador Lint do explorador de recursos) aplica o conjunto de regras predefinido do kbot-io: regras de estilo tiradas de erros reais nos próprios scripts da Cavedog e de correções de mods enviadas pela comunidade, duas verificações ao nível do ficheiro e — em todos os COB que não declarem a versão 6 — as regras de compatibilidade com o TA 3.1c. O catálogo de estilo:

Regra Gravidade Deteta
unused-piece aviso Peça declarada mas nunca usada em nenhum script.
unused-static / unused-local aviso Variável reservada mas nunca lida.
always-true / dead-code info / aviso if(1) / while(0).
long-function / high-complexity aviso > 100 instruções ou complexidade ciclomática > 15.
invalid-call erro call-script para uma função que não existe.
speed-zero aviso move/turn com speed <0> — a animação nunca termina.
empty-function info O corpo da função é só return 0.
duplicate-animation aviso Dois comandos de animação idênticos seguidos.
sleep-only-guard info Bloco if que só contém sleep.
duplicate-if info Dois if seguidos com a mesma condição.
raw-signal info signal 4 em vez de signal SIG_AIM.
unnamed-global info global_3 em vez de uma variável estática com nome.
signal-never-signalled aviso O script tem na máscara um sinal que ninguém emite.
recursive-call aviso Ciclo de call-script.
duplicate-function aviso Dois scripts com o mesmo nome; o jogo só chama o primeiro.
malformed-cob erro / aviso O ficheiro não carrega, ou o código de um script está truncado (erro); tabelas de nomes que o leitor tolerou (aviso).

As regras de compatibilidade com o TA assinalam aquilo em que o TA 3.1c falha ou que executa de outra maneira. São erros, aparecem na saída SARIF de --ci como as outras e ignoram os ficheiros do TA: Kingdoms (versão 6):

Regra Deteta
ta-kingdoms-opcode PLAY_SOUND, MISSION_COMMAND ou um operador TAK_MATH_* num script do TA. O TA não tem tratamento para eles e o script falha quando chega a um.
ta-unknown-opcode Uma palavra de opcode que nenhum jogo executa.
ta-push-flags Um PUSH cujos bits baixos não sejam 1 (constante), 2 (local) ou 4 (estática), ou um POP cujos bits baixos não sejam 2 ou 4 — incluindo os 0x10021000 e 0x10021008 sem flag que algumas ferramentas escreviam.
ta-stack-limit Mais de 32 STACK_ALLOC, ou variáveis locais mais valores pendentes acima das 32 posições da pilha do jogo; o jogo não verifica e escreve por cima da memória que fica a seguir ao script.
ta-get-arguments Um GET alcançado com menos de cinco valores pendentes. O GET retira sempre uma porta e quatro argumentos, por isso um get com um só argumento vai buscar o resto às variáveis locais da função e escreve por cima delas.
ta-stack-underflow Qualquer outra instrução que retire mais valores do que os que estão pendentes, ou caminhos que se juntam com profundidades de pilha diferentes.
ta-discard-call DISCARD_CALL com mais de quatro argumentos.

As descrições completas das regras estão no README do kbot. O descompilador preserva de propósito a numeração original das variáveis e das peças, para que um ciclo descompilar→lint→corrigir→compilar destaque os mods que precisam de manutenção.


Armadilhas

Warning

O call-script é 0x10062000. 0x10063000 é uma instrução diferente, DISCARD_CALL: tem as mesmas duas palavras em linha (número do script e número de argumentos), mas retira os argumentos da pilha e não executa nada. Nenhum script comercial a usa; o descompilador do kbot escreve-a como __discard_call(word, args…).

Warning

Várias tabelas de opcodes publicadas dão nomes errados às palavras bit a bit. O TA executa 0x10037000 como XOR bit a bit (não como módulo — o TA não tem instrução de módulo), 0x10038000 como o NOT bit a bit unário (não como XOR) e 0x10059000 como um segundo XOR bit a bit (não um XOR lógico: 6 XOR_ALT 3 dá 5). 0x1003A000 é um operador do TA: Kingdoms com que o TA falha. As listagens escritas com os nomes antigos (MOD, BITWISE_XOR, BITWISE_NOT, LOGICAL_XOR) continuam a ser montadas nas palavras a partir das quais foram feitas.

  • Todos os deslocamentos do cabeçalho do COB são deslocamentos absolutos no ficheiro, mas as entradas de ScriptCodeIndexArray são deslocamentos em palavras dentro da secção de código. É fácil confundi-los.
  • Os números das peças nos opcodes são índices na tabela de nomes das peças, e não cadeias. O COB guarda as cadeias dos nomes todas juntas (o conjunto de nomes); o runtime usa índices por rapidez.
  • Os opcodes get UNIT_VALUE variam subtilmente entre o TA e o TA: Kingdoms. O TA: Kingdoms acrescentou IDs de valores para a regeneração de magia, os pontos de comando, etc.; a tabela de valores do TA é mais compacta. Por agora, o descompilador do kbot dá nomes ao conjunto do TA; as portas do TAK ≥ 21 são escritas como literais numéricos (que continuam a fazer a ida e volta pelo montador). Todas as diferenças estão em TA: Kingdoms — diferenças do COB v6.
  • As variáveis são de dois tipos: local_N (posição da pilha local à função) e global_N (variável estática de cada unidade, que persiste entre chamadas). Por omissão, o descompilador escreve local_N / global_N; mude-lhes o nome no código-fonte para facilitar a leitura.
  • A ferramenta Scriptor da Cavedog às vezes substitui CURRENT_SPEED pelo literal 29, o que estraga os scripts do tipo MoveWatcher depois de uma ida e volta. O descompilador do kbot repõe o nome simbólico; se tiver mesmo de fazer a ida e volta pelo Scriptor, use parênteses: (get CURRENT_SPEED) > 5.

Tamanhos típicos

Recurso Intervalo observado nos scripts/*.cob da Cavedog
Tamanho do ficheiro 0.5 KB – 25 KB
Sobrecarga do cabeçalho sempre 44 bytes
Scripts por unidade 3 – 30 (unidades típicas: 5–10)
Peças por unidade 3 – 80 (Krogoth ≈ 80)
Tamanho da secção de código 300 B – 20 KB (varia muito com a complexidade)
armcom.cob (Comandante) ~14 KB, 30 scripts
cortruck.cob (unidade simples) ~750 B, 3 scripts

TA: Kingdoms — diferenças do COB v6

Note

Esta secção trata só do que muda no TA:K. Tudo o que está acima descreve o COB v4 (TA). O TA: Kingdoms mantém a mesma VM de pilha, o mesmo conjunto de nomes, o mesmo esquema de codificação dos opcodes e o mesmo mecanismo de nomes de scripts / nomes de peças; as diferenças são um cabeçalho alargado, uma tabela de cadeias adicional acrescentada depois do vetor de nomes de peças e quatro opcodes novos.

Estrutura do ficheiro 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)

O subcabeçalho de 8 bytes fica entre OffsetToNameArray (o fim do cabeçalho canónico de 11 DWORD, no byte 44) e o início da secção de código (OffsetToScriptCode, normalmente 0x34 = 52). Contém dois uint32 little-endian:

Campo Significado
[0] Deslocamento absoluto no ficheiro da tabela de deslocamentos dos nomes de sons. Sempre igual a OffsetToPieceNameOffsetArray + NumPieces × 4, ou seja, o byte logo a seguir ao vetor de deslocamentos dos nomes de peças. É redundante com a disposição canónica; o escritor reconstrói-o.
[1] Número de nomes de sons na tabela. É a contagem que faz fé.

Cada entrada da tabela de deslocamentos dos nomes de sons é o deslocamento absoluto no ficheiro de uma cadeia terminada em NUL no conjunto de nomes — coisas como "SetMission o 1, s", "create ARASWORD", "ARROW10". O bytecode refere-as pelo índice (e não pelo deslocamento) através do opcode MISSION_COMMAND (veja o Apêndice C). A tabela chama-se vetor de nomes de sons porque o compilador canónico Scriptor regista cadeias nela através da ação ADDSOUND; o conteúdo real são cadeias de comandos do motor, e não nomes de ficheiros de áudio.

A representação no kbot

O scripting.COB guarda os nomes de sons num campo estruturado []string, sem preservar bytes opacos. Tudo o resto (os valores do subcabeçalho, a tabela de deslocamentos, o deslocamento de cada cadeia no conjunto) é reconstruído a partir desta lista na escrita:

type COB struct {
    // …canonical TA fields…
    SoundNames []string // TAK-only; nil for v4
}

Nos ficheiros v4, a slice é sempre nil, e tanto o subcabeçalho como a tabela de deslocamentos dos nomes de sons são omitidos na saída.

O desmontador e o descompilador escrevem cada cadeia como uma diretiva .sound_name, para que sobrevivam a uma ida e volta por .coba e .bos:

.version 6
.statics 6
.sound_name "create NPCEMEN"
.sound_name "SetMission o 1, s"
.sound_name "ARROW10"
.script Start
…

Opcodes novos

Os ficheiros .cob do TAK usam instruções para as quais o TA 3.1c não tem tratamento (PLAY_SOUND, MISSION_COMMAND e os operadores TAK_MATH_*), além de DONT_SHADOW, que o TA executa como uma operação nula. A sua semântica exata no jogo não está documentada, mas a sua estrutura binária (tamanho + número de parâmetros em linha) é conhecida a partir de todos os .cob comerciais do TAK, o que chega para que a ida e volta desmontar→montar recupere os bytes originais tal e qual. A referência completa está no Apêndice C.

Estado da ida e volta

Processo TA TA: Kingdoms
disassemble → assemble (idêntico byte a byte) 278 / 278 ✅ 267 / 267 ✅
decompile → compile (idêntico byte a byte) 278 / 278 ✅ Os scripts de unidades simples e muitos COB de missões (≈ 70 / 267) fazem a ida e volta idênticos byte a byte. As restantes falhas são padrões de fluxo de controlo em que a reconstrução estruturada de if/while do descompilador escolhe uma disposição de JUMP que o compilador volta a gerar de forma ligeiramente diferente: o bytecode é semanticamente equivalente (todos os opcodes do TAK, as cadeias de comandos, o subcabeçalho e a palavra de versão v6 sobrevivem), só não é idêntico byte a byte. O mesmo tipo de diferença afetaria qualquer ciclo descompilar→compilar num script que não seja trivial.

Superfície BOS para as extensões do TAK

O descompilador escreve estas pseudochamadas para que os COB do TAK façam a ida e volta pelo BOS. O compilador aceita a mesma sintaxe, mas só depois de .version 6 (exceto dont-shadow, que o TA executa como operação nula). dont-shadow e Mission-Command usam as palavras-chave canónicas do TAK no Scriptor; os opcodes matemáticos não têm nome documentado no Scriptor (que lhes chama ?? e ????), por isso o kbot mantém os nomes provisórios __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

O primeiro argumento de Mission-Command é o nome de som como cadeia entre aspas: o compilador procura-o na tabela de nomes de sons (por isso a diretiva .sound_name correspondente tem de aparecer antes no ficheiro). Os restantes argumentos são postos na pilha e o motor consome-os.

Duas diretivas no início do ficheiro trazem o resto dos metadados v6:

.version 6
.sound_name "create NPCEMEN"
.sound_name "SetMission o 1, s"
.sound_name "ARROW10"

A .version é sempre escrita (para que os ficheiros v4 do TA façam a ida e volta idênticos pelo mesmo processo). Cada .sound_name é uma entrada na tabela de nomes de sons do COB; o escritor reconstrói o subcabeçalho de 8 bytes e a tabela de deslocamentos dos nomes de sons a partir de len(SoundNames) e da ordem pela qual as entradas aparecem.

O epílogo STACK_ALLOC ; RETURN do TAK, que cada return; sem valor produz na v6, é tratado pelo compilador: return; traduz-se em STACK_ALLOC ; RETURN quando há .version 6, e return <expr>; traduz-se em <expr> ; RETURN sem prefixo (como faz o compilador da Cavedog).

Reproduza-o na sua instalação:

kbot ctx use kingdoms  # or wherever you registered TAK
kbot cob roundtrip

Símbolos das portas acima da porta 20

Os scripts do TA: Kingdoms usam portas de get_unit_value até à 46 (CURRENT_SPEED, VETERAN_LEVEL, campos da reserva de magia, etc.). Por agora, o descompilador do kbot só dá nomes simbólicos às portas 1–20 (veja o Apêndice B); as portas do TAK ≥ 21 são escritas como literais numéricos. O bytecode guarda-as sempre como inteiros, por isso as idas e voltas não são afetadas: só a legibilidade do BOS sofre. A adição dos nomes das portas do TAK é acompanhada à parte.


Apêndice A — referência completa de opcodes

Todas as instruções que o TA ou o TA: Kingdoms executam. A fonte de referência é o formats/scripting/opcodes.go do kbot-io. A notação da pilha segue o estilo do Forth, ( before -- after ) (antes -- depois). As colunas Em linha mostram quantas palavras uint32 se seguem à própria palavra do opcode (consumidas durante a descodificação, e não retiradas da pilha).

Animação — manipulação das peças (0x100x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe 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); — o TA executa-o, mas os scripts das suas unidades não fazem nada com ele; nenhum script comercial o usa.
0x1000A000 DONT_SHADOW 1 (piece) ( -- ) dont-shadow <piece>; — o TA executa-o como operação nula; veja o Apêndice 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>;

Operações de espera (0x101x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe 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>;

Manipulação da pilha (0x102x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe BOS
0x10021000 PUSH_IMM 1 (value) — PUSH sem flag: o jogo falha com ele (ta-push-flags). O compilador nunca o escreve.
0x10021001 PUSH_CONST 1 (value) ( -- v ) <literal> em qualquer expressão
0x10021002 PUSH_LOCAL 1 (var#) ( -- v ) local_N em qualquer expressão
0x10021004 PUSH_STATIC 1 (var#) ( -- v ) global_N em qualquer expressão
0x10022000 STACK_ALLOC 0 ( -- ) (implícito — um por cada var e por cada parâmetro)
0x10023002 POP_LOCAL 1 (var#) ( v -- ) local_N = expr;
0x10023004 POP_STATIC 1 (var#) ( v -- ) global_N = expr;
0x10024000 POP_STACK 0 ( v -- ) (descarta o resultado de uma expressão)

Aritmética (0x103x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe 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

O TA não tem instrução de módulo: % é um erro de compilação num script do TA. 0x10039000–0x1003B000 são os operadores TAK_MATH_* do TA: Kingdoms (Apêndice C); o TA falha com eles.

Consultas ao motor (0x104x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe 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, …) — retira sempre uma porta e quatro argumentos; o compilador põe zeros na pilha no lugar dos que faltam
0x10044000 IS_CARRYING_UNIT 0 ( unit -- bool ) __is_carrying_unit(unit) — 1 quando essa unidade vai transportada por esta
0x10045000 CARRIER_UNIT_ID 0 ( -- id ) __carrier_unit_id() — a unidade que transporta esta, ou 0

O catálogo das portas está no Apêndice B.

Comparação e lógica (0x105x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe 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 — um XOR bit a bit como 0x10037000 (6 XOR 3 dá 5), e não um lógico
0x1005A000 LOGICAL_NOT 0 ( a -- !a ) NOT a

Fluxo de controlo e concorrência (0x106x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe 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…); — retira os argumentos da pilha (mais de quatro ultrapassam o buffer do jogo) e não executa nada
0x10064000 JUMP 1 (offset) ( -- ) (compilador — goto, ciclos)
0x10065000 RETURN 0 ( v -- ) return v;
0x10066000 JUMP_IF_FALSE 1 (offset) ( v -- ) (compilador — if/while)
0x10067000 SIGNAL 0 ( mask -- ) signal <mask>;
0x10068000 SET_SIGNAL_MASK 0 ( mask -- ) set-signal-mask <mask>;

O offset de JUMP / JUMP_IF_FALSE é um deslocamento em bytes relativo a OffsetToScriptCode, e não à instrução atual.

Efeitos (0x107x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe BOS
0x10071000 EXPLODE 1 (piece) ( type -- ) explode <piece> type <type>;
0x10072000 PLAY_SOUND 1 (vol) ( sound -- result ) play-sound(<sound>, <vol>); — só no TA: Kingdoms (.version 6); o TA falha com ele

Operações de definição (0x108x)

Opcode Mnemónica Em linha Efeito na pilha Sintaxe 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ção dos opcodes

Cada palavra de opcode é 0x10CCSSFF, em que:

  • CC = o byte da categoria (animação, matemática, fluxo de controlo, …).
  • SS = o nibble do subcomando, no nibble alto do segundo byte a contar de cima.
  • FF = o byte das flags.

O jogo escolhe a instrução por word & 0x100FF000 e ignora os outros bits, por isso uma palavra como 0x10064001 é executada como JUMP; o desmontador escreve-a JUMP@0x10064001 e o montador volta a lê-la como a mesma palavra. O PUSH e o POP leem a sua origem ou destino nos três bits baixos: o PUSH aceita 1 (constante), 2 (local) e 4 (estática), o POP aceita 2 e 4, e qualquer outro valor faz o script falhar.

As palavras que nenhum jogo executa são mostradas pelo desmontador como UNKNOWN_0x10xxxxxx (e assinaladas por ta-unknown-opcode); se encontrar uma num ficheiro real, abra uma issue com o COB de exemplo.


Apêndice B — tabela de portas de get_unit_value

As 20 portas a que o descompilador do kbot dá, por agora, nomes simbólicos. O TA real tem ~80; o TA: Kingdoms acrescenta mais ~15 para grandezas ligadas à magia. Tudo o que estiver acima da porta 20 aparece hoje como literal numérico no resultado do descompilador.

Porta Nome simbólico Devolve Notas
1 ACTIVATION bool O interruptor "ativo" da unidade está ligado (radar, bloqueador, fortificações)?
2 STANDINGMOVEORDERS enum 0 = manter posição, 1 = manobrar, 2 = vaguear.
3 STANDINGFIREORDERS enum 0 = cessar fogo, 1 = ripostar, 2 = fogo à vontade.
4 HEALTH 0–100 Percentagem de MaxDamage que resta.
5 INBUILDSTANCE bool A unidade está neste momento a estender os nanotornos?
6 BUSY bool A unidade está neste momento ocupada com uma ordem?
7 PIECE_XZ compactado X/Z combinados de uma peça (codificados). Usado na pontaria dos projéteis.
8 PIECE_Y int Coordenada Y de uma peça em relação à origem da unidade.
9 UNIT_XZ compactado X/Z combinados da unidade.
10 UNIT_Y int Coordenada Y da unidade.
11 UNIT_HEIGHT int Altura acima do terreno.
12 XZ_ATAN int atan2 entre dois valores codificados em XZ (para torres que apontam).
13 XZ_HYPOT int Hipotenusa entre dois valores codificados em XZ.
14 ATAN int atan2(y, x) de dois valores postos na pilha.
15 HYPOT int hypot(x, y).
16 GROUND_HEIGHT int Elevação do terreno na posição da unidade.
17 BUILD_PERCENT_LEFT 0–100 Usado pelo SmokeUnit para saber o progresso da construção. 0 depois de construída.
18 YARD_OPEN bool Nas construções com yard map: as portas estão abertas?
19 BUGGER_OFF bool Definido pelo motor quando a unidade tem de sair do caminho.
20 ARMORED bool A postura blindada da unidade está ativa (Bulldog, Goliath, etc.)?

As portas mais altas incluem CURRENT_SPEED (29 — que o Scriptor da Cavedog ficou famoso por substituir por um literal; veja as Armadilhas acima), MAX_ID, UNIT_TEAM, UNIT_BUILD_PERCENT, BUILDER, UNIT_ALLIED e muitas mais. A lista completa está na documentação interna da Cavedog; a wiki da comunidade nos fóruns do TAUniverse tem o catálogo público mais completo.

O TA: Kingdoms alarga o intervalo de portas. As portas exclusivas do TAK que o kbot reconhece (o descompilador mostra o nome simbólico; o compilador aceita-o como entrada) foram tiradas da tabela [UNITVLAUES] do Scriptor:

Porta Símbolo Notas
21 WEAPON_AIM_ABORTED Definido quando a solução de pontaria de uma arma é cancelada.
22 WEAPON_READY A arma recarregou e está pronta a disparar.
23 WEAPON_LAUNCH_NOW O motor disparou a arma.
26 FINISHED_DYING A animação de morte terminou.
27 ORIENTATION A direção para onde a unidade está virada.
28 IN_WATER A unidade está na água.
29 CURRENT_SPEED Velocidade de movimento atual (partilhada com o TA — o Scriptor dá-lhe aqui o mesmo número).
31 MAGIC_DEATH A unidade morreu por dano mágico.
32 VETERAN_LEVEL Nível de experiência.
34 ON_ROAD A unidade está num mosaico de estrada.

Apêndice C — opcodes do TA: Kingdoms

Note

Opcodes exclusivos do TA:K. Nenhum destes aparece nos ficheiros .cob comerciais do TA. DONT_SHADOW e MISSION_COMMAND usam as mnemónicas canónicas do Scriptor (o compilador/descompilador de Switeck que conhece o TAK); as operações matemáticas não têm semântica documentada — o Scriptor chama-lhes ?? e ????, por isso o kbot mantém os nomes provisórios TAK_MATH_09 / TAK_MATH_0A / TAK_MATH_0B. Os scripts comerciais do TA: Kingdoms usam cada um deles como operador binário (retira dois valores da pilha e põe lá um) para compactar e descompactar campos de bits. O TA 3.1c não tem tratamento para PLAY_SOUND, MISSION_COMMAND nem para os operadores matemáticos; o DONT_SHADOW executa-o como operação nula.

Opcode Mnemónica Em linha Ocorrências no TAK comercial Notas
0x1000A000 DONT_SHADOW 1 (piece) 66 Desativa a projeção de sombra de uma única peça. Fica na categoria de animação, ao lado de DONT_SHADE (0x1000E000) — o kbot chamava-lhe antes TAK_ANIM_0A. Tem no disco a mesma forma que as outras operações ANIM_*: opcode de 4 bytes + uma DWORD piece. Palavra-chave BOS: dont-shadow <piece>;.
0x10039000 TAK_MATH_09 0 264 Operador binário da categoria matemática, 4 bytes no total: ( a b -- r ). BOS: __tak_math_09(a, b).
0x1003A000 TAK_MATH_0A 0 — A mesma forma. Algumas tabelas de opcodes chamam-lhe BITWISE_NOT; o NOT bit a bit do TA é 0x10038000. BOS: __tak_math_0a(a, b).
0x1003B000 TAK_MATH_0B 0 66 A mesma forma que TAK_MATH_09. BOS: __tak_math_0b(a, b).
0x10073000 MISSION_COMMAND 2 (soundNameIdx, argCount) 3 885 Chamada de comando do motor. A primeira DWORD em linha é um índice na tabela SoundNames do COB; a segunda é o número de valores a retirar da pilha como argumentos. O opcode põe um único resultado de volta na pilha (normalmente descartado por um POP_STACK a seguir ou guardado com POP_STATIC/POP_LOCAL). É de longe o opcode exclusivo do TAK mais comum. Palavra-chave BOS: Mission-Command("name", args…), que corresponde à superfície canónica do Scriptor.

Codificação (resumo)

Cada palavra de opcode do COB é 0x10CCSSFF, em que CC é a categoria e SS o subcomando: estes opcodes do TAK limitam-se a ocupar posições CC:SS até então livres dentro das categorias existentes, em vez de introduzirem um byte de categoria novo. O DONT_SHADOW vive na categoria de animação, ao lado de DONT_CACHE/DONT_SHADE, e o MISSION_COMMAND na categoria de efeitos, ao lado de EXPLODE e PLAY_SOUND.

Tip

Compare com bytecode real.

kbot ctx use tak-30bb-flat
kbot cob disassemble scripts/araat.cob -a | grep -E 'DONT_SHADOW|MISSION|TAK_MATH|PLAY_SOUND'

Exemplos reais no catálogo de referência

Unidades reais que levam o formato aos extremos: abra-as para ver a complexidade real dos scripts, em vez de exemplos escritos no texto.

  • CORKROG — Krogoth (CORE) — um dos maiores COB do jogo (~25 KB, mais de 30 scripts, animações complexas com várias peças).
  • ARMCOM — Comandante ARM — unidade "rica" representativa: ciclos de marcha/pontaria/repouso/ construção, várias armas, tratamento do D-gun.
  • CORTRUCK — Camião de Construção — COB quase mínimo, usado como exemplo nesta página (3 scripts: SmokeUnit, Create, Killed).
  • ARMSAM — Samson — bom exemplo de uma unidade com torre (turret1 roda para apontar, animação de recuo, sincronização com wait-for-turn).

Depois de definir um contexto do kbot, descompile qualquer uma localmente com kbot cob decompile scripts/<name>.cob.


Ver também

  • 3DO — os nomes das peças referidos pelos scripts COB estão no .3do da unidade.
  • FBI — o ficheiro de metadados da unidade. O ObjectName que lá está liga o COB a um 3DO; Sounds define o banco de áudio que o script pode tocar.
  • A tabela de regras de cob lint da linha de comandos do kbot.
  • Glossário — piece, axis, signal mask, tick.