Files
ordoor/internal/assetstore/assetstore.go
Nick Thomas 5fccf97f4b Lazily load sprite image data
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.
2020-03-19 22:24:21 +00:00

126 lines
2.8 KiB
Go

package assetstore
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
const (
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
)
type entryMap map[string]map[string]string
// type AssetStore is responsible for lazily loading game data when it is
// required. Applications shouldn't need to do anything except set one of these
// up, pointing at the game dir root, to access all assets.
//
// Assets should be loaded on-demand to keep memory costs as low as possible.
// Cross-platform differences such as filename case sensitivity are also dealt
// with here.
//
// We assume the directory is read-only. You can run Refresh() if you make a
// change.
type AssetStore struct {
RootDir string
// Case-insensitive file lookup.
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
entries entryMap
// These members are used to store things we've already loaded
maps map[string]*Map
objs map[string]*Object
sets map[string]*Set
}
// New returns a new AssetStore
func New(dir string) (*AssetStore, error) {
store := &AssetStore{
RootDir: dir,
}
// fill entryMap
if err := store.Refresh(); err != nil {
return nil, err
}
return store, nil
}
func (a *AssetStore) Refresh() error {
rootEntries, err := processDir(a.RootDir)
if err != nil {
return fmt.Errorf("failed to process %v: %v", a.RootDir, err)
}
newEntryMap := make(entryMap, len(rootEntries))
newEntryMap[RootDir] = rootEntries
for lower, natural := range rootEntries {
path := filepath.Join(a.RootDir, natural)
fi, err := os.Stat(path)
if err != nil {
return fmt.Errorf("Failed to stat %v: %v", path, err)
}
if fi.IsDir() {
entries, err := processDir(path)
if err != nil {
return fmt.Errorf("Failed to process %v: %v", path, err)
}
newEntryMap[lower] = entries
}
}
// Refresh
a.entries = newEntryMap
a.maps = make(map[string]*Map)
a.objs = make(map[string]*Object)
a.sets = make(map[string]*Set)
return nil
}
func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
filename := canonical(name + "." + ext)
for _, dir := range dirs {
dir = canonical(dir)
if base, ok := a.entries[dir]; ok {
if file, ok := base[filename]; ok {
actualDir := a.entries[RootDir][dir]
return filepath.Join(a.RootDir, actualDir, file), nil
}
}
}
return "", os.ErrNotExist
}
func canonical(s string) string {
return strings.ToLower(s)
}
func processDir(dir string) (map[string]string, error) {
entries, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
out := make(map[string]string, len(entries))
for _, entry := range entries {
if entry.Name() == "." || entry.Name() == ".." {
continue
}
out[canonical(entry.Name())] = entry.Name()
}
return out, nil
}