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 = 0x120 // tentatively 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 } 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 } func (h Header) MapSetFilename() string { idx := bytes.IndexByte(h.SetName[:], 0) if idx < 0 { idx = 8 // all 8 bytes are used } return string(h.SetName[0:idx:idx]) + ".set" } type Cell struct { DoorAndCanisterRelated byte DoorLockAndReactorRelated byte Unknown2 byte Object0SurfaceArea byte Unknown4 byte Object1LeftArea byte Unknown6 byte Object2RightArea byte Unknown8 byte Object3CenterArea byte Unknown10 byte Unknown11 byte Unknown12 byte Unknown13 byte Unknown14 byte SquadRelated byte } func (c *Cell) At(n int) byte { switch n { case 0: return c.DoorAndCanisterRelated case 1: return c.DoorLockAndReactorRelated case 2: return c.Unknown2 case 3: return c.Object0SurfaceArea case 4: return c.Unknown4 case 5: return c.Object1LeftArea case 6: return c.Unknown6 case 7: return c.Object2RightArea case 8: return c.Unknown8 case 9: return c.Object3CenterArea case 10: return c.Unknown10 case 11: return c.Unknown11 case 12: return c.Unknown12 case 13: return c.Unknown13 case 14: return c.Unknown14 case 15: return c.SquadRelated } return 0 } // Cells is always a fixed size; use At to get a cell according to x,y,z type Cells []Cell // FIXME: Ordering may be incorrect? I assume z,y,x for now... func (c Cells) At(x, y, z int) Cell { return c[(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)) } 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, cellCount) if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil { return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err) } return &out, nil }