1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2025-02-13 20:16:41 -05:00
OpenDiablo2/d2core/d2systems/file_handle_resolver.go
gravestench deb63a95c8 changes to d2components, d2systems, d2ui, d2enum
go.mod, go.sum:
* updating akara, bugfix in akara.EntityManager.RemoveEntity

d2core
* adding d2core/d2label
* adding d2core/d2bitmapfont

d2ui
* exporting some constants for use elsewhere

d2components
* added bitmap font component (for ui labels)
* added FileLoaded tag component to simplify asset loading filters
* added locale component
* FilePath component renamed to File
* sprite component now contains the sprite and palette path as strings
* adding ui label component

d2enum
* added locale as file type for file "/data/local/use"

d2systems
* changed most info prints to debug prints
* removed unused scene graph testing file (oops!)
* terminal is now rendered above mouse cursor scene
* adding ui widget system for use by the game object factory
* adding test scene for ui labels created with the ui widget system

d2systems/AppBootstrap
* added command line args for profiler
* `--testscene labels` launches the label test
* now adds the local file for processing
* game loop init logic now inside of Init method (the call to
world.Update does this)

d2systems/AssetLoader
* loads the locale file and adds a locale component that other systems
can use
* adds a FileLoaded component after finished loading a file which other
systems can use (like the loading scene)

d2systems/FileSourceResolver
* Now looks for and uses the locale for language/charset filepath
substitution

d2systems/GameClientBootstrap
* game loop init moved to end of AppBootstrap.Init

d2systems/GameObjectFactory
* embedding UI widget factory system

d2systems/BaseScene
* made base scene a little more clear by breaking the process into more
methods

d2systems/LoadingScene
* simplified the entity subscriptions by using the new FileLoaded
component

d2systems/SceneObjectFactory
* adding method for adding labels, buttons to scenes (buttons still WIP)

d2systems/SceneSpriteSystem
* the sprite system now maintains a cache of rendered sprites
2020-12-08 11:24:10 -08:00

232 lines
6.1 KiB
Go

package d2systems
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/gravestench/akara"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
)
const (
languageTokenFont = "{LANG_FONT}" // nolint:gosec // no security issue here...
languageTokenStringTable = "{LANG}"
)
const (
fileHandleCacheBudget = 1024
fileHandleCacheEntryWeight = 1
)
const (
logPrefixFileHandleResolver = "File Handle Resolver"
)
// FileHandleResolver is responsible for using file sources to resolve files.
// File sources are checked in the order that the sources were added.
//
// A file source can be something like an MPQ archive or a file system directory on the host machine.
//
// A file handle is a primitive representation of a loaded file; something that has data
// in the form of a byte slice, but has not been parsed into a more meaningful struct, like a DC6 animation.
type FileHandleResolver struct {
akara.BaseSubscriberSystem
*d2util.Logger
cache *d2cache.Cache
filesToLoad *akara.Subscription
sourcesToUse *akara.Subscription
localesToCheck *akara.Subscription
locale struct {
charset string
language string
}
d2components.FileFactory
d2components.FileTypeFactory
d2components.FileSourceFactory
d2components.FileHandleFactory
d2components.LocaleFactory
}
// Init initializes the system with the given world
func (m *FileHandleResolver) Init(world *akara.World) {
m.World = world
m.cache = d2cache.CreateCache(fileHandleCacheBudget).(*d2cache.Cache)
m.setupLogger()
m.Debug("initializing ...")
m.setupSubscriptions()
m.setupFactories()
m.Debug("... initialization complete!")
}
func (m *FileHandleResolver) setupLogger() {
m.Logger = d2util.NewLogger()
m.SetPrefix(logPrefixFileHandleResolver)
}
func (m *FileHandleResolver) setupSubscriptions() {
m.Debug("setting up component subscriptions")
// this filter is for entities that have a file path and file type but no file handle.
filesToLoad := m.NewComponentFilter().
Require(
&d2components.File{},
&d2components.FileType{},
).
Forbid(
&d2components.FileHandle{},
&d2components.FileSource{},
).
Build()
sourcesToUse := m.NewComponentFilter().
Require(&d2components.FileSource{}).
Build()
localesToCheck := m.NewComponentFilter().
Require(&d2components.Locale{}).
Build()
m.filesToLoad = m.AddSubscription(filesToLoad)
m.sourcesToUse = m.AddSubscription(sourcesToUse)
m.localesToCheck = m.AddSubscription(localesToCheck)
}
func (m *FileHandleResolver) setupFactories() {
m.Debug("setting up component factories")
m.InjectComponent(&d2components.File{}, &m.File)
m.InjectComponent(&d2components.FileType{}, &m.FileType)
m.InjectComponent(&d2components.FileHandle{}, &m.FileHandle)
m.InjectComponent(&d2components.FileSource{}, &m.FileSource)
m.InjectComponent(&d2components.Locale{}, &m.Locale)
}
// Update iterates over entities which have not had a file handle resolved.
// For each source, it attempts to load this file with the given source.
// If the file can be opened by the source, we create the file handle using that source.
func (m *FileHandleResolver) Update() {
filesToLoad := m.filesToLoad.GetEntities()
sourcesToUse := m.sourcesToUse.GetEntities()
locales := m.localesToCheck.GetEntities()
if m.locale.charset == "" && m.locale.language == "" {
for _, eid := range locales {
locale, _ := m.GetLocale(eid)
m.locale.language = locale.String
m.locale.charset = d2resource.GetFontCharset(locale.String)
m.RemoveEntity(eid)
}
if m.locale.charset != "" && m.locale.language != "" {
m.Infof("locale set to `%s`", m.locale.language)
}
}
for _, fileID := range filesToLoad {
for _, sourceID := range sourcesToUse {
if m.loadFileWithSource(fileID, sourceID) {
break
}
}
}
}
// try to load a file with a source, returns true if loaded
func (m *FileHandleResolver) loadFileWithSource(fileID, sourceID akara.EID) bool {
fp, found := m.GetFile(fileID)
if !found {
return false
}
ft, found := m.GetFileType(fileID)
if !found {
return false
}
source, found := m.GetFileSource(sourceID)
if !found {
return false
}
sourceFp, found := m.GetFile(sourceID)
if !found {
return false
}
// replace the locale tokens if present
if strings.Contains(fp.Path, languageTokenFont) && m.locale.charset != "" {
fp.Path = strings.ReplaceAll(fp.Path, d2resource.LanguageFontToken, m.locale.charset)
} else if strings.Contains(fp.Path, languageTokenStringTable) && m.locale.language != "" {
fp.Path = strings.ReplaceAll(fp.Path, d2resource.LanguageTableToken, m.locale.language)
}
cacheKey := m.makeCacheKey(fp.Path, sourceFp.Path)
if entry, found := m.cache.Retrieve(cacheKey); found {
component := m.AddFileHandle(fileID)
component.Data = entry.(d2interface.DataStream)
return true
}
data, err := source.Open(fp)
if err != nil {
// HACK: sound environment stuff doesnt specify the path, just the filename
// so we gotta check this edge case
if ft.Type != d2enum.FileTypeWAV {
return false
}
if !strings.Contains(fp.Path, "sfx") {
return false
}
tryPath := strings.ReplaceAll(fp.Path, "sfx", "music")
tmpComponent := &d2components.File{Path: tryPath}
cacheKey = m.makeCacheKey(tryPath, sourceFp.Path)
if entry, found := m.cache.Retrieve(cacheKey); found {
component := m.AddFileHandle(fileID)
component.Data = entry.(d2interface.DataStream)
fp.Path = tryPath
return true
}
data, err = source.Open(tmpComponent)
if err != nil {
return false
}
fp.Path = tryPath
}
m.Debugf("resolved `%s` with source `%s`", fp.Path, sourceFp.Path)
component := m.AddFileHandle(fileID)
component.Data = data
if err := m.cache.Insert(cacheKey, data, fileHandleCacheEntryWeight); err != nil {
m.Error(err.Error())
}
return true
}
func (m *FileHandleResolver) makeCacheKey(path, source string) string {
const sep = "->"
return strings.Join([]string{source, path}, sep)
}