Battle Isle 2

Animations

ANI/*.LIB

The intro, interludes, briefings and reports: BBHD clips of whole pictures and XOR changes, and the ASCR scripts that play them.

The intro, outro, interludes, mission briefings and mission reports are BBHD clips in ANI/ABCK000, ANI/AFUL000..005 and ANI/APER000, played by ASCR scripts in ANI/ASCR000. A clip stores numbered frames and nothing else. A script is a short text file that places clips on the screen, sets their speed and direction, and adds text, sound and palette fades.

A BBHD clip is a 70-byte header (BbhdHeader), a chain of frame records (BbfrRecord) and a 12-byte trailer (BbftTrailer). Each frame record is either a whole picture, the changes from the previous picture, or an instruction to show the previous picture again. Clips have no timing of their own: the same clip plays at different speeds in different scenes, because the ASCR script that plays it chooses the speed.

One clip, in file order. A block is as wide as its bytes where its label fits. The frame records repeat, one for each of the header's record_count; the stack stands for all of them, split into the groups of the record below. Each record starts where the one before it ends. The two lines show the header's offsets of the first and last record. The trailer repeats both offsets, so a reader can also walk the chain from the end. Dashed blocks hold the same value in every clip.

Clips in the shipped archives

ArchiveClipsSizesContents
ANI/ABCK000.LIB46120x64Backgrounds for the report window (HGD###), up to 111 frames. The archive also holds 24 placeholder entries whose text reads "Das ist nur ein Dummy File!!!" ("this is only a dummy file").
ANI/APER000.LIB4628x16 to 120x64People and props for the report window (MOVI##): talking heads, caller cards, sender logos, weather symbols.
ANI/AFUL000.LIB2360x240The MILOP console opening and closing (KON_AUF, KON_PUSH).
ANI/AFUL001.LIB11320x200, 640x200Full-screen interludes.
ANI/AFUL002.LIB4320x200Full-screen interludes.
ANI/AFUL003.LIB4356x32 to 320x200Intro: the Blue Byte logo (ABB), the Battle Isle 2 logo (B201..B204), the planet scenes (DST1..DST4) and their parts.
ANI/AFUL004.LIB724x32 to 320x396Full-screen scenes, including EXTRO2, and their parts.
ANI/AFUL005.LIB472x40 to 84x22Small vehicles and a globe (PLANE, SHIP, TANK, WORLD), 100 frames each.

ANI/AFUL010..012.LIB are not animations. Each is 48 MiB of filler with no archive table.

Clip Layout

BbhdHeader      70 bytes, at 0
BbfrRecord      frame record 0, at 0x46
BbfrRecord      frame record 1, at 0x46 + length of record 0
...
BbfrRecord      last frame record, at last_record_offset
BbftTrailer     12 bytes
0xff            one pad byte, only when the clip length is odd

The records form a chain in both directions. Each record stores its own length and the length of the record before it, and the header and trailer both point at the first and last record. A reader can therefore walk the clip forwards from the header or backwards from the trailer.

Every offset is relative to the start of the clip. The archive table does not record clip lengths; clip_length gives the length without the pad byte.

70 bytes14 fields● 12 ◐ 2 ○ 0values from ANI/*.LIB, 163 clips
0x00char[4]tag ●Literal BBHD.

Chain

3 fields · 12 bytes
0x04u32lefirst_record_offset ●always 70Offset of the first frame record.
0x08u32lelast_record_offset ●70–1565855118 distinct valuesOffset of the last frame record.
0x0cu32leclip_length ●229–1586047162 distinct valuesClip length in bytes, through the end of the trailer.

Frames

2 fields · 8 bytes
0x10u32lerecord_count ●1–33344 distinct valuesNumber of frame records.
0x14u32leplayback_flags ◐Playback FlagsHow records depend on each other.
313029282726252423222120191817161514131211109876543210
2 named30 unused
bitmasknamemeaning
00x1dependent_framesRecords may be changes to, or repeats of, the previous picture, so they have to be applied in order.
10x2anchoredTogether with bit 0, the game expects anchor records at a fixed spacing of 11. Set alone in shipped clips whose records are all whole pictures or repeats.
2–31unusedNever set in any record.

Picture

5 fields · 10 bytes
0x18u8transparent_index ●0–2558 distinct valuesThe colour drawn as transparent.
0x19u8transparent_index_high ◐0–2557 distinct valuesNo visible effect.
0x1au16lewidth ●24–64021 distinct valuesPicture width in pixels.
0x1cu16leheight ●8–39618 distinct valuesPicture height in pixels.
0x1eu8[4]plane_column_counts ●6–16017 distinct valuesNumber of columns in each of the four VGA planes, ceil(width / 4) each.

Buffers

2 fields · 8 bytes
0x22u32lepacked_buffer_size ●119–25871150 distinct valuesBytes the game reserves for one compressed picture.
0x26u32lepicture_buffer_size ●306–12805035 distinct valuesBytes the game reserves for one decoded picture, handled like packed_buffer_size.

Reserved

1 field · 28 bytes
0x2au8[28]reserved ●always 0xff0xff in every shipped clip.
variable size7 fields● 7 ◐ 0 ○ 0values from ANI/*.LIB, 163 clips, 5546 frame records
0x00char[4]tag ●Literal BBFR.

Frames

4 fields · 16 bytes
0x04u16leframe_type ●0 · 1 · 2 · 3What the payload holds: 0 repeat, 1 whole picture, 2 compressed changes, 3 uncompressed changes.
0x0eu16leframe_number ●Frame Number BitsPicture number, counting from 0.
1514131211109876543210
15 named1 named but never set
bitmasknamemeaning
0–130x3fffnumberPicture number. Ordinary records count up by one.
140x4000anchorThis record is a whole-picture copy of the picture just shown, with the same number. It adds no new picture.
150x8000-never set in this fieldNever set.
0x10u8[12]reserved ●always 0xff0xff in every shipped record.
0x001c..endbytespayload ●Picture data; empty for a repeat.

Chain

2 fields · 8 bytes
0x06u32lerecord_length ●28–260793042 distinct valuesLength of this record including its 28-byte header.
0x0au32leprevious_length ●0–260792990 distinct valuesrecord_length of the record before this one; 0 for the first record.
12 bytes3 fields● 3 ◐ 0 ○ 0values from ANI/*.LIB, 163 clips
0x00char[4]tag ●Literal BBFT.

Chain

2 fields · 8 bytes
0x04u32lefirst_record_offset ●always 70Same as the header field: 0x46 in every shipped clip.
0x08u32lelast_record_offset ●70–1565855118 distinct valuesSame as the header field.
variable size3 fields● 3 ◐ 0 ○ 0
0x00char[4]tag ●Literal TPWM.

Header

2 fields · 4 bytes
0x04u32leunpacked_length ●Number of bytes after decompression.
0x0008..endbytesstream ●Compressed bytes.

Reference: frame types

TypeNamePayloadRecords
0repeatNone. Show the previous picture again.267
1whole pictureA TPWM-compressed picture, width * height palette indexes, row by row.2,335
2changesA TPWM-compressed change stream applied to the previous picture.2,904
3changes, uncompressedThe same change stream, stored without compression.40

A clip always starts with a whole picture. In clips with flags 3, an anchor (a whole picture with bit 14 set in frame_number) usually follows every 11 records, the first one at record 11. A few clips place the first anchor at record 2 or 4, or space later anchors more closely.

In forward play the game passes over anchors. Its frame counter counts records, but it skips an anchor's number, so the step that would land on an anchor goes straight to the next record and takes no longer than any other step. A decoder should therefore show one picture per record that is not an anchor; showing the anchors too holds every eleventh picture for an extra step. In all 63 clips with flags 3, the anchors are exactly the records that repeat the previous picture without being repeat records.

Some whole pictures decompress to more than width * height bytes. The extra bytes (20, 100 or 18 of them) are left over from the tool that made the files: zeros, or fragments of its memory such as the text SWEN. Readers should use the first width * height bytes and ignore the rest.

Reference: change stream

Types 2 and 3 store the difference from the previous picture. The stream is ordered the way VGA "Mode X" memory is laid out: by plane, then by column, then top to bottom. Each changed value is combined with the old pixel by exclusive-or (XOR), so applying the same change twice restores the original.

for plane in 0..3:
  for x in plane, plane + 4, plane + 8, ... below width:
    loop:
      y_start = next byte            # 1-based row; 0 ends this column
      if y_start == 0: break
      count = next byte as signed
      y = y_start - 1
      if count > 0:                  # count different values
        repeat count times: pixel[x, y] ^= next byte; y += 1
      if count < 0:                  # one value, -count times
        value = next byte
        repeat -count times: pixel[x, y] ^= value; y += 1

Columns with no changes are a single 0 byte. After the last column, 2,575 of the 2,904 compressed change streams carry 20 more bytes, 00 01 02 ... 13. They are not part of the stream and are ignored.

Reference: TPWM compression

The stream is a sequence of groups. Each group starts with a flag byte whose bits, highest first, describe the next eight items. A 0 bit is one literal byte, copied to the output. A 1 bit is a two-byte copy instruction a b:

count    = (a & 0x0f) + 3              # 3..18 bytes
distance = b + (a >> 4) * 256          # 1..4095 bytes back
copy count bytes, one at a time, from output[length - distance]

The copy may overlap the bytes it is producing, which repeats a short pattern. Decompression stops when unpacked_length bytes have been written.

Reference: scene scripts

ANI/ASCR000.LIB holds 256 scripts, ASCR000 to ASCR255. They are plain DOS text, one command per line; there is no compiled form on the disc. 125 of them contain no commands. A script runs on a clock of 25 ticks per second (40 ms per tick), counted from the start of the scene.

initpal (15,  0,  1)
rem  EFF,LNA,LNR,ENR,PMO,FRQ,BGN,DUR,END,PRI,STX,STY,DTX,DTY,WIL,WIT,WIW,WIH)
play ( 1, 32,  0, 43,  0,  8,  0, 10,  0,  0,  0,  0,  0,  0,  0,  0,120, 64)
play ( 3, 31,  0, 69,  0, 12, 10,290,  2,  0,  0,  0,  0,  0,  0,  0,120, 64)
End  ( 300 )

The rem lines are the authors' own comments; the argument names on this page come from them. Some lines carry stray text after the closing bracket, such as gto; it has no known effect.

CommandUsesDoes
play832Shows a clip as a layer.
fx700Plays a sound effect from ENG/AFXS000.
write314Shows text from ENG/ATXT000 in a box.
initpal131Chooses the starting palette.
End131Sets the scene length in ticks.
BLEND89Fades part of the palette from one palette to another.
clear42Clears a rectangle of the screen.
fxstop24Stops sound effects at a tick.
fmstart10Schedules an FM/audio request; the exact resource and audible result are not bound here.
remComment. remplay and remwrite are commented-out commands.

The scripts mix upper and lower case (End, BLEND). The screen they draw on is 360x240 pixels.

play arguments

ArgumentMeaning
EFFHow the layer is drawn over what is below it. Any value but 0 makes the clip's transparent_index see-through, for clips whose records stand alone. Its other effects are not fully understood.
LNAArchive family: 31 backgrounds (ABCK), 32 people (APER), 36 full-screen clips (AFUL).
LNRArchive number within the family, for example 3 for AFUL003.
ENRWhich clip, counting from 0 in the archive's sorted entry list. It is not the number in the entry's name: names have gaps.
PMOPlayback mode; see the next table.
FRQSpeed in pictures per second. FRQ=15 advances one record every 1.67 ticks.
BGNTick at which the layer appears.
DURNumber of ticks it stays.
ENDWhat happens when DUR runs out. 0: the layer is no longer drawn. 1: the frame counter goes back to 0 and the layer stays on screen at its final position. 2: the layer stays on screen showing the picture it had reached, and stops advancing. 5 also appears; values 3 to 5 take another path that is not yet understood.
PRIDrawing order.
STX, STYLayer position inside its window.
DTX, DTYMovement per picture step, for layers that slide across the screen.
WIL, WIT, WIW, WIHThe window: left, top, width and height. The layer is drawn relative to it and cut off at its edges.
PMOPlaysUsed
0Forwards once, then holds the last picture.213
1Backwards once, then holds the first picture.1
2Forwards and back again, holding the end picture for one step (0,1,2,3,3,2,1,0).1
3Forwards, repeating from the first picture.425
4Backwards, repeating from the last picture.0
5As 2, and also reverses DTX/DTY at each end.0
6Forwards, then repeats from the second picture. Later laps never show the first picture, nor an anchor that ends the clip.48
7Holds one picture.143

Other commands

write(TNA, TNR, TID, BGN, DUR, PRI, STY, SCO, PMA, PMB, PMC, ALG, BBH, WIL, WIT, WIW, WIH) shows text record TID of ENG/ATXT### (number TNR) in the window WIL..WIH from tick BGN for DUR ticks. STY=100 shows it at once and STY=101 types it out with a cursor. ALG sets the alignment: 0 left, 1 justified, 2 right, 3 centred. Fonts and colours are chosen by control codes inside the text record, not by write arguments.

fx(channel, bank, sound, BGN, volume, repeat) plays effect sound at tick BGN. A negative BGN ties the sound to the typed text instead of a tick; the scripts use -1 for the cursor sound and -2 for the line-return sound.

initpal(selector, archive, entry) and BLEND(selector, fromArchive, fromEntry, selector, toArchive, toEntry, low, high, begin, end) name palettes in ANI/APAL###. A fade changes only the colours low..high, starts from the palette it names (not from what is on screen) and lasts at most 127 ticks. A fade changes colours only; it never creates in-between pictures.

clear(begin, colour, left, top, width, height) clears a rectangle at tick begin. The second argument is 0 in every script and is probably the fill colour. End(ticks) ends the scene.

Still to investigate

Every field marked unknown or partial above, and every bit that is set without a name or named without ever being set.