diff --git a/d2app/app.go b/d2app/app.go index afdf3ced..ad667a80 100644 --- a/d2app/app.go +++ b/d2app/app.go @@ -15,20 +15,18 @@ import ( "os/signal" "path/filepath" "runtime" - "runtime/pprof" "strconv" "strings" "sync" "syscall" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types" "github.com/pkg/profile" "golang.org/x/image/colornames" "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" ebiten2 "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio/ebiten" @@ -260,7 +258,7 @@ func (a *App) LoadConfig() (*d2config.Configuration, error) { return nil, err } - config.SetPath(filepath.Join(configAsset.Source().Path(), configAsset.Path())) + //config.SetPath(filepath.Join(configAsset.Source().Path(), configAsset.Path())) a.Infof("loaded configuration file from %s", config.Path()) @@ -270,8 +268,8 @@ func (a *App) LoadConfig() (*d2config.Configuration, error) { // Run executes the application and kicks off the entire game process func (a *App) Run() (err error) { // add our possible config directories - _, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath())) - _, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath())) + _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()), types.AssetSourceFileSystem) + _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()), types.AssetSourceFileSystem) if a.config, err = a.LoadConfig(); err != nil { return err @@ -322,192 +320,6 @@ func (a *App) Run() (err error) { return nil } -func (a *App) initialize() error { - if err := a.initConfig(a.config); err != nil { - return err - } - - a.initLanguage() - - if err := a.initDataDictionaries(); err != nil { - return err - } - - a.timeScale = 1.0 - a.lastTime = d2util.Now() - a.lastScreenAdvance = a.lastTime - - a.renderer.SetWindowIcon("d2logo.png") - a.terminal.BindLogger() - - terminalCommands := []struct { - name string - desc string - args []string - fn func(args []string) error - }{ - {"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap}, - {"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen}, - {"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame}, - {"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture}, - {"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture}, - {"vsync", "toggles vsync", nil, a.toggleVsync}, - {"fps", "toggle fps counter", nil, a.toggleFpsCounter}, - {"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale}, - {"quit", "exits the game", nil, a.quitGame}, - {"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground}, - {"js", "eval JS scripts", []string{"code"}, a.evalJS}, - } - - for _, cmd := range terminalCommands { - if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil { - a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error()) - } - } - - gui, err := d2gui.CreateGuiManager(a.asset, *a.Options.LogLevel, a.inputManager) - if err != nil { - return err - } - - a.guiManager = gui - - a.screen = d2screen.NewScreenManager(a.ui, *a.Options.LogLevel, a.guiManager) - - a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume) - - if err := a.loadStrings(); err != nil { - return err - } - - a.ui.Initialize() - - return nil -} - -const ( - fmtErrSourceNotFound = `file not found: %s - -Please check your config file at %s - -Also, verify that the MPQ files exist at %s - -Capitalization in the file name matters. -` -) - -func (a *App) initConfig(config *d2config.Configuration) error { - a.config = config - - for _, mpqName := range a.config.MpqLoadOrder { - cleanDir := filepath.Clean(a.config.MpqPath) - srcPath := filepath.Join(cleanDir, mpqName) - - _, err := a.asset.AddSource(srcPath) - if err != nil { - // nolint:stylecheck // we want a multiline error message here.. - return fmt.Errorf(fmtErrSourceNotFound, srcPath, a.config.Path(), a.config.MpqPath) - } - } - - return nil -} - -func (a *App) initLanguage() { - a.language = a.asset.LoadLanguage(d2resource.LocalLanguage) - a.asset.Loader.SetLanguage(&a.language) - - a.charset = d2resource.GetFontCharset(a.language) - a.asset.Loader.SetCharset(&a.charset) -} - -func (a *App) initDataDictionaries() error { - dictPaths := []string{ - d2resource.LevelType, d2resource.LevelPreset, d2resource.LevelWarp, - d2resource.ObjectType, d2resource.ObjectDetails, d2resource.Weapons, - d2resource.Armor, d2resource.Misc, d2resource.Books, d2resource.ItemTypes, - d2resource.UniqueItems, d2resource.Missiles, d2resource.SoundSettings, - d2resource.MonStats, d2resource.MonStats2, d2resource.MonPreset, - d2resource.MonProp, d2resource.MonType, d2resource.MonMode, - d2resource.MagicPrefix, d2resource.MagicSuffix, d2resource.ItemStatCost, - d2resource.ItemRatio, d2resource.StorePage, d2resource.Overlays, - d2resource.CharStats, d2resource.Hireling, d2resource.Experience, - d2resource.Gems, d2resource.QualityItems, d2resource.Runes, - d2resource.DifficultyLevels, d2resource.AutoMap, d2resource.LevelDetails, - d2resource.LevelMaze, d2resource.LevelSubstitutions, d2resource.CubeRecipes, - d2resource.SuperUniques, d2resource.Inventory, d2resource.Skills, - d2resource.SkillCalc, d2resource.MissileCalc, d2resource.Properties, - d2resource.SkillDesc, d2resource.BodyLocations, d2resource.Sets, - d2resource.SetItems, d2resource.AutoMagic, d2resource.TreasureClass, - d2resource.TreasureClassEx, d2resource.States, d2resource.SoundEnvirons, - d2resource.Shrines, d2resource.ElemType, d2resource.PlrMode, - d2resource.PetType, d2resource.NPC, d2resource.MonsterUniqueModifier, - d2resource.MonsterEquipment, d2resource.UniqueAppellation, d2resource.MonsterLevel, - d2resource.MonsterSound, d2resource.MonsterSequence, d2resource.PlayerClass, - d2resource.MonsterPlacement, d2resource.ObjectGroup, d2resource.CompCode, - d2resource.MonsterAI, d2resource.RarePrefix, d2resource.RareSuffix, - d2resource.Events, d2resource.Colors, d2resource.ArmorType, - d2resource.WeaponClass, d2resource.PlayerType, d2resource.Composite, - d2resource.HitClass, d2resource.UniquePrefix, d2resource.UniqueSuffix, - d2resource.CubeModifier, d2resource.CubeType, d2resource.HirelingDescription, - d2resource.LowQualityItems, - } - - a.Info("Initializing asset manager") - - for _, path := range dictPaths { - err := a.asset.LoadRecords(path) - if err != nil { - return err - } - } - - err := a.initAnimationData(d2resource.AnimationData) - if err != nil { - return err - } - - return nil -} - -const ( - fmtLoadAnimData = "loading animation data from: %s" -) - -func (a *App) initAnimationData(path string) error { - animDataBytes, err := a.asset.LoadFile(path) - if err != nil { - return err - } - - a.Debugf(fmtLoadAnimData, path) - - animData := d2data.LoadAnimationData(animDataBytes) - - a.Infof("Loaded %d animation data records", len(animData)) - - a.asset.Records.Animation.Data = animData - - return nil -} - -func (a *App) loadStrings() error { - tablePaths := []string{ - d2resource.PatchStringTable, - d2resource.ExpansionStringTable, - d2resource.StringTable, - } - - for _, tablePath := range tablePaths { - _, err := a.asset.LoadStringTable(tablePath) - if err != nil { - return err - } - } - - return nil -} - func (a *App) renderDebug(target d2interface.Surface) { if !a.showFPS { return @@ -636,57 +448,6 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 { return deltaAllocPerFrame * fps / bytesToMegabyte } -func (a *App) dumpHeap([]string) error { - if _, err := os.Stat("./pprof/"); os.IsNotExist(err) { - if err := os.Mkdir("./pprof/", 0750); err != nil { - a.Fatal(err.Error()) - } - } - - fileOut, err := os.Create("./pprof/heap.pprof") - if err != nil { - a.Error(err.Error()) - } - - if err := pprof.WriteHeapProfile(fileOut); err != nil { - a.Fatal(err.Error()) - } - - if err := fileOut.Close(); err != nil { - a.Fatal(err.Error()) - } - - return nil -} - -func (a *App) evalJS(args []string) error { - val, err := a.scriptEngine.Eval(args[0]) - if err != nil { - a.terminal.Errorf(err.Error()) - return nil - } - - a.Info("%s" + val) - - return nil -} - -func (a *App) toggleFullScreen([]string) error { - fullscreen := !a.renderer.IsFullScreen() - a.renderer.SetFullScreen(fullscreen) - a.terminal.Infof("fullscreen is now: %v", fullscreen) - - return nil -} - -func (a *App) setupCaptureFrame(args []string) error { - a.captureState = captureStateFrame - a.capturePath = args[0] - a.captureFrames = nil - - return nil -} - func (a *App) doCaptureFrame(target d2interface.Surface) error { fp, err := os.Create(a.capturePath) if err != nil { @@ -769,58 +530,6 @@ func (a *App) convertFramesToGif() error { return nil } -func (a *App) startAnimationCapture(args []string) error { - a.captureState = captureStateGif - a.capturePath = args[0] - a.captureFrames = nil - - return nil -} - -func (a *App) stopAnimationCapture([]string) error { - a.captureState = captureStateNone - - return nil -} - -func (a *App) toggleVsync([]string) error { - vsync := !a.renderer.GetVSyncEnabled() - a.renderer.SetVSyncEnabled(vsync) - a.terminal.Infof("vsync is now: %v", vsync) - - return nil -} - -func (a *App) toggleFpsCounter([]string) error { - a.showFPS = !a.showFPS - a.terminal.Infof("fps counter is now: %v", a.showFPS) - - return nil -} - -func (a *App) setTimeScale(args []string) error { - timeScale, err := strconv.ParseFloat(args[0], 64) - if err != nil || timeScale <= 0 { - a.terminal.Errorf("invalid time scale value") - return nil - } - - a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale) - a.timeScale = timeScale - - return nil -} - -func (a *App) quitGame([]string) error { - os.Exit(0) - return nil -} - -func (a *App) enterGuiPlayground([]string) error { - a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset)) - return nil -} - func createZeroedRing(n int) *ring.Ring { r := ring.New(n) for i := 0; i < n; i++ { diff --git a/d2app/console_commands.go b/d2app/console_commands.go new file mode 100644 index 00000000..3edd4cd3 --- /dev/null +++ b/d2app/console_commands.go @@ -0,0 +1,139 @@ +package d2app + +import ( + "os" + "runtime/pprof" + "strconv" + + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen" +) + +func (a *App) initTerminalCommands() { + terminalCommands := []struct { + name string + desc string + args []string + fn func(args []string) error + }{ + {"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap}, + {"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen}, + {"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame}, + {"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture}, + {"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture}, + {"vsync", "toggles vsync", nil, a.toggleVsync}, + {"fps", "toggle fps counter", nil, a.toggleFpsCounter}, + {"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale}, + {"quit", "exits the game", nil, a.quitGame}, + {"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground}, + {"js", "eval JS scripts", []string{"code"}, a.evalJS}, + } + + for _, cmd := range terminalCommands { + if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil { + a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error()) + } + } +} + +func (a *App) dumpHeap([]string) error { + if _, err := os.Stat("./pprof/"); os.IsNotExist(err) { + if err := os.Mkdir("./pprof/", 0750); err != nil { + a.Fatal(err.Error()) + } + } + + fileOut, err := os.Create("./pprof/heap.pprof") + if err != nil { + a.Error(err.Error()) + } + + if err := pprof.WriteHeapProfile(fileOut); err != nil { + a.Fatal(err.Error()) + } + + if err := fileOut.Close(); err != nil { + a.Fatal(err.Error()) + } + + return nil +} + +func (a *App) evalJS(args []string) error { + val, err := a.scriptEngine.Eval(args[0]) + if err != nil { + a.terminal.Errorf(err.Error()) + return nil + } + + a.Info("%s" + val) + + return nil +} + +func (a *App) toggleFullScreen([]string) error { + fullscreen := !a.renderer.IsFullScreen() + a.renderer.SetFullScreen(fullscreen) + a.terminal.Infof("fullscreen is now: %v", fullscreen) + + return nil +} + +func (a *App) setupCaptureFrame(args []string) error { + a.captureState = captureStateFrame + a.capturePath = args[0] + a.captureFrames = nil + + return nil +} + +func (a *App) startAnimationCapture(args []string) error { + a.captureState = captureStateGif + a.capturePath = args[0] + a.captureFrames = nil + + return nil +} + +func (a *App) stopAnimationCapture([]string) error { + a.captureState = captureStateNone + + return nil +} + +func (a *App) toggleVsync([]string) error { + vsync := !a.renderer.GetVSyncEnabled() + a.renderer.SetVSyncEnabled(vsync) + a.terminal.Infof("vsync is now: %v", vsync) + + return nil +} + +func (a *App) toggleFpsCounter([]string) error { + a.showFPS = !a.showFPS + a.terminal.Infof("fps counter is now: %v", a.showFPS) + + return nil +} + +func (a *App) setTimeScale(args []string) error { + timeScale, err := strconv.ParseFloat(args[0], 64) + if err != nil || timeScale <= 0 { + a.terminal.Errorf("invalid time scale value") + return nil + } + + a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale) + a.timeScale = timeScale + + return nil +} + +func (a *App) quitGame([]string) error { + os.Exit(0) + return nil +} + +func (a *App) enterGuiPlayground([]string) error { + a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset)) + return nil +} diff --git a/d2app/initialization.go b/d2app/initialization.go new file mode 100644 index 00000000..dd84021f --- /dev/null +++ b/d2app/initialization.go @@ -0,0 +1,177 @@ +package d2app + +import ( + "fmt" + "path/filepath" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" +) + +func (a *App) initialize() error { + if err := a.initConfig(a.config); err != nil { + return err + } + + a.initLanguage() + + if err := a.initDataDictionaries(); err != nil { + return err + } + + a.timeScale = 1.0 + a.lastTime = d2util.Now() + a.lastScreenAdvance = a.lastTime + + a.renderer.SetWindowIcon("d2logo.png") + a.terminal.BindLogger() + a.initTerminalCommands() + + gui, err := d2gui.CreateGuiManager(a.asset, *a.Options.LogLevel, a.inputManager) + if err != nil { + return err + } + + a.guiManager = gui + + a.screen = d2screen.NewScreenManager(a.ui, *a.Options.LogLevel, a.guiManager) + + a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume) + + if err := a.loadStrings(); err != nil { + return err + } + + a.ui.Initialize() + + return nil +} + +const ( + fmtErrSourceNotFound = `file not found: %s + +Please check your config file at %s + +Also, verify that the MPQ files exist at %s + +Capitalization in the file name matters. +` +) + +func (a *App) initConfig(config *d2config.Configuration) error { + a.config = config + + for _, mpqName := range a.config.MpqLoadOrder { + cleanDir := filepath.Clean(a.config.MpqPath) + srcPath := filepath.Join(cleanDir, mpqName) + + err := a.asset.AddSource(srcPath, types.AssetSourceMPQ) + if err != nil { + // nolint:stylecheck // we want a multiline error message here.. + return fmt.Errorf(fmtErrSourceNotFound, srcPath, a.config.Path(), a.config.MpqPath) + } + } + + return nil +} + +func (a *App) initLanguage() { + a.language = a.asset.LoadLanguage(d2resource.LocalLanguage) + a.asset.Loader.SetLanguage(&a.language) + + a.charset = d2resource.GetFontCharset(a.language) + a.asset.Loader.SetCharset(&a.charset) +} + +func (a *App) initDataDictionaries() error { + dictPaths := []string{ + d2resource.LevelType, d2resource.LevelPreset, d2resource.LevelWarp, + d2resource.ObjectType, d2resource.ObjectDetails, d2resource.Weapons, + d2resource.Armor, d2resource.Misc, d2resource.Books, d2resource.ItemTypes, + d2resource.UniqueItems, d2resource.Missiles, d2resource.SoundSettings, + d2resource.MonStats, d2resource.MonStats2, d2resource.MonPreset, + d2resource.MonProp, d2resource.MonType, d2resource.MonMode, + d2resource.MagicPrefix, d2resource.MagicSuffix, d2resource.ItemStatCost, + d2resource.ItemRatio, d2resource.StorePage, d2resource.Overlays, + d2resource.CharStats, d2resource.Hireling, d2resource.Experience, + d2resource.Gems, d2resource.QualityItems, d2resource.Runes, + d2resource.DifficultyLevels, d2resource.AutoMap, d2resource.LevelDetails, + d2resource.LevelMaze, d2resource.LevelSubstitutions, d2resource.CubeRecipes, + d2resource.SuperUniques, d2resource.Inventory, d2resource.Skills, + d2resource.SkillCalc, d2resource.MissileCalc, d2resource.Properties, + d2resource.SkillDesc, d2resource.BodyLocations, d2resource.Sets, + d2resource.SetItems, d2resource.AutoMagic, d2resource.TreasureClass, + d2resource.TreasureClassEx, d2resource.States, d2resource.SoundEnvirons, + d2resource.Shrines, d2resource.ElemType, d2resource.PlrMode, + d2resource.PetType, d2resource.NPC, d2resource.MonsterUniqueModifier, + d2resource.MonsterEquipment, d2resource.UniqueAppellation, d2resource.MonsterLevel, + d2resource.MonsterSound, d2resource.MonsterSequence, d2resource.PlayerClass, + d2resource.MonsterPlacement, d2resource.ObjectGroup, d2resource.CompCode, + d2resource.MonsterAI, d2resource.RarePrefix, d2resource.RareSuffix, + d2resource.Events, d2resource.Colors, d2resource.ArmorType, + d2resource.WeaponClass, d2resource.PlayerType, d2resource.Composite, + d2resource.HitClass, d2resource.UniquePrefix, d2resource.UniqueSuffix, + d2resource.CubeModifier, d2resource.CubeType, d2resource.HirelingDescription, + d2resource.LowQualityItems, + } + + a.Info("Initializing asset manager") + + for _, path := range dictPaths { + err := a.asset.LoadRecords(path) + if err != nil { + return err + } + } + + err := a.initAnimationData(d2resource.AnimationData) + if err != nil { + return err + } + + return nil +} + +const ( + fmtLoadAnimData = "loading animation data from: %s" +) + +func (a *App) initAnimationData(path string) error { + animDataBytes, err := a.asset.LoadFile(path) + if err != nil { + return err + } + + a.Debugf(fmtLoadAnimData, path) + + animData := d2data.LoadAnimationData(animDataBytes) + + a.Infof("Loaded %d animation data records", len(animData)) + + a.asset.Records.Animation.Data = animData + + return nil +} + +func (a *App) loadStrings() error { + tablePaths := []string{ + d2resource.PatchStringTable, + d2resource.ExpansionStringTable, + d2resource.StringTable, + } + + for _, tablePath := range tablePaths { + _, err := a.asset.LoadStringTable(tablePath) + if err != nil { + return err + } + } + + return nil +} diff --git a/d2common/d2fileformats/d2mpq/mpq_stream.go b/d2common/d2fileformats/d2mpq/mpq_stream.go index 5be4951d..dfd43802 100644 --- a/d2common/d2fileformats/d2mpq/mpq_stream.go +++ b/d2common/d2fileformats/d2mpq/mpq_stream.go @@ -90,7 +90,7 @@ func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, er toRead := count for toRead > 0 { if read, err = v.readInternal(buffer, offset, toRead); err != nil { - return 0, err + return readTotal, err } if read == 0 { @@ -128,7 +128,7 @@ func (v *Stream) readInternal(buffer []byte, offset, count uint32) (uint32, erro func (v *Stream) copy(buffer []byte, offset, pos, count uint32) (uint32, error) { bytesToCopy := d2math.Min(uint32(len(v.Data))-pos, count) if bytesToCopy <= 0 { - return 0, nil + return 0, io.EOF } copy(buffer[offset:offset+bytesToCopy], v.Data[pos:pos+bytesToCopy]) diff --git a/d2common/d2loader/asset/source.go b/d2common/d2loader/asset/source.go index 58834345..e6e83645 100644 --- a/d2common/d2loader/asset/source.go +++ b/d2common/d2loader/asset/source.go @@ -2,14 +2,13 @@ package asset import ( "fmt" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types" + "io" ) // Source is an abstraction for something that can load and list assets type Source interface { fmt.Stringer - Type() types.SourceType - Open(name string) (Asset, error) + Open(name string) (io.ReadSeeker, error) Path() string + Exists(subPath string) bool } diff --git a/d2common/d2loader/filesystem/loader_provider.go b/d2common/d2loader/filesystem/loader_provider.go new file mode 100644 index 00000000..904b54d0 --- /dev/null +++ b/d2common/d2loader/filesystem/loader_provider.go @@ -0,0 +1,12 @@ +package filesystem + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset" +) + +// OnAddSource is a shim method to allow loading of filesystem sources +func OnAddSource(path string) (asset.Source, error) { + return &Source{ + Root: path, + }, nil +} diff --git a/d2common/d2loader/filesystem/source.go b/d2common/d2loader/filesystem/source.go index fc5b06b1..2c87b7fb 100644 --- a/d2common/d2loader/filesystem/source.go +++ b/d2common/d2loader/filesystem/source.go @@ -1,6 +1,7 @@ package filesystem import ( + "io" "os" "path/filepath" @@ -22,21 +23,14 @@ func (s *Source) Type() types.SourceType { } // Open opens a file with the given sub-path within the Root dir of the file system source -func (s *Source) Open(subPath string) (asset.Asset, error) { - file, err := os.Open(s.fullPath(subPath)) +func (s *Source) Open(subPath string) (io.ReadSeeker, error) { + return os.Open(s.fullPath(subPath)) +} - if err == nil { - a := &Asset{ - assetType: types.Ext2AssetType(filepath.Ext(subPath)), - source: s, - path: subPath, - file: file, - } - - return a, nil - } - - return nil, err +// Exists returns true if the file exists +func (s *Source) Exists(subPath string) bool { + _, err := os.Stat(s.fullPath(subPath)) + return os.IsExist(err) } func (s *Source) fullPath(subPath string) string { diff --git a/d2common/d2loader/loader.go b/d2common/d2loader/loader.go index f3f6ae66..64c6bd1b 100644 --- a/d2common/d2loader/loader.go +++ b/d2common/d2loader/loader.go @@ -2,24 +2,25 @@ package d2loader import ( "fmt" - "os" + "io" "path/filepath" "strings" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/mpq" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/filesystem" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset" "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" ) const ( - defaultCacheBudget = 1024 * 1024 * 512 - defaultCacheEntryWeight = 1 - errFmtFileNotFound = "file not found: %s" + defaultCacheBudget = 1024 * 1024 * 512 + errFmtFileNotFound = "file not found: %s" ) const ( @@ -33,7 +34,12 @@ const ( // NewLoader creates a new loader func NewLoader(l d2util.LogLevel) (*Loader, error) { - loader := &Loader{} + loader := &Loader{ + LoaderProviders: make(map[types.SourceType]func(path string) (asset.Source, error), 2), + } + + loader.LoaderProviders[types.AssetSourceMPQ] = mpq.NewSource + loader.LoaderProviders[types.AssetSourceFileSystem] = filesystem.OnAddSource loader.Cache = d2cache.CreateCache(defaultCacheBudget) loader.Logger = d2util.NewLogger() @@ -51,7 +57,8 @@ type Loader struct { charset *string d2interface.Cache *d2util.Logger - Sources []asset.Source + LoaderProviders map[types.SourceType]func(path string) (asset.Source, error) + Sources []asset.Source } // SetLanguage sets the language for loader @@ -66,7 +73,7 @@ func (l *Loader) SetCharset(charset *string) { // 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) { +func (l *Loader) Load(subPath string) (io.ReadSeeker, error) { subPath = filepath.Clean(subPath) if l.language != nil { @@ -77,16 +84,6 @@ func (l *Loader) Load(subPath string) (asset.Asset, error) { subPath = strings.ReplaceAll(subPath, tableToken, *language) } - // first, we check the cache for an existing entry - if cached, found := l.Retrieve(subPath); found { - l.Debug(fmt.Sprintf("Retrieved `%s` from cache", subPath)) - - 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 for idx := range l.Sources { source := l.Sources[idx] @@ -99,9 +96,9 @@ func (l *Loader) Load(subPath string) (asset.Asset, error) { } srcBase, _ := filepath.Abs(source.Path()) - l.Info(fmt.Sprintf("from %s, loading %s", srcBase, subPath)) + l.Info(fmt.Sprintf("Loaded %s -> %s", srcBase, subPath)) - return loadedAsset, l.Insert(subPath, loadedAsset, defaultCacheEntryWeight) + return loadedAsset, nil } return nil, fmt.Errorf(errFmtFileNotFound, subPath) @@ -111,52 +108,45 @@ func (l *Loader) Load(subPath string) (asset.Asset, error) { // or a file on the host filesystem. In the case that it is a file, the file extension is used // to determine the type of asset source. In the case that the path points to a directory, a // FileSystemSource will be added. -func (l *Loader) AddSource(path string) (asset.Source, error) { +func (l *Loader) AddSource(path string, sourceType types.SourceType) error { if l.Sources == nil { l.Sources = make([]asset.Source, 0) } cleanPath := filepath.Clean(path) - info, err := os.Lstat(cleanPath) + source, err := l.LoaderProviders[sourceType](cleanPath) + if err != nil { - l.Error(err.Error()) - return nil, err + return err } - mode := info.Mode() + l.Infof("Adding source: '%s'", cleanPath) + l.Sources = append(l.Sources, source) - sourceType := types.AssetSourceUnknown - - if mode.IsDir() { - sourceType = types.AssetSourceFileSystem - } - - if mode.IsRegular() { - sourceType = types.CheckSourceType(cleanPath) - } - - switch sourceType { - case types.AssetSourceMPQ: - source, err := mpq.NewSource(cleanPath) - if err == nil { - l.Info(fmt.Sprintf("adding MPQ source `%s`", cleanPath)) - l.Sources = append(l.Sources, source) - - return source, nil - } - case types.AssetSourceFileSystem: - source := &filesystem.Source{ - Root: cleanPath, - } - - l.Info(fmt.Sprintf("adding filesystem source `%s`", cleanPath)) - l.Sources = append(l.Sources, source) - - return source, nil - case types.AssetSourceUnknown: - l.Warning(fmt.Sprintf("unknown asset source `%s`", cleanPath)) - } - - return nil, fmt.Errorf("unknown asset source `%s`", cleanPath) + return nil +} + +func (l *Loader) Exists(subPath string) bool { + subPath = filepath.Clean(subPath) + + if l.language != nil { + charset := l.charset + language := l.language + + subPath = strings.ReplaceAll(subPath, fontToken, *charset) + subPath = strings.ReplaceAll(subPath, tableToken, *language) + } + + // if it isn't in the cache, we check if each source can open the file + for idx := range l.Sources { + source := l.Sources[idx] + + // if the source can open the file, then we cache it and return it + if source.Exists(subPath) { + return true + } + } + + return false } diff --git a/d2common/d2loader/loader_test.go b/d2common/d2loader/loader_test.go index 6efbe78b..f04c3ec4 100644 --- a/d2common/d2loader/loader_test.go +++ b/d2common/d2loader/loader_test.go @@ -2,12 +2,13 @@ package d2loader import ( "fmt" + "io" "log" "testing" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" ) const ( @@ -36,11 +37,11 @@ func TestLoader_NewLoader(t *testing.T) { func TestLoader_AddSource(t *testing.T) { loader, _ := NewLoader(d2util.LogLevelDefault) - sourceA, errA := loader.AddSource(sourcePathA) - sourceB, errB := loader.AddSource(sourcePathB) - sourceC, errC := loader.AddSource(sourcePathC) - sourceD, errD := loader.AddSource(sourcePathD) - sourceE, errE := loader.AddSource(badSourcePath) + errA := loader.AddSource(sourcePathA, types.AssetSourceFileSystem) + errB := loader.AddSource(sourcePathB, types.AssetSourceFileSystem) + errC := loader.AddSource(sourcePathC, types.AssetSourceFileSystem) + errD := loader.AddSource(sourcePathD, types.AssetSourceFileSystem) + errE := loader.AddSource(badSourcePath, types.AssetSourceMPQ) if errA != nil { t.Error(errA) @@ -62,25 +63,6 @@ func TestLoader_AddSource(t *testing.T) { t.Error("expecting error on bad file path") } - if sourceA.String() != sourcePathA { - t.Error("source path not the same as what we added") - } - - if sourceB.String() != sourcePathB { - t.Error("source path not the same as what we added") - } - - if sourceC.String() != sourcePathC { - t.Error("source path not the same as what we added") - } - - if sourceD.String() != sourcePathD { - t.Error("source path not the same as what we added") - } - - if sourceE != nil { - t.Error("source for bad path should be nil") - } } // nolint:gocyclo // this is just a test, not a big deal if we ignore linter here @@ -88,25 +70,25 @@ func TestLoader_Load(t *testing.T) { loader, _ := NewLoader(d2util.LogLevelDefault) // we expect files common to any source to come from here - commonSource, err := loader.AddSource(sourcePathB) + err := loader.AddSource(sourcePathB, types.AssetSourceFileSystem) if err != nil { t.Fail() log.Print(err) } - _, err = loader.AddSource(sourcePathD) + err = loader.AddSource(sourcePathD, types.AssetSourceMPQ) if err != nil { t.Fail() log.Print(err) } - _, err = loader.AddSource(sourcePathA) + err = loader.AddSource(sourcePathA, types.AssetSourceFileSystem) if err != nil { t.Fail() log.Print(err) } - _, err = loader.AddSource(sourcePathC) + err = loader.AddSource(sourcePathC, types.AssetSourceFileSystem) if err != nil { t.Fail() log.Print(err) @@ -124,8 +106,6 @@ func TestLoader_Load(t *testing.T) { if entryCommon == nil || errCommon != nil { t.Error("common entry should exist") - } else if entryCommon.Source() != commonSource { - t.Error("common entry should come from the first loader source") } if errA != nil || errB != nil || errC != nil || errD != nil { @@ -145,7 +125,7 @@ func TestLoader_Load(t *testing.T) { buffer := make([]byte, 1) tests := []struct { - entry asset.Asset + entry io.ReadSeeker data string }{ {entryCommon, "b"}, // sourcePathB is loaded first, we expect a "b" @@ -172,8 +152,8 @@ func TestLoader_Load(t *testing.T) { got := string(result[0]) if got != expected { - fmtStr := "unexpected data in file %s, loaded from source `%s`: expected `%s`, got `%s`" - msg := fmt.Sprintf(fmtStr, entry.Path(), entry.Source(), expected, got) + fmtStr := "unexpected data in file, expected %q, got %q" + msg := fmt.Sprintf(fmtStr, expected, got) t.Error(msg) } } diff --git a/d2common/d2loader/mpq/asset.go b/d2common/d2loader/mpq/asset.go index 4cd574ef..17ddc0a9 100644 --- a/d2common/d2loader/mpq/asset.go +++ b/d2common/d2loader/mpq/asset.go @@ -2,7 +2,6 @@ package mpq import ( "fmt" - "io" "path/filepath" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -42,12 +41,13 @@ func (a *Asset) Path() string { // Read will read asset data into the given buffer func (a *Asset) Read(buf []byte) (n int, err error) { - totalRead, err := a.stream.Read(buf) - if totalRead == 0 { - return 0, io.EOF - } - - return totalRead, err + 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 diff --git a/d2common/d2loader/mpq/source.go b/d2common/d2loader/mpq/source.go index d5b4236d..c83e470b 100644 --- a/d2common/d2loader/mpq/source.go +++ b/d2common/d2loader/mpq/source.go @@ -1,12 +1,12 @@ package mpq import ( + "io" "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2mpq" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader/asset/types" ) // static check that Source implements AssetSource @@ -27,27 +27,16 @@ type Source struct { MPQ d2interface.Archive } -// Type returns the asset type, for MPQ's it always returns the MPQ asset source type -func (v *Source) Type() types.SourceType { - return types.AssetSourceMPQ +// Open attempts to open a file within the MPQ archive +func (v *Source) Open(name string) (a io.ReadSeeker, err error) { + name = cleanName(name) + return v.MPQ.ReadFileStream(name) } -// 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 { - return nil, err - } - - a = &Asset{ - source: v, - stream: stream, - path: name, - } - - return a, nil +// Exists returns true if the file exists +func (v *Source) Exists(subPath string) bool { + subPath = cleanName(subPath) + return v.MPQ.Contains(subPath) } // Path returns the path of the MPQ on the host filesystem diff --git a/d2core/d2asset/asset_manager.go b/d2core/d2asset/asset_manager.go index 359befa0..fb5f7cc7 100644 --- a/d2core/d2asset/asset_manager.go +++ b/d2core/d2asset/asset_manager.go @@ -3,8 +3,17 @@ package d2asset import ( "fmt" "image/color" + "io" + "io/ioutil" + "path/filepath" "strconv" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" @@ -20,7 +29,6 @@ import ( "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" ) @@ -33,6 +41,10 @@ const ( fontBudget = 128 paletteBudget = 64 paletteTransformBudget = 64 + dt1Budget = 4096 * 2048 * 128 + ds1Budget = 4096 * 2048 * 128 + cofBudget = 4096 * 2048 * 128 + dccBudget = 4096 * 2048 * 128 ) const ( @@ -53,6 +65,10 @@ type AssetManager struct { *d2util.Logger *d2loader.Loader tables []d2tbl.TextDictionary + dt1s d2interface.Cache + ds1s d2interface.Cache + cofs d2interface.Cache + dccs d2interface.Cache animations d2interface.Cache fonts d2interface.Cache palettes d2interface.Cache @@ -69,7 +85,7 @@ func (am *AssetManager) SetLogLevel(level d2util.LogLevel) { } // LoadAsset loads an asset -func (am *AssetManager) LoadAsset(filePath string) (asset.Asset, error) { +func (am *AssetManager) LoadAsset(filePath string) (io.ReadSeeker, error) { data, err := am.Loader.Load(filePath) if err != nil { errStr := fmt.Sprintf(fmtLoadAsset, filePath, err.Error()) @@ -81,19 +97,19 @@ func (am *AssetManager) LoadAsset(filePath string) (asset.Asset, error) { } // LoadFileStream streams an MPQ file from a source file path -func (am *AssetManager) LoadFileStream(filePath string) (d2interface.DataStream, error) { +func (am *AssetManager) LoadFileStream(filePath string) (io.ReadSeeker, error) { am.Logger.Debugf("Loading FileStream: %s", filePath) 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) { +func (am *AssetManager) LoadFile(filePath string) ([]byte, error) { // I DO NOT LIKE THIS! - Essial fileAsset, err := am.LoadAsset(filePath) if err != nil { return nil, err } - data, err := fileAsset.Data() + data, err := ioutil.ReadAll(fileAsset) if err != nil { return nil, err } @@ -103,13 +119,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) { + filePath = filepath.Clean(filePath) + am.Logger.Debugf("Checking if file exists %s", filePath) - if loadedAsset, err := am.Loader.Load(filePath); err != nil || loadedAsset == nil { - return false, err - } - - return true, nil + return am.Loader.Exists(filePath), nil } // LoadLanguage loads language from resource path @@ -147,11 +161,6 @@ func (am *AssetManager) LoadAnimationWithEffect(animationPath, palettePath strin am.Debugf(fmtLoadAnimation, animationPath, palettePath, effect) - animAsset, err := am.LoadAsset(animationPath) - if err != nil { - return nil, err - } - palette, err := am.LoadPalette(palettePath) if err != nil { return nil, err @@ -159,7 +168,7 @@ func (am *AssetManager) LoadAnimationWithEffect(animationPath, palettePath strin var animation d2interface.Animation - switch animAsset.Type() { + switch types.Ext2AssetType(filepath.Ext(animationPath)) { case types.AssetTypeDC6: animation, err = am.loadDC6(animationPath, palette, effect) if err != nil { @@ -171,7 +180,7 @@ func (am *AssetManager) LoadAnimationWithEffect(animationPath, palettePath strin return nil, err } default: - return nil, fmt.Errorf("unknown Animation format for file: %s", animAsset.Path()) + return nil, fmt.Errorf("unknown Animation format for file: %s", animationPath) } err = am.animations.Insert(cachePath, animation, defaultCacheEntryWeight) @@ -237,12 +246,7 @@ func (am *AssetManager) LoadPalette(palettePath string) (d2interface.Palette, er return cached.(d2interface.Palette), nil } - paletteAsset, err := am.LoadAsset(palettePath) - if err != nil { - return nil, err - } - - if paletteAsset.Type() != types.AssetTypePalette { + if types.Ext2AssetType(filepath.Ext(palettePath)) != types.AssetTypePalette { return nil, fmt.Errorf("not an instance of a palette: %s", palettePath) } @@ -390,12 +394,7 @@ func (am *AssetManager) loadDC6(path string, // loadDCC creates an Animation from d2dcc.DCC and d2dat.DATPalette func (am *AssetManager) loadDCC(path string, palette d2interface.Palette, effect d2enum.DrawEffect) (d2interface.Animation, error) { - dccData, err := am.LoadFile(path) - if err != nil { - return nil, err - } - - dcc, err := d2dcc.Load(dccData) + dcc, err := am.LoadDCC(path) if err != nil { return nil, err } @@ -448,6 +447,10 @@ func (am *AssetManager) commandAssetSpam(term d2interface.Terminal) func([]strin am.fonts.SetVerbose(verbose) am.transforms.SetVerbose(verbose) am.animations.SetVerbose(verbose) + am.dt1s.SetVerbose(verbose) + am.ds1s.SetVerbose(verbose) + am.dccs.SetVerbose(verbose) + am.cofs.SetVerbose(verbose) return nil } @@ -474,6 +477,99 @@ func (am *AssetManager) commandAssetClear([]string) error { am.transforms.Clear() am.animations.Clear() am.fonts.Clear() + am.dt1s.Clear() + am.ds1s.Clear() + am.dccs.Clear() + am.cofs.Clear() return nil } + +func (am *AssetManager) LoadDT1(dt1Path string) (*d2dt1.DT1, error) { + if dt1Value, found := am.dt1s.Retrieve(dt1Path); found { + return dt1Value.(*d2dt1.DT1), nil + } + + fileData, err := am.LoadFile("/data/global/tiles/" + dt1Path) + if err != nil { + return nil, fmt.Errorf("Could not load /data/global/tiles/%s", dt1Path) + } + + dt1, err := d2dt1.LoadDT1(fileData) + if err != nil { + return nil, err + } + + if err := am.dt1s.Insert(dt1Path, dt1, defaultCacheEntryWeight); err != nil { + return nil, err + } + + return dt1, nil +} + +func (am *AssetManager) LoadDS1(ds1Path string) (*d2ds1.DS1, error) { + if ds1Value, found := am.dt1s.Retrieve(ds1Path); found { + return ds1Value.(*d2ds1.DS1), nil + } + + fileData, err := am.LoadFile("/data/global/tiles/" + ds1Path) + if err != nil { + return nil, err + } + + ds1, err := d2ds1.LoadDS1(fileData) + if err != nil { + return nil, err + } + + if err := am.dt1s.Insert(ds1Path, ds1, defaultCacheEntryWeight); err != nil { + return nil, err + } + + return ds1, nil + +} + +func (am *AssetManager) LoadCOF(cofPath string) (*d2cof.COF, error) { + if cofValue, found := am.cofs.Retrieve(cofPath); found { + return cofValue.(*d2cof.COF), nil + } + + fileData, err := am.LoadFile(cofPath) + if err != nil { + return nil, err + } + + cof, err := d2cof.Load(fileData) + if err != nil { + return nil, err + } + + if err := am.cofs.Insert(cofPath, cof, defaultCacheEntryWeight); err != nil { + return nil, err + } + + return cof, nil +} + +func (am *AssetManager) LoadDCC(dccPath string) (*d2dcc.DCC, error) { + if dccValue, found := am.dccs.Retrieve(dccPath); found { + return dccValue.(*d2dcc.DCC), nil + } + + fileData, err := am.LoadFile(dccPath) + if err != nil { + return nil, err + } + + dcc, err := d2dcc.Load(fileData) + if err != nil { + return nil, err + } + + if err := am.dccs.Insert(dccPath, dcc, defaultCacheEntryWeight); err != nil { + return nil, err + } + + return dcc, nil +} diff --git a/d2core/d2asset/composite.go b/d2core/d2asset/composite.go index cd5b5a8a..73ebf663 100644 --- a/d2core/d2asset/composite.go +++ b/d2core/d2asset/composite.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strings" + "sync" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof" @@ -161,15 +162,26 @@ func (c *Composite) SetDirection(direction int) { return } + wg := sync.WaitGroup{} + c.direction = direction + wg.Add(len(c.mode.layers)) + for layerIdx := range c.mode.layers { - layer := c.mode.layers[layerIdx] - if layer != nil { - if err := layer.SetDirection(c.direction); err != nil { - fmt.Printf("failed to set direction of layer: %d, err: %v\n", layerIdx, err) + go func(idx int) { + defer wg.Done() + + layer := c.mode.layers[idx] + + if layer != nil { + if err := layer.SetDirection(c.direction); err != nil { + fmt.Printf("failed to set direction of layer: %d, err: %v\n", idx, err) + } } - } + }(layerIdx) } + + wg.Wait() } // GetDirection returns the current direction the composite is facing @@ -248,7 +260,7 @@ func (c *Composite) createMode(animationMode animationMode, weaponClass string) return nil, fmt.Errorf("composite not found at path '%s': %v", cofPath, err) } - cof, err := c.loadCOF(cofPath) + cof, err := c.LoadCOF(cofPath) if err != nil { return nil, err } @@ -306,13 +318,13 @@ func (c *Composite) loadCompositeLayer(layerKey, layerValue, animationMode, weap fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dc6", c.basePath, c.token, layerKey, c.token, layerKey, layerValue, animationMode, weaponClass), } - for _, animationPath := range animationPaths { - exists, err := c.FileExists(animationPath) + for idx := range animationPaths { + exists, err := c.FileExists(animationPaths[idx]) if !exists || err != nil { - return nil, fmt.Errorf("animation path '%s' not found: %v", animationPath, err) + return nil, fmt.Errorf("animation path '%s' not found: %v", animationPaths[idx], err) } - animation, err := c.LoadAnimationWithEffect(animationPath, palettePath, drawEffect) + animation, err := c.LoadAnimationWithEffect(animationPaths[idx], palettePath, drawEffect) if err == nil { return animation, nil } @@ -359,15 +371,6 @@ func (c *Composite) updateSize() { c.size.Height = biggestH } -func (c *Composite) loadCOF(cofPath string) (*d2cof.COF, error) { - cofData, err := c.LoadFile(cofPath) - if err != nil { - return nil, err - } - - return d2cof.Load(cofData) -} - func baseString(baseType d2enum.ObjectType) string { switch baseType { case d2enum.ObjectTypePlayer: diff --git a/d2core/d2asset/d2asset.go b/d2core/d2asset/d2asset.go index 0e721554..ed5260c4 100644 --- a/d2core/d2asset/d2asset.go +++ b/d2core/d2asset/d2asset.go @@ -32,6 +32,10 @@ func NewAssetManager(logLevel d2util.LogLevel) (*AssetManager, error) { fonts: d2cache.CreateCache(fontBudget), palettes: d2cache.CreateCache(paletteBudget), transforms: d2cache.CreateCache(paletteTransformBudget), + dt1s: d2cache.CreateCache(dt1Budget), + ds1s: d2cache.CreateCache(ds1Budget), + cofs: d2cache.CreateCache(cofBudget), + dccs: d2cache.CreateCache(dccBudget), Records: records, } diff --git a/d2core/d2audio/ebiten/ebiten_audio_provider.go b/d2core/d2audio/ebiten/ebiten_audio_provider.go index ddee16b1..9fd6ea3c 100644 --- a/d2core/d2audio/ebiten/ebiten_audio_provider.go +++ b/d2core/d2audio/ebiten/ebiten_audio_provider.go @@ -137,6 +137,10 @@ func (eap *AudioProvider) createSoundEffect(sfx string, context *audio.Context, soundFile += sfx } + if fileExists, _ := eap.asset.FileExists(soundFile); !fileExists { + soundFile = "data/global/music/" + sfx + } + audioData, err := eap.asset.LoadFileStream(soundFile) if err != nil { @@ -153,6 +157,10 @@ func (eap *AudioProvider) createSoundEffect(sfx string, context *audio.Context, eap.Fatal(err.Error()) } + if d == nil { + eap.Fatal("Decoded data is nil") + } + var player *audio.Player if loop { diff --git a/d2core/d2audio/ebiten/ebiten_sound_effect.go b/d2core/d2audio/ebiten/ebiten_sound_effect.go index 84a35d83..05816f81 100644 --- a/d2core/d2audio/ebiten/ebiten_sound_effect.go +++ b/d2core/d2audio/ebiten/ebiten_sound_effect.go @@ -3,13 +3,15 @@ package ebiten import ( "io" "math" + "sync" "github.com/hajimehoshi/ebiten/v2/audio" ) type panStream struct { io.ReadSeeker - pan float64 // -1: left; 0: center; 1: right + pan float64 // -1: left; 0: center; 1: right + Lock sync.Mutex } const ( @@ -24,6 +26,9 @@ func newPanStreamFromReader(src io.ReadSeeker) *panStream { } func (s *panStream) Read(p []byte) (n int, err error) { + s.Lock.Lock() + defer s.Lock.Unlock() + n, err = s.ReadSeeker.Read(p) if err != nil { return @@ -54,7 +59,9 @@ type SoundEffect struct { // SetPan sets the audio pan, left is -1.0, center is 0.0, right is 1.0 func (v *SoundEffect) SetPan(pan float64) { + v.panStream.Lock.Lock() v.panStream.pan = pan + v.panStream.Lock.Unlock() } // SetVolume ets the volume diff --git a/d2core/d2map/d2mapengine/engine.go b/d2core/d2map/d2mapengine/engine.go index 97ae289d..0fc2b0fb 100644 --- a/d2core/d2map/d2mapengine/engine.go +++ b/d2core/d2map/d2mapengine/engine.go @@ -8,7 +8,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" "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/d2geom" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -97,13 +96,7 @@ func (m *MapEngine) addDT1(fileName string) { } } - fileData, err := m.asset.LoadFile("/data/global/tiles/" + fileName) - if err != nil { - m.Fatalf("Could not load /data/global/tiles/%s", fileName) - return - } - - dt1, err := d2dt1.LoadDT1(fileData) + dt1, err := m.asset.LoadDT1(fileName) if err != nil { m.Error(err.Error()) } @@ -120,12 +113,7 @@ func (m *MapEngine) AddDS1(fileName string) { return } - fileData, err := m.asset.LoadFile("/data/global/tiles/" + fileName) - if err != nil { - panic(err) - } - - ds1, err := d2ds1.LoadDS1(fileData) + ds1, err := m.asset.LoadDS1(fileName) if err != nil { m.Error(err.Error()) } diff --git a/d2core/d2map/d2maprenderer/renderer.go b/d2core/d2map/d2maprenderer/renderer.go index e4abd367..d693e578 100644 --- a/d2core/d2map/d2maprenderer/renderer.go +++ b/d2core/d2map/d2maprenderer/renderer.go @@ -88,11 +88,11 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere result.Camera.position = &startPosition result.viewport.SetCamera(&result.Camera) - if err := term.Bind("mapdebugvis", "set map debug visualization level", nil, result.commandMapDebugVis); err != nil { + if err := term.Bind("mapdebugvis", "set map debug visualization level", []string{"level"}, result.commandMapDebugVis); err != nil { result.Errorf("could not bind the mapdebugvis action, err: %v", err) } - if err := term.Bind("entitydebugvis", "set entity debug visualization level", nil, result.commandEntityDebugVis); err != nil { + if err := term.Bind("entitydebugvis", "set entity debug visualization level", []string{"level"}, result.commandEntityDebugVis); err != nil { result.Errorf("could not bind the entitydebugvis action, err: %v", err) } @@ -109,6 +109,10 @@ func (mr *MapRenderer) UnbindTerminalCommands(term d2interface.Terminal) error { } func (mr *MapRenderer) commandMapDebugVis(args []string) error { + if len(args) < 1 { + return fmt.Errorf("invalid argument supplied") + } + level, err := strconv.Atoi(args[0]) if err != nil { return fmt.Errorf("invalid argument supplied") diff --git a/d2core/d2map/d2mapstamp/factory.go b/d2core/d2map/d2mapstamp/factory.go index 774ee0a3..f8072509 100644 --- a/d2core/d2map/d2mapstamp/factory.go +++ b/d2core/d2map/d2mapstamp/factory.go @@ -8,7 +8,6 @@ import ( "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/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" ) @@ -54,12 +53,7 @@ func (f *StampFactory) LoadStamp(levelType d2enum.RegionIdType, levelPreset, fil continue } - fileData, err := f.asset.LoadFile("/data/global/tiles/" + levelTypeDt1) - if err != nil { - panic(err) - } - - dt1, err := d2dt1.LoadDT1(fileData) + dt1, err := f.asset.LoadDT1(levelTypeDt1) if err != nil { f.Error(err.Error()) return nil diff --git a/d2thread/mainthread.go b/d2thread/mainthread.go new file mode 100644 index 00000000..160aea83 --- /dev/null +++ b/d2thread/mainthread.go @@ -0,0 +1,94 @@ +// Package d2thread is a package graciously taken from https://github.com/faiface/mainthread +package d2thread + +import ( + "errors" + "runtime" +) + +// CallQueueCap is the capacity of the call queue. This means how many calls to CallNonBlock will not +// block until some call finishes. +// +// The default value is 16 and should be good for 99% usecases. +var ( + callQueue chan func() //nolint:gochecknoglobals +) + +func init() { + runtime.LockOSThread() +} + +func checkRun() { + if callQueue == nil { + panic(errors.New("mainthread: did not call Run")) + } +} + +// Run enables mainthread package functionality. To use mainthread package, put your main function +// code into the run function (the argument to Run) and simply call Run from the real main function. +// +// Run returns when run (argument) function finishes. +func Run(run func()) { + var CallQueueCap = 16 + + callQueue = make(chan func(), CallQueueCap) + + done := make(chan struct{}) + + go func() { + run() + done <- struct{}{} + }() + + for { + select { + case f := <-callQueue: + f() + case <-done: + return + } + } +} + +// CallNonBlock queues function f on the main thread and returns immediately. Does not wait until f +// finishes. +func CallNonBlock(f func()) { + checkRun() + callQueue <- f +} + +// Call queues function f on the main thread and blocks until the function f finishes. +func Call(f func()) { + checkRun() + + done := make(chan struct{}) + callQueue <- func() { + f() + done <- struct{}{} + } + <-done +} + +// CallErr queues function f on the main thread and returns an error returned by f. +func CallErr(f func() error) error { + checkRun() + + errChan := make(chan error) + callQueue <- func() { + errChan <- f() + } + + return <-errChan +} + +// CallVal queues function f on the main thread and returns a value returned by f. +func CallVal(f func() interface{}) interface{} { + checkRun() + + respChan := make(chan interface{}) + callQueue <- func() { + respChan <- f() + } + + return <-respChan +} diff --git a/go.sum b/go.sum index 7b52f021..357a3695 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0 h1:tDnuU0igiBiQFjs github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0/go.mod h1:h/5OEGj4G+fpYxluLjSMZbFY011ZxAntO98nCl8mrCs= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4 h1:EBTWhcAX7rNQ80RLwLCpHZBBrJuzallFHnF+yMXo928= github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=