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.
This commit is contained in:
2020-06-09 00:35:57 +01:00
parent 65bae80d40
commit cf624cc77b
7 changed files with 118 additions and 172 deletions

View File

@@ -139,16 +139,15 @@ func loadMapsFrom(mapsPath string) {
} }
log.Printf("Maps in %s:", mapsPath) log.Printf("Maps in %s:", mapsPath)
for key, gameMap := range gameMaps { for key, gm := range gameMaps {
rect := gameMap.Rect() rect := gm.Rect()
hdr := gameMap.Header
fmt.Printf( fmt.Printf(
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n", " * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
key, key,
hdr.IsCampaignMap, gm.IsCampaignMap,
rect.Min.X, rect.Max.X, rect.Min.X, rect.Max.X,
rect.Min.Y, rect.Max.Y, 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 y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
for x := int(rect.Min.X); x < int(rect.Max.X); x++ { for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
cell := gameMap.Cells.At(x, y, int(e.state.zIdx)) cell := gameMap.At(x, y, int(e.state.zIdx))
imd.Set(x, y, makeColour(&cell, e.state.cellIdx)) imd.Set(x, y, makeColour(cell, e.state.cellIdx))
} }
} }

3
go.mod
View File

@@ -8,7 +8,9 @@ require (
github.com/hajimehoshi/ebiten v1.11.1 github.com/hajimehoshi/ebiten v1.11.1
github.com/jfreymuth/oggvorbis v1.0.1 // indirect github.com/jfreymuth/oggvorbis v1.0.1 // indirect
github.com/kr/text v0.2.0 // 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/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/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect 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/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= 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-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 h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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 // FIXME: get rid of this
func (m *Map) Cell(x, y, z int) maps.Cell { func (m *Map) Cell(x, y, z int) *maps.Cell {
return m.raw.Cells.At(x, y, z) return m.raw.At(x, y, z)
} }
// SpritesForCell returns the sprites needed to correctly render this cell. // SpritesForCell returns the sprites needed to correctly render this cell.

View File

@@ -6,12 +6,12 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"image" "image"
"io"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/lunixbochs/struc"
) )
var ( var (
@@ -31,93 +31,105 @@ const (
cellCount = MaxHeight * MaxLength * MaxWidth cellCount = MaxHeight * MaxLength * MaxWidth
) )
type Header struct { type GameMap struct {
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes // Main Header
MinWidth uint32 IsCampaignMap bool `struc:"uint32"` // Tentatively: 0 = no, 1 = yes
MinLength uint32 MinWidth int `struc:"uint32"`
MaxWidth uint32 MinLength int `struc:"uint32"`
MaxLength uint32 MaxWidth int `struc:"uint32"`
Unknown1 uint32 MaxLength int `struc:"uint32"`
Unknown2 uint32 Unknown1 int `struc:"uint32"`
Unknown3 uint32 Unknown2 int `struc:"uint32"`
Unknown4 uint32 Unknown3 int `struc:"uint32"`
Magic [8]byte // "\x08\x00WHMAP\x00" Unknown4 int `struc:"uint32"`
Unknown5 uint32 Magic []byte `struc:"[8]byte"` // "\x08\x00WHMAP\x00"
Unknown6 uint32 Unknown5 int `struc:"uint32"`
SetName [8]byte // Links to a filename in `/Sets/*.set` Unknown6 int `struc:"uint32"`
// Need to investigate the rest of the header too 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:"uint32"`
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
TrailerUnknown5 []byte `struc:"[85]byte"`
} }
type TrailerHeader struct { type Cell struct {
Discard1 [3]byte // No idea what this lot is DoorAndCanisterRelated byte `struc:"byte"`
MaxWidth uint32 DoorLockAndReactorRelated byte `struc:"byte"`
MaxLength uint32 Unknown2 byte `struc:"byte"`
MinWidth uint32 Surface ObjRef
MinLength uint32 Left ObjRef
Right ObjRef
NumCharacters uint32 Center ObjRef
Unknown11 byte `struc:"byte"`
Unknown1 uint32 Unknown12 byte `struc:"byte"`
Unknown2 uint16 Unknown13 byte `struc:"byte"`
Unknown3 uint16 Unknown14 byte `struc:"byte"`
Unknown4 uint32 SquadRelated byte `struc:"byte"`
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 Character struct { type Character struct {
Unknown1 uint32 Unknown1 int `struc:"uint32"`
// TODO: each character may have a fixed number of subrecords for inventory
} }
type Characters []Character type Characters []Character
// TODO. These are triggers/reactors/etc. // TODO. These are triggers/reactors/etc.
type Thingy struct {} type Thingy struct {
Unknown1 int `struc:"uint32"`
}
type Thingies []Thingy type Thingies []Thingy
func (h Header) Width() int { func (g *GameMap) MapSetName() string {
return int(h.MaxWidth - h.MinWidth) return g.SetName
} }
func (h Header) Length() int { func (g *GameMap) MapSetFilename() string {
return int(h.MaxLength - h.MinLength) return g.MapSetName() + ".set"
}
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"
} }
type ObjRef struct { type ObjRef struct {
AreaByte byte AreaByte byte `struc:"byte"`
SpriteAndFlagByte byte SpriteAndFlagByte byte `struc:"byte"`
} }
// The index into a set palette to retrieve the object // The index into a set palette to retrieve the object
func (o ObjRef) Index() int { func (o *ObjRef) Index() int {
return int(o.AreaByte) 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 // The top bit seems to be a flag of some kind
return int(o.SpriteAndFlagByte & 0x7f) return int(o.SpriteAndFlagByte & 0x7f)
} }
@@ -127,21 +139,6 @@ func (o ObjRef) IsActive() bool {
return (o.SpriteAndFlagByte & 0x80) == 0x80 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 { func (c *Cell) At(n int) byte {
switch n { switch n {
case 0: case 0:
@@ -181,46 +178,26 @@ func (c *Cell) At(n int) byte {
return 0 return 0
} }
// Cells is always a fixed size; use At to get a cell according to x,y,z func (g *GameMap) At(x, y, z int) *Cell {
type Cells []Cell return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
func (c Cells) At(x, y, z int) Cell {
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
} }
func (h Header) Check() []error { func (g *GameMap) Check() error {
var out []error if bytes.Compare(expectedMagic, g.Magic) != 0 {
if h.IsCampaignMap > 1 { return fmt.Errorf("Unexpected magic value: %v", g.Magic)
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
} }
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 { // TODO: other consistency checks
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
}
return out return nil
}
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
} }
func (m *GameMap) Rect() image.Rectangle { func (m *GameMap) Rect() image.Rectangle {
return image.Rect( return image.Rect(
int(m.Header.MinWidth), int(m.MinWidth),
int(m.Header.MinLength), int(m.MinLength),
int(m.Header.MaxWidth), int(m.MaxWidth),
int(m.Header.MaxLength), int(m.MaxLength),
) )
} }
@@ -243,23 +220,17 @@ func LoadGameMap(prefix string) (*GameMap, error) {
return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix) 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) { func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
out, err := loadMapFile(mapFile) out, err := loadMapFile(mapFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: load text and parse into sections if err := out.Check(); err != nil {
txt, err := ioutil.ReadFile(txtFile)
if err != nil {
return nil, err return nil, err
} }
out.Text = string(txt)
for _, err := range out.Check() {
log.Printf("%s: %v", mapFile, err)
}
return out, nil return out, nil
} }
@@ -297,6 +268,7 @@ func LoadGameMaps(dir string) (map[string]*GameMap, error) {
func loadMapFile(filename string) (*GameMap, error) { func loadMapFile(filename string) (*GameMap, error) {
var out GameMap var out GameMap
out.NumCells = cellCount
mf, err := os.Open(filename) mf, err := os.Open(filename)
if err != nil { if err != nil {
@@ -312,52 +284,18 @@ func loadMapFile(filename string) (*GameMap, error) {
defer zr.Close() defer zr.Close()
if err := binary.Read(zr, binary.LittleEndian, &out.Header); err != nil { if err := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); 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 {
return nil, err return nil, err
} }
out.Cells = make(Cells, cellCount) // Trim any trailing nulls off of the strings
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil { trimRight(&out.SetName)
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err) trimRight(&out.Title)
} trimRight(&out.Briefing)
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerHeader); err != nil {
return nil, fmt.Errorf("Error parsing trailer header for %s: %v", filename, err)
}
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 return &out, nil
} }
func (t *TrailerTrailer) String() string { func trimRight(s *string) {
return fmt.Sprintf( *s = strings.TrimRight(*s, "\x00")
"title=%q briefing=%q rest=%#+v",
strings.TrimRight(string(t.Title[:]), "\x00"),
strings.TrimRight(string(t.Briefing[:]), "\x00"),
t.Unknown1,
)
} }

View File

@@ -9,7 +9,7 @@ type CellPoint struct {
Z int 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) cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
return cell, CellPoint{IsoPt: s.selectedCell, Z: 0} return cell, CellPoint{IsoPt: s.selectedCell, Z: 0}
} }