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)
This commit is contained in:
Noah 2019-06-27 15:07:34 -07:00
parent c7cc40a339
commit b17ca34de2
14 changed files with 237 additions and 11 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
pkg/bindata/bindata.go
fonts/
maps/
bin/

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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()

View File

@ -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)

View File

@ -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
}

View File

@ -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{})

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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{}) {

View File

@ -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

44
pkg/wasm/ajax.go Normal file
View File

@ -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
}