Signed Levels and Levelpacks
Add the ability for the free version of the game to allow loading levels that use embedded custom doodads if those levels are signed. * Uses the same signing keys as the JWT token for license registrations. * Levels and Levelpacks can both be signed. So individual levels with embedded doodads can work in free versions of the game. * Levelpacks now support embedded doodads properly: the individual levels in the pack don't need to embed a custom doodad, but if the doodad exists in the levelpack's doodads/ folder it will load from there instead - for full versions of the game OR when the levelpack is signed. Signatures are computed by getting a listing of embedded assets inside the zipfile (the assets/ folder in levels, and the doodads/ + levels/ folders in levelpacks). Thus for individual signed levels, the level geometry and metadata may be changed without breaking the signature but if custom doodads are changed the signature will break. The doodle-admin command adds subcommands to `sign-level` and `verify-level` to manage signatures on levels and levelpacks. When using the `doodad levelpack create` command, any custom doodads the levels mention that are found in your profile directory get embedded into the zipfile by default (with --doodads custom).
This commit is contained in:
parent
856de848c9
commit
82884c79ae
|
@ -56,7 +56,7 @@ func init() {
|
||||||
Name: "doodads",
|
Name: "doodads",
|
||||||
Aliases: []string{"D"},
|
Aliases: []string{"D"},
|
||||||
Usage: "which doodads to embed: none, custom, all",
|
Usage: "which doodads to embed: none, custom, all",
|
||||||
Value: "all",
|
Value: "custom",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: levelpackCreate,
|
Action: levelpackCreate,
|
||||||
|
@ -233,7 +233,7 @@ func levelpackCreate(c *cli.Context) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("New doodad: %s", actor.Filename)
|
log.Info("Adding doodad to zipfile: %s", actor.Filename)
|
||||||
|
|
||||||
// Get this doodad from the game's built-ins or the user's
|
// Get this doodad from the game's built-ins or the user's
|
||||||
// profile directory only. Pulling embedded doodads out of
|
// profile directory only. Pulling embedded doodads out of
|
||||||
|
|
92
cmd/doodle-admin/command/sign_level.go
Normal file
92
cmd/doodle-admin/command/sign_level.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license/levelsigning"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignLevel a license key for Sketchy Maze.
|
||||||
|
var SignLevel *cli.Command
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SignLevel = &cli.Command{
|
||||||
|
Name: "sign-level",
|
||||||
|
Usage: "sign a level file so that it may use embedded assets in free versions of the game.",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Aliases: []string{"k"},
|
||||||
|
Usage: "Private key .pem file for signing",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "input",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "Input file name (.level or .levelpack)",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "Output file, default outputs to console",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
key, err := license.AdminLoadPrivateKey(c.String("key"))
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
filename = c.String("input")
|
||||||
|
output = c.String("output")
|
||||||
|
)
|
||||||
|
if output == "" {
|
||||||
|
output = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign a level?
|
||||||
|
if strings.HasSuffix(filename, ".level") {
|
||||||
|
lvl, err := level.LoadJSON(filename)
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign it.
|
||||||
|
if sig, err := levelsigning.SignLevel(key, lvl); err != nil {
|
||||||
|
return cli.Exit(fmt.Errorf("couldn't sign level: %s", err), 1)
|
||||||
|
} else {
|
||||||
|
lvl.Signature = sig
|
||||||
|
err := lvl.WriteFile(output)
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(filename, ".levelpack") {
|
||||||
|
lp, err := levelpack.LoadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign it.
|
||||||
|
if sig, err := levelsigning.SignLevelPack(key, lp); err != nil {
|
||||||
|
return cli.Exit(fmt.Errorf("couldn't sign levelpack: %s", err), 1)
|
||||||
|
} else {
|
||||||
|
lp.Signature = sig
|
||||||
|
err := lp.WriteZipfile(output)
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
73
cmd/doodle-admin/command/verify_level.go
Normal file
73
cmd/doodle-admin/command/verify_level.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license/levelsigning"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyLevel a license key for Sketchy Maze.
|
||||||
|
var VerifyLevel *cli.Command
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
VerifyLevel = &cli.Command{
|
||||||
|
Name: "verify-level",
|
||||||
|
Usage: "check the signature on a level or levelpack file.",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Aliases: []string{"k"},
|
||||||
|
Usage: "Public key .pem file that signed the level",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "filename",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "File name of the .level or .levelpack",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
key, err := license.AdminLoadPublicKey(c.String("key"))
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := c.String("filename")
|
||||||
|
if strings.HasSuffix(filename, ".level") {
|
||||||
|
lvl, err := level.LoadJSON(filename)
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it.
|
||||||
|
if ok := levelsigning.VerifyLevel(key, lvl); !ok {
|
||||||
|
log.Error("Signature is not valid!")
|
||||||
|
return cli.Exit("", 1)
|
||||||
|
} else {
|
||||||
|
log.Info("Level signature is OK!")
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(filename, ".levelpack") {
|
||||||
|
lp, err := levelpack.LoadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it.
|
||||||
|
if ok := levelsigning.VerifyLevelPack(key, lp); !ok {
|
||||||
|
log.Error("Signature is not valid!")
|
||||||
|
return cli.Exit("", 1)
|
||||||
|
} else {
|
||||||
|
log.Info("Levelpack signature is OK!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,8 @@ func main() {
|
||||||
command.Key,
|
command.Key,
|
||||||
command.Sign,
|
command.Sign,
|
||||||
command.Verify,
|
command.Verify,
|
||||||
|
command.SignLevel,
|
||||||
|
command.VerifyLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(cli.FlagsByName(app.Flags))
|
sort.Sort(cli.FlagsByName(app.Flags))
|
||||||
|
|
|
@ -114,12 +114,20 @@ func ListBuiltin() ([]string, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFromEmbeddable reads a doodad file, checking a level's embeddable
|
/*
|
||||||
// file data in addition to the usual places.
|
LoadFromEmbeddable reads a doodad file, checking a level's embeddable
|
||||||
func LoadFromEmbeddable(filename string, fs filesystem.Embeddable) (*Doodad, error) {
|
file data in addition to the usual places.
|
||||||
|
|
||||||
|
Use a true value for `force` to always return the file if available. By
|
||||||
|
default it will do a license check and free versions of the game won't
|
||||||
|
read the asset and get an error instead. A "Signed Level" is allowed to
|
||||||
|
use embedded assets in free versions and the caller uses force=true to
|
||||||
|
communicate the signature status.
|
||||||
|
*/
|
||||||
|
func LoadFromEmbeddable(filename string, fs filesystem.Embeddable, force bool) (*Doodad, error) {
|
||||||
if bin, err := fs.GetFile(balance.EmbeddedDoodadsBasePath + filename); err == nil {
|
if bin, err := fs.GetFile(balance.EmbeddedDoodadsBasePath + filename); err == nil {
|
||||||
log.Debug("doodads.LoadFromEmbeddable: found %s", filename)
|
log.Debug("doodads.LoadFromEmbeddable: found %s", filename)
|
||||||
if !license.IsRegistered() {
|
if !force && !license.IsRegistered() {
|
||||||
return nil, license.ErrRegisteredFeature
|
return nil, license.ErrRegisteredFeature
|
||||||
}
|
}
|
||||||
return Deserialize(filename, bin)
|
return Deserialize(filename, bin)
|
||||||
|
|
|
@ -341,11 +341,11 @@ func (d *Doodle) PlayLevel(filename string) error {
|
||||||
|
|
||||||
// PlayFromLevelpack initializes the Play Scene from a level as part of
|
// PlayFromLevelpack initializes the Play Scene from a level as part of
|
||||||
// a levelpack.
|
// a levelpack.
|
||||||
func (d *Doodle) PlayFromLevelpack(pack levelpack.LevelPack, which levelpack.Level) error {
|
func (d *Doodle) PlayFromLevelpack(pack *levelpack.LevelPack, which levelpack.Level) error {
|
||||||
log.Info("Loading level %s from levelpack %s", which.Filename, pack.Title)
|
log.Info("Loading level %s from levelpack %s", which.Filename, pack.Title)
|
||||||
scene := &PlayScene{
|
scene := &PlayScene{
|
||||||
Filename: which.Filename,
|
Filename: which.Filename,
|
||||||
LevelPack: &pack,
|
LevelPack: pack,
|
||||||
}
|
}
|
||||||
d.Goto(scene)
|
d.Goto(scene)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -132,7 +132,9 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
|
||||||
"by "+s.Level.Author,
|
"by "+s.Level.Author,
|
||||||
)
|
)
|
||||||
s.UI.Canvas.LoadLevel(s.Level)
|
s.UI.Canvas.LoadLevel(s.Level)
|
||||||
s.UI.Canvas.InstallActors(s.Level.Actors)
|
if err := s.installActors(); err != nil {
|
||||||
|
log.Error("InstallActors: %s", err)
|
||||||
|
}
|
||||||
} else if s.filename != "" && s.OpenFile {
|
} else if s.filename != "" && s.OpenFile {
|
||||||
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
|
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
|
||||||
loadscreen.SetSubtitle(
|
loadscreen.SetSubtitle(
|
||||||
|
@ -141,7 +143,9 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
|
||||||
if err := s.LoadLevel(s.filename); err != nil {
|
if err := s.LoadLevel(s.filename); err != nil {
|
||||||
d.FlashError("LoadLevel error: %s", err)
|
d.FlashError("LoadLevel error: %s", err)
|
||||||
} else {
|
} else {
|
||||||
s.UI.Canvas.InstallActors(s.Level.Actors)
|
if err := s.installActors(); err != nil {
|
||||||
|
log.Error("InstallActors: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +243,25 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common function to install the actors into the level.
|
||||||
|
//
|
||||||
|
// InstallActors may return an error if doodads were not found - because the
|
||||||
|
// player is on the free version and can't load attached doodads from nonsigned
|
||||||
|
// files.
|
||||||
|
func (s *EditorScene) installActors() error {
|
||||||
|
if err := s.UI.Canvas.InstallActors(s.Level.Actors); err != nil {
|
||||||
|
summary := "This level references some doodads that were not found:"
|
||||||
|
if strings.Contains(err.Error(), license.ErrRegisteredFeature.Error()) {
|
||||||
|
summary = "This level contains embedded doodads, but this is not\n" +
|
||||||
|
"available in the free version of the game. The following\n" +
|
||||||
|
"doodads could not be loaded:"
|
||||||
|
}
|
||||||
|
modal.Alert("%s\n\n%s", summary, err).WithTitle("Level Errors")
|
||||||
|
return fmt.Errorf("EditorScene.LoadLevel: InstallActors: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Playtest switches the level into Play Mode.
|
// Playtest switches the level into Play Mode.
|
||||||
func (s *EditorScene) Playtest() {
|
func (s *EditorScene) Playtest() {
|
||||||
log.Info("Play Mode, Go!")
|
log.Info("Play Mode, Go!")
|
||||||
|
|
|
@ -34,7 +34,7 @@ func (u *EditorUI) startDragActor(doodad *doodads.Doodad, actor *level.Actor) {
|
||||||
|
|
||||||
if doodad == nil {
|
if doodad == nil {
|
||||||
if actor != nil {
|
if actor != nil {
|
||||||
obj, err := doodads.LoadFromEmbeddable(actor.Filename, u.Scene.Level)
|
obj, err := doodads.LoadFromEmbeddable(actor.Filename, u.Scene.Level, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("startDragExistingActor: actor doodad name %s not found: %s", actor.Filename, err)
|
log.Error("startDragExistingActor: actor doodad name %s not found: %s", actor.Filename, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -2,6 +2,7 @@ package level
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -93,6 +94,18 @@ func (fs *FileSystem) Delete(filename string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checksum returns a SHA-256 checksum of a file's data.
|
||||||
|
func (fs *FileSystem) Checksum(filename string) (string, error) {
|
||||||
|
data, err := fs.Get(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(data)
|
||||||
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
// List files in the FileSystem, including the ZIP file.
|
// List files in the FileSystem, including the ZIP file.
|
||||||
//
|
//
|
||||||
// In the ZIP file, attachments are under the "assets/" prefix so this
|
// In the ZIP file, attachments are under the "assets/" prefix so this
|
||||||
|
|
|
@ -95,7 +95,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) {
|
||||||
// Render the doodads.
|
// Render the doodads.
|
||||||
log.Debug("GiantScreenshot: Render actors...")
|
log.Debug("GiantScreenshot: Render actors...")
|
||||||
for _, actor := range lvl.Actors {
|
for _, actor := range lvl.Actors {
|
||||||
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl)
|
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GiantScreenshot: Load doodad: %s", err)
|
log.Error("GiantScreenshot: Load doodad: %s", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -52,7 +52,7 @@ func Publish(lvl *level.Level) error {
|
||||||
log.Debug("Embed filename: %s", filename)
|
log.Debug("Embed filename: %s", filename)
|
||||||
names[filename] = nil
|
names[filename] = nil
|
||||||
|
|
||||||
doodad, err := doodads.LoadFromEmbeddable(filename, lvl)
|
doodad, err := doodads.LoadFromEmbeddable(filename, lvl, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("couldn't load doodad %s: %s", filename, err)
|
return fmt.Errorf("couldn't load doodad %s: %s", filename, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,9 @@ type Level struct {
|
||||||
SaveDoodads bool `json:"saveDoodads"`
|
SaveDoodads bool `json:"saveDoodads"`
|
||||||
SaveBuiltins bool `json:"saveBuiltins"`
|
SaveBuiltins bool `json:"saveBuiltins"`
|
||||||
|
|
||||||
|
// Signature for a level with embedded doodads to still play in free mode.
|
||||||
|
Signature []byte `json:"signature,omitempty"`
|
||||||
|
|
||||||
// Undo history, temporary live data not persisted to the level file.
|
// Undo history, temporary live data not persisted to the level file.
|
||||||
UndoHistory *drawtool.History `json:"-"`
|
UndoHistory *drawtool.History `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,16 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/SketchyMaze/doodle/assets"
|
"git.kirsle.net/SketchyMaze/doodle/assets"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LevelPack describes the contents of a levelpack file.
|
// LevelPack describes the contents of a levelpack file.
|
||||||
type LevelPack struct {
|
type LevelPack struct {
|
||||||
Title string `json:"title`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
|
@ -40,6 +42,10 @@ type LevelPack struct {
|
||||||
|
|
||||||
// A reference to the original filename, not stored in json.
|
// A reference to the original filename, not stored in json.
|
||||||
Filename string `json:"-"`
|
Filename string `json:"-"`
|
||||||
|
|
||||||
|
// Signature to allow free versions of the game to load embedded
|
||||||
|
// custom doodads inside this levelpack for its levels.
|
||||||
|
Signature []byte `json:"signature,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level holds metadata about the levels in the levelpack.
|
// Level holds metadata about the levels in the levelpack.
|
||||||
|
@ -50,7 +56,7 @@ type Level struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFile reads a .levelpack zip file.
|
// LoadFile reads a .levelpack zip file.
|
||||||
func LoadFile(filename string) (LevelPack, error) {
|
func LoadFile(filename string) (*LevelPack, error) {
|
||||||
var (
|
var (
|
||||||
fh io.ReaderAt
|
fh io.ReaderAt
|
||||||
filesize int64
|
filesize int64
|
||||||
|
@ -66,33 +72,33 @@ func LoadFile(filename string) (LevelPack, error) {
|
||||||
if fh == nil {
|
if fh == nil {
|
||||||
stat, err := os.Stat(filename)
|
stat, err := os.Stat(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LevelPack{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
filesize = stat.Size()
|
filesize = stat.Size()
|
||||||
|
|
||||||
fh, err = os.Open(filename)
|
fh, err = os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LevelPack{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No luck?
|
// No luck?
|
||||||
if fh == nil {
|
if fh == nil {
|
||||||
return LevelPack{}, errors.New("no file found")
|
return nil, errors.New("no file found")
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := zip.NewReader(fh, filesize)
|
reader, err := zip.NewReader(fh, filesize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LevelPack{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lp := LevelPack{
|
lp := &LevelPack{
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Zipfile: reader,
|
Zipfile: reader,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the index.json.
|
// Read the index.json.
|
||||||
lp.GetJSON(&lp, "index.json")
|
lp.GetJSON(lp, "index.json")
|
||||||
|
|
||||||
return lp, nil
|
return lp, nil
|
||||||
}
|
}
|
||||||
|
@ -100,18 +106,18 @@ func LoadFile(filename string) (LevelPack, error) {
|
||||||
// LoadAllAvailable loads every levelpack visible to the game. Returns
|
// LoadAllAvailable loads every levelpack visible to the game. Returns
|
||||||
// the sorted list of filenames as from ListFiles, plus a deeply loaded
|
// the sorted list of filenames as from ListFiles, plus a deeply loaded
|
||||||
// hash map associating the filenames with their data.
|
// hash map associating the filenames with their data.
|
||||||
func LoadAllAvailable() ([]string, map[string]LevelPack, error) {
|
func LoadAllAvailable() ([]string, map[string]*LevelPack, error) {
|
||||||
filenames, err := ListFiles()
|
filenames, err := ListFiles()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filenames, nil, err
|
return filenames, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var dictionary = map[string]LevelPack{}
|
var dictionary = map[string]*LevelPack{}
|
||||||
for _, filename := range filenames {
|
for _, filename := range filenames {
|
||||||
// Resolve the filename to a definite path on disk.
|
// Resolve the filename to a definite path on disk.
|
||||||
path, err := filesystem.FindFile(filename)
|
path, err := filesystem.FindFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Errorf("LoadAllAvailable: FindFile(%s): %s", path, err)
|
log.Error("LoadAllAvailable: FindFile(%s): %s", path, err)
|
||||||
return filenames, nil, err
|
return filenames, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,12 +191,58 @@ func (l LevelPack) WriteFile(filename string) error {
|
||||||
return ioutil.WriteFile(filename, out, 0655)
|
return ioutil.WriteFile(filename, out, 0655)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetData returns file data from inside the loaded zipfile of a levelpack.
|
// WriteZipfile saves a levelpack back into a zip file.
|
||||||
func (l LevelPack) GetData(filename string) ([]byte, error) {
|
func (l LevelPack) WriteZipfile(filename string) error {
|
||||||
|
fh, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
// Copy all of the levels and other files from the old zip to new zip.
|
||||||
|
zf := zip.NewWriter(fh)
|
||||||
|
defer zf.Close()
|
||||||
|
|
||||||
|
// Copy attached doodads and levels.
|
||||||
|
for _, file := range l.Zipfile.File {
|
||||||
|
if !strings.HasPrefix(file.Name, "doodads/") &&
|
||||||
|
!strings.HasPrefix(file.Name, "levels/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := zf.Copy(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the index.json metadata.
|
||||||
|
meta, err := json.Marshal(l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := zf.Create("index.json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = writer.Write(meta)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFile returns file data from inside the loaded zipfile of a levelpack.
|
||||||
|
//
|
||||||
|
// This also implements the Embeddable interface.
|
||||||
|
func (l LevelPack) GetFile(filename string) ([]byte, error) {
|
||||||
if l.Zipfile == nil {
|
if l.Zipfile == nil {
|
||||||
return []byte{}, errors.New("zipfile not loaded")
|
return []byte{}, errors.New("zipfile not loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: levelpacks don't have an "assets/" prefix but the game
|
||||||
|
// might come looking for "assets/doodads"
|
||||||
|
if strings.HasPrefix(filename, balance.EmbeddedDoodadsBasePath) {
|
||||||
|
filename = strings.Replace(filename, balance.EmbeddedDoodadsBasePath, "doodads/", 1)
|
||||||
|
}
|
||||||
|
|
||||||
file, err := l.Zipfile.Open(filename)
|
file, err := l.Zipfile.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return []byte{}, err
|
||||||
|
@ -201,7 +253,7 @@ func (l LevelPack) GetData(filename string) ([]byte, error) {
|
||||||
|
|
||||||
// GetJSON loads a JSON file from the zipfile and marshals it into your struct.
|
// GetJSON loads a JSON file from the zipfile and marshals it into your struct.
|
||||||
func (l LevelPack) GetJSON(v interface{}, filename string) error {
|
func (l LevelPack) GetJSON(v interface{}, filename string) error {
|
||||||
data, err := l.GetData(filename)
|
data, err := l.GetFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
193
pkg/license/levelsigning/level_signing.go
Normal file
193
pkg/license/levelsigning/level_signing.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package levelsigning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsLevelSigned returns a quick answer.
|
||||||
|
func IsLevelSigned(lvl *level.Level) bool {
|
||||||
|
return VerifyLevel(license.Signer, lvl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLevelPackSigned returns a quick answer.
|
||||||
|
func IsLevelPackSigned(lp *levelpack.LevelPack) bool {
|
||||||
|
return VerifyLevelPack(license.Signer, lp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SignLevel creates a signature on a level file which allows it to load its
|
||||||
|
embedded doodads even for free versions of the game.
|
||||||
|
|
||||||
|
Free versions will verify a level's signature before bailing out with the
|
||||||
|
"can't play levels w/ embedded doodads" response.
|
||||||
|
|
||||||
|
NOTE: this only supported Zipfile levels and will assume the level you
|
||||||
|
pass has a Zipfile to access embedded assets.
|
||||||
|
*/
|
||||||
|
func SignLevel(key *ecdsa.PrivateKey, lvl *level.Level) ([]byte, error) {
|
||||||
|
// Encode the attached files data to deterministic JSON.
|
||||||
|
certificate, err := StringifyAssets(lvl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Sign file tree: %s", certificate)
|
||||||
|
digest := shasum(certificate)
|
||||||
|
|
||||||
|
signature, err := ecdsa.SignASN1(rand.Reader, key, digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Info("Digest: %x Signature: %x", digest, signature)
|
||||||
|
|
||||||
|
return signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyLevel verifies a level's signature and returns if it is OK.
|
||||||
|
func VerifyLevel(publicKey *ecdsa.PublicKey, lvl *level.Level) bool {
|
||||||
|
// No signature = not verified.
|
||||||
|
if lvl.Signature == nil || len(lvl.Signature) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the attached files data to deterministic JSON.
|
||||||
|
certificate, err := StringifyAssets(lvl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("VerifyLevel: couldn't stringify assets: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := shasum(certificate)
|
||||||
|
|
||||||
|
// Verify the signature against our public key.
|
||||||
|
return ecdsa.VerifyASN1(publicKey, digest, lvl.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SignLevelpack applies a signature to a levelpack as a whole, to allow its
|
||||||
|
shared custom doodads to be loaded by its levels in free games.
|
||||||
|
*/
|
||||||
|
func SignLevelPack(key *ecdsa.PrivateKey, lp *levelpack.LevelPack) ([]byte, error) {
|
||||||
|
// Encode the attached files data to deterministic JSON.
|
||||||
|
certificate, err := StringifyLevelpackAssets(lp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Sign file tree: %s", certificate)
|
||||||
|
digest := shasum(certificate)
|
||||||
|
|
||||||
|
signature, err := ecdsa.SignASN1(rand.Reader, key, digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Info("Digest: %x Signature: %x", digest, signature)
|
||||||
|
|
||||||
|
return signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyLevelPack verifies a levelpack's signature and returns if it is OK.
|
||||||
|
func VerifyLevelPack(publicKey *ecdsa.PublicKey, lp *levelpack.LevelPack) bool {
|
||||||
|
// No signature = not verified.
|
||||||
|
if lp.Signature == nil || len(lp.Signature) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the attached files data to deterministic JSON.
|
||||||
|
certificate, err := StringifyLevelpackAssets(lp)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("VerifyLevelPack: couldn't stringify assets: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := shasum(certificate)
|
||||||
|
|
||||||
|
// Verify the signature against our public key.
|
||||||
|
return ecdsa.VerifyASN1(publicKey, digest, lp.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringifyAssets creates the signing checksum of a level's attached assets.
|
||||||
|
func StringifyAssets(lvl *level.Level) ([]byte, error) {
|
||||||
|
// Get a listing of all embedded files. Note: gives us a conveniently
|
||||||
|
// sorted array of files too.
|
||||||
|
files := lvl.Files.List()
|
||||||
|
|
||||||
|
// Pair each filename with its SHA256 sum.
|
||||||
|
var checksum = map[string]string{}
|
||||||
|
for _, filename := range files {
|
||||||
|
if sum, err := lvl.Files.Checksum(filename); err != nil {
|
||||||
|
return nil, fmt.Errorf("when checksum %s got error: %s", filename, err)
|
||||||
|
} else {
|
||||||
|
checksum[filename] = sum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the payload to deterministic JSON.
|
||||||
|
certificate, err := json.Marshal(checksum)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringifyLevelpackAssets creates the signing checksum of a level's attached assets.
|
||||||
|
func StringifyLevelpackAssets(lp *levelpack.LevelPack) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
files = []string{}
|
||||||
|
seen = map[string]struct{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enumerate the files in the zipfile assets/ folder.
|
||||||
|
for _, file := range lp.Zipfile.File {
|
||||||
|
if file.Name == "index.json" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := seen[file.Name]; !ok {
|
||||||
|
files = append(files, file.Name)
|
||||||
|
seen[file.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pair each filename with its SHA256 sum.
|
||||||
|
var checksum = map[string]string{}
|
||||||
|
for _, filename := range files {
|
||||||
|
file, err := lp.Zipfile.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bin, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum[filename] = fmt.Sprintf("%x", shasum(bin))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the payload to deterministic JSON.
|
||||||
|
certificate, err := json.Marshal(checksum)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common function to SHA-256 checksum a thing.
|
||||||
|
func shasum(data []byte) []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
|
@ -175,7 +175,7 @@ func (s *MainScene) Setup(d *Doodle) error {
|
||||||
Supervisor: s.Supervisor,
|
Supervisor: s.Supervisor,
|
||||||
Engine: d.Engine,
|
Engine: d.Engine,
|
||||||
|
|
||||||
OnPlayLevel: func(lp levelpack.LevelPack, which levelpack.Level) {
|
OnPlayLevel: func(lp *levelpack.LevelPack, which levelpack.Level) {
|
||||||
if err := d.PlayFromLevelpack(lp, which); err != nil {
|
if err := d.PlayFromLevelpack(lp, which); err != nil {
|
||||||
shmem.FlashError(err.Error())
|
shmem.FlashError(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -353,7 +353,7 @@ func (s *MainScene) SetupDemoLevel(d *Doodle) error {
|
||||||
log.Error("Error loading DemoLevelPack(%s): %s", balance.DemoLevelPack, err)
|
log.Error("Error loading DemoLevelPack(%s): %s", balance.DemoLevelPack, err)
|
||||||
} else {
|
} else {
|
||||||
log.Debug("Loading selected level from pack: %s", levelName)
|
log.Debug("Loading selected level from pack: %s", levelName)
|
||||||
levelbin, err := lp.GetData("levels/" + levelName)
|
levelbin, err := lp.GetFile("levels/" + levelName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error getting level from DemoLevelpack(%s#%s): %s",
|
log.Error("Error getting level from DemoLevelpack(%s#%s): %s",
|
||||||
balance.DemoLevelPack,
|
balance.DemoLevelPack,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
||||||
|
@ -12,6 +13,8 @@ import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license/levelsigning"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen"
|
||||||
|
@ -242,11 +245,18 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
|
||||||
s.drawing.OnSetPlayerCharacter = s.SetPlayerCharacter
|
s.drawing.OnSetPlayerCharacter = s.SetPlayerCharacter
|
||||||
s.drawing.OnResetTimer = s.ResetTimer
|
s.drawing.OnResetTimer = s.ResetTimer
|
||||||
|
|
||||||
|
// If this level game from a signed LevelPack, inform the canvas.
|
||||||
|
if s.LevelPack != nil && levelsigning.IsLevelPackSigned(s.LevelPack) {
|
||||||
|
s.drawing.IsSignedLevelPack = s.LevelPack
|
||||||
|
}
|
||||||
|
|
||||||
// Given a filename or map data to play?
|
// Given a filename or map data to play?
|
||||||
if s.Level != nil {
|
if s.Level != nil {
|
||||||
log.Debug("PlayScene.Setup: received level from scene caller")
|
log.Debug("PlayScene.Setup: received level from scene caller")
|
||||||
s.drawing.LoadLevel(s.Level)
|
s.drawing.LoadLevel(s.Level)
|
||||||
s.drawing.InstallActors(s.Level.Actors)
|
if err := s.installActors(); err != nil {
|
||||||
|
log.Error("InstallActors: %s", err)
|
||||||
|
}
|
||||||
} else if s.Filename != "" {
|
} else if s.Filename != "" {
|
||||||
loadscreen.SetSubtitle("Opening: " + s.Filename)
|
loadscreen.SetSubtitle("Opening: " + s.Filename)
|
||||||
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
|
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
|
||||||
|
@ -258,7 +268,9 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
|
||||||
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
||||||
s.Level = level.New()
|
s.Level = level.New()
|
||||||
s.drawing.LoadLevel(s.Level)
|
s.drawing.LoadLevel(s.Level)
|
||||||
s.drawing.InstallActors(s.Level.Actors)
|
if err := s.installActors(); err != nil {
|
||||||
|
log.Error("InstallActors: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose a death barrier in case the user falls off the map,
|
// Choose a death barrier in case the user falls off the map,
|
||||||
|
@ -316,6 +328,25 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common function to install the actors into the level.
|
||||||
|
//
|
||||||
|
// InstallActors may return an error if doodads were not found - because the
|
||||||
|
// player is on the free version and can't load attached doodads from nonsigned
|
||||||
|
// files.
|
||||||
|
func (s *PlayScene) installActors() error {
|
||||||
|
if err := s.drawing.InstallActors(s.Level.Actors); err != nil {
|
||||||
|
summary := "This level references some doodads that were not found:"
|
||||||
|
if strings.Contains(err.Error(), license.ErrRegisteredFeature.Error()) {
|
||||||
|
summary = "This level contains embedded doodads, but this is not\n" +
|
||||||
|
"available in the free version of the game. The following\n" +
|
||||||
|
"doodads could not be loaded:"
|
||||||
|
}
|
||||||
|
modal.Alert("%s\n\n%s", summary, err).WithTitle("Level Errors")
|
||||||
|
return fmt.Errorf("EditorScene.LoadLevel: InstallActors: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// PlaceResizeCanvas updates the Canvas size and placement on the screen,
|
// PlaceResizeCanvas updates the Canvas size and placement on the screen,
|
||||||
// e.g. if an ultra HD monitor plays a Bounded level where the entirety of a
|
// e.g. if an ultra HD monitor plays a Bounded level where the entirety of a
|
||||||
// level bounds is on-screen, the drawing should be cut there and the
|
// level bounds is on-screen, the drawing should be cut there and the
|
||||||
|
@ -472,7 +503,7 @@ func (s *PlayScene) setupPlayer(playerCharacterFilename string) {
|
||||||
// centerIn is optional, ignored if zero.
|
// centerIn is optional, ignored if zero.
|
||||||
func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, centerIn render.Rect) {
|
func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, centerIn render.Rect) {
|
||||||
// Load in the player character.
|
// Load in the player character.
|
||||||
player, err := doodads.LoadFromEmbeddable(filename, s.Level)
|
player, err := doodads.LoadFromEmbeddable(filename, s.Level, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("PlayScene.Setup: failed to load player doodad: %s", err)
|
log.Error("PlayScene.Setup: failed to load player doodad: %s", err)
|
||||||
player = doodads.NewDummy(32)
|
player = doodads.NewDummy(32)
|
||||||
|
@ -726,7 +757,7 @@ func (s *PlayScene) ShowEndLevelModal(success bool, title, message string) {
|
||||||
config.OnNextLevel = func() {
|
config.OnNextLevel = func() {
|
||||||
nextLevel := s.LevelPack.Levels[i+1]
|
nextLevel := s.LevelPack.Levels[i+1]
|
||||||
log.Info("Advance to next level: %s", nextLevel.Filename)
|
log.Info("Advance to next level: %s", nextLevel.Filename)
|
||||||
s.d.PlayFromLevelpack(*s.LevelPack, nextLevel)
|
s.d.PlayFromLevelpack(s.LevelPack, nextLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -897,7 +928,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
|
||||||
|
|
||||||
// Are we playing out of a levelpack?
|
// Are we playing out of a levelpack?
|
||||||
if s.LevelPack != nil {
|
if s.LevelPack != nil {
|
||||||
levelbin, err := s.LevelPack.GetData("levels/" + filename)
|
levelbin, err := s.LevelPack.GetFile("levels/" + filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error reading levels/%s from zip: %s", filename, err)
|
log.Error("Error reading levels/%s from zip: %s", filename, err)
|
||||||
}
|
}
|
||||||
|
@ -921,7 +952,9 @@ func (s *PlayScene) LoadLevel(filename string) error {
|
||||||
|
|
||||||
s.Level = lvl
|
s.Level = lvl
|
||||||
s.drawing.LoadLevel(s.Level)
|
s.drawing.LoadLevel(s.Level)
|
||||||
s.drawing.InstallActors(s.Level.Actors)
|
if err := s.installActors(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ func (u *PlayScene) setupMenuBar(d *Doodle) *ui.MenuBar {
|
||||||
Supervisor: u.Supervisor,
|
Supervisor: u.Supervisor,
|
||||||
Engine: d.Engine,
|
Engine: d.Engine,
|
||||||
|
|
||||||
OnPlayLevel: func(lp levelpack.LevelPack, which levelpack.Level) {
|
OnPlayLevel: func(lp *levelpack.LevelPack, which levelpack.Level) {
|
||||||
if err := d.PlayFromLevelpack(lp, which); err != nil {
|
if err := d.PlayFromLevelpack(lp, which); err != nil {
|
||||||
shmem.FlashError(err.Error())
|
shmem.FlashError(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/drawtool"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/wallpaper"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/wallpaper"
|
||||||
|
@ -78,6 +79,11 @@ type Canvas struct {
|
||||||
doodad *doodads.Doodad
|
doodad *doodads.Doodad
|
||||||
modified bool // set to True when the drawing has been modified, like in Editor Mode.
|
modified bool // set to True when the drawing has been modified, like in Editor Mode.
|
||||||
|
|
||||||
|
// PlayScene can set whether the Levelpack has a valid signature on it, so that
|
||||||
|
// in InstallActors() we can allow a levelpack to load attached doodads on free
|
||||||
|
// mode for their levels.
|
||||||
|
IsSignedLevelPack *levelpack.LevelPack // filled in ONLY IF SIGNED.
|
||||||
|
|
||||||
// Actors to superimpose on top of the drawing.
|
// Actors to superimpose on top of the drawing.
|
||||||
actor *Actor // if this canvas IS an actor
|
actor *Actor // if this canvas IS an actor
|
||||||
actors []*Actor // if this canvas CONTAINS actors (i.e., is a level)
|
actors []*Actor // if this canvas CONTAINS actors (i.e., is a level)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license/levelsigning"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
||||||
|
@ -33,13 +34,30 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
||||||
actor.Canvas.Destroy()
|
actor.Canvas.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signed Levels: the free version normally won't load embedded assets from
|
||||||
|
// a level and the call to LoadFromEmbeddable below returns the error. If the
|
||||||
|
// level is signed it is allowed to use its embedded assets.
|
||||||
|
isSigned := w.IsSignedLevelPack != nil || levelsigning.IsLevelSigned(w.level)
|
||||||
|
|
||||||
w.actors = make([]*Actor, 0)
|
w.actors = make([]*Actor, 0)
|
||||||
for _, id := range actorIDs {
|
for _, id := range actorIDs {
|
||||||
var actor = actors[id]
|
var actor = actors[id]
|
||||||
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, w.level)
|
|
||||||
|
// Try loading the doodad from the level's own attached files.
|
||||||
|
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, w.level, isSigned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Sprintf("%s: %s", actor.Filename, err.Error()))
|
// If we have a signed levelpack, try loading from the levelpack.
|
||||||
continue
|
if w.IsSignedLevelPack != nil {
|
||||||
|
if found, err := doodads.LoadFromEmbeddable(actor.Filename, w.IsSignedLevelPack, true); err == nil {
|
||||||
|
doodad = found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, append the error and continue.
|
||||||
|
if doodad == nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s: %s", actor.Filename, err.Error()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the "live" Actor to exist in the world, and set its world
|
// Create the "live" Actor to exist in the world, and set its world
|
||||||
|
|
|
@ -20,7 +20,7 @@ type LevelPack struct {
|
||||||
Engine render.Engine
|
Engine render.Engine
|
||||||
|
|
||||||
// Callback functions.
|
// Callback functions.
|
||||||
OnPlayLevel func(pack levelpack.LevelPack, level levelpack.Level)
|
OnPlayLevel func(pack *levelpack.LevelPack, level levelpack.Level)
|
||||||
OnCloseWindow func()
|
OnCloseWindow func()
|
||||||
|
|
||||||
// Internal variables
|
// Internal variables
|
||||||
|
@ -131,12 +131,13 @@ func NewLevelPackWindow(config LevelPack) *ui.Window {
|
||||||
return window
|
return window
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Index screen for the LevelPack window.
|
/*
|
||||||
|
Index screen for the LevelPack window.
|
||||||
|
|
||||||
frame: a TabFrame to populate
|
frame: a TabFrame to populate
|
||||||
*/
|
*/
|
||||||
func (config LevelPack) makeIndexScreen(frame *ui.Frame, width, height int,
|
func (config LevelPack) makeIndexScreen(frame *ui.Frame, width, height int,
|
||||||
lpFiles []string, packmap map[string]levelpack.LevelPack, onChoose func(string)) {
|
lpFiles []string, packmap map[string]*levelpack.LevelPack, onChoose func(string)) {
|
||||||
var (
|
var (
|
||||||
buttonHeight = 60 // height of each LevelPack button
|
buttonHeight = 60 // height of each LevelPack button
|
||||||
buttonWidth = width - 40
|
buttonWidth = width - 40
|
||||||
|
@ -268,7 +269,7 @@ func (config LevelPack) makeIndexScreen(frame *ui.Frame, width, height int,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detail screen for a given levelpack.
|
// Detail screen for a given levelpack.
|
||||||
func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp levelpack.LevelPack) *ui.Frame {
|
func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp *levelpack.LevelPack) *ui.Frame {
|
||||||
var (
|
var (
|
||||||
buttonHeight = 40
|
buttonHeight = 40
|
||||||
buttonWidth = width - 40
|
buttonWidth = width - 40
|
||||||
|
|
Loading…
Reference in New Issue
Block a user