Compare commits

...

2 Commits

Author SHA1 Message Date
f971ba320c Some more character investigations 2020-06-11 02:54:57 +01:00
cf624cc77b Switch from encoding/binary to struc
It's not perfect, but struc can deserialize the whole thing into one
struct while encoding/binary can't. It's nice to have that.
2020-06-09 00:36:56 +01:00
8 changed files with 254 additions and 170 deletions

View File

@@ -139,16 +139,15 @@ func loadMapsFrom(mapsPath string) {
}
log.Printf("Maps in %s:", mapsPath)
for key, gameMap := range gameMaps {
rect := gameMap.Rect()
hdr := gameMap.Header
for key, gm := range gameMaps {
rect := gm.Rect()
fmt.Printf(
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
key,
hdr.IsCampaignMap,
gm.IsCampaignMap,
rect.Min.X, rect.Max.X,
rect.Min.Y, rect.Max.Y,
string(hdr.SetName[:]),
string(gm.SetName),
)
}
}

View File

@@ -181,8 +181,8 @@ func (e *env) Draw(screen *ebiten.Image) error {
for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
cell := gameMap.Cells.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(&cell, e.state.cellIdx))
cell := gameMap.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(cell, e.state.cellIdx))
}
}

View File

@@ -598,7 +598,8 @@ Ignoring them for now, here's a first guess at a header:
| 24 | 2 | ??? - varies. Seems related to character/squad position? |
| 26 | 2 | ??? - invariant `00 04` |
| 28 | 4 | ??? - varies (0 vs 5) |
| 32 | 4 | Number of thingies (26 vs 1) |
| 32 | 1 | Number of thingies |
| 33 | 3 | ???. With a Lord of Change on the map, only one byte works as thingies count |
| 36 | 20 | Padding? |
56 bytes of data is interesting because the value of that first, ignored byte is
@@ -625,7 +626,78 @@ Generating a map with no characters at all, the trailer is 2,447 bytes, and the
mission title starts at 0x3B (59). So we can say we have 20 bytes of padding as
a first approximation?
The "trailer trailer", for want of a better term, seems to be organised as:
Here's where we're at with the per-character data, going from the padding values
suggested above:
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| 0 | 179 | ??? |
| 179 | 80 | Character name |
| 259 | 10 | Character attributes |
| 269 | 495 | ??? |
| 764 | 1(?) | Squad number? |
| 765 | 927 | ??? |
There's still a lot of bytes to dig through, but this allows me to load the
character names from Chapter01 correctly, with the exception of record 57 which
just contains `\x02` and is then null-terminated all the way through. Looking
at the bytes for one character record, I can easily correlate certain bytes to
various attributes; that's just done in code for the moment.
Given two characters of the same time, just in different locations, differing
values are seen at:
* `0x103 - 0x10c` (hodgepodge)
* `0x178 - 0x1be` (hodgepodge)
* `0x2fc` (0, 1) - squad number?
In Chapter01, picking a random character (Gorgon) and looking at his squadmates,
they are all in the same squad, and no other characters are in that squad, so it
looks pretty diagnostic to me. There's nothing in the UI to indicate the squad,
though.
Now let's look for position. In my 2-character map, they're at 65,50 and 70,55.
Within a character, I see those numbers repeated twice - around `0x1b{9,a}` and
`0x1b{d,e}`.
Characters don't seem to take up multiple x,y squares, but they *can* take up
multiple Z levels. So maybe this is a bounding box of some kind?
Adding a (tall) Lord of Change to the map gave me `02 36 45 00 02 37 45`, which
doesn't quite match what my eyes are telling me for Z,Y,X. In addition, the data
immediately after this offset changed into a large number of coordinate-like
sets of values - far too many for it to actually be a bounding box. However, the
first one remains good as a position specifier.
Down in `0x679` (Chaos Sorcerer) or `0x68D` (Lord of Change), the map coords for
the *other* character appears, which is downright odd. For now, just use the
first-indexed value.
How about their types? We need to be able to work out which animations to show,
so they must be uniquely identified somehow. Ultramarine captain is animation
group 12, while chaos lord is group... 14? Not certain. I don't see either of
those numbers in a promising spot, anyway.
I do see differences at around `0x170`:
```
Ultramarine Captain: 00 00 00 00 00 00 00 00 50 03 00 00 00 00 00 00
Chaos Lord: 00 00 00 00 11 01 00 00 47 03 00 04 00 00 02 00
```
Maybe they're somewhat relevant, it's hard to say.
Thingies next: these aren't decoded at all yet, and the sizes seem to be
variable.
| Offset | Size | Meaning |
| ------ | ---- | ------- |
| | | |
Finally, the "trailer trailer", for want of a better term, seems to be organised
as:
| Offset | Size | Meaning |
| ----- | ---- | ------- |
@@ -633,6 +705,8 @@ The "trailer trailer", for want of a better term, seems to be organised as:
| 255 | 2048 | Briefing |
| 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 |
This duplicates the information found in the `.TXT` files. No idea what the end
data is yet.
## Soldiers At War

3
go.mod
View File

@@ -8,7 +8,9 @@ require (
github.com/hajimehoshi/ebiten v1.11.1
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
github.com/stretchr/testify v1.5.1
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
@@ -16,4 +18,5 @@ require (
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c
)

6
go.sum
View File

@@ -40,9 +40,13 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe h1:ewr1srjRCmcQogPQ/NCx6XCk6LGVmsVCc9Y3vvPZj+Y=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4 h1:Y/KOCu+ZLB730PudefxfsKVjtI0m0RhvFk9a0l4O1+c=
@@ -107,5 +111,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c h1:7j7Yy/3gedviEts3jKY0bEruQkTFKh+8pDmEFaM6UBc=
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c/go.mod h1:WJaLhyHHEQFOgwIxu/SJxvUHJA18glYsMETBTMIySTY=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -74,8 +74,8 @@ func (m *Map) LoadSprites() error {
}
// FIXME: get rid of this
func (m *Map) Cell(x, y, z int) maps.Cell {
return m.raw.Cells.At(x, y, z)
func (m *Map) Cell(x, y, z int) *maps.Cell {
return m.raw.At(x, y, z)
}
// SpritesForCell returns the sprites needed to correctly render this cell.
@@ -102,5 +102,21 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
sprites = append(sprites, sprite)
}
// FIXME: this just marks character positions with sprite 19 for now.
specialsObj, err := m.assets.Object("specials")
if err != nil {
return nil, err
}
chrSpr, err := specialsObj.Sprite(19)
if err != nil {
return nil, err
}
for _, chr := range m.raw.Characters {
if chr.XPos == x && chr.YPos == y && z == 1 { // FIXME: sort out ZPos
sprites = append(sprites, chrSpr)
}
}
return sprites, nil
}

View File

@@ -6,12 +6,12 @@ import (
"encoding/binary"
"fmt"
"image"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/lunixbochs/struc"
)
var (
@@ -31,93 +31,128 @@ const (
cellCount = MaxHeight * MaxLength * MaxWidth
)
type Header struct {
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes
MinWidth uint32
MinLength uint32
MaxWidth uint32
MaxLength uint32
Unknown1 uint32
Unknown2 uint32
Unknown3 uint32
Unknown4 uint32
Magic [8]byte // "\x08\x00WHMAP\x00"
Unknown5 uint32
Unknown6 uint32
SetName [8]byte // Links to a filename in `/Sets/*.set`
// Need to investigate the rest of the header too
type GameMap struct {
// Main Header
IsCampaignMap bool `struc:"uint32"` // Tentatively: 0 = no, 1 = yes
MinWidth int `struc:"uint32"`
MinLength int `struc:"uint32"`
MaxWidth int `struc:"uint32"`
MaxLength int `struc:"uint32"`
Unknown1 int `struc:"uint32"`
Unknown2 int `struc:"uint32"`
Unknown3 int `struc:"uint32"`
Unknown4 int `struc:"uint32"`
Magic []byte `struc:"[8]byte"` // "\x08\x00WHMAP\x00"
Unknown5 int `struc:"uint32"`
Unknown6 int `struc:"uint32"`
SetName string `struc:"[8]byte"` // Links to a filename in `/Sets/*.set`
Padding []byte `struc:"[212]byte"`
// Per-cell data
NumCells int `struc:"skip"` // FIXME: We can't use []Cell below without this field
Cells []Cell `struc:"[]Cell,sizefrom=NumCells"`
// Trailer header
Discard1 [3]byte `struc:"[3]byte"` // First byte is size of trailer header?
TrailerMaxWidth int `struc:"uint32"`
TrailerMaxLength int `struc:"uint32"`
TrailerMinWidth int `struc:"uint32"`
TrailerMinLength int `struc:"uint32"`
NumCharacters int `struc:"uint32"`
TrailerUnknown1 int `struc:"uint32"`
TrailerUnknown2 int `struc:"uint16"`
TrailerUnknown3 int `struc:"uint16"`
TrailerUnknown4 int `struc:"uint32"`
NumThingies int `struc:"byte"`
TrailerUnknown5 []byte `struc:"[3]byte"`
Padding1 []byte `struc:"[20]byte"`
// FIXME: The rest is trash until Character & Thingy are worked out
Characters []Character `struc:"[]Character,sizefrom=NumCharacters"`
Thingies []Thingy `struc:"[]Thingy,sizefrom=NumThingies"`
Title string `struc:"[255]byte"`
Briefing string `struc:"[2048]byte"`
// Maybe? each contains either 0 or 1? Hard to say
TrailerUnknown6 []byte `struc:"[85]byte"`
}
type TrailerHeader struct {
Discard1 [3]byte // No idea what this lot is
MaxWidth uint32
MaxLength uint32
MinWidth uint32
MinLength uint32
NumCharacters uint32
Unknown1 uint32
Unknown2 uint16
Unknown3 uint16
Unknown4 uint32
NumThingies uint32
Padding1 [20]byte
}
type TrailerTrailer struct {
Title [255]byte
Briefing [2048]byte
Unknown1 [85]uint8 // Maybe? each contains either 0 or 1? Hard to say
type Cell struct {
DoorAndCanisterRelated byte `struc:"byte"`
DoorLockAndReactorRelated byte `struc:"byte"`
Unknown2 byte `struc:"byte"`
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown11 byte `struc:"byte"`
Unknown12 byte `struc:"byte"`
Unknown13 byte `struc:"byte"`
Unknown14 byte `struc:"byte"`
SquadRelated byte `struc:"byte"`
}
type Character struct {
Unknown1 uint32
Unknown1 []byte `struc:"[179]byte"`
Name string `struc:"[80]byte"`
// Attributes guessed by matching up numbers. Starts at 0x103
WeaponSkill int `struc:"byte"`
BallisticSkill int `struc:"byte"`
Unknown2 byte `struc:"byte"`
Leadership int `struc:"byte"`
Toughness int `struc:"byte"`
Strength int `struc:"byte"`
ActionPoints int `struc:"byte"`
Unknown3 byte `struc:"byte"`
Unknown4 byte `struc:"byte"`
Health int `struc:"byte"`
Unknown5 []byte `struc:"[91]byte"`
Armor int `struc:"byte"`
Unknown6 []byte `struc:"[84]byte"`
YPos int `struc:"byte"` // These are actually much more complicated
XPos int `struc:"byte"`
Unknown7 []byte `struc:"[317]byte"`
SquadNumber byte `struc:"byte"`
Unknown8 []byte `struc:"[927]byte"`
// TODO: each character may have a fixed number of subrecords for inventory
}
type Characters []Character
// TODO. These are triggers/reactors/etc.
type Thingy struct {}
type Thingy struct {
Unknown1 int `struc:"uint32"`
}
type Thingies []Thingy
func (h Header) Width() int {
return int(h.MaxWidth - h.MinWidth)
func (g *GameMap) MapSetName() string {
return g.SetName
}
func (h Header) Length() int {
return int(h.MaxLength - h.MinLength)
}
func (h Header) Height() int {
return MaxHeight
}
func (h Header) MapSetName() string {
idx := bytes.IndexByte(h.SetName[:], 0)
if idx < 0 {
idx = 8 // all 8 bytes are used
}
return string(h.SetName[0:idx:idx])
}
func (h Header) MapSetFilename() string {
return h.MapSetName() + ".set"
func (g *GameMap) MapSetFilename() string {
return g.MapSetName() + ".set"
}
type ObjRef struct {
AreaByte byte
SpriteAndFlagByte byte
AreaByte byte `struc:"byte"`
SpriteAndFlagByte byte `struc:"byte"`
}
// The index into a set palette to retrieve the object
func (o ObjRef) Index() int {
func (o *ObjRef) Index() int {
return int(o.AreaByte)
}
func (o ObjRef) Sprite() int {
func (o *ObjRef) Sprite() int {
// The top bit seems to be a flag of some kind
return int(o.SpriteAndFlagByte & 0x7f)
}
@@ -127,21 +162,6 @@ func (o ObjRef) IsActive() bool {
return (o.SpriteAndFlagByte & 0x80) == 0x80
}
type Cell struct {
DoorAndCanisterRelated byte
DoorLockAndReactorRelated byte
Unknown2 byte
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown11 byte
Unknown12 byte
Unknown13 byte
Unknown14 byte
SquadRelated byte
}
func (c *Cell) At(n int) byte {
switch n {
case 0:
@@ -181,46 +201,26 @@ func (c *Cell) At(n int) byte {
return 0
}
// Cells is always a fixed size; use At to get a cell according to x,y,z
type Cells []Cell
func (c Cells) At(x, y, z int) Cell {
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
func (g *GameMap) At(x, y, z int) *Cell {
return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
}
func (h Header) Check() []error {
var out []error
if h.IsCampaignMap > 1 {
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
func (g *GameMap) Check() error {
if bytes.Compare(expectedMagic, g.Magic) != 0 {
return fmt.Errorf("Unexpected magic value: %v", g.Magic)
}
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
}
// TODO: other consistency checks
return out
}
type GameMap struct {
Header
Cells
TrailerHeader
Characters
Thingies
TrailerTrailer
// TODO: parse this into sections. This is the text that comes from the
// .TXT file. Maybe we don't need it at all since it should be duplicated in
// the trailer.
Text string
return nil
}
func (m *GameMap) Rect() image.Rectangle {
return image.Rect(
int(m.Header.MinWidth),
int(m.Header.MinLength),
int(m.Header.MaxWidth),
int(m.Header.MaxLength),
int(m.MinWidth),
int(m.MinLength),
int(m.MaxWidth),
int(m.MaxLength),
)
}
@@ -243,23 +243,17 @@ func LoadGameMap(prefix string) (*GameMap, error) {
return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix)
}
// A game map is composed of two files: .map and .txt
// A game map is composed of two files: .map and .txt. We ignore the text file,
// since the content is replicated in the map file.
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
out, err := loadMapFile(mapFile)
if err != nil {
return nil, err
}
// TODO: load text and parse into sections
txt, err := ioutil.ReadFile(txtFile)
if err != nil {
if err := out.Check(); err != nil {
return nil, err
}
out.Text = string(txt)
for _, err := range out.Check() {
log.Printf("%s: %v", mapFile, err)
}
return out, nil
}
@@ -297,6 +291,7 @@ func LoadGameMaps(dir string) (map[string]*GameMap, error) {
func loadMapFile(filename string) (*GameMap, error) {
var out GameMap
out.NumCells = cellCount
mf, err := os.Open(filename)
if err != nil {
@@ -312,52 +307,43 @@ func loadMapFile(filename string) (*GameMap, error) {
defer zr.Close()
if err := binary.Read(zr, binary.LittleEndian, &out.Header); err != nil {
return nil, fmt.Errorf("Error parsing %s: %v", filename, err)
}
// no gzip.SeekReader, so discard unread header bytes for now
discardSize := int64(cellDataOffset - binary.Size(&out.Header))
if _, err := io.CopyN(ioutil.Discard, zr, discardSize); err != nil {
if err := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); err != nil {
return nil, err
}
out.Cells = make(Cells, cellCount)
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil {
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
// Trim any trailing nulls off of the strings
nullTerminate(&out.SetName)
nullTerminate(&out.Title)
nullTerminate(&out.Briefing)
for i, chr := range out.Characters {
nullTerminate(&chr.Name)
fmt.Printf("Character %v: %s\n", i, chr.String())
}
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerHeader); err != nil {
return nil, fmt.Errorf("Error parsing trailer header for %s: %v", filename, err)
}
fmt.Printf("Mission Title: %q\n", out.Title)
fmt.Printf("Mission Briefing: %q\n", out.Briefing)
log.Printf("Trailer Header: %#+v", out.TrailerHeader)
/*
// TODO: until we know how large each character record should be, we can't read this lot
out.Characters = make(Characters, int(out.TrailerHeader.NumCharacters))
if err := binary.Read(zr, binary.LittleEndian, &out.Characters); err != nil {
return nil, fmt.Errorf("Error parsing characters for %s: %v", filename, err)
}
out.Thingies = make(Thingies, int(out.TrailerHeader.NumThingies))
if err := binary.Read(zr, binary.LittleEndian, &out.Thingies); err != nil {
return nil, fmt.Errorf("Error parsing thingies for %s: %v", filename, err)
}
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerTrailer); err != nil {
return nil, fmt.Errorf("Error parsing trailer trailer for %s: %v", filename, err)
}
log.Printf("Trailer Trailer: %s", out.TrailerTrailer.String())
*/
return &out, nil
}
func (t *TrailerTrailer) String() string {
func nullTerminate(s *string) {
sCpy := *s
idx := strings.Index(sCpy, "\x00")
if idx < 0 {
return
}
*s = sCpy[0:idx]
}
func (c *Character) String() string {
return fmt.Sprintf(
"title=%q briefing=%q rest=%#+v",
strings.TrimRight(string(t.Title[:]), "\x00"),
strings.TrimRight(string(t.Briefing[:]), "\x00"),
t.Unknown1,
"squad=%v pos=(%v,%v) name=%q\n\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n",
c.SquadNumber,
c.XPos, c.YPos,
c.Name,
c.ActionPoints, c.Health, c.Armor, c.BallisticSkill, c.WeaponSkill,
c.Strength, c.Toughness /*c.Initiative, c.Attacks,*/, c.Leadership,
)
}

View File

@@ -9,7 +9,7 @@ type CellPoint struct {
Z int
}
func (s *Scenario) CellAtCursor() (maps.Cell, CellPoint) {
func (s *Scenario) CellAtCursor() (*maps.Cell, CellPoint) {
cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
return cell, CellPoint{IsoPt: s.selectedCell, Z: 0}
}