doodle/pkg/level/publishing/publishing.go
Noah Petherbridge 82884c79ae 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).
2023-02-18 17:37:54 -08:00

121 lines
3.2 KiB
Go

/*
Package publishing contains functionality for "publishing" a Level, which
involves the writing and reading of custom doodads embedded inside
the levels.
Free tiers of the game will not read or write embedded doodads into
levels.
*/
package publishing
import (
"errors"
"fmt"
"sort"
"strings"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
)
/*
Level "Publishing" functions, involving the writing and reading of embedded
doodads within level files.
*/
// Publish writes a published level file, with embedded doodads included.
func Publish(lvl *level.Level) error {
// Not embedding doodads?
if !lvl.SaveDoodads {
if removed := lvl.DeleteFiles(balance.EmbeddedDoodadsBasePath); removed > 0 {
log.Info("Note: removed %d attached doodads because SaveDoodads is false", removed)
}
return nil
}
// Registered games only.
if !license.IsRegistered() {
return errors.New("only registered versions of the game can attach doodads to levels")
}
// Get and embed the doodads.
builtins, customs := GetUsedDoodadNames(lvl)
var names = map[string]interface{}{}
if lvl.SaveBuiltins {
log.Debug("including builtins: %+v", builtins)
customs = append(customs, builtins...)
}
for _, filename := range customs {
log.Debug("Embed filename: %s", filename)
names[filename] = nil
doodad, err := doodads.LoadFromEmbeddable(filename, lvl, false)
if err != nil {
return fmt.Errorf("couldn't load doodad %s: %s", filename, err)
}
bin, err := doodad.Serialize()
if err != nil {
return fmt.Errorf("couldn't serialize doodad %s: %s", filename, err)
}
// Embed it.
lvl.SetFile(balance.EmbeddedDoodadsBasePath+filename, bin)
}
// Trim any doodads not currently in the level.
for _, filename := range lvl.ListFilesAt(balance.EmbeddedDoodadsBasePath) {
basename := strings.TrimPrefix(filename, balance.EmbeddedDoodadsBasePath)
if _, ok := names[basename]; !ok {
log.Debug("Remove embedded doodad %s (cleanup)", basename)
lvl.DeleteFile(filename)
}
}
return nil
}
// GetUsedDoodadNames returns the lists of doodad filenames in use in a level,
// bucketed by built-in or custom user doodads.
func GetUsedDoodadNames(lvl *level.Level) (builtin []string, custom []string) {
// Collect all the doodad names in use in this level.
unique := map[string]interface{}{}
names := []string{}
if lvl != nil {
for _, actor := range lvl.Actors {
if _, ok := unique[actor.Filename]; ok {
continue
}
unique[actor.Filename] = nil
names = append(names, actor.Filename)
}
}
// Identify which of the doodads are built-ins.
// builtin = []string{}
builtinMap := map[string]interface{}{}
// custom := []string{}
if builtins, err := doodads.ListBuiltin(); err == nil {
for _, filename := range builtins {
if _, ok := unique[filename]; ok {
builtin = append(builtin, filename)
builtinMap[filename] = nil
}
}
}
for _, name := range names {
if _, ok := builtinMap[name]; ok {
continue
}
custom = append(custom, name)
}
sort.Strings(builtin)
sort.Strings(custom)
return builtin, custom
}