Toutes les pages de formats ↓
Unités, scripts, configurations

COB / BOS — bytecode des scripts d'unités

Source : coreprime/kbot @ 0ab97bf · Modifier sur GitHub

Chaque unité de Total Annihilation exécute un script qui anime ses pièces, produit les éclairs de tir des armes, joue les effets sonores et réagit aux événements de la partie. Le langage source est le BOS (« Basic Object Script ») ; le bytecode compilé que le moteur exécute réellement est le COB (« Cobble », le jeu de mots de Cavedog pour « compiled BOS »). Un fichier par unité, nommé d'après le champ ObjectName de son FBI.

Tip

Essayez vous-même.

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

Consultez la référence de kbot cob de la CLI pour toutes les options (et le tableau complet des règles de lint).

Depuis Go. Utilisez 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)
}

Deux langages, un seul format de fichier

Étape Format Outils
Source BOS — proche du C, structuré Écrit à la main ; Scriptor de Switeck ; kbot cob decompile le reconstitue depuis le COB en aller-retour
Compilé COB — bytecode à pile Chargé par le moteur ; produit par Scriptor ou kbot cob compile

Le moteur ne voit jamais que du COB. Le BOS est une commodité pour les auteurs humains : quand vous « modifiez le script d'une unité », vous modifiez en réalité le source BOS, vous le recompilez en COB, puis vous livrez le COB dans le .ufo d'extension de l'unité.

Un exemple minimal :

// 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 traduit ce code en un blob binaire. kbot cob disassemble -a affiche le flux d'opcodes avec, en regard, des flèches de flot de contrôle ; kbot cob decompile fait l'inverse et retrouve un BOS qui, presque toujours, refait l'aller-retour à l'octet près (kbot cob roundtrip le vérifie sur tout le catalogue du jeu).

Un source est compilé comme un script TA (COB version 4), sauf s'il commence par .version 6. Le compilateur refuse ce que TA 3.1c exécuterait mal au lieu d'écrire un bouche-trou : les constructions de TA: Kingdoms (play-sound, Mission-Command, __tak_math_*, .sound_name) et % sont des erreurs dans un script TA (TA n'a pas d'instruction modulo, et 0x10037000, le mot que % produisait autrefois, y est un XOR bit à bit), tout comme les identifiants inconnus et les fonctions qui demandent plus que les 32 emplacements de pile du jeu. Ce qui compile encore mais mérite un coup d'œil — une fonction définie deux fois (chaque appel se lie à la première définition, comme le fait la recherche de nom du jeu) — est affiché comme avertissement, avec tout constat des règles de lint de compatibilité TA ; --strict fait échouer la commande sur les avertissements. kbot cob assemble applique les mêmes vérifications aux listings écrits à la main.


Disposition du COB sur le disque

Important

Deux dialectes. Cette section décrit le COB v4, le dialecte livré avec Total Annihilation. TA: Kingdoms utilise le COB v6 (VersionSignature == 6), qui conserve tout ce qui suit mais allonge l'en-tête de 8 octets et insère une table d'offsets de chaînes de commande supplémentaire après le tableau des noms de pièces. Voir TA: Kingdoms — écarts du COB v6 pour les spécificités de la v6 ; la liste des opcodes vaut pour les deux, avec les opcodes supplémentaires propres à TAK documentés dans l'annexe 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

Les noms de champs canoniques sont ceux de la structure HEADER_COB de Scriptor. Les révisions antérieures de kbot appelaient LengthOfScripts / NumberOfStaticVars / UKZero / OffsetToNameArray « Unknown0/1/2/3 » : mêmes offsets, seulement avec les identités désormais documentées.

Viennent ensuite, dans cette disposition :

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

Résoudre un script

La table des index de code contient des offsets en mots uint32, pas des offsets en octets. Pour trouver le bytecode du script numéro i :

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

Le script se termine là où commence le suivant (ou au début de la table d'index, pour le dernier script).

Important

Les offsets de chaînes des tables de noms sont des offsets absolus dans le fichier, pas des offsets relatifs au pool de chaînes. Les outils de Cavedog ne déplaçaient jamais le pool de chaînes, donc cela ne comptait pas en pratique ; si vous réorganisez un COB à la main, il faut mettre à jour chaque offset.


Le flux d'opcodes

Le COB est une machine virtuelle à pile. La plupart des opcodes dépilent leurs arguments depuis une pile de données propre à chaque script, font leur travail et empilent un résultat. Certains opcodes ont des opérandes en ligne (cibles de saut, numéros de pièces).

Chaque opcode est un uint32 de la forme :

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

L'octet de poids fort vaut toujours 0x10 ; famille/op/flags varient selon l'opcode. Le jeu ne compare pas le mot entier : il aiguille sur word & 0x100FF000, donc les bits hors de ce masque sont ignorés et 0x10064001 s'exécute comme JUMP (le désassembleur de kbot écrit un tel mot JUMP@0x10064001 pour qu'il se réassemble à l'identique). PUSH (0x10021000) et POP (0x10023000) lisent ensuite leur source ou leur destination dans les trois bits de poids faible. Le fichier scripting/opcodes.go de kbot-io sert de catalogue ; scripting.Opcodes() liste chaque instruction avec ses opérandes en ligne et son effet sur la pile.

Familles d'opcodes

Famille Exemples Remarques
0x100x MOVE, TURN, SPIN, SHOW, HIDE, EMIT_SFX Animation des pièces. Le n° de pièce et le n° d'axe suivent en ligne.
0x101x WAIT_FOR_TURN, WAIT_FOR_MOVE, SLEEP Barrières asynchrones : le script rend la main jusqu'à ce que la condition soit remplie.
0x102x PUSH_CONST, PUSH_LOCAL, PUSH_STATIC, POP_* Manipulation de la pile.
0x103x ADD, SUB, MUL, DIV, BITWISE_AND, BITWISE_OR, XOR, NOT Arithmétique et opérations bit à bit. Il n'y a pas de modulo.
0x104x RAND, GET_UNIT_VALUE, GET, IS_CARRYING_UNIT, CARRIER_UNIT_ID Requêtes au moteur.
0x105x <, <=, >, >=, ==, !=, &&, ||, XOR_ALT, ! Comparaisons et opérations logiques. XOR_ALT est un XOR bit à bit, pas un XOR logique.
0x106x START_SCRIPT, CALL_SCRIPT, DISCARD_CALL, JUMP, RETURN, JUMP_IF_FALSE, SIGNAL, SET_SIGNAL_MASK Flot de contrôle et concurrence.
0x107x EXPLODE, PLAY_SOUND Effets spéciaux. PLAY_SOUND n'existe que dans TA: Kingdoms.
0x108x SET_VALUE, ATTACH_UNIT, DROP_UNIT Modifications de l'état du moteur.

Un petit échantillon, mais représentatif :

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

Les offsets de saut sont des offsets en octets relatifs au début de la section de code des scripts (et non à l'instruction courante). JUMP 0x4C signifie « sauter à OffsetToScriptCode + 0x4C ». L'option -a du désassembleur trace des flèches entre les instructions correspondantes pour le rendre évident.


Modèle de concurrence

Une unité peut faire tourner de nombreux scripts en même temps — Walk, Aim, SmokeUnit, MoveWatcher, etc. Leur ordonnancement est coopératif :

  • sleep <ticks> rend la main pendant N ticks (1/30 s chacun).
  • wait-for-turn <piece> around <axis> rend la main jusqu'à ce que cette pièce termine son animation turn en cours. Idem pour wait-for-move.
  • signal <n> tue chaque script dont signal_mask & n != 0.
  • set-signal-mask <n> déclare les signaux auxquels le script courant s'intéresse.

Le masque de signal est la façon dont les scripts réalisent la préemption : MoveWatcher fixe le masque SIG_MOVE et surveille la vitesse ; quand l'unité s'arrête, il envoie SIG_MOVE, ce qui tue proprement les animations de marche avant de lancer la pose de repos.

Warning

SET_SIGNAL_MASK ne fait qu'enregistrer un intérêt : il ne tue rien. C'est l'envoi de signal X qui arrête les scripts dont le masque contient X. La règle de lint signal-never-signalled de kbot cob lint repère les scripts qui fixent un masque que personne ne signale jamais (ce qui est presque toujours un bogue).

Comment la préemption se déroule réellement

Un schéma courant : MoveUnit déclenche un script d'animation Walk ; quand le mouvement s'arrête, le moteur envoie SIG_MOVE pour faire le ménage proprement avant que Idle ne démarre. La séquence :

   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)

Trois points à remarquer :

  1. Les deux scripts qui fixent SIG_MOVE sont tués quand le signal part — y compris MoveUnit lui-même (peu importe, son travail est fait).
  2. Le script qui signale (MoveUnit) survit assez longtemps pour envoyer le signal avant d'être tué ; l'exécution vérifie les masques après le retour de l'opcode signal.
  3. Idle ne fixe pas SIG_MOVE, donc le prochain signal SIG_MOVE ne le touchera pas.

Exemple commenté — cortruck.cob (camion de construction du 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);
}

Le désassemblage correspondant (kbot cob disassemble -a -s SmokeUnit) montre le flux d'opcodes réel, avec les flèches de JUMP_IF_FALSE/JUMP qui traversent les corps des while/if — utile quand la sortie du décompilateur est ambiguë.


Analyse statique (lint)

kbot cob lint (et l'onglet Lint de l'explorateur de ressources) exécute le jeu de règles par défaut de kbot-io : des règles de style tirées de vrais bogues des propres scripts de Cavedog et de correctifs de mods proposés par la communauté, deux vérifications au niveau du fichier et — pour chaque COB qui ne déclare pas la version 6 — les règles de compatibilité avec TA 3.1c. Le catalogue des règles de style :

Règle Gravité Ce qu'elle détecte
unused-piece avertissement Pièce déclarée mais jamais utilisée dans aucun script.
unused-static / unused-local avertissement Variable allouée mais jamais lue.
always-true / dead-code info / avertissement if(1) / while(0).
long-function / high-complexity avertissement Plus de 100 instructions ou complexité cyclomatique supérieure à 15.
invalid-call erreur call-script vers une fonction inexistante.
speed-zero avertissement move/turn avec speed <0> — l'animation ne se termine jamais.
empty-function info Le corps de la fonction se réduit à return 0.
duplicate-animation avertissement Deux commandes d'animation identiques à la suite.
sleep-only-guard info Un bloc if qui ne contient rien d'autre que sleep.
duplicate-if info Deux if consécutifs avec la même condition.
raw-signal info signal 4 au lieu de signal SIG_AIM.
unnamed-global info global_3 au lieu d'une variable statique nommée.
signal-never-signalled avertissement Le script masque un signal que personne n'émet.
recursive-call avertissement Cycle de call-script.
duplicate-function avertissement Deux scripts portent le même nom ; le jeu n'appelle que le premier.
malformed-cob erreur / avertissement Le fichier ne se charge pas, ou le code d'un script est tronqué (erreur) ; tables de noms que le lecteur a tolérées (avertissement).

Les règles de compatibilité TA signalent ce sur quoi TA 3.1c plante ou qu'il exécute différemment. Ce sont des erreurs ; elles apparaissent dans la sortie SARIF de --ci comme les autres et ignorent les fichiers de TA: Kingdoms (version 6) :

Règle Ce qu'elle détecte
ta-kingdoms-opcode PLAY_SOUND, MISSION_COMMAND ou un opérateur TAK_MATH_* dans un script TA. TA n'a aucun gestionnaire pour eux et le script plante quand il en atteint un.
ta-unknown-opcode Un mot d'opcode qu'aucun jeu n'exécute.
ta-push-flags Un PUSH dont les bits de poids faible ne valent pas 1 (constante), 2 (locale) ou 4 (statique), ou un POP dont les bits de poids faible ne valent pas 2 ou 4 — y compris les 0x10021000 et 0x10021008 sans flag qu'écrivaient certains outils.
ta-stack-limit Plus de 32 STACK_ALLOC, ou des locales et valeurs en attente qui dépassent les 32 emplacements de pile du jeu ; le jeu ne vérifie pas et écrase la mémoire située après le script.
ta-get-arguments Un GET atteint avec moins de cinq valeurs en attente. GET dépile toujours un port et quatre arguments, donc un get à un argument prend le reste dans les locales de la fonction et les écrase.
ta-stack-underflow Toute autre instruction qui dépile plus de valeurs qu'il n'y en a en attente, ou des chemins qui se rejoignent avec des profondeurs de pile différentes.
ta-discard-call DISCARD_CALL avec plus de quatre arguments.

Les descriptions complètes des règles se trouvent dans le README de kbot. Le décompilateur conserve délibérément la numérotation d'origine des variables et des pièces, de sorte qu'un cycle décompiler→lint→corriger→compiler fait ressortir les mods qui ont besoin de maintenance.


Pièges

Warning

call-script vaut 0x10062000. 0x10063000 est une autre instruction, DISCARD_CALL : elle a les deux mêmes mots en ligne (numéro de script et nombre d'arguments), mais elle dépile les arguments et n'exécute rien. Aucun script d'origine ne l'utilise ; le décompilateur de kbot l'écrit __discard_call(word, args…).

Warning

Plusieurs tables d'opcodes publiées nomment mal les mots bit à bit. TA exécute 0x10037000 comme un XOR bit à bit (pas un modulo : TA n'a pas d'instruction modulo), 0x10038000 comme le NOT bit à bit unaire (pas un XOR), et 0x10059000 comme un second XOR bit à bit (pas un XOR logique : 6 XOR_ALT 3 vaut 5). 0x1003A000 est un opérateur de TA: Kingdoms sur lequel TA plante. Les listings écrits avec les anciens noms (MOD, BITWISE_XOR, BITWISE_NOT, LOGICAL_XOR) s'assemblent toujours vers les mots dont ils sont issus.

  • Tous les offsets de l'en-tête COB sont des offsets absolus dans le fichier, mais les entrées de ScriptCodeIndexArray sont des offsets en mots à l'intérieur de la section de code. On les confond facilement.
  • Les numéros de pièces dans les opcodes sont des index dans la table des noms de pièces, pas des chaînes. Le COB porte les noms sous forme de pool de chaînes ; l'exécution utilise des index pour la rapidité.
  • Les opcodes get UNIT_VALUE diffèrent subtilement entre TA et TA: Kingdoms. TA: Kingdoms a ajouté des identifiants de valeurs pour la régénération de magie, les points de commandement, etc. ; la table des valeurs de TA est plus compacte. Le décompilateur de kbot nomme pour l'instant le jeu de TA ; les numéros de ports TAK ≥ 21 sont émis comme littéraux numériques (qui refont quand même l'aller-retour par l'assembleur). Voir TA: Kingdoms — écarts du COB v6 pour l'ensemble des écarts.
  • Les variables existent en deux variantes : local_N (emplacement de pile local à la fonction) et global_N (statique par unité, persistante d'un appel à l'autre). Le décompilateur émet local_N / global_N par défaut ; renommez-les dans le source pour la lisibilité.
  • L'outil Scriptor de Cavedog remplace parfois CURRENT_SPEED par le littéral 29, ce qui casse les scripts de type MoveWatcher après un aller-retour. Le décompilateur de kbot rétablit le nom symbolique ; si vous devez faire l'aller-retour par Scriptor, mettez des parenthèses : (get CURRENT_SPEED) > 5.

Tailles typiques

Ressource Plage observée dans les scripts/*.cob de Cavedog
Taille de fichier 0,5 Ko – 25 Ko
Surcoût de l'en-tête toujours 44 octets
Scripts par unité 3 – 30 (unités typiques : 5–10)
Pièces par unité 3 – 80 (Krogoth ≈ 80)
Taille de la section de code 300 o – 20 Ko (varie énormément selon la complexité)
armcom.cob (commandant) ~14 Ko, 30 scripts
cortruck.cob (unité simple) ~750 o, 3 scripts

TA: Kingdoms — écarts du COB v6

Note

Cette section ne décrit que les écarts propres à TA:K. Tout ce qui précède décrit le COB v4 (TA). TA: Kingdoms garde la même VM à pile, le même pool de chaînes, le même schéma d'encodage des opcodes et la même mécanique de noms de scripts et de pièces ; les différences sont un en-tête étendu, une table de chaînes supplémentaire ajoutée après le tableau des noms de pièces, et quatre nouveaux opcodes.

Disposition du fichier 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)

Le sous-en-tête de 8 octets se place entre OffsetToNameArray (fin de l'en-tête canonique de 11 DWORD, à l'octet 44) et le début de la section de code (OffsetToScriptCode, en général 0x34 = 52). Il contient deux uint32 little-endian :

Champ Signification
[0] Offset absolu dans le fichier de la table des offsets de noms de sons. Toujours égal à OffsetToPieceNameOffsetArray + NumPieces × 4, c'est-à-dire l'octet qui suit immédiatement le tableau des offsets de noms de pièces. Redondant avec la disposition canonique ; l'écrivain le reconstruit.
[1] Nombre de noms de sons dans la table. C'est ce nombre qui fait foi.

Chaque entrée de la table des offsets de noms de sons est un offset absolu dans le fichier vers une chaîne terminée par NUL du pool de chaînes — des choses comme "SetMission o 1, s", "create ARASWORD", "ARROW10". Le bytecode y fait référence par index (et non par offset) au moyen de l'opcode MISSION_COMMAND (voir l'annexe C). La table s'appelle le tableau des noms de sons parce que le compilateur Scriptor canonique y enregistre les chaînes par l'action ADDSOUND ; son contenu réel est fait de chaînes de commande du moteur plutôt que de noms de fichiers audio.

La représentation de kbot

scripting.COB porte les noms de sons dans un champ structuré []string, sans conserver d'octets opaques. Tout le reste (les valeurs du sous-en-tête, la table d'offsets, les offsets de chaque chaîne dans le pool) est reconstruit à partir de cette liste à l'écriture :

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

Pour les fichiers v4, la tranche est toujours nil, et le sous-en-tête comme la table des offsets de noms de sons sont omis de la sortie.

Le désassembleur et le décompilateur émettent chaque chaîne sous forme de directive .sound_name, pour qu'elles survivent à un aller-retour par .coba et .bos :

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

Nouveaux opcodes

Les fichiers .cob de TAK utilisent des instructions pour lesquelles TA 3.1c n'a aucun gestionnaire (PLAY_SOUND, MISSION_COMMAND et les opérateurs TAK_MATH_*), plus DONT_SHADOW, que TA exécute comme un no-op. Leur sémantique exacte côté jeu n'est pas documentée, mais leur disposition binaire (taille + nombre de paramètres en ligne) est connue grâce à chaque .cob d'origine de TAK, ce qui suffit pour que l'aller-retour désassembler→assembler restitue les octets d'origine à l'identique. Voir l'annexe C pour la référence complète.

État de l'aller-retour

Chaîne de traitement TA TA: Kingdoms
disassemble → assemble (identique à l'octet près) 278 / 278 ✅ 267 / 267 ✅
decompile → compile (identique à l'octet près) 278 / 278 ✅ Les scripts d'unités simples et de nombreux COB de missions (≈ 70 / 267) refont l'aller-retour à l'octet près. Les échecs restants sont des motifs de flot de contrôle pour lesquels la reconstruction structurée en if/while du décompilateur choisit une disposition de JUMP que le compilateur régénère un peu différemment — le bytecode est sémantiquement équivalent (chaque opcode TAK, chaîne de commande, sous-en-tête et le mot de version v6 survivent), simplement pas identique à l'octet près. Le même genre d'écart toucherait n'importe quel cycle décompiler→compiler sur un script non trivial.

Syntaxe BOS des extensions TAK

Le décompilateur émet ces pseudo-appels pour que les COB de TAK fassent l'aller-retour par le BOS. Le compilateur accepte la même syntaxe, mais seulement après .version 6 (sauf dont-shadow, que TA exécute comme un no-op). dont-shadow et Mission-Command reprennent les mots-clés TAK canoniques de Scriptor ; les opcodes mathématiques n'ont pas de nom documenté dans Scriptor (il les étiquette ?? et ????), donc kbot garde les bouche-trous __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

Le premier argument de Mission-Command est le nom de son, sous forme de chaîne entre guillemets : le compilateur le cherche dans la table des noms de sons (la directive .sound_name correspondante doit donc apparaître plus haut dans le fichier). Les arguments suivants sont empilés et le moteur les consomme.

Deux directives en tête de fichier portent le reste des métadonnées v6 :

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

.version est toujours émise (pour que les fichiers TA v4 refassent l'aller-retour à l'identique par la même chaîne). Chaque .sound_name est une entrée de la table des noms de sons du COB ; l'écrivain reconstruit le sous-en-tête de 8 octets et la table des offsets de noms de sons à partir de len(SoundNames) et de l'ordre d'apparition des entrées.

L'épilogue TAK STACK_ALLOC ; RETURN qu'émet en v6 chaque return; sans valeur est géré par le compilateur : return; se traduit en STACK_ALLOC ; RETURN quand .version 6, et return <expr>; se traduit en <expr> ; RETURN sans préfixe (comme le compilateur de Cavedog).

Pour le reproduire sur votre propre installation :

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

Symboles des ports au-delà du port 20

Les scripts de TA: Kingdoms utilisent des ports get_unit_value jusqu'à 46 (CURRENT_SPEED, VETERAN_LEVEL, champs de la réserve de magie, etc.). Le décompilateur de kbot ne nomme pour l'instant symboliquement que les ports 1–20 (voir l'annexe B) ; les ports TAK ≥ 21 sont émis comme littéraux numériques. Le bytecode les stocke de toute façon comme des entiers, donc les allers-retours ne sont pas affectés ; seule la lisibilité du BOS en pâtit. L'ajout des noms de ports TAK est suivi à part.


Annexe A — référence complète des opcodes

Toutes les instructions qu'exécute TA ou TA: Kingdoms. La référence qui fait foi est formats/scripting/opcodes.go de kbot-io. La notation de pile suit le style Forth ( before -- after ). La colonne En ligne indique combien de mots uint32 suivent le mot d'opcode lui-même (consommés au décodage, pas pris sur la pile).

Animation — manipulation des pièces (0x100x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe 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 l'exécute, mais ses scripts d'unités n'en font rien ; aucun script d'origine ne l'utilise.
0x1000A000 DONT_SHADOW 1 (piece) ( -- ) dont-shadow <piece>; — TA l'exécute comme un no-op ; voir l'annexe 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>;

Opérations d'attente (0x101x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe 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>;

Manipulation de la pile (0x102x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe BOS
0x10021000 PUSH_IMM 1 (value) — PUSH sans flag : le jeu plante dessus (ta-push-flags). Jamais écrit par le compilateur.
0x10021001 PUSH_CONST 1 (value) ( -- v ) <literal> dans n'importe quelle expression
0x10021002 PUSH_LOCAL 1 (var#) ( -- v ) local_N dans n'importe quelle expression
0x10021004 PUSH_STATIC 1 (var#) ( -- v ) global_N dans n'importe quelle expression
0x10022000 STACK_ALLOC 0 ( -- ) (implicite — un par var et par paramètre)
0x10023002 POP_LOCAL 1 (var#) ( v -- ) local_N = expr;
0x10023004 POP_STATIC 1 (var#) ( v -- ) global_N = expr;
0x10024000 POP_STACK 0 ( v -- ) (jette le résultat d'une expression)

Arithmétique (0x103x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe 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 n'a pas d'instruction modulo : % est une erreur de compilation dans un script TA. 0x10039000–0x1003B000 sont les opérateurs TAK_MATH_* de TA: Kingdoms (annexe C) ; TA plante dessus.

Requêtes au moteur (0x104x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe 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, …) — dépile toujours un port et quatre arguments ; le compilateur empile des zéros pour ceux qui manquent
0x10044000 IS_CARRYING_UNIT 0 ( unit -- bool ) __is_carrying_unit(unit) — 1 quand cette unité est transportée par celle-ci
0x10045000 CARRIER_UNIT_ID 0 ( -- id ) __carrier_unit_id() — l'unité qui transporte celle-ci, ou 0

Voir l'annexe B pour le catalogue des ports.

Comparaison et logique (0x105x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe 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 à bit comme 0x10037000 (6 XOR 3 vaut 5), pas un XOR logique
0x1005A000 LOGICAL_NOT 0 ( a -- !a ) NOT a

Flot de contrôle et concurrence (0x106x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe 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…); — dépile les arguments (au-delà de quatre, ils débordent du tampon du jeu) et n'exécute rien
0x10064000 JUMP 1 (offset) ( -- ) (compilateur — goto, boucles)
0x10065000 RETURN 0 ( v -- ) return v;
0x10066000 JUMP_IF_FALSE 1 (offset) ( v -- ) (compilateur — if/while)
0x10067000 SIGNAL 0 ( mask -- ) signal <mask>;
0x10068000 SET_SIGNAL_MASK 0 ( mask -- ) set-signal-mask <mask>;

L'offset de JUMP / JUMP_IF_FALSE est un offset en octets relatif à OffsetToScriptCode, et non à l'instruction courante.

Effets (0x107x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe BOS
0x10071000 EXPLODE 1 (piece) ( type -- ) explode <piece> type <type>;
0x10072000 PLAY_SOUND 1 (vol) ( sound -- result ) play-sound(<sound>, <vol>); — TA: Kingdoms uniquement (.version 6) ; TA plante dessus

Opérations d'affectation (0x108x)

Opcode Mnémonique En ligne Effet sur la pile Syntaxe 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>;

Encodage des opcodes

Chaque mot d'opcode est de la forme 0x10CCSSFF, où :

  • CC = l'octet de catégorie (animation, maths, flot de contrôle, …).
  • SS = le quartet de sous-commande, dans le quartet de poids fort du deuxième octet en partant du haut.
  • FF = l'octet de flags.

Le jeu aiguille sur word & 0x100FF000 et ignore les autres bits : un mot comme 0x10064001 s'exécute donc comme JUMP ; le désassembleur l'écrit JUMP@0x10064001 et l'assembleur le relit vers le même mot. PUSH et POP lisent leur source ou leur destination dans les trois bits de poids faible : PUSH accepte 1 (constante), 2 (locale) et 4 (statique), POP accepte 2 et 4, et toute autre valeur fait planter le script.

Les mots qu'aucun jeu n'exécute sont rendus UNKNOWN_0x10xxxxxx par le désassembleur (et signalés par ta-unknown-opcode) ; si vous en trouvez un dans un vrai fichier, merci d'ouvrir une issue avec l'échantillon COB.


Annexe B — table des ports de get_unit_value

Les 20 ports que le décompilateur de kbot nomme symboliquement pour l'instant. Le vrai TA en a environ 80 ; TA: Kingdoms en ajoute environ 15 de plus pour les grandeurs liées à la magie. Tout port au-delà de 20 est aujourd'hui rendu comme littéral numérique dans la sortie du décompilateur.

Port Nom symbolique Renvoie Remarques
1 ACTIVATION bool L'interrupteur « actif » de l'unité est-il activé (radar, brouilleur, fortifications) ?
2 STANDINGMOVEORDERS enum 0 = tenir la position, 1 = manœuvrer, 2 = se déplacer librement.
3 STANDINGFIREORDERS enum 0 = cesser le feu, 1 = riposter, 2 = feu à volonté.
4 HEALTH 0–100 Pourcentage de MaxDamage restant.
5 INBUILDSTANCE bool L'unité déploie-t-elle en ce moment ses nano-lathes ?
6 BUSY bool L'unité est-elle en ce moment occupée par un ordre ?
7 PIECE_XZ packed X/Z combinés d'une pièce (encodés). Sert à viser avec les projectiles.
8 PIECE_Y int Coordonnée Y d'une pièce par rapport à l'origine de l'unité.
9 UNIT_XZ packed X/Z combinés de l'unité.
10 UNIT_Y int Coordonnée Y de l'unité.
11 UNIT_HEIGHT int Hauteur au-dessus du terrain.
12 XZ_ATAN int atan2 entre deux valeurs encodées en XZ (pour viser avec les tourelles).
13 XZ_HYPOT int Hypoténuse entre deux valeurs encodées en XZ.
14 ATAN int atan2(y, x) de deux valeurs empilées.
15 HYPOT int hypot(x, y).
16 GROUND_HEIGHT int Altitude du terrain à la position de l'unité.
17 BUILD_PERCENT_LEFT 0–100 Utilisé par SmokeUnit pour connaître l'avancement de la construction. 0 une fois l'unité construite.
18 YARD_OPEN bool Pour les bâtiments à yard map : les portes sont-elles ouvertes ?
19 BUGGER_OFF bool Fixé par le moteur quand l'unité doit dégager le passage.
20 ARMORED bool La posture blindée de l'unité est-elle activée (Bulldog, Goliath, etc.) ?

Les ports supérieurs comprennent CURRENT_SPEED (29 — que le Scriptor de Cavedog remplace tristement par un littéral ; voir les Pièges plus haut), MAX_ID, UNIT_TEAM, UNIT_BUILD_PERCENT, BUILDER, UNIT_ALLIED et bien d'autres. La liste complète se trouve dans la documentation interne de Cavedog ; le wiki communautaire des forums TAUniverse offre le catalogue public le plus complet.

TA: Kingdoms étend la plage des ports. Les ports propres à TAK que kbot reconnaît (le décompilateur affiche le nom symbolique ; le compilateur l'accepte en entrée) sont tirés de la table [UNITVLAUES] de Scriptor :

Port Symbole Remarques
21 WEAPON_AIM_ABORTED Fixé quand la solution de visée d'une arme est annulée.
22 WEAPON_READY L'arme est rechargée et prête à tirer.
23 WEAPON_LAUNCH_NOW Le moteur a fait tirer l'arme.
26 FINISHED_DYING L'animation de mort est terminée.
27 ORIENTATION Direction vers laquelle l'unité est tournée.
28 IN_WATER L'unité est dans l'eau.
29 CURRENT_SPEED Vitesse de déplacement actuelle (commune avec TA — Scriptor lui donne ici le même numéro).
31 MAGIC_DEATH L'unité est morte de dégâts magiques.
32 VETERAN_LEVEL Niveau de vétérance.
34 ON_ROAD L'unité est sur une tuile de route.

Annexe C — opcodes de TA: Kingdoms

Note

Opcodes propres à TA:K. Aucun d'eux n'apparaît dans les fichiers .cob d'origine de TA. DONT_SHADOW et MISSION_COMMAND reprennent les mnémoniques canoniques de Scriptor (le compilateur/décompilateur de Switeck qui connaît TAK) ; les opérations mathématiques n'ont pas de sémantique documentée — Scriptor les étiquette ?? et ????, donc kbot garde les bouche-trous TAK_MATH_09 / TAK_MATH_0A / TAK_MATH_0B. Les scripts d'origine de TA: Kingdoms emploient chacun d'eux comme opérateur binaire (deux valeurs dépilées, une empilée) pour empaqueter et dépaqueter des champs de bits. TA 3.1c n'a aucun gestionnaire pour PLAY_SOUND, MISSION_COMMAND ni les opérateurs mathématiques ; DONT_SHADOW, il l'exécute comme un no-op.

Opcode Mnémonique En ligne Occurrences dans TAK d'origine Remarques
0x1000A000 DONT_SHADOW 1 (piece) 66 Désactive la projection d'ombre pour une seule pièce. Se range dans la catégorie animation à côté de DONT_SHADE (0x1000E000) — kbot l'appelait auparavant TAK_ANIM_0A. Même forme sur le disque que les autres opérations ANIM_* : opcode de 4 octets + un DWORD piece. Mot-clé BOS : dont-shadow <piece>;.
0x10039000 TAK_MATH_09 0 264 Opérateur binaire de la catégorie maths, 4 octets au total : ( a b -- r ). BOS : __tak_math_09(a, b).
0x1003A000 TAK_MATH_0A 0 — Même forme. Certaines tables d'opcodes l'appellent BITWISE_NOT ; le NOT bit à bit de TA est 0x10038000. BOS : __tak_math_0a(a, b).
0x1003B000 TAK_MATH_0B 0 66 Même forme que TAK_MATH_09. BOS : __tak_math_0b(a, b).
0x10073000 MISSION_COMMAND 2 (soundNameIdx, argCount) 3 885 Appel de commande du moteur. Le premier DWORD en ligne est un index dans la table SoundNames du COB ; le second est le nombre de valeurs à dépiler comme arguments. L'opcode empile un unique résultat (en général jeté par un POP_STACK qui suit, ou stocké par POP_STATIC/POP_LOCAL). De loin l'opcode propre à TAK le plus courant. Mot-clé BOS : Mission-Command("name", args…) — identique à la syntaxe canonique de Scriptor.

Encodage (rappel)

Chaque mot d'opcode COB est de la forme 0x10CCSSFF, où CC est la catégorie et SS la sous-commande : ces opcodes TAK occupent simplement des emplacements CC:SS jusque-là inutilisés dans les catégories existantes, au lieu d'introduire un nouvel octet de catégorie. DONT_SHADOW se trouve dans la catégorie animation avec DONT_CACHE/DONT_SHADE, et MISSION_COMMAND dans la catégorie effets avec EXPLODE et PLAY_SOUND.

Tip

Comparez avec du vrai bytecode.

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

Exemples concrets dans le catalogue de référence

De vraies unités qui poussent le format dans ses extrêmes ; ouvrez-les pour voir la complexité réelle des scripts plutôt que des exemples rédigés pour le texte :

  • CORKROG — Krogoth (Core) — l'un des plus gros COB du jeu (~25 Ko, plus de 30 scripts, animations complexes à plusieurs pièces).
  • ARMCOM — commandant Arm — une unité « riche » représentative : cycles de marche, de visée, de repos et de construction, plusieurs armes, gestion du D-gun.
  • CORTRUCK — camion de construction — COB quasi minimal, celui qui sert d'exemple commenté sur cette page (3 scripts : SmokeUnit, Create, Killed).
  • ARMSAM — Samson — bon exemple d'unité à tourelle (rotation de turret1 pour viser, animation de recul, synchronisation par wait-for-turn).

Décompilez-en n'importe lequel en local avec kbot cob decompile scripts/<name>.cob une fois un contexte kbot défini.


Voir aussi

  • 3DO — les noms de pièces référencés par les scripts COB se trouvent dans le .3do de l'unité.
  • FBI — le fichier de métadonnées de l'unité. Son champ ObjectName lie le COB à un 3DO ; Sounds définit la banque de sons que le script peut jouer.
  • Le tableau des règles de cob lint de la boîte à outils kbot.
  • Glossaire — pièce, axe, masque de signal, tick.