COB / BOS – Bytecode für Einheitenskripte
Jede Einheit in Total Annihilation führt ein Skript aus, das ihre Pieces animiert, Mündungsblitze der Waffen erzeugt, Soundeffekte abspielt und auf Spielereignisse reagiert. Die Quellsprache ist BOS („Basic Object Script“); der kompilierte Bytecode, den die Engine tatsächlich ausführt, ist COB („Cobble“ – Cavedogs Wortspiel für „compiled BOS“). Eine Datei pro Einheit, benannt nach dem
ObjectNameaus der FBI der Einheit.
Tip
Probier es selbst.
# 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/
Alle Flags (und die vollständige Tabelle der Lint-Regeln) stehen in der
kbot cob-Referenz der CLI.
Aus Go heraus. Verwende formats/scripting aus 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)
}
Zwei Sprachen, ein Dateiformat
| Stufe | Format | Werkzeuge |
|---|---|---|
| Quelltext | BOS – C-ähnlich, strukturiert | Von Hand geschrieben; Switecks Scriptor; kbot cob decompile gewinnt es per Round-Trip aus COB zurück |
| Kompiliert | COB – stackbasierter Bytecode | Von der Engine geladen; erzeugt von Scriptor oder kbot cob compile |
Die Engine sieht immer nur COB. BOS ist eine Erleichterung für menschliche
Autoren; wenn du „das Skript einer Einheit bearbeitest“, bearbeitest du in
Wahrheit den BOS-Quelltext, kompilierst ihn neu zu COB und lieferst das COB
im Add-on-.ufo der Einheit aus.
Ein minimales Beispiel:
// 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 übersetzt das in einen binären Blob. kbot cob disassemble -a
zeigt den Opcode-Strom mit Kontrollfluss-Pfeilen daneben; kbot cob decompile
geht den umgekehrten Weg und gewinnt BOS zurück, das fast immer bytegleich
den Round-Trip übersteht (kbot cob roundtrip prüft das über den gesamten
Spielkatalog).
Ein Quelltext wird als TA-Skript (COB-Version 4) kompiliert, sofern er
nicht mit .version 6 beginnt. Der Compiler verweigert, was TA 3.1c falsch
ausführen würde, statt einen Platzhalter zu schreiben: Die Konstrukte von
TA: Kingdoms (play-sound, Mission-Command, __tak_math_*, .sound_name)
und % sind in einem TA-Skript Fehler (TA hat keine Modulo-Instruktion, und
0x10037000, das Wort, zu dem % früher wurde, ist dort das bitweise XOR),
ebenso unbekannte Bezeichner und Funktionen, die mehr als die 32 Stack-Slots
des Spiels brauchen. Was sich trotzdem kompilieren lässt, aber einen Blick
verdient – eine zweimal definierte Funktion (jeder Aufruf bindet an die erste
Definition, so wie die Namenssuche des Spiels) –, wird als Warnung ausgegeben,
zusammen mit jedem Befund der TA-Kompatibilitätsregeln des Linters;
mit --strict lassen Warnungen den Befehl fehlschlagen. kbot cob assemble
wendet dieselben Prüfungen auf handgeschriebene Listings an.
COB-Aufbau auf der Platte
Important
Zwei Dialekte. Dieser Abschnitt beschreibt COB v4, den Dialekt,
den Total Annihilation ausliefert. TA: Kingdoms verwendet COB v6
(VersionSignature == 6), das alles Folgende beibehält, den Header aber
um 8 Byte erweitert und hinter dem Array der Piece-Namen eine zusätzliche
Offset-Tabelle für Befehls-Strings einfügt. Die Einzelheiten von v6 stehen
unter TA: Kingdoms – Unterschiede von COB v6;
die Opcode-Liste gilt für beide, die zusätzlichen Opcodes, die es nur in TAK
gibt, sind in Anhang C dokumentiert.
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
Die kanonischen Feldnamen entsprechen dem Struct HEADER_COB von Scriptor.
Frühere kbot-Versionen nannten LengthOfScripts / NumberOfStaticVars /
UKZero / OffsetToNameArray „Unknown0/1/2/3“ – dieselben Offsets, nur
jetzt mit den dokumentierten Bedeutungen.
Danach folgt, in diesem Aufbau:
┌─ 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
Ein Skript auflösen
Die Code-Index-Tabelle enthält Wort-Offsets als uint32, keine
Byte-Offsets. So findest du den Bytecode für Skript Nummer i:
uint32 first_instr_word = ScriptCodeIndexArray[i];
uint32 byte_offset = OffsetToScriptCode + first_instr_word * 4;
Das Skript endet dort, wo das nächste beginnt (beim letzten Skript: am Anfang der Index-Tabelle).
Important
String-Offsets in den Namenstabellen sind absolute Datei-Offsets, nicht relativ zum String-Pool. Cavedogs Werkzeuge haben den String-Pool nie verschoben, deshalb spielte das in der Praxis keine Rolle; wenn du ein COB von Hand umorganisierst, muss jeder Offset angepasst werden.
Der Opcode-Strom
COB ist eine stackbasierte virtuelle Maschine. Die meisten Opcodes holen ihre Argumente vom Datenstack des jeweiligen Skripts, erledigen ihre Arbeit und legen ein Ergebnis zurück. Manche Opcodes haben Inline-Operanden (Sprungziele, Piece-Nummern).
Jeder Opcode ist ein uint32 mit diesem Aufbau:
MSB LSB
┌─────────┬─────────┬─────────┬─────────┐
│ 0x10 │ family │ op │ flags │
└─────────┴─────────┴─────────┴─────────┘
Das höchste Byte ist immer 0x10; family/op/flags unterscheiden sich je
Opcode. Das Spiel vergleicht nicht das ganze Wort: Es verzweigt anhand von
word & 0x100FF000, Bits außerhalb dieser Maske werden also ignoriert, und
0x10064001 läuft als JUMP (kbots Disassembler schreibt ein solches Wort
als JUMP@0x10064001, damit es unverändert zurückassembliert wird). PUSH
(0x10021000) und POP (0x10023000) lesen dann ihre Quelle bzw. ihr Ziel
aus den unteren drei Bits. Der Katalog ist
scripting/opcodes.go in kbot-io;
scripting.Opcodes() listet jede Instruktion mit ihren Inline-Operanden
und ihrem Stack-Effekt auf.
Opcode-Familien
| Familie | Beispiele | Anmerkungen |
|---|---|---|
0x100x |
MOVE, TURN, SPIN, SHOW, HIDE, EMIT_SFX |
Piece-Animation. Piece-Nr. und Achsen-Nr. folgen inline. |
0x101x |
WAIT_FOR_TURN, WAIT_FOR_MOVE, SLEEP |
Asynchrone Barrieren – das Skript gibt die Kontrolle ab, bis die Bedingung erfüllt ist. |
0x102x |
PUSH_CONST, PUSH_LOCAL, PUSH_STATIC, POP_* |
Stack-Manipulation. |
0x103x |
ADD, SUB, MUL, DIV, BITWISE_AND, BITWISE_OR, XOR, NOT |
Arithmetik und Bitoperationen. Es gibt kein Modulo. |
0x104x |
RAND, GET_UNIT_VALUE, GET, IS_CARRYING_UNIT, CARRIER_UNIT_ID |
Abfragen an die Engine. |
0x105x |
<, <=, >, >=, ==, !=, &&, ||, XOR_ALT, ! |
Vergleiche und logische Operationen. XOR_ALT ist ein bitweises XOR, kein logisches. |
0x106x |
START_SCRIPT, CALL_SCRIPT, DISCARD_CALL, JUMP, RETURN, JUMP_IF_FALSE, SIGNAL, SET_SIGNAL_MASK |
Kontrollfluss und Nebenläufigkeit. |
0x107x |
EXPLODE, PLAY_SOUND |
Spezialeffekte. PLAY_SOUND gibt es nur in TA: Kingdoms. |
0x108x |
SET_VALUE, ATTACH_UNIT, DROP_UNIT |
Änderungen am Zustand der Engine. |
Eine kleine, aber repräsentative Auswahl:
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
Sprung-Offsets sind Byte-Offsets relativ zum Anfang des
Skript-Code-Abschnitts (nicht zur aktuellen Instruktion). JUMP 0x4C
bedeutet „springe nach OffsetToScriptCode + 0x4C“. Das Flag -a des
Disassemblers zeichnet Pfeile zwischen zusammengehörigen Instruktionen,
damit das sofort ins Auge fällt.
Nebenläufigkeitsmodell
Bei einer Einheit können viele Skripte gleichzeitig laufen – Walk, Aim, SmokeUnit, MoveWatcher usw. Sie werden kooperativ eingeplant:
sleep <ticks>gibt für N Ticks (je 1/30 s) die Kontrolle ab.wait-for-turn <piece> around <axis>wartet, bis dieses Piece seine aktuelleturn-Animation beendet hat. Dasselbe gilt fürwait-for-move.signal <n>beendet jedes Skript, für dassignal_mask & n != 0gilt.set-signal-mask <n>legt fest, an welchen Signalen das aktuelle Skript interessiert ist.
Über die Signalmaske setzen Skripte Präemption um: MoveWatcher setzt die
Maske SIG_MOVE und beobachtet die Geschwindigkeit; hält die Einheit an,
sendet es SIG_MOVE und beendet so die Laufanimationen sauber, bevor die
Ruhepose beginnt.
Warning
SET_SIGNAL_MASK meldet nur Interesse an – es beendet nichts. Erst
das Senden von signal X stoppt Skripte, bei denen die Maske X gesetzt ist.
Die Lint-Regel signal-never-signalled in kbot cob lint findet Skripte,
die eine Maske setzen, die nie jemand signalisiert (was fast immer ein Bug
ist).
Wie Präemption tatsächlich abläuft
Ein gängiges Muster: MoveUnit startet ein Animationsskript Walk; endet
die Bewegung, sendet die Engine SIG_MOVE, um sauber aufzuräumen, bevor
Idle beginnt. Der Ablauf:
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)
Drei Dinge fallen auf:
- Beide Skripte, die
SIG_MOVEsetzen, werden beendet, wenn das Signal ausgelöst wird – auchMoveUnitselbst (was egal ist; seine Arbeit ist getan). - Das signalisierende Skript (
MoveUnit) überlebt lange genug, um das Signal zu senden, bevor es beendet wird; die Laufzeitumgebung prüft die Masken erst nachdem der Opcodesignalzurückgekehrt ist. IdlesetztSIG_MOVEnicht, das nächstesignal SIG_MOVElässt es also unberührt.
Beispiel im Detail – 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);
}
Die zugehörige Disassemblierung (kbot cob disassemble -a -s SmokeUnit)
zeigt den tatsächlichen Opcode-Strom, mit JUMP_IF_FALSE/JUMP-Pfeilen, die
sich durch die Rümpfe der while/if-Blöcke ziehen – nützlich, wenn die
Ausgabe des Decompilers mehrdeutig ist.
Linting
kbot cob lint (und der Lint-Tab des Asset-Explorers) führt den
Standard-Regelsatz von kbot-io aus: Stilregeln, die aus echten Bugs in
Cavedogs eigenen Skripten und aus von der Community eingereichten Mod-Patches
abgeleitet sind, zwei Prüfungen auf Dateiebene und – für jedes COB, das nicht
Version 6 angibt – die Kompatibilitätsregeln für TA 3.1c. Der Stilkatalog:
| Regel | Schweregrad | Findet |
|---|---|---|
unused-piece |
Warnung | Piece deklariert, aber in keinem Skript verwendet. |
unused-static / unused-local |
Warnung | Variable angelegt, aber nie gelesen. |
always-true / dead-code |
Info / Warnung | if(1) / while(0). |
long-function / high-complexity |
Warnung | > 100 Instruktionen oder zyklomatische Komplexität > 15. |
invalid-call |
Fehler | call-script auf eine nicht existierende Funktion. |
speed-zero |
Warnung | move/turn mit speed <0> – die Animation endet nie. |
empty-function |
Info | Der Funktionsrumpf besteht nur aus return 0. |
duplicate-animation |
Warnung | Zwei identische Animationsbefehle direkt hintereinander. |
sleep-only-guard |
Info | Ein if-Block enthält nichts außer sleep. |
duplicate-if |
Info | Zwei if mit derselben Bedingung hintereinander. |
raw-signal |
Info | signal 4 statt signal SIG_AIM. |
unnamed-global |
Info | global_3 statt einer benannten statischen Variable. |
signal-never-signalled |
Warnung | Ein Skript maskiert ein Signal, das niemand sendet. |
recursive-call |
Warnung | Zyklus aus call-script-Aufrufen. |
duplicate-function |
Warnung | Zwei Skripte teilen sich einen Namen; das Spiel ruft nur das erste auf. |
malformed-cob |
Fehler / Warnung | Die Datei lässt sich nicht laden, oder der Code eines Skripts ist abgeschnitten (Fehler); Namenstabellen, die der Leser toleriert hat (Warnung). |
Die TA-Kompatibilitätsregeln melden, woran TA 3.1c abbricht oder was es
anders ausführt. Sie sind Fehler, erscheinen wie die anderen in der
SARIF-Ausgabe von --ci und überspringen Dateien von TA: Kingdoms
(Version 6):
| Regel | Findet |
|---|---|
ta-kingdoms-opcode |
PLAY_SOUND, MISSION_COMMAND oder einen TAK_MATH_*-Operator in einem TA-Skript. TA hat dafür keinen Handler, und das Skript bricht ab, sobald es einen davon erreicht. |
ta-unknown-opcode |
Ein Opcode-Wort, das kein Spiel ausführt. |
ta-push-flags |
Ein PUSH, dessen untere Bits nicht 1 (Konstante), 2 (lokal) oder 4 (statisch) sind, oder ein POP, dessen untere Bits nicht 2 oder 4 sind – einschließlich der flaglosen Wörter 0x10021000 und 0x10021008, die manche Werkzeuge geschrieben haben. |
ta-stack-limit |
Mehr als 32 STACK_ALLOCs, oder lokale Variablen plus anstehende Werte über die 32 Stack-Slots des Spiels hinaus; das Spiel prüft das nicht und überschreibt den Speicher hinter dem Skript. |
ta-get-arguments |
Ein GET, das mit weniger als fünf anstehenden Werten erreicht wird. GET holt immer einen Port und vier Argumente vom Stack, ein get mit nur einem Argument nimmt den Rest also aus den lokalen Variablen der Funktion und überschreibt sie. |
ta-stack-underflow |
Jede andere Instruktion, die mehr Werte vom Stack holt, als anstehen, oder Pfade, die mit unterschiedlicher Stacktiefe zusammenlaufen. |
ta-discard-call |
DISCARD_CALL mit mehr als vier Argumenten. |
Die vollständigen Regelbeschreibungen stehen in der README von kbot. Der Decompiler behält die ursprüngliche Nummerierung von Variablen und Pieces absichtlich bei, damit ein Zyklus dekompilieren→linten→korrigieren→kompilieren die Mods sichtbar macht, die Pflege brauchen.
Stolperfallen
Warning
call-script ist 0x10062000. 0x10063000 ist eine andere
Instruktion, DISCARD_CALL: Sie hat dieselben zwei Inline-Wörter
(Skriptnummer und Argumentanzahl), holt aber nur die Argumente vom Stack
und führt nichts aus. Kein Skript der Verkaufsversion verwendet sie; kbots
Decompiler schreibt sie als __discard_call(word, args…).
Warning
Mehrere veröffentlichte Opcode-Tabellen benennen die Bit-Wörter falsch.
TA führt 0x10037000 als bitweises XOR aus (nicht als Modulo – TA hat
keine Modulo-Instruktion), 0x10038000 als unäres bitweises NOT (nicht
als XOR) und 0x10059000 als zweites bitweises XOR (kein logisches XOR:
6 XOR_ALT 3 ergibt 5). 0x1003A000 ist ein Operator von TA: Kingdoms, an
dem TA abbricht. Listings mit den alten Namen (MOD, BITWISE_XOR,
BITWISE_NOT, LOGICAL_XOR) werden weiterhin zu genau den Wörtern
assembliert, aus denen sie entstanden sind.
- Alle Offsets im COB-Header sind absolute Datei-Offsets, die Einträge
von
ScriptCodeIndexArraydagegen Wort-Offsets innerhalb des Code-Abschnitts. Beides wird leicht verwechselt. - Piece-Nummern in Opcodes sind Indizes in die Tabelle der Piece-Namen, keine Strings. Das COB enthält die Namen als String-Pool; die Laufzeitumgebung verwendet der Geschwindigkeit wegen Indizes.
- Die
get UNIT_VALUE-Opcodes unterscheiden sich subtil zwischen TA und TA: Kingdoms. TA: Kingdoms hat Wert-IDs für Magie-Regeneration, Befehlspunkte usw. hinzugefügt; die Werttabelle von TA ist kompakter. kbots Decompiler benennt derzeit die TA-Menge; TAK-Portnummern ≥ 21 werden als numerische Literale ausgegeben (die trotzdem den Round-Trip durch den Assembler überstehen). Den vollständigen Unterschied beschreibt TA: Kingdoms – Unterschiede von COB v6. - Variablen gibt es in zwei Sorten:
local_N(funktionslokaler Stack-Slot) undglobal_N(statische Variable pro Einheit, die über Aufrufe hinweg erhalten bleibt). Der Decompiler gibt standardmäßiglocal_N/global_Naus; benenne sie im Quelltext um, damit er lesbarer wird. - Das Cavedog-Scriptor-Tool setzt
CURRENT_SPEEDgelegentlich als Literal29ein, was Skripte im Stil vonMoveWatchernach einem Round-Trip kaputtmacht. kbots Decompiler stellt den symbolischen Namen wieder her; wenn du unbedingt einen Round-Trip durch Scriptor machen musst, setze Klammern:(get CURRENT_SPEED) > 5.
Typische Größen
| Asset | Beobachteter Bereich in Cavedogs scripts/*.cob |
|---|---|
| Dateigröße | 0,5 KB – 25 KB |
| Header-Overhead | immer 44 Byte |
| Skripte pro Einheit | 3 – 30 (typische Einheiten: 5–10) |
| Pieces pro Einheit | 3 – 80 (Krogoth ≈ 80) |
| Größe des Code-Abschnitts | 300 B – 20 KB (schwankt stark mit der Komplexität) |
armcom.cob (Commander) |
~14 KB, 30 Skripte |
cortruck.cob (einfache Einheit) |
~750 B, 3 Skripte |
TA: Kingdoms – Unterschiede von COB v6
Note
Dieser Abschnitt beschreibt nur, was in TA:K anders ist. Alles oben beschreibt COB v4 (TA). TA: Kingdoms behält dieselbe Stack-VM, denselben String-Pool, dasselbe Kodierungsschema der Opcodes und dieselbe Mechanik für Skript- und Piece-Namen bei; die Unterschiede sind ein erweiterter Header, eine zusätzliche String-Tabelle hinter dem Array der Piece-Namen und vier neue Opcodes.
Dateiaufbau von 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)
Der 8 Byte große Sub-Header liegt zwischen OffsetToNameArray (dem Ende
des kanonischen Headers aus 11 DWORDs bei Byte 44) und dem Anfang des
Code-Abschnitts (OffsetToScriptCode, typischerweise 0x34 = 52). Er
enthält zwei uint32-Werte in Little-Endian:
| Feld | Bedeutung |
|---|---|
[0] |
Absoluter Datei-Offset der Offset-Tabelle der Sound-Namen. Immer gleich OffsetToPieceNameOffsetArray + NumPieces × 4 – also das Byte direkt hinter dem Offset-Array der Piece-Namen. Redundant zum kanonischen Aufbau; der Writer rekonstruiert ihn. |
[1] |
Anzahl der Sound-Namen in der Tabelle. Maßgebliche Anzahl. |
Jeder Eintrag der Offset-Tabelle der Sound-Namen ist der absolute
Datei-Offset eines NUL-terminierten Strings im String-Pool – etwa
"SetMission o 1, s", "create ARASWORD", "ARROW10". Der Bytecode
verweist über den Opcode MISSION_COMMAND per Index (nicht per Offset)
auf sie (siehe Anhang C). Die Tabelle
heißt Sound-Namen-Array, weil der kanonische Scriptor-Compiler Strings
über die Aktion ADDSOUND darin registriert; tatsächlich enthält sie
Befehls-Strings für die Engine und keine Audio-Dateinamen.
Die Darstellung in kbot
scripting.COB führt die
Sound-Namen als strukturiertes []string-Feld – es werden keine
undurchsichtigen Bytes mitgeschleppt. Alles andere (die Werte des
Sub-Headers, die Offset-Tabelle, die Offsets der einzelnen Strings im Pool)
wird beim Schreiben aus dieser Liste rekonstruiert:
type COB struct {
// …canonical TA fields…
SoundNames []string // TAK-only; nil for v4
}
Bei v4-Dateien ist der Slice immer nil, und sowohl der Sub-Header als auch die Offset-Tabelle der Sound-Namen fehlen in der Ausgabe.
Disassembler und Decompiler geben jeden String als Direktive .sound_name
aus, damit die Strings einen Round-Trip durch .coba und .bos überstehen:
.version 6
.statics 6
.sound_name "create NPCEMEN"
.sound_name "SetMission o 1, s"
.sound_name "ARROW10"
.script Start
…
Neue Opcodes
TAK-.cob-Dateien verwenden Instruktionen, für die TA 3.1c keinen Handler
hat (PLAY_SOUND, MISSION_COMMAND und die TAK_MATH_*-Operatoren), dazu
DONT_SHADOW, das TA als No-op ausführt. Ihre genaue Semantik im Spiel ist
nicht dokumentiert, aber ihr binärer Aufbau (Größe + Anzahl der
Inline-Parameter) ist aus jeder .cob der TAK-Verkaufsversion bekannt, und
das genügt, damit der Round-Trip disassemblieren→assemblieren die
ursprünglichen Bytes unverändert wiederherstellt. Die vollständige Referenz
steht in Anhang C.
Stand des Round-Trips
| Pipeline | TA | TA: Kingdoms |
|---|---|---|
disassemble → assemble (bytegleich) |
278 / 278 ✅ | 267 / 267 ✅ |
decompile → compile (bytegleich) |
278 / 278 ✅ | Einfache Einheitenskripte und viele Missions-COBs (≈ 70 / 267) überstehen den Round-Trip bytegenau. Die übrigen Fehlschläge sind Kontrollflussmuster, bei denen die strukturierte Rekonstruktion von if/while durch den Decompiler ein JUMP-Layout wählt, das der Compiler leicht anders erzeugt – der Bytecode ist semantisch gleichwertig (jeder TAK-Opcode, jeder Befehls-String, der Sub-Header und das Versionswort von v6 bleiben erhalten), nur eben nicht bytegleich. Dieselbe Art von Abweichung träfe jeden Zyklus dekompilieren→kompilieren bei einem nicht trivialen Skript. |
BOS-Syntax für die TAK-Erweiterungen
Der Decompiler gibt diese Pseudoaufrufe aus, damit TAK-COBs einen Round-Trip
durch BOS überstehen. Der Compiler akzeptiert dieselbe Syntax, aber nur nach
.version 6 (mit Ausnahme von dont-shadow – TA führt es als No-op aus).
dont-shadow und Mission-Command verwenden die kanonischen
TAK-Schlüsselwörter von Scriptor; die Mathe-Opcodes haben in Scriptor keinen
dokumentierten Namen (es bezeichnet sie als ?? und ????), daher behält
kbot die Platzhalter __tak_math_* bei:
// 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
Das erste Argument von Mission-Command ist der Sound-Name als String in
Anführungszeichen – der Compiler sucht ihn in der Sound-Namen-Tabelle (die
zugehörige Direktive .sound_name muss also weiter oben in der Datei
stehen). Die übrigen Argumente werden auf den Stack gelegt, und die Engine
verbraucht sie.
Zwei Direktiven am Dateianfang tragen die restlichen Metadaten von v6:
.version 6
.sound_name "create NPCEMEN"
.sound_name "SetMission o 1, s"
.sound_name "ARROW10"
.version wird immer ausgegeben (damit v4-Dateien von TA dieselbe Pipeline
identisch durchlaufen). Jedes .sound_name ist ein Eintrag in der
Sound-Namen-Tabelle des jeweiligen COB; der Writer baut den 8-Byte-Sub-Header
und die Offset-Tabelle der Sound-Namen aus len(SoundNames) und der
Reihenfolge der Einträge neu auf.
Den TAK-Epilog STACK_ALLOC ; RETURN, den jedes wertlose return; in v6
erzeugt, übernimmt der Compiler: return; wird bei .version 6 zu
STACK_ALLOC ; RETURN, return <expr>; wird ohne Präfix zu
<expr> ; RETURN (wie bei Cavedogs Compiler).
Auf deiner eigenen Installation nachvollziehen:
kbot ctx use kingdoms # or wherever you registered TAK
kbot cob roundtrip
Port-Symbole jenseits von Port 20
Skripte von TA: Kingdoms verwenden get_unit_value-Ports bis 46
(CURRENT_SPEED, VETERAN_LEVEL, Felder des Magie-Pools usw.). kbots
Decompiler benennt derzeit nur die Ports 1–20 symbolisch (siehe
Anhang B); TAK-Ports ≥ 21 werden
als numerische Literale ausgegeben. Der Bytecode speichert sie ohnehin als
Ganzzahlen, Round-Trips sind also nicht betroffen – nur die Lesbarkeit des
BOS leidet. Die Ergänzung der TAK-Portnamen wird separat verfolgt.
Anhang A – Vollständige Opcode-Referenz
Jede Instruktion, die TA oder TA: Kingdoms ausführt. Maßgeblich ist
formats/scripting/opcodes.go in kbot-io.
Die Stack-Notation verwendet die Forth-Schreibweise ( before -- after ).
Die Spalten Inline zeigen, wie viele uint32-Wörter auf das Opcode-Wort
selbst folgen (beim Dekodieren verbraucht, nicht vom Stack).
Animation – Piece-Manipulation (0x100x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
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 führt es aus, aber seine Einheitenskripte machen nichts damit; kein Skript der Verkaufsversion verwendet es. |
0x1000A000 |
DONT_SHADOW |
1 (piece) |
( -- ) |
dont-shadow <piece>; – TA führt es als No-op aus; siehe Anhang 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>; |
Warteoperationen (0x101x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
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>; |
Stack-Manipulation (0x102x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
0x10021000 |
PUSH_IMM |
1 (value) |
– | PUSH ohne Flags: Das Spiel bricht daran ab (ta-push-flags). Wird vom Compiler nie geschrieben. |
0x10021001 |
PUSH_CONST |
1 (value) |
( -- v ) |
<literal> in einem beliebigen Ausdruck |
0x10021002 |
PUSH_LOCAL |
1 (var#) |
( -- v ) |
local_N in einem beliebigen Ausdruck |
0x10021004 |
PUSH_STATIC |
1 (var#) |
( -- v ) |
global_N in einem beliebigen Ausdruck |
0x10022000 |
STACK_ALLOC |
0 | ( -- ) |
(implizit – eines pro var und Parameter) |
0x10023002 |
POP_LOCAL |
1 (var#) |
( v -- ) |
local_N = expr; |
0x10023004 |
POP_STATIC |
1 (var#) |
( v -- ) |
global_N = expr; |
0x10024000 |
POP_STACK |
0 | ( v -- ) |
(verwirft das Ergebnis eines Ausdrucks) |
Arithmetik (0x103x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
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 hat keine Modulo-Instruktion: % ist in einem TA-Skript ein
Kompilierfehler. 0x10039000–0x1003B000 sind die TAK_MATH_*-Operatoren
von TA: Kingdoms (Anhang C); TA bricht an
ihnen ab.
Abfragen an die Engine (0x104x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
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, …) – holt immer einen Port und vier Argumente vom Stack; für fehlende legt der Compiler Nullen auf den Stack |
0x10044000 |
IS_CARRYING_UNIT |
0 | ( unit -- bool ) |
__is_carrying_unit(unit) – 1, wenn die angegebene Einheit auf dieser mitfährt |
0x10045000 |
CARRIER_UNIT_ID |
0 | ( -- id ) |
__carrier_unit_id() – die Einheit, die diese trägt, oder 0 |
Den Katalog der Ports findest du in Anhang B.
Vergleich und Logik (0x105x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
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 – ein bitweises XOR wie 0x10037000 (6 XOR 3 ergibt 5), kein logisches |
0x1005A000 |
LOGICAL_NOT |
0 | ( a -- !a ) |
NOT a |
Kontrollfluss und Nebenläufigkeit (0x106x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
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…); – holt die Argumente vom Stack (mehr als vier laufen über den Puffer des Spiels hinaus) und führt nichts aus |
0x10064000 |
JUMP |
1 (offset) |
( -- ) |
(Compiler – goto, Schleifen) |
0x10065000 |
RETURN |
0 | ( v -- ) |
return v; |
0x10066000 |
JUMP_IF_FALSE |
1 (offset) |
( v -- ) |
(Compiler – if/while) |
0x10067000 |
SIGNAL |
0 | ( mask -- ) |
signal <mask>; |
0x10068000 |
SET_SIGNAL_MASK |
0 | ( mask -- ) |
set-signal-mask <mask>; |
offset ist bei JUMP / JUMP_IF_FALSE ein Byte-Offset relativ zu
OffsetToScriptCode, nicht relativ zur aktuellen Instruktion.
Effekte (0x107x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
0x10071000 |
EXPLODE |
1 (piece) |
( type -- ) |
explode <piece> type <type>; |
0x10072000 |
PLAY_SOUND |
1 (vol) |
( sound -- result ) |
play-sound(<sound>, <vol>); – nur TA: Kingdoms (.version 6); TA bricht daran ab |
Setz-Operationen (0x108x)
| Opcode | Mnemonic | Inline | Stack-Effekt | BOS-Syntax |
|---|---|---|---|---|
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>; |
Kodierung der Opcodes
Jedes Opcode-Wort hat die Form 0x10CCSSFF, wobei:
CC= das Kategorie-Byte (Animation, Mathematik, Kontrollfluss, …).SS= das Unterbefehls-Nibble im oberen Nibble des zweithöchsten Bytes.FF= das Flags-Byte.
Das Spiel verzweigt anhand von word & 0x100FF000 und ignoriert die übrigen
Bits, ein Wort wie 0x10064001 läuft also als JUMP; der Disassembler
schreibt es als JUMP@0x10064001, und der Assembler liest das wieder zum
selben Wort ein. PUSH und POP lesen ihre Quelle bzw. ihr Ziel aus den
unteren drei Bits: PUSH akzeptiert 1 (Konstante), 2 (lokal) und 4
(statisch), POP akzeptiert 2 und 4, und bei jedem anderen Wert bricht das
Skript ab.
Wörter, die kein Spiel ausführt, stellt der Disassembler als
UNKNOWN_0x10xxxxxx dar (und ta-unknown-opcode meldet sie); wenn du eines
in einer echten Datei findest, eröffne bitte ein Issue mit dem COB als
Beispiel.
Anhang B – Port-Tabelle von get_unit_value
Die 20 Ports, die kbots Decompiler derzeit symbolisch benennt. Das echte TA hat ~80; TA: Kingdoms fügt weitere ~15 für magiebezogene Größen hinzu. Alles oberhalb von Port 20 erscheint in der Ausgabe des Decompilers heute als numerisches Literal.
| Port | Symbolischer Name | Liefert | Anmerkungen |
|---|---|---|---|
| 1 | ACTIVATION |
bool | Ist der „Aktiv“-Schalter der Einheit an (Radar, Störsender, Befestigungen)? |
| 2 | STANDINGMOVEORDERS |
enum | 0 = Position halten, 1 = manövrieren, 2 = umherstreifen. |
| 3 | STANDINGFIREORDERS |
enum | 0 = Feuer halten, 1 = Feuer erwidern, 2 = Feuer frei. |
| 4 | HEALTH |
0–100 | Verbleibender Prozentsatz von MaxDamage. |
| 5 | INBUILDSTANCE |
bool | Fährt die Einheit gerade ihre Nano-Lathes aus? |
| 6 | BUSY |
bool | Ist die Einheit gerade mit einem Befehl beschäftigt? |
| 7 | PIECE_XZ |
gepackt | Kombiniertes X/Z eines Piece (kodiert). Wird zum Zielen von Projektilen verwendet. |
| 8 | PIECE_Y |
int | Y-Koordinate eines Piece relativ zum Ursprung der Einheit. |
| 9 | UNIT_XZ |
gepackt | Kombiniertes X/Z der Einheit. |
| 10 | UNIT_Y |
int | Y-Koordinate der Einheit. |
| 11 | UNIT_HEIGHT |
int | Höhe über dem Gelände. |
| 12 | XZ_ATAN |
int | atan2 zwischen zwei XZ-kodierten Werten (zum Ausrichten von Geschütztürmen). |
| 13 | XZ_HYPOT |
int | Hypotenuse zwischen zwei XZ-kodierten Werten. |
| 14 | ATAN |
int | atan2(y, x) zweier auf den Stack gelegter Werte. |
| 15 | HYPOT |
int | hypot(x, y). |
| 16 | GROUND_HEIGHT |
int | Geländehöhe an der Position der Einheit. |
| 17 | BUILD_PERCENT_LEFT |
0–100 | Wird von SmokeUnit verwendet, um den Baufortschritt zu kennen. 0, sobald fertig gebaut. |
| 18 | YARD_OPEN |
bool | Für Gebäude mit Yardmap: Sind die Tore offen? |
| 19 | BUGGER_OFF |
bool | Wird von der Engine gesetzt, wenn die Einheit aus dem Weg fahren muss. |
| 20 | ARMORED |
bool | Ist die gepanzerte Haltung der Einheit aktiv (Bulldog, Goliath usw.)? |
Zu den höheren Ports gehören CURRENT_SPEED (29 – berüchtigt dafür, dass der
Cavedog-Scriptor ihn als Literal einsetzt; siehe Stolperfallen oben),
MAX_ID, UNIT_TEAM, UNIT_BUILD_PERCENT, BUILDER, UNIT_ALLIED und
viele mehr. Die vollständige Liste steht in Cavedogs interner Dokumentation;
das Community-Wiki in den TAUniverse-Foren hat den umfassendsten öffentlichen
Katalog.
TA: Kingdoms erweitert den Portbereich. Die reinen TAK-Ports, die kbot erkennt
(der Decompiler gibt den symbolischen Namen aus, der Compiler akzeptiert ihn
als Eingabe), stammen aus der Tabelle [UNITVLAUES] von Scriptor:
| Port | Symbol | Anmerkungen |
|---|---|---|
| 21 | WEAPON_AIM_ABORTED |
Wird gesetzt, wenn die Zielberechnung einer Waffe abgebrochen wird. |
| 22 | WEAPON_READY |
Die Waffe ist nachgeladen und feuerbereit. |
| 23 | WEAPON_LAUNCH_NOW |
Die Engine hat die Waffe abgefeuert. |
| 26 | FINISHED_DYING |
Die Todesanimation ist abgeschlossen. |
| 27 | ORIENTATION |
Blickrichtung der Einheit. |
| 28 | IN_WATER |
Die Einheit ist im Wasser. |
| 29 | CURRENT_SPEED |
Aktuelle Bewegungsgeschwindigkeit (mit TA gemeinsam – Scriptor nummeriert ihn hier genauso). |
| 31 | MAGIC_DEATH |
Die Einheit starb durch Magieschaden. |
| 32 | VETERAN_LEVEL |
Veteranenstufe. |
| 34 | ON_ROAD |
Die Einheit steht auf einer Straßenkachel. |
Anhang C – Opcodes von TA: Kingdoms
Note
Opcodes nur in TA:K. Keiner davon kommt in .cob-Dateien der
TA-Verkaufsversion vor. DONT_SHADOW und MISSION_COMMAND verwenden die
kanonischen Mnemonics von Scriptor (Switecks TAK-fähigem
Compiler/Decompiler); die Mathe-Operationen haben keine dokumentierte
Semantik – Scriptor bezeichnet sie als ?? und ????, daher behält kbot
die Platzhalter TAK_MATH_09 / TAK_MATH_0A / TAK_MATH_0B bei. Die
Skripte der Verkaufsversion von TA: Kingdoms verwenden jeden davon als
binären Operator (zwei Werte vom Stack, einer zurück), wenn sie Bitfelder
packen und entpacken. TA 3.1c hat keinen Handler für PLAY_SOUND,
MISSION_COMMAND oder die Mathe-Operatoren; DONT_SHADOW führt es als
No-op aus.
| Opcode | Mnemonic | Inline | Vorkommen in der TAK-Verkaufsversion | Anmerkungen |
|---|---|---|---|---|
0x1000A000 |
DONT_SHADOW |
1 (piece) |
66 | Schaltet den Schattenwurf für ein einzelnes Piece ab. Steht in der Animationskategorie neben DONT_SHADE (0x1000E000) – kbot nannte es früher TAK_ANIM_0A. Gleiche Form auf der Platte wie die anderen ANIM_*-Operationen: 4-Byte-Opcode + ein piece-DWORD. BOS-Schlüsselwort: dont-shadow <piece>;. |
0x10039000 |
TAK_MATH_09 |
0 | 264 | Binärer Operator der Mathe-Kategorie, insgesamt 4 Byte: ( a b -- r ). BOS: __tak_math_09(a, b). |
0x1003A000 |
TAK_MATH_0A |
0 | – | Gleiche Form. Manche Opcode-Tabellen nennen ihn BITWISE_NOT; das bitweise NOT von TA ist 0x10038000. BOS: __tak_math_0a(a, b). |
0x1003B000 |
TAK_MATH_0B |
0 | 66 | Gleiche Form wie TAK_MATH_09. BOS: __tak_math_0b(a, b). |
0x10073000 |
MISSION_COMMAND |
2 (soundNameIdx, argCount) |
3.885 | Aufruf eines Engine-Befehls. Das erste Inline-DWORD ist ein Index in die Tabelle SoundNames des COB; das zweite ist die Anzahl der Werte, die als Argumente vom Stack geholt werden. Der Opcode legt ein einzelnes Ergebnis zurück auf den Stack (das meist von einem folgenden POP_STACK verworfen oder per POP_STATIC/POP_LOCAL gespeichert wird). Mit Abstand der häufigste Opcode, den es nur in TAK gibt. BOS-Schlüsselwort: Mission-Command("name", args…) – entspricht der kanonischen Syntax von Scriptor. |
Kodierung (Zusammenfassung)
Jedes COB-Opcode-Wort hat die Form 0x10CCSSFF, wobei CC die Kategorie und
SS der Unterbefehl ist – diese TAK-Opcodes belegen einfach bisher
ungenutzte CC:SS-Plätze innerhalb der bestehenden Kategorien, statt ein
neues Kategorie-Byte einzuführen. DONT_SHADOW liegt in der
Animationskategorie neben DONT_CACHE/DONT_SHADE, MISSION_COMMAND in der
Effektkategorie neben EXPLODE und PLAY_SOUND.
Tip
Mit echtem Bytecode abgleichen.
kbot ctx use tak-30bb-flat
kbot cob disassemble scripts/araat.cob -a | grep -E 'DONT_SHADOW|MISSION|TAK_MATH|PLAY_SOUND'
Echte Beispiele im Referenzkatalog
Echte Einheiten, die die Extreme des Formats ausreizen – öffne sie, um die tatsächliche Komplexität von Skripten zu sehen statt Beispielen, die für den Text zurechtgelegt wurden:
CORKROG– Krogoth (Core) – eines der größten COBs im Spiel (~25 KB, über 30 Skripte, komplexe Animationen über viele Pieces).ARMCOM– Arm Commander – repräsentative „reichhaltige“ Einheit: Zyklen für Laufen/Zielen/Ruhe/Bauen, mehrere Waffen, Behandlung der D-Gun.CORTRUCK– Construction Truck – nahezu minimales COB, das auf dieser Seite als durchgehendes Beispiel dient (3 Skripte:SmokeUnit,Create,Killed).ARMSAM– Samson – gutes Beispiel für eine Einheit mit Geschützturm (turret1dreht sich zum Zielen, Rückstoßanimation, Synchronisation perwait-for-turn).
Dekompiliere jedes davon lokal mit kbot cob decompile scripts/<name>.cob,
sobald ein kbot-Kontext gesetzt ist.
Siehe auch
- 3DO – die Piece-Namen, auf die COB-Skripte verweisen, stehen in
der
.3doder Einheit. - FBI – die Metadatendatei der Einheit.
ObjectNameverbindet dort das COB mit einem 3DO;Soundslegt die Soundbank fest, die das Skript abspielen kann. - Die Regeltabelle von
cob lintin der kbot-CLI. - Glossar – piece, axis, signal mask, tick.