Todas as páginas de formatos ↓
Unidades, scripts e configurações

COB / BOS — bytecode de script das unidades

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

Toda unidade do Total Annihilation roda um script que anima suas peças, emite os clarões das armas, toca efeitos sonoros e responde aos eventos do jogo. A linguagem-fonte é o BOS ("Basic Object Script"); o bytecode compilado que o motor de fato executa é o COB ("Cobble" — trocadilho da Cavedog para "BOS compilado"). Um arquivo por unidade, com o nome do ObjectName do FBI da unidade.

Tip

Experimente você mesmo.

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

Veja a referência de kbot cob da linha de comando para todas as opções (e a tabela completa de regras de verificação).

Em 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 arquivo

Etapa Formato Ferramentas
Fonte BOS — parecido com C, estruturado Escrito à mão; o Scriptor de Switeck; 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 por kbot cob compile

O motor só enxerga COB. O BOS é uma conveniência para os autores humanos: quando você "edita o script de uma unidade", na verdade está editando o código-fonte BOS, recompilando para COB e distribuindo o COB no add-on .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;
    }
}

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

Um código-fonte é compilado como script do TA (COB versão 4), a menos que comece com .version 6. O compilador recusa o que o TA 3.1c executaria errado, em vez de gravar um marcador provisório: as construções do TA: Kingdoms (play-sound, Mission-Command, __tak_math_*, .sound_name) e o % são erros num script do TA (o TA não tem instrução de módulo, e 0x10037000, a palavra em que o % costumava se transformar, é o seu XOR bit a bit), assim como identificadores desconhecidos e funções que precisam de mais do que as 32 posições de pilha do jogo. O que ainda compila, mas merece atenção — uma função definida duas vezes (toda chamada se liga à primeira definição, como faz a busca de nomes do jogo) — é impresso como aviso, junto com qualquer ocorrência das regras de verificação de compatibilidade com o TA; --strict faz os avisos falharem o comando. kbot cob assemble aplica as mesmas verificações às listagens escritas à mão.


Estrutura do COB no disco

Important

Dois dialetos. Esta seção descreve o COB v4, o dialeto que vem com o Total Annihilation. O TA: Kingdoms usa o COB v6 (VersionSignature == 6), que mantém tudo o que está abaixo, mas estende o cabeçalho em 8 bytes e insere uma tabela adicional de offsets de strings de comando depois do array de nomes de peças. Veja TA: Kingdoms — diferenças do COB v6 para os detalhes específicos do v6; a lista de opcodes vale para os 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 são os da estrutura HEADER_COB do Scriptor. Revisões anteriores do kbot chamavam LengthOfScripts / NumberOfStaticVars / UKZero / OffsetToNameArray de "Unknown0/1/2/3" — os mesmos offsets, só que agora com as identidades documentadas.

Depois vêm, nesta 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

Localizar um script

A tabela de índices de código contém offsets em palavras uint32, e não offsets 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 próximo (ou, no caso do último, no início da tabela de índices).

Important

Os offsets de strings nas tabelas de nomes são offsets absolutos no arquivo, e não relativos ao pool de strings. As ferramentas da Cavedog nunca mudavam o pool de strings de lugar, então isso não fazia diferença na prática; se você reorganizar um COB à mão, todos os offsets precisam ser atualizados.


O fluxo de opcodes

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

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

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

O byte mais alto é sempre 0x10; família/op/flags variam de opcode para opcode. O jogo não compara a palavra inteira: ele despacha por word & 0x100FF000, então os bits fora dessa máscara são ignorados e 0x10064001 é executado como JUMP (o desmontador do kbot escreve essa palavra como JUMP@0x10064001 para que ela seja montada de volta sem alterações). PUSH (0x10021000) e POP (0x10023000) leem então 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 seus operandos embutidos e seu efeito na pilha.

Famílias de opcodes

Família Exemplos Observações
0x100x MOVE, TURN, SPIN, SHOW, HIDE, EMIT_SFX Animação de peças. O nº da peça e o nº do eixo vêm embutidos em seguida.
0x101x WAIT_FOR_TURN, WAIT_FOR_MOVE, SLEEP Barreiras assíncronas — o script cede a vez até a condição ser satisfeita.
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 existe 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, não lógico.
0x106x START_SCRIPT, CALL_SCRIPT, DISCARD_CALL, JUMP, RETURN, JUMP_IF_FALSE, SIGNAL, SET_SIGNAL_MASK Fluxo de controle e concorrência.
0x107x EXPLODE, PLAY_SOUND Efeitos especiais. PLAY_SOUND existe só no TA: Kingdoms.
0x108x SET_VALUE, ATTACH_UNIT, DROP_UNIT Alterações no 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 offsets de salto são offsets em bytes relativos ao início da seçã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 deixar isso evidente.


Modelo de concorrência

Uma unidade pode ter vários scripts rodando ao mesmo tempo — Walk, Aim, SmokeUnit, MoveWatcher etc. Eles são escalonados de forma cooperativa:

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

A máscara de sinais (signal mask) é como os scripts implementam a preempção: o MoveWatcher define a máscara SIG_MOVE e acompanha a velocidade; quando a unidade para de se mover, ele envia SIG_MOVE, matando de forma limpa as animações de caminhada antes de começar a pose de repouso.

Warning

SET_SIGNAL_MASK só registra interesse — não mata nada. É enviar signal X que interrompe os scripts com a máscara X definida. A regra signal-never-signalled do kbot cob lint detecta scripts que definem uma máscara que ninguém nunca sinaliza (o que quase sempre é um bug).

Como a preempção acontece na prática

Um padrão comum: MoveUnit dispara um script de animação Walk; quando o movimento para, o motor envia SIG_MOVE para fazer uma limpeza organizada antes de 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 — inclusive o próprio MoveUnit (não tem importância; o trabalho dele já terminou).
  2. O script que sinaliza (MoveUnit) sobrevive o bastante para enviar o sinal antes de ser morto; o runtime verifica as máscaras depois que o opcode signal retorna.
  3. Idle não define SIG_MOVE, então o próximo signal SIG_MOVE não o afeta.

Exemplo comentado: cortruck.cob (Caminhã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 setas de JUMP_IF_FALSE/JUMP atravessando os corpos dos while/if — útil quando a saída do descompilador é ambígua.


Verificação (lint)

kbot cob lint (e a aba Lint do explorador de recursos) roda o conjunto de regras padrão do kbot-io: regras de estilo tiradas de bugs reais nos próprios scripts da Cavedog e em patches de mods enviados pela comunidade, duas verificações no nível do arquivo e — para todo COB que não declare a versão 6 — as regras de compatibilidade com o TA 3.1c. O catálogo de estilo:

Regra Gravidade Detecta
unused-piece aviso Peça declarada, mas nunca usada em nenhum script.
unused-static / unused-local aviso Variável alocada, 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 inexistente.
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 em sequência.
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 mascara 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 arquivo 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 apontam o que faz o TA 3.1c falhar ou se comportar de outro jeito. Elas são erros, aparecem na saída SARIF de --ci como as demais e ignoram os arquivos do TA: Kingdoms (versão 6):

Regra Detecta
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 ao chegar a um deles.
ta-unknown-opcode Uma palavra de opcode que nenhum jogo executa.
ta-push-flags Um PUSH cujos bits baixos não são 1 (constante), 2 (local) ou 4 (estática), ou um POP cujos bits baixos não são 2 ou 4 — inclusive os 0x10021000 e 0x10021008 sem flag que algumas ferramentas gravavam.
ta-stack-limit Mais de 32 STACK_ALLOC, ou variáveis locais mais valores pendentes além das 32 posições de pilha do jogo; o jogo não verifica isso e sobrescreve a memória depois do script.
ta-get-arguments Um GET alcançado com menos de cinco valores pendentes. O GET sempre desempilha uma porta e quatro argumentos, então um get com um argumento pega o restante das variáveis locais da função e as sobrescreve.
ta-stack-underflow Qualquer outra instrução que desempilha mais valores do que os 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→verificar→corrigir→compilar destaque os mods que precisam de manutenção.


Armadilhas

Warning

call-script é 0x10062000. 0x10063000 é outra instrução, DISCARD_CALL: ela tem as mesmas duas palavras embutidas (número do script e quantidade de argumentos), mas desempilha os argumentos e não executa nada. Nenhum script comercial a usa; o descompilador do kbot a escreve 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 módulo — o TA não tem instrução de módulo), 0x10038000 como NOT bit a bit unário (não 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 que faz o TA falhar. Listagens escritas com os nomes antigos (MOD, BITWISE_XOR, BITWISE_NOT, LOGICAL_XOR) continuam sendo montadas nas palavras de que vieram.

  • Todos os offsets do cabeçalho do COB são offsets absolutos no arquivo, mas as entradas de ScriptCodeIndexArray são offsets em palavras dentro da seção de código. É fácil confundi-los.
  • Os números de peças nos opcodes são índices na tabela de nomes de peças, e não strings. O COB traz os nomes num pool de strings; o runtime usa índices por questão de velocidade.
  • Os opcodes get UNIT_VALUE variam sutilmente entre o TA e o TA: Kingdoms. O TA: Kingdoms acrescentou IDs de valores para regeneração de magia, pontos de comando etc.; a tabela de valores do TA é mais compacta. O descompilador do kbot hoje rotula o conjunto do TA; as portas do TAK ≥ 21 saem como literais numéricos (que continuam fazendo a ida e volta pelo montador). Veja TA: Kingdoms — diferenças do COB v6 para todas as diferenças.
  • As variáveis vêm em dois tipos: local_N (posição de pilha local da função) e global_N (estática por unidade, que persiste entre as chamadas). O descompilador emite local_N / global_N por padrão; renomeie-as no código-fonte para facilitar a leitura.
  • A ferramenta Scriptor da Cavedog às vezes embute CURRENT_SPEED como o literal 29, o que quebra scripts no estilo de MoveWatcher depois de uma ida e volta. O descompilador do kbot restaura o nome simbólico; se você precisar fazer a ida e volta pelo Scriptor, use parênteses: (get CURRENT_SPEED) > 5.

Tamanhos típicos

Recurso Faixa observada em scripts/*.cob da Cavedog
Tamanho do arquivo 0,5 KB – 25 KB
Custo fixo 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 seçã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 seçã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 pool de strings, o mesmo esquema de codificação de opcodes e o mesmo mecanismo de nomes de scripts / nomes de peças; as diferenças são um cabeçalho estendido, uma tabela de strings adicional acrescentada depois do array de nomes de peças e quatro opcodes novos.

Estrutura do arquivo 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 (fim do cabeçalho canônico de 11 DWORDs, no byte 44) e o início da seção de código (OffsetToScriptCode, normalmente 0x34 = 52). Ele traz dois uint32 little-endian:

Campo Significado
[0] Offset absoluto no arquivo da tabela de offsets de nomes de sons. Sempre igual a OffsetToPieceNameOffsetArray + NumPieces × 4 — ou seja, o byte logo depois do array de offsets de nomes de peças. Redundante com a disposição canônica; o gravador o reconstrói.
[1] Quantidade de nomes de sons na tabela. Contagem confiável.

Cada entrada da tabela de offsets de nomes de sons é um offset absoluto no arquivo de uma string terminada em NUL no pool de strings — coisas como "SetMission o 1, s", "create ARASWORD", "ARROW10". O bytecode se refere a elas pelo índice (não pelo offset) por meio do opcode MISSION_COMMAND (veja o Apêndice C). A tabela se chama array de nomes de sons porque o compilador canônico, o Scriptor, registra strings nela pela ação ADDSOUND; o conteúdo real são strings de comando do motor, e não nomes de arquivos de áudio.

A representação no kbot

scripting.COB guarda os nomes de sons num campo estruturado []string — sem preservar bytes opacos. Todo o resto (os valores do subcabeçalho, a tabela de offsets, os offsets de cada string no pool) é reconstruído a partir dessa lista na gravação:

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

Nos arquivos v4, a slice é sempre nil, e tanto o subcabeçalho quanto a tabela de offsets de nomes de sons são omitidos da saída.

O desmontador e o descompilador emitem cada string como uma diretiva .sound_name, para que elas 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 arquivos .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 no-op (sem efeito). A semântica exata deles no jogo não está documentada, mas o formato binário (tamanho + quantidade de parâmetros embutidos) é conhecido a partir de todos os .cob comerciais do TAK, o que basta para que a ida e volta desmontar→montar recupere os bytes originais exatamente. Veja o Apêndice C para a referência completa.

Situação 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 ✅ Scripts de unidades simples e muitos COBs de missão (≈ 70 / 267) fazem a ida e volta perfeita byte a byte. As falhas restantes são padrões de fluxo de controle em que a reconstrução estruturada de if/while do descompilador escolhe uma disposição de JUMP que o compilador regenera de forma um pouco diferente — o bytecode é semanticamente equivalente (todo opcode do TAK, string de comando, subcabeçalho e a palavra de versão v6 sobrevivem), só não é idêntico byte a byte. O mesmo tipo de divergência afetaria qualquer ciclo descompilar→compilar num script não trivial.

A sintaxe BOS das extensões do TAK

O descompilador emite estas pseudochamadas para que os COBs 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 no-op). 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 (ele os rotula como ?? e ????), então o kbot mantém os marcadores 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 do som como string entre aspas — o compilador o procura na tabela de nomes de sons (por isso a diretiva .sound_name correspondente precisa aparecer antes no arquivo). Os demais argumentos são empilhados, e o motor os consome.

Duas diretivas no topo do arquivo carregam o restante dos metadados do v6:

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

.version é sempre emitida (para que os arquivos v4 do TA façam a ida e volta de forma idêntica pelo mesmo processo). Cada .sound_name é uma entrada na tabela de nomes de sons do COB; o gravador reconstrói o subcabeçalho de 8 bytes e a tabela de offsets de nomes de sons a partir de len(SoundNames) e da ordem em que as entradas aparecem.

O epílogo STACK_ALLOC ; RETURN que todo return; sem valor emite no v6 do TAK é tratado pelo compilador: return; vira STACK_ALLOC ; RETURN quando há .version 6, e return <expr>; vira <expr> ; RETURN sem prefixo (como faz o compilador da Cavedog).

Reproduza na sua própria instalação:

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

Símbolos de portas acima da porta 20

Os scripts do TA: Kingdoms usam portas de get_unit_value até a 46 (CURRENT_SPEED, VETERAN_LEVEL, campos da reserva de magia etc.). O descompilador do kbot hoje só rotula simbolicamente as portas 1–20 (veja o Apêndice B); as portas do TAK ≥ 21 saem como literais numéricos. O bytecode as guarda como inteiros de qualquer forma, então as idas e voltas não são afetadas — só a legibilidade do BOS sofre. O acréscimo dos nomes das portas do TAK é acompanhado à parte.


Apêndice A — referência completa de opcodes

Todas as instruções que o TA ou o TA: Kingdoms executam. A fonte de verdade é o formats/scripting/opcodes.go do kbot-io. A notação de pilha usa o estilo do Forth, ( before -- after ). A coluna Embutidos mostra quantas palavras uint32 vêm depois da própria palavra do opcode (consumidas na decodificação, não da pilha).

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

Opcode Mnemônico Embutidos 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 a executa, mas os scripts de unidades dele não fazem nada com ela; nenhum script comercial a usa.
0x1000A000 DONT_SHADOW 1 (piece) ( -- ) dont-shadow <piece>; — o TA a executa como no-op; 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ônico Embutidos 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ônico Embutidos Efeito na pilha Sintaxe BOS
0x10021000 PUSH_IMM 1 (value) — PUSH sem flag: o jogo falha com ele (ta-push-flags). Nunca é gravado pelo compilador.
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 var e por 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ônico Embutidos 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ônico Embutidos 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, …) — sempre desempilha uma porta e quatro argumentos; o compilador empilha zeros no lugar dos que faltam
0x10044000 IS_CARRYING_UNIT 0 ( unit -- bool ) __is_carrying_unit(unit) — 1 quando aquela unidade está sendo carregada por esta
0x10045000 CARRIER_UNIT_ID 0 ( -- id ) __carrier_unit_id() — a unidade que carrega esta, ou 0

Veja o Apêndice B para o catálogo de portas.

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

Opcode Mnemônico Embutidos 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), não um XOR lógico
0x1005A000 LOGICAL_NOT 0 ( a -- !a ) NOT a

Fluxo de controle e concorrência (0x106x)

Opcode Mnemônico Embutidos 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…); — desempilha os argumentos (mais de quatro estouram o buffer do jogo) e não executa nada
0x10064000 JUMP 1 (offset) ( -- ) (compilador — goto, laços)
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 offset em bytes relativo a OffsetToScriptCode, e não à instrução atual.

Efeitos (0x107x)

Opcode Mnemônico Embutidos 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 ela

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

Opcode Mnemônico Embutidos 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

Toda palavra de opcode é 0x10CCSSFF, em que:

  • CC = o byte de categoria (animação, matemática, fluxo de controle, …).
  • SS = o nibble de subcomando, no nibble alto do segundo byte a partir do topo.
  • FF = o byte de flags.

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

Palavras que nenhum jogo executa aparecem como UNKNOWN_0x10xxxxxx no desmontador (e são apontadas por ta-unknown-opcode); se você encontrar uma num arquivo real, abra uma issue com a amostra do COB.


Apêndice B — tabela de portas de get_unit_value

As 20 portas que o descompilador do kbot hoje rotula simbolicamente. O TA real tem ~80; o TA: Kingdoms acrescenta mais ~15 para grandezas relacionadas à magia. Qualquer porta acima da 20 aparece hoje como literal numérico na saída do descompilador.

Porta Nome simbólico Retorna Observações
1 ACTIVATION bool O botão "ativo" da unidade está ligado (radar, bloqueador, fortificações)?
2 STANDINGMOVEORDERS enum 0 = manter posição, 1 = manobrar, 2 = vagar.
3 STANDINGFIREORDERS enum 0 = cessar fogo, 1 = revidar, 2 = fogo à vontade.
4 HEALTH 0–100 Porcentagem restante de MaxDamage.
5 INBUILDSTANCE bool A unidade está estendendo os nanotornos neste momento?
6 BUSY bool A unidade está ocupada com uma ordem neste momento?
7 PIECE_XZ compactado X/Z combinados de uma peça (codificados). Usado para mirar 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 miram).
13 XZ_HYPOT int Hipotenusa entre dois valores codificados em XZ.
14 ATAN int atan2(y, x) de dois valores empilhados.
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 por SmokeUnit para saber o andamento da construção. 0 depois de construída.
18 YARD_OPEN bool Para construções com yard map: as portas estão abertas?
19 BUGGER_OFF bool Definido pelo motor quando a unidade precisa sair do caminho.
20 ARMORED bool A postura blindada da unidade está ativada (Bulldog, Goliath etc.)?

Entre as portas mais altas estão CURRENT_SPEED (29 — famosa por ser embutida como literal pelo Scriptor da Cavedog; veja Armadilhas acima), MAX_ID, UNIT_TEAM, UNIT_BUILD_PERCENT, BUILDER, UNIT_ALLIED e muitas outras. 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 amplia a faixa de portas. As portas exclusivas do TAK que o kbot reconhece (o descompilador exibe o nome simbólico; o compilador o aceita como entrada) vêm da tabela [UNITVLAUES] do Scriptor:

Porta Símbolo Observações
21 WEAPON_AIM_ABORTED Definido quando a solução de mira de uma arma é cancelada.
22 WEAPON_READY A arma foi recarregada e está pronta para disparar.
23 WEAPON_LAUNCH_NOW O motor disparou a arma.
26 FINISHED_DYING A animação de morte terminou.
27 ORIENTATION Direção para a qual a unidade está voltada.
28 IN_WATER A unidade está na água.
29 CURRENT_SPEED Velocidade de movimento atual (compartilhada com o TA — o Scriptor a numera da mesma forma aqui).
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 bloco de estrada.

Apêndice C — opcodes do TA: Kingdoms

Note

Opcodes exclusivos do TA:K. Nenhum deles aparece nos .cob comerciais do TA. DONT_SHADOW e MISSION_COMMAND usam os mnemônicos canônicos 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 as rotula como ?? e ????, então o kbot mantém os marcadores 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 (dois valores desempilhados, um empilhado) ao 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; DONT_SHADOW, ele executa como no-op.

Opcode Mnemônico Embutidos Ocorrências no TAK comercial Observações
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 antes a chamava de TAK_ANIM_0A. Mesmo formato no disco das 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 — Mesmo formato. Algumas tabelas de opcodes o chamam de BITWISE_NOT; o NOT bit a bit do TA é 0x10038000. BOS: __tak_math_0a(a, b).
0x1003B000 TAK_MATH_0B 0 66 Mesmo formato de 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 embutida é um índice na tabela SoundNames do COB; a segunda é a quantidade de valores a desempilhar como argumentos. O opcode empilha um único resultado de volta (normalmente descartado por um POP_STACK em seguida ou guardado por POP_STATIC/POP_LOCAL). É de longe o opcode exclusivo do TAK mais comum. Palavra-chave BOS: Mission-Command("name", args…) — igual à sintaxe canônica do Scriptor.

Codificação (recapitulação)

Toda palavra de opcode do COB é 0x10CCSSFF, em que CC é a categoria e SS é o subcomando — esses opcodes do TAK simplesmente ocupam posições CC:SS antes não usadas dentro das categorias existentes, em vez de introduzir um novo byte de categoria. DONT_SHADOW fica na categoria de animação, junto com DONT_CACHE/DONT_SHADE, e MISSION_COMMAND na categoria de efeitos, junto com 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 montados no texto:

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

Descompile qualquer um localmente com kbot cob decompile scripts/<name>.cob depois de definir um contexto do kbot.


Veja também

  • 3DO — os nomes de peças referenciados pelos scripts COB ficam no .3do da unidade.
  • FBI — o arquivo de metadados da unidade. O ObjectName dele 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 comando do kbot.
  • Glossário — peça, eixo, máscara de sinais, tick.