From cfe26cb9640b2bf5e3ffff133a0c08c981aab6a3 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Tue, 2 Oct 2018 10:11:38 -0700 Subject: [PATCH] Add configdir and unify file loading/saving * Create a configuration directory to store the user's local levels and doodads. On Linux this is at ~/.config/doodle * Unify the file loading and saving functions: you can type into the console "edit example" and it will open `example.level` from your levels folder or else `example.doodad` from the doodads folder, in the appropriate mode. * You can further specify the file extension: `edit example.doodad` and it will load it from the doodads folder only. * Any slash characters in a file name are taken literally as a relative or absolute path. * The UI Save/Load buttons now share the same code path as the console commands, so the `save` command always saves as a Doodad when the EditorScene is in Doodad Mode. --- README.md | 4 +- cmd/doodle/main.go | 2 +- commands.go | 18 ++++-- config.go | 136 ++++++++++++++++++++++++++++++++++++++++++++- editor_scene.go | 17 +++++- 5 files changed, 167 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0b24457..3f92ab7 100644 --- a/README.md +++ b/README.md @@ -176,8 +176,8 @@ Lesser important UI features that can come at any later time: ## Doodad Editor -* [ ] The Edit Mode should support creating drawings for Doodads. - * [ ] It should know whether you're drawing a Map or a Doodad as some +* [x] The Edit Mode should support creating drawings for Doodads. + * [x] It should know whether you're drawing a Map or a Doodad as some behaviors may need to be different between the two. * [ ] Compress the coordinates down toward `(0,0)` when saving a Doodad, by finding the toppest, leftest point and making that `(0,0)` and adjusting diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index e86e791..b0729c4 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -43,7 +43,7 @@ func main() { app.SetupEngine() if filename != "" { if edit { - app.EditDrawing(filename) + app.EditFile(filename) } else { app.PlayLevel(filename) } diff --git a/commands.go b/commands.go index 4274823..7aa5e3a 100644 --- a/commands.go +++ b/commands.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "strconv" + + "git.kirsle.net/apps/doodle/enum" ) // Command is a parsed shell command. @@ -112,11 +114,17 @@ func (c Command) Save(d *Doodle) error { } else if scene.filename != "" { filename = scene.filename } else { - return errors.New("usage: save ") + return errors.New("usage: save ") } - d.shell.Write("Saving to file: " + filename) - scene.SaveLevel(filename) + switch scene.DrawingType { + case enum.LevelDrawing: + d.shell.Write("Saving Level: " + filename) + scene.SaveLevel(filename) + case enum.DoodadDrawing: + d.shell.Write("Saving Doodad: " + filename) + scene.SaveDoodad(filename) + } } else { return errors.New("save: only available in Edit Mode") } @@ -131,8 +139,8 @@ func (c Command) Edit(d *Doodle) error { } filename := c.Args[0] - d.shell.Write("Editing level: " + filename) - d.EditDrawing(filename) + d.shell.Write("Editing file: " + filename) + d.EditFile(filename) return nil } diff --git a/config.go b/config.go index 03a38ec..2e5535c 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,15 @@ package doodle -import "git.kirsle.net/apps/doodle/render" +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "git.kirsle.net/apps/doodle/render" + "github.com/kirsle/configdir" +) // Configuration constants. var ( @@ -10,3 +19,128 @@ var ( DebugTextStroke = render.Grey DebugTextShadow = render.Black ) + +// Profile Directory settings. +var ( + ConfigDirectoryName = "doodle" + ProfileDirectory string + LevelDirectory string + DoodadDirectory string + + // Regexp to match simple filenames for maps and doodads. + reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`) +) + +// File extensions +const ( + extLevel = ".level" + extDoodad = ".doodad" +) + +func init() { + ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName) + LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels") + DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads") + configdir.MakePath(LevelDirectory, DoodadDirectory) +} + +// LevelPath will turn a "simple" filename into an absolute path in the user's +// local levels folder. If the filename already contains slashes, it is returned +// as-is as an absolute or relative path. +func LevelPath(filename string) string { + return resolvePath(LevelDirectory, filename, extLevel) +} + +// DoodadPath is like LevelPath but for Doodad files. +func DoodadPath(filename string) string { + return resolvePath(DoodadDirectory, filename, extDoodad) +} + +// resolvePath is the inner logic for LevelPath and DoodadPath. +func resolvePath(directory, filename, extension string) string { + if strings.Contains(filename, "/") { + return filename + } + + // Attach the file extension? + if strings.ToLower(filepath.Ext(filename)) != extension { + filename += extension + } + + return filepath.Join(directory, filename) +} + +/* +EditFile opens a drawing file (Level or Doodad) in the EditorScene. + +The filename can be one of the following: + + - A simple filename with no path separators in it and/or no file extension. + - An absolute path beginning with "/" + - A relative path beginning with "./" + +If the filename has an extension (`.level` or `.doodad`), that will disambiguate +how to find the file and which mode to start the EditorMode in. Otherwise, the +"levels" folder is checked first and the "doodads" folder second. +*/ +func (d *Doodle) EditFile(filename string) error { + var absPath string + + // Is it a simple filename? + if m := reSimpleFilename.FindStringSubmatch(filename); len(m) > 0 { + log.Debug("EditFile: simple filename %s", filename) + extension := strings.ToLower(filepath.Ext(filename)) + if foundFilename := d.ResolvePath(filename, extension, false); foundFilename != "" { + log.Info("EditFile: resolved name '%s' to path %s", filename, foundFilename) + absPath = foundFilename + } else { + return fmt.Errorf("EditFile: %s: no level or doodad found", filename) + } + } else { + log.Debug("Not a simple: %s %+v", filename, reSimpleFilename) + if _, err := os.Stat(filename); !os.IsNotExist(err) { + log.Debug("EditFile: verified path %s exists", filename) + absPath = filename + } + } + + d.EditDrawing(absPath) + + return nil +} + +// ResolvePath takes an ambiguous simple filename and searches for a Level or +// Doodad that matches. Returns a blank string if no files found. +// +// Pass a true value for `one` if you are intending to create the file. It will +// only test one filepath and return the first one, regardless if the file +// existed. So the filename should have a ".level" or ".doodad" extension and +// then this path will resolve the ProfileDirectory of the file. +func (d *Doodle) ResolvePath(filename, extension string, one bool) string { + // If the filename exists outright, return it. + if _, err := os.Stat(filename); !os.IsNotExist(err) { + return filename + } + + var paths []string + if extension == extLevel { + paths = append(paths, filepath.Join(LevelDirectory, filename)) + } else if extension == extDoodad { + paths = append(paths, filepath.Join(DoodadDirectory, filename)) + } else { + paths = append(paths, + filepath.Join(LevelDirectory, filename+".level"), + filepath.Join(DoodadDirectory, filename+".doodad"), + ) + } + + for _, test := range paths { + log.Debug("findFilename: try to find '%s' as %s", filename, test) + if _, err := os.Stat(test); os.IsNotExist(err) { + continue + } + return test + } + + return "" +} diff --git a/editor_scene.go b/editor_scene.go index 279e875..f7870a9 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" "git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/doodads" @@ -167,6 +168,10 @@ func (s *EditorScene) SaveLevel(filename string) error { return errors.New("SaveLevel: current drawing is not a Level type") } + if !strings.HasSuffix(filename, extLevel) { + filename += extLevel + } + s.filename = filename m := s.Level @@ -185,6 +190,9 @@ func (s *EditorScene) SaveLevel(filename string) error { return fmt.Errorf("SaveLevel error: %s", err) } + // Save it to their profile directory. + filename = LevelPath(filename) + log.Info("Write Level: %s", filename) err = ioutil.WriteFile(filename, json, 0644) if err != nil { return fmt.Errorf("Create map file error: %s", err) @@ -215,6 +223,10 @@ func (s *EditorScene) SaveDoodad(filename string) error { return errors.New("SaveDoodad: current drawing is not a Doodad type") } + if !strings.HasSuffix(filename, extDoodad) { + filename += extDoodad + } + s.filename = filename d := s.Doodad if d.Title == "" { @@ -228,7 +240,10 @@ func (s *EditorScene) SaveDoodad(filename string) error { d.Palette = s.drawing.Palette d.Layers[0].Chunker = s.drawing.Chunker() - err := d.WriteJSON(s.filename) + // Save it to their profile directory. + filename = DoodadPath(filename) + log.Info("Write Doodad: %s", filename) + err := d.WriteJSON(filename) return err }