Files
ordoor/doc/formats/ani.md

19 KiB

Anim/WarHammer.ani

This turns out to simply be an obj file.

The first 1,064 sprites are all of the same Ultramarine, carryng a bolter. There are eight "facing" orientations:

  • North
  • Northeast
  • East
  • Southeast
  • South
  • Southwest
  • West
  • Northwest

For each orientation, an action is pictured in a variable number of frames. The final frame for each action appears to be "stationary".

  • Walk (13 frames)
  • Run (9 frames)
  • Crouch down (8 frames)
  • Stand up (8 frames)
  • Take aim (standing) (6 frames)
  • Fire (standing) (6 frames)
  • Relax aim (standing) (6 frames)
  • Throw grenade (standing) (18 frames)
  • Take aim (crouched) (5 frames)
  • Fire (crouched) (5 frames)
  • Relax aim (crouched) (5 frames)
  • Throw grenade (crouched) (17 frames)
  • Draw melee weapon (standing) (10 frames)
  • Strike down with melee weapon (standing) (8 frames)
  • Stab with melee weapon (standing) (9 frames)

Added together and multiplied by 87, that's 1064.

The next sprite is a walking-north action for an ultramarine with a flamer. The total number of frames for this character is 1120 - 56 additional frames, or 7 per orientation. Could be an extra action, or an extra frame per action.

Also notable is that while the bolter showed muzzle flash in the animation, the flamer only showed a tiny hint of fire. I think the animation for spewing flame is held elsewhere.

I strongly suspect the actions and the number of frames in each action are configurable. So, what other files are implicated in its interpretation? Here's a few possibilities:

  • Data/AniObDef.dat
  • Data/Coordinates.dat
  • Data/HasAction.dat
  • Data/VehicDef.dat
  • Data/WeapDef.dat
  • Idx/WarHammer.idx

Data/AniObDef.dat

Including comments, this is 4098 lines, giving approx. 45 lines for each of the ~188 characters in the ani. That doesn't seem many, and there's no obvious correspondence between the commented-on names (SMOKE01?) and the viewed frames... but then, I've not viewed all the frames.

Data/HasAction.dat

This file seems relevant as it says whether or not particular animations exist for the different types of character, which maps directly to what is stored in the .ani file - and so must affect lookups thereof.

Fortunately, it's commented extensively. For each "Character Type", there are 36 different possible animations.

Here's a table representation of the data:

    Tac  Ass  Dev  Term Apo  Tech Chp  Lib  Cpt  CMar CLrd CChp CSrc CTrm Kbz  BTh  BL   FHnd LoC  Flm  PHr  BHr  Cult
00  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x  
01  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x  
02  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x  
03                                                                                                                   
04                                                                                                                   
05                                                                                                                   
06  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x         x    x    x              x  
07  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x  
08  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x                                       x  
09                                                                                                                   
10                                                                                                                   
11                                                                                                                   
12                                                                                                                   
13                                                                                                                   
14  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
15  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
16  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
17  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
18  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
19  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
20  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
21  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
22  x    x    x         x    x    x    x    x    x    x    x    x         x                                       x  
23  x    x    x         x    x    x    x    x    x    x    x    x         x                                          
24  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x                                     
25  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x       
26  x    x    x         x    x    x    x    x    x    x    x    x         x    x    x    x         x                 
27  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x            
28  x    x    x         x    x    x    x    x    x    x    x    x    x    x                                          
29       x                                                                     x              x                      
30                      x                                                                                            
31                           x                                                                                       
32                                                              x                             x    x                 
33  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x              x  
34  x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x    x  
35                                                                             x    x    x    x    x    x    x       

WarHammer.ani doesn't have blank sprites for the unchecked cells, so this must surely be used to map between set-of-sprites and AnimAction. The names map very well to the descriptions I came up with when observing the sprites.

I think we still need the data in .idx for a full picture, though. Things we still need:

  • Mapping of character type to sprite directory index in WarHammer.ani
  • Number of frames in each AnimAction

Either of these could be hardcoded, or dynamic.

Idx/WarHammer.idx

WarHammer.idx (1,880,078 bytes, binary, so around 10KiB per character, in theory) is more reasonable.

Here's a list of operations on the file when WH40K_TD.EXE is instructed to place a single Librarian:

_llseek(<WarHammer.idx>, 132, [132], SEEK_SET) = 0
read(<WarHammer.idx>, "\x30\x7c\x09\x00\x98\x00\x00\x00\x88\xf8\x00\x00", 12) = 12

_llseek(<WarHammer.idx>, 132, [132], SEEK_SET) = 0
read(<WarHammer.idx>, "\x30\x7c\x09\x00\x98\x00\x00\x00\x88\xf8\x00\x00", 12) = 12

_llseek(<WarHammer.idx>, 621616, [621616], SEEK_SET) = 0
read(<WarHammer.idx>, "\x02\x01\x01\x33\x50\x83\x09\x00\x0d\x00\x00\x00", 12) = 12

_llseek(<WarHammer.idx>, 621628, [621628], SEEK_SET) = 0
read(<WarHammer.idx>, "\x02\x01\x02\x33\xb2\x83\x09\x00\x0d\x00\x00\x00", 12) = 12

_llseek(<WarHammer.idx>, 621640, [621640], SEEK_SET) = 0
read(<WarHammer.idx>, "\x02\x01\x03\x33\x14\x84\x09\x00\x0d\x00\x00\x00", 12) = 12

_llseek(<WarHammer.idx>, 621652, [621652], SEEK_SET) = 0
read(<WarHammer.idx>, "\x02\x01\x04\x33\x76\x84\x09\x00\x0d\x00\x00\x00", 12) = 12

_llseek(<WarHammer.idx>, 621664, [621664], SEEK_SET) = 0
read(<WarHammer.idx>, "\x02\x01\x05\x33\xd8\x84\x09\x00\x0d\x00\x00\x00", 12) = 12

_llseek(<WarHammer.idx>, 623832, [623832], SEEK_SET) = 0
read(<WarHammer.idx>, "\x34\x00\x40\x00\x40\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 20) = 20

_llseek(<WarHammer.idx>, 0, [623852], SEEK_CUR) = 0
_llseek(<WarHammer.idx>, 623852, [623852], SEEK_SET) = 0
_llseek(<WarHammer.idx>, 623930, [623930], SEEK_SET) = 0

_llseek(<WarHammer.ani>, 509440, [509440], SEEK_SET) = 0
read(<WarHammer.ani>, "\x0c\x4d\xb6\x09\x68\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509448, [509448], SEEK_SET) = 0
read(<WarHammer.ani>, "\x74\x5b\xb6\x09\xfe\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509456, [509456], SEEK_SET) = 0
read(<WarHammer.ani>, "\x72\x6a\xb6\x09\x67\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509464, [509464], SEEK_SET) = 0
read(<WarHammer.ani>, "\xd9\x79\xb6\x09\xa9\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509472, [509472], SEEK_SET) = 0
read(<WarHammer.ani>, "\x82\x89\xb6\x09\xbb\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509480, [509480], SEEK_SET) = 0
read(<WarHammer.ani>, "\x3d\x99\xb6\x09\x08\x10\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509488, [509488], SEEK_SET) = 0
read(<WarHammer.ani>, "\x45\xa9\xb6\x09\xd1\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509496, [509496], SEEK_SET) = 0
read(<WarHammer.ani>, "\x16\xb9\xb6\x09\x01\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509504, [509504], SEEK_SET) = 0
read(<WarHammer.ani>, "\x17\xc8\xb6\x09\xc4\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509512, [509512], SEEK_SET) = 0
read(<WarHammer.ani>, "\xdb\xd6\xb6\x09\xe3\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509520, [509520], SEEK_SET) = 0
read(<WarHammer.ani>, "\xbe\xe5\xb6\x09\x0c\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509528, [509528], SEEK_SET) = 0
read(<WarHammer.ani>, "\xca\xf4\xb6\x09\x41\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509536, [509536], SEEK_SET) = 0
read(<WarHammer.ani>, "\x0b\x04\xb7\x09\xa6\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 509440, [509440], SEEK_SET) = 0
read(<WarHammer.ani>, "\x0c\x4d\xb6\x09\x68\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164448540, [164448540], SEEK_SET) = 0
read(<WarHammer.ani>, "\xf7\x00\x0a\x01\x35\x00\x54\x00\x00\x00\x00\x00\x50\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x13\x87\x2a\x2a\x2b\x2a\x2b"..., 3688) = 3688

_llseek(<WarHammer.ani>, 509448, [509448], SEEK_SET) = 0
read(<WarHammer.ani>, "\x74\x5b\xb6\x09\xfe\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164452228, [164452228], SEEK_SET) = 0
read(<WarHammer.ani>, "\xf5\x00\x08\x01\x37\x00\x57\x00\x00\x00\x00\x00\xe6\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x15\x03\x2a\x8a\x2b\x2a\x2c"..., 3838) = 3838

_llseek(<WarHammer.ani>, 509456, [509456], SEEK_SET) = 0
read(<WarHammer.ani>, "\x72\x6a\xb6\x09\x67\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164456066, [164456066], SEEK_SET) = 0
read(<WarHammer.ani>, "\xf4\x00\x08\x01\x39\x00\x5a\x00\x00\x00\x00\x00\x4f\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x18\x81\x2a\x05\x2b\x80\x1b"..., 3943) = 3943

_llseek(<WarHammer.ani>, 509464, [509464], SEEK_SET) = 0
read(<WarHammer.ani>, "\xd9\x79\xb6\x09\xa9\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164460009, [164460009], SEEK_SET) = 0
read(<WarHammer.ani>, "\xee\x00\x07\x01\x42\x00\x5b\x00\x00\x00\x00\x00\x91\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x22\x81\x2a\x80\x1f\x00\x80"..., 4009) = 4009

_llseek(<WarHammer.ani>, 509472, [509472], SEEK_SET) = 0
read(<WarHammer.ani>, "\x82\x89\xb6\x09\xbb\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164464018, [164464018], SEEK_SET) = 0
read(<WarHammer.ani>, "\xee\x00\x0a\x01\x43\x00\x5c\x00\x00\x00\x00\x00\xa3\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x23\x03\x29\x03\x2b\x80\x1a"..., 4027) = 4027

_llseek(<WarHammer.ani>, 509480, [509480], SEEK_SET) = 0
read(<WarHammer.ani>, "\x3d\x99\xb6\x09\x08\x10\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164468045, [164468045], SEEK_SET) = 0
read(<WarHammer.ani>, "\xec\x00\x09\x01\x43\x00\x5a\x00\x00\x00\x00\x00\xf0\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x24\x81\x2a\x80\x1e\x00\x80"..., 4104) = 4104

_llseek(<WarHammer.ani>, 509488, [509488], SEEK_SET) = 0
read(<WarHammer.ani>, "\x45\xa9\xb6\x09\xd1\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164472149, [164472149], SEEK_SET) = 0
read(<WarHammer.ani>, "\xee\x00\x09\x01\x3f\x00\x56\x00\x00\x00\x00\x00\xb9\x0f\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x1d\x8a\x26\x26\x48\x29\x2a"..., 4049) = 4049

_llseek(<WarHammer.ani>, 509496, [509496], SEEK_SET) = 0
read(<WarHammer.ani>, "\x16\xb9\xb6\x09\x01\x0f\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164476198, [164476198], SEEK_SET) = 0
read(<WarHammer.ani>, "\xf6\x00\x08\x01\x35\x00\x5b\x00\x00\x00\x00\x00\xe9\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x10\x81\x2a\x04\x2b\x86\x2a"..., 3841) = 3841

_llseek(<WarHammer.ani>, 509504, [509504], SEEK_SET) = 0
read(<WarHammer.ani>, "\x17\xc8\xb6\x09\xc4\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164480039, [164480039], SEEK_SET) = 0
read(<WarHammer.ani>, "\xf7\x00\x07\x01\x33\x00\x5b\x00\x00\x00\x00\x00\xac\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x1b\x88\x2b\x2c\x2b\x2c\x2c"..., 3780) = 3780

_llseek(<WarHammer.ani>, 509512, [509512], SEEK_SET) = 0
read(<WarHammer.ani>, "\xdb\xd6\xb6\x09\xe3\x0e\x00\x00", 8) = 8

_llseek(<WarHammer.ani>, 164483819, [164483819], SEEK_SET) = 0
read(<WarHammer.ani>, "\xf8\x00\x07\x01\x41\x00\x5f\x00\x00\x00\x00\x00\xcb\x0e\x00\x00\xd4\x71\x3b\x01\x00\x00\x00\x00\x80\x1c\x83\x2c\x2c\x2a\x80\x22"..., 3811) = 3811

_llseek(<WarHammer.ani>, 509520, [509520], SEEK_SET) = 0
read(<WarHammer.ani>, "\xbe\xe5\xb6\x09\x0c\x0f\x00\x00", 8) = 8

Notable is that we read from idx before we read from ani - so it does seem like the former should tell us where to pull from the latter.

So what are we doing here? What did we read? Here's what I get:

Type 1 record

From 0x84:

# xxd -s 0x84 -c 12 -e -l 12 -u orig/Idx/WarHammer.idx 
00000084: 00097C30 00000098 0000F888  0|..........

The first read contains 0x097C30. The second (+5) read, at 0x097C30, contains 0x0984D8. We then read 20, followed by 78, bytes, and go on to read from the .ani file.

The whole start of the file looks like a directory of the same kind of records (call them type 1). The record at offset 0 is empty, as are the last few, but the rest have always-increasing offsets in the first and third position. The first appears to be for a "tactical marine", or at least, it is read (similarly to the librarian) when placing a "tactical squad". That has an offset of 0x1800` in the first position, which gives us space for 512 of these 12-byte records. We can say they look like:

Is there anything in here that can link us to what we're reading from the .ani file? From it, we read 14 entries from the sprite directory, starting at byte offset 0x07C600 and direntry offset 63676 (0xF8BC). We then load 10 sprites. The first is at byte offset 0x9CD491C, and is 3688 bytes.

Looking at that sprite in the object viewer, it is the librarian \o/ - facing south \o/. However, it's not the sprite we see in WH40K_TD.exe. That one is, I think, number 63688 (0xF8C8) - 12 sprites on. Nothing matches these numbers.

However, the first librarian sprite is at index 63624 (0xF888), which matches the value at offset 8. This, then, must be the link.

If the first sprite is 0, the displayed sprite is 64 (0x40)...

We still need to know how to go from "librarian" to "index 11", though. The CTYPE_LIBRARIAN value in HasAction.ani gives librarians an 8...

Offset Size Meaning
0 4 Offset of type 2 records
4 4 Number of type 2 records
8 4 First sprite in WarHammer.ani for this record

Type 2 record(s)

From 0x097C30:

# xxd -s 0x00097C30 -g 1 -c 12 -l 60 -u orig/Idx/WarHammer.idx 
00097c30: 02 01 01 33 50 83 09 00 0D 00 00 00  ...3P.......
00097c3c: 02 01 02 33 B2 83 09 00 0D 00 00 00  ...3........
00097c48: 02 01 03 33 14 84 09 00 0D 00 00 00  ...3........
00097c54: 02 01 04 33 76 84 09 00 0D 00 00 00  ...3v.......
00097c60: 02 01 05 33 D8 84 09 00 0D 00 00 00  ...3........

Next, we read 5x 12-byte records - 60 bytes total - from that offset in the type 1 record. The address of the next read is embedded in the fifth, which is where the reads of type 2 records stop - so we were searching for it.

In the first 12-byte record, we have a close offset: 0x098350. So we have 1,824 bytes available in this block of type 2 records - enough for 152 of them, which is the number specified in the second position of the type 1 header.

What is the significance of the fifth 12-byte read? Why do we move onto type 3 records when we reach it? When we place the librarian, he is facing south, and that facing is the fifth one in the listing (N, NE, E, SE, S). It's all I can come up with.

Perhaps this is the fifth facing of the first action? Looking ahead in the file, we can see that the third byte counts from 1 to 8 and falls again, so this is a tempting idea.

If so, since we know the librarian has 23 actions, we'd expect room for 23 * 8 type 2 records in this block. That would need 2208 bytes, and we only have 1824

  • enough for 19 animations, which is quite close.

Looking at the librarian in the ani file, we see they have 1055 sprites in total, but I haven't counted the actions yet.

Offset Size Meaning
0 2? ActionID? Static per each group of 8 type-2 records?
2 1? Counts up from 01 to 08 in each group of 8 type-2 records?
3 1? Is 0x33 for all but the last 4 groups of 8 type-2 records?
4 4 Position of type 3 record
8 4? ??? - small values though. Count of frames?

Type 3 record

From 0x0984D8:

# xxd -s 0x984D8 -g 1 -c 12 -l 20 -u orig/Idx/WarHammer.idx 
000984d8: 34 00 40 00 40 00 01 00 00 00 00 00  4.@.@.......
000984e4: 00 00 00 00 00 00 00 00

# xxd -s 0x984EC -g 1 -c 12 -l 78 -u orig/Idx/WarHammer.idx
000984ec: 00 00 06 00 04 00 00 00 06 00 04 00  ............
000984f8: 00 00 06 00 04 00 00 00 05 00 04 00  ............
00098504: 00 00 05 00 04 00 00 00 03 00 04 00  ............
00098510: 00 00 04 00 FC FF 00 00 05 00 FC FF  ............
0009851c: 00 00 06 00 FC FF 00 00 05 00 FC FF  ............
00098528: 00 00 06 00 FC FF 00 00 06 00 FC FF  ............
00098534: 00 00 00 00 00 00    

Here, in the first read, we see 34 00 and 40 00. These are the relative offsets of the frames we load.

Offset Size Meaning
0 2 First sprite in animation (relative offset)
2 2 Last sprite in animation (relative offset)?
4 2? Could also be last sprite in animation?
6 2? ???
8 12? ??? - unset in this case

The remaining 78-byte chunk is impenetrable so far, but we should now have the information we need to display all the animated sequences in WarHammer.ani!

How do we know it needs to be 78 bytes? One option is multiplying the final field of the type 2 record by 6. Maybe we have 6 bytes of description per frame, or maybe it's unrelated to frames?