`WarHammer.ani` turns out to be a regular `obj` file; `WarHammer.idx` is partially decoded, but I'm struggling to link it to the former in a reasonable way at the moment.
231 lines
5.2 KiB
Go
231 lines
5.2 KiB
Go
package data
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"code.ur.gs/lupine/ordoor/internal/data/rle"
|
|
)
|
|
|
|
type SpriteHeader struct {
|
|
XOffset uint16
|
|
YOffset uint16
|
|
Width uint16
|
|
Height uint16
|
|
Padding1 uint32 // I don't think this is used. Could be wrong.
|
|
PixelSize uint32
|
|
Unknown1 [4]byte // ??? Only observed in `WarHammer.ani` so far
|
|
Padding2 uint32 // I don't think this is used either. Could be wrong.
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// TODO: WarHammer.ani sets Unknown1 to this for all 188,286 sprites. I am
|
|
// very interested in seeing if there are any others
|
|
if s.Unknown1[0] != 212 || s.Unknown1[1] != 113 || s.Unknown1[2] != 59 || s.Unknown1[3] != 1 {
|
|
log.Printf("Value of Unknown1 field: %v", s.Unknown1)
|
|
}
|
|
|
|
// Remove 24 bytes from passed-in size to account for the header
|
|
if s.PixelSize != expectedSize-24 {
|
|
return fmt.Errorf("Advertised pixel size: %d differs from expected: %v", s.PixelSize, expectedSize-24)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Sprite struct {
|
|
SpriteHeader
|
|
|
|
Data []byte
|
|
}
|
|
|
|
func (s *Sprite) ToImage() *image.Paletted {
|
|
return &image.Paletted{
|
|
Pix: s.Data,
|
|
Stride: int(s.Width),
|
|
Rect: image.Rect(0, 0, int(s.Width), int(s.Height)),
|
|
Palette: ColorPalette,
|
|
}
|
|
}
|
|
|
|
// dirEntry totals 8 bytes on disk
|
|
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
|
|
}
|
|
|
|
func (d dirEntry) Check() error {
|
|
if d.Size < 24 {
|
|
return fmt.Errorf("Unexpected sprite size: %d (expected >= 24)", d.Size)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ObjectHeader totals 24 bytes on disk
|
|
type ObjectHeader struct {
|
|
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
|
|
}
|
|
|
|
func (h ObjectHeader) ExpectedDirSize() uint32 {
|
|
return h.NumSprites * 8
|
|
}
|
|
|
|
func (h ObjectHeader) Check() error {
|
|
// TODO: check for overlaps
|
|
|
|
if h.ExpectedDirSize() != h.DirSize {
|
|
return fmt.Errorf("Unexpected sprite directory size: %d (expected %d)", h.DirSize, h.ExpectedDirSize())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Object struct {
|
|
ObjectHeader
|
|
|
|
Filename string
|
|
Name string // left blank for use by you
|
|
Sprites []*Sprite
|
|
}
|
|
|
|
func LoadObjectLazily(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, fmt.Errorf("Reading object header: %v", err)
|
|
}
|
|
|
|
if err := out.ObjectHeader.Check(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out.Sprites = make([]*Sprite, int(out.NumSprites))
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func LoadObject(filename string) (*Object, error) {
|
|
obj, err := LoadObjectLazily(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := obj.LoadAllSprites(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return obj, nil
|
|
}
|
|
|
|
func (o *Object) LoadAllSprites() error {
|
|
for i := 0; i < int(o.NumSprites); i++ {
|
|
if err := o.LoadSprite(i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (o *Object) LoadSprite(idx int) error {
|
|
if idx < 0 || idx >= int(o.NumSprites) {
|
|
return fmt.Errorf("Asked for idx %v of %v", idx, o.NumSprites)
|
|
}
|
|
|
|
f, err := os.Open(o.Filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
var entry dirEntry
|
|
|
|
if _, err := f.Seek(int64(o.DirOffset)+int64(idx*8), io.SeekStart); err != nil {
|
|
return fmt.Errorf("Seeking to sprite directory entry %v: %v", idx, err)
|
|
}
|
|
|
|
if err := binary.Read(f, binary.LittleEndian, &entry); err != nil {
|
|
return fmt.Errorf("Reading sprite directory entry %v: %v", idx, err)
|
|
}
|
|
|
|
if err := entry.Check(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := f.Seek(int64(o.DataOffset+entry.Offset), io.SeekStart); err != nil {
|
|
return fmt.Errorf("Seeking to sprite %v: %v", idx, err)
|
|
}
|
|
|
|
sprite := &Sprite{}
|
|
|
|
if err := binary.Read(f, binary.LittleEndian, &sprite.SpriteHeader); err != nil {
|
|
return fmt.Errorf("Reading sprite %v header: %v", idx, err)
|
|
}
|
|
|
|
if err := sprite.Check(entry.Size); err != nil {
|
|
return err
|
|
}
|
|
|
|
buf := io.LimitReader(f, int64(sprite.PixelSize))
|
|
sprite.Data = make([]byte, int(sprite.Height)*int(sprite.Width))
|
|
|
|
// The pixel data is RLE-compressed. Uncompress it here.
|
|
if err := rle.Expand(buf, sprite.Data); err != nil {
|
|
return err
|
|
}
|
|
|
|
o.Sprites[idx] = sprite
|
|
return 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
|
|
}
|
|
|
|
obj, err := LoadObject(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %v", filename, err)
|
|
}
|
|
|
|
out[basename] = obj
|
|
}
|
|
|
|
return out, nil
|
|
}
|