373 lines
8.4 KiB
Go
373 lines
8.4 KiB
Go
package maps
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
expectedMagic = []byte("\x15\x00AMB_MAP\x00")
|
|
expectedSetNameOffset = uint32(0x10)
|
|
notImplemented = fmt.Errorf("Not implemented")
|
|
)
|
|
|
|
const (
|
|
MaxHeight = 7 // Z coordinate
|
|
MaxLength = 100 // Y coordinate
|
|
MaxWidth = 130 // X coordinate
|
|
|
|
CellSize = 13 // seems to be
|
|
|
|
cellDataOffset = 0xc0
|
|
cellCount = MaxHeight * MaxLength * MaxWidth
|
|
)
|
|
|
|
type Header struct {
|
|
Magic [10]byte // "\x15\x00AMB_MAP\x00"
|
|
SetName [8]byte // Links to a filename in `/Sets/*.set`
|
|
// Need to investigate the rest of the header too
|
|
IsCampaignMap byte
|
|
}
|
|
|
|
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) 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 & 0x7f)
|
|
}
|
|
|
|
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
|
|
} // PARIS is 78 x 60 x 7
|
|
// 4E 3C 7
|
|
/*
|
|
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
|
|
}*/
|
|
|
|
type Cell struct {
|
|
Unknown1 byte
|
|
Surface ObjRef
|
|
Left ObjRef
|
|
Right ObjRef
|
|
Center ObjRef
|
|
Unknown2 [4]byte
|
|
|
|
/*
|
|
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.Unknown1
|
|
case 1:
|
|
return c.Surface.AreaByte
|
|
case 2:
|
|
return c.Surface.SpriteAndFlagByte
|
|
case 3:
|
|
return c.Left.AreaByte
|
|
case 4:
|
|
return c.Left.SpriteAndFlagByte
|
|
case 5:
|
|
return c.Right.AreaByte
|
|
case 6:
|
|
return c.Right.SpriteAndFlagByte
|
|
case 7:
|
|
return c.Center.AreaByte
|
|
case 8:
|
|
return c.Center.SpriteAndFlagByte
|
|
case 9:
|
|
return c.Unknown2[0]
|
|
case 10:
|
|
return c.Unknown2[1]
|
|
case 11:
|
|
return c.Unknown2[2]
|
|
case 12:
|
|
return c.Unknown2[3]
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// Cells is always a fixed size; use At to get a cell according to x,y,z
|
|
type Cells []Cell
|
|
|
|
// 6 Possibilities for being laid out in memory. Most likely:
|
|
// XXYYZZ
|
|
// OR
|
|
// XYZXYZ
|
|
|
|
func (c Cells) At(x, y, z int) Cell {
|
|
// log.Printf("At (%v,%v,%v)=%v", x, y, z, x*y*z)
|
|
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.TrailerHeader.MinWidth),
|
|
int(m.TrailerHeader.MinLength),
|
|
int(m.TrailerHeader.MaxWidth-1),
|
|
int(m.TrailerHeader.MaxLength-1),
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// no gzip.SeekReader, so discard unread trailer bytes for now
|
|
if _, err := io.CopyN(ioutil.Discard, zr, int64(3320-2)); err != nil { // observed
|
|
return nil, 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,
|
|
)
|
|
}
|