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 = 0x110 // 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 ObjRef struct { AreaByte byte FrameAndUnknownByte byte } // The index into a set palette to retrieve the object func (o ObjRef) Index() int { return int(o.AreaByte) } func (o ObjRef) Frame() int { return int(o.FrameAndUnknownByte - 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: return c.DoorAndCanisterRelated case 1: return c.DoorLockAndReactorRelated case 2: return c.Unknown2 case 3: return c.Surface.AreaByte case 4: return c.Surface.FrameAndUnknownByte case 5: return c.Left.AreaByte case 6: return c.Left.FrameAndUnknownByte case 7: return c.Right.AreaByte case 8: return c.Right.FrameAndUnknownByte case 9: return c.Center.AreaByte case 10: return c.Center.FrameAndUnknownByte 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 }