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.
This commit is contained in:
parent
d042457365
commit
f76ba6fbb7
31
Building.md
31
Building.md
|
@ -4,6 +4,10 @@ Makefile commands for Linux:
|
||||||
|
|
||||||
* `make setup`: install Go dependencies and set up the build environment
|
* `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`: 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 run`: run a local dev build of Doodle in debug mode
|
||||||
* `make guitest`: run a local dev build in the GUITest scene
|
* `make guitest`: run a local dev build in the GUITest scene
|
||||||
* `make test`: run the test suite
|
* `make test`: run the test suite
|
||||||
|
@ -17,6 +21,33 @@ Makefile commands for Linux:
|
||||||
* `make docker.fedora`
|
* `make docker.fedora`
|
||||||
* `make clean`: clean all build artifacts
|
* `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
|
## Linux
|
||||||
|
|
||||||
Dependencies are Go, SDL2 and SDL2_ttf:
|
Dependencies are Go, SDL2 and SDL2_ttf:
|
||||||
|
|
7
Makefile
7
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/doodle cmd/doodle/main.go
|
||||||
go build $(LDFLAGS) -tags="shareware" -i -o bin/doodad cmd/doodad/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.
|
# `make doodads` to build the doodads from the dev-assets folder.
|
||||||
.PHONY: doodads
|
.PHONY: doodads
|
||||||
doodads:
|
doodads:
|
||||||
|
|
|
@ -1,4 +1,52 @@
|
||||||
function main() {
|
function main() {
|
||||||
log.Info("Azulian '%s' initialized!", Self.Doodad.Title);
|
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));
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/vmihailenco/msgpack"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -155,6 +157,52 @@ func (c *Color) UnmarshalJSON(b []byte) error {
|
||||||
return nil
|
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.
|
// Add a relative color value to the color.
|
||||||
func (c Color) Add(r, g, b, a int) Color {
|
func (c Color) Add(r, g, b, a int) Color {
|
||||||
var (
|
var (
|
||||||
|
|
22
mp2json.py
Normal file
22
mp2json.py
Normal file
|
@ -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 <filename.level>")
|
||||||
|
|
||||||
|
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))
|
|
@ -30,6 +30,9 @@ var (
|
||||||
// Put a border around all Canvas widgets.
|
// Put a border around all Canvas widgets.
|
||||||
DebugCanvasBorder = render.Invisible
|
DebugCanvasBorder = render.Invisible
|
||||||
DebugCanvasLabel = false // Tag the canvas with a label.
|
DebugCanvasLabel = false // Tag the canvas with a label.
|
||||||
|
|
||||||
|
// Pretty-print JSON files when writing.
|
||||||
|
JSONIndent = false
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
118
pkg/balance/filesystem.go
Normal file
118
pkg/balance/filesystem.go
Normal file
|
@ -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")
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package doodle
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -171,7 +170,7 @@ func (s *EditorScene) Draw(d *Doodle) error {
|
||||||
func (s *EditorScene) LoadLevel(filename string) error {
|
func (s *EditorScene) LoadLevel(filename string) error {
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
|
|
||||||
level, err := level.LoadJSON(filename)
|
level, err := level.LoadFile(filename)
|
||||||
fmt.Printf("%+v\n", level)
|
fmt.Printf("%+v\n", level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
|
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.Palette = s.UI.Canvas.Palette
|
||||||
m.Chunker = s.UI.Canvas.Chunker()
|
m.Chunker = s.UI.Canvas.Chunker()
|
||||||
|
|
||||||
json, err := m.ToJSON()
|
return m.WriteFile(filename)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDoodad loads a doodad from disk.
|
// LoadDoodad loads a doodad from disk.
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"github.com/satori/go.uuid"
|
"github.com/satori/go.uuid"
|
||||||
|
"github.com/vmihailenco/msgpack"
|
||||||
"golang.org/x/image/bmp"
|
"golang.org/x/image/bmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,8 +42,9 @@ type Chunk struct {
|
||||||
// JSONChunk holds a lightweight (interface-free) copy of the Chunk for
|
// JSONChunk holds a lightweight (interface-free) copy of the Chunk for
|
||||||
// unmarshalling JSON files from disk.
|
// unmarshalling JSON files from disk.
|
||||||
type JSONChunk struct {
|
type JSONChunk struct {
|
||||||
Type int `json:"type"`
|
Type int `json:"type" msgpack:"0"`
|
||||||
Data json.RawMessage `json:"data"`
|
Data json.RawMessage `json:"data" msgpack:"-"`
|
||||||
|
BinData interface{} `json:"-" msgpack:"1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accessor provides a high-level API to interact with absolute pixel coordinates
|
// Accessor provides a high-level API to interact with absolute pixel coordinates
|
||||||
|
@ -57,6 +59,9 @@ type Accessor interface {
|
||||||
Len() int
|
Len() int
|
||||||
MarshalJSON() ([]byte, error)
|
MarshalJSON() ([]byte, error)
|
||||||
UnmarshalJSON([]byte) error
|
UnmarshalJSON([]byte) error
|
||||||
|
// MarshalMsgpack() ([]byte, error)
|
||||||
|
// UnmarshalMsgpack([]byte) error
|
||||||
|
// Serialize() interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChunk creates a new chunk.
|
// 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)
|
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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/lib/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"github.com/vmihailenco/msgpack"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MapAccessor implements a chunk accessor by using a map of points to their
|
// 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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/lib/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"github.com/vmihailenco/msgpack"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chunker is the data structure that manages the chunks of a level, and
|
// 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)
|
out, err := json.Marshal(dict)
|
||||||
return out, err
|
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
|
||||||
|
}
|
||||||
|
|
71
pkg/level/fmt_binary.go
Normal file
71
pkg/level/fmt_binary.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -6,32 +6,21 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToJSON serializes the level as JSON.
|
// ToJSON serializes the level as JSON.
|
||||||
func (m *Level) ToJSON() ([]byte, error) {
|
func (m *Level) ToJSON() ([]byte, error) {
|
||||||
out := bytes.NewBuffer([]byte{})
|
out := bytes.NewBuffer([]byte{})
|
||||||
encoder := json.NewEncoder(out)
|
encoder := json.NewEncoder(out)
|
||||||
encoder.SetIndent("", "\t")
|
if balance.JSONIndent {
|
||||||
|
encoder.SetIndent("", "\t")
|
||||||
|
}
|
||||||
err := encoder.Encode(m)
|
err := encoder.Encode(m)
|
||||||
return out.Bytes(), err
|
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.
|
// LoadJSON loads a map from JSON file.
|
||||||
func LoadJSON(filename string) (*Level, error) {
|
func LoadJSON(filename string) (*Level, error) {
|
||||||
fh, err := os.Open(filename)
|
fh, err := os.Open(filename)
|
||||||
|
@ -61,3 +50,18 @@ func LoadJSON(filename string) (*Level, error) {
|
||||||
m.Palette.Inflate()
|
m.Palette.Inflate()
|
||||||
return m, err
|
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
|
||||||
|
}
|
65
pkg/level/fmt_readwrite.go
Normal file
65
pkg/level/fmt_readwrite.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -16,36 +16,36 @@ var (
|
||||||
// Base provides the common struct keys that are shared between Levels and
|
// Base provides the common struct keys that are shared between Levels and
|
||||||
// Doodads.
|
// Doodads.
|
||||||
type Base struct {
|
type Base struct {
|
||||||
Version int `json:"version"` // File format version spec.
|
Version int `json:"version" msgpack:"0"` // File format version spec.
|
||||||
GameVersion string `json:"gameVersion"` // Game version that created the level.
|
GameVersion string `json:"gameVersion" msgpack:"1"` // Game version that created the level.
|
||||||
Title string `json:"title"`
|
Title string `json:"title" msgpack:"2"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author" msgpack:"3"`
|
||||||
|
|
||||||
// Every drawing type is able to embed other files inside of itself.
|
// 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.
|
// Level is the container format for Doodle map drawings.
|
||||||
type Level struct {
|
type Level struct {
|
||||||
Base
|
Base
|
||||||
Password string `json:"passwd"`
|
Password string `json:"passwd" msgpack:"10"`
|
||||||
Locked bool `json:"locked"`
|
Locked bool `json:"locked" msgpack:"11"`
|
||||||
|
|
||||||
// Chunked pixel data.
|
// 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
|
// The Palette holds the unique "colors" used in this map file, and their
|
||||||
// properties (solid, fire, slippery, etc.)
|
// properties (solid, fire, slippery, etc.)
|
||||||
Palette *Palette `json:"palette"`
|
Palette *Palette `json:"palette" msgpack:"13"`
|
||||||
|
|
||||||
// Page boundaries and wallpaper settings.
|
// Page boundaries and wallpaper settings.
|
||||||
PageType PageType `json:"pageType"`
|
PageType PageType `json:"pageType" msgpack:"14"`
|
||||||
MaxWidth int64 `json:"boundedWidth"` // only if bounded or bordered
|
MaxWidth int64 `json:"boundedWidth" msgpack:"15"` // only if bounded or bordered
|
||||||
MaxHeight int64 `json:"boundedHeight"`
|
MaxHeight int64 `json:"boundedHeight" msgpack:"16"`
|
||||||
Wallpaper string `json:"wallpaper"`
|
Wallpaper string `json:"wallpaper" msgpack:"17"`
|
||||||
|
|
||||||
// Actors keep a list of the doodad instances in this map.
|
// 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.
|
// New creates a blank level object with all its members initialized.
|
||||||
|
|
|
@ -198,6 +198,11 @@ func (s *PlayScene) movePlayer(ev *events.State) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Player.SetVelocity(velocity)
|
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.
|
// Drawing returns the private world drawing, for debugging with the console.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package scripting
|
package scripting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/lib/events"
|
||||||
"github.com/robertkrimen/otto"
|
"github.com/robertkrimen/otto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,6 +37,16 @@ func (e *Events) RunCollide() error {
|
||||||
return e.run(CollideEvent)
|
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.
|
// register a named event.
|
||||||
func (e *Events) register(name string, callback otto.Value) otto.Value {
|
func (e *Events) register(name string, callback otto.Value) otto.Value {
|
||||||
if !callback.IsFunction() {
|
if !callback.IsFunction() {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user