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
ObjectNamede 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 animationturnen cours. Idem pourwait-for-move.signal <n>tue chaque script dontsignal_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 :
- Les deux scripts qui fixent
SIG_MOVEsont tués quand le signal part — y comprisMoveUnitlui-même (peu importe, son travail est fait). - 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'opcodesignal. Idlene fixe pasSIG_MOVE, donc le prochainsignal SIG_MOVEne 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
ScriptCodeIndexArraysont 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_VALUEdiffè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) etglobal_N(statique par unité, persistante d'un appel à l'autre). Le décompilateur émetlocal_N/global_Npar défaut ; renommez-les dans le source pour la lisibilité. - L'outil Scriptor de Cavedog remplace parfois
CURRENT_SPEEDpar le littéral29, ce qui casse les scripts de typeMoveWatcheraprè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 deturret1pour viser, animation de recul, synchronisation parwait-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
.3dode l'unité. - FBI — le fichier de métadonnées de l'unité. Son champ
ObjectNamelie le COB à un 3DO ;Soundsdéfinit la banque de sons que le script peut jouer. - Le tableau des règles de
cob lintde la boîte à outils kbot. - Glossaire — pièce, axe, masque de signal, tick.