所有格式页面 ↓
单位、脚本与配置

COB / BOS——单位脚本字节码

来自 coreprime/kbot @ 0ab97bf · 在 GitHub 上编辑

横扫千军中的每个单位都运行一个脚本,用来驱动其部件(piece)的动画、发出武器闪光、播放音效,并响应游戏事件。源语言是 BOS(“Basic Object Script”);引擎实际运行的编译后字节码是 COB(“Cobble”——Cavedog 对“compiled BOS”的文字游戏)。每个单位一个文件,以该单位 FBI 中的 ObjectName 命名。

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/

所有参数(以及完整的 lint 规则表)见 CLI 的 kbot cob 参考。

在 Go 中调用。 使用 kbot-io 的 formats/scripting:

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 的结构化语言 手写;Switeck 的 Scriptor;kbot cob decompile 可从 COB 往返还原
编译后 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 把操作码(opcode)流与控制流箭头并排显示;kbot cob decompile 则反向操作,还原出的 BOS 几乎总能逐字节一致地往返(kbot cob roundtrip 会在整个游戏目录上验证这一点)。

除非源代码以 .version 6 开头,否则它会被编译为 TA 脚本(COB 版本 4)。对于 TA 3.1c 会错误执行的内容,编译器直接拒绝,而不是写入占位符:横扫千军:王国的构造(play-sound、Mission-Command、__tak_math_*、.sound_name)和 % 在 TA 脚本中都是错误(TA 没有取模指令,而 % 过去被编译成的字 0x10037000 在 TA 中是按位 XOR),未知标识符以及需要超过游戏 32 个栈槽的函数同样是错误。仍能编译但值得一看的情况——同一函数定义了两次(每次调用都绑定到第一个定义,与游戏的名称查找方式一致)——会作为警告打印出来,同时打印的还有 TA 兼容性 lint 规则的任何发现;--strict 会让警告导致命令失败。kbot cob assemble 对手写的汇编清单执行同样的检查。


COB 磁盘布局

Important

两种方言。 本节描述的是 COB v4,即《横扫千军》所用的方言。《横扫千军:王国》使用 COB v6(VersionSignature == 6),它保留下面的全部内容,但把文件头扩展了 8 字节,并在部件名数组之后插入一个额外的命令字符串偏移表。v6 的具体细节见横扫千军:王国——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

规范字段名与 Scriptor 的 HEADER_COB 结构体一致。早期的 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)从最低三位读取它们的来源或目标。kbot-io 的 scripting/opcodes.go 就是操作码目录;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,不是逻辑 XOR。
0x106x START_SCRIPT、CALL_SCRIPT、DISCARD_CALL、JUMP、RETURN、JUMP_IF_FALSE、SIGNAL、SET_SIGNAL_MASK 控制流与并发。
0x107x EXPLODE、PLAY_SOUND 特效。PLAY_SOUND 仅用于横扫千军:王国。
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 个 tick(每个 1/30 秒)。
  • wait-for-turn <piece> around <axis> 让出执行,直到该部件完成当前的 turn 动画。wait-for-move 同理。
  • signal <n> 终止所有满足 signal_mask & n != 0 的脚本。
  • set-signal-mask <n> 声明当前脚本关心哪些信号。

脚本正是靠信号掩码(signal mask)实现抢占:MoveWatcher 设置掩码 SIG_MOVE 并监视速度;当单位停止移动时,它发出 SIG_MOVE,在开始空闲姿态之前干净地终止行走动画。

Warning

SET_SIGNAL_MASK 只是登记关注——它不会终止任何东西。 真正让设置了掩码 X 的脚本停止的,是发送 signal X。kbot cob lint 中的 signal-never-signalled lint 规则会捕获那些设置了却没有任何人发送的掩码的脚本(这几乎总是 bug)。

抢占的实际过程

一种常见模式: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 的循环体与分支体——当反编译器的输出有歧义时,这很有用。


Lint 检查

kbot cob lint(以及资源浏览器的 Lint 标签页)运行 kbot-io 的默认规则集:风格规则取材于 Cavedog 自己脚本中的真实 bug 和社区提交的模组补丁,另有两项文件级检查,并且——对每个没有声明版本 6 的 COB——还会运行 TA 3.1c 兼容性规则。风格规则目录:

规则 严重级别 捕获的问题
unused-piece warning 部件已声明,但没有在任何脚本中使用。
unused-static / unused-local warning 变量已分配,但从未被读取。
always-true / dead-code info / warning if(1) / while(0)。
long-function / high-complexity warning 超过 100 条指令,或圈复杂度超过 15。
invalid-call error 对不存在的函数执行 call-script。
speed-zero warning move/turn 使用 speed <0>——动画永远不会完成。
empty-function info 函数体只有 return 0。
duplicate-animation warning 两条完全相同的动画命令紧挨在一起。
sleep-only-guard info if 块中只有 sleep。
duplicate-if info 连续两个条件相同的 if。
raw-signal info 写了 signal 4 而不是 signal SIG_AIM。
unnamed-global info 写了 global_3 而不是一个有名字的静态变量。
signal-never-signalled warning 脚本屏蔽了一个没有任何人发出的信号。
recursive-call warning call-script 形成环。
duplicate-function warning 两个脚本同名;游戏只会调用第一个。
malformed-cob error / warning 文件无法加载,或某个脚本的代码被截断(error);读取器容忍了的异常名称表(warning)。

TA 兼容性规则报告 TA 3.1c 会出错或执行结果不同的内容。它们都是 error,与其他规则一样出现在 --ci 的 SARIF 输出中,并且跳过横扫千军:王国(版本 6)文件:

规则 捕获的问题
ta-kingdoms-opcode TA 脚本中出现 PLAY_SOUND、MISSION_COMMAND 或 TAK_MATH_* 运算符。TA 没有它们的处理程序,脚本执行到其中之一时就会出错。
ta-unknown-opcode 任何游戏都不会执行的操作码字。
ta-push-flags 低位不是 1(常量)、2(局部变量)或 4(静态变量)的 PUSH,或低位不是 2 或 4 的 POP——包括某些工具写出的不带标志位的 0x10021000 和 0x10021008。
ta-stack-limit 超过 32 个 STACK_ALLOC,或局部变量加上待处理值超出游戏的 32 个栈槽;游戏不做检查,会覆盖脚本之后的内存。
ta-get-arguments 执行到 GET 时待处理值少于五个。GET 总是弹出一个端口和四个参数,因此只带一个参数的 get 会从函数的局部变量中取走其余的值并将其覆盖。
ta-stack-underflow 任何其他弹出值多于待处理值的指令,或以不同栈深度汇合的路径。
ta-discard-call 参数超过四个的 DISCARD_CALL。

完整的规则说明见 kbot README。反编译器有意保留原始的变量和部件编号,这样“反编译→lint→修复→编译”的循环就能凸显需要维护的模组。


坑点

Warning

call-script 是 0x10062000。 0x10063000 是另一条指令 DISCARD_CALL:它带有同样的两个内联字(脚本编号和参数个数),但只弹出参数,什么也不执行。没有任何零售版脚本使用它;kbot 的反编译器把它写成 __discard_call(word, args…)。

Warning

一些已发布的操作码表把按位运算的字命名错了。 TA 把 0x10037000 当作按位 XOR 执行(不是取模——TA 没有取模指令),把 0x10038000 当作一元按位 NOT 执行(不是 XOR),把 0x10059000 当作第二个按位 XOR 执行(不是逻辑 XOR:6 XOR_ALT 3 等于 5)。0x1003A000 是横扫千军:王国的运算符,TA 遇到它会出错。用旧名称(MOD、BITWISE_XOR、BITWISE_NOT、LOGICAL_XOR)写的汇编清单仍会汇编成它们原本对应的字。

  • COB 文件头中的所有偏移量都是文件绝对偏移量,但 ScriptCodeIndexArray 的条目是代码段内以字为单位的偏移量。两者很容易混淆。
  • 操作码中的部件编号是部件名表的索引,而不是字符串。COB 以字符串池的形式携带名称;运行时为了速度使用索引。
  • get UNIT_VALUE 操作码在 TA 与横扫千军:王国之间有细微差别。 横扫千军:王国为魔法回复、指挥点数等增加了值 ID;TA 的值表更紧凑。kbot 的反编译器目前标注的是 TA 的这一组;TAK 中 ≥ 21 的端口号以数字字面量输出(仍可通过汇编器往返)。完整的差异见横扫千军:王国——COB v6 差异。
  • 变量有两种:local_N(函数局部的栈槽)和 global_N(每个单位一份、跨调用保持的静态变量)。反编译器默认输出 local_N / global_N;为了可读性,请在源代码中给它们改名。
  • Cavedog 的 Scriptor 工具偶尔会把 CURRENT_SPEED 内联为字面量 29,导致 MoveWatcher 一类的脚本在往返后出错。kbot 的反编译器会恢复符号名;如果你必须经由 Scriptor 往返,请加上括号:(get CURRENT_SPEED) > 5。

典型大小

项目 在 Cavedog scripts/*.cob 中观察到的范围
文件大小 0.5 KB – 25 KB
文件头开销 固定 44 字节
每个单位的脚本数 3 – 30(典型单位:5–10)
每个单位的部件数 3 – 80(Krogoth ≈ 80)
代码段大小 300 B – 20 KB(随复杂度变化很大)
armcom.cob(指挥官) 约 14 KB,30 个脚本
cortruck.cob(简单单位) 约 750 B,3 个脚本

横扫千军:王国——COB v6 差异

Note

本节只讲 TA:K 的差异。 上面的内容描述的都是 COB v4(TA)。横扫千军:王国保留了同样的栈式虚拟机、同样的字符串池、同样的操作码编码方案,以及同样的脚本名/部件名机制;区别在于扩展的文件头、在部件名数组之后追加的一个额外字符串表,以及四个新操作码。

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:

字段 含义
[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 伪指令,使其能经由 .coba 和 .bos 往返保留下来:

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

新操作码

TAK 的 .cob 文件使用 TA 3.1c 没有处理程序的指令(PLAY_SOUND、MISSION_COMMAND 和 TAK_MATH_* 运算符),另外还有 DONT_SHADOW,TA 把它当作空操作执行。它们在游戏中的确切语义没有文档,但它们的二进制布局(大小 + 内联参数个数)可以从每个零售版 TAK .cob 中得知,这足以让“反汇编→汇编”往返逐字节还原原始数据。完整参考见附录 C。

往返状态

流程 TA 横扫千军:王国
disassemble → assemble(逐字节一致) 278 / 278 ✅ 267 / 267 ✅
decompile → compile(逐字节一致) 278 / 278 ✅ 简单的单位脚本和许多任务 COB(≈ 70 / 267)可以逐字节完美往返。其余失败的是一些控制流模式:反编译器在重建结构化的 if/while 时选择的 JUMP 布局,与编译器重新生成的布局略有不同——字节码语义等价(每个 TAK 操作码、命令字符串、子文件头以及 v6 版本字都得以保留),只是不逐字节一致。对任何非平凡脚本做“反编译→编译”循环,都会受到同类不匹配的影响。

TAK 扩展在 BOS 中的写法

反编译器输出以下伪调用,使 TAK 的 COB 能经由 BOS 往返。编译器接受同样的语法,但只在 .version 6 之后才接受(dont-shadow 除外——TA 把它当作空操作执行)。dont-shadow 和 Mission-Command 使用 Scriptor 规范的 TAK 关键字;数学操作码在 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 总是会输出(这样 v4 的 TA 文件经由同一流程也能完全一致地往返)。每条 .sound_name 是该 COB 的声音名表中的一个条目;写入器根据 len(SoundNames) 和条目出现的顺序重建 8 字节的子文件头和声音名偏移表。

在 v6 中,每个不带值的 return; 都会产生 TAK 的 STACK_ALLOC ; 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 以上的端口符号

横扫千军:王国的脚本使用的 get_unit_value 端口最高到 46(CURRENT_SPEED、VETERAN_LEVEL、魔法池字段等)。kbot 的反编译器目前只为端口 1–20 标注符号名(见附录 B);TAK 中 ≥ 21 的端口以数字字面量输出。字节码无论如何都以整数存储它们,所以往返不受影响——只是 BOS 的可读性会差一些。添加 TAK 端口名的工作另行跟踪。


附录 A——完整操作码参考

TA 或横扫千军:王国执行的每一条指令。以 kbot-io 的 formats/scripting/opcodes.go 为准。栈表示法采用 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 把它当作空操作执行;见附录 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 是横扫千军:王国的 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——与 0x10037000 一样是按位 XOR(6 XOR 3 等于 5),不是逻辑 XOR
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>;

JUMP / JUMP_IF_FALSE 的 offset 是相对于 OffsetToScriptCode 的字节偏移量,而不是相对于当前指令。

特效(0x107x)

操作码 助记符 内联 栈效果 BOS 语法
0x10071000 EXPLODE 1(piece) ( type -- ) explode <piece> type <type>;
0x10072000 PLAY_SOUND 1(vol) ( sound -- result ) play-sound(<sound>, <vol>);——仅用于横扫千军:王国(.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 报告);如果你在真实文件中发现了这样的字,请附上该 COB 样本提交 issue。


附录 B——get_unit_value 端口表

kbot 的反编译器目前标注符号名的 20 个端口。真实的 TA 大约有 80 个;横扫千军:王国又为魔法相关的数值增加了约 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-lathe)?
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 两个 XZ 编码值之间的 atan2(用于瞄准炮塔)。
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 对带 yardmap 的建筑:门是否打开?
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 论坛的社区 wiki 拥有最全面的公开目录。

横扫千军:王国扩展了端口范围。kbot 识别的 TAK 专有端口(反编译器输出符号名;编译器接受其作为输入)取自 Scriptor 的 [UNITVLAUES] 表:

端口 符号 说明
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——横扫千军:王国操作码

Note

TA:K 专有操作码。 这些操作码都不会出现在零售版 TA 的 .cob 文件中。DONT_SHADOW 和 MISSION_COMMAND 使用 Scriptor(Switeck 的支持 TAK 的编译器/反编译器)中的规范助记符;数学操作没有有据可查的语义——Scriptor 把它们标为 ?? 和 ????,所以 kbot 保留 TAK_MATH_09 / TAK_MATH_0A / TAK_MATH_0B 占位符。零售版横扫千军:王国的脚本在打包和解包位字段时,把它们各自用作二元运算符(弹出两个值,压入一个)。TA 3.1c 对 PLAY_SOUND、MISSION_COMMAND 和这些数学运算符都没有处理程序;DONT_SHADOW 则被它当作空操作执行。

操作码 助记符 内联 零售版 TAK 中的出现次数 说明
0x1000A000 DONT_SHADOW 1(piece) 66 为单个部件禁用阴影投射。位于动画类别中,紧挨着 DONT_SHADE(0x1000E000)——kbot 早先把它叫作 TAK_ANIM_0A。磁盘上的形态与其他 ANIM_* 操作相同:4 字节操作码 + 一个 piece DWORD。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;TA 的按位 NOT 是 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 是 COB 的 SoundNames 表的索引;第二个是要从栈中弹出作为参数的值的个数。该操作码把一个结果压回栈中(通常随后用 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 KB,30 多个脚本,复杂的多部件动画)。
  • ARMCOM——Arm 指挥官——有代表性的“丰富”单位:行走/瞄准/空闲/建造循环、多种武器、D-gun 处理。
  • CORTRUCK——建造卡车——近乎最小的 COB,即本页实例解析所用的文件(3 个脚本:SmokeUnit、Create、Killed)。
  • ARMSAM——Samson——炮塔单位的好例子(turret1 旋转瞄准、后坐动画、wait-for-turn 同步)。

设置好 kbot 上下文后,可以用 kbot cob decompile scripts/<name>.cob 在本地反编译其中任何一个。


另见

  • 3DO——COB 脚本引用的部件名存放在单位的 .3do 中。
  • FBI——单位的元数据文件。其中的 ObjectName 把 COB 与 3DO 联系起来;Sounds 定义脚本可以播放的音效库。
  • kbot CLI 的 cob lint 规则表。
  • 术语表——piece、axis、signal mask、tick。