# `Anim/WarHammer.ani` This turns out to simply be an [`obj`](obj.md) 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(, 132, [132], SEEK_SET) = 0 read(, "\x30\x7c\x09\x00\x98\x00\x00\x00\x88\xf8\x00\x00", 12) = 12 _llseek(, 132, [132], SEEK_SET) = 0 read(, "\x30\x7c\x09\x00\x98\x00\x00\x00\x88\xf8\x00\x00", 12) = 12 _llseek(, 621616, [621616], SEEK_SET) = 0 read(, "\x02\x01\x01\x33\x50\x83\x09\x00\x0d\x00\x00\x00", 12) = 12 _llseek(, 621628, [621628], SEEK_SET) = 0 read(, "\x02\x01\x02\x33\xb2\x83\x09\x00\x0d\x00\x00\x00", 12) = 12 _llseek(, 621640, [621640], SEEK_SET) = 0 read(, "\x02\x01\x03\x33\x14\x84\x09\x00\x0d\x00\x00\x00", 12) = 12 _llseek(, 621652, [621652], SEEK_SET) = 0 read(, "\x02\x01\x04\x33\x76\x84\x09\x00\x0d\x00\x00\x00", 12) = 12 _llseek(, 621664, [621664], SEEK_SET) = 0 read(, "\x02\x01\x05\x33\xd8\x84\x09\x00\x0d\x00\x00\x00", 12) = 12 _llseek(, 623832, [623832], SEEK_SET) = 0 read(, "\x34\x00\x40\x00\x40\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 20) = 20 _llseek(, 0, [623852], SEEK_CUR) = 0 _llseek(, 623852, [623852], SEEK_SET) = 0 _llseek(, 623930, [623930], SEEK_SET) = 0 _llseek(, 509440, [509440], SEEK_SET) = 0 read(, "\x0c\x4d\xb6\x09\x68\x0e\x00\x00", 8) = 8 _llseek(, 509448, [509448], SEEK_SET) = 0 read(, "\x74\x5b\xb6\x09\xfe\x0e\x00\x00", 8) = 8 _llseek(, 509456, [509456], SEEK_SET) = 0 read(, "\x72\x6a\xb6\x09\x67\x0f\x00\x00", 8) = 8 _llseek(, 509464, [509464], SEEK_SET) = 0 read(, "\xd9\x79\xb6\x09\xa9\x0f\x00\x00", 8) = 8 _llseek(, 509472, [509472], SEEK_SET) = 0 read(, "\x82\x89\xb6\x09\xbb\x0f\x00\x00", 8) = 8 _llseek(, 509480, [509480], SEEK_SET) = 0 read(, "\x3d\x99\xb6\x09\x08\x10\x00\x00", 8) = 8 _llseek(, 509488, [509488], SEEK_SET) = 0 read(, "\x45\xa9\xb6\x09\xd1\x0f\x00\x00", 8) = 8 _llseek(, 509496, [509496], SEEK_SET) = 0 read(, "\x16\xb9\xb6\x09\x01\x0f\x00\x00", 8) = 8 _llseek(, 509504, [509504], SEEK_SET) = 0 read(, "\x17\xc8\xb6\x09\xc4\x0e\x00\x00", 8) = 8 _llseek(, 509512, [509512], SEEK_SET) = 0 read(, "\xdb\xd6\xb6\x09\xe3\x0e\x00\x00", 8) = 8 _llseek(, 509520, [509520], SEEK_SET) = 0 read(, "\xbe\xe5\xb6\x09\x0c\x0f\x00\x00", 8) = 8 _llseek(, 509528, [509528], SEEK_SET) = 0 read(, "\xca\xf4\xb6\x09\x41\x0f\x00\x00", 8) = 8 _llseek(, 509536, [509536], SEEK_SET) = 0 read(, "\x0b\x04\xb7\x09\xa6\x0f\x00\x00", 8) = 8 _llseek(, 509440, [509440], SEEK_SET) = 0 read(, "\x0c\x4d\xb6\x09\x68\x0e\x00\x00", 8) = 8 _llseek(, 164448540, [164448540], SEEK_SET) = 0 read(, "\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(, 509448, [509448], SEEK_SET) = 0 read(, "\x74\x5b\xb6\x09\xfe\x0e\x00\x00", 8) = 8 _llseek(, 164452228, [164452228], SEEK_SET) = 0 read(, "\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(, 509456, [509456], SEEK_SET) = 0 read(, "\x72\x6a\xb6\x09\x67\x0f\x00\x00", 8) = 8 _llseek(, 164456066, [164456066], SEEK_SET) = 0 read(, "\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(, 509464, [509464], SEEK_SET) = 0 read(, "\xd9\x79\xb6\x09\xa9\x0f\x00\x00", 8) = 8 _llseek(, 164460009, [164460009], SEEK_SET) = 0 read(, "\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(, 509472, [509472], SEEK_SET) = 0 read(, "\x82\x89\xb6\x09\xbb\x0f\x00\x00", 8) = 8 _llseek(, 164464018, [164464018], SEEK_SET) = 0 read(, "\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(, 509480, [509480], SEEK_SET) = 0 read(, "\x3d\x99\xb6\x09\x08\x10\x00\x00", 8) = 8 _llseek(, 164468045, [164468045], SEEK_SET) = 0 read(, "\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(, 509488, [509488], SEEK_SET) = 0 read(, "\x45\xa9\xb6\x09\xd1\x0f\x00\x00", 8) = 8 _llseek(, 164472149, [164472149], SEEK_SET) = 0 read(, "\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(, 509496, [509496], SEEK_SET) = 0 read(, "\x16\xb9\xb6\x09\x01\x0f\x00\x00", 8) = 8 _llseek(, 164476198, [164476198], SEEK_SET) = 0 read(, "\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(, 509504, [509504], SEEK_SET) = 0 read(, "\x17\xc8\xb6\x09\xc4\x0e\x00\x00", 8) = 8 _llseek(, 164480039, [164480039], SEEK_SET) = 0 read(, "\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(, 509512, [509512], SEEK_SET) = 0 read(, "\xdb\xd6\xb6\x09\xe3\x0e\x00\x00", 8) = 8 _llseek(, 164483819, [164483819], SEEK_SET) = 0 read(, "\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(, 509520, [509520], SEEK_SET) = 0 read(, "\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?