Decode WarHammer.ani

This commit is contained in:
2020-04-16 01:48:44 +01:00
parent 87c0aae54b
commit beebfda3ba
2 changed files with 190 additions and 216 deletions

View File

@@ -61,6 +61,68 @@ 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`
@@ -68,163 +130,7 @@ viewed frames... but then, I've not viewed all the frames.
theory) is more reasonable.
Here's a list of operations on the file when `WH40K_TD.EXE` is instructed to
place an Ultramarine squad:
<details>
```
# Operation 1
_llseek(34, 12, [12], SEEK_SET) = 0
read(34, "\0\30\0\0X\4\0\0\0\0\0\0", 12) = 12
# Operation 2
_llseek(34, 6144, [6144], SEEK_SET) = 0
read(34, "\2\6\1\0 L\0\0\r\0\0\0", 12) = 12
_llseek(34, 6156, [6156], SEEK_SET) = 0
read(34, "\2\6\2\0\202L\0\0\r\0\0\0", 12) = 12
_llseek(34, 6168, [6168], SEEK_SET) = 0
read(34, "\2\6\3\0\344L\0\0\r\0\0\0", 12) = 12
_llseek(34, 6180, [6180], SEEK_SET) = 0
read(34, "\2\6\4\0FM\0\0\r\0\0\0", 12) = 12
_llseek(34, 6192, [6192], SEEK_SET) = 0
read(34, "\2\6\5\0\250M\0\0\r\0\0\0", 12) = 12
# Operation 3
_llseek(34, 19880, [19880], SEEK_SET) = 0
read(34, "4\0@\0@\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0", 20) = 20
read(34, "\0\0\6\0\4\0\0\0\6\0\4\0\0\0\6\0\4\0\0\0\5\0\4\0\0\0\5\0\4\0\0\0"..., 78) = 78
# Operation 4: repeat 1
# Operation 5: repeat 2
# Operation 6: repeat 3
# Operation 7
_llseek(34, 24, [24], SEEK_SET) = 0
read(34, "0\212\1\0X\4\0\0\210&\0\0", 12) = 12
# Operation 8
_llseek(34, 100912, [100912], SEEK_SET) = 0
read(34, "\2\6\1\0P\276\1\0\r\0\0\0", 12) = 12
_llseek(34, 100924, [100924], SEEK_SET) = 0
read(34, "\2\6\2\0\262\276\1\0\r\0\0\0", 12) = 12
_llseek(34, 100936, [100936], SEEK_SET) = 0
read(34, "\2\6\3\0\24\277\1\0\r\0\0\0", 12) = 12
_llseek(34, 100948, [100948], SEEK_SET) = 0
read(34, "\2\6\4\0v\277\1\0\r\0\0\0", 12) = 12
_llseek(34, 100960, [100960], SEEK_SET) = 0
read(34, "\2\6\5\0\330\277\1\0\r\0\0\0", 12) = 12
# Operation 9
_llseek(34, 114648, [114648], SEEK_SET) = 0
read(34, "4\0@\0@\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0", 20) = 20
# Operation 10
_llseek(34, 114648, [114648], SEEK_SET) = 0
read(34, "4\0@\0@\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0", 20) = 20
read(34, "\0\0\6\0\4\0\0\0\6\0\4\0\0\0\6\0\4\0\0\0\5\0\4\0\0\0\5\0\4\0\0\0"..., 78) = 78
# Operation 11: repeat 7
# Operation 12: repeat 8
# Operation 13: repeat 9
# lots of repeats
```
</details>
So what are we doing here? What did we read? Here's another view of it:
```
# Operation 1: read 12 bytes from 0x0C
0000000C 00 18 00 00 58 04 00 00 00 00 00 00 ....X.......
# Operation 7: read 12 bytes from offset 0x18
00000018 30 8A 01 00 58 04 00 00 88 26 00 00 0...X....&..
# Operation 2: 5 times, read 12 bytes from offset 0x1800
00001800 02 06 01 00 20 4C 00 00 0D 00 00 00 .... L......
0000180C 02 06 02 00 82 4C 00 00 0D 00 00 00 .....L......
00001818 02 06 03 00 E4 4C 00 00 0D 00 00 00 .....L......
00001824 02 06 04 00 46 4D 00 00 0D 00 00 00 ....FM......
00001830 02 06 05 00 A8 4D 00 00 0D 00 00 00 .....M......
# Operation 3a: read 20 bytes from offset 0x4DA8
00004DA8 34 00 40 00 40 00 01 00 00 00 00 00 4.@.@.......
00004DB4 00 00 00 00 00 00 00 00 ........
# Operation 3b: read 78 more bytes
00004DBC 00 00 06 00 04 00 00 00 06 00 04 00
00004DC8 00 00 06 00 04 00 00 00 05 00 04 00
00004DD4 00 00 05 00 04 00 00 00 03 00 04 00
00004DE0 00 00 04 00 FC FF 00 00 05 00 FC FF
00004DEC 00 00 06 00 FC FF 00 00 05 00 FC FF
00004DF8 00 00 06 00 FC FF 00 00 06 00 FC FF
00004E04 00 00 00 00 00 00
# Operation 8: 5 times, read 12 bytes from offset 0x18A30
00018A30 02 06 01 00 50 BE 01 00 0D 00 00 00 ....P.......
00018A3C 02 06 02 00 B2 BE 01 00 0D 00 00 00 ............
00018A48 02 06 03 00 14 BF 01 00 0D 00 00 00 ............
00018A54 02 06 04 00 76 BF 01 00 0D 00 00 00 ....v.......
00018A60 02 06 05 00 D8 BF 01 00 0D 00 00 00 ............
# Operation 9a: read 20 bytes from offset 0x1BFD8
0001BFD8 34 00 40 00 40 00 01 00 00 00 00 00 4.@.@.......
0001BFE4 00 00 00 00 00 00 00 00 ........
# Operation 9b: read 78 more bytes
0001BFEC 00 00 06 00 04 00 00 00 06 00 04 00
0001BFF8 00 00 06 00 04 00 00 00 05 00 04 00
0001C004 00 00 05 00 04 00 00 00 03 00 04 00
0001C010 00 00 04 00 FC FF 00 00 05 00 FC FF
0001C01C 00 00 06 00 FC FF 00 00 05 00 FC FF
0001C028 00 00 06 00 FC FF 00 00 06 00 FC FF
0001C034 00 00 00 00 00 00
```
Operation 1 contains `[18 00]`. Operation 2, at `0x1800`, contains `[A8 4D]` -
and operation 3 starts at `0x4DA8`. The pattern repeats for operations
7 -> 8 -> 9.
Assumimg operations 1 & 7 represent one type of record, while 2 & 8 represent
another, that would give us a 511-entry directory starting at 0xC. We know the
first 4 bytes represent an offset to find the type of record represented by
2 & 8. We don't know about the other 8 bytes.
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 4 | Position of type 2 record(s) |
| 4 | ? | Unknown |
The records represented by operations 2 & 8 both read 5x 12-byte records, but it
looks like they're arranged in blocks of 8. Nothing in type 1 records indicates
how many type 2 records there are.
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 2? | `02 06` - ??? |
| 2 | 2? | `n xx` - ??? `n` counts from 1 to 8 |
| 4 | 4 | Position of type 3 record |
| 8 | 4 | ??? |
The two type 3a and 3b records are identical to each other, it's hard to know
what's what.
None of the unknown numbers, however sliced, seem to be sprite indices for
`Anim/WarHammer.ani`.
Here's a trace of both `idx` and `ani` files when placing a single librarian for
me to dig into in more detail. No other files are touched when doing this.
place a single Librarian:
<details>
@@ -366,69 +272,123 @@ 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.
We still load 5 type 2 records, even though there's just a single librarian, and
8 compass points. Why 5? After looking in `idx`, we load 10 sprites from `ani`,
which is at least a multiple. Do any of the records we load from `idx` specify
sprite directory offsets that match?
So what are we doing here? What did we read? Here's what I get:
## `Data/HasAction.dat`
### Type 1 record
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:
From `0x84`:
```
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
# xxd -s 0x84 -c 12 -e -l 12 -u orig/Idx/WarHammer.idx
00000084: 00097C30 00000098 0000F888 0|..........
```
`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 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.
I think we still need the data in `.idx` for a full picture, though. Things we
still need:
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:
* Mapping of character type to sprite directory index in `WarHammer.ani`
* Number of frames in each AnimAction
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.
Either of these could be hardcoded, or dynamic.
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`)...
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 4 | Offset of type 2 record(s) |
| 4 | 4 | Unknown |
| 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.
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? | 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`!

View File

@@ -19,7 +19,7 @@ module Obj
def self.parse(data)
hdr = new(*data[0..SIZE - 1].unpack("VVVVV"))
pp hdr
# pp hdr
hdr.validate!(data.bytes.size)
hdr
end
@@ -96,7 +96,7 @@ module Obj
DirEntry.parse(rel_data.byteslice(rel_offset, DirEntry::SIZE))
end
pp entries
# pp entries
new(entries)
end
@@ -380,6 +380,18 @@ def correlate(filenames)
pp results
end
def directory(filename, num)
data = File.read(filename).force_encoding("BINARY")
hdr = Obj::Header.parse(data)
dir = Obj::SpriteDir.parse(data[hdr.dir_range])
entry = dir.entries[num]
puts "Sprite directory starts at 0x#{hdr.dir_offset.to_s(16)}"
puts "Directory entry for sprite #{num} is at 0x#{(hdr.dir_offset + (Obj::DirEntry::SIZE * num)).to_s(16)}"
puts "Sprite #{num} is at 0x#{(hdr.data_offset + entry.rel_offset).to_s(16)} and is #{entry.sprite_size} bytes"
end
def sprites(filename)
obj = load_obj(filename)
@@ -514,6 +526,8 @@ def unknown16(filenames)
end
case command = ARGV.shift
when "directory" then
directory(ARGV[0], ARGV[1].to_i)
when "unknown16" then
unknown16(ARGV)
when "sprites" then