diff --git a/d2app/app.go b/d2app/app.go index 9e7bb75e..b9783342 100644 --- a/d2app/app.go +++ b/d2app/app.go @@ -17,8 +17,6 @@ import ( "strings" "sync" - "golang.org/x/image/colornames" - "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" @@ -34,7 +32,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen" "github.com/OpenDiablo2/OpenDiablo2/d2script" "github.com/pkg/profile" - + "golang.org/x/image/colornames" "gopkg.in/alecthomas/kingpin.v2" ) @@ -47,7 +45,6 @@ type App struct { captureState captureState capturePath string captureFrames []*image.RGBA - profileOption string gitBranch string gitCommit string terminal d2interface.Terminal @@ -62,9 +59,12 @@ type bindTerminalEntry struct { action interface{} } -const defaultFPS = 0.04 // 1/25 -const bytesToMegabyte = 1024 * 1024 -const nSamplesTAlloc = 100 +const ( + defaultFPS = 0.04 // 1/25 + bytesToMegabyte = 1024 * 1024 + nSamplesTAlloc = 100 + debugPopN = 6 +) // Create creates a new instance of the application func Create(gitBranch, gitCommit string, @@ -84,7 +84,7 @@ func Create(gitBranch, gitCommit string, } // Run executes the application and kicks off the entire game process -func (p *App) Run() { +func (p *App) Run() error { profileOption := kingpin.Flag("profile", "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)").String() kingpin.Parse() @@ -99,12 +99,10 @@ func (p *App) Run() { // If we fail to initialize, we will show the error screen if err := p.initialize(); err != nil { if gameErr := p.renderer.Run(updateInitError, 800, 600, windowTitle); gameErr != nil { - log.Fatal(gameErr) + return gameErr } - log.Fatal(err) - - return + return err } d2screen.SetNextScreen(d2gamescreen.CreateMainMenu(p.renderer, p.audio, p.terminal)) @@ -116,8 +114,10 @@ func (p *App) Run() { d2common.SetBuildInfo(p.gitBranch, p.gitCommit) if err := p.renderer.Run(p.update, 800, 600, windowTitle); err != nil { - log.Panic(err) + return err } + + return nil } func (p *App) initialize() error { @@ -125,12 +125,8 @@ func (p *App) initialize() error { p.lastTime = d2common.Now() p.lastScreenAdvance = p.lastTime - if err := d2config.Load(); err != nil { - return err - } - - config := d2config.Get() - d2resource.LanguageCode = config.Language() + config := d2config.Config + d2resource.LanguageCode = config.Language p.renderer.SetWindowIcon("d2logo.png") p.terminal.BindLogger() @@ -164,7 +160,7 @@ func (p *App) initialize() error { return err } - p.audio.SetVolumes(config.BgmVolume(), config.SfxVolume()) + p.audio.SetVolumes(config.BgmVolume, config.SfxVolume) if err := p.loadDataDict(); err != nil { return err @@ -280,7 +276,7 @@ func (p *App) renderDebug(target d2interface.Surface) error { target.DrawText("NumGC " + strconv.FormatInt(int64(m.NumGC), 10)) target.PushTranslation(0, 16) target.DrawText("Coords " + strconv.FormatInt(int64(cx), 10) + "," + strconv.FormatInt(int64(cy), 10)) - target.PopN(6) //nolint:gomnd This is the number of records we have popped + target.PopN(debugPopN) return nil } @@ -584,7 +580,7 @@ func updateInitError(target d2interface.Surface) error { target.PushTranslation(width/5, height/2) target.DrawText(`Could not find the MPQ files in the directory: - %s\nPlease put the files and re-run the game.`, d2config.Get().MpqPath()) + %s\nPlease put the files and re-run the game.`, d2config.Config.MpqPath) return nil } diff --git a/d2common/d2interface/configuration.go b/d2common/d2interface/configuration.go deleted file mode 100644 index 71b23714..00000000 --- a/d2common/d2interface/configuration.go +++ /dev/null @@ -1,21 +0,0 @@ -package d2interface - -// Configuration saves, loads, and returns the OpenDiablo2 -// configuration. This is either loaded from disk, or generated -// when one is not found. -type Configuration interface { - Load() error - Save() error - // Get() Configuration - MpqLoadOrder() []string - Language() string - MpqPath() string - TicksPerSecond() int - FpsCap() int - SfxVolume() float64 - BgmVolume() float64 - FullScreen() bool - RunInBackground() bool - VsyncEnabled() bool - Backend() string -} diff --git a/d2core/d2asset/archive_manager.go b/d2core/d2asset/archive_manager.go index 78631e70..039d1085 100644 --- a/d2core/d2asset/archive_manager.go +++ b/d2core/d2asset/archive_manager.go @@ -5,15 +5,15 @@ import ( "path" "sync" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2mpq" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" ) type archiveManager struct { cache d2interface.Cache - config d2interface.Configuration + config *d2config.Configuration archives []d2interface.Archive mutex sync.Mutex } @@ -22,7 +22,7 @@ const ( archiveBudget = 1024 * 1024 * 512 ) -func createArchiveManager(config d2interface.Configuration) d2interface.ArchiveManager { +func createArchiveManager(config *d2config.Configuration) d2interface.ArchiveManager { return &archiveManager{cache: d2common.CreateCache(archiveBudget), config: config} } @@ -85,14 +85,14 @@ func (am *archiveManager) LoadArchive(archivePath string) (d2interface.Archive, // CacheArchiveEntries updates the archive entries func (am *archiveManager) CacheArchiveEntries() error { - if len(am.archives) == len(am.config.MpqLoadOrder()) { + 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) + for _, archiveName := range am.config.MpqLoadOrder { + archivePath := path.Join(am.config.MpqPath, archiveName) archive, err := am.LoadArchive(archivePath) if err != nil { diff --git a/d2core/d2asset/archived_file_manager.go b/d2core/d2asset/archived_file_manager.go index 9b3593c5..fded9517 100644 --- a/d2core/d2asset/archived_file_manager.go +++ b/d2core/d2asset/archived_file_manager.go @@ -1,11 +1,12 @@ package d2asset import ( - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" ) const ( @@ -15,10 +16,10 @@ const ( type fileManager struct { cache d2interface.Cache archiveManager d2interface.ArchiveManager - config d2interface.Configuration + config *d2config.Configuration } -func createFileManager(config d2interface.Configuration, +func createFileManager(config *d2config.Configuration, archiveManager d2interface.ArchiveManager) d2interface.ArchivedFileManager { return &fileManager{ d2common.CreateCache(fileBudget), @@ -90,7 +91,7 @@ func (fm *fileManager) removeLocaleTokens(filePath string) string { tableToken := d2resource.LanguageTableToken fontToken := d2resource.LanguageFontToken - filePath = strings.ReplaceAll(filePath, tableToken, fm.config.Language()) + filePath = strings.ReplaceAll(filePath, tableToken, fm.config.Language) // fixme: not all languages==latin filePath = strings.ReplaceAll(filePath, fontToken, "latin") diff --git a/d2core/d2asset/d2asset.go b/d2core/d2asset/d2asset.go index 01a57fe8..c998bc52 100644 --- a/d2core/d2asset/d2asset.go +++ b/d2core/d2asset/d2asset.go @@ -1,22 +1,21 @@ package d2asset import ( - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" "log" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" ) -var singleton *assetManager +var singleton *assetManager //nolint:gochecknoglobals // Currently global by design // Initialize creates and assigns all necessary dependencies for the assetManager top-level functions to work correctly func Initialize(renderer d2interface.Renderer, term d2interface.Terminal) error { var ( - config = d2config.Get() - archiveManager = createArchiveManager(config) - archivedFileManager = createFileManager(config, archiveManager) + archiveManager = createArchiveManager(d2config.Config) + archivedFileManager = createFileManager(d2config.Config, archiveManager) paletteManager = createPaletteManager() paletteTransformManager = createPaletteTransformManager() animationManager = createAnimationManager(renderer) diff --git a/d2core/d2config/config.go b/d2core/d2config/config.go deleted file mode 100644 index fa7eb56c..00000000 --- a/d2core/d2config/config.go +++ /dev/null @@ -1,194 +0,0 @@ -package d2config - -import ( - "encoding/json" - "io/ioutil" - "os" - "os/user" - "path" - "runtime" -) - -const defaultSfxVolume = 1.0 -const defaultBgmVolume = 0.3 - -func getDefaultConfig() *Configuration { - config := &Configuration{ - language: "ENG", - fullScreen: false, - ticksPerSecond: -1, - runInBackground: true, - vsyncEnabled: true, - sfxVolume: defaultSfxVolume, - bgmVolume: defaultBgmVolume, - mpqPath: "C:/Program Files (x86)/Diablo II", - backend: "Ebiten", - mpqLoadOrder: []string{ - "Patch_D2.mpq", - "d2exp.mpq", - "d2xmusic.mpq", - "d2xtalk.mpq", - "d2xvideo.mpq", - "d2data.mpq", - "d2char.mpq", - "d2music.mpq", - "d2sfx.mpq", - "d2video.mpq", - "d2speech.mpq", - }, - } - - switch runtime.GOOS { - case "windows": - if runtime.GOARCH == "386" { - config.mpqPath = "C:/Program Files/Diablo II" - } - case "darwin": - config.mpqPath = "/Applications/Diablo II/" - config.mpqLoadOrder = []string{ - "Diablo II Patch", - "Diablo II Expansion Data", - "Diablo II Expansion Movies", - "Diablo II Expansion Music", - "Diablo II Expansion Speech", - "Diablo II Game Data", - "Diablo II Graphics", - "Diablo II Movies", - "Diablo II Music", - "Diablo II Sounds", - "Diablo II Speech", - } - case "linux": - if usr, err := user.Current(); err == nil { - config.mpqPath = path.Join(usr.HomeDir, ".wine/drive_c/Program Files (x86)/Diablo II") - } - } - - return config -} - -func getDefaultConfigPath() string { - if configDir, err := os.UserConfigDir(); err == nil { - return path.Join(configDir, "OpenDiablo2", "config.json") - } - - return getLocalConfigPath() -} - -func getLocalConfigPath() string { - return path.Join(path.Dir(os.Args[0]), "config.json") -} - -func load(configPath string) error { - configFile, err := os.Open(configPath) //nolint:gosec will fix the security error later - - if err != nil { - return err - } - - data, err := ioutil.ReadAll(configFile) - - if err != nil { - return err - } - - if err := unmarshalIntoInterface(data); err != nil { - return err - } - - err = configFile.Close() - - if err != nil { - return err - } - - return nil -} - -func unmarshalIntoInterface(d []byte) error { - tmp := &hack{} // an empty concrete implementation - if err := json.Unmarshal(d, tmp); err != nil { - return err - } - - tmp2cfg(tmp, singleton) // transfer tmp values to singleton - - return nil -} - -// TODO figure out a way to unmarshal into an interface -type hack struct{ - MpqLoadOrder []string - Language string - MpqPath string - TicksPerSecond int - FpsCap int - SfxVolume float64 - BgmVolume float64 - FullScreen bool - RunInBackground bool - VsyncEnabled bool - Backend string -} - -func cfg2tmp (a *Configuration, b *hack) { - b.MpqLoadOrder = a.mpqLoadOrder - b.Language = a.language - b.MpqPath = a.mpqPath - b.TicksPerSecond = a.ticksPerSecond - b.FpsCap = a.fpsCap - b.SfxVolume = a.sfxVolume - b.BgmVolume = a.bgmVolume - b.FullScreen = a.fullScreen - b.RunInBackground = a.runInBackground - b.VsyncEnabled = a.vsyncEnabled - b.Backend = a.backend -} - -func tmp2cfg (b *hack, a *Configuration) { - a.mpqLoadOrder = b.MpqLoadOrder - a.language = b.Language - a.mpqPath = b.MpqPath - a.ticksPerSecond = b.TicksPerSecond - a.fpsCap = b.FpsCap - a.sfxVolume = b.SfxVolume - a.bgmVolume = b.BgmVolume - a.fullScreen = b.FullScreen - a.runInBackground = b.RunInBackground - a.vsyncEnabled = b.VsyncEnabled - a.backend = b.Backend -} - -func save(configPath string) error { - configDir := path.Dir(configPath) - - if err := os.MkdirAll(configDir, 0750); err != nil { - return err - } - - configFile, err := os.Create(configPath) - - if err != nil { - return err - } - - tmp := &hack{} - cfg2tmp(singleton, tmp) - data, err := json.MarshalIndent(tmp, "", " ") - - if err != nil { - return err - } - - if _, writeErr := configFile.Write(data); writeErr != nil { - return writeErr - } - - err = configFile.Close() - - if err != nil { - return err - } - - return nil -} diff --git a/d2core/d2config/d2config.go b/d2core/d2config/d2config.go index 2fbc081a..8e78e2c2 100644 --- a/d2core/d2config/d2config.go +++ b/d2core/d2config/d2config.go @@ -1,106 +1,108 @@ package d2config import ( + "encoding/json" "log" + "os" + "path" ) +// Config holds the configuration from config.json +var Config *Configuration //nolint:gochecknoglobals // Currently global by design + // Configuration defines the configuration for the engine, loaded from config.json type Configuration struct { - mpqLoadOrder []string - language string - mpqPath string - ticksPerSecond int - fpsCap int - sfxVolume float64 - bgmVolume float64 - fullScreen bool - runInBackground bool - vsyncEnabled bool - backend string + MpqLoadOrder []string + Language string + MpqPath string + TicksPerSecond int + FpsCap int + SfxVolume float64 + BgmVolume float64 + FullScreen bool + RunInBackground bool + VsyncEnabled bool + Backend string } -func (c *Configuration) MpqLoadOrder() []string { - return c.mpqLoadOrder -} - -func (c *Configuration) Language() string { - return c.language -} - -func (c *Configuration) MpqPath() string { - return c.mpqPath -} - -func (c *Configuration) TicksPerSecond() int { - return c.ticksPerSecond -} - -func (c *Configuration) FpsCap() int { - return c.fpsCap -} - -func (c *Configuration) SfxVolume() float64 { - return c.sfxVolume -} - -func (c *Configuration) BgmVolume() float64 { - return c.bgmVolume -} - -func (c *Configuration) FullScreen() bool { - return c.fullScreen -} - -func (c *Configuration) RunInBackground() bool { - return c.runInBackground -} - -func (c *Configuration) VsyncEnabled() bool { - return c.vsyncEnabled -} - -func (c *Configuration) Backend() string { - return c.backend +// Load loads a configuration object from disk +func Load() error { + Config = new(Configuration) + return Config.Load() } // Load loads a configuration object from disk func (c *Configuration) Load() error { configPaths := []string{ - getDefaultConfigPath(), - getLocalConfigPath(), + defaultConfigPath(), + localConfigPath(), } - var loaded bool - for _, configPath := range configPaths { log.Printf("loading configuration file from %s...", configPath) - if err := load(configPath); err == nil { - loaded = true - break + if _, err := os.Stat(configPath); os.IsNotExist(err) { + continue } - } - if !loaded { - log.Println("failed to load configuration file, saving default configuration...") - - if err := Save(); err != nil { + configFile, err := os.Open(path.Clean(configPath)) + if err != nil { return err } + + if err := json.NewDecoder(configFile).Decode(&Config); err != nil { + return err + } + + if err := configFile.Close(); err != nil { + return err + } + + return nil } - return nil + log.Println("failed to load configuration file, saving default configuration...") + + Config = defaultConfig() + + return Config.Save() } // Save saves the configuration object to disk func (c *Configuration) Save() error { - configPath := getDefaultConfigPath() + configPath := defaultConfigPath() log.Printf("saving configuration file to %s...", configPath) - var err error - if err = save(configPath); err != nil { - log.Printf("failed to write configuration file (%s)", err) + configDir := path.Dir(configPath) + if err := os.MkdirAll(configDir, 0750); err != nil { + return err } - return err + configFile, err := os.Create(configPath) + if err != nil { + return err + } + + buf, err := json.MarshalIndent(Config, "", " ") + if err != nil { + return err + } + + if _, err := configFile.Write(buf); err != nil { + return err + } + + return configFile.Close() +} + +func defaultConfigPath() string { + if configDir, err := os.UserConfigDir(); err == nil { + return path.Join(configDir, "OpenDiablo2", "config.json") + } + + return localConfigPath() +} + +func localConfigPath() string { + return path.Join(path.Dir(os.Args[0]), "config.json") } diff --git a/d2core/d2config/defaults.go b/d2core/d2config/defaults.go new file mode 100644 index 00000000..716e7523 --- /dev/null +++ b/d2core/d2config/defaults.go @@ -0,0 +1,67 @@ +package d2config + +import ( + "os/user" + "path" + "runtime" +) + +func defaultConfig() *Configuration { + const ( + defaultSfxVolume = 1.0 + defaultBgmVolume = 0.3 + ) + + config := &Configuration{ + Language: "ENG", + FullScreen: false, + TicksPerSecond: -1, + RunInBackground: true, + VsyncEnabled: true, + SfxVolume: defaultSfxVolume, + BgmVolume: defaultBgmVolume, + MpqPath: "C:/Program Files (x86)/Diablo II", + Backend: "Ebiten", + MpqLoadOrder: []string{ + "Patch_D2.mpq", + "d2exp.mpq", + "d2xmusic.mpq", + "d2xtalk.mpq", + "d2xvideo.mpq", + "d2data.mpq", + "d2char.mpq", + "d2music.mpq", + "d2sfx.mpq", + "d2video.mpq", + "d2speech.mpq", + }, + } + + switch runtime.GOOS { + case "windows": + if runtime.GOARCH == "386" { + config.MpqPath = "C:/Program Files/Diablo II" + } + case "darwin": + config.MpqPath = "/Applications/Diablo II/" + config.MpqLoadOrder = []string{ + "Diablo II Patch", + "Diablo II Expansion Data", + "Diablo II Expansion Movies", + "Diablo II Expansion Music", + "Diablo II Expansion Speech", + "Diablo II Game Data", + "Diablo II Graphics", + "Diablo II Movies", + "Diablo II Music", + "Diablo II Sounds", + "Diablo II Speech", + } + case "linux": + if usr, err := user.Current(); err == nil { + config.MpqPath = path.Join(usr.HomeDir, ".wine/drive_c/Program Files (x86)/Diablo II") + } + } + + return config +} diff --git a/d2core/d2config/singleton_stuff.go b/d2core/d2config/singleton_stuff.go deleted file mode 100644 index 6b0185c5..00000000 --- a/d2core/d2config/singleton_stuff.go +++ /dev/null @@ -1,22 +0,0 @@ -package d2config - -import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - -// TODO remove this shit - -var singleton = getDefaultConfig() - -// Load loads a configuration object from disk -func Load() error { - return singleton.Load() -} - -// Save saves the configuration object to disk -func Save() error { - return singleton.Save() -} - -// Get returns a configuration object -func Get() d2interface.Configuration { - return singleton -} diff --git a/d2core/d2render/ebiten/ebiten_renderer.go b/d2core/d2render/ebiten/ebiten_renderer.go index 53efbf59..9e1cef46 100644 --- a/d2core/d2render/ebiten/ebiten_renderer.go +++ b/d2core/d2render/ebiten/ebiten_renderer.go @@ -1,15 +1,13 @@ package ebiten import ( - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "image" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" - - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" ) type Renderer struct { @@ -31,13 +29,12 @@ func (r *Renderer) Layout(outsideWidth, outsideHeight int) (screenWidth, screenH func CreateRenderer() (*Renderer, error) { result := &Renderer{} - config := d2config.Get() - + config := d2config.Config ebiten.SetCursorMode(ebiten.CursorModeHidden) - ebiten.SetFullscreen(config.FullScreen()) - ebiten.SetRunnableOnUnfocused(config.RunInBackground()) - ebiten.SetVsyncEnabled(config.VsyncEnabled()) - ebiten.SetMaxTPS(config.TicksPerSecond()) + ebiten.SetFullscreen(config.FullScreen) + ebiten.SetRunnableOnUnfocused(config.RunInBackground) + ebiten.SetVsyncEnabled(config.VsyncEnabled) + ebiten.SetMaxTPS(config.TicksPerSecond) return result, nil } diff --git a/main.go b/main.go index 226cb2f1..1b4f4393 100644 --- a/main.go +++ b/main.go @@ -3,13 +3,11 @@ package main import ( "log" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render/ebiten" - "github.com/OpenDiablo2/OpenDiablo2/d2app" - ebiten2 "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio/ebiten" - + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render/ebiten" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2term" ) @@ -25,6 +23,10 @@ func main() { log.SetFlags(log.Lshortfile) log.Println("OpenDiablo2 - Open source Diablo 2 engine") + if err := d2config.Load(); err != nil { + panic(err) + } + // Initialize our providers renderer, err := ebiten.CreateRenderer() if err != nil { @@ -44,5 +46,7 @@ func main() { } app := d2app.Create(GitBranch, GitCommit, term, audio, renderer) - app.Run() + if err := app.Run(); err != nil { + log.Fatal(err) + } }