This makes menus display more correctly, and also fixes trees and other objects on the main map, although it messes up bounds clipping (sigh).
223 lines
4.9 KiB
Go
223 lines
4.9 KiB
Go
package data
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"io/ioutil"
|
|
"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 // Size of PixelData, excluding this sprite header
|
|
Padding2 uint64 // 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)
|
|
}
|
|
|
|
// 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
|
|
}
|