package data import ( "encoding/binary" "fmt" "image" "image/color" "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]|s.Unknown1[1]|s.Unknown1[2]|s.Unknown1[3] > 0 { 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(palette color.Palette) *image.Paletted { return &image.Paletted{ Pix: s.Data, Stride: int(s.Width), Rect: image.Rect(0, 0, int(s.Width), int(s.Height)), Palette: palette, } } // 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 }