package izapple2 import ( "embed" "flag" "fmt" "strings" "golang.org/x/exp/slices" ) const configSuffix = ".cfg" const defaultConfiguration = "2enh" const ( confParent = "parent" confModel = "model" confName = "name" confBoard = "board" confRom = "rom" confCharRom = "charrom" confCpu = "cpu" confSpeed = "speed" confRamworks = "ramworks" confNsc = "nsc" confTrace = "trace" confProfile = "profile" confForceCaps = "forceCaps" confRgb = "rgb" confRomx = "romx" confMods = "mods" confS0 = "s0" confS1 = "s1" confS2 = "s2" confS3 = "s3" confS4 = "s4" confS5 = "s5" confS6 = "s6" confS7 = "s7" ) //go:embed configs/*.cfg var configurationFiles embed.FS type configurationModels struct { preconfiguredConfigs map[string]*configuration } type configuration struct { data map[string]string } func newConfiguration() *configuration { c := configuration{} c.data = make(map[string]string) return &c } func (c *configuration) getHas(key string) (string, bool) { key = strings.ToLower(key) value, ok := c.data[key] return value, ok } func (c *configuration) get(key string) string { key = strings.ToLower(key) value, ok := c.data[key] if !ok { // Should not happen panic(fmt.Errorf("key %s not found", key)) } return value } func (c *configuration) getFlag(key string) bool { return c.get(key) == "true" } func (c *configuration) set(key string, value string) { key = strings.ToLower(key) c.data[key] = value } func loadConfigurationModelsAndDefault() (*configurationModels, *configuration, error) { models := &configurationModels{} dir, err := configurationFiles.ReadDir("configs") if err != nil { return nil, nil, err } models.preconfiguredConfigs = make(map[string]*configuration) for _, file := range dir { if file.Type().IsRegular() && strings.HasSuffix(strings.ToLower(file.Name()), configSuffix) { content, err := configurationFiles.ReadFile("configs/" + file.Name()) if err != nil { return nil, nil, err } lines := strings.Split(string(content), "\n") config := newConfiguration() for iLine, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } colonPos := strings.Index(line, ":") if colonPos < 0 { return nil, nil, fmt.Errorf("invalid configuration in %s:%d", file.Name(), iLine) } key := strings.TrimSpace(line[:colonPos]) value := strings.TrimSpace(line[colonPos+1:]) config.data[key] = value } name_no_ext := file.Name()[:len(file.Name())-len(configSuffix)] models.preconfiguredConfigs[name_no_ext] = config } } defaultConfig, err := models.get(defaultConfiguration) if err != nil { return nil, nil, err } defaultConfig.set(confModel, defaultConfiguration) return models, defaultConfig, nil } func mergeConfigs(base *configuration, addition *configuration) *configuration { result := newConfiguration() for k, v := range base.data { result.set(k, v) } for k, v := range addition.data { result.set(k, v) } return result } func (c *configurationModels) get(name string) (*configuration, error) { name = strings.TrimSpace(name) config, ok := c.preconfiguredConfigs[name] if !ok { return nil, fmt.Errorf("configuration %s.cfg not found", name) } parentName, hasParent := config.getHas(confParent) if !hasParent { return config, nil } parent, err := c.get(parentName) if err != nil { return nil, err } result := mergeConfigs(parent, config) return result, nil } func (c *configurationModels) availableModels() []string { models := make([]string, 0, len(c.preconfiguredConfigs)-1) for name := range c.preconfiguredConfigs { if !strings.HasPrefix(name, "_") { models = append(models, name) } } slices.Sort(models) return models } func (c *configurationModels) getWithOverrides(model string, overrides *configuration) (*configuration, error) { configValues, err := c.get(model) if err != nil { return nil, err } if overrides != nil { configValues = mergeConfigs(configValues, overrides) } return configValues, nil } func setupFlags(models *configurationModels, configuration *configuration) error { paramDescription := map[string]string{ confModel: "set base model", confRom: "main rom file", confCharRom: "rom file for the character generator", confCpu: "cpu type, can be '6502' or '65c02'", confSpeed: "cpu speed in Mhz, can be 'ntsc', 'pal', 'full' or a decimal nunmber", confMods: "comma separated list of mods applied to the board, available mods are 'shift', 'four-colors", confRamworks: "memory to use with RAMWorks card, max is 16384", confNsc: "add a DS1216 No-Slot-Clock on the main ROM (use 'main') or a slot ROM", confTrace: "trace CPU execution with one or more comma separated tracers", confProfile: "generate profile trace to analyse with pprof", confForceCaps: "force all letters to be uppercased (no need for caps lock!)", confRgb: "emulate the RGB modes of the 80col RGB card for DHGR", confRomx: "emulate a RomX", confS0: "slot 0 configuration.", confS1: "slot 1 configuration.", confS2: "slot 2 configuration.", confS3: "slot 3 configuration.", confS4: "slot 4 configuration.", confS5: "slot 5 configuration.", confS6: "slot 6 configuration.", confS7: "slot 7 configuration.", } boolParams := []string{confProfile, confForceCaps, confRgb, confRomx} for name, description := range paramDescription { defaultValue, ok := configuration.getHas(name) if !ok { return fmt.Errorf("default value not found for %s", name) } if slices.Contains(boolParams, name) { flag.Bool(name, defaultValue == "true", description) } else { flag.String(name, defaultValue, description) } } flag.Usage = func() { out := flag.CommandLine.Output() fmt.Fprintf(out, "Usage: %s [file]\n", flag.CommandLine.Name()) fmt.Fprintf(out, " file\n") fmt.Fprintf(out, " path to image to use on the boot device\n") flag.PrintDefaults() fmt.Fprintf(out, "\nThe available pre-configured models are:\n") for _, model := range models.availableModels() { config, _ := models.get(model) fmt.Fprintf(out, " %s: %s\n", model, config.get(confName)) } fmt.Fprintf(out, "\nThe available cards are:\n") for _, card := range availableCards() { builder := getCardFactory()[card] fmt.Fprintf(out, " %s: %s\n", card, builder.description) } fmt.Fprintf(out, "\nThe available tracers are:\n") for _, tracer := range availableTracers() { builder := getTracerFactory()[tracer] fmt.Fprintf(out, " %s: %s\n", tracer, builder.description) } } return nil } func getConfigurationFromCommandLine() (*configuration, string, error) { models, configuration, err := loadConfigurationModelsAndDefault() if err != nil { return nil, "", err } setupFlags(models, configuration) flag.Parse() modelFlag := flag.Lookup(confModel) if modelFlag != nil && strings.TrimSpace(modelFlag.Value.String()) != defaultConfiguration { // Replace the model configuration, err = models.get(modelFlag.Value.String()) if err != nil { return nil, "", err } } flag.Visit(func(f *flag.Flag) { configuration.set(f.Name, f.Value.String()) }) filename := flag.Arg(0) return configuration, filename, nil }