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
ObjectNameque 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ónturnen curso. Lo mismo vale parawait-for-move.signal <n>mata todos los scripts cuyosignal_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:
- Los dos scripts que establecen
SIG_MOVEmueren cuando se dispara la señal, incluido el propioMoveUnit(no importa: su trabajo ya está hecho). - 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 opcodesignal. Idleno estableceSIG_MOVE, así que el siguientesignal SIG_MOVEno 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
ScriptCodeIndexArrayson 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_VALUEvarí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) yglobal_N(variable estática de cada unidad que persiste entre llamadas). El descompilador emitelocal_N/global_Npor defecto; renómbralas en el fuente para que se lea mejor. - La herramienta Scriptor de Cavedog a veces inserta
CURRENT_SPEEDcomo el literal29, lo que rompe los scripts del estilo deMoveWatchertras 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 (turret1que gira para apuntar, animación de retroceso, sincronización conwait-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
.3dode la unidad. - FBI: el archivo de metadatos de la unidad. Su
ObjectNameune el COB con un 3DO;Soundsdefine el banco de sonido que puede reproducir el script. - La tabla de reglas de
cob lintde la CLI de kbot. - Glosario: piece, axis, signal mask, tick.