From f76ba6fbb7e0820d26c64183bdb56555d2be2101 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 5 May 2019 14:03:20 -0700 Subject: [PATCH] WIP: MsgPack stubs, Level Filesystem Module * Add some encoding/decoding functions for binary msgpack format for levels and doodads. Currently it writes msgpack files that can be decoded and printed by Python (mp2json.py) but it can't re-read from the binary format. For now, levels will continue to write in JSON format. * Add filesystem abstraction functions to the balance/ package to search multiple paths to find Levels and Doodads, to make way for system-level doodads. --- Building.md | 31 +++++++ Makefile | 7 ++ dev-assets/doodads/azulian/azulian.js | 50 ++++++++++- lib/render/color.go | 48 +++++++++++ mp2json.py | 22 +++++ pkg/balance/debug.go | 3 + pkg/balance/filesystem.go | 118 ++++++++++++++++++++++++++ pkg/editor_scene.go | 18 +--- pkg/level/chunk.go | 72 +++++++++++++++- pkg/level/chunk_map.go | 63 ++++++++++++++ pkg/level/chunker.go | 12 +++ pkg/level/fmt_binary.go | 71 ++++++++++++++++ pkg/level/{json.go => fmt_json.go} | 36 ++++---- pkg/level/fmt_readwrite.go | 65 ++++++++++++++ pkg/level/types.go | 28 +++--- pkg/play_scene.go | 5 ++ pkg/scripting/events.go | 11 +++ 17 files changed, 611 insertions(+), 49 deletions(-) create mode 100644 mp2json.py create mode 100644 pkg/balance/filesystem.go create mode 100644 pkg/level/fmt_binary.go rename pkg/level/{json.go => fmt_json.go} (92%) create mode 100644 pkg/level/fmt_readwrite.go diff --git a/Building.md b/Building.md index 6cb8f94..bb3809f 100644 --- a/Building.md +++ b/Building.md @@ -4,6 +4,10 @@ Makefile commands for Linux: * `make setup`: install Go dependencies and set up the build environment * `make build`: build the Doodle and Doodad binaries to the `bin/` folder. +* `make build-free`: build the shareware binaries to the `bin/` folder. See + Build Tags below. +* `make build-debug`: build a debug binary (not release-mode) to the `bin/` + folder. See Build Tags below. * `make run`: run a local dev build of Doodle in debug mode * `make guitest`: run a local dev build in the GUITest scene * `make test`: run the test suite @@ -17,6 +21,33 @@ Makefile commands for Linux: * `make docker.fedora` * `make clean`: clean all build artifacts +## Build Tags + +### shareware + +> Files ending with `_free.go` are for the shareware release as opposed to +> `_paid.go` for the full version. + +Builds the game in the free shareware release mode. + +Run `make build-free` to build the shareware binary. + +Shareware releases of the game have the following changes compared to the default +(release) mode: + +* No access to the Doodad Editor scene in-game (soft toggle) + +### developer + +> Files ending with `_developer.go` are for the developer build as opposed to +> `_release.go` for the public version. + +Developer builds support extra features over the standard release version: + +* Ability to write the JSON file format for Levels and Doodads. + +Run `make build-debug` to build a developer version of the program. + ## Linux Dependencies are Go, SDL2 and SDL2_ttf: diff --git a/Makefile b/Makefile index 1934b0e..d5c3c3b 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,13 @@ build-free: go build $(LDFLAGS) -tags="shareware" -i -o bin/doodle cmd/doodle/main.go go build $(LDFLAGS) -tags="shareware" -i -o bin/doodad cmd/doodad/main.go +# `make build-debug` to build the binary in developer mode. +.PHONY: build-debug +build-debug: + gofmt -w . + 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 doodads` to build the doodads from the dev-assets folder. .PHONY: doodads doodads: diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index e208d10..25ff361 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -1,4 +1,52 @@ function main() { log.Info("Azulian '%s' initialized!", Self.Doodad.Title); - Self.ShowLayer(2); + + var playerSpeed = 12; + var gravity = 4; + var Vx = Vy = 0; + + var animating = false; + var animStart = animEnd = 0; + var animFrame = animStart; + + setInterval(function() { + if (animating) { + if (animFrame < animStart || animFrame > animEnd) { + animFrame = animStart; + } + + animFrame++; + if (animFrame === animEnd) { + animFrame = animStart; + } + Self.ShowLayer(animFrame); + } else { + Self.ShowLayer(animStart); + } + }, 100); + + Events.OnKeypress(function(ev) { + Vx = 0; + Vy = 0; + + if (ev.Right.Now) { + animStart = 2; + animEnd = animStart+4; + animating = true; + Vx = playerSpeed; + } else if (ev.Left.Now) { + animStart = 6; + animEnd = animStart+4; + animating = true; + Vx = -playerSpeed; + } else { + animating = false; + } + + if (!Self.Grounded()) { + Vy += gravity; + } + + // Self.SetVelocity(Point(Vx, Vy)); + }) } diff --git a/lib/render/color.go b/lib/render/color.go index 7f1b2f5..699abb9 100644 --- a/lib/render/color.go +++ b/lib/render/color.go @@ -7,6 +7,8 @@ import ( "image/color" "regexp" "strconv" + + "github.com/vmihailenco/msgpack" ) var ( @@ -155,6 +157,52 @@ func (c *Color) UnmarshalJSON(b []byte) error { return nil } +func (c Color) EncodeMsgpack(enc *msgpack.Encoder) error { + return enc.EncodeString(fmt.Sprintf( + `"#%02x%02x%02x"`, + c.Red, c.Green, c.Blue, + )) +} + +func (c Color) DecodeMsgpack(dec *msgpack.Decoder) error { + hex, err := dec.DecodeString() + if err != nil { + return fmt.Errorf("Color.DecodeMsgpack: %s", err) + } + + parsed, err := HexColor(hex) + if err != nil { + return fmt.Errorf("Color.DecodeMsgpack: HexColor: %s", err) + } + + c.Red = parsed.Red + c.Blue = parsed.Blue + c.Green = parsed.Green + c.Alpha = parsed.Alpha + return nil +} + +// // MarshalMsgpack serializes the Color for msgpack. +// func (c Color) MarshalMsgpack() ([]byte, error) { +// data := []uint8{ +// c.Red, c.Green, c.Blue, c.Alpha, +// } +// return msgpack.Marshal(data) +// } +// +// // UnmarshalMsgpack decodes a Color from msgpack format. +// func (c *Color) UnmarshalMsgpack(b []byte) error { +// var data []uint8 +// if err := msgpack.Unmarshal(data, b); err != nil { +// return err +// } +// c.Red = 255 +// c.Green = data[1] +// c.Blue = data[2] +// c.Alpha = data[3] +// return nil +// } + // Add a relative color value to the color. func (c Color) Add(r, g, b, a int) Color { var ( diff --git a/mp2json.py b/mp2json.py new file mode 100644 index 0000000..78da962 --- /dev/null +++ b/mp2json.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +"""mp2json: convert a msgpack binary file into JSON for debugging.""" + +import msgpack +import json +import sys + +if len(sys.argv) < 2: + print("Usage: mp2json ") + +with open(sys.argv[1], 'rb') as fh: + header = fh.read(8) + magic = header[:6].decode("utf-8") + if magic != "DOODLE": + print("input file doesn't appear to be a doodle drawing binary") + sys.exit(1) + + reader = msgpack.Unpacker(fh, raw=False, max_buffer_size=10*1024*1024) + for o in reader: + print(o) + print(json.dumps(o, indent=2)) diff --git a/pkg/balance/debug.go b/pkg/balance/debug.go index d9a20b2..6e8d50f 100644 --- a/pkg/balance/debug.go +++ b/pkg/balance/debug.go @@ -30,6 +30,9 @@ var ( // Put a border around all Canvas widgets. DebugCanvasBorder = render.Invisible DebugCanvasLabel = false // Tag the canvas with a label. + + // Pretty-print JSON files when writing. + JSONIndent = false ) func init() { diff --git a/pkg/balance/filesystem.go b/pkg/balance/filesystem.go new file mode 100644 index 0000000..451b8c2 --- /dev/null +++ b/pkg/balance/filesystem.go @@ -0,0 +1,118 @@ +package balance + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "git.kirsle.net/apps/doodle/pkg/enum" + "git.kirsle.net/apps/doodle/pkg/userdir" +) + +// Binary file format headers for Levels and Doodads. +// +// The header is 8 bytes long: "DOODLE" + file format version + file type number. +const ( + BinMagic = "DOODLE" + BinVersion uint8 = 1 // version of the file format we support + BinLevelType uint8 = 1 + BinDoodadType uint8 = 2 +) + +// MakeHeader creates the binary file header. +func MakeHeader(filetype uint8) []byte { + header := make([]byte, len(BinMagic)+2) + for i := 0; i < len(BinMagic); i++ { + header[i] = BinMagic[i] + } + + header[len(header)-2] = BinVersion + header[len(header)-1] = filetype + + return header +} + +// ReadHeader reads and verifies a header from a filehandle. +func ReadHeader(filetype uint8, fh io.Reader) error { + header := make([]byte, len(BinMagic)+2) + _, err := fh.Read(header) + if err != nil { + return fmt.Errorf("ReadHeader: %s", err) + } + + if string(header[:len(BinMagic)]) != BinMagic { + return errors.New("not a doodle drawing (no magic number in header)") + } + + // Verify the file format version and type. + var ( + fileVersion = header[len(header)-2] + fileType = header[len(header)-1] + ) + + if fileVersion == 0 || fileVersion > BinVersion { + return errors.New("binary format was created using a newer version of the game") + } else if fileType != filetype { + return errors.New("drawing type is not the type we expected") + } + + return nil +} + +/* +FindFile looks for a file (level or doodad) in a few places. + +The filename should already have a ".level" or ".doodad" file extension. If +neither is given, the exact filename will be searched in all places. + +1. Check in the files built into the program binary. +2. Check for system files in the binary's assets/ folder. +3. Check the user folders. + +Returns the file path and an error if not found anywhere. +*/ +func FindFile(filename string) (string, error) { + var filetype string + + // Any hint on what type of file we're looking for? + if strings.HasSuffix(filename, enum.LevelExt) { + filetype = enum.LevelExt + } else if strings.HasSuffix(filename, enum.DoodadExt) { + filetype = enum.DoodadExt + } + + // Search level directories. + if filetype == enum.LevelExt || filetype == "" { + // system levels + candidate := filepath.Join(".", "assets", "levels", filename) + if _, err := os.Stat(candidate); !os.IsNotExist(err) { + return candidate, nil + } + + // user levels + candidate = userdir.LevelPath(filename) + if _, err := os.Stat(candidate); !os.IsNotExist(err) { + return candidate, nil + } + } + + // Search doodad directories. + if filetype == enum.DoodadExt || filetype == "" { + // system doodads + candidate := filepath.Join(".", "assets", "doodads", filename) + if _, err := os.Stat(candidate); !os.IsNotExist(err) { + return candidate, nil + } + + // user doodads + candidate = userdir.DoodadPath(filename) + if _, err := os.Stat(candidate); !os.IsNotExist(err) { + return candidate, nil + } + } + + return "", errors.New("file not found") +} diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 9005093..d5ab0d0 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -3,7 +3,6 @@ package doodle import ( "errors" "fmt" - "io/ioutil" "os" "strings" @@ -171,7 +170,7 @@ func (s *EditorScene) Draw(d *Doodle) error { func (s *EditorScene) LoadLevel(filename string) error { s.filename = filename - level, err := level.LoadJSON(filename) + level, err := level.LoadFile(filename) fmt.Printf("%+v\n", level) if err != nil { return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err) @@ -213,20 +212,7 @@ func (s *EditorScene) SaveLevel(filename string) error { m.Palette = s.UI.Canvas.Palette m.Chunker = s.UI.Canvas.Chunker() - json, err := m.ToJSON() - if err != nil { - return fmt.Errorf("SaveLevel error: %s", err) - } - - // Save it to their profile directory. - filename = userdir.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) - } - - return nil + return m.WriteFile(filename) } // LoadDoodad loads a doodad from disk. diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index 1d539ee..fda8515 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -12,6 +12,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/userdir" "github.com/satori/go.uuid" + "github.com/vmihailenco/msgpack" "golang.org/x/image/bmp" ) @@ -41,8 +42,9 @@ type Chunk struct { // JSONChunk holds a lightweight (interface-free) copy of the Chunk for // unmarshalling JSON files from disk. type JSONChunk struct { - Type int `json:"type"` - Data json.RawMessage `json:"data"` + Type int `json:"type" msgpack:"0"` + Data json.RawMessage `json:"data" msgpack:"-"` + BinData interface{} `json:"-" msgpack:"1"` } // Accessor provides a high-level API to interact with absolute pixel coordinates @@ -57,6 +59,9 @@ type Accessor interface { Len() int MarshalJSON() ([]byte, error) UnmarshalJSON([]byte) error + // MarshalMsgpack() ([]byte, error) + // UnmarshalMsgpack([]byte) error + // Serialize() interface{} } // NewChunk creates a new chunk. @@ -277,3 +282,66 @@ func (c *Chunk) UnmarshalJSON(b []byte) error { return fmt.Errorf("Chunk.UnmarshalJSON: unsupported chunk type '%d'", c.Type) } } + +func (c *Chunk) EncodeMsgpack(enc *msgpack.Encoder) error { + data := c.Accessor + + generic := &JSONChunk{ + Type: c.Type, + BinData: data, + } + + return enc.Encode(generic) +} + +func (c *Chunk) DecodeMsgpack(dec *msgpack.Decoder) error { + generic := &JSONChunk{} + err := dec.Decode(generic) + if err != nil { + return fmt.Errorf("Chunk.DecodeMsgpack: %s", err) + } + + switch c.Type { + case MapType: + c.Accessor = generic.BinData.(MapAccessor) + default: + return fmt.Errorf("Chunk.DecodeMsgpack: unsupported chunk type '%d'", c.Type) + } + + return nil +} + +// // MarshalMsgpack writes the chunk to msgpack format. +// func (c *Chunk) MarshalMsgpack() ([]byte, error) { +// // data, err := c.Accessor.MarshalMsgpack() +// // if err != nil { +// // return []byte{}, err +// // } +// data := c.Accessor +// +// generic := &JSONChunk{ +// Type: c.Type, +// BinData: data, +// } +// b, err := msgpack.Marshal(generic) +// return b, err +// } +// +// // UnmarshalMsgpack loads the chunk from msgpack format. +// func (c *Chunk) UnmarshalMsgpack(b []byte) error { +// // Parse it generically so we can hand off the inner "data" object to the +// // right accessor for unmarshalling. +// generic := &JSONChunk{} +// err := msgpack.Unmarshal(b, generic) +// if err != nil { +// return fmt.Errorf("Chunk.UnmarshalMsgpack: failed to unmarshal into generic JSONChunk type: %s", err) +// } +// +// switch c.Type { +// case MapType: +// c.Accessor = NewMapAccessor() +// return c.Accessor.UnmarshalMsgpack(generic.Data) +// default: +// return fmt.Errorf("Chunk.UnmarshalMsgpack: unsupported chunk type '%d'", c.Type) +// } +// } diff --git a/pkg/level/chunk_map.go b/pkg/level/chunk_map.go index abc0e7e..3342830 100644 --- a/pkg/level/chunk_map.go +++ b/pkg/level/chunk_map.go @@ -6,6 +6,7 @@ import ( "fmt" "git.kirsle.net/apps/doodle/lib/render" + "github.com/vmihailenco/msgpack" ) // MapAccessor implements a chunk accessor by using a map of points to their @@ -127,3 +128,65 @@ func (a MapAccessor) UnmarshalJSON(b []byte) error { return nil } + +// // MarshalMsgpack serializes for msgpack. +// func (a MapAccessor) MarshalMsgpack() ([]byte, error) { +// dict := map[string]int{} +// for point, sw := range a { +// dict[point.String()] = sw.Index() +// } +// return msgpack.Marshal(dict) +// } +// +// // Serialize converts the chunk accessor to a map for serialization. +// func (a MapAccessor) Serialize() interface{} { +// dict := map[string]int{} +// for point, sw := range a { +// dict[point.String()] = sw.Index() +// } +// return dict +// } +// +// // UnmarshalMsgpack decodes from msgpack format. +// func (a MapAccessor) UnmarshalMsgpack(b []byte) error { +// var dict map[string]int +// err := msgpack.Unmarshal(b, &dict) +// if err != nil { +// return err +// } +// +// for coord, index := range dict { +// point, err := render.ParsePoint(coord) +// if err != nil { +// return fmt.Errorf("MapAccessor.UnmarshalJSON: %s", err) +// } +// a[point] = NewSparseSwatch(index) +// } +// +// return nil +// } + +func (a MapAccessor) EncodeMsgpack(enc *msgpack.Encoder) error { + dict := map[string]int{} + for point, sw := range a { + dict[point.String()] = sw.Index() + } + return enc.Encode(dict) +} + +func (a MapAccessor) DecodeMsgpack(dec *msgpack.Decoder) error { + v, err := dec.DecodeMap() + if err != nil { + return fmt.Errorf("MapAccessor.DecodeMsgpack: %s", err) + } + dict := v.(map[string]int) + + for coord, index := range dict { + point, err := render.ParsePoint(coord) + if err != nil { + return fmt.Errorf("MapAccessor.UnmarshalJSON: %s", err) + } + a[point] = NewSparseSwatch(index) + } + return nil +} diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index 05735bc..b8ff013 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -6,6 +6,7 @@ import ( "math" "git.kirsle.net/apps/doodle/lib/render" + "github.com/vmihailenco/msgpack" ) // Chunker is the data structure that manages the chunks of a level, and @@ -240,3 +241,14 @@ func (c ChunkMap) MarshalJSON() ([]byte, error) { out, err := json.Marshal(dict) return out, err } + +// MarshalMsgpack to convert the chunk map to binary. +func (c ChunkMap) MarshalMsgpack() ([]byte, error) { + dict := map[string]*Chunk{} + for point, chunk := range c { + dict[point.String()] = chunk + } + + out, err := msgpack.Marshal(dict) + return out, err +} diff --git a/pkg/level/fmt_binary.go b/pkg/level/fmt_binary.go new file mode 100644 index 0000000..bee89c7 --- /dev/null +++ b/pkg/level/fmt_binary.go @@ -0,0 +1,71 @@ +package level + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + + "git.kirsle.net/apps/doodle/pkg/balance" + "github.com/vmihailenco/msgpack" +) + +// ToBinary serializes the level to binary format. +func (m *Level) ToBinary() ([]byte, error) { + header := balance.MakeHeader(balance.BinLevelType) + out := bytes.NewBuffer(header) + encoder := msgpack.NewEncoder(out) + err := encoder.Encode(m) + return out.Bytes(), err +} + +// WriteBinary writes a level to binary format on disk. +func (m *Level) WriteBinary(filename string) error { + bin, err := m.ToBinary() + if err != nil { + return fmt.Errorf("Level.WriteBinary: encode error: %s", err) + } + + err = ioutil.WriteFile(filename, bin, 0755) + if err != nil { + return fmt.Errorf("Level.WriteBinary: WriteFile error: %s", err) + } + + return nil +} + +// LoadBinary loads a map from binary file on disk. +func LoadBinary(filename string) (*Level, error) { + fh, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fh.Close() + + // Read and verify the file header from the binary format. + err = balance.ReadHeader(balance.BinLevelType, fh) + if err != nil { + return nil, err + } + + // Decode the file from disk. + m := New() + decoder := msgpack.NewDecoder(fh) + err = decoder.Decode(&m) + if err != nil { + return m, fmt.Errorf("level.LoadBinary: decode error: %s", err) + } + + // 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 +} diff --git a/pkg/level/json.go b/pkg/level/fmt_json.go similarity index 92% rename from pkg/level/json.go rename to pkg/level/fmt_json.go index 50a68d2..48781cb 100644 --- a/pkg/level/json.go +++ b/pkg/level/fmt_json.go @@ -6,32 +6,21 @@ import ( "fmt" "io/ioutil" "os" + + "git.kirsle.net/apps/doodle/pkg/balance" ) // ToJSON serializes the level as JSON. func (m *Level) ToJSON() ([]byte, error) { out := bytes.NewBuffer([]byte{}) encoder := json.NewEncoder(out) - encoder.SetIndent("", "\t") + if balance.JSONIndent { + encoder.SetIndent("", "\t") + } err := encoder.Encode(m) return out.Bytes(), err } -// WriteJSON writes a level to JSON on disk. -func (m *Level) WriteJSON(filename string) error { - json, err := m.ToJSON() - if err != nil { - return fmt.Errorf("Level.WriteJSON: JSON encode error: %s", err) - } - - err = ioutil.WriteFile(filename, json, 0755) - if err != nil { - return fmt.Errorf("Level.WriteJSON: WriteFile error: %s", err) - } - - return nil -} - // LoadJSON loads a map from JSON file. func LoadJSON(filename string) (*Level, error) { fh, err := os.Open(filename) @@ -61,3 +50,18 @@ func LoadJSON(filename string) (*Level, error) { m.Palette.Inflate() return m, err } + +// WriteJSON writes a level to JSON on disk. +func (m *Level) WriteJSON(filename string) error { + json, err := m.ToJSON() + if err != nil { + return fmt.Errorf("Level.WriteJSON: JSON encode error: %s", err) + } + + err = ioutil.WriteFile(filename, json, 0755) + if err != nil { + return fmt.Errorf("Level.WriteJSON: WriteFile error: %s", err) + } + + return nil +} diff --git a/pkg/level/fmt_readwrite.go b/pkg/level/fmt_readwrite.go new file mode 100644 index 0000000..07bd774 --- /dev/null +++ b/pkg/level/fmt_readwrite.go @@ -0,0 +1,65 @@ +package level + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/enum" + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/userdir" +) + +// LoadFile reads a level file from disk, checking a few locations. +func LoadFile(filename string) (*Level, error) { + if !strings.HasSuffix(filename, enum.LevelExt) { + filename += enum.LevelExt + } + + // Search the system and user paths for this level. + filename, err := balance.FindFile(filename) + if err != nil { + return nil, err + } + + // Try the binary format. + if level, err := LoadBinary(filename); err == nil { + return level, nil + } else { + log.Warn(err.Error()) + } + + // Then the JSON format. + if level, err := LoadJSON(filename); err == nil { + return level, nil + } else { + log.Warn(err.Error()) + } + + return nil, errors.New("invalid file type") +} + +// WriteFile saves a level to disk in the user's config directory. +func (m *Level) WriteFile(filename string) error { + if !strings.HasSuffix(filename, enum.LevelExt) { + filename += enum.LevelExt + } + + // bin, err := m.ToBinary() + bin, err := m.ToJSON() + if err != nil { + return err + } + + // Save it to their profile directory. + filename = userdir.LevelPath(filename) + log.Info("Write Level: %s", filename) + err = ioutil.WriteFile(filename, bin, 0644) + if err != nil { + return fmt.Errorf("level.WriteFile: %s", err) + } + + return nil +} diff --git a/pkg/level/types.go b/pkg/level/types.go index 596d1dd..fa64593 100644 --- a/pkg/level/types.go +++ b/pkg/level/types.go @@ -16,36 +16,36 @@ var ( // Base provides the common struct keys that are shared between Levels and // Doodads. type Base struct { - Version int `json:"version"` // File format version spec. - GameVersion string `json:"gameVersion"` // Game version that created the level. - Title string `json:"title"` - Author string `json:"author"` + Version int `json:"version" msgpack:"0"` // File format version spec. + GameVersion string `json:"gameVersion" msgpack:"1"` // Game version that created the level. + Title string `json:"title" msgpack:"2"` + Author string `json:"author" msgpack:"3"` // Every drawing type is able to embed other files inside of itself. - Files FileSystem `json:"files"` + Files FileSystem `json:"files" msgpack:"4"` } // Level is the container format for Doodle map drawings. type Level struct { Base - Password string `json:"passwd"` - Locked bool `json:"locked"` + Password string `json:"passwd" msgpack:"10"` + Locked bool `json:"locked" msgpack:"11"` // Chunked pixel data. - Chunker *Chunker `json:"chunks"` + Chunker *Chunker `json:"chunks" msgpack:"12"` // The Palette holds the unique "colors" used in this map file, and their // properties (solid, fire, slippery, etc.) - Palette *Palette `json:"palette"` + Palette *Palette `json:"palette" msgpack:"13"` // Page boundaries and wallpaper settings. - PageType PageType `json:"pageType"` - MaxWidth int64 `json:"boundedWidth"` // only if bounded or bordered - MaxHeight int64 `json:"boundedHeight"` - Wallpaper string `json:"wallpaper"` + PageType PageType `json:"pageType" msgpack:"14"` + MaxWidth int64 `json:"boundedWidth" msgpack:"15"` // only if bounded or bordered + MaxHeight int64 `json:"boundedHeight" msgpack:"16"` + Wallpaper string `json:"wallpaper" msgpack:"17"` // Actors keep a list of the doodad instances in this map. - Actors ActorMap `json:"actors"` + Actors ActorMap `json:"actors" msgpack:"18"` } // New creates a blank level object with all its members initialized. diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 9076d8b..8b6f69d 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -198,6 +198,11 @@ func (s *PlayScene) movePlayer(ev *events.State) { } s.Player.SetVelocity(velocity) + + // TODO: invoke the player OnKeypress for animation testing + // if velocity != render.Origin { + s.scripting.To(s.Player.ID()).Events.RunKeypress(ev) + // } } // Drawing returns the private world drawing, for debugging with the console. diff --git a/pkg/scripting/events.go b/pkg/scripting/events.go index 7cf63de..8fac86b 100644 --- a/pkg/scripting/events.go +++ b/pkg/scripting/events.go @@ -1,6 +1,7 @@ package scripting import ( + "git.kirsle.net/apps/doodle/lib/events" "github.com/robertkrimen/otto" ) @@ -36,6 +37,16 @@ func (e *Events) RunCollide() error { return e.run(CollideEvent) } +// OnKeypress fires when another actor collides with yours. +func (e *Events) OnKeypress(call otto.FunctionCall) otto.Value { + return e.register(KeypressEvent, call.Argument(0)) +} + +// RunKeypress invokes the OnCollide handler function. +func (e *Events) RunKeypress(ev *events.State) error { + return e.run(KeypressEvent, ev) +} + // register a named event. func (e *Events) register(name string, callback otto.Value) otto.Value { if !callback.IsFunction() {