From a54064e3997322dcb701b61ad0b8165781c8c1d5 Mon Sep 17 00:00:00 2001 From: Gabe Cook Date: Thu, 24 Oct 2024 12:52:29 -0500 Subject: [PATCH] feat(config): Add support for per-game config overrides --- cmd/gones/cmd.go | 18 ++++-- cmd/gones/cmd_js.go | 11 ++-- cmd/gones/console.go | 11 ++-- cmd/gones/console_js.go | 6 +- cmd/gones/run.go | 5 +- internal/config/load.go | 137 ++++++++++++++++++++++++++++------------ 6 files changed, 129 insertions(+), 59 deletions(-) diff --git a/cmd/gones/cmd.go b/cmd/gones/cmd.go index 0d98cc74..ab09634d 100644 --- a/cmd/gones/cmd.go +++ b/cmd/gones/cmd.go @@ -33,19 +33,25 @@ func New(opts ...options.Option) *cobra.Command { } func runCobra(cmd *cobra.Command, args []string) error { - conf, err := config.Load(cmd) - if err != nil { - return err - } + cmd.SilenceUsage = true var path string if len(args) > 0 { path = args[0] } - cmd.SilenceUsage = true + + cart, err := loadCartridge(path) + if err != nil { + return err + } + + conf := config.NewDefault() + if err := conf.Load(cmd, cart.Name(), cart.Hash()); err != nil { + return err + } ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() - return run(ctx, conf, path) + return run(ctx, conf, cart) } diff --git a/cmd/gones/cmd_js.go b/cmd/gones/cmd_js.go index a10a8385..59b0941f 100644 --- a/cmd/gones/cmd_js.go +++ b/cmd/gones/cmd_js.go @@ -1,8 +1,6 @@ package gones import ( - "log/slog" - "gabe565.com/gones/cmd/options" "gabe565.com/gones/internal/config" ) @@ -10,9 +8,14 @@ import ( type Command struct{} func (c *Command) Execute() error { - slog.Info("Loaded config") conf := config.NewDefault() - return run(nil, conf, "") + + cart, err := loadCartridge() + if err != nil { + return err + } + + return run(nil, conf, cart) } func New(_ ...options.Option) *Command { diff --git a/cmd/gones/console.go b/cmd/gones/console.go index 393dd9ca..3e675276 100644 --- a/cmd/gones/console.go +++ b/cmd/gones/console.go @@ -11,18 +11,17 @@ import ( "github.com/ncruces/zenity" ) -func newConsole(conf *config.Config, path string) (*console.Console, error) { +func loadCartridge(path string) (*cartridge.Cartridge, error) { if path == "" { var err error - path, err = zenity.SelectFile( + if path, err = zenity.SelectFile( zenity.Title("Choose a ROM file"), zenity.FileFilter{ Name: "NES ROM", Patterns: []string{"*.nes"}, CaseFold: true, }, - ) - if err != nil { + ); err != nil { return nil, err } } @@ -33,5 +32,9 @@ func newConsole(conf *config.Config, path string) (*console.Console, error) { } slog.Info("Loaded cartridge", "", cart) + return cart, nil +} + +func newConsole(conf *config.Config, cart *cartridge.Cartridge) (*console.Console, error) { return console.New(conf, cart) } diff --git a/cmd/gones/console_js.go b/cmd/gones/console_js.go index a2b4d2b6..309d790d 100644 --- a/cmd/gones/console_js.go +++ b/cmd/gones/console_js.go @@ -12,7 +12,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -func newConsole(conf *config.Config, _ string) (*console.Console, error) { +func loadCartridge() (*cartridge.Cartridge, error) { jsCartridge := js.Global().Get("GonesCartridge") jsData := jsCartridge.Get("data") goData := make([]byte, jsData.Get("length").Int()) @@ -29,6 +29,10 @@ func newConsole(conf *config.Config, _ string) (*console.Console, error) { } slog.Info("Loaded cartridge", "", cart) + return cart, nil +} + +func newConsole(conf *config.Config, cart *cartridge.Cartridge) (*console.Console, error) { js.Global().Get("GonesClient").Call("setRomName", cart.Name()) c, err := console.New(conf, cart) diff --git a/cmd/gones/run.go b/cmd/gones/run.go index 4e5eb6ee..5e9621c3 100644 --- a/cmd/gones/run.go +++ b/cmd/gones/run.go @@ -6,13 +6,14 @@ import ( "log/slog" "runtime" + "gabe565.com/gones/internal/cartridge" "gabe565.com/gones/internal/config" "gabe565.com/gones/internal/console" "gabe565.com/gones/internal/pprof" "github.com/hajimehoshi/ebiten/v2" ) -func run(ctx context.Context, conf *config.Config, path string) error { +func run(ctx context.Context, conf *config.Config, cart *cartridge.Cartridge) error { if pprof.Enabled { go func() { if err := pprof.ListenAndServe(); err != nil { @@ -21,7 +22,7 @@ func run(ctx context.Context, conf *config.Config, path string) error { }() } - c, err := newConsole(conf, path) + c, err := newConsole(conf, cart) if err != nil { return err } diff --git a/internal/config/load.go b/internal/config/load.go index 82cfc1c5..b1ac2e67 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -12,6 +12,7 @@ import ( "gabe565.com/gones/internal/consts" "gabe565.com/gones/internal/log" + "gabe565.com/utils/must" "github.com/knadh/koanf/providers/posflag" "github.com/knadh/koanf/providers/rawbytes" "github.com/knadh/koanf/providers/structs" @@ -20,107 +21,159 @@ import ( "github.com/spf13/cobra" ) -func Load(cmd *cobra.Command) (*Config, error) { +func (conf *Config) Load(cmd *cobra.Command, name, hash string) error { log.Init(cmd.ErrOrStderr()) k := koanf.New(".") - conf := NewDefault() // Load default config if err := k.Load(structs.Provider(conf, "toml"), nil); err != nil { - return nil, err + return err } // Find config file - cfgFile, err := cmd.Flags().GetString("config") - if err != nil { - return nil, err - } + var gameCfgFile string + cfgFile := must.Must2(cmd.Flags().GetString("config")) if cfgFile == "" { cfgDir, err := GetDir() if err != nil { - return nil, err + return err } cfgFile = filepath.Join(cfgDir, "config.toml") + gameCfgFile = filepath.Join(cfgDir, "games", hash+".toml") + } + + if err := conf.loadMainConfig(k, cfgFile); err != nil { + return err + } + + if gameCfgFile != "" { + if err := conf.loadGameOverrides(k, gameCfgFile, name); err != nil { + return err + } + } + + if err := conf.loadFlags(k, cmd); err != nil { + return err } - logger := slog.With("file", cfgFile) + + paletteDir, err := GetPaletteDir() + if err != nil { + return err + } + + if err := os.MkdirAll(paletteDir, 0o777); err != nil { + return err + } + + return err +} + +func (conf *Config) loadMainConfig(k *koanf.Koanf, path string) error { + logger := slog.With("file", path) var cfgNotExists bool // Load config file if exists - cfgContents, err := os.ReadFile(cfgFile) + cfgContents, err := os.ReadFile(path) if err != nil { if errors.Is(err, os.ErrNotExist) { cfgNotExists = true } else { - return nil, err + return err } } // Parse config file - parser := TOMLParser{} - if err := k.Load(rawbytes.Provider(cfgContents), parser); err != nil { - return nil, err + if err := k.Load(rawbytes.Provider(cfgContents), TOMLParser{}); err != nil { + return err } if err := fixConfig(k); err != nil { - return nil, err + return err } if err := k.UnmarshalWithConf("", conf, koanf.UnmarshalConf{Tag: "toml"}); err != nil { - return nil, err + return err } + // Update config if necessary newCfg, err := toml.Marshal(conf) if err != nil { - return nil, err + return err } if !bytes.Equal(cfgContents, newCfg) { if cfgNotExists { - logger.Info("Creating config") + logger.Info("Creating main config") - if err := os.MkdirAll(filepath.Dir(cfgFile), 0o777); err != nil { - return nil, err + if err := os.MkdirAll(filepath.Dir(path), 0o777); err != nil { + return err } } else { - logger.Info("Updating config") + logger.Info("Updating main config") } - if err := os.WriteFile(cfgFile, newCfg, 0o666); err != nil { - return nil, err + if err := os.WriteFile(path, newCfg, 0o666); err != nil { + return err } } - // Load flags - flagTable := flagTable() - err = k.Load(posflag.ProviderWithValue(cmd.Flags(), ".", k, func(key string, value string) (string, any) { - if k, ok := flagTable[key]; ok { - key = k - } else { - key = "" - } - return key, value - }), nil) + logger.Info("Loaded main config") + return nil +} + +func (conf *Config) loadGameOverrides(k *koanf.Koanf, path, name string) error { + logger := slog.With("file", path) + + b, err := os.ReadFile(path) if err != nil { - return nil, err + if !os.IsNotExist(err) { + return err + } + + logger.Info("Creating game config") + if err := os.MkdirAll(filepath.Dir(path), 0o777); err != nil { + return err + } + + if err := os.WriteFile(path, []byte("# Overrides for "+name), 0o666); err != nil { + return err + } + + return nil + } + + if err := k.Load(rawbytes.Provider(b), TOMLParser{}); err != nil { + return err } if err := k.UnmarshalWithConf("", conf, koanf.UnmarshalConf{Tag: "toml"}); err != nil { - return nil, err + return err } - paletteDir, err := GetPaletteDir() - if err != nil { - return nil, err + if err := fixConfig(k); err != nil { + return err } - if err := os.MkdirAll(paletteDir, 0o777); err != nil && !errors.Is(err, os.ErrExist) { - return nil, err + logger.Info("Loaded game config") + return nil +} + +func (conf *Config) loadFlags(k *koanf.Koanf, cmd *cobra.Command) error { + lookup := flagTable() + if err := k.Load(posflag.ProviderWithValue(cmd.Flags(), ".", k, func(key string, value string) (string, any) { + if k, ok := lookup[key]; ok { + key = k + } else { + key = "" + } + return key, value + }), nil); err != nil { + return err } - logger.Info("Loaded config") - return conf, err + return k.UnmarshalWithConf("", conf, koanf.UnmarshalConf{Tag: "toml"}) } func fixConfig(k *koanf.Koanf) error {