2018-02-24 13:50:35 +00:00
|
|
|
package data
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/binary"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// FIXME: My poor understanding of the .obj format prevents me from
|
|
|
|
// successfully loading these files
|
|
|
|
objBlacklist = []string{
|
|
|
|
/* Lots of unexpected magic values, disable the check for now
|
|
|
|
"15_rocks.obj", // Unexpected magic value: 19726564 (expected 21102801)
|
|
|
|
"2_cath.obj", // Unexpected magic value: 21364973 (expected 21102801)
|
|
|
|
"2nd_flor.obj", // Unexpected magic value: 22151377 (expected 21102801)
|
|
|
|
"3_cath.obj", // Unexpected magic value: 21102809 (expected 21102801)
|
|
|
|
"4_cath.obj", // Unexpected magic value: 21496017 (expected 21102801)
|
|
|
|
"BODIES.obj", // Unexpected magic value: 21627103 (expected 21102801)
|
|
|
|
"BRDG_TIL.OBJ", // Unexpected magic value: 17957074 (expected 21102801)
|
|
|
|
"Cheveron.obj", // Unexpected magic value: 10879118 (expected 21102801)
|
|
|
|
|
|
|
|
"Heavy_Plasma_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Heavy_Plasma_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Heavy_Plasma_Pain_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Heavy_Plasma_Pain_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
|
|
|
|
"Ht_drt.obj", // Unexpected magic value: 22806737 (expected 21102801)
|
|
|
|
"Ht_grs.obj", // Unexpected magic value: 22806737 (expected 21102801)
|
|
|
|
|
|
|
|
"IVY02.OBJ", // Unexpected magic value: 18481399 (expected 21102801)
|
|
|
|
"J_top2.obj", // Unexpected magic value: 22347993 (expected 21102801)
|
|
|
|
|
|
|
|
"Lascannon_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Lascannon_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Lascannon_Pain_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Lascannon_Pain_Mask.obj", // Unexpected magic value: 14811136 (expected 21102801)
|
|
|
|
|
|
|
|
"Man_Shadow.obj", // Unexpected magic value: 22479091 (expected 21102801)
|
|
|
|
|
|
|
|
"Melta_Effect.obj", // Unexpected magic value: 14745777 (expected 21102801)
|
|
|
|
"Melta_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Melta_Pain_Effect.obj", // Unexpected magic value: 14745777 (expected 21102801)
|
|
|
|
"Melta_Pain_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
|
|
|
|
"Multi_Melta_Effect.obj",
|
|
|
|
"Multi_Melta_Mask.obj",
|
|
|
|
"Multi_Melta_Pain_Effect.obj",
|
|
|
|
"Multi_Melta_Pain_Mask.obj",
|
|
|
|
|
|
|
|
"Plasma_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Plasma_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Plasma_Pain_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
"Plasma_Pain_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
|
|
|
|
|
|
|
"TZEENTCH.OBJ", // Unexpected magic value: 18088209 (expected 21102801)
|
|
|
|
|
|
|
|
"altar.obj", // Unexpected magic value: 18219222 (expected 21102801)
|
|
|
|
*/
|
|
|
|
|
|
|
|
"j_tree2.obj", // ObjectHeader is completely empty
|
|
|
|
"inven.obj", // Main header padding contains unknown values: [134744072 134744072 134744072]
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
sort.Strings(objBlacklist)
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
type SpriteHeader struct {
|
|
|
|
Unknown0 uint32
|
|
|
|
Width uint16 // FIXME: I'm not certain this is what these are.
|
|
|
|
Height uint16 // FIXME: If they are, they may be the wrong way around
|
2018-02-24 13:50:35 +00:00
|
|
|
Padding1 uint32 // I don't think this is used. Could be wrong.
|
2018-03-21 05:08:24 +00:00
|
|
|
PixelSize uint32 // Size of PixelData, excluding this sprite header
|
2018-02-24 13:50:35 +00:00
|
|
|
Padding2 uint64 // I don't think this is used either. Could be wrong.
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
func (s SpriteHeader) Check(expectedSize uint32) error {
|
|
|
|
if s.Padding1 != 0 || s.Padding2 != 0 {
|
|
|
|
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove 24 bytes from passed-in size to account for the header
|
2018-03-21 05:08:24 +00:00
|
|
|
if s.PixelSize != expectedSize-24 {
|
|
|
|
return fmt.Errorf("Advertised pixel size: %d differs from expected: %v", s.PixelSize, expectedSize-24)
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
type Sprite struct {
|
|
|
|
SpriteHeader
|
2018-02-24 13:50:35 +00:00
|
|
|
|
|
|
|
PixelData []byte
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
type dirEntry struct {
|
|
|
|
Offset uint32 // Offset of the sprite relative to the data block
|
|
|
|
Size uint32 // Size of the sprite in bytes, including any header
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
func (d dirEntry) Check() error {
|
|
|
|
if d.Size < 24 {
|
|
|
|
return fmt.Errorf("Unexpected sprite size: %d (expected >= 24)", d.Size)
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
// ObjectHeader totals 24 bytes on disk
|
2018-02-24 13:50:35 +00:00
|
|
|
type ObjectHeader struct {
|
2018-03-21 05:08:24 +00:00
|
|
|
NumSprites uint32 // How many sprites does this object have?
|
|
|
|
DirOffset uint32 // Offset of the directory block
|
|
|
|
DirSize uint32 // Size of the directory block. 8 * NumSprites
|
|
|
|
DataOffset uint32 // Offset of the sprite data block
|
|
|
|
DataSize uint32 // Size of the sprite data block
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
func (h ObjectHeader) ExpectedDirSize() uint32 {
|
|
|
|
return h.NumSprites * 8
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (h ObjectHeader) Check() error {
|
2018-03-21 05:08:24 +00:00
|
|
|
// TODO: check for overlaps
|
2018-02-24 13:50:35 +00:00
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
if h.ExpectedDirSize() != h.DirSize {
|
|
|
|
return fmt.Errorf("Unexpected sprite directory size: %d (expected %d)", h.DirSize, h.ExpectedDirSize())
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type Object struct {
|
|
|
|
ObjectHeader
|
|
|
|
|
|
|
|
Filename string
|
2018-03-21 05:08:24 +00:00
|
|
|
Sprites []*Sprite
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func LoadObject(filename string) (*Object, error) {
|
|
|
|
f, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
out := &Object{Filename: filename}
|
|
|
|
if err := binary.Read(f, binary.LittleEndian, &out.ObjectHeader); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := out.ObjectHeader.Check(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
// Now load all sprites into memory
|
|
|
|
dir := make([]dirEntry, out.NumSprites)
|
|
|
|
if _, err := f.Seek(int64(out.DirOffset), io.SeekStart); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-02-24 13:50:35 +00:00
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
if err := binary.Read(f, binary.LittleEndian, &dir); err != nil {
|
2018-02-24 13:50:35 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
if _, err := f.Seek(int64(out.DataOffset), io.SeekStart); err != nil {
|
2018-02-24 13:50:35 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: this might - *might* - load interstitial data we don't really
|
|
|
|
// need, so wasting memory.
|
|
|
|
data, err := ioutil.ReadAll(f)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
buf := bytes.NewReader(data)
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
for _, dirEntry := range dir {
|
|
|
|
if err := dirEntry.Check(); err != nil {
|
2018-02-24 13:50:35 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
if _, err := buf.Seek(int64(dirEntry.Offset), io.SeekStart); err != nil {
|
2018-02-24 13:50:35 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
sprite := &Sprite{}
|
2018-02-24 13:50:35 +00:00
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
if err := binary.Read(buf, binary.LittleEndian, &sprite.SpriteHeader); err != nil {
|
2018-02-24 13:50:35 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
if err := sprite.Check(dirEntry.Size); err != nil {
|
2018-02-24 13:50:35 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// It's safe to assume that a `bytes.Reader` will always satisfy the
|
|
|
|
// requested read size.
|
2018-03-21 05:08:24 +00:00
|
|
|
sprite.PixelData = make([]byte, sprite.PixelSize)
|
|
|
|
if _, err := buf.Read(sprite.PixelData); err != nil {
|
2018-02-24 13:50:35 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-21 05:08:24 +00:00
|
|
|
out.Sprites = append(out.Sprites, sprite)
|
2018-02-24 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func LoadObjects(dir string) (map[string]*Object, error) {
|
|
|
|
fis, err := ioutil.ReadDir(dir)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
out := make(map[string]*Object, len(fis))
|
|
|
|
|
|
|
|
for _, fi := range fis {
|
|
|
|
filename := filepath.Join(dir, fi.Name())
|
|
|
|
basename := filepath.Base(filename)
|
|
|
|
extname := filepath.Ext(filename)
|
|
|
|
|
|
|
|
// Don't try to load non-.obj files
|
|
|
|
if !strings.EqualFold(extname, ".obj") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
i := sort.SearchStrings(objBlacklist, basename)
|
|
|
|
if i >= len(objBlacklist) || objBlacklist[i] == basename {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
obj, err := LoadObject(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("%s: %v", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
out[basename] = obj
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|