Files
ordoor/internal/assetstore/assetstore.go

147 lines
3.2 KiB
Go

package assetstore
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"code.ur.gs/lupine/ordoor/internal/data"
)
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
cursorObj *Object
cursors map[CursorName]*Cursor
fonts map[string]*Font
generic *data.Generic
maps map[string]*Map
menus map[string]*Menu
objs map[string]*Object
sets map[string]*Set
sounds map[string]*Sound
strings *data.I18n
}
// 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.cursorObj = nil
a.cursors = make(map[CursorName]*Cursor)
a.entries = newEntryMap
a.fonts = make(map[string]*Font)
a.maps = make(map[string]*Map)
a.menus = make(map[string]*Menu)
a.objs = make(map[string]*Object)
a.sets = make(map[string]*Set)
a.sounds = make(map[string]*Sound)
a.strings = nil
return nil
}
func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
var filename string
if ext != "" {
filename = canonical(name + "." + ext)
} else {
filename = canonical(name)
}
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
}