From b17ca34de23f5f38c67418199333e1090f5a1c56 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 27 Jun 2019 15:07:34 -0700 Subject: [PATCH] Bindata: Embedding Doodads and Levels (for WASM) * Use `go-bindata` to embed built-in doodads and levels directly into the Doodle binary. `make bindata` produces the bindata source file. * Add `FromJSON()` method to Levels and Doodads to load objects from JSON strings in memory (for bindata built-ins or WASM ajax requests) * Update file loading functions to check the embedded bindata files. * pkg/config.go#EditFile: * Supports editing a level from bindata (TODO: remove this support) * If the "assets/levels/%(simple-name.level)" exists in bindata, edits that drawing. * No such support for editing built-in doodads. * WASM has no filesystem access to edit files except built-in levels (yet) * pkg/doodads#ListDoodads: * Prepends built-in doodads from bindata to the returned list. * WASM: no filesystem access so gets only the built-ins. * pkg/doodads#LoadFile: * Checks built-in bindata store first for doodad files. * WASM: tries an HTTP request if not found in bindata but can go no further if not found (no filesystem access) * pkg/filesystem#FindFile: * This function finds a level/doodad by checking all the places. * If the level or doodad exists in bindata built-in, always returns its system path like "assets/doodads/test.doodad" * WASM: always returns the built-in candidate path even if not found in bindata so that ajax GET can be attempted. * pkg/level#ListSystemLevels: * New function that lists the system level files, similar to the equivalent doodads function. * Prepends the bindata built-in level files. * WASM: only returns the built-ins (no filesystem support) * Desktop: also lists and returns the assets/levels/ directory. * pkg/level#LoadFile: * Like the doodads.LoadFile, tries from built-in bindata first, then ajax request (WASM) before accessing the filesystem (desktop) * Menu Scene: TODO, list the built-in levels in the Load Level menu. This feature will soon go away when WASM gets its own storage for user levels (localStorage instead of filesystem) --- .gitignore | 1 + Makefile | 10 ++++++++ pkg/config.go | 23 ++++++++++++++++++ pkg/doodads/fmt_readwrite.go | 37 +++++++++++++++++++++++------ pkg/doodads/json.go | 12 ++++++++++ pkg/editor_ui_doodad.go | 1 + pkg/filesystem/filesystem.go | 34 +++++++++++++++++++++++--- pkg/level/fmt_json.go | 20 ++++++++++++++++ pkg/level/fmt_readwrite.go | 46 ++++++++++++++++++++++++++++++++++++ pkg/menu_scene.go | 6 +++++ pkg/play_scene.go | 2 +- pkg/shmem/globals.go | 5 ++++ pkg/userdir/userdir.go | 7 ++++++ pkg/wasm/ajax.go | 44 ++++++++++++++++++++++++++++++++++ 14 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 pkg/wasm/ajax.go diff --git a/.gitignore b/.gitignore index 3d1e81f..6070f29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +pkg/bindata/bindata.go fonts/ maps/ bin/ diff --git a/Makefile b/Makefile index 25a7ee3..0388412 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,16 @@ build-debug: go build $(LDFLAGS) -tags="developer" -i -o bin/doodle cmd/doodle/main.go go build $(LDFLAGS) -tags="developer" -i -o bin/doodad cmd/doodad/main.go +# `make bindata` generates the embedded binary assets package. +.PHONY: bindata +bindata: + go-bindata -pkg bindata -o pkg/bindata/bindata.go assets/... fonts/ + +# `make bindata-dev` generates the debug version of bindata package. +.PHONY: bindata-dev +bindata-dev: + go-bindata -debug -pkg bindata -o pkg/bindata/bindata.go assets/... fonts/ + # `make wasm` builds the WebAssembly port. .PHONY: wasm wasm: diff --git a/pkg/config.go b/pkg/config.go index 83fd87c..c664ea9 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -5,8 +5,10 @@ import ( "os" "path/filepath" "regexp" + "runtime" "strings" + "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/userdir" ) @@ -34,14 +36,35 @@ func (d *Doodle) EditFile(filename string) error { if m := reSimpleFilename.FindStringSubmatch(filename); len(m) > 0 { log.Debug("EditFile: simple filename %s", filename) extension := strings.ToLower(filepath.Ext(filename)) + + // Check the system level storage. TODO: no editing of system levels + if _, err := bindata.Asset("assets/levels/" + filename); err == nil { + log.Info("Found level %s in bindata", filename) + return d.EditDrawing(filename) + } + + // WASM: no filesystem access, can go no further. + if runtime.GOOS == "js" { + return fmt.Errorf("EditFile(%s): not found for WASM and can go no further", filename) + } + + // Check the user's levels directory. if foundFilename := userdir.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) + + // WASM: no filesystem access. + if runtime.GOOS == "js" { + log.Error("EditFile(%s): wasm can't open file paths", filename) + return fmt.Errorf("EditFile(%s): wasm can't open file paths", filename) + } + if _, err := os.Stat(filename); !os.IsNotExist(err) { log.Debug("EditFile: verified path %s exists", filename) absPath = filename diff --git a/pkg/doodads/fmt_readwrite.go b/pkg/doodads/fmt_readwrite.go index ec50578..fab3ae5 100644 --- a/pkg/doodads/fmt_readwrite.go +++ b/pkg/doodads/fmt_readwrite.go @@ -1,16 +1,18 @@ package doodads import ( - "errors" "fmt" "io/ioutil" + "runtime" "strings" + "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/filesystem" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/userdir" + "git.kirsle.net/apps/doodle/pkg/wasm" ) // ListDoodads returns a listing of all available doodads between all locations, @@ -18,6 +20,18 @@ import ( func ListDoodads() ([]string, error) { var names []string + // List doodads embedded into the binary. + if files, err := bindata.AssetDir("assets/doodads"); err == nil { + names = append(names, files...) + } + + // WASM + if runtime.GOOS == "js" { + // Return the array of doodads embedded in the bindata. + // TODO: append user doodads to the list. + return names, nil + } + // Read system-level doodads first. Ignore errors, if the system path is // empty we still go on to read the user directory. files, _ := ioutil.ReadDir(filesystem.SystemDoodadsPath) @@ -47,14 +61,23 @@ func LoadFile(filename string) (*Doodad, error) { return nil, fmt.Errorf("doodads.LoadFile(%s): %s", filename, err) } - // Load the JSON format. - if doodad, err := LoadJSON(filename); err == nil { - return doodad, nil - } else { - log.Warn(err.Error()) + // Do we have the file in bindata? + if jsonData, err := bindata.Asset(filename); err == nil { + return FromJSON(filename, jsonData) } - return nil, errors.New("invalid file type") + // WASM: try the file over HTTP ajax request. + if runtime.GOOS == "js" { + jsonData, err := wasm.HTTPGet(filename) + if err != nil { + return nil, err + } + + return FromJSON(filename, jsonData) + } + + // Load the JSON file from the filesystem. + return LoadJSON(filename) } // WriteFile saves a doodad to disk in the user's config directory. diff --git a/pkg/doodads/json.go b/pkg/doodads/json.go index 30c406a..e7c7b2a 100644 --- a/pkg/doodads/json.go +++ b/pkg/doodads/json.go @@ -18,6 +18,18 @@ func (d *Doodad) ToJSON() ([]byte, error) { return out.Bytes(), err } +// FromJSON loads a doodad from JSON string. +func FromJSON(filename string, data []byte) (*Doodad, error) { + var doodad = &Doodad{} + err := json.Unmarshal(data, doodad) + + // Inflate the chunk metadata to map the pixels to their palette indexes. + doodad.Filename = filepath.Base(filename) + doodad.Inflate() + + return doodad, err +} + // WriteJSON writes a Doodad to JSON on disk. func (d *Doodad) WriteJSON(filename string) error { json, err := d.ToJSON() diff --git a/pkg/editor_ui_doodad.go b/pkg/editor_ui_doodad.go index f86f6da..aba37d9 100644 --- a/pkg/editor_ui_doodad.go +++ b/pkg/editor_ui_doodad.go @@ -167,6 +167,7 @@ func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Fra // NOTE: The drag target is the EditorUI.Canvas in // editor_ui.go#SetupCanvas() btn.Handle(ui.MouseDown, func(e render.Point) { + log.Warn("MouseDown on doodad %s (%s)", doodad.Filename, doodad.Title) u.startDragActor(doodad) }) u.Supervisor.Add(btn) diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go index 5b3dd39..f2bb7d1 100644 --- a/pkg/filesystem/filesystem.go +++ b/pkg/filesystem/filesystem.go @@ -6,8 +6,10 @@ import ( "io" "os" "path/filepath" + "runtime" "strings" + "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/userdir" ) @@ -24,8 +26,8 @@ const ( // Paths to system-level assets bundled with the application. var ( - SystemDoodadsPath = filepath.Join(".", "assets", "doodads") - SystemLevelsPath = filepath.Join(".", "assets", "levels") + SystemDoodadsPath = filepath.Join("assets", "doodads") + SystemLevelsPath = filepath.Join("assets", "levels") ) // MakeHeader creates the binary file header. @@ -94,6 +96,19 @@ func FindFile(filename string) (string, error) { if filetype == enum.LevelExt || filetype == "" { // system levels candidate := filepath.Join(SystemLevelsPath, filename) + + // embedded system doodad? + if _, err := bindata.Asset(candidate); err == nil { + return candidate, nil + } + + // WASM: can't check the filesystem. Let the caller go ahead and try + // loading via ajax request. + if runtime.GOOS == "js" { + return candidate, nil + } + + // external system level? if _, err := os.Stat(candidate); !os.IsNotExist(err) { return candidate, nil } @@ -107,8 +122,21 @@ func FindFile(filename string) (string, error) { // Search doodad directories. if filetype == enum.DoodadExt || filetype == "" { - // system doodads + // system doodads path candidate := filepath.Join(SystemDoodadsPath, filename) + + // embedded system doodad? + if _, err := bindata.Asset(candidate); err == nil { + return candidate, nil + } + + // WASM: can't check the filesystem. Let the caller go ahead and try + // loading via ajax request. + if runtime.GOOS == "js" { + return candidate, nil + } + + // external system doodad? if _, err := os.Stat(candidate); !os.IsNotExist(err) { return candidate, nil } diff --git a/pkg/level/fmt_json.go b/pkg/level/fmt_json.go index 48781cb..9a07e5b 100644 --- a/pkg/level/fmt_json.go +++ b/pkg/level/fmt_json.go @@ -10,6 +10,26 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" ) +// FromJSON loads a level from JSON string. +func FromJSON(filename string, data []byte) (*Level, error) { + var m = &Level{} + err := json.Unmarshal(data, m) + + // Fill in defaults. + if m.Wallpaper == "" { + m.Wallpaper = DefaultWallpaper + } + + // Inflate the chunk metadata to map the pixels to their palette indexes. + m.Chunker.Inflate(m.Palette) + m.Actors.Inflate() + + // Inflate the private instance values. + m.Palette.Inflate() + + return m, err +} + // ToJSON serializes the level as JSON. func (m *Level) ToJSON() ([]byte, error) { out := bytes.NewBuffer([]byte{}) diff --git a/pkg/level/fmt_readwrite.go b/pkg/level/fmt_readwrite.go index 7c33719..46c8a89 100644 --- a/pkg/level/fmt_readwrite.go +++ b/pkg/level/fmt_readwrite.go @@ -4,15 +4,45 @@ import ( "errors" "fmt" "io/ioutil" + "runtime" "strings" + "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/filesystem" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/userdir" + "git.kirsle.net/apps/doodle/pkg/wasm" ) +// ListSystemLevels returns a list of built-in levels. +func ListSystemLevels() ([]string, error) { + var names = []string{} + + // Add the levels embedded inside the binary. + if levels, err := bindata.AssetDir("assets/levels"); err == nil { + names = append(names, levels...) + } + + // WASM + if runtime.GOOS == "js" { + // Return just the embedded ones, no filesystem access. + return names, nil + } + + // Read filesystem for system levels. + files, err := ioutil.ReadDir(filesystem.SystemLevelsPath) + for _, file := range files { + name := file.Name() + if strings.HasSuffix(strings.ToLower(name), enum.DoodadExt) { + names = append(names, name) + } + } + + return names, err +} + // LoadFile reads a level file from disk, checking a few locations. func LoadFile(filename string) (*Level, error) { if !strings.HasSuffix(filename, enum.LevelExt) { @@ -25,6 +55,22 @@ func LoadFile(filename string) (*Level, error) { return nil, err } + // Do we have the file in bindata? + if jsonData, err := bindata.Asset(filename); err == nil { + log.Info("loaded from embedded bindata") + return FromJSON(filename, jsonData) + } + + // WASM: try the file over HTTP ajax request. + if runtime.GOOS == "js" { + jsonData, err := wasm.HTTPGet(filename) + if err != nil { + return nil, err + } + + return FromJSON(filename, jsonData) + } + // Try the binary format. if level, err := LoadBinary(filename); err == nil { return level, nil diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index ff7f6a3..5662599 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -337,7 +337,13 @@ func (s *MenuScene) setupLoadWindow(d *Doodle) error { FillX: true, }) + // Get the user's levels. levels, _ := userdir.ListLevels() + + // Embedded levels, TODO + sysLevels, _ := level.ListSystemLevels() + levels = append(levels, sysLevels...) + lvlRow := ui.NewFrame("Level Row 0") frame.Pack(lvlRow, ui.Pack{ Anchor: ui.N, diff --git a/pkg/play_scene.go b/pkg/play_scene.go index e44773b..79161db 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -104,7 +104,7 @@ func (s *PlayScene) Setup(d *Doodle) error { } // Load in the player character. - player, err := doodads.LoadFile("./assets/doodads/azu-blu.doodad") + player, err := doodads.LoadFile("azu-blu.doodad") if err != nil { log.Error("PlayScene.Setup: failed to load player doodad: %s", err) player = doodads.NewDummy(32) diff --git a/pkg/shmem/globals.go b/pkg/shmem/globals.go index 64549af..2f00a30 100644 --- a/pkg/shmem/globals.go +++ b/pkg/shmem/globals.go @@ -15,9 +15,14 @@ var ( // Globally available Flash() function so we can emit text to the Doodle UI. Flash func(string, ...interface{}) + + // Ajax file cache for WASM use. + AjaxCache map[string][]byte ) func init() { + AjaxCache = map[string][]byte{} + // Default Flash function in case the app misconfigures it. Output to the // console in an obvious way. Flash = func(tmpl string, v ...interface{}) { diff --git a/pkg/userdir/userdir.go b/pkg/userdir/userdir.go index 6df1518..dbe3df0 100644 --- a/pkg/userdir/userdir.go +++ b/pkg/userdir/userdir.go @@ -7,6 +7,7 @@ import ( "runtime" "strings" + "git.kirsle.net/apps/doodle/pkg/log" "github.com/kirsle/configdir" ) @@ -132,6 +133,12 @@ func resolvePath(directory, filename, extension string) string { // existed. So the filename should have a ".level" or ".doodad" extension and // then this path will resolve the ProfileDirectory of the file. func ResolvePath(filename, extension string, one bool) string { + // WASM has no file system. + if runtime.GOOS == "js" { + log.Error("userdir.ResolvePath: not supported in WASM build") + return "" + } + // If the filename exists outright, return it. if _, err := os.Stat(filename); !os.IsNotExist(err) { return filename diff --git a/pkg/wasm/ajax.go b/pkg/wasm/ajax.go new file mode 100644 index 0000000..0068897 --- /dev/null +++ b/pkg/wasm/ajax.go @@ -0,0 +1,44 @@ +package wasm + +import ( + "fmt" + "io/ioutil" + "net/http" + + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/shmem" +) + +// HTTPGet fetches a path via ajax request. +func HTTPGet(filename string) ([]byte, error) { + // Already cached? + jsonData, ok := shmem.AjaxCache[filename] + if ok { + return jsonData, nil + } + + // Fetch the URI. + resp, err := http.Get(filename) + if err != nil { + log.Error("http error: %s", err) + return nil, err + } + defer resp.Body.Close() + + // Error? + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + return nil, fmt.Errorf("failed to load URI %s: HTTP %d response", + filename, + resp.StatusCode, + ) + } + + // Parse and store the response in cache. + jsonData, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + shmem.AjaxCache[filename] = jsonData + + return jsonData, nil +}