diff --git a/cmd/view-obj/main.go b/cmd/view-obj/main.go
index 5768302..e3260dc 100644
--- a/cmd/view-obj/main.go
+++ b/cmd/view-obj/main.go
@@ -17,6 +17,7 @@ var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
+ sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -62,8 +63,9 @@ func main() {
}
state := state{
- zoom: 6.0,
- origin: image.Point{0, 0},
+ zoom: 6.0,
+ origin: image.Point{0, 0},
+ spriteIdx: *sprIdx,
}
env := &env{
diff --git a/doc/WarHammer.ani.md b/doc/WarHammer.ani.md
deleted file mode 100644
index 6b34f0c..0000000
--- a/doc/WarHammer.ani.md
+++ /dev/null
@@ -1,167 +0,0 @@
-Hypothesis: Idx/WarHammer.idx points objects into bitmap data in Anim/WarHammer.ani
-
-We can use WH40K_TD.exe and investigate reads of .idx followed by reads of .ani
-to test this.
-
-WH40K_TD.exe opens files in this order:
-
-1. Data/USEng.dta
-1. WH40K_TD.exe (?)
-1. Cursor/Cursors.cur
- 1. pread64(fd, 23, 0) = 23
- 1. _llseek(fd, 0, [0], SEEK_CUR) = 0
- 1. _llseek(fd, 0, [0], SEEK_CUR) = 0
- 1. _llseek(fd, 40666, [40666], SEEK_SET) = 0
- 1. _llseek(fd, 0, [0], SEEK_SET) = 0
- 1. close(fd) = 0
- 1. read(fd, "\x26\x00\x00\x00\x20\x00\x00\x00\x30\x01\x00\x00\x50\x01\x00\x00\x8a\x9d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 32) = 32
- 1. (...)
- 1. _llseek(15, 39868, [39868], SEEK_SET) = 0
- 1. read(15, "...", 798) = 798
-1. (some statting of Idx/WarHammer.idx, no reading that I saw)
-1. Anim/WarHammer.ani
- 1. read(15, "...", 32) = 32
-1. (some clones of child procs, I didn't follow them)
-1. Sounds/wh40k.ds
- 1. pread64(31, "...", 23, 0) = 23
- 1. read(31, "...", 417792) = 417792
- 1. read(31, "...", 4096) = 4096
-1. Data/Sounds.dat
- 1. pread64(34, "#**********************", 23, 0) = 23
- 1. ...
-1. Pic/wh40k.pcx
- 1. read(34, "...", 168509) = 168509
-1. Sets/*
- 1. (lots of statting these)
-1. Data/Randchar.dat
- 1. pread64(34, "#**********************", 23, 0) = 23
- 1. read(34, "#***************************************************************"..., 4096) = 4096
- 1. ...
-1. Data/WeapDef.dat
- 1. pread64(35, "#**********************", 23, 0) = 23
- 1. read(35, "#***************************************************************"..., 4096) = 4096
- 1. ...
-1. Data/SpellDef.dat
- 1. pread64(35, "#**********************", 23, 0) = 23
- 1. read(35, "#***************************************************************"..., 4096) = 4096
- 1. ...
-1. Data/AniObDef.dat
- 1. pread64(35, "# ******** ANIMATED OBJ", 23, 0) = 23
- 1. read(35, "# ******** ANIMATED OBJECT DEFINITIONS **************\r\n#\t\t0 : **"..., 4096) = 4096
- 1. ...
-1. Data/VehicDef.dat
- 1. pread64(35, "# ******** VEHICLE DEFI", 23, 0) = 23
- 1. read(35, "# ******** VEHICLE DEFINITIONS **************\r\n#\t\t0 : *** VEHICL"..., 4096) = 4096
- 1. ...
-1. Data/StdWeap.dat
- 1. pread64(35, "# ******** SQUAD STANDA", 23, 0) = 23
- 1. read(35, "# ******** SQUAD STANDARD WEAPONS **************\r\n#\t\t0 : *** SQU"..., 4096) = 4096
- 1. ...
-1. Data/Ultnames.dat
-1. Data/Chanames.dat
-1. Data/keymap.dta
-1. Filters/wh40k.flt
- 1. _llseek(35, 0, [0], SEEK_SET) = 0
- 1. read(35, "\x01\x00\x00\x00", 4) = 4
- 1. _llseek(35, 4, [4], SEEK_SET) = 0
- 1. read(35, "...", 72) = 72
- 1. _llseek(35, 1444, [1444], SEEK_SET) = 0
- 1. read(35, "...", 327680) = 327680
-1. Misc/occlusio.lis
- 1. pread64(35, "62 # Number of Absol", 23, 0) = 23
- 1. read(35, "62 # Number of Absolute Deltas.\r\n # These Deltas are off"..., 4096) = 982
- 1. read(35, "", 3114) = 0
-1. Data/GDestroy.dat
-1. (stat Obj/destroy.obj)
-1. Data/minimap.dat
-1. Misc/occlusio.list
-1. Obj/specials.obj
-1. Obj/Man_Shadow.obj
-1. Sets/map01.set
-1. Data/Defs.dat
-1. [`Assign/jungtil.asn`](docs/formats/obj.md#assign)
-1. [`Obj/jungtil.obj`](docs/formats/obj.md)
-1. (more assign + obj pairs)
-1. Data/Cycle.cyc
-
-Adding a Librarian to the mission builder performs these seeks and reads:
-
-_llseek(15, 509440, [509440], SEEK_SET) = 0
-read(15, "\fM\266\th\16\0\0", 8) = 8
-_llseek(15, 509448, [509448], SEEK_SET) = 0
-read(15, "t[\266\t\376\16\0\0", 8) = 8
-_llseek(15, 509456, [509456], SEEK_SET) = 0
-read(15, "rj\266\tg\17\0\0", 8) = 8
-_llseek(15, 509464, [509464], SEEK_SET) = 0
-read(15, "\331y\266\t\251\17\0\0", 8) = 8
-_llseek(15, 509472, [509472], SEEK_SET) = 0
-read(15, "\202\211\266\t\273\17\0\0", 8) = 8
-_llseek(15, 509480, [509480], SEEK_SET) = 0
-read(15, "=\231\266\t\10\20\0\0", 8) = 8
-_llseek(15, 509488, [509488], SEEK_SET) = 0
-read(15, "E\251\266\t\321\17\0\0", 8) = 8
-_llseek(15, 509496, [509496], SEEK_SET) = 0
-read(15, "\26\271\266\t\1\17\0\0", 8) = 8
-_llseek(15, 509504, [509504], SEEK_SET) = 0
-read(15, "\27\310\266\t\304\16\0\0", 8) = 8
-_llseek(15, 509512, [509512], SEEK_SET) = 0
-read(15, "\333\326\266\t\343\16\0\0", 8) = 8
-_llseek(15, 509520, [509520], SEEK_SET) = 0
-read(15, "\276\345\266\t\f\17\0\0", 8) = 8
-_llseek(15, 509528, [509528], SEEK_SET) = 0
-read(15, "\312\364\266\tA\17\0\0", 8) = 8
-_llseek(15, 509536, [509536], SEEK_SET) = 0
-read(15, "\v\4\267\t\246\17\0\0", 8) = 8
-_llseek(15, 509440, [509440], SEEK_SET) = 0
-read(15, "\fM\266\th\16\0\0", 8) = 8
-_llseek(15, 164448540, [164448540], SEEK_SET) = 0
-read(15, "\367\0\n\0015\0T\0\0\0\0\0P\16\0\0\324q;\1\0\0\0\0\200\23\207**+*+"..., 3688) = 3688
-_llseek(15, 509448, [509448], SEEK_SET) = 0
-read(15, "t[\266\t\376\16\0\0", 8) = 8
-_llseek(15, 164452228, [164452228], SEEK_SET) = 0
-read(15, "\365\0\10\0017\0W\0\0\0\0\0\346\16\0\0\324q;\1\0\0\0\0\200\25\3*\212+*,"..., 3838) = 3838
-_llseek(15, 509456, [509456], SEEK_SET) = 0
-read(15, "rj\266\tg\17\0\0", 8) = 8
-_llseek(15, 164456066, [164456066], SEEK_SET) = 0
-read(15, "\364\0\10\0019\0Z\0\0\0\0\0O\17\0\0\324q;\1\0\0\0\0\200\30\201*\5+\200\33"..., 3943) = 3943
-_llseek(15, 509464, [509464], SEEK_SET) = 0
-read(15, "\331y\266\t\251\17\0\0", 8) = 8
-_llseek(15, 164460009, [164460009], SEEK_SET) = 0
-read(15, "\356\0\7\1B\0[\0\0\0\0\0\221\17\0\0\324q;\1\0\0\0\0\200\"\201*\200\37\0\200"..., 4009) = 4009
-_llseek(15, 509472, [509472], SEEK_SET) = 0
-read(15, "\202\211\266\t\273\17\0\0", 8) = 8
-_llseek(15, 164464018, [164464018], SEEK_SET) = 0
-read(15, "\356\0\n\1C\0\\\0\0\0\0\0\243\17\0\0\324q;\1\0\0\0\0\200#\3)\3+\200\32"..., 4027) = 4027
-_llseek(15, 509480, [509480], SEEK_SET) = 0
-read(15, "=\231\266\t\10\20\0\0", 8) = 8
-_llseek(15, 164468045, [164468045], SEEK_SET) = 0
-read(15, "\354\0\t\1C\0Z\0\0\0\0\0\360\17\0\0\324q;\1\0\0\0\0\200$\201*\200\36\0\200"..., 4104) = 4104
-_llseek(15, 509488, [509488], SEEK_SET) = 0
-read(15, "E\251\266\t\321\17\0\0", 8) = 8
-_llseek(15, 164472149, [164472149], SEEK_SET) = 0
-read(15, "\356\0\t\1?\0V\0\0\0\0\0\271\17\0\0\324q;\1\0\0\0\0\200\35\212&&H)*"..., 4049) = 4049
-_llseek(15, 509496, [509496], SEEK_SET) = 0
-read(15, "\26\271\266\t\1\17\0\0", 8) = 8
-_llseek(15, 164476198, [164476198], SEEK_SET) = 0
-read(15, "\366\0\10\0015\0[\0\0\0\0\0\351\16\0\0\324q;\1\0\0\0\0\200\20\201*\4+\206*"..., 3841) = 3841
-_llseek(15, 509504, [509504], SEEK_SET) = 0
-read(15, "\27\310\266\t\304\16\0\0", 8) = 8
-_llseek(15, 164480039, [164480039], SEEK_SET) = 0
-read(15, "\367\0\7\0013\0[\0\0\0\0\0\254\16\0\0\324q;\1\0\0\0\0\200\33\210+,+,,"..., 3780) = 3780
-_llseek(15, 509512, [509512], SEEK_SET) = 0
-read(15, "\333\326\266\t\343\16\0\0", 8) = 8
-_llseek(15, 164483819, [164483819], SEEK_SET) = 0
-read(15, "\370\0\7\1A\0_\0\0\0\0\0\313\16\0\0\324q;\1\0\0\0\0\200\34\203,,*\200\""..., 3811) = 3811
-_llseek(15, 509520, [509520], SEEK_SET) = 0
-read(15, "\276\345\266\t\f\17\0\0", 8) = 8
-_llseek(15, 164487630, [164487630], SEEK_SET) = 0
-read(15, "\370\0\7\1H\0`\0\0\0\0\0\364\16\0\0\324q;\1\0\0\0\0\200\35\201,\200*\0\200"..., 3852) = 3852
-_llseek(15, 509528, [509528], SEEK_SET) = 0
-read(15, "\312\364\266\tA\17\0\0", 8) = 8
-_llseek(15, 164491482, [164491482], SEEK_SET) = 0
-read(15, "\373\0\10\1;\0Z\0\0\0\0\0)\17\0\0\324q;\1\0\0\0\0\200\36\202+,\200\33\0"..., 3905) = 3905
-_llseek(15, 509536, [509536], SEEK_SET) = 0
-read(15, "\v\4\267\t\246\17\0\0", 8) = 8
-_llseek(15, 164495387, [164495387], SEEK_SET) = 0
-read(15, "\366\0\n\0018\0Y\0\0\0\0\0\216\17\0\0\324q;\1\0\0\0\0\200\32\t+\205*\4,"..., 4006) = 4006
-
diff --git a/doc/formats/ani.md b/doc/formats/ani.md
new file mode 100644
index 0000000..6cd11be
--- /dev/null
+++ b/doc/formats/ani.md
@@ -0,0 +1,305 @@
+# `Anim/WarHammer.ani`
+
+This turns out to simply be an [`obj`](obj.md#WarHammer.ani) file. However, some
+other files are implicated in its interpretation:
+
+- `Data/AniObDef.dat`
+- `Idx/WarHammer.idx`
+
+Including comments, the former 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.
+
+Still, I think a focus in `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 an Ultramarine squad:
+
+
+
+```
+# 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
+```
+
+
+
+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 |
+
+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` (fd 34) and `ani` (fd 14) files when placing a
+single librarian for me to dig into in more detail.
+
+
+
+```
+read(14, "~\337\2\0 \0\0\0\360\373\26\0\20\374\26\0\245J\303\30\0\0\0\0\0\0\0\0(\222R\0", 32) = 32
+
+_llseek(34, 132, [132], SEEK_SET) = 0
+read(34, "0|\t\0\230\0\0\0\210\370\0\0", 12) = 12
+
+_llseek(34, 132, [132], SEEK_SET) = 0
+read(34, "0|\t\0\230\0\0\0\210\370\0\0", 12) = 12
+
+_llseek(34, 621616, [621616], SEEK_SET) = 0
+read(34, "\2\1\0013P\203\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621628, [621628], SEEK_SET) = 0
+read(34, "\2\1\0023\262\203\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621640, [621640], SEEK_SET) = 0
+read(34, "\2\1\0033\24\204\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621652, [621652], SEEK_SET) = 0
+read(34, "\2\1\0043v\204\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621664, [621664], SEEK_SET) = 0
+read(34, "\2\1\0053\330\204\t\0\r\0\0\0", 12) = 12
+
+_llseek(34, 623832, [623832], 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
+
+_llseek(14, 509440, [509440], SEEK_SET) = 0
+read(14, "\fM\266\th\16\0\0", 8) = 8
+_llseek(14, 509448, [509448], SEEK_SET) = 0
+read(14, "t[\266\t\376\16\0\0", 8) = 8
+_llseek(14, 509456, [509456], SEEK_SET) = 0
+read(14, "rj\266\tg\17\0\0", 8) = 8
+_llseek(14, 509464, [509464], SEEK_SET) = 0
+read(14, "\331y\266\t\251\17\0\0", 8) = 8
+_llseek(14, 509472, [509472], SEEK_SET) = 0
+read(14, "\202\211\266\t\273\17\0\0", 8) = 8
+_llseek(14, 509480, [509480], SEEK_SET) = 0
+read(14, "=\231\266\t\10\20\0\0", 8) = 8
+_llseek(14, 509488, [509488], SEEK_SET) = 0
+read(14, "E\251\266\t\321\17\0\0", 8) = 8
+_llseek(14, 509496, [509496], SEEK_SET) = 0
+read(14, "\26\271\266\t\1\17\0\0", 8) = 8
+_llseek(14, 509504, [509504], SEEK_SET) = 0
+read(14, "\27\310\266\t\304\16\0\0", 8) = 8
+_llseek(14, 509512, [509512], SEEK_SET) = 0
+read(14, "\333\326\266\t\343\16\0\0", 8) = 8
+_llseek(14, 509520, [509520], SEEK_SET) = 0
+read(14, "\276\345\266\t\f\17\0\0", 8) = 8
+_llseek(14, 509528, [509528], SEEK_SET) = 0
+read(14, "\312\364\266\tA\17\0\0", 8) = 8
+_llseek(14, 509536, [509536], SEEK_SET) = 0
+read(14, "\v\4\267\t\246\17\0\0", 8) = 8
+_llseek(14, 509440, [509440], SEEK_SET) = 0
+read(14, "\fM\266\th\16\0\0", 8) = 8
+
+_llseek(14, 164448540, [164448540], SEEK_SET) = 0
+read(14, "\367\0\n\0015\0T\0\0\0\0\0P\16\0\0\324q;\1\0\0\0\0\200\23\207**+*+"..., 3688) = 3688
+_llseek(14, 509448, [509448], SEEK_SET) = 0
+read(14, "t[\266\t\376\16\0\0", 8) = 8
+_llseek(14, 164452228, [164452228], SEEK_SET) = 0
+read(14, "\365\0\10\0017\0W\0\0\0\0\0\346\16\0\0\324q;\1\0\0\0\0\200\25\3*\212+*,"..., 3838) = 3838
+_llseek(14, 509456, [509456], SEEK_SET) = 0
+read(14, "rj\266\tg\17\0\0", 8) = 8
+_llseek(14, 164456066, [164456066], SEEK_SET) = 0
+read(14, "\364\0\10\19\0Z\0\0\0\0\0O\17\0\0\324q;\1\0\0\0\0\200\30\201*\5+\200\33"..., 3943) = 3943
+_llseek(14, 509464, [509464], SEEK_SET) = 0
+read(14, "\331y\266\t\251\17\0\0", 8) = 8
+_llseek(14, 164460009, [164460009], SEEK_SET) = 0
+read(14, "\356\0\7\1B\0[\0\0\0\0\0\221\17\0\0\324q;\1\0\0\0\0\200\"\201*\200\37\0\200"..., 4009) = 4009
+_llseek(14, 509472, [509472], SEEK_SET) = 0
+read(14, "\202\211\266\t\273\17\0\0", 8) = 8
+_llseek(14, 164464018, [164464018], SEEK_SET) = 0
+read(14, "\356\0\n\1C\0\\\0\0\0\0\0\243\17\0\0\324q;\1\0\0\0\0\200#\3)\3+\200\32"..., 4027) = 4027
+_llseek(14, 509480, [509480], SEEK_SET) = 0
+read(14, "=\231\266\t\10\20\0\0", 8) = 8
+_llseek(14, 164468045, [164468045], SEEK_SET) = 0
+read(14, "\354\0\t\1C\0Z\0\0\0\0\0\360\17\0\0\324q;\1\0\0\0\0\200$\201*\200\36\0\200"..., 4104) = 4104
+_llseek(14, 509488, [509488], SEEK_SET) = 0
+read(14, "E\251\266\t\321\17\0\0", 8) = 8
+_llseek(14, 164472149, [164472149], SEEK_SET) = 0
+read(14, "\356\0\t\1?\0V\0\0\0\0\0\271\17\0\0\324q;\1\0\0\0\0\200\35\212&&H)*"..., 4049) = 4049
+_llseek(14, 509496, [509496], SEEK_SET) = 0
+read(14, "\26\271\266\t\1\17\0\0", 8) = 8
+_llseek(14, 164476198, [164476198], SEEK_SET) = 0
+read(14, "\366\0\10\0015\0[\0\0\0\0\0\351\16\0\0\324q;\1\0\0\0\0\200\20\201*\4+\206*"..., 3841) = 3841
+_llseek(14, 509504, [509504], SEEK_SET) = 0
+read(14, "\27\310\266\t\304\16\0\0", 8) = 8
+_llseek(14, 164480039, [164480039], SEEK_SET) = 0
+read(14, "\367\0\7\0013\0[\0\0\0\0\0\254\16\0\0\324q;\1\0\0\0\0\200\33\210+,+,,"..., 3780) = 3780
+_llseek(14, 509512, [509512], SEEK_SET) = 0
+read(14, "\333\326\266\t\343\16\0\0", 8) = 8
+_llseek(14, 164483819, [164483819], SEEK_SET) = 0
+read(14, "\370\0\7\1A\0_\0\0\0\0\0\313\16\0\0\324q;\1\0\0\0\0\200\34\203,,*\200\""..., 3811) = 3811
+_llseek(14, 509520, [509520], SEEK_SET) = 0
+read(14, "\276\345\266\t\f\17\0\0", 8) = 8
+_llseek(14, 164487630, [164487630], SEEK_SET) = 0
+read(14, "\370\0\7\1H\0`\0\0\0\0\0\364\16\0\0\324q;\1\0\0\0\0\200\35\201,\200*\0\200"..., 3852) = 3852
+_llseek(14, 509528, [509528], SEEK_SET) = 0
+read(14, "\312\364\266\tA\17\0\0", 8) = 8
+_llseek(14, 164491482, [164491482], SEEK_SET) = 0
+read(14, "\373\0\10\1;\0Z\0\0\0\0\0)\17\0\0\324q;\1\0\0\0\0\200\36\202+,\200\33\0"..., 3905) = 3905
+_llseek(14, 509536, [509536], SEEK_SET) = 0
+read(14, "\v\4\267\t\246\17\0\0", 8) = 8
+_llseek(14, 164495387, [164495387], SEEK_SET) = 0
+read(14, "\366\0\n\18\0Y\0\0\0\0\0\216\17\0\0\324q;\1\0\0\0\0\200\32\t+\205*\4,"..., 4006) = 4006
+
+_llseek(34, 623832, [623832], 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
+
+_llseek(34, 132, [132], SEEK_SET) = 0
+read(34, "0|\t\0\230\0\0\0\210\370\0\0", 12) = 12
+
+_llseek(34, 621616, [621616], SEEK_SET) = 0
+read(34, "\2\1\0013P\203\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621628, [621628], SEEK_SET) = 0
+read(34, "\2\1\0023\262\203\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621640, [621640], SEEK_SET) = 0
+read(34, "\2\1\0033\24\204\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621652, [621652], SEEK_SET) = 0
+read(34, "\2\1\0043v\204\t\0\r\0\0\0", 12) = 12
+_llseek(34, 621664, [621664], SEEK_SET) = 0
+read(34, "\2\1\0053\330\204\t\0\r\0\0\0", 12) = 12
+
+_llseek(34, 623832, [623832], 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
+```
+
+
+
+ Notable is that we still load 5 type 2 records, even though there's just a
+ single librarian, and 8 compass points. Why 5?
diff --git a/doc/formats/index.md b/doc/formats/index.md
index a9eb5a7..a54a0d9 100644
--- a/doc/formats/index.md
+++ b/doc/formats/index.md
@@ -6,22 +6,18 @@ remake.
## Filesystem layout
-* `Anim/`
- * `WarHammer.ani` # Doesn't seem to be a RIFF file. 398M so very important.
- * There's a pcx image header at `dd ... bs=1 skip=213` but it seems to be a false alert
- * Hits for "AmigaOS bitmap font"... probably a false positive
- * Lots of 8-byte reads when loading stuff in the mission editor
- * Some ~4K reads, havent found one corresponding to a known format yet
+* [✓] [`Anim/`](obj.md#WarHammer.ani)
+ * [`WarHammer.ani`](obj.md#WarHammer.ani)
* [`Assign/`](obj.md#assign)
* `*.asn` # Unknown, seems to be related to .obj files
* `Cursor/`
- * `*.ani` # RIFF data
- * `*.cur` # Presumably standard windows-format non-animated cursors
+ * `*.ani` # RIFF data, standard ANI format \o/
+ * [`Cursors.cur`](obj.md) # `obj` file containing pointers and drag elements
* `Data/`
* `*.dat` # plaintext files defining properties of objects. No single format
* **PARSED**
* `Accounting.dat` # key = value => internal/data/accounting.go
- * `AniObjDef.dat` # animated object definitions
+ * [`AniObjDef.dat`](ani.mnu) # animated object definitions
* `GenericData.dat` # Generic Game Settings
* **TODO**
* `ChaNames.dat` # list of character names
@@ -63,24 +59,24 @@ remake.
* `wh40k.flt` # Audio filter(s?)
* [✓] [`Fonts/`](fonts.md)
* `cboxfont` # ???
- * `*.fnt`
- * `*.spr`
-* `Idx/`
- * `WarHammer.idx` # unknown, 1.8M
+ * [`*.fnt`](fonts.md)
+ * [`*.spr`](obj.md) # `obj` file
+* [ ] [`Idx/`](ani.md)
+ * [`WarHammer.idx`](ani.md) # unknown, 1.8M
* [`Maps/`](maps.md)
- * `*.MAP`
- * `*.TXT`
+ * [`*.MAP`](maps.md)
+ * [`*.TXT`](maps.md)
* [`Menu/`](mnu.md) - UI element definitions
- * `*.mni`
- * `*.mnu`
+ * [`*.mni`](mnu.md) # Menu include file
+ * [`*.mnu`](mnu.md)
* [`*.obj`](obj.md)
* `Misc/`
* `occlusio.lis` # plain text, presumably occlusion mappings?
* [`MultiMaps/`](maps.md#multimaps)
- * `*.MAP`
- * `*.TXT`
+ * [`*.MAP`](maps.md)
+ * [`*.TXT`](maps.md)
* [✓] [`Obj/`](obj.md)
- * `*.obj`
+ * [`*.obj`](obj.md)
* [✓] `Pic/`
* `*.pcx` # Standard .pcx format
* `RandomMaps/`
@@ -91,12 +87,12 @@ remake.
* `*.txt` # Seems to be a copy of one of Maps/*.txt
* [✓] [`Sets/`](sets.md)
* `Data.chk`
- * `*.set`
+ * [`*.set`](sets.md)
* [✓] `SMK/`
* `*.smk` # Videos: RAD Game Tools Smacker Multimedia version 2
-* [ ] `Sounds/`
+* [ ] [`Sounds/`](sound.md)
* [`wh40k.ds`](sound.md)
-* [ ] `Wav/`
+* [ ] [`Wav/`](sound.md)
* [`*.wav`](sound.md)
Phew.
diff --git a/doc/formats/obj.md b/doc/formats/obj.md
index dc5ab6c..8c3bb21 100644
--- a/doc/formats/obj.md
+++ b/doc/formats/obj.md
@@ -206,8 +206,8 @@ in the CENTER position. Interesting.
| 0x0004 | x,y size (16 bits each) |
| 0x0008 | ? (blank in all cases so far)
| 0x000c | Size of remaining pixeldata |
-| 0x0010 | Padding? |
-| 0x0014 | Padding? |
+| 0x0010 | Set in `WarHammer.ani` |
+| 0x0014 | ? (blank in all cases so far) |
The volume represented by a cell is a little odd. We see three faces of a fake
3D volume of size 64x64x32(ish). This is presented in an isomorphic fashion, so
@@ -512,3 +512,60 @@ break *0x41DD10
This lets me focus very narrowly on what happens when loading sprites, and
might give clues.
+## WarHammer.ani
+
+This 400MiB file appears to be a standard object file, it's just very large.
+The directory contains 188,286 sprites!
+
+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 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. But where?
+
+The field at 0x10 in the sprite header is set in `WarHammer.ani`, but not in
+the other object files encountered so far. However, it seems to be
+set statically to the bytes `[212 113 59 1]` for all of them.
+
+Assuming ~1000 sprites per character, `WarHammer.ani` contains 188 characters.
+
+Two other files have been implicated in animation - `Data/AniObDefs.dat` and
+`Idx/WarHammer.idx`. More on those in `ani.md`.
diff --git a/internal/data/object.go b/internal/data/object.go
index b5b77a3..2fd3e1f 100644
--- a/internal/data/object.go
+++ b/internal/data/object.go
@@ -6,6 +6,7 @@ import (
"image"
"io"
"io/ioutil"
+ "log"
"os"
"path/filepath"
"strings"
@@ -19,8 +20,9 @@ type SpriteHeader struct {
Width uint16
Height uint16
Padding1 uint32 // I don't think this is used. Could be wrong.
- PixelSize uint32 // Size of PixelData, excluding this sprite header
- Padding2 uint64 // I don't think this is used either. Could be wrong.
+ PixelSize uint32
+ Unknown1 [4]byte // ??? Only observed in `WarHammer.ani` so far
+ Padding2 uint32 // I don't think this is used either. Could be wrong.
}
func (s SpriteHeader) Check(expectedSize uint32) error {
@@ -28,6 +30,12 @@ func (s SpriteHeader) Check(expectedSize uint32) error {
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
}
+ // TODO: WarHammer.ani sets Unknown1 to this for all 188,286 sprites. I am
+ // very interested in seeing if there are any others
+ if s.Unknown1[0] != 212 || s.Unknown1[1] != 113 || s.Unknown1[2] != 59 || s.Unknown1[3] != 1 {
+ log.Printf("Value of Unknown1 field: %v", s.Unknown1)
+ }
+
// Remove 24 bytes from passed-in size to account for the header
if s.PixelSize != expectedSize-24 {
return fmt.Errorf("Advertised pixel size: %d differs from expected: %v", s.PixelSize, expectedSize-24)
diff --git a/scripts/try-uncompress b/scripts/try-uncompress
index ddff211..be2b362 100755
--- a/scripts/try-uncompress
+++ b/scripts/try-uncompress
@@ -499,7 +499,23 @@ def build(filename)
File.open(filename, "w") { |f| f.write(built.to_data) }
end
+def unknown16(filenames)
+ objs = filenames.map { |f| load_obj(f) }
+ results = Set.new
+
+ objs.each do |obj|
+ obj.sprites.each do |spr|
+ results << spr.header.unknown16
+ end
+ end
+
+ puts "Unique widths for u16,4"
+ pp results
+end
+
case command = ARGV.shift
+when "unknown16" then
+ unknown16(ARGV)
when "sprites" then
ARGV.each { |filename| sprites(filename) }
when "sprite" then