πŸ›‘οΈ TA Archive

COB Bytecode: Reading TA's Compiled Unit Scripts

Total Annihilation unit animations are written in BOS (a small C-like language) and shipped compiled as .COB bytecode. The .bos sources we have are only the main files β€” the animation libraries they #include from anim3d\<unit>\ were never shipped, because the compiler inlines includes at compile time and the game only needs the bytecode.

That means sequences like the Annihilator’s box-opening animation are invisible in the shipped text files, but fully present in the bytecode. This page documents the container format and the two number encodings needed to read it back, and ends with the recovered Annihilator sequence.

COB container layout

The header is 11 little-endian DWORDs (TA version signature 4; TA: Kingdoms uses 6 plus an extra sub-header and a sound-name table):

#FieldMeaning
0versionSignature4 = TA, 6 = TA:K
1numScriptsnumber of script names
2numPiecesnumber of piece names
3lengthOfScriptscode size in DWORDs
4numberOfStaticVarsglobal variables
5reservedalways 0
6offsetToScriptCodeIndexArrayindex array
7offsetToScriptNameOffsetArrayabsolute byte offsets
8offsetToPieceNameOffsetArrayabsolute byte offsets
9offsetToScriptCodestart of the code section
10offsetToNameArraystart of the trailing string pool

Both name arrays are arrays of DWORD absolute file offsets pointing at NUL-terminated strings β€” so you can read the script and piece names without decoding a single instruction. For ARMANNI.COB that yields 18 scripts and 20 pieces, including the two scripts we cannot see in the text sources:

activatescr, deactivatescr, SmokeUnit, Go, Stop, InitState, RequestState,
Create, Activate, Deactivate, SetMaxReloadTime, RestoreAfterDelay, AimPrimary,
TargetCleared, FirePrimary, QueryPrimary, SweetSpot, Killed

activatescr and deactivatescr are exactly the missing animation libraries β€” Go() and Stop() call them:

Go()   { dont-cache <19 pieces>; call-script activatescr(); spin radar ... ; }
Stop() { active = FALSE; stop-spin radar; turn radar to x-axis <0> ...;
         call-script deactivatescr(); cache <19 pieces>; }

Movement opcodes

Each movement instruction pushes its operands as constants, then emits the opcode followed by two post-data DWORDs (piece index, axis index β€” axis 0=x, 1=y, 2=z):

OpcodeInstructionOperands on stack
0x10001000move (animated)speed, distance
0x10002000turn (animated)speed, angle
0x10003000spinspeed
0x1000B000move-nowdistance
0x1000C000turn-nowangle
0x10011000 / 0x10012000wait-for-turn / wait-for-moveβ€”

Constants are pushed with 0x10021001, immediately followed by the value. Because the pair speed, target always sits directly in front of the opcode, a movement sequence can be extracted with a linear scan β€” no full interpreter required.

The two number formats

Angles use binary angle measurement (BAM), not floats:

raw = degrees * 65536 / 360          degrees = raw * 360 / 65536

Verified: ARMANNI.BOS contains spin radar around x-axis speed <100> and the bytecode holds 18204 β€” and 18204 * 360 / 65536 = 100.000Β° exactly.

Distances are in elmos, scaled by 163840:

raw = elmos * 163840                 3DO units = raw / 5

Verified: ARMARAD.BOS has move post to y-axis [9.1] speed [16.134753], and the bytecode holds 1490944 for the distance β€” 1490944 / 9.1 = 163840 exactly. Since one elmo is 32768 3DO units, a COB distance divides by 5 to reach the same units the model files use.

Note the asymmetry: because turn/move push both operands, turn-now and move-now push only the target β€” a 0 there means “back to the rest pose”.

Recovered: the Annihilator activation sequence

activatescr (ARMANNI.COB, offsets @24–@1052) β€” opening, all pieces animate in parallel, total duration β‰ˆ 0.52 s:

PieceMotionTargetSpeed
case5 / case6move yβˆ’1196032 / βˆ’1204224β‰ˆ14 elmo/s
case1 / case4turn z+89.39Β°β‰ˆ172Β°/s
case2 / case3turn zβˆ’90.00Β°β‰ˆ173Β°/s
plate1move y+1032188β‰ˆ12 elmo/s
armmove y+918192β‰ˆ11 elmo/s
container1 / container2turn z+90.60Β° / βˆ’90.00Β°β‰ˆ174Β°/s
container1 / container2move x+336960 / βˆ’312300
door1turn x / move z+87.57Β° / βˆ’270720
door2turn z / move x+91.82Β° / +262800
plate2move xβˆ’386100
barrelmove z+1590048
energymove z+532800
radarturn x+93.04Β°β‰ˆ179Β°/s

deactivatescr (@1532–@2512) is the exact mirror image: every piece returns to zero, in reverse order. And InitState() (@1124–@1484) is the instant version of the open pose β€” a row of move-now/turn-now statements using the same targets, which is how the engine restores state without animating.

So the box does not merely “fold open”: six walls hinge 90Β° about their bottom edge, both side containers swing and slide, two doors open, the barrel and energy drums extend, and the turret assembly rises β€” while the radar dish, already tilted 93Β°, starts spinning at exactly 100Β°/s.

Why this matters for the archive

Reading bytecode means the archive can display the original animations for units whose source libraries were never shipped, instead of approximations. The same technique applies to every other unit: walk, open, close and activation scripts are all in the .cob files, and the name tables tell you which pieces they drive before you decode anything.

A companion tool counts and locates every file across all installed archives (10,560 files from 296 .hpi/.gp3/.ufo volumes), so a format’s inputs can be found without re-scanning the installation each time.