Files
ordoor/internal/maps/maps.go

277 lines
5.8 KiB
Go

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
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
// 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
}