Files
ordoor/internal/maps/maps.go

302 lines
6.8 KiB
Go
Raw Normal View History

2018-03-17 04:15:40 +00:00
package maps
import (
"bytes"
"compress/gzip"
"encoding/binary"
"fmt"
2020-05-20 01:43:40 +01:00
"image"
2018-03-17 04:15:40 +00:00
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/lunixbochs/struc"
2018-03-17 04:15:40 +00:00
)
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
2018-03-18 13:57:01 +00:00
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"`
2020-06-08 00:24:57 +01:00
}
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
}
2020-06-08 00:24:57 +01:00
type Characters []Character
// TODO. These are triggers/reactors/etc.
type Thingy struct {
Unknown1 int `struc:"uint32"`
}
type Thingies []Thingy
2018-03-18 13:57:01 +00:00
func (g *GameMap) MapSetName() string {
return g.SetName
}
func (g *GameMap) MapSetFilename() string {
return g.MapSetName() + ".set"
2018-03-18 13:57:01 +00:00
}
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
}
2018-03-18 13:57:01 +00:00
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
2018-03-18 13:57:01 +00:00
case 4:
return c.Surface.SpriteAndFlagByte
2018-03-18 13:57:01 +00:00
case 5:
return c.Left.AreaByte
2018-03-18 13:57:01 +00:00
case 6:
return c.Left.SpriteAndFlagByte
2018-03-18 13:57:01 +00:00
case 7:
return c.Right.AreaByte
2018-03-18 13:57:01 +00:00
case 8:
return c.Right.SpriteAndFlagByte
2018-03-18 13:57:01 +00:00
case 9:
return c.Center.AreaByte
2018-03-18 13:57:01 +00:00
case 10:
return c.Center.SpriteAndFlagByte
2018-03-18 13:57:01 +00:00
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]
2018-03-17 04:15:40 +00:00
}
func (g *GameMap) Check() error {
if bytes.Compare(expectedMagic, g.Magic) != 0 {
return fmt.Errorf("Unexpected magic value: %v", g.Magic)
2018-03-17 04:15:40 +00:00
}
// TODO: other consistency checks
2018-03-17 04:15:40 +00:00
return nil
2018-03-17 04:15:40 +00:00
}
2020-05-20 01:43:40 +01:00
func (m *GameMap) Rect() image.Rectangle {
return image.Rect(
int(m.MinWidth),
int(m.MinLength),
int(m.MaxWidth),
int(m.MaxLength),
2020-05-20 01:43:40 +01:00
)
}
2018-03-17 04:15:40 +00:00
// 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"} {
2018-03-17 04:15:40 +00:00
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.
2018-03-17 04:15:40 +00:00
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
out, err := loadMapFile(mapFile)
if err != nil {
return nil, err
}
if err := out.Check(); err != nil {
2018-03-17 04:15:40 +00:00
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
2018-03-17 04:15:40 +00:00
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)
2020-06-08 00:24:57 +01:00
2018-03-17 04:15:40 +00:00
return &out, nil
}
2020-06-08 00:24:57 +01:00
func trimRight(s *string) {
*s = strings.TrimRight(*s, "\x00")
2020-06-08 00:24:57 +01:00
}