Все страницы форматов ↓
Юниты, скрипты, конфигурации

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

Источник: coreprime/kbot @ 0ab97bf · Редактировать на GitHub

Каждый юнит в Total Annihilation выполняет скрипт, который анимирует его детали (pieces), выпускает вспышки выстрелов, проигрывает звуковые эффекты и реагирует на игровые события. Исходный язык называется BOS («Basic Object Script»), а скомпилированный байткод, который на самом деле выполняет движок, — COB («Cobble», каламбур Cavedog: «скомпилированный 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/

Все флаги (и полная таблица правил линтера) — в справке по 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 — Си-подобный, структурный Пишется вручную; 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, а семейство, операция и флаги у каждого опкода свои. Игра не сравнивает слово целиком: она выбирает обработчик по 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)

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 добавила ID значений для восстановления магии, очков командования и т. д., а таблица значений TA компактнее. Декомпилятор kbot сейчас подписывает набор TA, а номера портов TAK ≥ 21 выводит числовыми литералами (которые всё равно проходят round-trip через ассемблер). Все отличия — в разделе TA: Kingdoms — отличия COB v6.
  • Переменные бывают двух видов: local_N (ячейка стека, локальная для функции) и global_N (статическая переменная юнита, которая сохраняется между вызовами). По умолчанию декомпилятор выводит local_N / global_N; для читаемости переименуйте их в исходнике.
  • Инструмент Cavedog Scriptor иногда подставляет вместо CURRENT_SPEED литерал 29, из-за чего после round-trip ломаются скрипты в духе MoveWatcher. Декомпилятор kbot восстанавливает символьное имя; если round-trip через Scriptor неизбежен, ставьте скобки: (get CURRENT_SPEED) > 5.

Типичные размеры

Ресурс Диапазон в scripts/*.cob от Cavedog
Размер файла 0,5 КБ – 25 КБ
Накладные расходы на заголовок всегда 44 байта
Скриптов на юнит 3 – 30 (у типичных юнитов 5–10)
Деталей на юнит 3 – 80 (у Крогота ≈ 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 выполняет его как пустую операцию). 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) и порядку, в котором идут записи.

Эпилог STACK_ALLOC ; RETURN, который в TAK порождает в v6 каждый return; без значения, обрабатывается компилятором: при .version 6 return; превращается в STACK_ALLOC ; RETURN, а 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 ( до -- после ). Столбец Inline показывает, сколько слов uint32 следует за самим словом опкода (они читаются при декодировании, а не снимаются со стека).

Анимация — работа с деталями (0x100x)

Опкод Мнемоника Inline Действие на стек Синтаксис 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 выполняет её как пустую операцию; см. приложение 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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)

Опкод Мнемоника Inline Действие на стек Синтаксис 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 Выдвинуты ли у юнита сейчас наносборщики?
6 BUSY bool Занят ли юнит сейчас выполнением приказа?
7 PIECE_XZ packed Совмещённые X/Z детали (в закодированном виде). Используется для наведения снарядов.
8 PIECE_Y int Координата Y детали относительно начала координат юнита.
9 UNIT_XZ packed Совмещённые 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 она выполняет как пустую операцию.

Опкод Мнемоника Inline Вхождений в оригинальных файлах 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 — Крогот (CORE) — один из самых больших COB в игре (~25 КБ, 30+ скриптов, сложные анимации из множества деталей).
  • ARMCOM — Командир ARM — типичный «насыщенный» юнит: циклы ходьбы, прицеливания, бездействия и строительства, несколько видов оружия, работа с D-пушкой.
  • CORTRUCK — строительная машина — почти минимальный COB, который разобран на этой странице (3 скрипта: SmokeUnit, Create, Killed).
  • ARMSAM — Самсон — хороший пример юнита с турелью (поворот turret1 для наведения, анимация отдачи, синхронизация через wait-for-turn).

Любой из них можно декомпилировать локально командой kbot cob decompile scripts/<name>.cob, когда контекст kbot задан.


См. также

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