From cf624cc77b11f1b0127f8a27124b2c708056d640 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 9 Jun 2020 00:35:57 +0100 Subject: [PATCH] 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. --- cmd/loader/main.go | 9 +- cmd/view-minimap/main.go | 4 +- go.mod | 3 + go.sum | 6 + internal/assetstore/map.go | 6 +- internal/maps/maps.go | 260 ++++++++++++++---------------------- internal/scenario/manage.go | 2 +- 7 files changed, 118 insertions(+), 172 deletions(-) diff --git a/cmd/loader/main.go b/cmd/loader/main.go index 440db0f..4a938a4 100644 --- a/cmd/loader/main.go +++ b/cmd/loader/main.go @@ -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), ) } } diff --git a/cmd/view-minimap/main.go b/cmd/view-minimap/main.go index 7a80be5..e784863 100644 --- a/cmd/view-minimap/main.go +++ b/cmd/view-minimap/main.go @@ -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)) } } diff --git a/go.mod b/go.mod index c602e83..214ca20 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 92f91d7..be83799 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/assetstore/map.go b/internal/assetstore/map.go index 5a3f053..b38a946 100644 --- a/internal/assetstore/map.go +++ b/internal/assetstore/map.go @@ -47,7 +47,7 @@ func (a *AssetStore) Map(name string) (*Map, error) { } m := &Map{ - Rect: raw.Rect(), + Rect: raw.Rect(), assets: a, raw: raw, set: set, @@ -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. diff --git a/internal/maps/maps.go b/internal/maps/maps.go index 253b321..b6f92f8 100644 --- a/internal/maps/maps.go +++ b/internal/maps/maps.go @@ -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,105 @@ 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:"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 { - 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 int `struc:"uint32"` + // 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 +139,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 +178,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 +220,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 +268,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 +284,18 @@ 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 + trimRight(&out.SetName) + 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 } -func (t *TrailerTrailer) 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, - ) +func trimRight(s *string) { + *s = strings.TrimRight(*s, "\x00") } diff --git a/internal/scenario/manage.go b/internal/scenario/manage.go index ed08380..168e6e1 100644 --- a/internal/scenario/manage.go +++ b/internal/scenario/manage.go @@ -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} }