package maps import ( "bytes" "compress/gzip" "encoding/binary" "fmt" "image" "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 // definitely 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 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 Character struct { Unknown1 uint32 } type Characters []Character // TODO. These are triggers/reactors/etc. type Thingy struct {} type Thingies []Thingy 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) 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 { AreaByte byte SpriteAndFlagByte byte } // The index into a set palette to retrieve the object func (o ObjRef) Index() int { return int(o.AreaByte) } func (o ObjRef) Sprite() int { // The top bit seems to be a flag of some kind return int(o.SpriteAndFlagByte & 0x7f) } // The top bit seems to say whether we should draw or not. 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: return c.DoorAndCanisterRelated case 1: return c.DoorLockAndReactorRelated case 2: return c.Unknown2 case 3: return c.Surface.AreaByte case 4: return c.Surface.SpriteAndFlagByte case 5: return c.Left.AreaByte case 6: return c.Left.SpriteAndFlagByte case 7: return c.Right.AreaByte case 8: return c.Right.SpriteAndFlagByte case 9: return c.Center.AreaByte case 10: return c.Center.SpriteAndFlagByte 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 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 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 { return image.Rect( int(m.Header.MinWidth), int(m.Header.MinLength), int(m.Header.MaxWidth), int(m.Header.MaxLength), ) } // 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) } 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, ) }