Add asset manager (#253)

* Add asset manager

* Fix rebase

* Update asset manager to support mpq hash caching

* Update vendoring
This commit is contained in:
Alex Yatskov 2019-12-16 19:23:01 -08:00 committed by Tim Sarbin
parent d033c63e18
commit cc678ba747
5 changed files with 250 additions and 61 deletions

143
d2core/asset_manager.go Normal file
View File

@ -0,0 +1,143 @@
package d2core
import (
"fmt"
"log"
"os"
"path"
"strings"
"sync"
"github.com/OpenDiablo2/D2Shared/d2common/d2resource"
"github.com/OpenDiablo2/D2Shared/d2data/d2mpq"
"github.com/OpenDiablo2/OpenDiablo2/d2corecommon"
)
type archiveEntry struct {
archivePath string
hashEntryMap d2mpq.HashEntryMap
}
type assetManager struct {
fileCache *cache
archiveCache *cache
archiveEntries []archiveEntry
config *d2corecommon.Configuration
mutex sync.Mutex
}
func createAssetManager(config *d2corecommon.Configuration) *assetManager {
return &assetManager{
fileCache: createCache(1024 * 1024 * 32),
archiveCache: createCache(1024 * 1024 * 128),
config: config,
}
}
func (am *assetManager) LoadFile(filePath string) []byte {
data, err := am.loadFile(am.fixupFilePath(filePath))
if err != nil {
log.Println(err)
}
return data
}
func (am *assetManager) loadFile(filePath string) ([]byte, error) {
if value, found := am.fileCache.retrieve(filePath); found {
return value.([]byte), nil
}
archive, err := am.loadArchiveForFilePath(filePath)
if err != nil {
return nil, err
}
data, err := archive.ReadFile(filePath)
if err != nil {
return nil, err
}
if err := am.fileCache.insert(filePath, data, len(data)); err != nil {
return nil, err
}
return data, nil
}
func (am *assetManager) loadArchiveForFilePath(filePath string) (*d2mpq.MPQ, error) {
am.mutex.Lock()
defer am.mutex.Unlock()
if err := am.cacheArchiveEntries(); err != nil {
return nil, err
}
for _, archiveEntry := range am.archiveEntries {
if archiveEntry.hashEntryMap.Contains(filePath) {
return am.loadArchive(archiveEntry.archivePath)
}
}
return nil, fmt.Errorf("file not found: %s", filePath)
}
func (am *assetManager) loadArchive(archivePath string) (*d2mpq.MPQ, error) {
if archive, found := am.archiveCache.retrieve(archivePath); found {
return archive.(*d2mpq.MPQ), nil
}
archive, err := d2mpq.Load(archivePath)
if err != nil {
return nil, err
}
stat, err := os.Stat(archivePath)
if err != nil {
return nil, err
}
if err := am.archiveCache.insert(archivePath, archive, int(stat.Size())); err != nil {
return nil, err
}
return archive, nil
}
func (am *assetManager) cacheArchiveEntries() error {
if len(am.archiveEntries) == len(am.config.MpqLoadOrder) {
return nil
}
am.archiveEntries = nil
for _, archiveName := range am.config.MpqLoadOrder {
archivePath := path.Join(am.config.MpqPath, archiveName)
archive, err := am.loadArchive(archivePath)
if err != nil {
return err
}
am.archiveEntries = append(
am.archiveEntries,
archiveEntry{archivePath, archive.HashEntryMap},
)
}
return nil
}
func (am *assetManager) fixupFilePath(filePath string) string {
filePath = strings.ReplaceAll(filePath, "{LANG}", am.config.Language)
if strings.ToUpper(d2resource.LanguageCode) == "CHI" {
filePath = strings.ReplaceAll(filePath, "{LANG_FONT}", am.config.Language)
} else {
filePath = strings.ReplaceAll(filePath, "{LANG_FONT}", "latin")
}
filePath = strings.ToLower(filePath)
filePath = strings.ReplaceAll(filePath, `/`, "\\")
filePath = strings.TrimPrefix(filePath, "\\")
return filePath
}

97
d2core/cache.go Normal file
View File

@ -0,0 +1,97 @@
package d2core
import (
"errors"
"sync"
)
type cacheNode struct {
next *cacheNode
prev *cacheNode
key string
value interface{}
weight int
}
type cache struct {
head *cacheNode
tail *cacheNode
lookup map[string]*cacheNode
weight int
budget int
mutex sync.Mutex
}
func createCache(budget int) *cache {
return &cache{lookup: make(map[string]*cacheNode), budget: budget}
}
func (c *cache) insert(key string, value interface{}, weight int) error {
c.mutex.Lock()
defer c.mutex.Unlock()
if _, found := c.lookup[key]; found {
return errors.New("key already exists in cache")
}
node := &cacheNode{
key: key,
value: value,
weight: weight,
next: c.head,
}
if c.head != nil {
c.head.prev = node
}
c.head = node
if c.tail == nil {
c.tail = node
}
c.lookup[key] = node
c.weight += node.weight
for ; c.tail != nil && c.tail != c.head && c.weight > c.budget; c.tail = c.tail.prev {
c.weight -= c.tail.weight
c.tail.prev.next = nil
delete(c.lookup, c.tail.key)
}
return nil
}
func (c *cache) retrieve(key string) (interface{}, bool) {
c.mutex.Lock()
defer c.mutex.Unlock()
node, found := c.lookup[key]
if !found {
return nil, false
}
if node != c.head {
if node.next != nil {
node.next.prev = node.prev
}
if node.prev != nil {
node.prev.next = node.next
}
if node == c.tail {
c.tail = c.tail.prev
}
node.next = c.head
node.prev = nil
if c.head != nil {
c.head.prev = node
}
c.head = node
}
return node.value, true
}

View File

@ -1,16 +1,14 @@
package d2core
import (
"github.com/OpenDiablo2/D2Shared/d2data/d2dc6"
"log"
"math"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/OpenDiablo2/D2Shared/d2data/d2dc6"
"github.com/OpenDiablo2/D2Shared/d2common/d2resource"
"github.com/OpenDiablo2/D2Shared/d2helper"
@ -25,8 +23,6 @@ import (
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/D2Shared/d2data/d2mpq"
"github.com/OpenDiablo2/OpenDiablo2/d2audio"
"github.com/OpenDiablo2/D2Shared/d2common"
@ -41,7 +37,6 @@ import (
// Engine is the core OpenDiablo2 engine
type Engine struct {
Settings *d2corecommon.Configuration // Engine configuration settings from json file
Files map[string]string // Map that defines which files are in which MPQs
CheckedPatch map[string]bool // First time we check a file, we'll check if it's in the patch. This notes that we've already checked that.
LoadingSprite d2render.Sprite // The sprite shown when loading stuff
loadingProgress float64 // LoadingProcess is a range between 0.0 and 1.0. If set, loading screen displays.
@ -55,18 +50,16 @@ type Engine struct {
fullscreenKey bool // When true, the fullscreen toggle is still being pressed
lastTime float64 // Last time we updated the scene
showFPS bool
assetManager *assetManager
}
// CreateEngine creates and instance of the OpenDiablo2 engine
func CreateEngine() Engine {
result := Engine{
CurrentScene: nil,
nextScene: nil,
}
var result Engine
result.loadConfigurationFile()
result.assetManager = createAssetManager(result.Settings)
d2resource.LanguageCode = result.Settings.Language
result.mapMpqFiles()
d2datadict.LoadPalettes(result.Files, &result)
d2datadict.LoadPalettes(nil, &result)
d2common.LoadTextDictionary(&result)
d2datadict.LoadLevelTypes(&result)
d2datadict.LoadLevelPresets(&result)
@ -88,7 +81,6 @@ func CreateEngine() Engine {
result.LoadingSprite = result.LoadSprite(d2resource.LoadingScreen, d2enum.Loading)
loadingSpriteSizeX, loadingSpriteSizeY := result.LoadingSprite.GetSize()
result.LoadingSprite.MoveTo(int(400-(loadingSpriteSizeX/2)), int(300+(loadingSpriteSizeY/2)))
//result.SetNextScene(Scenes.CreateBlizzardIntro(result, result))
return result
}
@ -97,53 +89,8 @@ func (v *Engine) loadConfigurationFile() {
v.Settings = d2corecommon.LoadConfiguration()
}
func (v *Engine) mapMpqFiles() {
v.Files = make(map[string]string)
}
var mutex sync.Mutex
func (v *Engine) LoadFile(fileName string) []byte {
fileName = strings.ReplaceAll(fileName, "{LANG}", d2resource.LanguageCode)
// todo: separate CJK and latin characters from LanguageCode
if "CHI" == strings.ToUpper(d2resource.LanguageCode) {
fileName = strings.ReplaceAll(fileName, "{LANG_FONT}", d2resource.LanguageCode)
} else {
fileName = strings.ReplaceAll(fileName, "{LANG_FONT}", "latin")
}
fileName = strings.ToLower(fileName)
fileName = strings.ReplaceAll(fileName, `/`, "\\")
if fileName[0] == '\\' {
fileName = fileName[1:]
}
mutex.Lock()
defer mutex.Unlock()
// TODO: May want to cache some things if performance becomes an issue
cachedMpqFile, cacheExists := v.Files[fileName]
if cacheExists {
archive, _ := d2mpq.Load(cachedMpqFile)
result, _ := archive.ReadFile(fileName)
return result
}
for _, mpqFile := range v.Settings.MpqLoadOrder {
archive, _ := d2mpq.Load(path.Join(v.Settings.MpqPath, mpqFile))
if archive == nil {
log.Fatalf("Failed to load specified MPQ file: %s", mpqFile)
}
if !archive.FileExists(fileName) {
continue
}
result, _ := archive.ReadFile(fileName)
if len(result) == 0 {
continue
}
v.Files[fileName] = path.Join(v.Settings.MpqPath, mpqFile)
// log.Printf("%v in %v", fileName, mpqFile)
return result
}
log.Printf("Could not load %s from MPQs\n", fileName)
return []byte{}
return v.assetManager.LoadFile(fileName)
}
// IsLoading returns true if the engine is currently in a loading state

2
go.mod
View File

@ -3,7 +3,7 @@ module github.com/OpenDiablo2/OpenDiablo2
go 1.12
require (
github.com/OpenDiablo2/D2Shared v0.0.0-20191215185943-ef5f17453dfd
github.com/OpenDiablo2/D2Shared v0.0.0-20191217031331-027f7bbcb9fc
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/hajimehoshi/ebiten v1.11.0-alpha.0.20191121152720-3df198f68eea

2
go.sum
View File

@ -4,6 +4,8 @@ github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0 h1:tDnuU0igiBiQFjs
github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0/go.mod h1:h/5OEGj4G+fpYxluLjSMZbFY011ZxAntO98nCl8mrCs=
github.com/OpenDiablo2/D2Shared v0.0.0-20191215185943-ef5f17453dfd h1:f7THoOT1W1BTaSm/hwz9S/THw0ZLkuYQI9BLV1Pa+Iw=
github.com/OpenDiablo2/D2Shared v0.0.0-20191215185943-ef5f17453dfd/go.mod h1:mY8Ll5/iLRAQsaHvIdqSZiHX3aFCys/Q4Sot+xYpero=
github.com/OpenDiablo2/D2Shared v0.0.0-20191217031331-027f7bbcb9fc h1:f+PIG0HmaNYzBZuWuJAVau8SnllRLbzv6O8ttADXKrY=
github.com/OpenDiablo2/D2Shared v0.0.0-20191217031331-027f7bbcb9fc/go.mod h1:mY8Ll5/iLRAQsaHvIdqSZiHX3aFCys/Q4Sot+xYpero=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=