package assetstore import ( "fmt" "image/color" "io/ioutil" "os" "path/filepath" "strings" "github.com/hajimehoshi/ebiten" "code.ur.gs/lupine/ordoor/internal/config" "code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/idx" "code.ur.gs/lupine/ordoor/internal/palettes" ) 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 for that game. // // 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. // // To mix assets from different games, either construct a synthetic directory // or instantiate two separate asset stores. type AssetStore struct { RootDir string Palette color.Palette // 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 aniObj *Object cursorObj *Object cursors map[CursorName]*Cursor fonts map[string]*Font generic *data.Generic hasAction *data.HasAction idx *idx.Idx images map[string]*ebiten.Image 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(engine *config.Engine) (*AssetStore, error) { if engine == nil { return nil, fmt.Errorf("Unconfigured engine passed to assetstore") } palette, ok := palettes.Get(engine.Palette) if !ok { return nil, fmt.Errorf("Couldn't find palette %q for engine", engine.Palette) } store := &AssetStore{ RootDir: engine.DataDir, Palette: palette, } // 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[""] = 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.aniObj = nil a.cursorObj = nil a.cursors = make(map[CursorName]*Cursor) a.entries = newEntryMap a.fonts = make(map[string]*Font) a.generic = nil a.hasAction = nil a.idx = nil a.images = make(map[string]*ebiten.Image) 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[""][dir] return filepath.Join(a.RootDir, actualDir, file), nil } } } return "", fmt.Errorf("file %q does not exist", filename) } 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 }