356 lines
8.4 KiB
Go
356 lines
8.4 KiB
Go
package maps
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/lunixbochs/struc"
|
|
|
|
"code.ur.gs/lupine/ordoor/internal/data"
|
|
)
|
|
|
|
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:"byte"`
|
|
TrailerUnknown5 []byte `struc:"[3]byte"`
|
|
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
|
|
TrailerUnknown6 []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 []byte `struc:"[178]byte"`
|
|
Type data.CharacterType `struc:"byte"`
|
|
Name string `struc:"[80]byte"`
|
|
|
|
// Attributes guessed by matching up numbers. Starts at 0x103
|
|
WeaponSkill int `struc:"byte"`
|
|
BallisticSkill int `struc:"byte"`
|
|
Unknown2 byte `struc:"byte"`
|
|
Leadership int `struc:"byte"`
|
|
Toughness int `struc:"byte"`
|
|
Strength int `struc:"byte"`
|
|
ActionPoints int `struc:"byte"`
|
|
Unknown3 byte `struc:"byte"`
|
|
Unknown4 byte `struc:"byte"`
|
|
Health int `struc:"byte"`
|
|
|
|
Unknown5 []byte `struc:"[91]byte"`
|
|
Armor int `struc:"byte"`
|
|
Unknown6 []byte `struc:"[84]byte"`
|
|
YPos int `struc:"byte"` // These are actually much more complicated
|
|
XPos int `struc:"byte"`
|
|
Unknown7 []byte `struc:"[317]byte"`
|
|
SquadNumber byte `struc:"byte"`
|
|
Unknown8 []byte `struc:"[927]byte"`
|
|
// 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
|
|
nullTerminate(&out.SetName)
|
|
nullTerminate(&out.Title)
|
|
nullTerminate(&out.Briefing)
|
|
|
|
for i, _ := range out.Characters {
|
|
chr := &out.Characters[i]
|
|
nullTerminate(&chr.Name)
|
|
fmt.Printf("Character %v: %s\n", i, chr.String())
|
|
}
|
|
|
|
fmt.Printf("Mission Title: %q\n", out.Title)
|
|
fmt.Printf("Mission Briefing: %q\n", out.Briefing)
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func nullTerminate(s *string) {
|
|
sCpy := *s
|
|
idx := strings.Index(sCpy, "\x00")
|
|
if idx < 0 {
|
|
return
|
|
}
|
|
|
|
*s = sCpy[0:idx]
|
|
}
|
|
|
|
func (c *Character) String() string {
|
|
return fmt.Sprintf(
|
|
"squad=%v pos=(%v,%v) type=%q name=%q\n"+
|
|
"\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n",
|
|
c.SquadNumber,
|
|
c.XPos, c.YPos,
|
|
c.Type.String(),
|
|
c.Name,
|
|
c.ActionPoints, c.Health, c.Armor, c.BallisticSkill, c.WeaponSkill,
|
|
c.Strength, c.Toughness /*c.Initiative, c.Attacks,*/, c.Leadership,
|
|
)
|
|
}
|