WIP Doodle++

pull/93/head
Noah 2024-04-18 20:23:07 -07:00
parent f4ef0f8d8f
commit 7eb7f6148c
33 changed files with 338 additions and 568 deletions

View File

@ -1,12 +1,21 @@
# Building Doodle
* [Automated Release Scripts](#automated-release-scripts)
* [Quickstart with bootstrap.py](#quickstart-with-bootstrap-py)
* [Detailed Instructions](#detailed-instructions)
* [Linux](#linux)
* [Flatpak for Linux](#flatpak-for-linux)
* [Windows Cross-Compile from Linux](#windows-cross-compile-from-linux)
* [Old Docs](#old-docs)
- [Building Doodle](#building-doodle)
- [Dockerfile](#dockerfile)
- [Automated Release Scripts](#automated-release-scripts)
- [Go Environment](#go-environment)
- [Quickstart with bootstrap.py](#quickstart-with-bootstrappy)
- [Detailed Instructions](#detailed-instructions)
- [Fonts](#fonts)
- [Makefile](#makefile)
- [Dependencies](#dependencies)
- [Flatpak for Linux](#flatpak-for-linux)
- [Windows Cross-Compile from Linux](#windows-cross-compile-from-linux)
- [Windows DLLs](#windows-dlls)
- [Build on macOS from scratch](#build-on-macos-from-scratch)
- [WebAssembly](#webassembly)
- [Build Tags](#build-tags)
- [dpp](#dpp)
# Dockerfile
@ -43,6 +52,21 @@ Other Dockerfiles and scripts used to release the game:
The Docker container depends on all the git servers being up, but if you have
the uber blob source code you can read the Dockerfile to see what it does.
# Go Environment
Part of the build scripts involve building and running the `doodad` command
from this repo in order to generate the game's built-in doodads. For this to
work smoothly from your Linux or macOS build environment, you may need to
ensure that your `${GOPATH}/bin` directory is on your `$PATH` by, for example,
configuring this in your bash/zsh profile:
```bash
export GOPATH="${HOME}/go"
export PATH="${PATH}:${GOPATH}/bin"
```
For a complete example, see the "Build on macOS from scratch" section below.
# Quickstart with bootstrap.py
From any Unix-like system (Fedora, Ubuntu, macOS) the bootstrap.py script
@ -278,6 +302,47 @@ cp /usr/x86_64-w64-mingw32/bin/SDL*.dll bin/
SDL2_ttf requires libfreetype, you can get its DLL here:
https://github.com/ubawurinna/freetype-windows-binaries
## Build on macOS from scratch
Here are some detailed instructions how to build Sketchy Maze from a fresh
install of macOS Ventura that assumes no previous software or configuration
was applied to the system yet.
Install homebrew: https://brew.sh pay attention to the instructions at the end
of the install to set up your zsh profile for homebrew to work correctly.
Clone the doodle repository:
```bash
git clone https://git.kirsle.net/SketchyMaze/doodle
cd doodle
```
Note: on a fresh install, invoking the `git` command may cause macOS to install
developer tools and Xcode. After installed, run the git clone again to finish
cloning the repository.
Set your Go environment variables: edit your ~/.zprofile and ensure that $GOPATH
is configured and that your $PATH includes $GOPATH/bin. **Note:** restart your
terminal session or reload the config file (e.g. `. ~/.zprofile`) after making
this change.
```bash
# in your .zprofile, .bash_profile, .zshrc or similar shell config
export GOPATH="${HOME}/go"
export PATH="${PATH}:${GOPATH}/bin"
```
Run the bootstrap script:
```bash
python3 bootstrap.py
```
Answer N (default) when asked to clone dependency repos over ssh. The bootstrap
script will `brew install` any necessary dependencies (Go, SDL2, etc.) and clone
support repos for the game (doodads, levelpacks, assets).
# WebAssembly
There is some **experimental** support for a WebAssembly build of Sketchy Maze
@ -304,33 +369,33 @@ Some tips to get a WASM build to work:
* Run `make wasm` to build the WASM binary and `make wasm-serve` to run a simple
Go web server to serve it from.
# Old Docs
# Build Tags
## Build Tags
Go build tags used by this game:
These aren't really used much anymore but documented here:
## dpp
### shareware
The dpp tag stands for Doodle++ and is used for official commercial builds of
the game. Doodle++ builds include additional code not found in the free & open
source release of the game engine.
> Files ending with `_free.go` are for the shareware release as opposed to
> `_paid.go` for the full version.
This build tag should be set automatically by the Makefile **if** the deps/
folder has a git clone of the dpp project. The bootstrap.py script will clone
the dpp repo **if** you use SSH to clone dependencies: so you will need SSH
credentials to the upstream git server. It basically means that third-party
users who download the open source release will not have the dpp dependency,
and will not build dpp copies of the game.
Builds the game in the free shareware release mode.
If you _do_ have the dpp dependency, you can force build (and run) FOSS
versions of the game via the Makefile commands `make build-free`,
`make run-free` or `make dist-free` which are counterparts to the main make
commands but which deliberately do not set the dpp build tag.
Run `make build-free` to build the shareware binary.
In source code, files ending with `_dpp.go` and `_foss.go` are conditionally
compiled depending on this build tag.
Shareware releases of the game have the following changes compared to the default
(release) mode:
How to tell whether your build of Sketchy Maze is Doodle++ include:
* 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.
* The version string on the title screen.
* FOSS builds (not dpp) will say "open source" in the version.
* DPP builds may say "shareware" if unregistered or just the version.

View File

@ -9,6 +9,12 @@ CURDIR=$(shell curdir)
LDFLAGS := -ldflags "-X main.Build=$(BUILD) -X main.BuildDate=$(BUILD_DATE)"
LDFLAGS_W := -ldflags "-X main.Build=$(BUILD) -X main.BuildDate=$(BUILD_DATE) -H windowsgui"
# Doodle++ build tag for official builds of the game.
BUILD_TAGS := -tags=""
ifneq ("$(wildcard ./deps/dpp)", "")
BUILD_TAGS = -tags="dpp"
endif
# `make setup` to set up a new environment, pull dependencies, etc.
.PHONY: setup
setup: clean
@ -17,8 +23,8 @@ setup: clean
# `make build` to build the binary.
.PHONY: build
build:
go build $(LDFLAGS) -o bin/sketchymaze cmd/doodle/main.go
go build $(LDFLAGS) -o bin/doodad cmd/doodad/main.go
go build $(LDFLAGS) $(BUILD_TAGS) -o bin/sketchymaze cmd/doodle/main.go
go build $(LDFLAGS) $(BUILD_TAGS) -o bin/doodad cmd/doodad/main.go
# `make buildall` to run all build steps including doodads.
.PHONY: buildall
@ -28,15 +34,8 @@ buildall: doodads build
.PHONY: build-free
build-free:
gofmt -w .
go build $(LDFLAGS) -tags="shareware" -o bin/sketchymaze cmd/doodle/main.go
go build $(LDFLAGS) -tags="shareware" -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" -o bin/sketchymaze cmd/doodle/main.go
go build $(LDFLAGS) -tags="developer" -o bin/doodad cmd/doodad/main.go
go build $(LDFLAGS) -o bin/sketchymaze cmd/doodle/main.go
go build $(LDFLAGS) -o bin/doodad cmd/doodad/main.go
# `make bindata` generates the embedded binary assets package.
.PHONY: bindata
@ -74,30 +73,30 @@ doodads:
mingw:
env CGO_ENABLED="1" CC="/usr/bin/x86_64-w64-mingw32-gcc" \
GOOS="windows" CGO_LDFLAGS="-lmingw32 -lSDL2" CGO_CFLAGS="-D_REENTRANT" \
go build $(LDFLAGS_W) -i -o bin/sketchymaze.exe cmd/doodle/main.go
go build $(LDFLAGS_W) $(BUILD_TAGS) -i -o bin/sketchymaze.exe cmd/doodle/main.go
env CGO_ENABLED="1" CC="/usr/bin/x86_64-w64-mingw32-gcc" \
GOOS="windows" CGO_LDFLAGS="-lmingw32 -lSDL2" CGO_CFLAGS="-D_REENTRANT" \
go build $(LDFLAGS) -i -o bin/doodad.exe cmd/doodad/main.go
go build $(LDFLAGS) $(BUILD_TAGS) -i -o bin/doodad.exe cmd/doodad/main.go
# `make mingw32` to cross-compile a Windows binary with mingw (32-bit).
.PHONY: mingw32
mingw32:
env CGO_ENABLED="1" CC="/usr/bin/i686-w64-mingw32-gcc" \
GOOS="windows" CGO_LDFLAGS="-lmingw32 -lSDL2" CGO_CFLAGS="-D_REENTRANT" \
go build $(LDFLAGS_W) -i -o bin/sketchymaze.exe cmd/doodle/main.go
go build $(LDFLAGS_W) $(BUILD_TAGS) -i -o bin/sketchymaze.exe cmd/doodle/main.go
env CGO_ENABLED="1" CC="/usr/bin/i686-w64-mingw32-gcc" \
GOOS="windows" CGO_LDFLAGS="-lmingw32 -lSDL2" CGO_CFLAGS="-D_REENTRANT" \
go build $(LDFLAGS) -i -o bin/doodad.exe cmd/doodad/main.go
go build $(LDFLAGS) $(BUILD_TAGS) -i -o bin/doodad.exe cmd/doodad/main.go
# `make mingw-free` for Windows binary in free mode.
.PHONY: mingw-free
mingw-free:
env CGO_ENABLED="1" CC="/usr/bin/x86_64-w64-mingw32-gcc" \
GOOS="windows" CGO_LDFLAGS="-lmingw32 -lSDL2" CGO_CFLAGS="-D_REENTRANT" \
go build $(LDFLAGS_W) -tags="shareware" -i -o bin/sketchymaze.exe cmd/doodle/main.go
go build $(LDFLAGS_W) -i -o bin/sketchymaze.exe cmd/doodle/main.go
env CGO_ENABLED="1" CC="/usr/bin/x86_64-w64-mingw32-gcc" \
GOOS="windows" CGO_LDFLAGS="-lmingw32 -lSDL2" CGO_CFLAGS="-D_REENTRANT" \
go build $(LDFLAGS) -tags="shareware" -i -o bin/doodad.exe cmd/doodad/main.go
go build $(LDFLAGS) -i -o bin/doodad.exe cmd/doodad/main.go
# `make release` runs the release.sh script, must be run
# after `make dist`
@ -145,12 +144,17 @@ from-docker32: build mingw32 __dist-common
# `make run` to run it from source.
.PHONY: run
run:
go run ${BUILD_TAGS} cmd/doodle/main.go
# `make run-free` to run it from source with no build tags (foss version).
.PHONY: run-free
run-free:
go run cmd/doodle/main.go
# `make debug` to run it in -debug mode.
.PHONY: debug
debug:
go run cmd/doodle/main.go -debug
go run $(BUILD_TAGS) cmd/doodle/main.go -debug
# `make guitest` to run it in guitest mode.
.PHONY: guitest

View File

@ -37,6 +37,10 @@ repos_github = {
"git@github.com:kirsle/audio": "audio",
# TODO: the rest
}
repos_ssh = {
# SSH-only (private) repos.
"git@git.kirsle.net:SketchyMaze/dpp": "dpp",
}
# Software dependencies.
dep_fedora = ["make", "golang", "SDL2-devel", "SDL2_ttf-devel", "SDL2_mixer-devel", "zip", "rsync"]
@ -167,4 +171,8 @@ if __name__ == "__main__":
https = k.replace("git@git.kirsle.net:", "https://git.kirsle.net/")
repos[https] = repos[k]
del repos[k]
else:
# mix in SSH-only repos
repos.update(repos_ssh)
main()

View File

@ -8,8 +8,7 @@ import (
"time"
"git.kirsle.net/SketchyMaze/doodle/cmd/doodad/commands"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding/builds"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"github.com/urfave/cli/v2"
)
@ -31,15 +30,9 @@ func main() {
app.Name = "doodad"
app.Usage = "command line interface for Doodle"
var freeLabel string
if !license.IsRegistered() {
freeLabel = " (shareware)"
}
app.Version = fmt.Sprintf("%s build %s%s. Built on %s",
branding.Version,
app.Version = fmt.Sprintf("%s build %s. Built on %s",
builds.Version,
Build,
freeLabel,
BuildDate,
)

View File

@ -15,9 +15,9 @@ import (
doodle "git.kirsle.net/SketchyMaze/doodle/pkg"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding/builds"
"git.kirsle.net/SketchyMaze/doodle/pkg/chatbot"
"git.kirsle.net/SketchyMaze/doodle/pkg/gamepad"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
@ -55,11 +55,6 @@ func main() {
app.Name = "doodle"
app.Usage = fmt.Sprintf("%s - %s", branding.AppName, branding.Summary)
var freeLabel string
if !license.IsRegistered() {
freeLabel = " (shareware)"
}
// Load user settings from disk ASAP.
if err := usercfg.Load(); err != nil {
log.Error("Error loading user settings (defaults will be used): %s", err)
@ -73,10 +68,9 @@ func main() {
// Set GameController style.
gamepad.SetStyle(gamepad.Style(usercfg.Current.ControllerStyle))
app.Version = fmt.Sprintf("%s build %s%s. Built on %s",
branding.Version,
app.Version = fmt.Sprintf("%s build %s. Built on %s",
builds.Version,
Build,
freeLabel,
BuildDate,
)
@ -119,6 +113,8 @@ func main() {
}
app.Action = func(c *cli.Context) error {
log.Info("Starting %s %s", app.Name, app.Version)
// --chdir into a different working directory? e.g. for Flatpak especially.
if doodlePath := c.String("chdir"); doodlePath != "" {
if err := os.Chdir(doodlePath); err != nil {

View File

@ -1,7 +0,0 @@
//go:build shareware
// +build shareware
package balance
// FreeVersion is true in the free version of the game.
const FreeVersion = true

View File

@ -1,7 +0,0 @@
//go:build !shareware
// +build !shareware
package balance
// FreeVersion is true in the free version of the game.
const FreeVersion = false

7
pkg/balance/tag_dpp.go Normal file
View File

@ -0,0 +1,7 @@
//go:build dpp
// +build dpp
package balance
// Doodle++ tag compiled in.
const DPP = true

7
pkg/balance/tag_foss.go Normal file
View File

@ -0,0 +1,7 @@
//go:build !dpp
// +build !dpp
package balance
// Doodle++ tag compiled in.
const DPP = false

View File

@ -0,0 +1,36 @@
// Package builds handles build-specific branding strings.
package builds
import (
"fmt"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
)
var (
/*
Version string for user display.
It may look like the following:
- "v1.2.3 (open source)" for FOSS builds of the game.
- "v1.2.3 (shareware)" for unregistered Doodle++ builds.
- "v1.2.3" for registered Doodle++ builds.
*/
Version = branding.Version
VersionSuffix = " (unknown)"
)
func init() {
if !balance.DPP {
VersionSuffix = " (open source)"
} else if !license.IsRegistered() {
VersionSuffix = " (shareware)"
} else {
VersionSuffix = ""
}
Version = fmt.Sprintf("v%s%s", branding.Version, VersionSuffix)
}

View File

@ -9,11 +9,9 @@ import (
"strings"
"git.kirsle.net/SketchyMaze/doodle/assets"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
"git.kirsle.net/SketchyMaze/doodle/pkg/wasm"
@ -114,27 +112,6 @@ func ListBuiltin() ([]string, error) {
return result, nil
}
/*
LoadFromEmbeddable reads a doodad file, checking a level's embeddable
file data in addition to the usual places.
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 {
log.Debug("doodads.LoadFromEmbeddable: found %s", filename)
if !force && !license.IsRegistered() {
return nil, license.ErrRegisteredFeature
}
return Deserialize(filename, bin)
}
return LoadFile(filename)
}
// LoadFile reads a doodad file from disk, checking a few locations.
//
// It checks for embedded bindata, system-level doodads on the filesystem,

View File

@ -6,11 +6,11 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding/builds"
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/drawtool"
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
"git.kirsle.net/SketchyMaze/doodle/pkg/uix"
@ -595,12 +595,8 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
}
}
var shareware string
if !license.IsRegistered() {
shareware = " (shareware)"
}
extraLabel := ui.NewLabel(ui.Label{
Text: fmt.Sprintf("%s v%s%s", branding.AppName, branding.Version, shareware),
Text: fmt.Sprintf("%s %s", branding.AppName, builds.Version),
Font: balance.StatusFont,
})
extraLabel.Configure(ui.Config{

View File

@ -9,6 +9,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/plus"
"git.kirsle.net/SketchyMaze/doodle/pkg/uix"
"git.kirsle.net/go/render"
)
@ -34,7 +35,7 @@ func (u *EditorUI) startDragActor(doodad *doodads.Doodad, actor *level.Actor) {
if doodad == nil {
if actor != nil {
obj, err := doodads.LoadFromEmbeddable(actor.Filename, u.Scene.Level, false)
obj, err := plus.DoodadFromEmbeddable(actor.Filename, u.Scene.Level, false)
if err != nil {
log.Error("startDragExistingActor: actor doodad name %s not found: %s", actor.Filename, err)
return

View File

@ -276,17 +276,22 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
////////
// Help menu
var (
helpMenu = u.d.MakeHelpMenu(menu, u.Supervisor)
registerText = "Register"
helpMenu = u.d.MakeHelpMenu(menu, u.Supervisor)
)
helpMenu.AddSeparator()
if license.IsRegistered() {
registerText = "Registration"
// Registration item for Doodle++ builds.
if balance.DPP {
var registerText = "Register"
if license.IsRegistered() {
registerText = "Registration"
}
helpMenu.AddSeparator()
helpMenu.AddItem(registerText, func() {
u.licenseWindow.Show()
u.Supervisor.FocusWindow(u.licenseWindow)
})
}
helpMenu.AddItem(registerText, func() {
u.licenseWindow.Show()
u.Supervisor.FocusWindow(u.licenseWindow)
})
menu.Supervise(u.Supervisor)
menu.Compute(d.Engine)

View File

@ -9,9 +9,9 @@ import (
"path/filepath"
"time"
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/plus"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
"git.kirsle.net/SketchyMaze/doodle/pkg/wallpaper"
@ -95,7 +95,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) {
// Render the doodads.
log.Debug("GiantScreenshot: Render actors...")
for _, actor := range lvl.Actors {
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl, false)
doodad, err := plus.DoodadFromEmbeddable(actor.Filename, lvl, false)
if err != nil {
log.Error("GiantScreenshot: Load doodad: %s", err)
continue

View File

@ -12,9 +12,9 @@ import (
"time"
"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/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/plus"
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
"git.kirsle.net/go/render"
"golang.org/x/image/draw"
@ -78,7 +78,7 @@ func CroppedScreenshot(lvl *level.Level, viewport render.Rect) (image.Image, err
// Render the doodads.
log.Debug("CroppedScreenshot: Render actors...")
for _, actor := range lvl.Actors {
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl, false)
doodad, err := plus.DoodadFromEmbeddable(actor.Filename, lvl, false)
if err != nil {
log.Error("CroppedScreenshot: Load doodad: %s", err)
continue

View File

@ -19,6 +19,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/plus"
)
/*
@ -37,7 +38,7 @@ func Publish(lvl *level.Level) error {
}
// Registered games only.
if !license.IsRegistered() {
if !balance.DPP || !license.IsRegistered() {
return errors.New("only registered versions of the game can attach doodads to levels")
}
@ -52,7 +53,7 @@ func Publish(lvl *level.Level) error {
log.Debug("Embed filename: %s", filename)
names[filename] = nil
doodad, err := doodads.LoadFromEmbeddable(filename, lvl, false)
doodad, err := plus.DoodadFromEmbeddable(filename, lvl, false)
if err != nil {
return fmt.Errorf("couldn't load doodad %s: %s", filename, err)
}

View File

@ -1,100 +0,0 @@
package license
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"time"
"github.com/dgrijalva/jwt-go"
)
// AdminGenerateKeys generates the ECDSA public and private key pair for the admin
// side of creating signed license files.
func AdminGenerateKeys() (*ecdsa.PrivateKey, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
return privateKey, err
}
// AdminWriteKeys writes the admin signing key to .pem files on disk.
func AdminWriteKeys(key *ecdsa.PrivateKey, privateFile, publicFile string) error {
// Encode the private key to PEM format.
x509Encoded, err := x509.MarshalECPrivateKey(key)
if err != nil {
return err
}
pemEncoded := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: x509Encoded,
})
// Encode the public key to PEM format.
x509EncodedPub, err := x509.MarshalPKIXPublicKey(key.Public())
if err != nil {
return err
}
pemEncodedPub := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: x509EncodedPub,
})
// Write the files.
if err := ioutil.WriteFile(privateFile, pemEncoded, 0600); err != nil {
return err
}
if err := ioutil.WriteFile(publicFile, pemEncodedPub, 0644); err != nil {
return err
}
return nil
}
// AdminLoadPrivateKey loads the private key from disk.
func AdminLoadPrivateKey(privateFile string) (*ecdsa.PrivateKey, error) {
// Read the private key file.
pemEncoded, err := ioutil.ReadFile(privateFile)
if err != nil {
return nil, err
}
// Decode the private key.
block, _ := pem.Decode([]byte(pemEncoded))
x509Encoded := block.Bytes
privateKey, _ := x509.ParseECPrivateKey(x509Encoded)
return privateKey, nil
}
// AdminLoadPublicKey loads the private key from disk.
func AdminLoadPublicKey(publicFile string) (*ecdsa.PublicKey, error) {
pemEncodedPub, err := ioutil.ReadFile(publicFile)
if err != nil {
return nil, err
}
// Decode the public key.
blockPub, _ := pem.Decode([]byte(pemEncodedPub))
x509EncodedPub := blockPub.Bytes
genericPublicKey, _ := x509.ParsePKIXPublicKey(x509EncodedPub)
publicKey := genericPublicKey.(*ecdsa.PublicKey)
return publicKey, nil
}
// AdminSignRegistration signs the registration object.
func AdminSignRegistration(key *ecdsa.PrivateKey, reg Registration) (string, error) {
reg.StandardClaims = jwt.StandardClaims{
Issuer: "Maze Admin",
IssuedAt: time.Now().Unix(),
NotBefore: time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodES384, reg)
signed, err := token.SignedString(key)
if err != nil {
return "", err
}
return signed, nil
}

View File

@ -1,25 +0,0 @@
package license
import (
"crypto/ecdsa"
"fmt"
)
// Run-time configuration variables provided by the application.
const pemSigningKey = `-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMrqAMHjZ1dPlKwDOsiCSr5N3OSvnYKLM
efe2xD+5hJYrpvparRFnaMbMuqde4M6d6sCCKO8BHtfAzmyiQ/CD38zs9MiDsamy
FDYEEJu+Fqx482I7fIa5ZEE770+wWJ3k
-----END PUBLIC KEY-----`
var Signer *ecdsa.PublicKey
func init() {
key, err := ParsePublicKeyPEM(pemSigningKey)
if err != nil {
fmt.Printf("license: failed to parse app keys: %s\n", err)
return
}
Signer = key
}

View File

@ -1,193 +0,0 @@
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)
}

View File

@ -1,108 +0,0 @@
// Package license holds functions related to paid product activation.
package license
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"path/filepath"
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
"github.com/dgrijalva/jwt-go"
)
// Errors
var (
ErrRegisteredFeature = errors.New("feature not available")
)
// Registration object encoded into a license key file.
type Registration struct {
Name string `json:"name"`
Email string `json:"email"`
jwt.StandardClaims
}
// IsRegistered returns a boolean answer: is the product registered?
func IsRegistered() bool {
if _, err := GetRegistration(); err == nil {
return true
}
return false
}
// GetRegistration returns the currently registered user, by checking
// for the license.key file in the profile folder.
func GetRegistration() (Registration, error) {
if Signer == nil {
return Registration{}, errors.New("signer not ready")
}
filename := filepath.Join(userdir.ProfileDirectory, "license.key")
jwt, err := ioutil.ReadFile(filename)
if err != nil {
return Registration{}, err
}
// Check if the JWT is valid.
reg, err := Validate(Signer, string(jwt))
if err != nil {
return Registration{}, err
}
return reg, err
}
// UploadLicenseFile handles the user selecting the license key file, and it is
// validated and ingested.
func UploadLicenseFile(filename string) (Registration, error) {
if Signer == nil {
return Registration{}, errors.New("signer not ready")
}
jwt, err := ioutil.ReadFile(filename)
if err != nil {
return Registration{}, err
}
// Check if the JWT is valid.
reg, err := Validate(Signer, string(jwt))
if err != nil {
return Registration{}, err
}
// Upload the license to Doodle's profile directory.
outfile := filepath.Join(userdir.ProfileDirectory, "license.key")
if err := ioutil.WriteFile(outfile, jwt, 0644); err != nil {
return Registration{}, err
}
return reg, nil
}
// Validate the registration is signed by the appropriate public key.
func Validate(publicKey *ecdsa.PublicKey, tokenString string) (Registration, error) {
var reg Registration
token, err := jwt.ParseWithClaims(tokenString, &reg, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
return reg, err
}
if !token.Valid {
return reg, errors.New("token not valid")
}
return reg, nil
}
// ParsePublicKeyPEM loads a public key from PEM format.
func ParsePublicKeyPEM(keytext string) (*ecdsa.PublicKey, error) {
blockPub, _ := pem.Decode([]byte(keytext))
x509EncodedPub := blockPub.Bytes
genericPublicKey, _ := x509.ParsePKIXPublicKey(x509EncodedPub)
publicKey := genericPublicKey.(*ecdsa.PublicKey)
return publicKey, nil
}

View File

@ -6,6 +6,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding/builds"
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
@ -120,12 +121,8 @@ func (s *MainScene) Setup(d *Doodle) error {
s.labelSubtitle.Compute(d.Engine)
// Version label.
var shareware string
if !license.IsRegistered() {
shareware = " (shareware)"
}
ver := ui.NewLabel(ui.Label{
Text: fmt.Sprintf("v%s%s", branding.Version, shareware),
Text: builds.Version,
Font: balance.TitleScreenVersionFont,
})
ver.Compute(d.Engine)
@ -228,7 +225,7 @@ func (s *MainScene) Setup(d *Doodle) error {
{
Name: "Register",
If: func() bool {
return !license.IsRegistered()
return balance.DPP && !license.IsRegistered()
},
Func: func() {
if s.winRegister == nil {

View File

@ -3,7 +3,7 @@ package native
import (
"os"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/plus"
)
var USER string = os.Getenv("USER")
@ -18,8 +18,8 @@ Otherwise fall back to their native operating system user.
*/
func DefaultAuthor() string {
// Are we registered?
if license.IsRegistered() {
if reg, err := license.GetRegistration(); err == nil {
if plus.IsRegistered() {
if reg, err := plus.GetRegistration(); err == nil {
return reg.Name
}
}

View File

@ -19,6 +19,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen"
"git.kirsle.net/SketchyMaze/doodle/pkg/physics"
"git.kirsle.net/SketchyMaze/doodle/pkg/plus"
"git.kirsle.net/SketchyMaze/doodle/pkg/savegame"
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
@ -503,7 +504,7 @@ func (s *PlayScene) setupPlayer(playerCharacterFilename string) {
// centerIn is optional, ignored if zero.
func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, centerIn render.Rect) {
// Load in the player character.
player, err := doodads.LoadFromEmbeddable(filename, s.Level, false)
player, err := plus.DoodadFromEmbeddable(filename, s.Level, false)
if err != nil {
log.Error("PlayScene.Setup: failed to load player doodad: %s", err)
player = doodads.NewDummy(32)

24
pkg/plus/dpp/plus_dpp.go Normal file
View File

@ -0,0 +1,24 @@
//go:build dpp
// +build dpp
package plus
import (
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
"git.kirsle.net/SketchyMaze/dpp/embedding"
"git.kirsle.net/SketchyMaze/dpp/license"
)
func DoodadFromEmbeddable(filename string, fs filesystem.Embeddable, force bool) (*doodads.Doodad, error) {
return embedding.LoadFromEmbeddable(filename, fs, force)
}
func IsRegistered() bool {
return license.IsRegistered()
}
func GetRegistration() (*Registration, error) {
reg, err := license.GetRegistration()
return reg.(*Registration), err
}

View File

@ -0,0 +1,22 @@
//go:build !dpp
// +build !dpp
package plus
import (
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
)
// DoodadFromEmbeddable may load a doodad from an embedding filesystem, such as a Level or LevelPack.
func DoodadFromEmbeddable(filename string, fs filesystem.Embeddable, force bool) (*doodads.Doodad, error) {
return doodads.LoadFile(filename)
}
func IsRegistered() bool {
return false
}
func GetRegistration() (*Registration, error) {
return nil, ErrNotImplemented
}

View File

@ -0,0 +1,24 @@
//go:build dpp
// +build dpp
package plus
import (
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
"git.kirsle.net/SketchyMaze/dpp/embedding"
"git.kirsle.net/SketchyMaze/dpp/license"
)
func DoodadFromEmbeddable(filename string, fs filesystem.Embeddable, force bool) (*doodads.Doodad, error) {
return embedding.LoadFromEmbeddable(filename, fs, force)
}
func IsRegistered() bool {
return license.IsRegistered()
}
func GetRegistration() (*Registration, error) {
reg, err := license.GetRegistration()
return reg.(*Registration), err
}

View File

@ -0,0 +1,22 @@
//go:build !dpp
// +build !dpp
package plus
import (
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
)
// DoodadFromEmbeddable may load a doodad from an embedding filesystem, such as a Level or LevelPack.
func DoodadFromEmbeddable(filename string, fs filesystem.Embeddable, force bool) (*doodads.Doodad, error) {
return doodads.LoadFile(filename)
}
func IsRegistered() bool {
return false
}
func GetRegistration() (*Registration, error) {
return nil, ErrNotImplemented
}

View File

@ -0,0 +1 @@
package initdpp

23
pkg/plus/plus.go Normal file
View File

@ -0,0 +1,23 @@
// Package plus connects the open source Doodle engine to the Doodle++ feature.
package plus
import (
"errors"
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
"github.com/dgrijalva/jwt-go"
)
var ErrNotImplemented = errors.New("not implemented")
type Bridge interface {
DoodadFromEmbeddable(filename string, fs filesystem.Embeddable, force bool) (*doodads.Doodad, error)
}
// Registration object encoded into a license key file.
type Registration struct {
Name string `json:"name"`
Email string `json:"email"`
jwt.StandardClaims
}

View File

@ -6,10 +6,10 @@ import (
"sort"
"strings"
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"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/plus"
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting"
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
"git.kirsle.net/go/render"
@ -44,11 +44,11 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
var actor = actors[id]
// Try loading the doodad from the level's own attached files.
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, w.level, isSigned)
doodad, err := plus.DoodadFromEmbeddable(actor.Filename, w.level, isSigned)
if err != nil {
// If we have a signed levelpack, try loading from the levelpack.
if w.IsSignedLevelPack != nil {
if found, err := doodads.LoadFromEmbeddable(actor.Filename, w.IsSignedLevelPack, true); err == nil {
if found, err := plus.DoodadFromEmbeddable(actor.Filename, w.IsSignedLevelPack, true); err == nil {
doodad = found
}
}

View File

@ -6,6 +6,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding"
"git.kirsle.net/SketchyMaze/doodle/pkg/branding/builds"
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
@ -51,7 +52,7 @@ func NewLicenseWindow(cfg License) *ui.Window {
valueSize = render.NewRect(windowWidth-labelSize.W-4, labelSize.H)
isRegistered bool
registration license.Registration
summary = "Unregistered (shareware)"
summary = "Unregistered" + builds.VersionSuffix
)
// Get our current registration status.

View File

@ -171,9 +171,8 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window {
* Frame for selecting User Doodads
******************/
// Doodads not shown if we're loading a map to play, nor are they
// available to the free version.
if !config.LoadForPlay && !balance.FreeVersion {
// Doodads not shown if we're loading a map to play.
if !config.LoadForPlay {
label2 := ui.NewLabel(ui.Label{
Text: "Doodads",
Font: balance.LabelFont,