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)
На що варто звернути увагу:
- Обидва скрипти, які встановили
SIG_MOVE, знищуються, коли спрацьовує сигнал, — зокрема й самMoveUnit(це не має значення: його робота виконана). - Скрипт, що надсилає сигнал (
MoveUnit), живе достатньо довго, щоб надіслати сигнал, перш ніж його буде знищено; середовище виконання перевіряє маски після повернення з опкодуsignal. 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). Їхня
точна семантика в грі не задокументована, але двійкова структура (розмір
- кількість вбудованих параметрів) відома з кожного релізного
.cobTAK, і цього достатньо, щоб 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.