Embeddable Doodads In Levels
* The Publisher is all hooked up. No native Save File dialogs yet, so uses the dev shell Prompt() to ask for output filename. * Custom-only or builtin doodads too can be stored in the level's file data, at "assets/doodads/*.doodad" * When loading the embedded level in the Editor: it gets its custom doodads out of its file, and you can drag and drop them elsehwere, link them, Play Mode can use them, etc. but they won't appear in the Doodad Dropper if they are not installed in your local doodads directory. * Fleshed out serialization API for the Doodad files: - LoadFromEmbeddable() looks to load a doodad from embeddable file data in addition to the usual places. - Serialize() returns the doodad in bytes, for easy access to embed into level data. - Deserialize() to parse and return from bytes. * When loading a level that references doodads not found in its embedded data or the filesystem: an Alert modal appears listing the missing doodads. The rest of the level loads fine, but the actors referenced by these doodads don't load.
This commit is contained in:
parent
d9bca2152a
commit
7093b102e3
|
@ -14,15 +14,15 @@ var (
|
|||
ScrollboxHoz = 256 // horizontal px from window border to start scrol
|
||||
ScrollboxVert = 160
|
||||
// NEW: set scrollbox bounds by percents
|
||||
ScrollboxHozPercent float64 = 0.25
|
||||
ScrollboxHozPercent float64 = 0.25
|
||||
ScrollboxVertPercent float64 = 0.40
|
||||
|
||||
// Player speeds
|
||||
PlayerMaxVelocity float64 = 6
|
||||
PlayerAcceleration float64 = 0.9
|
||||
Gravity float64 = 6
|
||||
PlayerMaxVelocity float64 = 6
|
||||
PlayerAcceleration float64 = 0.9
|
||||
Gravity float64 = 6
|
||||
GravityAcceleration float64 = 0.2
|
||||
SlopeMaxHeight = 8 // max pixel height for player to walk up a slope
|
||||
SlopeMaxHeight = 8 // max pixel height for player to walk up a slope
|
||||
|
||||
// Default chunk size for canvases.
|
||||
ChunkSize = 128
|
||||
|
@ -57,8 +57,12 @@ var (
|
|||
|
||||
// Level attachment filename for the custom wallpaper.
|
||||
// NOTE: due to hard-coded "assets/wallpapers/" prefix in uix/canvas.go#LoadLevel.
|
||||
CustomWallpaperFilename = "custom.b64img"
|
||||
CustomWallpaperFilename = "custom.b64img"
|
||||
CustomWallpaperEmbedPath = "assets/wallpapers/custom.b64img"
|
||||
|
||||
// Publishing: Doodads-embedded-within-levels.
|
||||
EmbeddedDoodadsBasePath = "assets/doodads/"
|
||||
EmbeddedWallpaperBasePath = "assets/wallpapers/"
|
||||
)
|
||||
|
||||
// Edit Mode Values
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package doodads
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/bindata"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||
|
@ -16,6 +18,11 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/wasm"
|
||||
)
|
||||
|
||||
// Errors.
|
||||
var (
|
||||
ErrNotFound = errors.New("file not found")
|
||||
)
|
||||
|
||||
// ListDoodads returns a listing of all available doodads between all locations,
|
||||
// including user doodads.
|
||||
func ListDoodads() ([]string, error) {
|
||||
|
@ -106,7 +113,20 @@ func ListBuiltin() ([]string, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// LoadFromEmbeddable reads a doodad file, checking a level's embeddable
|
||||
// file data in addition to the usual places.
|
||||
func LoadFromEmbeddable(filename string, fs filesystem.Embeddable) (*Doodad, error) {
|
||||
if bin, err := fs.GetFile(balance.EmbeddedDoodadsBasePath + filename); err == nil {
|
||||
log.Debug("doodads.LoadFromEmbeddable: found %s", filename)
|
||||
return Deserialize(filename, bin)
|
||||
}
|
||||
return LoadFile(filename)
|
||||
}
|
||||
|
||||
// LoadFile reads a doodad file from disk, checking a few locations.
|
||||
//
|
||||
// It checks for embedded bindata, system-level doodads on the filesystem,
|
||||
// and then user-owned doodads in their profile folder.
|
||||
func LoadFile(filename string) (*Doodad, error) {
|
||||
if !strings.HasSuffix(filename, enum.DoodadExt) {
|
||||
filename += enum.DoodadExt
|
||||
|
@ -177,3 +197,24 @@ func (d *Doodad) WriteFile(filename string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serialize encodes a doodad to bytes and returns them, instead
|
||||
// of writing to a file.
|
||||
// WriteFile saves a doodad to disk in the user's config directory.
|
||||
func (d *Doodad) Serialize() ([]byte, error) {
|
||||
// Set the version information.
|
||||
d.Version = 1
|
||||
d.GameVersion = branding.Version
|
||||
|
||||
bin, err := d.ToJSON()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return bin, nil
|
||||
}
|
||||
|
||||
// Deserialize loads a doodad from its bytes format.
|
||||
func Deserialize(filename string, bin []byte) (*Doodad, error) {
|
||||
return FromJSON(filename, bin)
|
||||
}
|
||||
|
|
|
@ -279,6 +279,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
|||
|
||||
log.Info("Installing %d actors into the drawing", len(level.Actors))
|
||||
if err := s.UI.Canvas.InstallActors(level.Actors); err != nil {
|
||||
modal.Alert("This level references some doodads that were not found:\n\n%s", err).WithTitle("Level Errors")
|
||||
return fmt.Errorf("EditorScene.LoadLevel: InstallActors: %s", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ func (u *EditorUI) startDragActor(doodad *doodads.Doodad, actor *level.Actor) {
|
|||
|
||||
if doodad == nil {
|
||||
if actor != nil {
|
||||
obj, err := doodads.LoadFile(actor.Filename)
|
||||
obj, err := doodads.LoadFromEmbeddable(actor.Filename, u.Scene.Level)
|
||||
if err != nil {
|
||||
log.Error("startDragExistingActor: actor doodad name %s not found: %s", actor.Filename, err)
|
||||
return
|
||||
|
|
|
@ -2,9 +2,13 @@ package doodle
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/level/publishing"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||
"git.kirsle.net/apps/doodle/pkg/windows"
|
||||
|
@ -114,13 +118,27 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
|||
Engine: d.Engine,
|
||||
Level: scene.Level,
|
||||
|
||||
OnPublish: func() {
|
||||
modal.Alert("Not Yet Implemented")
|
||||
// filename, err := native.SaveFile("Save your level", "*.level")
|
||||
// if err != nil {
|
||||
// modal.Alert(err.Error())
|
||||
// }
|
||||
// log.Info("Write to: %s", filename)
|
||||
OnPublish: func(includeBuiltins bool) {
|
||||
log.Debug("OnPublish: include builtins=%+v", includeBuiltins)
|
||||
cwd, _ := os.Getwd()
|
||||
d.Prompt(fmt.Sprintf("File name (relative to %s)> ", cwd), func(answer string) {
|
||||
if answer == "" {
|
||||
d.Flash("A file name is required to publish this level.")
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(answer, ".level") {
|
||||
answer += ".level"
|
||||
}
|
||||
|
||||
answer = filepath.Join(cwd, answer)
|
||||
log.Debug("call with includeBuiltins=%+v", includeBuiltins)
|
||||
if _, err := publishing.Publish(scene.Level, answer, includeBuiltins); err != nil {
|
||||
modal.Alert("Error when publishing the level: %s", err)
|
||||
return
|
||||
}
|
||||
d.Flash("Exported published level to: %s", answer)
|
||||
})
|
||||
},
|
||||
OnCancel: func() {
|
||||
u.publishWindow.Hide()
|
||||
|
|
|
@ -3,6 +3,7 @@ package level
|
|||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileSystem embeds a map of files inside a parent drawing.
|
||||
|
@ -63,4 +64,18 @@ func (l *Level) ListFiles() []string {
|
|||
|
||||
sort.Strings(files)
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
// ListFilesAt returns the list of files having a common prefix.
|
||||
func (l *Level) ListFilesAt(prefix string) []string {
|
||||
var (
|
||||
files = l.ListFiles()
|
||||
match = []string{}
|
||||
)
|
||||
for _, name := range files {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
match = append(match, name)
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
|
94
pkg/level/publishing/publishing.go
Normal file
94
pkg/level/publishing/publishing.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Package publishing contains functionality for "publishing" a Level, which
|
||||
involves the writing and reading of custom doodads embedded inside
|
||||
the levels.
|
||||
|
||||
Free tiers of the game will not read or write embedded doodads into
|
||||
levels.
|
||||
*/
|
||||
package publishing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
)
|
||||
|
||||
/*
|
||||
Level "Publishing" functions, involving the writing and reading of embedded
|
||||
doodads within level files.
|
||||
*/
|
||||
|
||||
// Publish writes a published level file, with embedded doodads included.
|
||||
func Publish(lvl *level.Level, filename string, includeBuiltins bool) (*level.Level, error) {
|
||||
// Get and embed the doodads.
|
||||
builtins, customs := GetUsedDoodadNames(lvl)
|
||||
if includeBuiltins {
|
||||
log.Debug("including builtins: %+v", builtins)
|
||||
customs = append(customs, builtins...)
|
||||
}
|
||||
for _, filename := range customs {
|
||||
log.Debug("Embed filename: %s", filename)
|
||||
doodad, err := doodads.LoadFromEmbeddable(filename, lvl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't load doodad %s: %s", filename, err)
|
||||
}
|
||||
|
||||
bin, err := doodad.Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't serialize doodad %s: %s", filename, err)
|
||||
}
|
||||
|
||||
// Embed it.
|
||||
lvl.SetFile(balance.EmbeddedDoodadsBasePath+filename, bin)
|
||||
}
|
||||
|
||||
log.Info("Publish: write file to %s", filename)
|
||||
err := lvl.WriteFile(filename)
|
||||
return lvl, err
|
||||
}
|
||||
|
||||
// GetUsedDoodadNames returns the lists of doodad filenames in use in a level,
|
||||
// bucketed by built-in or custom user doodads.
|
||||
func GetUsedDoodadNames(lvl *level.Level) (builtin []string, custom []string) {
|
||||
// Collect all the doodad names in use in this level.
|
||||
unique := map[string]interface{}{}
|
||||
names := []string{}
|
||||
if lvl != nil {
|
||||
for _, actor := range lvl.Actors {
|
||||
if _, ok := unique[actor.Filename]; ok {
|
||||
continue
|
||||
}
|
||||
unique[actor.Filename] = nil
|
||||
names = append(names, actor.Filename)
|
||||
}
|
||||
}
|
||||
|
||||
// Identify which of the doodads are built-ins.
|
||||
// builtin = []string{}
|
||||
builtinMap := map[string]interface{}{}
|
||||
// custom := []string{}
|
||||
if builtins, err := doodads.ListBuiltin(); err == nil {
|
||||
for _, filename := range builtins {
|
||||
if _, ok := unique[filename]; ok {
|
||||
builtin = append(builtin, filename)
|
||||
builtinMap[filename] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range names {
|
||||
if _, ok := builtinMap[name]; ok {
|
||||
continue
|
||||
}
|
||||
custom = append(custom, name)
|
||||
}
|
||||
|
||||
sort.Strings(builtin)
|
||||
sort.Strings(custom)
|
||||
|
||||
return builtin, custom
|
||||
}
|
|
@ -150,12 +150,12 @@ func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) {
|
|||
w.Load(level.Palette, level.Chunker)
|
||||
|
||||
// TODO: wallpaper paths
|
||||
filename := "assets/wallpapers/" + level.Wallpaper
|
||||
filename := balance.EmbeddedWallpaperBasePath + level.Wallpaper
|
||||
if runtime.GOOS != "js" {
|
||||
// Check if the wallpaper wasn't found. Check bindata and file system.
|
||||
if _, err := filesystem.FindFileEmbedded(filename, level); err != nil {
|
||||
log.Error("LoadLevel: wallpaper %s did not appear to exist, default to notebook.png", filename)
|
||||
filename = "assets/wallpapers/notebook.png"
|
||||
filename = balance.EmbeddedWallpaperBasePath + "notebook.png"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package uix
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
|
@ -14,11 +14,14 @@ import (
|
|||
// InstallActors adds external Actors to the canvas to be superimposed on top
|
||||
// of the drawing.
|
||||
func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
||||
var errs []string
|
||||
|
||||
w.actors = make([]*Actor, 0)
|
||||
for id, actor := range actors {
|
||||
doodad, err := doodads.LoadFile(actor.Filename)
|
||||
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, w.level)
|
||||
if err != nil {
|
||||
return fmt.Errorf("InstallActors: %s", err)
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Create the "live" Actor to exist in the world, and set its world
|
||||
|
@ -28,6 +31,10 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
|||
|
||||
w.actors = append(w.actors, liveActor)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -189,41 +189,41 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window {
|
|||
}
|
||||
}(i, dd)
|
||||
}
|
||||
}
|
||||
|
||||
// Browse button for local filesystem.
|
||||
browseDoodadFrame := ui.NewFrame("Browse Doodad Frame")
|
||||
frame.Pack(browseDoodadFrame, ui.Pack{
|
||||
Side: ui.N,
|
||||
Expand: true,
|
||||
FillX: true,
|
||||
PadY: 1,
|
||||
})
|
||||
// Browse button for local filesystem.
|
||||
browseDoodadFrame := ui.NewFrame("Browse Doodad Frame")
|
||||
frame.Pack(browseDoodadFrame, ui.Pack{
|
||||
Side: ui.N,
|
||||
Expand: true,
|
||||
FillX: true,
|
||||
PadY: 1,
|
||||
})
|
||||
|
||||
browseDoodadButton := ui.NewButton("Browse Doodad", ui.NewLabel(ui.Label{
|
||||
Text: "Browse...",
|
||||
Font: balance.MenuFont,
|
||||
}))
|
||||
browseDoodadButton.SetStyle(&balance.ButtonPrimary)
|
||||
browseDoodadFrame.Pack(browseDoodadButton, ui.Pack{
|
||||
Side: ui.W,
|
||||
})
|
||||
browseDoodadButton := ui.NewButton("Browse Doodad", ui.NewLabel(ui.Label{
|
||||
Text: "Browse...",
|
||||
Font: balance.MenuFont,
|
||||
}))
|
||||
browseDoodadButton.SetStyle(&balance.ButtonPrimary)
|
||||
browseDoodadFrame.Pack(browseDoodadButton, ui.Pack{
|
||||
Side: ui.W,
|
||||
})
|
||||
|
||||
browseDoodadButton.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
filename, err := native.OpenFile("Choose a .doodad file", "*.doodad")
|
||||
if err != nil {
|
||||
log.Error("Couldn't show file dialog: %s", err)
|
||||
browseDoodadButton.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
filename, err := native.OpenFile("Choose a .doodad file", "*.doodad")
|
||||
if err != nil {
|
||||
log.Error("Couldn't show file dialog: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.LoadForPlay {
|
||||
config.OnPlayLevel(filename)
|
||||
} else {
|
||||
config.OnEditLevel(filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.LoadForPlay {
|
||||
config.OnPlayLevel(filename)
|
||||
} else {
|
||||
config.OnEditLevel(filename)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
config.Supervisor.Add(browseDoodadButton)
|
||||
})
|
||||
config.Supervisor.Add(browseDoodadButton)
|
||||
}
|
||||
|
||||
/******************
|
||||
* Confirm/cancel buttons.
|
||||
|
|
|
@ -3,12 +3,11 @@ package windows
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/level/publishing"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui"
|
||||
|
@ -21,7 +20,7 @@ type Publish struct {
|
|||
Engine render.Engine
|
||||
Level *level.Level
|
||||
|
||||
OnPublish func()
|
||||
OnPublish func(builtinToo bool)
|
||||
OnCancel func()
|
||||
|
||||
// Private vars.
|
||||
|
@ -124,39 +123,40 @@ func NewPublishWindow(cfg Publish) *ui.Window {
|
|||
PadX: 2,
|
||||
})
|
||||
|
||||
// Collect all the doodad names in use in this level.
|
||||
unique := map[string]interface{}{}
|
||||
names := []string{}
|
||||
if cfg.Level != nil {
|
||||
for _, actor := range cfg.Level.Actors {
|
||||
if _, ok := unique[actor.Filename]; ok {
|
||||
continue
|
||||
}
|
||||
unique[actor.Filename] = nil
|
||||
names = append(names, actor.Filename)
|
||||
}
|
||||
}
|
||||
// // Collect all the doodad names in use in this level.
|
||||
// unique := map[string]interface{}{}
|
||||
// names := []string{}
|
||||
// if cfg.Level != nil {
|
||||
// for _, actor := range cfg.Level.Actors {
|
||||
// if _, ok := unique[actor.Filename]; ok {
|
||||
// continue
|
||||
// }
|
||||
// unique[actor.Filename] = nil
|
||||
// names = append(names, actor.Filename)
|
||||
// }
|
||||
// }
|
||||
|
||||
sort.Strings(names)
|
||||
// sort.Strings(names)
|
||||
|
||||
// Identify which of the doodads are built-ins.
|
||||
usedBuiltins := []string{}
|
||||
builtinMap := map[string]interface{}{}
|
||||
usedCustom := []string{}
|
||||
if builtins, err := doodads.ListBuiltin(); err == nil {
|
||||
for _, filename := range builtins {
|
||||
if _, ok := unique[filename]; ok {
|
||||
usedBuiltins = append(usedBuiltins, filename)
|
||||
builtinMap[filename] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range names {
|
||||
if _, ok := builtinMap[name]; ok {
|
||||
continue
|
||||
}
|
||||
usedCustom = append(usedCustom, name)
|
||||
}
|
||||
// // Identify which of the doodads are built-ins.
|
||||
// usedBuiltins := []string{}
|
||||
// builtinMap := map[string]interface{}{}
|
||||
// usedCustom := []string{}
|
||||
// if builtins, err := doodads.ListBuiltin(); err == nil {
|
||||
// for _, filename := range builtins {
|
||||
// if _, ok := unique[filename]; ok {
|
||||
// usedBuiltins = append(usedBuiltins, filename)
|
||||
// builtinMap[filename] = nil
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// for _, name := range names {
|
||||
// if _, ok := builtinMap[name]; ok {
|
||||
// continue
|
||||
// }
|
||||
// usedCustom = append(usedCustom, name)
|
||||
// }
|
||||
usedBuiltins, usedCustom := publishing.GetUsedDoodadNames(cfg.Level)
|
||||
|
||||
// Helper function to draw the button rows for a set of doodads.
|
||||
mkDoodadRows := func(filenames []string, builtin bool) []*ui.Frame {
|
||||
|
@ -201,7 +201,7 @@ func NewPublishWindow(cfg Publish) *ui.Window {
|
|||
builtinRows = []*ui.Frame{}
|
||||
customRows = []*ui.Frame{}
|
||||
)
|
||||
if len(names) > 0 {
|
||||
if len(usedCustom) > 0 {
|
||||
customRows = mkDoodadRows(usedCustom, false)
|
||||
btnRows = append(btnRows, customRows...)
|
||||
}
|
||||
|
@ -284,7 +284,7 @@ func NewPublishWindow(cfg Publish) *ui.Window {
|
|||
}{
|
||||
{"Export Level", true, func() {
|
||||
if cfg.OnPublish != nil {
|
||||
cfg.OnPublish()
|
||||
cfg.OnPublish(cfg.includeBuiltins)
|
||||
}
|
||||
}},
|
||||
{"Close", false, func() {
|
||||
|
|
Loading…
Reference in New Issue
Block a user