1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2025-02-04 07:37:48 -05:00
OpenDiablo2/d2core/d2systems/file_handle_resolver.go
gravestench 3731e631cf
Resolve #969 (#970)
* fixed #969
2020-12-13 14:41:19 -08:00

234 lines
6.4 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
}
Components struct {
File d2components.FileFactory
FileType d2components.FileTypeFactory
FileSource d2components.FileSourceFactory
FileHandle d2components.FileHandleFactory
Locale 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.World.AddSubscription(filesToLoad)
m.sourcesToUse = m.World.AddSubscription(sourcesToUse)
m.localesToCheck = m.World.AddSubscription(localesToCheck)
}
func (m *FileHandleResolver) setupFactories() {
m.Debug("setting up component factories")
m.InjectComponent(&d2components.File{}, &m.Components.File.ComponentFactory)
m.InjectComponent(&d2components.FileType{}, &m.Components.FileType.ComponentFactory)
m.InjectComponent(&d2components.FileSource{}, &m.Components.FileSource.ComponentFactory)
m.InjectComponent(&d2components.FileHandle{}, &m.Components.FileHandle.ComponentFactory)
m.InjectComponent(&d2components.Locale{}, &m.Components.Locale.ComponentFactory)
}
// 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.Components.Locale.Get(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.Components.File.Get(fileID)
if !found {
return false
}
ft, found := m.Components.FileType.Get(fileID)
if !found {
return false
}
source, found := m.Components.FileSource.Get(sourceID)
if !found {
return false
}
sourceFp, found := m.Components.File.Get(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.Components.FileHandle.Add(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.Components.FileHandle.Add(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.Components.FileHandle.Add(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)
}