package maps import ( "bytes" "compress/gzip" "encoding/binary" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "strings" ) var ( expectedMagic = []byte("\x08\x00WHMAP\x00") expectedSetNameOffset = uint32(0x34) notImplemented = fmt.Errorf("Not implemented") ) const ( MaxHeight = 7 // Z coordinate MaxLength = 100 // Y coordinate MaxWidth = 130 // X coordinate CellSize = 16 // seems to be cellDataOffset = 0x100 // tentatively cellDataSize = MaxHeight * MaxLength * MaxWidth * CellSize ) 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 } func (h Header) Width() int { return int(h.MaxWidth - h.MinWidth) } func (h Header) Length() int { return int(h.MaxLength - h.MinLength) } func (h Header) Height() int { return MaxHeight } type Cell []byte // FIXME: need to deconstruct this into the various fields // Cells is always a fixed size; use At to get a cell according to x,y,z type Cells []byte // FIXME: Ordering may be incorrect? I assume z,y,x for now... func (c Cells) At(x, y, z int) Cell { start := (z * MaxLength * MaxWidth * CellSize) + (y * MaxWidth * CellSize) + (x * CellSize) end := start + CellSize return Cell(c[start:end:end]) } 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)) } if bytes.Compare(expectedMagic, h.Magic[:]) != 0 { out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic)) } return out } type GameMap struct { Header Cells // TODO: parse this into sections Text string } // A game map contains a .txt and a .map. If they're in the same directory, // just pass the directory + basename to load both func LoadGameMap(prefix string) (*GameMap, error) { for _, txtExt := range []string{".TXT", ".txt"} { for _, mapExt := range []string{".MAP", ".map"} { out, err := LoadGameMapByFiles(prefix+mapExt, prefix+txtExt) if err != nil && !os.IsNotExist(err) { return nil, err } if err == nil { return out, nil } } } 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 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 { return nil, err } out.Text = string(txt) for _, err := range out.Check() { log.Printf("%s: %v", mapFile, err) } return out, nil } func LoadGameMaps(dir string) (map[string]*GameMap, error) { fis, err := ioutil.ReadDir(dir) if err != nil { return nil, err } out := make(map[string]*GameMap) for _, fi := range fis { filename := filepath.Join(dir, fi.Name()) basename := filepath.Base(filename) extname := filepath.Ext(filename) // Only pay attention to .MAP files. Assume they will have a .txt too if !strings.EqualFold(extname, ".MAP") { continue } prefix := filename[0 : len(filename)-4] gameMap, err := LoadGameMap(prefix) if err != nil { return nil, fmt.Errorf("Error processing %v: %s", filename, err) } out[basename] = gameMap } return out, nil } func loadMapFile(filename string) (*GameMap, error) { var out GameMap mf, err := os.Open(filename) if err != nil { return nil, err } defer mf.Close() zr, err := gzip.NewReader(mf) if err != nil { return nil, err } 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 { return nil, err } out.Cells = make(Cells, cellDataSize) if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil { return nil, fmt.Errorf("Error parsing %s: %v", filename, err) } return &out, nil }