Усі сторінки форматів ↓
Юніти, скрипти, конфігурації

COB / BOS — байткод скриптів юнітів

Джерело: coreprime/kbot @ 0ab97bf · Редагувати на GitHub

Кожен юніт у Total Annihilation виконує скрипт, який анімує його частини (pieces), випускає спалахи пострілів, відтворює звукові ефекти й реагує на ігрові події. Мова вихідного коду — BOS («Basic Object Script»); скомпільований байткод, який насправді виконує рушій, — це COB («Cobble» — каламбур Cavedog на «compiled BOS»). Один файл на юніт, названий за ObjectName юніта з його FBI.

Tip

Спробуйте самі.

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

Усі прапорці (і повну таблицю правил лінтера) див. у довіднику CLI kbot cob.

З Go. Скористайтеся пакетом formats/scripting з 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)
}

Дві мови, один формат файлу

Етап Формат Інструменти
Вихідний код BOS — C-подібна, структурована мова Пишеться вручну; Scriptor від Switeck; kbot cob decompile відтворює його з COB (round-trip)
Скомпільований COB — стековий байткод Завантажується рушієм; створюється Scriptor або kbot cob compile

Рушій бачить лише COB. BOS — це зручність для людей-авторів: коли ви «редагуєте скрипт юніта», ви насправді редагуєте вихідний код BOS, перекомпільовуєте його в COB і постачаєте COB у додатковому .ufo юніта.

Мінімальний приклад:

// 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 перетворює це на двійковий блоб. kbot cob disassemble -a показує потік опкодів поруч зі стрілками потоку керування; kbot cob decompile робить зворотне й відновлює BOS, який майже завжди проходить round-trip (розбір і повторне збирання) байт у байт (kbot cob roundtrip перевіряє це на всьому каталозі гри).

Вихідний код компілюється як скрипт TA (COB версії 4), якщо він не починається з .version 6. Компілятор відмовляється від того, що TA 3.1c виконала б неправильно, замість того щоб записати заглушку: конструкції TA: Kingdoms (play-sound, Mission-Command, __tak_math_*, .sound_name) і % у скрипті TA є помилками (у TA немає інструкції остачі від ділення, а 0x10037000 — слово, на яке раніше перетворювався %, — є її побітовим XOR), так само як невідомі ідентифікатори й функції, яким потрібно більше за 32 комірки стека гри. Те, що все ж компілюється, але заслуговує на увагу, — функція, визначена двічі (кожен виклик прив'язується до першого визначення, як і під час пошуку імені в грі), — виводиться як попередження разом із будь-якими знахідками правил лінтера сумісності з TA; --strict змушує команду завершуватися помилкою через попередження. kbot cob assemble застосовує ті самі перевірки до написаних вручну лістингів.


Структура COB на диску

Important

Два діалекти. Цей розділ описує COB v4 — діалект, з яким постачається Total Annihilation. TA: Kingdoms використовує COB v6 (VersionSignature == 6), який зберігає все описане нижче, але розширює заголовок на 8 байтів і вставляє після масиву імен частин додаткову таблицю зміщень командних рядків. Подробиці v6 див. у розділі TA: Kingdoms — відмінності COB v6; список опкодів стосується обох версій, а додаткові опкоди, наявні лише в TAK, описано в додатку 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

Канонічні назви полів відповідають структурі HEADER_COB зі Scriptor. Раніші версії kbot називали LengthOfScripts / NumberOfStaticVars / UKZero / OffsetToNameArray «Unknown0/1/2/3» — зміщення ті самі, лише тепер застосовано задокументовані назви.

Далі в такому порядку йдуть:

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

Пошук скрипту

Таблиця індексів коду містить зміщення у словах uint32, а не в байтах. Щоб знайти байткод скрипту номер i:

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

Скрипт закінчується там, де починається наступний (або, для останнього скрипту, на початку таблиці індексів).

Important

Зміщення рядків у таблицях імен — це абсолютні зміщення у файлі, а не зміщення відносно пулу рядків. Інструменти Cavedog ніколи не переміщували пул рядків, тож на практиці це не мало значення; якщо ж ви переупорядковуєте COB вручну, доведеться оновити кожне зміщення.


Потік опкодів

COB — це стекова віртуальна машина. Більшість опкодів знімають аргументи зі стека даних, окремого для кожного скрипту, виконують свою дію й кладуть результат назад. Деякі опкоди мають вбудовані операнди (цілі переходів, номери частин).

Кожен опкод — один uint32 з такою структурою:

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

Старший байт завжди 0x10; family/op/flags залежать від опкоду. Гра не порівнює слово цілком: вона диспетчеризує за word & 0x100FF000, тож біти поза цією маскою ігноруються, і 0x10064001 виконується як JUMP (дизасемблер kbot записує таке слово як JUMP@0x10064001, щоб воно асемблювалося назад без змін). Потім PUSH (0x10021000) і POP (0x10023000) беруть своє джерело або призначення з трьох молодших бітів. Каталогом слугує scripting/opcodes.go з kbot-io; scripting.Opcodes() перелічує кожну інструкцію з її вбудованими операндами й дією на стек.

Сімейства опкодів

Сімейство Приклади Примітки
0x100x MOVE, TURN, SPIN, SHOW, HIDE, EMIT_SFX Анімація частин. Номер частини й номер осі йдуть одразу за опкодом.
0x101x WAIT_FOR_TURN, WAIT_FOR_MOVE, SLEEP Асинхронні бар'єри — скрипт віддає керування, доки умову не виконано.
0x102x PUSH_CONST, PUSH_LOCAL, PUSH_STATIC, POP_* Робота зі стеком.
0x103x ADD, SUB, MUL, DIV, BITWISE_AND, BITWISE_OR, XOR, NOT Арифметика й побітові операції. Остачі від ділення немає.
0x104x RAND, GET_UNIT_VALUE, GET, IS_CARRYING_UNIT, CARRIER_UNIT_ID Запити до рушія.
0x105x <, <=, >, >=, ==, !=, &&, ||, XOR_ALT, ! Порівняння й логічні операції. XOR_ALT — побітовий XOR, а не логічний.
0x106x START_SCRIPT, CALL_SCRIPT, DISCARD_CALL, JUMP, RETURN, JUMP_IF_FALSE, SIGNAL, SET_SIGNAL_MASK Потік керування й паралельність.
0x107x EXPLODE, PLAY_SOUND Спецефекти. PLAY_SOUND є лише в TA: Kingdoms.
0x108x SET_VALUE, ATTACH_UNIT, DROP_UNIT Зміни стану рушія.

Невелика, але показова добірка:

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

Зміщення переходів — це зміщення в байтах відносно початку секції коду скриптів (а не поточної інструкції). JUMP 0x4C означає «перейти до OffsetToScriptCode + 0x4C». Прапорець дизасемблера -a малює стрілки між відповідними інструкціями, щоб це було наочно.


Модель паралельності

Юніт може мати багато скриптів, що виконуються одночасно, — Walk, Aim, SmokeUnit, MoveWatcher тощо. Вони плануються кооперативно:

  • sleep <ticks> віддає керування на N тіків (кожен по 1/30 с).
  • wait-for-turn <piece> around <axis> віддає керування, доки ця частина не завершить поточну анімацію turn. Те саме для wait-for-move.
  • signal <n> знищує кожен скрипт, у якого signal_mask & n != 0.
  • set-signal-mask <n> оголошує, які сигнали цікавлять поточний скрипт.

Маска сигналів — це спосіб, у який скрипти реалізують витіснення: MoveWatcher установлює маску SIG_MOVE і стежить за швидкістю; коли юніт зупиняється, він надсилає SIG_MOVE і чисто знищує анімації ходьби, перш ніж увімкнути позу спокою.

Warning

SET_SIGNAL_MASK лише реєструє зацікавленість — він нічого не знищує. Скрипти з установленою маскою X зупиняє саме надсилання signal X. Правило signal-never-signalled у kbot cob lint виявляє скрипти, що встановлюють маску, якої ніхто ніколи не надсилає (це майже завжди помилка).

Як насправді відбувається витіснення

Поширений шаблон: MoveUnit запускає скрипт анімації Walk; коли рух припиняється, рушій надсилає SIG_MOVE, щоб чисто все прибрати до старту Idle. Послідовність:

   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)

На що варто звернути увагу:

  1. Обидва скрипти, які встановили SIG_MOVE, знищуються, коли спрацьовує сигнал, — зокрема й сам MoveUnit (це не має значення: його робота виконана).
  2. Скрипт, що надсилає сигнал (MoveUnit), живе достатньо довго, щоб надіслати сигнал, перш ніж його буде знищено; середовище виконання перевіряє маски після повернення з опкоду signal.
  3. Idle не встановлює SIG_MOVE, тож наступний signal SIG_MOVE його не зачепить.

Розібраний приклад — 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);
}

Відповідний дизасембльований код (kbot cob disassemble -a -s SmokeUnit) показує справжній потік опкодів зі стрілками JUMP_IF_FALSE/JUMP, що проходять крізь тіла while/if, — це корисно, коли вивід декомпілятора неоднозначний.


Лінтинг

kbot cob lint (і вкладка Lint в оглядачі ресурсів) застосовує типовий набір правил kbot-io: стильові правила, виведені зі справжніх помилок у власних скриптах Cavedog і з надісланих спільнотою латок до модів, дві перевірки на рівні файлу та — для кожного COB, що не оголошує версію 6, — правила сумісності з TA 3.1c. Каталог стильових правил:

Правило Рівень Що виявляє
unused-piece попередження Частину оголошено, але жоден скрипт її не використовує.
unused-static / unused-local попередження Змінну виділено, але ніколи не прочитано.
always-true / dead-code інформація / попередження if(1) / while(0).
long-function / high-complexity попередження > 100 інструкцій або цикломатична складність > 15.
invalid-call помилка call-script до неіснуючої функції.
speed-zero попередження move/turn із speed <0> — анімація ніколи не завершиться.
empty-function інформація Тіло функції складається лише з return 0.
duplicate-animation попередження Дві однакові команди анімації поспіль.
sleep-only-guard інформація Блок if не містить нічого, крім sleep.
duplicate-if інформація Два if поспіль з однаковою умовою.
raw-signal інформація signal 4 замість signal SIG_AIM.
unnamed-global інформація global_3 замість іменованої статичної змінної.
signal-never-signalled попередження Скрипт маскує сигнал, якого ніхто не надсилає.
recursive-call попередження Цикл викликів call-script.
duplicate-function попередження Два скрипти мають однакове ім'я; гра викликає лише перший.
malformed-cob помилка / попередження Файл не завантажується або код скрипту обрізано (помилка); таблиці імен, з якими читач упорався, хоч вони й некоректні (попередження).

Правила сумісності з TA повідомляють про те, на чому TA 3.1c дає збій або що виконує інакше. Це помилки; вони потрапляють у SARIF-вивід --ci, як і решта, і пропускають файли TA: Kingdoms (версії 6):

Правило Що виявляє
ta-kingdoms-opcode PLAY_SOUND, MISSION_COMMAND або оператор TAK_MATH_* у скрипті TA. У TA для них немає обробника, і скрипт дає збій, щойно доходить до такого.
ta-unknown-opcode Слово опкоду, якого не виконує жодна гра.
ta-push-flags PUSH, у якого молодші біти не дорівнюють 1 (константа), 2 (локальна) чи 4 (статична), або POP, у якого молодші біти не 2 і не 4, — зокрема 0x10021000 і 0x10021008 без прапорців, які записували деякі інструменти.
ta-stack-limit Понад 32 STACK_ALLOC або локальні змінні разом з очікуваними значеннями понад 32 комірки стека гри; гра цього не перевіряє й перезаписує пам'ять після скрипту.
ta-get-arguments GET, до якого виконання доходить, коли на стеку менше п'яти значень. GET завжди знімає порт і чотири аргументи, тож get з одним аргументом забирає решту з локальних змінних функції й перезаписує їх.
ta-stack-underflow Будь-яка інша інструкція, що знімає більше значень, ніж є на стеку, або шляхи виконання, що сходяться з різною глибиною стека.
ta-discard-call DISCARD_CALL з більш ніж чотирма аргументами.

Повні описи правил наведено в README kbot. Декомпілятор навмисно зберігає оригінальну нумерацію змінних і частин, щоб цикл декомпіляція→лінтинг→виправлення→компіляція виявляв моди, які потребують супроводу.


Підводні камені

Warning

call-script — це 0x10062000. 0x10063000 — інша інструкція, DISCARD_CALL: вона має ті самі два вбудовані слова (номер скрипту й кількість аргументів), але знімає аргументи зі стека й нічого не запускає. Жоден релізний скрипт її не використовує; декомпілятор kbot записує її як __discard_call(word, args…).

Warning

Кілька опублікованих таблиць опкодів неправильно називають побітові слова. TA виконує 0x10037000 як побітовий XOR (а не остачу від ділення — у TA немає такої інструкції), 0x10038000 як унарне побітове NOT (а не XOR), а 0x10059000 як другий побітовий XOR (а не логічний: 6 XOR_ALT 3 дає 5). 0x1003A000 — оператор TA: Kingdoms, на якому TA дає збій. Лістинги, написані зі старими назвами (MOD, BITWISE_XOR, BITWISE_NOT, LOGICAL_XOR), і далі асемблюються в ті слова, з яких їх було отримано.

  • Усі зміщення в заголовку COB — це абсолютні зміщення у файлі, але елементи ScriptCodeIndexArray — це зміщення у словах у межах секції коду. Їх легко сплутати.
  • Номери частин в опкодах — це індекси в таблиці імен частин, а не рядки. COB зберігає імена як пул рядків; середовище виконання для швидкості використовує індекси.
  • Опкоди get UNIT_VALUE дещо відрізняються між TA і TA: Kingdoms. TA: Kingdoms додала ідентифікатори значень для відновлення магії, очок команд тощо; таблиця значень TA компактніша. Декомпілятор kbot наразі підписує набір TA; номери портів TAK ≥ 21 виводяться як числові літерали (які все одно проходять round-trip через асемблер). Повний перелік відмінностей див. у розділі TA: Kingdoms — відмінності COB v6.
  • Змінні бувають двох видів: local_N (комірка стека, локальна для функції) і global_N (статична змінна юніта, що зберігається між викликами). Декомпілятор за замовчуванням виводить local_N / global_N; для читабельності перейменуйте їх у вихідному коді.
  • Інструмент Cavedog Scriptor іноді вбудовує CURRENT_SPEED як літерал 29, що ламає скрипти на кшталт MoveWatcher після round-trip. Декомпілятор kbot відновлює символьне ім'я; якщо вам доводиться проганяти round-trip через Scriptor, візьміть вираз у дужки: (get CURRENT_SPEED) > 5.

Типові розміри

Ресурс Діапазон, помічений у scripts/*.cob від Cavedog
Розмір файлу 0,5 КБ – 25 КБ
Накладні витрати заголовка завжди 44 байти
Скриптів на юніт 3 – 30 (типові юніти: 5–10)
Частин на юніт 3 – 80 (Krogoth ≈ 80)
Розмір секції коду 300 Б – 20 КБ (дуже залежить від складності)
armcom.cob (командир) ~14 КБ, 30 скриптів
cortruck.cob (простий юніт) ~750 Б, 3 скрипти

TA: Kingdoms — відмінності COB v6

Note

Цей розділ описує лише відмінності TA:K. Усе вище описує COB v4 (TA). TA: Kingdoms зберігає ту саму стекову віртуальну машину, той самий пул рядків, ту саму схему кодування опкодів і той самий механізм імен скриптів та імен частин; відмінності — це розширений заголовок, додаткова таблиця рядків після масиву імен частин і чотири нові опкоди.

Структура файлу 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)

8-байтовий підзаголовок розташований між OffsetToNameArray (кінцем канонічного заголовка з 11 DWORD на байті 44) і початком секції коду (OffsetToScriptCode, зазвичай 0x34 = 52). Він містить два uint32 у little-endian:

Поле Значення
[0] Абсолютне зміщення у файлі таблиці зміщень імен звуків. Завжди дорівнює OffsetToPieceNameOffsetArray + NumPieces × 4, тобто байту одразу за масивом зміщень імен частин. Надлишкове щодо канонічної структури; записувач обчислює його заново.
[1] Кількість імен звуків у таблиці. Визначальне значення кількості.

Кожен елемент таблиці зміщень імен звуків — це абсолютне зміщення у файлі рядка із завершальним NUL у пулі рядків: щось на кшталт "SetMission o 1, s", "create ARASWORD", "ARROW10". Байткод посилається на них за індексом (а не за зміщенням) через опкод MISSION_COMMAND (див. додаток C). Таблицю називають масивом імен звуків, бо канонічний компілятор Scriptor реєструє в ній рядки через дію ADDSOUND; насправді ж там командні рядки рушія, а не імена аудіофайлів.

Представлення в kbot

scripting.COB зберігає імена звуків як структуроване поле []string, без збереження непрозорих байтів. Усе інше (значення підзаголовка, таблиця зміщень, зміщення кожного рядка в пулі) під час запису відновлюється з цього списку:

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

Для файлів v4 цей зріз завжди nil, і ні підзаголовок, ні таблиця зміщень імен звуків у вивід не потрапляють.

Дизасемблер і декомпілятор виводять кожен рядок як директиву .sound_name, щоб рядки пережили round-trip через .coba і .bos:

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

Нові опкоди

Файли .cob з TAK використовують інструкції, для яких у TA 3.1c немає обробника (PLAY_SOUND, MISSION_COMMAND і оператори TAK_MATH_*), а також DONT_SHADOW, який TA виконує як порожню операцію (no-op). Їхня точна семантика в грі не задокументована, але двійкова структура (розмір

  • кількість вбудованих параметрів) відома з кожного релізного .cob TAK, і цього достатньо, щоб round-trip дизасемблювання→асемблювання відновлював оригінальні байти дослівно. Повний довідник див. у додатку C.

Стан round-trip

Конвеєр TA TA: Kingdoms
disassemble → assemble (байт у байт) 278 / 278 ✅ 267 / 267 ✅
decompile → compile (байт у байт) 278 / 278 ✅ Прості скрипти юнітів і багато COB місій (≈ 70 / 267) проходять round-trip байт у байт. Решта невдач — це шаблони потоку керування, для яких структурована реконструкція if/while у декомпіляторі обирає розташування JUMP, яке компілятор генерує трохи інакше: байткод семантично еквівалентний (кожен опкод TAK, командний рядок, підзаголовок і слово версії v6 зберігаються), лише не ідентичний байт у байт. Така сама розбіжність вплинула б на будь-який цикл декомпіляція→компіляція для нетривіального скрипту.

Синтаксис BOS для розширень TAK

Декомпілятор виводить ці псевдовиклики, щоб COB з TAK проходили round-trip через BOS. Компілятор приймає той самий синтаксис, але лише після .version 6 (за винятком dont-shadow, який TA виконує як no-op). dont-shadow і Mission-Command використовують канонічні ключові слова TAK зі Scriptor; математичні опкоди не мають у Scriptor задокументованої назви (він позначає їх ?? і ????), тож kbot залишає заглушки __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

Перший аргумент Mission-Command — це ім'я звуку як рядок у лапках: компілятор шукає його в таблиці імен звуків (тож відповідна директива .sound_name має з'явитися у файлі раніше). Решта аргументів кладуться на стек, і їх споживає рушій.

Решту метаданих v6 несуть дві директиви на початку файлу:

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

.version виводиться завжди (тож файли TA v4 проходять round-trip тим самим конвеєром без змін). Кожен .sound_name — це один елемент таблиці імен звуків конкретного COB; записувач заново будує 8-байтовий підзаголовок і таблицю зміщень імен звуків із len(SoundNames) і порядку, в якому з'являються елементи.

Епілог TAK STACK_ALLOC ; RETURN, який у v6 генерує кожен return; без значення, обробляє компілятор: return; перетворюється на STACK_ALLOC ; RETURN за .version 6, а return <expr>; — на <expr> ; RETURN без префікса (як у компіляторі Cavedog).

Відтворіть це на власній інсталяції:

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

Символьні імена портів після порту 20

Скрипти TA: Kingdoms використовують порти get_unit_value аж до 46 (CURRENT_SPEED, VETERAN_LEVEL, поля запасу магії тощо). Декомпілятор kbot наразі підписує символьними іменами лише порти 1–20 (див. додаток B); порти TAK ≥ 21 виводяться як числові літерали. Байткод у будь-якому разі зберігає їх як цілі числа, тож на round-trip це не впливає — страждає лише читабельність BOS. Додавання імен портів TAK відстежується окремо.


Додаток A — повний довідник опкодів

Кожна інструкція, яку виконує TA або TA: Kingdoms. Джерело істини — formats/scripting/opcodes.go з kbot-io. Запис дії на стек — у стилі Forth: ( before -- after ). Стовпець «Вбуд.» показує, скільки слів uint32 іде одразу за словом самого опкоду (вони зчитуються під час декодування, а не зі стека).

Анімація — керування частинами (0x100x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис 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 його виконує, але її скрипти юнітів нічого з ним не роблять; жоден релізний скрипт його не використовує.
0x1000A000 DONT_SHADOW 1 (piece) ( -- ) dont-shadow <piece>; — TA виконує його як no-op; див. додаток 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>;

Операції очікування (0x101x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис 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>;

Робота зі стеком (0x102x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис BOS
0x10021000 PUSH_IMM 1 (value) — PUSH без прапорців: гра на ньому дає збій (ta-push-flags). Компілятор його ніколи не записує.
0x10021001 PUSH_CONST 1 (value) ( -- v ) <literal> у будь-якому виразі
0x10021002 PUSH_LOCAL 1 (var#) ( -- v ) local_N у будь-якому виразі
0x10021004 PUSH_STATIC 1 (var#) ( -- v ) global_N у будь-якому виразі
0x10022000 STACK_ALLOC 0 ( -- ) (неявно — по одному на кожен var і параметр)
0x10023002 POP_LOCAL 1 (var#) ( v -- ) local_N = expr;
0x10023004 POP_STATIC 1 (var#) ( v -- ) global_N = expr;
0x10024000 POP_STACK 0 ( v -- ) (відкидає результат виразу)

Арифметика (0x103x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис 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 немає інструкції остачі від ділення: % у скрипті TA — помилка компіляції. 0x10039000–0x1003B000 — це оператори TA: Kingdoms TAK_MATH_* (додаток C); TA на них дає збій.

Запити до рушія (0x104x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис 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, …) — завжди знімає порт і чотири аргументи; замість відсутніх компілятор кладе нулі
0x10044000 IS_CARRYING_UNIT 0 ( unit -- bool ) __is_carrying_unit(unit) — 1, коли той юніт їде на цьому
0x10045000 CARRIER_UNIT_ID 0 ( -- id ) __carrier_unit_id() — юніт, який везе цей, або 0

Каталог портів див. у додатку B.

Порівняння й логіка (0x105x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис 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 — побітовий XOR, як і 0x10037000 (6 XOR 3 дає 5), а не логічний
0x1005A000 LOGICAL_NOT 0 ( a -- !a ) NOT a

Потік керування й паралельність (0x106x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис 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…); — знімає аргументи (більше чотирьох переповнюють буфер гри) і нічого не запускає
0x10064000 JUMP 1 (offset) ( -- ) (компілятор — goto, цикли)
0x10065000 RETURN 0 ( v -- ) return v;
0x10066000 JUMP_IF_FALSE 1 (offset) ( v -- ) (компілятор — if/while)
0x10067000 SIGNAL 0 ( mask -- ) signal <mask>;
0x10068000 SET_SIGNAL_MASK 0 ( mask -- ) set-signal-mask <mask>;

offset для JUMP / JUMP_IF_FALSE — це зміщення в байтах відносно OffsetToScriptCode, а не відносно поточної інструкції.

Ефекти (0x107x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис BOS
0x10071000 EXPLODE 1 (piece) ( type -- ) explode <piece> type <type>;
0x10072000 PLAY_SOUND 1 (vol) ( sound -- result ) play-sound(<sound>, <vol>); — лише TA: Kingdoms (.version 6); TA на ньому дає збій

Операції встановлення (0x108x)

Опкод Мнемоніка Вбуд. Дія на стек Синтаксис 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>;

Кодування опкодів

Кожне слово опкоду має вигляд 0x10CCSSFF, де:

  • CC — байт категорії (анімація, математика, потік керування, …).
  • SS — підкоманда: півбайт у старшій половині другого згори байта.
  • FF — байт прапорців.

Гра диспетчеризує за word & 0x100FF000 й ігнорує решту бітів, тож слово на кшталт 0x10064001 виконується як JUMP; дизасемблер записує його як JUMP@0x10064001, а асемблер читає цей запис назад у те саме слово. PUSH і POP беруть своє джерело або призначення з трьох молодших бітів: PUSH приймає 1 (константа), 2 (локальна) і 4 (статична), POP — 2 і 4, а будь-яке інше значення спричиняє збій скрипту.

Слова, яких не виконує жодна гра, дизасемблер показує як UNKNOWN_0x10xxxxxx (і про них повідомляє ta-unknown-opcode); якщо ви знайдете таке в справжньому файлі, будь ласка, відкрийте issue із зразком COB.


Додаток B — таблиця портів get_unit_value

20 портів, які декомпілятор kbot наразі підписує символьними іменами. Справжня TA має ~80; TA: Kingdoms додає ще ~15 для величин, пов'язаних із магією. Усе вище порту 20 у виводі декомпілятора сьогодні подається як числовий літерал.

Порт Символьне ім'я Повертає Примітки
1 ACTIVATION bool Чи ввімкнено перемикач «активний» юніта (радар, глушилка, укріплення)?
2 STANDINGMOVEORDERS enum 0 = утримувати позицію, 1 = маневрувати, 2 = вільно пересуватися.
3 STANDINGFIREORDERS enum 0 = не стріляти, 1 = відповідати на вогонь, 2 = вогонь на власний розсуд.
4 HEALTH 0–100 Залишок MaxDamage у відсотках.
5 INBUILDSTANCE bool Чи висуває юніт зараз нанотокарні (nano-lathes)?
6 BUSY bool Чи зайнятий юніт зараз виконанням наказу?
7 PIECE_XZ упаковане Об'єднані X/Z частини (закодовані). Використовується для наведення снарядів.
8 PIECE_Y int Координата Y частини відносно початку координат юніта.
9 UNIT_XZ упаковане Об'єднані X/Z юніта.
10 UNIT_Y int Координата Y юніта.
11 UNIT_HEIGHT int Висота над рельєфом.
12 XZ_ATAN int atan2 між двома значеннями, закодованими як XZ (для наведення турелей).
13 XZ_HYPOT int Гіпотенуза між двома значеннями, закодованими як XZ.
14 ATAN int atan2(y, x) двох покладених на стек значень.
15 HYPOT int hypot(x, y).
16 GROUND_HEIGHT int Висота рельєфу в позиції юніта.
17 BUILD_PERCENT_LEFT 0–100 Використовується в SmokeUnit, щоб знати хід будівництва. 0 після завершення.
18 YARD_OPEN bool Для будівель із yard map: чи відчинені двері?
19 BUGGER_OFF bool Рушій установлює його, коли юніту треба відійти з дороги.
20 ARMORED bool Чи ввімкнено броньований режим юніта (Bulldog, Goliath тощо)?

До вищих портів належать CURRENT_SPEED (29 — Cavedog Scriptor горезвісно вбудовує його як літерал; див. «Підводні камені» вище), MAX_ID, UNIT_TEAM, UNIT_BUILD_PERCENT, BUILDER, UNIT_ALLIED і багато інших. Повний список міститься у внутрішній документації Cavedog; найповніший публічний каталог — у вікі спільноти на форумах TAUniverse.

TA: Kingdoms розширює діапазон портів. Порти, наявні лише в TAK, які розпізнає kbot (декомпілятор виводить символьне ім'я, компілятор приймає його на вході), узято з таблиці [UNITVLAUES] у Scriptor:

Порт Символ Примітки
21 WEAPON_AIM_ABORTED Установлюється, коли розрахунок наведення зброї скасовано.
22 WEAPON_READY Зброя перезарядилася й готова до пострілу.
23 WEAPON_LAUNCH_NOW Рушій зробив постріл зі зброї.
26 FINISHED_DYING Анімацію смерті завершено.
27 ORIENTATION Напрямок, у який повернутий юніт.
28 IN_WATER Юніт у воді.
29 CURRENT_SPEED Поточна швидкість руху (спільна з TA — Scriptor і тут дає йому той самий номер).
31 MAGIC_DEATH Юніт загинув від магічної шкоди.
32 VETERAN_LEVEL Рівень ветеранства.
34 ON_ROAD Юніт на тайлі дороги.

Додаток C — опкоди TA: Kingdoms

Note

Опкоди лише TA:K. Жоден із них не трапляється в релізних файлах .cob TA. DONT_SHADOW і MISSION_COMMAND використовують канонічні мнемоніки зі Scriptor (компілятора/декомпілятора Switeck, що підтримує TAK); математичні операції не мають задокументованої семантики — Scriptor позначає їх ?? і ????, тож kbot залишає заглушки TAK_MATH_09 / TAK_MATH_0A / TAK_MATH_0B. Релізні скрипти TA: Kingdoms використовують кожну з них як бінарний оператор (знімає два значення, кладе одне), коли пакують і розпаковують бітові поля. TA 3.1c не має обробника для PLAY_SOUND, MISSION_COMMAND чи математичних операторів; DONT_SHADOW вона виконує як no-op.

Опкод Мнемоніка Вбуд. Входжень у релізній TAK Примітки
0x1000A000 DONT_SHADOW 1 (piece) 66 Вимикає відкидання тіні для однієї частини. Розташований у категорії анімації поруч із DONT_SHADE (0x1000E000) — раніше kbot називав його TAK_ANIM_0A. Та сама форма на диску, що й в інших операцій ANIM_*: 4-байтовий опкод + один DWORD piece. Ключове слово BOS: dont-shadow <piece>;.
0x10039000 TAK_MATH_09 0 264 Бінарний оператор математичної категорії, загалом 4 байти: ( a b -- r ). BOS: __tak_math_09(a, b).
0x1003A000 TAK_MATH_0A 0 — Та сама форма. Деякі таблиці опкодів називають його BITWISE_NOT; побітове NOT у TA — це 0x10038000. BOS: __tak_math_0a(a, b).
0x1003B000 TAK_MATH_0B 0 66 Та сама форма, що й у TAK_MATH_09. BOS: __tak_math_0b(a, b).
0x10073000 MISSION_COMMAND 2 (soundNameIdx, argCount) 3 885 Виклик команди рушія. Перший вбудований DWORD — індекс у таблиці SoundNames цього COB; другий — кількість значень, які треба зняти зі стека як аргументи. Опкод кладе на стек один результат (зазвичай його відкидає наступний POP_STACK або зберігає POP_STATIC/POP_LOCAL). Безумовно найпоширеніший опкод, наявний лише в TAK. Ключове слово BOS: Mission-Command("name", args…) — відповідає канонічному синтаксису Scriptor.

Кодування (коротко)

Кожне слово опкоду COB має вигляд 0x10CCSSFF, де CC — категорія, а SS — підкоманда; ці опкоди TAK просто займають раніше не використані слоти CC:SS в наявних категоріях, а не вводять новий байт категорії. DONT_SHADOW живе в категорії анімації поряд із DONT_CACHE/DONT_SHADE, а MISSION_COMMAND — у категорії ефектів поряд з EXPLODE і PLAY_SOUND.

Tip

Звіртеся зі справжнім байткодом.

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

Живі приклади в довідковому каталозі

Справжні юніти, що демонструють крайнощі формату, — відкрийте їх, щоб побачити реальну складність скриптів, а не приклади, вигадані для тексту:

  • CORKROG — Krogoth (Core) — один із найбільших COB у грі (~25 КБ, понад 30 скриптів, складні анімації з багатьма частинами).
  • ARMCOM — Arm Commander — типовий «насичений» юніт: цикли ходьби, наведення, спокою й будівництва, кілька видів зброї, обробка D-gun.
  • CORTRUCK — Construction Truck — майже мінімальний COB, розібраний на цій сторінці (3 скрипти: SmokeUnit, Create, Killed).
  • ARMSAM — Samson — добрий приклад юніта з турелью (turret1 повертається для наведення, анімація віддачі, синхронізація через wait-for-turn).

Будь-який із них можна декомпілювати локально командою kbot cob decompile scripts/<name>.cob, щойно задано контекст kbot.


Див. також

  • 3DO — імена частин, на які посилаються скрипти COB, містяться в .3do юніта.
  • FBI — файл метаданих юніта. ObjectName у ньому пов'язує COB з 3DO; Sounds визначає звуковий банк, звуки з якого може відтворювати скрипт.
  • Таблиця правил cob lint у CLI kbot.
  • Глосарій — piece, axis, signal mask, tick.