Files
ordoor/doc/formats/ani.md

403 lines
19 KiB
Markdown
Raw Normal View History

# `Anim/WarHammer.ani`
2020-04-15 22:18:53 +01:00
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:
2020-04-15 22:18:53 +01:00
* `Data/AniObDef.dat`
* `Data/Coordinates.dat`
* `Data/HasAction.dat`
* `Data/VehicDef.dat`
* `Data/WeapDef.dat`
- `Idx/WarHammer.idx`
2020-04-15 22:18:53 +01:00
## `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.
2020-04-16 01:48:44 +01:00
## `Data/HasAction.dat`
2020-04-15 22:18:53 +01:00
2020-04-16 01:48:44 +01:00
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.
2020-04-16 01:48:44 +01:00
Fortunately, it's commented extensively. For each "Character Type", there are
36 different possible animations.
2020-04-16 01:48:44 +01:00
Here's a table representation of the data:
```
2020-04-16 01:48:44 +01:00
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
```
2020-04-16 01:48:44 +01:00
`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.
2020-04-16 01:48:44 +01:00
I think we still need the data in `.idx` for a full picture, though. Things we
still need:
2020-04-16 01:48:44 +01:00
* Mapping of character type to sprite directory index in `WarHammer.ani`
* Number of frames in each AnimAction
2020-04-16 01:48:44 +01:00
Either of these could be hardcoded, or dynamic.
2020-04-16 01:48:44 +01:00
## `Idx/WarHammer.idx`
2020-04-16 01:48:44 +01:00
`WarHammer.idx` (1,880,078 bytes, binary, so around 10KiB per character, in
theory) is more reasonable.
2020-04-16 01:48:44 +01:00
Here's a list of operations on the file when `WH40K_TD.EXE` is instructed to
place a single Librarian:
<details>
```
2020-04-15 21:11:01 +01:00
_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
2020-04-15 21:11:01 +01:00
_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
```
</details>
2020-04-15 22:18:53 +01:00
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.
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
So what are we doing here? What did we read? Here's what I get:
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
### Type 1 record
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
From `0x84`:
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
```
# xxd -s 0x84 -c 12 -e -l 12 -u orig/Idx/WarHammer.idx
00000084: 00097C30 00000098 0000F888 0|..........
```
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
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...
2020-04-16 01:48:44 +01:00
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 4 | Offset of type 2 records |
| 4 | 4 | Number of type 2 records |
2020-04-16 01:48:44 +01:00
| 8 | 4 | First sprite in `WarHammer.ani` for this record |
### Type 2 record(s)
From `0x097C30`:
2020-04-15 21:11:01 +01:00
```
2020-04-16 01:48:44 +01:00
# 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........
2020-04-15 21:11:01 +01:00
```
2020-04-16 01:48:44 +01:00
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.
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
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.
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
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.
2020-04-15 21:11:01 +01:00
2020-04-16 01:48:44 +01:00
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? |
2020-04-16 01:48:44 +01:00
| 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 |
2020-04-16 01:48:44 +01:00
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?