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.
This commit is contained in:
Noah 2018-10-02 10:11:38 -07:00
parent a7fd3aa1ca
commit cfe26cb964
5 changed files with 167 additions and 10 deletions

View File

@ -176,8 +176,8 @@ Lesser important UI features that can come at any later time:
## Doodad Editor ## Doodad Editor
* [ ] The Edit Mode should support creating drawings for Doodads. * [x] The Edit Mode should support creating drawings for Doodads.
* [ ] It should know whether you're drawing a Map or a Doodad as some * [x] It should know whether you're drawing a Map or a Doodad as some
behaviors may need to be different between the two. behaviors may need to be different between the two.
* [ ] Compress the coordinates down toward `(0,0)` when saving a Doodad, * [ ] Compress the coordinates down toward `(0,0)` when saving a Doodad,
by finding the toppest, leftest point and making that `(0,0)` and adjusting by finding the toppest, leftest point and making that `(0,0)` and adjusting

View File

@ -43,7 +43,7 @@ func main() {
app.SetupEngine() app.SetupEngine()
if filename != "" { if filename != "" {
if edit { if edit {
app.EditDrawing(filename) app.EditFile(filename)
} else { } else {
app.PlayLevel(filename) app.PlayLevel(filename)
} }

View File

@ -4,6 +4,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"git.kirsle.net/apps/doodle/enum"
) )
// Command is a parsed shell command. // Command is a parsed shell command.
@ -112,11 +114,17 @@ func (c Command) Save(d *Doodle) error {
} else if scene.filename != "" { } else if scene.filename != "" {
filename = scene.filename filename = scene.filename
} else { } else {
return errors.New("usage: save <filename.json>") return errors.New("usage: save <filename>")
} }
d.shell.Write("Saving to file: " + filename) switch scene.DrawingType {
scene.SaveLevel(filename) 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 { } else {
return errors.New("save: only available in Edit Mode") return errors.New("save: only available in Edit Mode")
} }
@ -131,8 +139,8 @@ func (c Command) Edit(d *Doodle) error {
} }
filename := c.Args[0] filename := c.Args[0]
d.shell.Write("Editing level: " + filename) d.shell.Write("Editing file: " + filename)
d.EditDrawing(filename) d.EditFile(filename)
return nil return nil
} }

136
config.go
View File

@ -1,6 +1,15 @@
package doodle 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. // Configuration constants.
var ( var (
@ -10,3 +19,128 @@ var (
DebugTextStroke = render.Grey DebugTextStroke = render.Grey
DebugTextShadow = render.Black 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 ""
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads" "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") return errors.New("SaveLevel: current drawing is not a Level type")
} }
if !strings.HasSuffix(filename, extLevel) {
filename += extLevel
}
s.filename = filename s.filename = filename
m := s.Level m := s.Level
@ -185,6 +190,9 @@ func (s *EditorScene) SaveLevel(filename string) error {
return fmt.Errorf("SaveLevel error: %s", err) 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) err = ioutil.WriteFile(filename, json, 0644)
if err != nil { if err != nil {
return fmt.Errorf("Create map file error: %s", err) 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") return errors.New("SaveDoodad: current drawing is not a Doodad type")
} }
if !strings.HasSuffix(filename, extDoodad) {
filename += extDoodad
}
s.filename = filename s.filename = filename
d := s.Doodad d := s.Doodad
if d.Title == "" { if d.Title == "" {
@ -228,7 +240,10 @@ func (s *EditorScene) SaveDoodad(filename string) error {
d.Palette = s.drawing.Palette d.Palette = s.drawing.Palette
d.Layers[0].Chunker = s.drawing.Chunker() 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 return err
} }