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
ObjectNamedo 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 deturnem andamento. O mesmo vale parawait-for-move.signal <n>mata todo script cujosignal_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:
- Os dois scripts que definem
SIG_MOVEsão mortos quando o sinal é disparado — inclusive o próprioMoveUnit(não tem importância; o trabalho dele já terminou). - 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 opcodesignalretorna. Idlenão defineSIG_MOVE, então o próximosignal SIG_MOVEnã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
ScriptCodeIndexArraysã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_VALUEvariam 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) eglobal_N(estática por unidade, que persiste entre as chamadas). O descompilador emitelocal_N/global_Npor padrão; renomeie-as no código-fonte para facilitar a leitura. - A ferramenta Scriptor da Cavedog às vezes embute
CURRENT_SPEEDcomo o literal29, o que quebra scripts no estilo deMoveWatcherdepois 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 (turret1gira para mirar, animação de recuo, sincronização comwait-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
.3doda unidade. - FBI — o arquivo de metadados da unidade. O
ObjectNamedele liga o COB a um 3DO;Soundsdefine o banco de áudio que o script pode tocar. - A tabela de regras de
cob lintda linha de comando do kbot. - Glossário — peça, eixo, máscara de sinais, tick.