From 7f6ae1b78508bf7170371d197c527d8faad5ed01 Mon Sep 17 00:00:00 2001 From: lord Date: Mon, 14 Sep 2020 11:47:11 -0700 Subject: [PATCH] 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 --- d2app/app.go | 5 +- d2common/d2fileformats/d2mpq/mpq.go | 2 +- .../d2fileformats/d2mpq/mpq_data_stream.go | 2 +- .../d2fileformats/d2tbl/text_dictionary.go | 9 +- d2common/d2interface/animation_manager.go | 9 - d2common/d2interface/archive.go | 2 +- d2common/d2interface/archive_manager.go | 10 - ...{archive_data_stream.go => data_stream.go} | 4 +- d2common/d2interface/file_manager.go | 10 - d2common/d2interface/font_manager.go | 8 - d2common/d2interface/palette_manager.go | 7 - d2common/d2loader/asset/asset.go | 5 +- d2common/d2loader/asset/types/source_types.go | 20 +- d2common/d2loader/filesystem/asset.go | 44 +++ d2common/d2loader/loader.go | 61 +++- d2common/d2loader/loader_test.go | 13 +- d2common/d2loader/mpq/asset.go | 61 +++- d2common/d2loader/mpq/source.go | 16 +- d2common/d2loader/testdata/D.mpq | Bin 815 -> 891 bytes .../d2asset => d2common/d2util}/palette.go | 2 +- d2core/d2asset/animation.go | 16 +- d2core/d2asset/animation_manager.go | 159 --------- d2core/d2asset/archive_manager.go | 119 ------- d2core/d2asset/archived_file_manager.go | 95 ----- d2core/d2asset/asset_manager.go | 330 ++++++++++++++++-- d2core/d2asset/d2asset.go | 42 +-- d2core/d2asset/dc6_animation.go | 16 +- d2core/d2asset/dcc_animation.go | 17 +- d2core/d2asset/font_manager.go | 87 ----- d2core/d2asset/palette_manager.go | 52 --- d2core/d2asset/palette_transform_manager.go | 37 -- d2core/d2map/d2maprenderer/tile_cache.go | 9 +- d2game/d2gamescreen/select_hero_class.go | 3 +- d2game/d2player/game_controls.go | 2 +- d2game/d2player/hero_stats_panel.go | 2 +- d2game/d2player/inventory.go | 2 +- main.go | 2 +- 37 files changed, 559 insertions(+), 721 deletions(-) delete mode 100644 d2common/d2interface/animation_manager.go delete mode 100644 d2common/d2interface/archive_manager.go rename d2common/d2interface/{archive_data_stream.go => data_stream.go} (59%) delete mode 100644 d2common/d2interface/file_manager.go delete mode 100644 d2common/d2interface/font_manager.go delete mode 100644 d2common/d2interface/palette_manager.go rename {d2core/d2asset => d2common/d2util}/palette.go (97%) delete mode 100644 d2core/d2asset/animation_manager.go delete mode 100644 d2core/d2asset/archive_manager.go delete mode 100644 d2core/d2asset/archived_file_manager.go delete mode 100644 d2core/d2asset/font_manager.go delete mode 100644 d2core/d2asset/palette_manager.go delete mode 100644 d2core/d2asset/palette_transform_manager.go diff --git a/d2app/app.go b/d2app/app.go index d5177355..686658f8 100644 --- a/d2app/app.go +++ b/d2app/app.go @@ -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" diff --git a/d2common/d2fileformats/d2mpq/mpq.go b/d2common/d2fileformats/d2mpq/mpq.go index 961f277c..432a30a9 100644 --- a/d2common/d2fileformats/d2mpq/mpq.go +++ b/d2common/d2fileformats/d2mpq/mpq.go @@ -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 { diff --git a/d2common/d2fileformats/d2mpq/mpq_data_stream.go b/d2common/d2fileformats/d2mpq/mpq_data_stream.go index ddbab1e4..db66260c 100644 --- a/d2common/d2fileformats/d2mpq/mpq_data_stream.go +++ b/d2common/d2fileformats/d2mpq/mpq_data_stream.go @@ -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 { diff --git a/d2common/d2fileformats/d2tbl/text_dictionary.go b/d2common/d2fileformats/d2tbl/text_dictionary.go index 640f5ee1..b4313ec4 100644 --- a/d2common/d2fileformats/d2tbl/text_dictionary.go +++ b/d2common/d2fileformats/d2tbl/text_dictionary.go @@ -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) diff --git a/d2common/d2interface/animation_manager.go b/d2common/d2interface/animation_manager.go deleted file mode 100644 index 00999d03..00000000 --- a/d2common/d2interface/animation_manager.go +++ /dev/null @@ -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) -} diff --git a/d2common/d2interface/archive.go b/d2common/d2interface/archive.go index 744afa20..5e9d60da 100644 --- a/d2common/d2interface/archive.go +++ b/d2common/d2interface/archive.go @@ -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) } diff --git a/d2common/d2interface/archive_manager.go b/d2common/d2interface/archive_manager.go deleted file mode 100644 index 95b64edb..00000000 --- a/d2common/d2interface/archive_manager.go +++ /dev/null @@ -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 -} diff --git a/d2common/d2interface/archive_data_stream.go b/d2common/d2interface/data_stream.go similarity index 59% rename from d2common/d2interface/archive_data_stream.go rename to d2common/d2interface/data_stream.go index fc2b864f..af953834 100644 --- a/d2common/d2interface/archive_data_stream.go +++ b/d2common/d2interface/data_stream.go @@ -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 diff --git a/d2common/d2interface/file_manager.go b/d2common/d2interface/file_manager.go deleted file mode 100644 index 6729d506..00000000 --- a/d2common/d2interface/file_manager.go +++ /dev/null @@ -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) -} diff --git a/d2common/d2interface/font_manager.go b/d2common/d2interface/font_manager.go deleted file mode 100644 index 34457e34..00000000 --- a/d2common/d2interface/font_manager.go +++ /dev/null @@ -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) -} diff --git a/d2common/d2interface/palette_manager.go b/d2common/d2interface/palette_manager.go deleted file mode 100644 index 7a3cb0d9..00000000 --- a/d2common/d2interface/palette_manager.go +++ /dev/null @@ -1,7 +0,0 @@ -package d2interface - -// PaletteManager is responsible for loading palettes -type PaletteManager interface { - Cacher - LoadPalette(palettePath string) (Palette, error) -} diff --git a/d2common/d2loader/asset/asset.go b/d2common/d2loader/asset/asset.go index 081caa09..417418d9 100644 --- a/d2common/d2loader/asset/asset.go +++ b/d2common/d2loader/asset/asset.go @@ -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) } diff --git a/d2common/d2loader/asset/types/source_types.go b/d2common/d2loader/asset/types/source_types.go index 8ebaebc5..563c3a7b 100644 --- a/d2common/d2loader/asset/types/source_types.go +++ b/d2common/d2loader/asset/types/source_types.go @@ -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) +} diff --git a/d2common/d2loader/filesystem/asset.go b/d2common/d2loader/filesystem/asset.go index 2eb2e4c4..2b622e8d 100644 --- a/d2common/d2loader/filesystem/asset.go +++ b/d2common/d2loader/filesystem/asset.go @@ -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() diff --git a/d2common/d2loader/loader.go b/d2common/d2loader/loader.go index 46c36155..91d5bf77 100644 --- a/d2common/d2loader/loader.go +++ b/d2common/d2loader/loader.go @@ -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 { diff --git a/d2common/d2loader/loader_test.go b/d2common/d2loader/loader_test.go index 46e566a4..e0f96181 100644 --- a/d2common/d2loader/loader_test.go +++ b/d2common/d2loader/loader_test.go @@ -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 { diff --git a/d2common/d2loader/mpq/asset.go b/d2common/d2loader/mpq/asset.go index 45c65933..4cd574ef 100644 --- a/d2common/d2loader/mpq/asset.go +++ b/d2common/d2loader/mpq/asset.go @@ -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() } diff --git a/d2common/d2loader/mpq/source.go b/d2common/d2loader/mpq/source.go index 375d57b7..4a326a42 100644 --- a/d2common/d2loader/mpq/source.go +++ b/d2common/d2loader/mpq/source.go @@ -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 +} diff --git a/d2common/d2loader/testdata/D.mpq b/d2common/d2loader/testdata/D.mpq index ad8b17d9190e205c4f8bb232fcc725a1d0a12c47..a75d806c9c667dedccd5a07eea91a7d5d184bcbe 100644 GIT binary patch delta 775 zcmZ3__M5HVHy}{z0s{j>H8TSPGXpEbY9rJ$N;1U1lWOUK;RG~gl>RP z3;|FYMwvnxQ(yuP3=C5z&b(dxPI^<`w0m2om#GEH>zXCs)M=F7v+wdh>j=rhDwtXr z-OILr-=_(e9(`lXTK&5=S=vQc`Jw#W-9r2>O6#n4-thBTDtkcOx$U^U0sDlS`A=?7 zykIi%l~g^G#8mHd=g*!!e_4B_#>1yxCw00!-5xCuQ+Yl`EPdM2W6K>_80vckEPg&K zD_R`w=VyCHlWCKF@E?%A#Ds(g>`BWn?Bq_cu=)C~eu03q^R(CN+Rt8>ko@xFd&7&i zy`15CTbX}mJh+&0Fkk+=_urKNKkN3L=3AWS^ymMz`Tjcf4>lWV&$Zn7`RSBO&KJ)N z85o|bSe_RDA9S4e$gWUr+vr9so{ILpp$lFsFOi@?7CK6;OY|^v53^(~RvK zZyU{G{aW&?Zr+bWUnQprgl(Ffe{n_gs;8&VKHQdFH!te7!5nv{8)xn>5n4Cn&4Zg$ zJ1&dm@3Vd?Gj)c0;FB%?6Hag$R!s>H7k_IrS+*|TOmo`NQvQ>-HN4h7vY4t>OR@{<$qJ^HkM~a)qzak)=${frE=4ry=RuJk0{S8w^tkW8nx#ooK5r; zGPCWp=ssGgUi7);kA2>wOEz~o3-$$@J3py;o9CuE`8|`0PH}UEj>xTv6Ruc)kc_oq juoZ9EoMxn3Tdvo6F46qq zj82$<0|Ubi10M7F)BWG7y<7R?ZZ*e&dAj~>#Y!_omh62wvFCN(f0$Aj9n%q$cVb$h z)596+b87k9j_b^EiZa|Ea^qj+?Aa0u|9*X)&nmzE*rFeo4zBlkt5G-~ves6A~VpOFZJl{FUv$4N?;puf zgB-WdB5fY7AyS}_RR`kN=WW`4tDQ|Roj4&OL1~j%e6nDTak03?v|_`fLaQyOruY0d z*?nRidZ;)-ji?10@h#oM_Sa>OQ%8=O{&@V?B*SazRiplF-4eaUkMGT1!tDC!)I)+)EF7R@a&#NS+V@1@%%LWIh zFOfMXv52uyOZeTJd3I+%1axe3othC142`X@h diff --git a/d2core/d2asset/palette.go b/d2common/d2util/palette.go similarity index 97% rename from d2core/d2asset/palette.go rename to d2common/d2util/palette.go index 1303dadc..19d1c428 100644 --- a/d2core/d2asset/palette.go +++ b/d2common/d2util/palette.go @@ -1,4 +1,4 @@ -package d2asset +package d2util import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" diff --git a/d2core/d2asset/animation.go b/d2core/d2asset/animation.go index a4caa0f4..fc031d59 100644 --- a/d2core/d2asset/animation.go +++ b/d2core/d2asset/animation.go @@ -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] diff --git a/d2core/d2asset/animation_manager.go b/d2core/d2asset/animation_manager.go deleted file mode 100644 index c7ba2513..00000000 --- a/d2core/d2asset/animation_manager.go +++ /dev/null @@ -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) -} diff --git a/d2core/d2asset/archive_manager.go b/d2core/d2asset/archive_manager.go deleted file mode 100644 index 210de0f9..00000000 --- a/d2core/d2asset/archive_manager.go +++ /dev/null @@ -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 -} diff --git a/d2core/d2asset/archived_file_manager.go b/d2core/d2asset/archived_file_manager.go deleted file mode 100644 index 4826b3e4..00000000 --- a/d2core/d2asset/archived_file_manager.go +++ /dev/null @@ -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 -} diff --git a/d2core/d2asset/asset_manager.go b/d2core/d2asset/asset_manager.go index c338adef..c4e4a93a 100644 --- a/d2core/d2asset/asset_manager.go +++ b/d2core/d2asset/asset_manager.go @@ -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 } diff --git a/d2core/d2asset/d2asset.go b/d2core/d2asset/d2asset.go index ec30a590..46900780 100644 --- a/d2core/d2asset/d2asset.go +++ b/d2core/d2asset/d2asset.go @@ -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 diff --git a/d2core/d2asset/dc6_animation.go b/d2core/d2asset/dc6_animation.go index 69e233d4..9a2f9c06 100644 --- a/d2core/d2asset/dc6_animation.go +++ b/d2core/d2asset/dc6_animation.go @@ -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 } diff --git a/d2core/d2asset/dcc_animation.go b/d2core/d2asset/dcc_animation.go index 02a7895e..5ad57604 100644 --- a/d2core/d2asset/dcc_animation.go +++ b/d2core/d2asset/dcc_animation.go @@ -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 { diff --git a/d2core/d2asset/font_manager.go b/d2core/d2asset/font_manager.go deleted file mode 100644 index 5145a0f8..00000000 --- a/d2core/d2asset/font_manager.go +++ /dev/null @@ -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 -} diff --git a/d2core/d2asset/palette_manager.go b/d2core/d2asset/palette_manager.go deleted file mode 100644 index 59f374e0..00000000 --- a/d2core/d2asset/palette_manager.go +++ /dev/null @@ -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 -} diff --git a/d2core/d2asset/palette_transform_manager.go b/d2core/d2asset/palette_transform_manager.go deleted file mode 100644 index 82d6d989..00000000 --- a/d2core/d2asset/palette_transform_manager.go +++ /dev/null @@ -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 -} diff --git a/d2core/d2map/d2maprenderer/tile_cache.go b/d2core/d2map/d2maprenderer/tile_cache.go index f5f1ef74..ff7629a7 100644 --- a/d2core/d2map/d2maprenderer/tile_cache.go +++ b/d2core/d2map/d2maprenderer/tile_cache.go @@ -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()) diff --git a/d2game/d2gamescreen/select_hero_class.go b/d2game/d2gamescreen/select_hero_class.go index b0f4b894..1c602a8c 100644 --- a/d2game/d2gamescreen/select_hero_class.go +++ b/d2game/d2gamescreen/select_hero_class.go @@ -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, diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index d4137e0b..9670e95e 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -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) diff --git a/d2game/d2player/hero_stats_panel.go b/d2game/d2player/hero_stats_panel.go index 7dcbc0b8..e5752061 100644 --- a/d2game/d2player/hero_stats_panel.go +++ b/d2game/d2player/hero_stats_panel.go @@ -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) diff --git a/d2game/d2player/inventory.go b/d2game/d2player/inventory.go index 9a4e067a..262d250e 100644 --- a/d2game/d2player/inventory.go +++ b/d2game/d2player/inventory.go @@ -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) diff --git a/main.go b/main.go index 94f1c925..8be4d67b 100644 --- a/main.go +++ b/main.go @@ -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) }