This cuts memory use significantly, since many sprites in an object are never used. We can get savings over time by evicting sprites when they go out of scope, but that's, well, out of scope. To achieve this, I introduce an assetstore package that is in charge of loading things from the filesystem. This also allows some lingering case-sensitivity issues to be handled cleanly. I'd hoped that creating fewer ebiten.Image instances would help CPU usage, but that doesn't seem to be the case.
222 lines
4.9 KiB
Go
222 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 {
|
|
Unknown0 uint32
|
|
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
|
|
}
|