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)
Обратите внимание на три вещи:
- Когда срабатывает сигнал, завершаются оба скрипта, установившие
SIG_MOVE, включая самMoveUnit(это неважно: свою работу он уже сделал). - Посылающий скрипт (
MoveUnit) живёт достаточно долго, чтобы отправить сигнал, прежде чем будет завершён: среда выполнения проверяет маски после возврата из опкодаsignal. 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), тик.