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.
126 lines
2.8 KiB
Go
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
|
|
}
|