refactored game bootstrap, removed `d2config.Config` singleton (#899)

* Remove d2config.Config singleton

* refactored config file bootstrap
* `d2loader.Loader` adds the config directories during init
* `d2asset.AssetManager` loads the config file during init
* mpq verification logic removed from d2config; this is done by d2loader
* added `errorMessage` to `d2app.App` for setting the error message for the error screen.

* fixed loader test
This commit is contained in:
gravestench 2020-11-03 12:54:15 +00:00 committed by GitHub
parent d6c9748fef
commit 5ac03d6f49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 195 deletions

View File

@ -73,6 +73,8 @@ type App struct {
ui *d2ui.UIManager
tAllocSamples *ring.Ring
guiManager *d2gui.GuiManager
config *d2config.Configuration
errorMessage error
*Options
}
@ -99,12 +101,20 @@ const (
// Create creates a new instance of the application
func Create(gitBranch, gitCommit string) *App {
assetManager, assetError := d2asset.NewAssetManager()
// we can throw away the error here because by this time it's already been loaded
config, _ := assetManager.LoadConfig()
return &App{
gitBranch: gitBranch,
gitCommit: gitCommit,
asset: assetManager,
config: config,
Options: &Options{
Server: &d2networking.ServerOptions{},
},
errorMessage: assetError,
}
}
@ -113,20 +123,6 @@ func updateNOOP() error {
}
func (a *App) startDedicatedServer() error {
// hack, for now we need to create the asset manager here
// Attempt to load the configuration file
err := d2config.Load()
if err != nil {
return err
}
asset, err := d2asset.NewAssetManager(d2config.Config, *a.Options.LogLevel)
if err != nil {
return err
}
a.asset = asset
min, max := d2networking.ServerMinPlayers, d2networking.ServerMaxPlayersDefault
maxPlayers := d2math.ClampInt(*a.Options.Server.MaxPlayers, min, max)
@ -154,35 +150,27 @@ func (a *App) startDedicatedServer() error {
}
func (a *App) loadEngine() error {
// Attempt to load the configuration file
configError := d2config.Load()
// Create our renderer
renderer, err := ebiten.CreateRenderer()
renderer, err := ebiten.CreateRenderer(a.config)
if err != nil {
return err
}
a.renderer = renderer
// If we failed to load our config, lets show the boot panic screen
if configError != nil {
return configError
if a.errorMessage != nil {
return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2")
}
// if the log level was specified at the command line, use it
logLevel := *a.Options.LogLevel
if logLevel == d2util.LogLevelUnspecified {
logLevel = d2config.Config.LogLevel
logLevel = a.config.LogLevel
}
// Create the asset manager
asset, err := d2asset.NewAssetManager(d2config.Config, logLevel)
if err != nil {
return err
}
a.asset.SetLogLevel(logLevel)
audio := ebiten2.CreateAudio(asset)
audio := ebiten2.CreateAudio(a.asset)
inputManager := d2input.NewInputManager()
@ -191,21 +179,20 @@ func (a *App) loadEngine() error {
return err
}
err = asset.BindTerminalCommands(term)
err = a.asset.BindTerminalCommands(term)
if err != nil {
return err
}
scriptEngine := d2script.CreateScriptEngine()
uiManager := d2ui.NewUIManager(asset, renderer, inputManager, audio)
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, audio)
a.inputManager = inputManager
a.terminal = term
a.scriptEngine = scriptEngine
a.audio = audio
a.ui = uiManager
a.asset = asset
a.tAllocSamples = createZeroedRing(nSamplesTAlloc)
if a.gitBranch == "" {
@ -273,6 +260,13 @@ func (a *App) Run() error {
return fmt.Errorf(fmtVersion, a.gitBranch, a.gitCommit)
}
logLevel := *a.Options.LogLevel
if logLevel == d2util.LogLevelUnspecified {
logLevel = a.config.LogLevel
}
a.asset.SetLogLevel(logLevel)
// start profiler if argument was supplied
if len(*a.Options.profiler) > 0 {
profiler := enableProfiler(*a.Options.profiler)
@ -296,7 +290,11 @@ func (a *App) Run() error {
windowTitle := fmt.Sprintf("OpenDiablo2 (%s)", a.gitBranch)
// If we fail to initialize, we will show the error screen
if err := a.initialize(); err != nil {
if gameErr := a.renderer.Run(updateInitError, updateNOOP, 800, 600,
if a.errorMessage == nil {
a.errorMessage = err // if there was an error during init, don't clobber it
}
if gameErr := a.renderer.Run(a.updateInitError, updateNOOP, 800, 600,
windowTitle); gameErr != nil {
return gameErr
}
@ -352,8 +350,7 @@ func (a *App) initialize() error {
a.screen = d2screen.NewScreenManager(a.ui, a.guiManager)
config := d2config.Config
a.audio.SetVolumes(config.BgmVolume, config.SfxVolume)
a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)
if err := a.loadStrings(); err != nil {
return err
@ -727,11 +724,10 @@ func enableProfiler(profileOption string) interface{ Stop() } {
return nil
}
func updateInitError(target d2interface.Surface) error {
func (a *App) updateInitError(target d2interface.Surface) error {
target.Clear(colornames.Darkred)
target.PushTranslation(errMsgPadding, errMsgPadding)
target.DrawTextf(`Could not find the MPQ files in the directory:
%s\nPlease put the files and re-run the game.`, d2config.Config.MpqPath)
target.DrawTextf(a.errorMessage.Error())
return nil
}

View File

@ -1,7 +1,6 @@
package d2loader
import (
"errors"
"fmt"
"os"
"path/filepath"
@ -35,10 +34,8 @@ const (
)
// NewLoader creates a new loader
func NewLoader(config *d2config.Configuration, l d2util.LogLevel) (*Loader, error) {
loader := &Loader{
config: config,
}
func NewLoader(l d2util.LogLevel) (*Loader, error) {
loader := &Loader{}
loader.Cache = d2cache.CreateCache(defaultCacheBudget)
loader.Logger = d2util.NewLogger()
@ -46,9 +43,9 @@ func NewLoader(config *d2config.Configuration, l d2util.LogLevel) (*Loader, erro
loader.Logger.SetPrefix(logPrefix)
loader.Logger.SetLevel(l)
err := loader.initFromConfig()
loader.bootstrap()
return loader, err
return loader, nil
}
// Loader represents the manager that handles loading and caching assets with the asset Sources
@ -60,35 +57,9 @@ type Loader struct {
Sources []asset.Source
}
const (
errConfigFileNotFound = "config file not found"
fmtErrSourceNotFound = `file not found: %s
Please check your config file at %s
Also, verify that the MPQ files exist at %s
Capitalization matters!
`
)
func (l *Loader) initFromConfig() error {
if l.config == nil {
return errors.New(errConfigFileNotFound)
}
for _, mpqName := range l.config.MpqLoadOrder {
cleanDir := filepath.Clean(l.config.MpqPath)
srcPath := filepath.Join(cleanDir, mpqName)
_, err := l.AddSource(srcPath)
if err != nil {
// nolint:stylecheck // we want a multiline error message here..
return fmt.Errorf(fmtErrSourceNotFound, srcPath, l.config.Path(), l.config.MpqPath)
}
}
return nil
func (l *Loader) bootstrap() {
_, _ = l.AddSource(filepath.Dir(d2config.LocalConfigPath()))
_, _ = l.AddSource(filepath.Dir(d2config.DefaultConfigPath()))
}
// Load attempts to load an asset with the given sub-path. The sub-path is relative to the root
@ -125,7 +96,7 @@ func (l *Loader) Load(subPath string) (asset.Asset, error) {
continue
}
srcBase := filepath.Base(source.Path())
srcBase, _ := filepath.Abs(source.Path())
l.Info(fmt.Sprintf("from %s, loading %s", srcBase, subPath))
return loadedAsset, l.Insert(subPath, loadedAsset, defaultCacheEntryWeight)

View File

@ -26,7 +26,7 @@ const (
)
func TestLoader_NewLoader(t *testing.T) {
loader, _ := NewLoader(nil, d2util.LogLevelDefault)
loader, _ := NewLoader(d2util.LogLevelDefault)
if loader.Cache == nil {
t.Error("loader should not be nil")
@ -34,7 +34,7 @@ func TestLoader_NewLoader(t *testing.T) {
}
func TestLoader_AddSource(t *testing.T) {
loader, _ := NewLoader(nil, d2util.LogLevelDefault)
loader, _ := NewLoader(d2util.LogLevelDefault)
sourceA, errA := loader.AddSource(sourcePathA)
sourceB, errB := loader.AddSource(sourcePathB)
@ -85,9 +85,10 @@ func TestLoader_AddSource(t *testing.T) {
// nolint:gocyclo // this is just a test, not a big deal if we ignore linter here
func TestLoader_Load(t *testing.T) {
loader, _ := NewLoader(nil, d2util.LogLevelDefault)
loader, _ := NewLoader(d2util.LogLevelDefault)
_, err := loader.AddSource(sourcePathB) // we expect files common to any source to come from here
// we expect files common to any source to come from here
commonSource, err := loader.AddSource(sourcePathB)
if err != nil {
t.Fail()
log.Print(err)
@ -123,7 +124,7 @@ func TestLoader_Load(t *testing.T) {
if entryCommon == nil || errCommon != nil {
t.Error("common entry should exist")
} else if entryCommon.Source() != loader.Sources[0] {
} else if entryCommon.Source() != commonSource {
t.Error("common entry should come from the first loader source")
}

View File

@ -37,10 +37,10 @@ const colorEscapeReset = "\033[0m"
// Log format strings for log levels
const (
fmtPrefix = "[%s]"
LogFmtDebug = "[DEBUG]" + colorEscapeReset + " %s"
LogFmtInfo = "[INFO]" + colorEscapeReset + " %s"
LogFmtWarning = "[WARNING]" + colorEscapeReset + " %s"
LogFmtError = "[ERROR]" + colorEscapeReset + " %s"
LogFmtDebug = "[DEBUG]" + colorEscapeReset + " %s\r\n"
LogFmtInfo = "[INFO]" + colorEscapeReset + " %s\r\n"
LogFmtWarning = "[WARNING]" + colorEscapeReset + " %s\r\n"
LogFmtError = "[ERROR]" + colorEscapeReset + " %s\r\n"
)
// NewLogger creates a new logger with a default
@ -73,6 +73,10 @@ func (l *Logger) SetPrefix(s string) {
// SetLevel sets the log level
func (l *Logger) SetLevel(level LogLevel) {
if level == LogLevelUnspecified {
level = LogLevelDefault
}
l.level = level
}

View File

@ -1,8 +1,12 @@
package d2asset
import (
"encoding/json"
"fmt"
"image/color"
"path/filepath"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
@ -38,8 +42,22 @@ const (
paletteTransformBudget = 64
)
const (
logPrefix = "Asset Manager"
fmtLoadAsset = "could not load file stream %s (%v)"
fmtLoadAnimation = "loading animation %s with palette %s, draw effect %d"
fmtLoadComposite = "loading composite: type %d, token %s, palette %s"
fmtLoadFont = "loading font: table %s, sprite %s, palette %s"
fmtLoadPalette = "loading palette %s"
fmtLoadStringTable = "loading string table: %s"
fmtLoadTransform = "loading palette transform: %s"
fmtLoadDict = "loading data dictionary: %s"
fmtLoadAnimData = "loading animation data from: %s"
)
// AssetManager loads files and game objects
type AssetManager struct {
config *d2config.Configuration
logger *d2util.Logger
loader *d2loader.Loader
tables d2interface.Cache
@ -51,14 +69,98 @@ type AssetManager struct {
}
func (am *AssetManager) init() error {
err := am.initDataDictionaries()
var err error
config, err := am.LoadConfig()
if err != nil {
return err
}
am.logger.SetLevel(config.LogLevel)
am.Records.Logger.SetLevel(config.LogLevel)
am.loader.Logger.SetLevel(config.LogLevel)
err = am.initConfig(config)
if err != nil {
return err
}
if err := am.initDataDictionaries(); err != nil {
return err
}
return nil
}
func (am *AssetManager) initConfig(config *d2config.Configuration) error {
am.config = config
for _, mpqName := range am.config.MpqLoadOrder {
cleanDir := filepath.Clean(am.config.MpqPath)
srcPath := filepath.Join(cleanDir, mpqName)
_, err := am.loader.AddSource(srcPath)
if err != nil {
// nolint:stylecheck // we want a multiline error message here..
return fmt.Errorf(fmtErrSourceNotFound, srcPath, am.config.Path(), am.config.MpqPath)
}
}
return nil
}
// SetLogLevel sets the log level for the asset manager, record manager, and file loader
func (am *AssetManager) SetLogLevel(level d2util.LogLevel) {
am.logger.SetLevel(level)
am.Records.Logger.SetLevel(level)
am.loader.Logger.SetLevel(level)
}
// LoadConfig loads the OpenDiablo2 config file
func (am *AssetManager) LoadConfig() (*d2config.Configuration, error) {
// by now the, the loader has initialized and added our config dirs as sources...
configBaseName := filepath.Base(d2config.DefaultConfigPath())
configAsset, _ := am.LoadAsset(configBaseName)
config := &d2config.Configuration{}
// create the default if not found
if configAsset == nil {
config = d2config.DefaultConfig()
fullPath := filepath.Join(config.Dir(), config.Base())
config.SetPath(fullPath)
am.logger.Infof("creating default configuration file at %s...", fullPath)
saveErr := config.Save()
return config, saveErr
}
if err := json.NewDecoder(configAsset).Decode(config); err != nil {
return nil, err
}
config.SetPath(filepath.Join(configAsset.Source().Path(), configAsset.Path()))
am.logger.Infof("loaded configuration file from %s", config.Path())
return config, nil
}
const (
fmtErrSourceNotFound = `file not found: %s
Please check your config file at %s
Also, verify that the MPQ files exist at %s
Capitalization matters!
`
)
func (am *AssetManager) initDataDictionaries() error {
dictPaths := []string{
d2resource.LevelType, d2resource.LevelPreset, d2resource.LevelWarp,
@ -108,19 +210,6 @@ func (am *AssetManager) initDataDictionaries() error {
return nil
}
const (
logPrefix = "Asset Manager"
fmtLoadAsset = "could not load file stream %s (%v)"
fmtLoadAnimation = "loading animation %s with palette %s, draw effect %d"
fmtLoadComposite = "loading composite: type %d, token %s, palette %s"
fmtLoadFont = "loading font: table %s, sprite %s, palette %s"
fmtLoadPalette = "loading palette %s"
fmtLoadStringTable = "loading string table: %s"
fmtLoadTransform = "loading palette transform: %s"
fmtLoadDict = "loading data dictionary: %s"
fmtLoadAnimData = "loading animation data from: %s"
)
// LoadAsset loads an asset
func (am *AssetManager) LoadAsset(filePath string) (asset.Asset, error) {
data, err := am.loader.Load(filePath)

View File

@ -4,40 +4,35 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
// NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly
func NewAssetManager(config *d2config.Configuration, l d2util.LogLevel) (*AssetManager, error) {
loader, err := d2loader.NewLoader(config, l)
func NewAssetManager() (*AssetManager, error) {
loader, err := d2loader.NewLoader(d2util.LogLevelDefault)
if err != nil {
return nil, err
}
records, err := d2records.NewRecordManager(l)
records, err := d2records.NewRecordManager(d2util.LogLevelDebug)
if err != nil {
return nil, err
}
manager := &AssetManager{
d2util.NewLogger(),
loader,
d2cache.CreateCache(tableBudget),
d2cache.CreateCache(animationBudget),
d2cache.CreateCache(fontBudget),
d2cache.CreateCache(paletteBudget),
d2cache.CreateCache(paletteTransformBudget),
records,
logger: d2util.NewLogger(),
loader: loader,
tables: d2cache.CreateCache(tableBudget),
animations: d2cache.CreateCache(animationBudget),
fonts: d2cache.CreateCache(fontBudget),
palettes: d2cache.CreateCache(paletteBudget),
transforms: d2cache.CreateCache(paletteTransformBudget),
Records: records,
}
manager.logger.SetPrefix(logPrefix)
manager.logger.SetLevel(l)
err = manager.init()
if err != nil {
return nil, err
}
return manager, nil
return manager, err
}

View File

@ -120,7 +120,7 @@ func (eap *AudioProvider) createSoundEffect(sfx string, context *audio.Context,
loop bool) *SoundEffect {
result := &SoundEffect{}
soundFile := "/data/global/sfx/"
soundFile := "data/global/sfx/"
if _, exists := eap.asset.Records.Sound.Details[sfx]; exists {
soundEntry := eap.asset.Records.Sound.Details[sfx]
@ -132,7 +132,7 @@ func (eap *AudioProvider) createSoundEffect(sfx string, context *audio.Context,
audioData, err := eap.asset.LoadFileStream(soundFile)
if err != nil {
audioData, err = eap.asset.LoadFileStream("/data/global/music/" + sfx)
audioData, err = eap.asset.LoadFileStream("data/global/music/" + sfx)
}
if err != nil {

View File

@ -2,16 +2,13 @@ package d2config
import (
"encoding/json"
"log"
"os"
"path"
"path/filepath"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
// 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
@ -29,72 +26,19 @@ type Configuration struct {
path string
}
// Load loads a configuration object from disk
func Load() error {
Config = new(Configuration)
if Config.Load() != nil {
return Config.Save()
}
return nil
}
// Load loads a configuration object from disk
func (c *Configuration) Load() error {
configPaths := []string{
defaultConfigPath(),
localConfigPath(),
}
for _, configPath := range configPaths {
log.Printf("loading configuration file from %s...", configPath)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
continue
}
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
}
c.path = configPath
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 := defaultConfigPath()
log.Printf("saving configuration file to %s...", configPath)
configDir := path.Dir(configPath)
configDir := path.Dir(c.path)
if err := os.MkdirAll(configDir, 0750); err != nil {
return err
}
configFile, err := os.Create(configPath)
configFile, err := os.Create(c.path)
if err != nil {
return err
}
buf, err := json.MarshalIndent(Config, "", " ")
buf, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
@ -106,23 +50,22 @@ func (c *Configuration) Save() error {
return configFile.Close()
}
// Path returns the path of the config file
func (c *Configuration) Path() string {
if c.path == "" {
c.path = defaultConfigPath()
}
// Dir returns the directory component of the path
func (c *Configuration) Dir() string {
return filepath.Dir(c.path)
}
// Base returns the base component of the path
func (c *Configuration) Base() string {
return filepath.Base(c.path)
}
// Path returns the config file path
func (c *Configuration) Path() string {
return c.path
}
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")
// SetPath sets where the config file is saved to (a full path)
func (c *Configuration) SetPath(p string) {
c.path = p
}

View File

@ -0,0 +1,28 @@
package d2config
import (
"os"
"path"
)
const (
od2ConfigDirName = "OpenDiablo2"
)
const (
od2ConfigFileName = "config.json"
)
// DefaultConfigPath returns the absolute path for the default config file location
func DefaultConfigPath() string {
if configDir, err := os.UserConfigDir(); err == nil {
return path.Join(configDir, od2ConfigDirName, od2ConfigFileName)
}
return LocalConfigPath()
}
// LocalConfigPath returns the absolute path to the directory of the OpenDiablo2 executable
func LocalConfigPath() string {
return path.Join(path.Dir(os.Args[0]), od2ConfigFileName)
}

View File

@ -8,7 +8,8 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
func defaultConfig() *Configuration {
// DefaultConfig creates and returns a default configuration
func DefaultConfig() *Configuration {
const (
defaultSfxVolume = 1.0
defaultBgmVolume = 0.3
@ -38,6 +39,7 @@ func defaultConfig() *Configuration {
"d2speech.mpq",
},
LogLevel: d2util.LogLevelDefault,
path: DefaultConfigPath(),
}
switch runtime.GOOS {

View File

@ -66,11 +66,11 @@ func (r *Renderer) Layout(_, _ int) (width, height int) {
}
// CreateRenderer creates an ebiten renderer instance
func CreateRenderer() (*Renderer, error) {
func CreateRenderer(cfg *d2config.Configuration) (*Renderer, error) {
result := &Renderer{}
if d2config.Config != nil {
config := d2config.Config
if cfg != nil {
config := cfg
ebiten.SetCursorMode(ebiten.CursorModeHidden)
ebiten.SetFullscreen(config.FullScreen)