package maps import ( "bytes" "compress/gzip" "encoding/binary" "fmt" "image" "io/ioutil" "os" "path/filepath" "strings" "github.com/lunixbochs/struc" ) 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 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 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 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 { Unknown1 int `struc:"uint32"` } type Thingies []Thingy func (g *GameMap) MapSetName() string { return g.SetName } func (g *GameMap) MapSetFilename() string { return g.MapSetName() + ".set" } type ObjRef struct { AreaByte byte `struc:"byte"` SpriteAndFlagByte byte `struc:"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 } 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 } func (g *GameMap) At(x, y, z int) *Cell { return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x] } func (g *GameMap) Check() error { if bytes.Compare(expectedMagic, g.Magic) != 0 { return fmt.Errorf("Unexpected magic value: %v", g.Magic) } // TODO: other consistency checks return nil } func (m *GameMap) Rect() image.Rectangle { return image.Rect( int(m.MinWidth), int(m.MinLength), int(m.MaxWidth), int(m.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. 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 } if err := out.Check(); err != nil { return nil, 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 out.NumCells = cellCount 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 := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); err != nil { return nil, err } // Trim any trailing nulls off of the strings trimRight(&out.SetName) trimRight(&out.Title) trimRight(&out.Briefing) return &out, nil } func trimRight(s *string) { *s = strings.TrimRight(*s, "\x00") }