package d2loader import ( "fmt" "io" "path/filepath" "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/mpq" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/filesystem" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" ) const ( defaultCacheBudget = 1024 * 1024 * 512 errFmtFileNotFound = "file not found: %s" ) const ( logPrefix = "File Loader" ) const ( fontToken = d2resource.LanguageFontToken tableToken = d2resource.LanguageTableToken ) // NewLoader creates a new loader func NewLoader(l d2util.LogLevel) (*Loader, error) { loader := &Loader{ LoaderProviders: make(map[types.SourceType]func(path string) (asset.Source, error), 2), } loader.LoaderProviders[types.AssetSourceMPQ] = mpq.NewSource loader.LoaderProviders[types.AssetSourceFileSystem] = filesystem.OnAddSource loader.Cache = d2cache.CreateCache(defaultCacheBudget) loader.Logger = d2util.NewLogger() loader.Logger.SetPrefix(logPrefix) loader.Logger.SetLevel(l) return loader, nil } // Loader represents the manager that handles loading and caching assets with the asset Sources // that have been added type Loader struct { language *string charset *string d2interface.Cache *d2util.Logger LoaderProviders map[types.SourceType]func(path string) (asset.Source, error) Sources []asset.Source } // SetLanguage sets the language for loader func (l *Loader) SetLanguage(language *string) { l.language = language } // SetCharset sets the charset for loader func (l *Loader) SetCharset(charset *string) { l.charset = charset } // Load attempts to load an asset with the given sub-path. The sub-path is relative to the root // of each asset source root (regardless of the type of asset source) func (l *Loader) Load(subPath string) (io.ReadSeeker, error) { subPath = filepath.Clean(subPath) if l.language != nil { charset := l.charset language := l.language subPath = strings.ReplaceAll(subPath, fontToken, *charset) subPath = strings.ReplaceAll(subPath, tableToken, *language) } // if it isn't in the cache, we check if each source can open the file for idx := range l.Sources { source := l.Sources[idx] // if the source can open the file, then we cache it and return it loadedAsset, err := source.Open(subPath) if err != nil { l.Debug(fmt.Sprintf("Checked `%s`, file not found", source.Path())) continue } srcBase, _ := filepath.Abs(source.Path()) l.Info(fmt.Sprintf("Loaded %s -> %s", srcBase, subPath)) return loadedAsset, nil } return nil, fmt.Errorf(errFmtFileNotFound, subPath) } // AddSource adds an asset source with the given path. The path will either resolve to a directory // or a file on the host filesystem. In the case that it is a file, the file extension is used // to determine the type of asset source. In the case that the path points to a directory, a // FileSystemSource will be added. func (l *Loader) AddSource(path string, sourceType types.SourceType) error { if l.Sources == nil { l.Sources = make([]asset.Source, 0) } cleanPath := filepath.Clean(path) source, err := l.LoaderProviders[sourceType](cleanPath) if err != nil { return err } l.Infof("Adding source: '%s'", cleanPath) l.Sources = append(l.Sources, source) return nil } // Exists checks if the given path exists in at least one source func (l *Loader) Exists(subPath string) bool { subPath = filepath.Clean(subPath) if l.language != nil { charset := l.charset language := l.language subPath = strings.ReplaceAll(subPath, fontToken, *charset) subPath = strings.ReplaceAll(subPath, tableToken, *language) } // if it isn't in the cache, we check if each source can open the file for idx := range l.Sources { source := l.Sources[idx] // if the source can open the file, then we cache it and return it if source.Exists(subPath) { return true } } return false }