Todas las páginas de formatos ↓
Unidades, scripts y configuración

COB / BOS: bytecode de los scripts de unidades

Fuente: coreprime/kbot @ 0ab97bf · Editar en GitHub

Cada unidad de Total Annihilation ejecuta un script que anima sus piezas, emite los destellos de las armas, reproduce efectos de sonido y responde a los eventos del juego. El lenguaje fuente es BOS («Basic Object Script»); el bytecode compilado que ejecuta realmente el motor es COB («Cobble», el juego de palabras de Cavedog para «BOS compilado»). Hay un archivo por unidad, con el nombre del ObjectName que figura en su FBI.

Tip

Pruébalo tú mismo.

# Decompile a unit's bytecode back to readable BOS
kbot cob decompile scripts/ARMCOM.cob

# Disassemble to opcode-level assembly with control-flow arrows
kbot cob disassemble scripts/ARMCOM.cob -a

# Lint a whole directory for common script issues
kbot cob lint scripts/ -q

# Round-trip the entire scripts/ tree to verify byte-perfect compile
kbot cob roundtrip scripts/

Consulta la referencia de kbot cob de la CLI para ver todas las opciones (y la tabla completa de reglas de lint).

Desde Go. Usa formats/scripting de 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)
}

Dos lenguajes, un formato de archivo

Etapa Formato Herramientas
Fuente BOS: estructurado, parecido a C Escrito a mano; Scriptor de Switeck; kbot cob decompile lo recupera de un COB
Compilado COB: bytecode basado en pila Lo carga el motor; lo generan Scriptor o kbot cob compile

El motor solo ve COB. BOS es una comodidad para los autores humanos: cuando «editas el script de una unidad», en realidad editas el fuente BOS, lo recompilas a COB y distribuyes el COB en el .ufo complementario de la unidad.

Un ejemplo 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 lo traduce a un bloque binario. kbot cob disassemble -a muestra el flujo de opcodes junto a flechas de flujo de control; kbot cob decompile hace lo contrario y recupera un BOS que casi siempre vuelve a compilar idéntico byte a byte (kbot cob roundtrip lo valida en todo el catálogo del juego).

Un fuente se compila como script de TA (COB versión 4) salvo que empiece por .version 6. El compilador rechaza lo que TA 3.1c ejecutaría mal en vez de escribir un relleno: las construcciones de TA: Kingdoms (play-sound, Mission-Command, __tak_math_*, .sound_name) y % son errores en un script de TA (TA no tiene instrucción de módulo, y 0x10037000, la palabra en la que se convertía %, es su XOR bit a bit), igual que los identificadores desconocidos y las funciones que necesitan más de las 32 posiciones de pila del juego. Lo que aún compila pero merece una revisión (una función definida dos veces: toda llamada se enlaza con la primera definición, igual que hace la búsqueda por nombre del juego) se muestra como aviso, junto con cualquier hallazgo de las reglas de lint de compatibilidad con TA; --strict hace que los avisos hagan fallar el comando. kbot cob assemble aplica las mismas comprobaciones a los listados escritos a mano.


Estructura en disco de COB

Important

Dos dialectos. Esta sección describe COB v4, el dialecto que incluye Total Annihilation. TA: Kingdoms usa COB v6 (VersionSignature == 6), que conserva todo lo que sigue pero amplía la cabecera en 8 bytes e inserta una tabla adicional de desplazamientos de cadenas de órdenes después del array de nombres de piezas. Consulta TA: Kingdoms: diferencias de COB v6 para los detalles de v6; la lista de opcodes se aplica a ambos, y los opcodes adicionales exclusivos de TAK se documentan en el 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

Los nombres canónicos de los campos coinciden con la estructura HEADER_COB de Scriptor. Las revisiones anteriores de kbot llamaban «Unknown0/1/2/3» a LengthOfScripts / NumberOfStaticVars / UKZero / OffsetToNameArray: mismos desplazamientos, solo que ahora con su identidad documentada.

A continuación vienen, en este orden:

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

Cómo localizar un script

La tabla de índices de código contiene desplazamientos en palabras uint32, no en bytes. Para encontrar el bytecode del script número i:

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

El script termina donde empieza el siguiente (o, en el caso del último, al principio de la tabla de índices).

Important

Los desplazamientos de cadena de las tablas de nombres son desplazamientos absolutos en el archivo, no relativos al pool de cadenas. Las herramientas de Cavedog nunca movían el pool de cadenas, así que en la práctica daba igual; si reorganizas un COB a mano, hay que actualizar todos los desplazamientos.


El flujo de opcodes

COB es una máquina virtual basada en pila. La mayoría de los opcodes sacan sus argumentos de una pila de datos propia de cada script, hacen su trabajo y vuelven a apilar un resultado. Algunos opcodes llevan operandos en línea (destinos de salto, números de pieza).

Cada opcode es un uint32 con esta estructura:

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

El byte alto siempre es 0x10; familia, operación y flags varían según el opcode. El juego no compara la palabra entera: despacha según word & 0x100FF000, así que los bits fuera de esa máscara se ignoran y 0x10064001 se ejecuta como JUMP (el desensamblador de kbot escribe esa palabra como JUMP@0x10064001 para que se vuelva a ensamblar sin cambios). Después, PUSH (0x10021000) y POP (0x10023000) leen su origen o su destino de los tres bits bajos. scripting/opcodes.go de kbot-io es el catálogo; scripting.Opcodes() enumera cada instrucción con sus operandos en línea y su efecto en la pila.

Familias de opcodes

Familia Ejemplos Notas
0x100x MOVE, TURN, SPIN, SHOW, HIDE, EMIT_SFX Animación de piezas. El n.º de pieza y el n.º de eje van a continuación, en línea.
0x101x WAIT_FOR_TURN, WAIT_FOR_MOVE, SLEEP Barreras asíncronas: el script cede el control hasta que se cumple la condición.
0x102x PUSH_CONST, PUSH_LOCAL, PUSH_STATIC, POP_* Manipulación de la pila.
0x103x ADD, SUB, MUL, DIV, BITWISE_AND, BITWISE_OR, XOR, NOT Aritmética y operaciones bit a bit. No hay módulo.
0x104x RAND, GET_UNIT_VALUE, GET, IS_CARRYING_UNIT, CARRIER_UNIT_ID Consultas al motor.
0x105x <, <=, >, >=, ==, !=, &&, ||, XOR_ALT, ! Comparaciones y operaciones lógicas. XOR_ALT es un XOR bit a bit, no lógico.
0x106x START_SCRIPT, CALL_SCRIPT, DISCARD_CALL, JUMP, RETURN, JUMP_IF_FALSE, SIGNAL, SET_SIGNAL_MASK Flujo de control y concurrencia.
0x107x EXPLODE, PLAY_SOUND Efectos especiales. PLAY_SOUND solo existe en TA: Kingdoms.
0x108x SET_VALUE, ATTACH_UNIT, DROP_UNIT Modificaciones del estado del motor.

Una muestra pequeña pero 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

Los desplazamientos de salto son desplazamientos en bytes relativos al inicio de la sección de código de los scripts (no a la instrucción actual). JUMP 0x4C significa «salta a OffsetToScriptCode + 0x4C». La opción -a del desensamblador dibuja flechas entre las instrucciones relacionadas para que se vea claramente.


Modelo de concurrencia

Una unidad puede tener muchos scripts ejecutándose a la vez: Walk, Aim, SmokeUnit, MoveWatcher, etc. Su planificación es cooperativa:

  • sleep <ticks> cede el control durante N ticks (1/30 s cada uno).
  • wait-for-turn <piece> around <axis> cede el control hasta que esa pieza termina su animación turn en curso. Lo mismo vale para wait-for-move.
  • signal <n> mata todos los scripts cuyo signal_mask & n != 0.
  • set-signal-mask <n> declara qué señales le interesan al script actual.

La máscara de señales es el mecanismo con el que los scripts implementan la expropiación (preemption): MoveWatcher establece la máscara SIG_MOVE y vigila la velocidad; cuando la unidad deja de moverse, envía SIG_MOVE y mata limpiamente las animaciones de caminar antes de iniciar la postura de reposo.

Warning

SET_SIGNAL_MASK solo registra el interés; no mata nada. Lo que detiene los scripts que tienen la máscara X es enviar signal X. La regla signal-never-signalled de kbot cob lint detecta los scripts que establecen una máscara que nadie llega a señalar (lo que casi siempre es un error).

Cómo se produce realmente la expropiación

Un patrón habitual: MoveUnit lanza un script de animación Walk; cuando el movimiento se detiene, el motor envía SIG_MOVE para dejarlo todo limpio antes de que empiece Idle. La secuencia:

   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)

Tres cosas a tener en cuenta:

  1. Los dos scripts que establecen SIG_MOVE mueren cuando se dispara la señal, incluido el propio MoveUnit (no importa: su trabajo ya está hecho).
  2. El script que envía la señal (MoveUnit) sobrevive lo suficiente para enviarla antes de morir; el intérprete comprueba las máscaras después de que vuelva el opcode signal.
  3. Idle no establece SIG_MOVE, así que el siguiente signal SIG_MOVE no lo tocará.

Ejemplo desarrollado: cortruck.cob (Core Construction Truck)

kbot cob decompile scripts/cortruck.cob:

piece base;

SmokeUnit() {
    var local_0, local_1, local_2;

    while (get BUILD_PERCENT_LEFT) { sleep 400; }

    while (1) {
        local_0 = get HEALTH;
        if (local_0 < 66) {
            local_2 = (256 | 2);
            if (rand(1, 66) < local_0) { local_2 = (256 | 1); }
            emit-sfx local_2 from base;
        }
        local_1 = (local_0 * 50);
        if (local_1 < 200) { local_1 = 200; }
        sleep local_1;
    }
    return 0;
}

Create() { return 0; }

Killed() {
    var severity;
    if (severity <= 25) { explode base type (32 | 256); return 0; }
    if (severity <= 50) { explode base type (32 | 256); return 0; }
    if (severity <= 99) { explode base type (32 | 256); return 0; }
    explode base type (32 | 256);
}

El desensamblado correspondiente (kbot cob disassemble -a -s SmokeUnit) muestra el flujo real de opcodes, con flechas de JUMP_IF_FALSE/JUMP que recorren los cuerpos de los while/if; resulta útil cuando la salida del descompilador es ambigua.


Revisión con lint

kbot cob lint (y la pestaña Lint del explorador de recursos) ejecuta el conjunto de reglas predeterminado de kbot-io: reglas de estilo extraídas de errores reales en los propios scripts de Cavedog y en parches de mods enviados por la comunidad, dos comprobaciones a nivel de archivo y, para cada COB que no declare la versión 6, las reglas de compatibilidad con TA 3.1c. El catálogo de estilo:

Regla Gravedad Detecta
unused-piece aviso Pieza declarada pero que ningún script usa.
unused-static / unused-local aviso Variable reservada que nunca se lee.
always-true / dead-code info / aviso if(1) / while(0).
long-function / high-complexity aviso Más de 100 instrucciones o complejidad ciclomática mayor que 15.
invalid-call error call-script a una función que no existe.
speed-zero aviso move/turn con speed <0>: la animación nunca termina.
empty-function info El cuerpo de la función es solo return 0.
duplicate-animation aviso Dos órdenes de animación idénticas seguidas.
sleep-only-guard info Un bloque if que no contiene más que sleep.
duplicate-if info Dos if seguidos con la misma condición.
raw-signal info signal 4 en lugar de signal SIG_AIM.
unnamed-global info global_3 en lugar de una variable estática con nombre.
signal-never-signalled aviso El script enmascara una señal que nadie emite.
recursive-call aviso Ciclo de call-script.
duplicate-function aviso Dos scripts comparten nombre; el juego solo llama al primero.
malformed-cob error / aviso El archivo no carga o el código de un script está truncado (error); tablas de nombres que el lector toleró (aviso).

Las reglas de compatibilidad con TA señalan aquello con lo que TA 3.1c falla o que ejecuta de otra manera. Son errores, aparecen en la salida SARIF de --ci como las demás y omiten los archivos de TA: Kingdoms (versión 6):

Regla Detecta
ta-kingdoms-opcode PLAY_SOUND, MISSION_COMMAND o un operador TAK_MATH_* en un script de TA. TA no tiene manejador para ellos y el script falla al llegar a uno.
ta-unknown-opcode Una palabra de opcode que ningún juego ejecuta.
ta-push-flags Un PUSH cuyos bits bajos no son 1 (constante), 2 (local) ni 4 (estática), o un POP cuyos bits bajos no son 2 ni 4, incluidos los 0x10021000 y 0x10021008 sin flags que escribían algunas herramientas.
ta-stack-limit Más de 32 STACK_ALLOC, o variables locales más valores pendientes por encima de las 32 posiciones de pila del juego; el juego no lo comprueba y sobrescribe la memoria que hay después del script.
ta-get-arguments Un GET al que se llega con menos de cinco valores pendientes. GET siempre saca de la pila un puerto y cuatro argumentos, así que un get con un solo argumento toma el resto de las variables locales de la función y las sobrescribe.
ta-stack-underflow Cualquier otra instrucción que saque más valores de los que hay pendientes, o caminos que confluyen con distinta profundidad de pila.
ta-discard-call DISCARD_CALL con más de cuatro argumentos.

Las descripciones completas de las reglas están en el README de kbot. El descompilador conserva a propósito la numeración original de variables y piezas, de modo que un ciclo descompilar→lint→corregir→compilar pone de relieve los mods que necesitan mantenimiento.


Escollos

Warning

call-script es 0x10062000. 0x10063000 es otra instrucción, DISCARD_CALL: tiene las mismas dos palabras en línea (número de script y número de argumentos), pero saca los argumentos de la pila y no ejecuta nada. Ningún script de la versión comercial la usa; el descompilador de kbot la escribe como __discard_call(word, args…).

Warning

Varias tablas de opcodes publicadas dan nombres equivocados a las palabras bit a bit. TA ejecuta 0x10037000 como XOR bit a bit (no como módulo: TA no tiene instrucción de módulo), 0x10038000 como el NOT bit a bit unario (no como XOR) y 0x10059000 como un segundo XOR bit a bit (no un XOR lógico: 6 XOR_ALT 3 es 5). 0x1003A000 es un operador de TA: Kingdoms con el que TA falla. Los listados escritos con los nombres antiguos (MOD, BITWISE_XOR, BITWISE_NOT, LOGICAL_XOR) siguen ensamblándose en las palabras de las que salieron.

  • Todos los desplazamientos de la cabecera COB son desplazamientos absolutos en el archivo, pero las entradas de ScriptCodeIndexArray son desplazamientos en palabras dentro de la sección de código. Es fácil confundirlos.
  • Los números de pieza de los opcodes son índices en la tabla de nombres de piezas, no cadenas. El COB guarda los nombres en un pool de cadenas; el intérprete usa índices por velocidad.
  • Los opcodes get UNIT_VALUE varían sutilmente entre TA y TA: Kingdoms. TA: Kingdoms añadió identificadores de valor para la regeneración de magia, los puntos de mando, etc.; la tabla de valores de TA es más compacta. Por ahora el descompilador de kbot pone nombre al conjunto de TA; los números de puerto de TAK ≥ 21 se emiten como literales numéricos (que siguen haciendo el viaje de ida y vuelta por el ensamblador). Consulta Diferencias de COB v6 en TA: Kingdoms para la lista completa de diferencias.
  • Las variables son de dos tipos: local_N (posición de pila local de la función) y global_N (variable estática de cada unidad que persiste entre llamadas). El descompilador emite local_N / global_N por defecto; renómbralas en el fuente para que se lea mejor.
  • La herramienta Scriptor de Cavedog a veces inserta CURRENT_SPEED como el literal 29, lo que rompe los scripts del estilo de MoveWatcher tras un viaje de ida y vuelta. El descompilador de kbot restaura el nombre simbólico; si no tienes más remedio que pasar por Scriptor, usa paréntesis: (get CURRENT_SPEED) > 5.

Tamaños habituales

Recurso Rango observado en los scripts/*.cob de Cavedog
Tamaño del archivo 0,5 KB – 25 KB
Coste de la cabecera siempre 44 bytes
Scripts por unidad 3 – 30 (unidades típicas: 5–10)
Piezas por unidad 3 – 80 (Krogoth ≈ 80)
Tamaño de la sección de código 300 B – 20 KB (varía muchísimo con la complejidad)
armcom.cob (comandante) ~14 KB, 30 scripts
cortruck.cob (unidad sencilla) ~750 B, 3 scripts

Diferencias de COB v6 en TA: Kingdoms

Note

Esta sección recoge solo las diferencias de TA:K. Todo lo anterior describe COB v4 (TA). TA: Kingdoms conserva la misma máquina virtual de pila, el mismo pool de cadenas, el mismo esquema de codificación de opcodes y el mismo mecanismo de nombres de scripts y de piezas; las diferencias son una cabecera ampliada, una tabla de cadenas adicional añadida tras el array de nombres de piezas y cuatro opcodes nuevos.

Estructura del archivo v6

┌─ 44-byte v4 header ──────────────────────────────────┐
│ VersionSignature == 6, NumScripts, NumPieces, …      │
├─ Sub-header (8 bytes, TAK-only) ─────────────────────┤  (soundNameOffsetTableStart, soundNameCount)
├─ Script code (uint32 opcode stream) ─────────────────┤  same encoding as v4
├─ ScriptCodeIndexArray (uint32 × NumScripts) ─────────┤  unchanged
├─ ScriptNameOffsetArray (uint32 × NumScripts) ────────┤  unchanged
├─ PieceNameOffsetArray (uint32 × NumPieces) ──────────┤  unchanged
├─ SoundNameOffsetArray (uint32 × Count) ──────────────┤  TAK-only — count from sub-header[1]
└─ String pool ────────────────────────────────────────┘  script names → piece names → sound names (in that order)

La subcabecera de 8 bytes se sitúa entre OffsetToNameArray (el final de la cabecera canónica de 11 DWORD, en el byte 44) y el inicio de la sección de código (OffsetToScriptCode, normalmente 0x34 = 52). Contiene dos uint32 little-endian:

Campo Significado
[0] Desplazamiento absoluto en el archivo de la tabla de desplazamientos de nombres de sonido. Siempre igual a OffsetToPieceNameOffsetArray + NumPieces × 4, es decir, el byte justo después del array de desplazamientos de nombres de piezas. Es redundante con la estructura canónica; el escritor lo reconstruye.
[1] Número de nombres de sonido de la tabla. Es el recuento que manda.

Cada entrada de la tabla de desplazamientos de nombres de sonido es el desplazamiento absoluto en el archivo de una cadena terminada en NUL del pool de cadenas, cosas como "SetMission o 1, s", "create ARASWORD" o "ARROW10". El bytecode las referencia por índice (no por desplazamiento) mediante el opcode MISSION_COMMAND (consulta el Apéndice C). La tabla se llama array de nombres de sonido porque el compilador canónico Scriptor registra en ella las cadenas mediante la acción ADDSOUND; su contenido real son cadenas de órdenes del motor, no nombres de archivos de audio.

Cómo lo representa kbot

scripting.COB guarda los nombres de sonido como un campo estructurado []string, sin conservar bytes opacos. Todo lo demás (los valores de la subcabecera, la tabla de desplazamientos, el desplazamiento de cada cadena en el pool) se reconstruye a partir de esta lista al escribir:

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

En los archivos v4 el slice es siempre nil, y tanto la subcabecera como la tabla de desplazamientos de nombres de sonido se omiten en la salida.

El desensamblador y el descompilador emiten cada cadena como una directiva .sound_name para que sobreviva a un viaje de ida y vuelta por .coba y .bos:

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

Opcodes nuevos

Los archivos .cob de TAK usan instrucciones para las que TA 3.1c no tiene manejador (PLAY_SOUND, MISSION_COMMAND y los operadores TAK_MATH_*), además de DONT_SHADOW, que TA ejecuta como una operación nula (no-op). Su semántica exacta en el juego no está documentada, pero su estructura binaria (tamaño + número de parámetros en línea) se conoce a partir de todos los .cob de la versión comercial de TAK, y eso basta para que el viaje desensamblar→ensamblar recupere los bytes originales tal cual. Consulta el Apéndice C para la referencia completa.

Estado del viaje de ida y vuelta

Proceso TA TA: Kingdoms
disassemble → assemble (idéntico byte a byte) 278 / 278 ✅ 267 / 267 ✅
decompile → compile (idéntico byte a byte) 278 / 278 ✅ Los scripts de unidades sencillos y muchos COB de misiones (≈ 70 / 267) hacen el viaje de ida y vuelta byte a byte. Los fallos restantes son patrones de flujo de control en los que la reconstrucción estructurada de if/while del descompilador elige una disposición de JUMP que el compilador regenera de forma ligeramente distinta: el bytecode es semánticamente equivalente (sobreviven todos los opcodes de TAK, las cadenas de órdenes, la subcabecera y la palabra de versión v6), solo que no es idéntico byte a byte. El mismo tipo de discrepancia afectaría a cualquier ciclo descompilar→compilar sobre un script no trivial.

Sintaxis BOS de las extensiones de TAK

El descompilador emite estas pseudollamadas para que los COB de TAK hagan el viaje de ida y vuelta a través de BOS. El compilador acepta la misma sintaxis, pero solo después de .version 6 (salvo dont-shadow, que TA ejecuta como no-op). dont-shadow y Mission-Command usan las palabras clave canónicas de TAK de Scriptor; los opcodes matemáticos no tienen un nombre documentado en Scriptor (los etiqueta como ?? y ????), así que kbot mantiene los marcadores provisionales __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

El primer argumento de Mission-Command es el nombre de sonido como cadena entre comillas: el compilador lo busca en la tabla de nombres de sonido (por eso la directiva .sound_name correspondiente debe aparecer antes en el archivo). Los demás argumentos se apilan y el motor los consume.

Dos directivas al principio del archivo llevan el resto de los metadatos de v6:

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

.version se emite siempre (para que los archivos v4 de TA hagan el viaje de ida y vuelta idénticos por el mismo proceso). Cada .sound_name es una entrada de la tabla de nombres de sonido de ese COB; el escritor reconstruye la subcabecera de 8 bytes y la tabla de desplazamientos de nombres de sonido a partir de len(SoundNames) y del orden en que aparecen las entradas.

El compilador se encarga del epílogo STACK_ALLOC ; RETURN de TAK que emite en v6 todo return; sin valor: return; se traduce a STACK_ALLOC ; RETURN con .version 6, y return <expr>; se traduce a <expr> ; RETURN sin prefijo (igual que el compilador de Cavedog).

Reprodúcelo en tu propia instalación:

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

Símbolos de puerto por encima del puerto 20

Los scripts de TA: Kingdoms usan puertos de get_unit_value hasta el 46 (CURRENT_SPEED, VETERAN_LEVEL, campos de la reserva de magia, etc.). Por ahora el descompilador de kbot solo da nombre simbólico a los puertos 1–20 (consulta el Apéndice B); los puertos de TAK ≥ 21 se emiten como literales numéricos. El bytecode los guarda como enteros en cualquier caso, así que el viaje de ida y vuelta no se ve afectado; solo pierde legibilidad el BOS. La incorporación de los nombres de puerto de TAK se sigue por separado.


Apéndice A: referencia completa de opcodes

Todas las instrucciones que ejecutan TA o TA: Kingdoms. La referencia que manda es formats/scripting/opcodes.go de kbot-io. La notación de pila sigue el estilo de Forth, ( before -- after ). La columna En línea indica cuántas palabras uint32 siguen a la propia palabra del opcode (se consumen al decodificar, no de la pila).

Animación: manipulación de piezas (0x100x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis BOS
0x10001000 MOVE 2 (piece, axis) ( speed dist -- ) move <piece> to <axis> <dist> speed <speed>;
0x10002000 TURN 2 (piece, axis) ( speed dir -- ) turn <piece> to <axis> <dir> speed <speed>;
0x10003000 SPIN 2 (piece, axis) ( speed -- ) spin <piece> around <axis> speed <speed>;
0x10004000 STOP_SPIN 2 (piece, axis) ( decel -- ) stop-spin <piece> around <axis> [decel <d>];
0x10005000 SHOW 1 (piece) ( -- ) show <piece>;
0x10006000 HIDE 1 (piece) ( -- ) hide <piece>;
0x10007000 CACHE 1 (piece) ( -- ) cache <piece>;
0x10008000 DONT_CACHE 1 (piece) ( -- ) dont-cache <piece>;
0x10009000 PIECE_OP_09 1 (piece) ( a b -- ) __piece_op_09(<piece>, a, b);: TA lo ejecuta, pero sus scripts de unidad no hacen nada con él; ningún script de la versión comercial lo usa.
0x1000A000 DONT_SHADOW 1 (piece) ( -- ) dont-shadow <piece>;: TA lo ejecuta como no-op; consulta el 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>;

Operaciones de espera (0x101x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis 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>;

Manipulación de la pila (0x102x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis BOS
0x10021000 PUSH_IMM 1 (value) — PUSH sin flags: el juego falla con él (ta-push-flags). El compilador nunca lo escribe.
0x10021001 PUSH_CONST 1 (value) ( -- v ) <literal> en cualquier expresión
0x10021002 PUSH_LOCAL 1 (var#) ( -- v ) local_N en cualquier expresión
0x10021004 PUSH_STATIC 1 (var#) ( -- v ) global_N en cualquier expresión
0x10022000 STACK_ALLOC 0 ( -- ) (implícito: uno por cada var y 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 el resultado de una expresión)

Aritmética (0x103x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis BOS
0x10031000 ADD 0 ( a b -- a+b ) a + b
0x10032000 SUB 0 ( a b -- a-b ) a - b
0x10033000 MUL 0 ( a b -- a*b ) a * b
0x10034000 DIV 0 ( a b -- a/b ) a / b
0x10035000 BITWISE_AND 0 ( a b -- a&b ) a & b
0x10036000 BITWISE_OR 0 ( a b -- a|b ) a | b
0x10037000 XOR 0 ( a b -- a^b ) a ^ b
0x10038000 NOT 0 ( a -- ~a ) ~a

TA no tiene instrucción de módulo: % es un error de compilación en un script de TA. De 0x10039000 a 0x1003B000 están los operadores TAK_MATH_* de TA: Kingdoms (Apéndice C); TA falla con ellos.

Consultas al motor (0x104x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis 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, …): siempre saca un puerto y cuatro argumentos; el compilador apila ceros para los que falten
0x10044000 IS_CARRYING_UNIT 0 ( unit -- bool ) __is_carrying_unit(unit): 1 cuando esa unidad va montada en esta
0x10045000 CARRIER_UNIT_ID 0 ( -- id ) __carrier_unit_id(): la unidad que transporta a esta, o 0

Consulta el Apéndice B para el catálogo de puertos.

Comparación y lógica (0x105x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis 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: un XOR bit a bit como 0x10037000 (6 XOR 3 es 5), no uno lógico
0x1005A000 LOGICAL_NOT 0 ( a -- !a ) NOT a

Flujo de control y concurrencia (0x106x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis 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…);: saca los argumentos (más de cuatro desbordan el búfer del juego) y no ejecuta nada
0x10064000 JUMP 1 (offset) ( -- ) (compilador: goto, bucles)
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>;

El offset de JUMP / JUMP_IF_FALSE es un desplazamiento en bytes relativo a OffsetToScriptCode, no a la instrucción actual.

Efectos (0x107x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis BOS
0x10071000 EXPLODE 1 (piece) ( type -- ) explode <piece> type <type>;
0x10072000 PLAY_SOUND 1 (vol) ( sound -- result ) play-sound(<sound>, <vol>);: solo en TA: Kingdoms (.version 6); TA falla con él

Operaciones de escritura (0x108x)

Opcode Mnemónico En línea Efecto en la pila Sintaxis 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>;

Codificación de los opcodes

Cada palabra de opcode es 0x10CCSSFF, donde:

  • CC = el byte de categoría (animación, matemáticas, flujo de control, …).
  • SS = el nibble de subcomando, en el nibble alto del segundo byte empezando por arriba.
  • FF = el byte de flags.

El juego despacha según word & 0x100FF000 e ignora los demás bits, así que una palabra como 0x10064001 se ejecuta como JUMP; el desensamblador la escribe como JUMP@0x10064001 y el ensamblador la vuelve a leer como la misma palabra. PUSH y POP leen su origen o su destino de los tres bits bajos: PUSH acepta 1 (constante), 2 (local) y 4 (estática), POP acepta 2 y 4, y cualquier otro valor hace fallar el script.

El desensamblador representa las palabras que ningún juego ejecuta como UNKNOWN_0x10xxxxxx (y ta-unknown-opcode las señala); si encuentras una en un archivo real, abre una issue con la muestra del COB.


Apéndice B: tabla de puertos de get_unit_value

Los 20 puertos a los que el descompilador de kbot da nombre simbólico por ahora. El TA real tiene unos 80; TA: Kingdoms añade otros ~15 para magnitudes relacionadas con la magia. Hoy, todo lo que está por encima del puerto 20 aparece como literal numérico en la salida del descompilador.

Puerto Nombre simbólico Devuelve Notas
1 ACTIVATION bool ¿Está activado el interruptor «activo» de la unidad (radar, interferidor, fortificaciones)?
2 STANDINGMOVEORDERS enum 0 = mantener la posición, 1 = maniobrar, 2 = deambular.
3 STANDINGFIREORDERS enum 0 = no disparar, 1 = devolver el fuego, 2 = fuego a discreción.
4 HEALTH 0–100 Porcentaje de MaxDamage que queda.
5 INBUILDSTANCE bool ¿Tiene la unidad desplegados ahora mismo los nanotornos (nano-lathes)?
6 BUSY bool ¿Está la unidad ocupada con una orden?
7 PIECE_XZ empaquetado X/Z combinados de una pieza (codificados). Se usa para apuntar proyectiles.
8 PIECE_Y int Coordenada Y de una pieza relativa al origen de la unidad.
9 UNIT_XZ empaquetado X/Z combinados de la unidad.
10 UNIT_Y int Coordenada Y de la unidad.
11 UNIT_HEIGHT int Altura sobre el terreno.
12 XZ_ATAN int atan2 entre dos valores codificados en XZ (para torretas que apuntan).
13 XZ_HYPOT int Hipotenusa entre dos valores codificados en XZ.
14 ATAN int atan2(y, x) de dos valores apilados.
15 HYPOT int hypot(x, y).
16 GROUND_HEIGHT int Elevación del terreno en la posición de la unidad.
17 BUILD_PERCENT_LEFT 0–100 SmokeUnit lo usa para conocer el avance de la construcción. 0 una vez construida.
18 YARD_OPEN bool En los edificios con yardmap: ¿están abiertas las puertas?
19 BUGGER_OFF bool Lo activa el motor cuando la unidad tiene que apartarse del camino.
20 ARMORED bool ¿Está activada la postura blindada de la unidad (Bulldog, Goliath, etc.)?

Entre los puertos superiores están CURRENT_SPEED (29, famoso porque el Scriptor de Cavedog lo inserta como literal; consulta Escollos más arriba), MAX_ID, UNIT_TEAM, UNIT_BUILD_PERCENT, BUILDER, UNIT_ALLIED y muchos más. La lista completa está en la documentación interna de Cavedog; la wiki de la comunidad en los foros de TAUniverse tiene el catálogo público más completo.

TA: Kingdoms amplía el rango de puertos. Los puertos exclusivos de TAK que reconoce kbot (el descompilador muestra el nombre simbólico y el compilador lo acepta como entrada) proceden de la tabla [UNITVLAUES] de Scriptor:

Puerto Símbolo Notas
21 WEAPON_AIM_ABORTED Se activa cuando se cancela la solución de puntería de un arma.
22 WEAPON_READY El arma se ha recargado y está lista para disparar.
23 WEAPON_LAUNCH_NOW El motor ha disparado el arma.
26 FINISHED_DYING La animación de muerte ha terminado.
27 ORIENTATION Dirección hacia la que mira la unidad.
28 IN_WATER La unidad está en el agua.
29 CURRENT_SPEED Velocidad de movimiento actual (compartido con TA; Scriptor le da el mismo número aquí).
31 MAGIC_DEATH La unidad murió por daño mágico.
32 VETERAN_LEVEL Nivel de veteranía.
34 ON_ROAD La unidad está sobre una casilla de carretera.

Apéndice C: opcodes de TA: Kingdoms

Note

Opcodes exclusivos de TA:K. Ninguno aparece en los .cob de la versión comercial de TA. DONT_SHADOW y MISSION_COMMAND usan los mnemónicos canónicos de Scriptor (el compilador/descompilador de Switeck que conoce TAK); las operaciones matemáticas no tienen semántica documentada: Scriptor las etiqueta como ?? y ????, así que kbot mantiene los marcadores provisionales TAK_MATH_09 / TAK_MATH_0A / TAK_MATH_0B. Los scripts de la versión comercial de TA: Kingdoms usan cada uno como operador binario (saca dos valores y apila uno) al empaquetar y desempaquetar campos de bits. TA 3.1c no tiene manejador para PLAY_SOUND, MISSION_COMMAND ni los operadores matemáticos; DONT_SHADOW lo ejecuta como no-op.

Opcode Mnemónico En línea Apariciones en TAK comercial Notas
0x1000A000 DONT_SHADOW 1 (piece) 66 Desactiva la proyección de sombra de una sola pieza. Está en la categoría de animación, junto a DONT_SHADE (0x1000E000); antes kbot lo llamaba TAK_ANIM_0A. Misma forma en disco que las demás operaciones ANIM_*: opcode de 4 bytes + un DWORD piece. Palabra clave BOS: dont-shadow <piece>;.
0x10039000 TAK_MATH_09 0 264 Operador binario de la categoría matemática, 4 bytes en total: ( a b -- r ). BOS: __tak_math_09(a, b).
0x1003A000 TAK_MATH_0A 0 — Misma forma. Algunas tablas de opcodes lo llaman BITWISE_NOT; el NOT bit a bit de TA es 0x10038000. BOS: __tak_math_0a(a, b).
0x1003B000 TAK_MATH_0B 0 66 Misma forma que TAK_MATH_09. BOS: __tak_math_0b(a, b).
0x10073000 MISSION_COMMAND 2 (soundNameIdx, argCount) 3885 Llamada a una orden del motor. El primer DWORD en línea es un índice en la tabla SoundNames del COB; el segundo es el número de valores que se sacan de la pila como argumentos. El opcode vuelve a apilar un único resultado (que normalmente se descarta con un POP_STACK posterior o se guarda con POP_STATIC/POP_LOCAL). Es, con diferencia, el opcode exclusivo de TAK más frecuente. Palabra clave BOS: Mission-Command("name", args…), que coincide con la sintaxis canónica de Scriptor.

Codificación (resumen)

Cada palabra de opcode COB es 0x10CCSSFF, donde CC es la categoría y SS el subcomando: estos opcodes de TAK simplemente ocupan huecos CC:SS que antes no se usaban dentro de las categorías existentes, en lugar de introducir un byte de categoría nuevo. DONT_SHADOW vive en la categoría de animación junto a DONT_CACHE/DONT_SHADE, y MISSION_COMMAND en la categoría de efectos junto a EXPLODE y PLAY_SOUND.

Tip

Compáralo con bytecode real.

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

Ejemplos reales en el catálogo de referencia

Unidades reales que llevan el formato al límite; ábrelas para ver la complejidad real de los scripts en lugar de ejemplos preparados para el texto:

  • CORKROG: Krogoth (Core), uno de los COB más grandes del juego (~25 KB, más de 30 scripts, animaciones complejas de varias piezas).
  • ARMCOM: Arm Commander, una unidad «rica» representativa: ciclos de caminar, apuntar, reposo y construcción, varias armas y la gestión del D-gun.
  • CORTRUCK: Construction Truck, un COB casi mínimo que es el que se recorre en esta página (3 scripts: SmokeUnit, Create, Killed).
  • ARMSAM: Samson, un buen ejemplo de unidad con torreta (turret1 que gira para apuntar, animación de retroceso, sincronización con wait-for-turn).

Descompila cualquiera en local con kbot cob decompile scripts/<name>.cob una vez definido un contexto de kbot.


Véase también

  • 3DO: los nombres de piezas a los que hacen referencia los scripts COB están en el .3do de la unidad.
  • FBI: el archivo de metadatos de la unidad. Su ObjectName une el COB con un 3DO; Sounds define el banco de sonido que puede reproducir el script.
  • La tabla de reglas de cob lint de la CLI de kbot.
  • Glosario: piece, axis, signal mask, tick.