improve AssetManager implementation (#728)

* improve AssetManager implementation

Notable changes are:
 * removed the individual managers inside of d2asset, only one asset manager
 * AssetManager now has caches for the types of files it loads
 * created a type for TextDictionary (the txt file structs)
 * fixed a file path bug in d2loader Source
 * fixed a asset stream bug in d2loader Asset
 * d2loader.Loader now needs a d2config.Config on creation (for resolving locale files)
 * updated the mpq file in d2asset test data, added test case for "sub-directory"
 * added a Data method to d2asset.Asset. The data is cached on first full read.
 * renamed ArchiveDataStream to DataStream in d2interface
 * moved palette utility func out of d2asset and into d2util
 * bugfix for MacOS mpq loader issue

* minor lint fixes

* removed obsolete interfaces from d2interface

* lint fixes, added data caching to filesystem asset

* adding comment for mpq asset close

* adding comment for mpq asset close
This commit is contained in:
lord 2020-09-14 11:47:11 -07:00 committed by GitHub
parent 854fce3b14
commit 7f6ae1b785
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 559 additions and 721 deletions

View File

@ -17,18 +17,17 @@ import (
"strings"
"sync"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/pkg/profile"
"golang.org/x/image/colornames"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"

View File

@ -308,7 +308,7 @@ func (v *MPQ) ReadFile(fileName string) ([]byte, error) {
}
// ReadFileStream reads the mpq file data and returns a stream
func (v *MPQ) ReadFileStream(fileName string) (d2interface.ArchiveDataStream, error) {
func (v *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := v.getFileBlockData(fileName)
if err != nil {

View File

@ -2,7 +2,7 @@ package d2mpq
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
var _ d2interface.ArchiveDataStream = &MpqDataStream{} // Static check to confirm struct conforms to interface
var _ d2interface.DataStream = &MpqDataStream{} // Static check to confirm struct conforms to interface
// MpqDataStream represents a stream for MPQ data.
type MpqDataStream struct {

View File

@ -7,6 +7,9 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
)
// TextDictionary is a string map
type TextDictionary map[string]string
type textDictionaryHashEntry struct {
IsActive bool
Index uint16
@ -16,7 +19,7 @@ type textDictionaryHashEntry struct {
NameLength uint16
}
var lookupTable map[string]string //nolint:gochecknoglobals // currently global by design
var lookupTable TextDictionary //nolint:gochecknoglobals // currently global by design
const (
crcByteCount = 2
@ -35,9 +38,9 @@ func TranslateString(key string) string {
}
// LoadTextDictionary loads the text dictionary from the given data
func LoadTextDictionary(dictionaryData []byte) map[string]string {
func LoadTextDictionary(dictionaryData []byte) TextDictionary {
if lookupTable == nil {
lookupTable = make(map[string]string)
lookupTable = make(TextDictionary)
}
br := d2datautils.CreateStreamReader(dictionaryData)

View File

@ -1,9 +0,0 @@
package d2interface
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// AnimationManager loads animations
type AnimationManager interface {
Cacher
LoadAnimation(animationPath, palettePath string, drawEffect d2enum.DrawEffect) (Animation, error)
}

View File

@ -11,7 +11,7 @@ type Archive interface {
Close()
FileExists(fileName string) bool
ReadFile(fileName string) ([]byte, error)
ReadFileStream(fileName string) (ArchiveDataStream, error)
ReadFileStream(fileName string) (DataStream, error)
ReadTextFile(fileName string) (string, error)
GetFileList() ([]string, error)
}

View File

@ -1,10 +0,0 @@
package d2interface
// ArchiveManager manages loading files from archives
type ArchiveManager interface {
Cacher
LoadArchiveForFile(filePath string) (Archive, error)
FileExistsInArchive(filePath string) (bool, error)
LoadArchive(archivePath string) (Archive, error)
CacheArchiveEntries() error
}

View File

@ -1,7 +1,7 @@
package d2interface
// ArchiveDataStream is an archive data stream
type ArchiveDataStream interface {
// DataStream is a data stream
type DataStream interface {
Read(p []byte) (n int, err error)
Seek(offset int64, whence int) (int64, error)
Close() error

View File

@ -1,10 +0,0 @@
package d2interface
// FileManager manages file access to the archives being managed
// by the ArchiveManager
type FileManager interface {
Cacher
LoadFileStream(filePath string) (ArchiveDataStream, error)
LoadFile(filePath string) ([]byte, error)
FileExists(filePath string) (bool, error)
}

View File

@ -1,8 +0,0 @@
package d2interface
// FontManager manages fonts that are in archives being
// managed by the ArchiveManager
type FontManager interface {
Cacher
LoadFont(tablePath, spritePath, palettePath string) (Font, error)
}

View File

@ -1,7 +0,0 @@
package d2interface
// PaletteManager is responsible for loading palettes
type PaletteManager interface {
Cacher
LoadPalette(palettePath string) (Palette, error)
}

View File

@ -11,8 +11,11 @@ import (
// asset source), and it can read data and seek within the data
type Asset interface {
fmt.Stringer
io.ReadSeeker
io.Reader
io.Seeker
io.Closer
Type() types.AssetType
Source() Source
Path() string
Data() ([]byte, error)
}

View File

@ -1,6 +1,11 @@
package types
import "strings"
import (
"path/filepath"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2mpq"
)
// SourceType represents the type of the asset source
type SourceType int
@ -27,3 +32,16 @@ func Ext2SourceType(ext string) SourceType {
return AssetSourceUnknown
}
// CheckSourceType attempts to determine the source type of the source
func CheckSourceType(path string) SourceType {
// on MacOS, the MPQ's from blizzard don't have file extensions
// so we just attempt to init the file as an mpq
if _, err := d2mpq.Load(path); err == nil {
return AssetSourceMPQ
}
ext := filepath.Ext(path)
return Ext2SourceType(ext)
}

View File

@ -1,12 +1,17 @@
package filesystem
import (
"fmt"
"os"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
)
const (
bufLength = 32
)
// static check that Asset implements Asset
var _ asset.Asset = &Asset{}
@ -14,6 +19,7 @@ var _ asset.Asset = &Asset{}
type Asset struct {
assetType types.AssetType
source *Source
data []byte
path string
file *os.File
}
@ -43,6 +49,44 @@ func (fsa *Asset) Seek(offset int64, whence int) (int64, error) {
return fsa.file.Seek(offset, whence)
}
// Close closes the file
func (fsa *Asset) Close() error {
return fsa.file.Close()
}
// Data returns the raw file data as a slice of bytes
func (fsa *Asset) Data() ([]byte, error) {
if fsa.file == nil {
return nil, fmt.Errorf("asset has no file: %s", fsa.Path())
}
if fsa.data != nil {
return fsa.data, nil
}
_, seekErr := fsa.file.Seek(0, 0)
if seekErr != nil {
return nil, seekErr
}
buf := make([]byte, bufLength)
data := make([]byte, 0)
for {
numBytesRead, readErr := fsa.Read(buf)
data = append(data, buf[:numBytesRead]...)
if readErr != nil {
break
}
}
fsa.data = data
return data, nil
}
// String returns the path
func (fsa *Asset) String() string {
return fsa.Path()

View File

@ -1,11 +1,10 @@
package d2loader
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
@ -13,40 +12,85 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/filesystem"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/mpq"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
)
const (
defaultCacheBudget = 1024 * 1024 * 512
defaultCacheEntryWeight = 1
errFileNotFound = "file not found"
errFmtFileNotFound = "file not found: %s"
)
const (
defaultLanguage = "ENG"
)
const (
fontToken = d2resource.LanguageFontToken
tableToken = d2resource.LanguageTableToken
)
// NewLoader creates a new loader
func NewLoader() *Loader {
loader := &Loader{}
func NewLoader(config *d2config.Configuration) *Loader {
loader := &Loader{
config: config,
}
loader.Cache = d2cache.CreateCache(defaultCacheBudget)
loader.initFromConfig()
return loader
}
// Loader represents the manager that handles loading and caching assets with the asset Sources
// that have been added
type Loader struct {
config *d2config.Configuration
d2interface.Cache
*d2util.Logger
Sources []asset.Source
}
func (l *Loader) initFromConfig() {
if l.config == nil {
return
}
for _, mpqName := range l.config.MpqLoadOrder {
cleanDir := filepath.Clean(l.config.MpqPath)
srcPath := filepath.Join(cleanDir, mpqName)
_, err := l.AddSource(srcPath)
if err != nil {
fmt.Println(err.Error())
}
}
}
// 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) (asset.Asset, error) {
lang := defaultLanguage
if l.config != nil {
lang = l.config.Language
}
subPath = filepath.Clean(subPath)
subPath = strings.ReplaceAll(subPath, fontToken, "latin")
subPath = strings.ReplaceAll(subPath, tableToken, lang)
// first, we check the cache for an existing entry
if cached, found := l.Retrieve(subPath); found {
l.Debug(fmt.Sprintf("file `%s` exists in loader cache", subPath))
return cached.(asset.Asset), nil
a := cached.(asset.Asset)
_, err := a.Seek(0, 0)
return a, err
}
// if it isn't in the cache, we check if each source can open the file
@ -60,7 +104,7 @@ func (l *Loader) Load(subPath string) (asset.Asset, error) {
}
}
return nil, errors.New(errFileNotFound)
return nil, fmt.Errorf(errFmtFileNotFound, subPath)
}
// AddSource adds an asset source with the given path. The path will either resolve to a directory
@ -89,8 +133,7 @@ func (l *Loader) AddSource(path string) (asset.Source, error) {
}
if mode.IsRegular() {
ext := filepath.Ext(cleanPath)
sourceType = types.Ext2SourceType(ext)
sourceType = types.CheckSourceType(cleanPath)
}
switch sourceType {

View File

@ -17,12 +17,13 @@ const (
exclusiveB = "exclusive_b.txt"
exclusiveC = "exclusive_c.txt"
exclusiveD = "exclusive_d.txt"
subdirCommonD = "dir\\common.txt"
badSourcePath = "/x/y/z.mpq"
badFilePath = "a/bad/file/path.txt"
)
func TestLoader_NewLoader(t *testing.T) {
loader := NewLoader()
loader := NewLoader(nil)
if loader.Cache == nil {
t.Error("loader should not be nil")
@ -30,7 +31,7 @@ func TestLoader_NewLoader(t *testing.T) {
}
func TestLoader_AddSource(t *testing.T) {
loader := NewLoader()
loader := NewLoader(nil)
sourceA, errA := loader.AddSource(sourcePathA)
sourceB, errB := loader.AddSource(sourcePathB)
@ -80,7 +81,7 @@ func TestLoader_AddSource(t *testing.T) {
}
func TestLoader_Load(t *testing.T) {
loader := NewLoader()
loader := NewLoader(nil)
_, _ = loader.AddSource(sourcePathB) // we expect files common to any source to come from here
_, _ = loader.AddSource(sourcePathD)
@ -93,6 +94,7 @@ func TestLoader_Load(t *testing.T) {
entryB, errB := loader.Load(exclusiveB)
entryC, errC := loader.Load(exclusiveC)
entryD, errD := loader.Load(exclusiveD)
entryDsubdir, errDsubdir := loader.Load(subdirCommonD)
_, expectedError := loader.Load(badFilePath) // we expect an Error for this bad file path
@ -106,6 +108,10 @@ func TestLoader_Load(t *testing.T) {
t.Error("files exclusive to each source don't exist")
}
if errDsubdir != nil {
t.Error("mpq subdir entry not found")
}
if expectedError == nil {
t.Error("expected Error for nonexistant file path")
}
@ -123,6 +129,7 @@ func TestLoader_Load(t *testing.T) {
{entryB, "b"},
{entryC, "c"},
{entryD, "d"},
{entryDsubdir, "d"},
}
for idx := range tests {

View File

@ -1,6 +1,8 @@
package mpq
import (
"fmt"
"io"
"path/filepath"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
@ -8,12 +10,17 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
)
const (
bufLength = 32
)
// static check that Asset implements Asset
var _ asset.Asset = &Asset{}
// Asset represents a file record within an MPQ archive
type Asset struct {
stream d2interface.ArchiveDataStream
stream d2interface.DataStream
data []byte
path string
source *Source
}
@ -35,7 +42,12 @@ func (a *Asset) Path() string {
// Read will read asset data into the given buffer
func (a *Asset) Read(buf []byte) (n int, err error) {
return a.stream.Read(buf)
totalRead, err := a.stream.Read(buf)
if totalRead == 0 {
return 0, io.EOF
}
return totalRead, err
}
// Seek will seek the read position for the next read operation
@ -43,7 +55,50 @@ func (a *Asset) Seek(offset int64, whence int) (n int64, err error) {
return a.stream.Seek(offset, whence)
}
// Path returns the path
// Close will seek the read position for the next read operation
func (a *Asset) Close() (err error) {
// Calling a.stream.Close() will set the stream to nil, we dont want to do that.
// Because this asset gets cached, it may get retrieved again and used, in which
// case we will want the stream ready. So, instead of closing, we just seek back to the start.
// The garbage collector should get around to it if it ever gets ejected from the cache.
_, err = a.Seek(0, 0)
return err
}
// Data returns the raw file data as a slice of bytes
func (a *Asset) Data() ([]byte, error) {
if a.stream == nil {
return nil, fmt.Errorf("asset has no file: %s", a.Path())
}
if a.data != nil {
return a.data, nil
}
_, seekErr := a.Seek(0, 0)
if seekErr != nil {
return nil, seekErr
}
buf := make([]byte, bufLength)
data := make([]byte, 0)
for {
numBytesRead, readErr := a.Read(buf)
data = append(data, buf[:numBytesRead]...)
if readErr != nil || numBytesRead == 0 {
break
}
}
a.data = data
return data, nil
}
// String returns the path
func (a *Asset) String() string {
return a.Path()
}

View File

@ -1,6 +1,8 @@
package mpq
import (
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2mpq"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
@ -32,6 +34,7 @@ func (v *Source) Type() types.SourceType {
// Open attempts to open a file within the MPQ archive
func (v *Source) Open(name string) (a asset.Asset, err error) {
name = cleanName(name)
stream, err := v.MPQ.ReadFileStream(name)
if err != nil {
@ -41,12 +44,13 @@ func (v *Source) Open(name string) (a asset.Asset, err error) {
a = &Asset{
source: v,
stream: stream,
path: name,
}
return a, nil
}
// String returns the path of the MPQ on the host filesystem
// Path returns the path of the MPQ on the host filesystem
func (v *Source) Path() string {
return v.MPQ.Path()
}
@ -55,3 +59,13 @@ func (v *Source) Path() string {
func (v *Source) String() string {
return v.Path()
}
func cleanName(name string) string {
name = strings.ReplaceAll(name, "/", "\\")
if string(name[0]) == "\\" {
name = name[1:]
}
return name
}

Binary file not shown.

View File

@ -1,4 +1,4 @@
package d2asset
package d2util
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"

View File

@ -7,11 +7,9 @@ import (
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
d2iface "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
type playMode int
@ -30,7 +28,7 @@ type animationFrame struct {
offsetX int
offsetY int
image d2iface.Surface
image d2interface.Surface
}
type animationDirection struct {
@ -114,7 +112,7 @@ func (a *animation) Advance(elapsed float64) error {
return nil
}
func (a *animation) renderShadow(target d2iface.Surface) error {
func (a *animation) renderShadow(target d2interface.Surface) error {
direction := a.directions[a.directionIndex]
frame := direction.frames[a.frameIndex]
@ -133,7 +131,7 @@ func (a *animation) renderShadow(target d2iface.Surface) error {
}
// Render renders the animation to the given surface
func (a *animation) Render(target d2iface.Surface) error {
func (a *animation) Render(target d2interface.Surface) error {
direction := a.directions[a.directionIndex]
frame := direction.frames[a.frameIndex]
@ -150,7 +148,7 @@ func (a *animation) Render(target d2iface.Surface) error {
}
// RenderFromOrigin renders the animation from the animation origin
func (a *animation) RenderFromOrigin(target d2iface.Surface, shadow bool) error {
func (a *animation) RenderFromOrigin(target d2interface.Surface, shadow bool) error {
if a.originAtBottom {
direction := a.directions[a.directionIndex]
frame := direction.frames[a.frameIndex]
@ -173,7 +171,7 @@ func (a *animation) RenderFromOrigin(target d2iface.Surface, shadow bool) error
}
// RenderSection renders the section of the animation frame enclosed by bounds
func (a *animation) RenderSection(sfc d2iface.Surface, bound image.Rectangle) error {
func (a *animation) RenderSection(sfc d2interface.Surface, bound image.Rectangle) error {
direction := a.directions[a.directionIndex]
frame := direction.frames[a.frameIndex]

View File

@ -1,159 +0,0 @@
package d2asset
import (
"fmt"
"path/filepath"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
const (
animationBudget = 64
)
// Static checks to confirm struct conforms to interface
var _ d2interface.AnimationManager = &animationManager{}
var _ d2interface.Cacher = &animationManager{}
type animationManager struct {
*AssetManager
cache d2interface.Cache
renderer d2interface.Renderer
}
func (am *animationManager) ClearCache() {
am.cache.Clear()
}
func (am *animationManager) GetCache() d2interface.Cache {
return am.cache
}
func (am *animationManager) LoadAnimation(
animationPath, palettePath string,
effect d2enum.DrawEffect) (d2interface.Animation, error) {
cachePath := fmt.Sprintf("%s;%s;%d", animationPath, palettePath, effect)
if animation, found := am.cache.Retrieve(cachePath); found {
return animation.(d2interface.Animation).Clone(), nil
}
var animation d2interface.Animation
ext := strings.ToLower(filepath.Ext(animationPath))
switch ext {
case ".dc6":
palette, err := am.LoadPalette(palettePath)
if err != nil {
return nil, err
}
animation, err = am.CreateDC6Animation(animationPath, palette, d2enum.DrawEffectNone)
if err != nil {
return nil, err
}
case ".dcc":
palette, err := am.LoadPalette(palettePath)
if err != nil {
return nil, err
}
animation, err = am.CreateDCCAnimation(animationPath, palette, effect)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown animation format: %s", ext)
}
if err := am.cache.Insert(cachePath, animation.Clone(), 1); err != nil {
return nil, err
}
return animation, nil
}
// CreateDC6Animation creates an Animation from d2dc6.DC6 and d2dat.DATPalette
func (am *animationManager) CreateDC6Animation(dc6Path string,
palette d2interface.Palette, effect d2enum.DrawEffect) (d2interface.Animation, error) {
dc6, err := am.loadDC6(dc6Path)
if err != nil {
return nil, err
}
anim := DC6Animation{
animation: animation{
directions: make([]animationDirection, dc6.Directions),
playLength: defaultPlayLength,
playLoop: true,
originAtBottom: true,
effect: effect,
},
dc6Path: dc6Path,
dc6: dc6,
palette: palette,
renderer: am.renderer,
}
err = anim.SetDirection(0)
return &anim, err
}
// CreateDCCAnimation creates an animation from d2dcc.DCC and d2dat.DATPalette
func (am *animationManager) CreateDCCAnimation(dccPath string,
palette d2interface.Palette,
effect d2enum.DrawEffect) (d2interface.Animation, error) {
dcc, err := am.loadDCC(dccPath)
if err != nil {
return nil, err
}
anim := animation{
playLength: defaultPlayLength,
playLoop: true,
directions: make([]animationDirection, dcc.NumberOfDirections),
effect: effect,
}
DCC := DCCAnimation{
animation: anim,
animationManager: am,
dccPath: dccPath,
palette: palette,
renderer: am.renderer,
}
err = DCC.SetDirection(0)
if err != nil {
return nil, err
}
return &DCC, nil
}
func (am *animationManager) loadDC6(path string) (*d2dc6.DC6, error) {
dc6Data, err := am.LoadFile(path)
if err != nil {
return nil, err
}
dc6, err := d2dc6.Load(dc6Data)
if err != nil {
return nil, err
}
return dc6, nil
}
func (am *animationManager) loadDCC(path string) (*d2dcc.DCC, error) {
dccData, err := am.LoadFile(path)
if err != nil {
return nil, err
}
return d2dcc.Load(dccData)
}

View File

@ -1,119 +0,0 @@
package d2asset
import (
"errors"
"path"
"sync"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2mpq"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
)
// Static checks to confirm struct conforms to interface
var _ d2interface.ArchiveManager = &archiveManager{}
var _ d2interface.Cacher = &archiveManager{}
type archiveManager struct {
*AssetManager
cache d2interface.Cache
config *d2config.Configuration
archives []d2interface.Archive
mutex sync.Mutex
}
const (
archiveBudget = 1024 * 1024 * 512
)
// LoadArchiveForFile loads the archive for the given (in-archive) file path
func (am *archiveManager) LoadArchiveForFile(filePath string) (d2interface.Archive, error) {
am.mutex.Lock()
defer am.mutex.Unlock()
if err := am.CacheArchiveEntries(); err != nil {
return nil, err
}
for _, archive := range am.archives {
if archive.Contains(filePath) {
result, ok := am.LoadArchive(archive.Path())
if ok == nil {
return result, nil
}
}
}
return nil, errors.New("file not found")
}
// FileExistsInArchive checks if a file exists in an archive
func (am *archiveManager) FileExistsInArchive(filePath string) (bool, error) {
am.mutex.Lock()
defer am.mutex.Unlock()
if err := am.CacheArchiveEntries(); err != nil {
return false, err
}
for _, archiveEntry := range am.archives {
if archiveEntry.Contains(filePath) {
return true, nil
}
}
return false, nil
}
// LoadArchive loads and caches an archive
func (am *archiveManager) LoadArchive(archivePath string) (d2interface.Archive, error) {
if archive, found := am.cache.Retrieve(archivePath); found {
return archive.(d2interface.Archive), nil
}
archive, err := d2mpq.Load(archivePath)
if err != nil {
return nil, err
}
if err := am.cache.Insert(archivePath, archive, int(archive.Size())); err != nil {
return nil, err
}
return archive, nil
}
// CacheArchiveEntries updates the archive entries
func (am *archiveManager) CacheArchiveEntries() error {
if len(am.archives) == len(am.config.MpqLoadOrder) {
return nil
}
am.archives = 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.archives = append(
am.archives,
archive,
)
}
return nil
}
// ClearCache clears the archive manager cache
func (am *archiveManager) ClearCache() {
am.cache.Clear()
}
// GetCache returns the archive manager cache
func (am *archiveManager) GetCache() d2interface.Cache {
return am.cache
}

View File

@ -1,95 +0,0 @@
package d2asset
import (
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
)
const (
fileBudget = 1024 * 1024 * 32
)
// Static checks to confirm struct conforms to interface
var _ d2interface.FileManager = &fileManager{}
var _ d2interface.Cacher = &fileManager{}
type fileManager struct {
*AssetManager
cache d2interface.Cache
archiveManager d2interface.ArchiveManager
config *d2config.Configuration
}
// LoadFileStream loads a file as a stream automatically from an archive
func (fm *fileManager) LoadFileStream(filePath string) (d2interface.ArchiveDataStream, error) {
filePath = fm.fixupFilePath(filePath)
archive, err := fm.archiveManager.LoadArchiveForFile(filePath)
if err != nil {
return nil, err
}
return archive.ReadFileStream(filePath)
}
// LoadFile loads a file automatically from a managed archive
func (fm *fileManager) LoadFile(filePath string) ([]byte, error) {
filePath = fm.fixupFilePath(filePath)
if value, found := fm.cache.Retrieve(filePath); found {
return value.([]byte), nil
}
archive, err := fm.archiveManager.LoadArchiveForFile(filePath)
if err != nil {
return nil, err
}
data, err := archive.ReadFile(filePath)
if err != nil {
return nil, err
}
if err := fm.cache.Insert(filePath, data, len(data)); err != nil {
return nil, err
}
return data, nil
}
// FileExists checks if a file exists in an archive
func (fm *fileManager) FileExists(filePath string) (bool, error) {
filePath = fm.fixupFilePath(filePath)
return fm.archiveManager.FileExistsInArchive(filePath)
}
func (fm *fileManager) ClearCache() {
fm.cache.Clear()
}
func (fm *fileManager) GetCache() d2interface.Cache {
return fm.cache
}
func (fm *fileManager) fixupFilePath(filePath string) string {
filePath = fm.removeLocaleTokens(filePath)
filePath = strings.ToLower(filePath)
filePath = strings.ReplaceAll(filePath, `/`, "\\")
filePath = strings.TrimPrefix(filePath, "\\")
return filePath
}
func (fm *fileManager) removeLocaleTokens(filePath string) string {
tableToken := d2resource.LanguageTableToken
fontToken := d2resource.LanguageFontToken
filePath = strings.ReplaceAll(filePath, tableToken, fm.config.Language)
// fixme: not all languages==latin
filePath = strings.ReplaceAll(filePath, fontToken, "latin")
return filePath
}

View File

@ -1,25 +1,49 @@
package d2asset
import (
"encoding/binary"
"fmt"
"image/color"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dat"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2pl2"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types"
)
const (
defaultCacheEntryWeight = 1
)
const (
animationBudget = 1024 * 1024 * 128
fontBudget = 128
tableBudget = 64
paletteBudget = 64
paletteTransformBudget = 64
)
// AssetManager loads files and game objects
type AssetManager struct {
archiveManager d2interface.ArchiveManager
archivedFileManager d2interface.FileManager
paletteManager d2interface.PaletteManager
paletteTransformManager *paletteTransformManager
animationManager d2interface.AnimationManager
fontManager d2interface.FontManager
renderer d2interface.Renderer
loader *d2loader.Loader
tables d2interface.Cache
animations d2interface.Cache
fonts d2interface.Cache
palettes d2interface.Cache
transforms d2interface.Cache
}
// LoadFileStream streams an MPQ file from a source file path
func (am *AssetManager) LoadFileStream(filePath string) (d2interface.ArchiveDataStream, error) {
data, err := am.archivedFileManager.LoadFileStream(filePath)
// LoadAsset loads an asset
func (am *AssetManager) LoadAsset(filePath string) (asset.Asset, error) {
data, err := am.loader.Load(filePath)
if err != nil {
log.Printf("error loading file stream %s (%v)", filePath, err.Error())
}
@ -27,11 +51,21 @@ func (am *AssetManager) LoadFileStream(filePath string) (d2interface.ArchiveData
return data, err
}
// LoadFileStream streams an MPQ file from a source file path
func (am *AssetManager) LoadFileStream(filePath string) (d2interface.DataStream, error) {
return am.LoadAsset(filePath)
}
// LoadFile loads an entire file from a source file path as a []byte
func (am *AssetManager) LoadFile(filePath string) ([]byte, error) {
data, err := am.archivedFileManager.LoadFile(filePath)
fileAsset, err := am.LoadAsset(filePath)
if err != nil {
log.Printf("error loading file %s (%v)", filePath, err.Error())
return nil, err
}
data, err := fileAsset.Data()
if err != nil {
return nil, err
}
return data, err
@ -39,7 +73,11 @@ func (am *AssetManager) LoadFile(filePath string) ([]byte, error) {
// FileExists checks if a file exists on the underlying file system at the given file path.
func (am *AssetManager) FileExists(filePath string) (bool, error) {
return am.archivedFileManager.FileExists(filePath)
if loadedAsset, err := am.loader.Load(filePath); err != nil || loadedAsset == nil {
return false, err
}
return true, nil
}
// LoadAnimation loads an animation by its resource path and its palette path
@ -49,8 +87,43 @@ func (am *AssetManager) LoadAnimation(animationPath, palettePath string) (d2inte
// LoadAnimationWithEffect loads an animation by its resource path and its palette path with a given transparency value
func (am *AssetManager) LoadAnimationWithEffect(animationPath, palettePath string,
drawEffect d2enum.DrawEffect) (d2interface.Animation, error) {
return am.animationManager.LoadAnimation(animationPath, palettePath, drawEffect)
effect d2enum.DrawEffect) (d2interface.Animation, error) {
cachePath := fmt.Sprintf("%s;%s;%d", animationPath, palettePath, effect)
if animation, found := am.animations.Retrieve(cachePath); found {
return animation.(d2interface.Animation).Clone(), nil
}
animAsset, err := am.LoadAsset(animationPath)
if err != nil {
return nil, err
}
palette, err := am.LoadPalette(palettePath)
if err != nil {
return nil, err
}
var animation d2interface.Animation
switch animAsset.Type() {
case types.AssetTypeDC6:
animation, err = am.createDC6Animation(animationPath, palette, effect)
if err != nil {
return nil, err
}
case types.AssetTypeDCC:
animation, err = am.createDCCAnimation(animationPath, palette, effect)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown animation format for file: %s", animAsset.Path())
}
err = am.animations.Insert(cachePath, animation, defaultCacheEntryWeight)
return animation, err
}
// LoadComposite creates a composite object from a ObjectLookupRecord and palettePath describing it
@ -68,14 +141,209 @@ func (am *AssetManager) LoadComposite(baseType d2enum.ObjectType, token, palette
// LoadFont loads a font the resource files
func (am *AssetManager) LoadFont(tablePath, spritePath, palettePath string) (d2interface.Font, error) {
return am.fontManager.LoadFont(tablePath, spritePath, palettePath)
cachePath := fmt.Sprintf("%s;%s;%s", tablePath, spritePath, palettePath)
if cached, found := am.fonts.Retrieve(cachePath); found {
return cached.(d2interface.Font), nil
}
sheet, err := am.LoadAnimation(spritePath, palettePath)
if err != nil {
return nil, err
}
tableData, err := am.LoadFile(tablePath)
if err != nil {
return nil, err
}
if string(tableData[:5]) != "Woo!\x01" {
return nil, fmt.Errorf("invalid font table format: %s", tablePath)
}
_, maxCharHeight := sheet.GetFrameBounds()
glyphs := make(map[rune]fontGlyph)
for i := 12; i < len(tableData); i += 14 {
code := rune(binary.LittleEndian.Uint16(tableData[i : i+2]))
var glyph fontGlyph
glyph.frame = int(binary.LittleEndian.Uint16(tableData[i+8 : i+10]))
glyph.width = int(tableData[i+3])
glyph.height = maxCharHeight
glyphs[code] = glyph
}
font := &Font{
sheet: sheet,
glyphs: glyphs,
color: color.White,
}
err = am.fonts.Insert(cachePath, font, defaultCacheEntryWeight)
return font, err
}
// LoadPalette loads a palette from a given palette path
func (am *AssetManager) LoadPalette(palettePath string) (d2interface.Palette, error) {
return am.paletteManager.LoadPalette(palettePath)
if cached, found := am.palettes.Retrieve(palettePath); found {
return cached.(d2interface.Palette), nil
}
paletteAsset, err := am.LoadAsset(palettePath)
if err != nil {
return nil, err
}
if paletteAsset.Type() != types.AssetTypePalette {
return nil, fmt.Errorf("not an instance of a palette: %s", palettePath)
}
data, err := am.LoadFile(palettePath)
if err != nil {
return nil, err
}
palette, err := d2dat.Load(data)
if err != nil {
return nil, err
}
err = am.palettes.Insert(palettePath, palette, defaultCacheEntryWeight)
return palette, err
}
// LoadStringTable loads a string table from the given path
func (am *AssetManager) LoadStringTable(tablePath string) (d2tbl.TextDictionary, error) {
if cached, found := am.tables.Retrieve(tablePath); found {
return cached.(d2tbl.TextDictionary), nil
}
data, err := am.LoadFile(tablePath)
if err != nil {
return nil, err
}
table := d2tbl.LoadTextDictionary(data)
if table != nil {
return nil, fmt.Errorf("table not found: %s", tablePath)
}
err = am.tables.Insert(tablePath, table, defaultCacheEntryWeight)
return table, err
}
// LoadPaletteTransform loads a palette transform file
func (am *AssetManager) LoadPaletteTransform(path string) (*d2pl2.PL2, error) {
if pl2, found := am.transforms.Retrieve(path); found {
return pl2.(*d2pl2.PL2), nil
}
data, err := am.LoadFile(path)
if err != nil {
return nil, err
}
pl2, err := d2pl2.Load(data)
if err != nil {
return nil, err
}
if err := am.transforms.Insert(path, pl2, 1); err != nil {
return nil, err
}
return pl2, nil
}
// createDC6Animation creates an Animation from d2dc6.DC6 and d2dat.DATPalette
func (am *AssetManager) createDC6Animation(dc6Path string,
palette d2interface.Palette, effect d2enum.DrawEffect) (d2interface.Animation, error) {
dc6, err := am.loadDC6(dc6Path)
if err != nil {
return nil, err
}
anim := DC6Animation{
animation: animation{
directions: make([]animationDirection, dc6.Directions),
playLength: defaultPlayLength,
playLoop: true,
originAtBottom: true,
effect: effect,
},
dc6Path: dc6Path,
dc6: dc6,
palette: palette,
renderer: am.renderer,
}
err = anim.SetDirection(0)
return &anim, err
}
// createDCCAnimation creates an animation from d2dcc.DCC and d2dat.DATPalette
func (am *AssetManager) createDCCAnimation(dccPath string,
palette d2interface.Palette,
effect d2enum.DrawEffect) (d2interface.Animation, error) {
dcc, err := am.loadDCC(dccPath)
if err != nil {
return nil, err
}
anim := animation{
playLength: defaultPlayLength,
playLoop: true,
directions: make([]animationDirection, dcc.NumberOfDirections),
effect: effect,
}
DCC := DCCAnimation{
animation: anim,
AssetManager: am,
dccPath: dccPath,
palette: palette,
renderer: am.renderer,
}
err = DCC.SetDirection(0)
if err != nil {
return nil, err
}
return &DCC, nil
}
func (am *AssetManager) loadDC6(path string) (*d2dc6.DC6, error) {
dc6Data, err := am.LoadFile(path)
if err != nil {
return nil, err
}
dc6, err := d2dc6.Load(dc6Data)
if err != nil {
return nil, err
}
return dc6, nil
}
func (am *AssetManager) loadDCC(path string) (*d2dcc.DCC, error) {
dccData, err := am.LoadFile(path)
if err != nil {
return nil, err
}
return d2dcc.Load(dccData)
}
// BindTerminalCommands binds the in-game terminal comands for the asset manager.
func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error {
if err := term.BindAction("assetspam", "display verbose asset manager logs", func(verbose bool) {
if verbose {
@ -84,11 +352,10 @@ func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error {
term.OutputInfof("asset manager verbose logging disabled")
}
am.archiveManager.GetCache().SetVerbose(verbose)
am.archivedFileManager.GetCache().SetVerbose(verbose)
am.paletteManager.GetCache().SetVerbose(verbose)
am.paletteTransformManager.cache.SetVerbose(verbose)
am.animationManager.GetCache().SetVerbose(verbose)
am.palettes.SetVerbose(verbose)
am.fonts.SetVerbose(verbose)
am.transforms.SetVerbose(verbose)
am.animations.SetVerbose(verbose)
}); err != nil {
return err
}
@ -99,24 +366,19 @@ func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error {
return float64(c.GetWeight()) / float64(c.GetBudget()) * percent
}
term.OutputInfof("archive cache: %f", cacheStatistics(am.archiveManager.GetCache()))
term.OutputInfof("file cache: %f", cacheStatistics(am.archivedFileManager.GetCache()))
term.OutputInfof("palette cache: %f", cacheStatistics(am.paletteManager.GetCache()))
term.OutputInfof("palette transform cache: %f", cacheStatistics(am.paletteTransformManager.
cache))
term.OutputInfof("animation cache: %f", cacheStatistics(am.animationManager.GetCache()))
term.OutputInfof("font cache: %f", cacheStatistics(am.fontManager.GetCache()))
term.OutputInfof("palette cache: %f", cacheStatistics(am.palettes))
term.OutputInfof("palette transform cache: %f", cacheStatistics(am.transforms))
term.OutputInfof("animation cache: %f", cacheStatistics(am.animations))
term.OutputInfof("font cache: %f", cacheStatistics(am.fonts))
}); err != nil {
return err
}
if err := term.BindAction("assetclear", "clear asset manager cache", func() {
am.archiveManager.ClearCache()
am.archivedFileManager.GetCache().Clear()
am.paletteManager.ClearCache()
am.paletteTransformManager.cache.Clear()
am.animationManager.ClearCache()
am.fontManager.ClearCache()
am.palettes.Clear()
am.transforms.Clear()
am.animations.Clear()
am.fonts.Clear()
}); err != nil {
return err
}

View File

@ -3,48 +3,26 @@ package d2asset
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
)
// NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly
func NewAssetManager(renderer d2interface.Renderer,
func NewAssetManager(renderer d2interface.Renderer, config *d2config.Configuration,
term d2interface.Terminal) (*AssetManager, error) {
manager := &AssetManager{}
manager.archiveManager = &archiveManager{
AssetManager: manager,
cache: d2cache.CreateCache(archiveBudget),
config: d2config.Config,
}
manager.archivedFileManager = &fileManager{
manager,
d2cache.CreateCache(fileBudget),
manager.archiveManager,
d2config.Config,
}
manager.paletteManager = &paletteManager{
manager,
manager := &AssetManager{
renderer,
d2loader.NewLoader(config),
d2cache.CreateCache(animationBudget),
d2cache.CreateCache(tableBudget),
d2cache.CreateCache(fontBudget),
d2cache.CreateCache(paletteBudget),
}
manager.paletteTransformManager = &paletteTransformManager{
manager,
d2cache.CreateCache(paletteTransformBudget),
}
manager.animationManager = &animationManager{
AssetManager: manager,
renderer: renderer,
cache: d2cache.CreateCache(animationBudget),
}
manager.fontManager = &fontManager{manager, d2cache.CreateCache(fontBudget)}
if term != nil {
return manager, manager.BindTerminalCommands(term)
err := manager.BindTerminalCommands(term)
return manager, err
}
return manager, nil

View File

@ -3,22 +3,24 @@ package d2asset
import (
"errors"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc"
d2iface "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
var _ d2iface.Animation = &DC6Animation{} // Static check to confirm struct conforms to interface
var _ d2interface.Animation = &DC6Animation{} // Static check to confirm struct conforms to
// interface
// DC6Animation is an animation made from a DC6 file
type DC6Animation struct {
animation
dc6Path string
dc6 *d2dc6.DC6
palette d2iface.Palette
renderer d2iface.Renderer
palette d2interface.Palette
renderer d2interface.Renderer
}
// SetDirection decodes and sets the direction
@ -56,7 +58,7 @@ func (a *DC6Animation) decodeDirection(directionIndex int) error {
}
indexData := dc6.DecodeFrame(startFrame + i)
colorData := ImgIndexToRGBA(indexData, a.palette)
colorData := d2util.ImgIndexToRGBA(indexData, a.palette)
if err := sfc.ReplacePixels(colorData); err != nil {
return err
@ -76,7 +78,7 @@ func (a *DC6Animation) decodeDirection(directionIndex int) error {
}
// Clone creates a copy of the animation
func (a *DC6Animation) Clone() d2iface.Animation {
func (a *DC6Animation) Clone() d2interface.Animation {
animation := *a
return &animation
}

View File

@ -4,26 +4,29 @@ import (
"errors"
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc"
d2iface "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
var _ d2iface.Animation = &DCCAnimation{} // Static check to confirm struct conforms to interface
var _ d2interface.Animation = &DCCAnimation{} // Static check to confirm struct conforms to
// interface
// DCCAnimation represents an animation decoded from DCC
type DCCAnimation struct {
animation
*animationManager
*AssetManager
dccPath string
palette d2iface.Palette
renderer d2iface.Renderer
palette d2interface.Palette
renderer d2interface.Renderer
}
// Clone creates a copy of the animation
func (a *DCCAnimation) Clone() d2iface.Animation {
func (a *DCCAnimation) Clone() d2interface.Animation {
animation := *a
return &animation
}
@ -71,7 +74,7 @@ func (a *DCCAnimation) decodeDirection(directionIndex int) error {
frameHeight := maxY - minY
for _, dccFrame := range direction.Frames {
pixels := ImgIndexToRGBA(dccFrame.PixelData, a.palette)
pixels := d2util.ImgIndexToRGBA(dccFrame.PixelData, a.palette)
sfc, err := a.renderer.NewSurface(frameWidth, frameHeight, d2enum.FilterNearest)
if err != nil {

View File

@ -1,87 +0,0 @@
package d2asset
import (
"encoding/binary"
"errors"
"fmt"
"image/color"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
const (
fontBudget = 64
)
// Static checks to confirm struct conforms to interface
var _ d2interface.FontManager = &fontManager{}
var _ d2interface.Cacher = &fontManager{}
type fontManager struct {
*AssetManager
cache d2interface.Cache
}
// LoadFont loads a font from the archives managed by the ArchiveManager
func (fm *fontManager) LoadFont(tablePath, spritePath, palettePath string) (d2interface.Font,
error) {
cachePath := fmt.Sprintf("%s;%s;%s", tablePath, spritePath, palettePath)
if font, found := fm.cache.Retrieve(cachePath); found {
return font.(d2interface.Font), nil
}
sheet, err := fm.LoadAnimation(spritePath, palettePath)
if err != nil {
return nil, err
}
data, err := fm.LoadFile(tablePath)
if err != nil {
return nil, err
}
if string(data[:5]) != "Woo!\x01" {
return nil, errors.New("invalid font table format")
}
_, maxCharHeight := sheet.GetFrameBounds()
glyphs := make(map[rune]fontGlyph)
for i := 12; i < len(data); i += 14 {
code := rune(binary.LittleEndian.Uint16(data[i : i+2]))
var glyph fontGlyph
glyph.frame = int(binary.LittleEndian.Uint16(data[i+8 : i+10]))
glyph.width = int(data[i+3])
glyph.height = maxCharHeight
glyphs[code] = glyph
}
font := &Font{
sheet: sheet,
glyphs: glyphs,
color: color.White,
}
if err != nil {
return nil, err
}
if err := fm.cache.Insert(cachePath, font, 1); err != nil {
return nil, err
}
return font, nil
}
// ClearCache clears the font cache
func (fm *fontManager) ClearCache() {
fm.cache.Clear()
}
// GetCache returns the font managers cache
func (fm *fontManager) GetCache() d2interface.Cache {
return fm.cache
}

View File

@ -1,52 +0,0 @@
package d2asset
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dat"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
// Static checks to confirm struct conforms to interface
var _ d2interface.PaletteManager = &paletteManager{}
var _ d2interface.Cacher = &paletteManager{}
type paletteManager struct {
*AssetManager
cache d2interface.Cache
}
const (
paletteBudget = 64
)
// LoadPalette loads a palette from archives managed by the ArchiveManager
func (pm *paletteManager) LoadPalette(palettePath string) (d2interface.Palette, error) {
if palette, found := pm.cache.Retrieve(palettePath); found {
return palette.(d2interface.Palette), nil
}
paletteData, err := pm.LoadFile(palettePath)
if err != nil {
return nil, err
}
palette, err := d2dat.Load(paletteData)
if err != nil {
return nil, err
}
if err := pm.cache.Insert(palettePath, palette, 1); err != nil {
return nil, err
}
return palette, nil
}
// ClearCache clears the palette cache
func (pm *paletteManager) ClearCache() {
pm.cache.Clear()
}
// GetCache returns the palette managers cache
func (pm *paletteManager) GetCache() d2interface.Cache {
return pm.cache
}

View File

@ -1,37 +0,0 @@
package d2asset
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2pl2"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
type paletteTransformManager struct {
*AssetManager
cache d2interface.Cache
}
const (
paletteTransformBudget = 64
)
func (pm *paletteTransformManager) loadPaletteTransform(path string) (*d2pl2.PL2, error) {
if pl2, found := pm.cache.Retrieve(path); found {
return pl2.(*d2pl2.PL2), nil
}
data, err := pm.LoadFile(path)
if err != nil {
return nil, err
}
pl2, err := d2pl2.Load(data)
if err != nil {
return nil, err
}
if err := pm.cache.Insert(path, pl2, 1); err != nil {
return nil, err
}
return pl2, nil
}

View File

@ -3,11 +3,12 @@ package d2maprenderer
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
)
func (mr *MapRenderer) generateTileCache() {
@ -85,7 +86,7 @@ func (mr *MapRenderer) generateFloorCache(tile *d2ds1.FloorShadowRecord) {
image, _ := mr.renderer.NewSurface(int(tileData[i].Width), int(tileHeight), d2enum.FilterNearest)
indexData := make([]byte, tileData[i].Width*tileHeight)
d2dt1.DecodeTileGfxData(tileData[i].Blocks, &indexData, tileYOffset, tileData[i].Width)
pixels := d2asset.ImgIndexToRGBA(indexData, mr.palette)
pixels := d2util.ImgIndexToRGBA(indexData, mr.palette)
_ = image.ReplacePixels(pixels)
mr.setImageCacheRecord(tile.Style, tile.Sequence, 0, tileIndex, image)
@ -127,7 +128,7 @@ func (mr *MapRenderer) generateShadowCache(tile *d2ds1.FloorShadowRecord) {
image, _ := mr.renderer.NewSurface(int(tileData.Width), tileHeight, d2enum.FilterNearest)
indexData := make([]byte, tileData.Width*int32(tileHeight))
d2dt1.DecodeTileGfxData(tileData.Blocks, &indexData, tileYOffset, tileData.Width)
pixels := d2asset.ImgIndexToRGBA(indexData, mr.palette)
pixels := d2util.ImgIndexToRGBA(indexData, mr.palette)
_ = image.ReplacePixels(pixels)
mr.setImageCacheRecord(tile.Style, tile.Sequence, 13, tile.RandomIndex, image)
}
@ -192,7 +193,7 @@ func (mr *MapRenderer) generateWallCache(tile *d2ds1.WallRecord) {
d2dt1.DecodeTileGfxData(newTileData.Blocks, &indexData, tileYOffset, 160)
}
pixels := d2asset.ImgIndexToRGBA(indexData, mr.palette)
pixels := d2util.ImgIndexToRGBA(indexData, mr.palette)
if err := image.ReplacePixels(pixels); err != nil {
log.Panicf(err.Error())

View File

@ -298,7 +298,7 @@ type SelectHeroClass struct {
// CreateSelectHeroClass creates an instance of a SelectHeroClass
func CreateSelectHeroClass(
navigator Navigator,
manager *d2asset.AssetManager,
asset *d2asset.AssetManager,
renderer d2interface.Renderer,
audioProvider d2interface.AudioProvider,
ui *d2ui.UIManager,
@ -306,6 +306,7 @@ func CreateSelectHeroClass(
connectionHost string,
) *SelectHeroClass {
result := &SelectHeroClass{
asset: asset,
heroRenderInfo: make(map[d2enum.Hero]*HeroRenderInfo),
selectedHero: d2enum.HeroNone,
connectionType: connectionType,

View File

@ -379,7 +379,7 @@ func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool {
return false
}
// Load loads the resources required for the GameControls
// Load the resources required for the GameControls
func (g *GameControls) Load() {
animation, _ := g.asset.LoadAnimation(d2resource.GameGlobeOverlap, d2resource.PaletteSky)
g.globeSprite, _ = g.uiManager.NewSprite(animation)

View File

@ -76,7 +76,7 @@ func NewHeroStatsPanel(asset *d2asset.AssetManager, ui *d2ui.UIManager, heroName
}
}
// Load loads the data for the hero status panel
// Load the data for the hero status panel
func (s *HeroStatsPanel) Load() {
animation, _ := s.asset.LoadAnimation(d2resource.Frame, d2resource.PaletteSky)
s.frame, _ = s.uiManager.NewSprite(animation)

View File

@ -69,7 +69,7 @@ func (g *Inventory) Close() {
g.isOpen = false
}
// Load loads the resources required by the inventory
// Load the resources required by the inventory
func (g *Inventory) Load() {
animation, _ := g.asset.LoadAnimation(d2resource.Frame, d2resource.PaletteSky)
g.frame, _ = g.uiManager.NewSprite(animation)

View File

@ -35,7 +35,7 @@ func main() {
panic(err)
}
asset, err := d2asset.NewAssetManager(renderer, nil)
asset, err := d2asset.NewAssetManager(renderer, d2config.Config, nil)
if err != nil {
panic(err)
}