From 1e80304061af25c27add289799203a57c1e336b2 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 15 Apr 2019 23:07:15 -0700 Subject: [PATCH] Initial Doodad JavaScript System * Add the JavaScript system for Doodads to run their scripts in levels, and wire initial OnCollide() handler support. * CLI: Add a `doodad install-script` command to the doodad tool. * Usage: `doodad install-script ` * Add dev-assets folder for storing source files for the official default doodads, sprites, levels, etc. and for now add a JavaScript for the first test doodad. --- cmd/doodad/commands/install_script.go | 61 +++++++++++++++++++++ cmd/doodad/main.go | 1 + dev-assets/doodads/test/index.js | 13 +++++ pkg/balance/debug.go | 2 +- pkg/doodle.go | 4 +- pkg/log/log.go | 2 +- pkg/play_scene.go | 19 ++++++- pkg/scripting/events.go | 53 ++++++++++++++++++ pkg/scripting/scripting.go | 62 +++++++++++++++++++++ pkg/scripting/vm.go | 79 +++++++++++++++++++++++++++ pkg/uix/canvas.go | 12 ++++ pkg/uix/canvas_actors.go | 35 ++++++++++++ 12 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 cmd/doodad/commands/install_script.go create mode 100644 dev-assets/doodads/test/index.js create mode 100644 pkg/scripting/events.go create mode 100644 pkg/scripting/scripting.go create mode 100644 pkg/scripting/vm.go diff --git a/cmd/doodad/commands/install_script.go b/cmd/doodad/commands/install_script.go new file mode 100644 index 0000000..172215c --- /dev/null +++ b/cmd/doodad/commands/install_script.go @@ -0,0 +1,61 @@ +package commands + +import ( + "fmt" + "io/ioutil" + + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/log" + "github.com/urfave/cli" +) + +// InstallScript to add the script to a doodad file. +var InstallScript cli.Command + +func init() { + InstallScript = cli.Command{ + Name: "install-script", + Usage: "install the JavaScript source to a doodad", + ArgsUsage: " ", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "key", + Usage: "chroma key color for transparency on input image files", + Value: "#ffffff", + }, + }, + Action: func(c *cli.Context) error { + if c.NArg() != 2 { + return cli.NewExitError( + "Usage: doodad install-script ", + 1, + ) + } + + var ( + args = c.Args() + scriptFile = args[0] + doodadFile = args[1] + ) + + // Read the JavaScript source. + javascript, err := ioutil.ReadFile(scriptFile) + if err != nil { + return cli.NewExitError(err.Error(), 1) + } + + doodad, err := doodads.LoadJSON(doodadFile) + if err != nil { + return cli.NewExitError( + fmt.Sprintf("Failed to read doodad file: %s", err), + 1, + ) + } + doodad.Script = string(javascript) + doodad.WriteJSON(doodadFile) + log.Info("Installed script successfully") + + return nil + }, + } +} diff --git a/cmd/doodad/main.go b/cmd/doodad/main.go index 5c60d7a..4c6ab2b 100644 --- a/cmd/doodad/main.go +++ b/cmd/doodad/main.go @@ -44,6 +44,7 @@ func main() { app.Commands = []cli.Command{ commands.Convert, + commands.InstallScript, } sort.Sort(cli.FlagsByName(app.Flags)) diff --git a/dev-assets/doodads/test/index.js b/dev-assets/doodads/test/index.js new file mode 100644 index 0000000..07ee2f7 --- /dev/null +++ b/dev-assets/doodads/test/index.js @@ -0,0 +1,13 @@ +// Test Doodad Script +function main() { + console.log("I am actor ID " + Self.ID()); + + // Set our doodad's background color to pink. It will be turned + // red whenever something collides with us. + Self.Canvas.SetBackground(RGBA(255, 153, 255, 153)); + + Events.OnCollide( function(e) { + console.log("Collided with something!"); + Self.Canvas.SetBackground(RGBA(255, 0, 0, 153)); + }); +} diff --git a/pkg/balance/debug.go b/pkg/balance/debug.go index aa7c110..d9a20b2 100644 --- a/pkg/balance/debug.go +++ b/pkg/balance/debug.go @@ -29,7 +29,7 @@ var ( // Put a border around all Canvas widgets. DebugCanvasBorder = render.Invisible - DebugCanvasLabel = true // Tag the canvas with a label. + DebugCanvasLabel = false // Tag the canvas with a label. ) func init() { diff --git a/pkg/doodle.go b/pkg/doodle.go index 2475a57..b99b9fa 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -58,8 +58,8 @@ func New(debug bool, engine render.Engine) *Doodle { } d.shell = NewShell(d) - if !debug { - log.Logger.Config.Level = golog.InfoLevel + if debug { + log.Logger.Config.Level = golog.DebugLevel } return d diff --git a/pkg/log/log.go b/pkg/log/log.go index 06f820f..7dabb01 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -8,7 +8,7 @@ var Logger *golog.Logger func init() { Logger = golog.GetLogger("doodle") Logger.Configure(&golog.Config{ - Level: golog.DebugLevel, + Level: golog.InfoLevel, Theme: golog.DarkTheme, Colors: golog.ExtendedColor, TimeFormat: "2006-01-02 15:04:05.000000", diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 8610532..9ae4e19 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -9,6 +9,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/doodads/dummy" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/uix" ) @@ -19,8 +20,9 @@ type PlayScene struct { Level *level.Level // Private variables. - d *Doodle - drawing *uix.Canvas + d *Doodle + drawing *uix.Canvas + scripting *scripting.Supervisor // Custom debug labels. debPosition *string @@ -40,6 +42,7 @@ func (s *PlayScene) Name() string { // Setup the play scene. func (s *PlayScene) Setup(d *Doodle) error { s.d = d + s.scripting = scripting.NewSupervisor() // Initialize debug overlay values. s.debPosition = new(string) @@ -67,6 +70,7 @@ func (s *PlayScene) Setup(d *Doodle) error { s.drawing.InstallActors(s.Level.Actors) } else if s.Filename != "" { log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) + // NOTE: s.LoadLevel also calls s.drawing.InstallActors s.LoadLevel(s.Filename) } @@ -77,6 +81,15 @@ func (s *PlayScene) Setup(d *Doodle) error { s.drawing.InstallActors(s.Level.Actors) } + // Load all actor scripts. + s.drawing.SetScriptSupervisor(s.scripting) + if err := s.scripting.InstallScripts(s.Level); err != nil { + log.Error("PlayScene.Setup: failed to InstallScripts: %s", err) + } + if err := s.drawing.InstallScripts(); err != nil { + log.Error("PlayScene.Setup: failed to drawing.InstallScripts: %s", err) + } + player := dummy.NewPlayer() s.Player = uix.NewActor(player.ID(), &level.Actor{}, player.Doodad) s.Player.MoveTo(render.NewPoint(128, 128)) @@ -196,7 +209,7 @@ func (s *PlayScene) LoadLevel(filename string) error { s.Level = level s.drawing.LoadLevel(s.d.Engine, s.Level) - s.drawing.InstallActors(s.Level.Actors) + // s.drawing.InstallActors(s.Level.Actors) return nil } diff --git a/pkg/scripting/events.go b/pkg/scripting/events.go new file mode 100644 index 0000000..37aef9a --- /dev/null +++ b/pkg/scripting/events.go @@ -0,0 +1,53 @@ +package scripting + +import ( + "github.com/robertkrimen/otto" +) + +// Events API for Doodad scripts. +type Events struct { + registry map[string][]otto.Value +} + +// NewEvents initializes the Events API. +func NewEvents() *Events { + return &Events{ + registry: map[string][]otto.Value{}, + } +} + +// OnCollide fires when another actor collides with yours. +func (e *Events) OnCollide(call otto.FunctionCall) otto.Value { + callback := call.Argument(0) + if !callback.IsFunction() { + return otto.Value{} // TODO + } + + if _, ok := e.registry[CollideEvent]; !ok { + e.registry[CollideEvent] = []otto.Value{} + } + + e.registry[CollideEvent] = append(e.registry[CollideEvent], callback) + return otto.Value{} +} + +// RunCollide invokes the OnCollide handler function. +func (e *Events) RunCollide() error { + if _, ok := e.registry[CollideEvent]; !ok { + return nil + } + + for _, callback := range e.registry[CollideEvent] { + _, err := callback.Call(otto.Value{}, "test argument") + if err != nil { + return err + } + } + + return nil +} + +// Event name constants. +const ( + CollideEvent = "collide" +) diff --git a/pkg/scripting/scripting.go b/pkg/scripting/scripting.go new file mode 100644 index 0000000..0252edb --- /dev/null +++ b/pkg/scripting/scripting.go @@ -0,0 +1,62 @@ +// Package scripting manages the JavaScript VMs for Doodad +// scripts. +package scripting + +import ( + "errors" + "fmt" + + "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" +) + +// Supervisor manages the JavaScript VMs for each doodad by its +// unique ID. +type Supervisor struct { + scripts map[string]*VM +} + +// NewSupervisor creates a new JavaScript Supervior. +func NewSupervisor() *Supervisor { + return &Supervisor{ + scripts: map[string]*VM{}, + } +} + +// InstallScripts loads scripts for all actors in the level. +func (s *Supervisor) InstallScripts(level *level.Level) error { + for _, actor := range level.Actors { + id := actor.ID() + log.Debug("InstallScripts: load script from Actor %s", id) + + if _, ok := s.scripts[id]; ok { + return fmt.Errorf("duplicate actor ID %s in level", id) + } + + s.scripts[id] = NewVM(id) + if err := s.scripts[id].RegisterLevelHooks(); err != nil { + return err + } + } + return nil +} + +// To returns the VM for a named script. +func (s *Supervisor) To(name string) *VM { + if vm, ok := s.scripts[name]; ok { + return vm + } + + log.Error("scripting.Supervisor.To(%s): no such VM but returning blank VM", + name, + ) + return NewVM(name) +} + +// GetVM returns a script VM from the supervisor. +func (s *Supervisor) GetVM(name string) (*VM, error) { + if vm, ok := s.scripts[name]; ok { + return vm, nil + } + return nil, errors.New("not found") +} diff --git a/pkg/scripting/vm.go b/pkg/scripting/vm.go new file mode 100644 index 0000000..c5304bf --- /dev/null +++ b/pkg/scripting/vm.go @@ -0,0 +1,79 @@ +package scripting + +import ( + "errors" + "fmt" + + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/log" + "github.com/robertkrimen/otto" +) + +// VM manages a single isolated JavaScript VM. +type VM struct { + Name string + + // Globals available to the scripts. + Events *Events + Self interface{} + + vm *otto.Otto +} + +// NewVM creates a new JavaScript VM. +func NewVM(name string) *VM { + vm := &VM{ + Name: name, + Events: NewEvents(), + vm: otto.New(), + } + return vm +} + +// Run code in the VM. +func (vm *VM) Run(src interface{}) (otto.Value, error) { + v, err := vm.vm.Run(src) + return v, err +} + +// Set a value in the VM. +func (vm *VM) Set(name string, v interface{}) error { + return vm.vm.Set(name, v) +} + +// RegisterLevelHooks registers accessors to the level hooks +// and Doodad API for Play Mode. +func (vm *VM) RegisterLevelHooks() error { + bindings := map[string]interface{}{ + "log": log.Logger, + "RGBA": render.RGBA, + "Point": render.NewPoint, + "Self": vm.Self, + "Events": vm.Events, + } + for name, v := range bindings { + err := vm.vm.Set(name, v) + if err != nil { + return fmt.Errorf("RegisterLevelHooks(%s): %s", + name, err, + ) + } + } + vm.vm.Run(`console = {}; console.log = log.Info;`) + return nil +} + +// Main calls the main function of the script. +func (vm *VM) Main() error { + function, err := vm.vm.Get("main") + if err != nil { + return err + } + + if !function.IsFunction() { + return errors.New("main is not a function") + } + + _, err = function.Call(otto.Value{}) + return err +} diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index ef070c6..7b2c369 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -13,6 +13,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/wallpaper" ) @@ -49,6 +50,10 @@ type Canvas struct { actor *Actor // if this canvas IS an actor actors []*Actor // if this canvas CONTAINS actors (i.e., is a level) + // Doodad scripting engine supervisor. + // NOTE: initialized and managed by the play_scene. + scripting *scripting.Supervisor + // Wallpaper settings. wallpaper *Wallpaper @@ -210,6 +215,13 @@ func (w *Canvas) Loop(ev *events.State) error { w.actors[tuple[0]].ID(), w.actors[tuple[1]].ID(), ) + a, b := w.actors[tuple[0]], w.actors[tuple[1]] + + // Call the OnCollide handler. + if w.scripting != nil { + w.scripting.To(a.ID()).Events.RunCollide() + w.scripting.To(b.ID()).Events.RunCollide() + } } // If the canvas is editable, only care if it's over our space. diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index ea1b267..3e2ee90 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -1,12 +1,14 @@ package uix import ( + "errors" "fmt" "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/userdir" ) @@ -30,6 +32,39 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error { return nil } +// SetScriptSupervisor assigns the Canvas scripting supervisor to enable +// interaction with actor scripts. +func (w *Canvas) SetScriptSupervisor(s *scripting.Supervisor) { + w.scripting = s +} + +// InstallScripts loads all the current actors' scripts into the scripting +// engine supervisor. +func (w *Canvas) InstallScripts() error { + if w.scripting == nil { + return errors.New("no script supervisor is configured for this canvas") + } + + if len(w.actors) == 0 { + return errors.New("no actors exist in this canvas to install scripts for") + } + + for _, actor := range w.actors { + vm := w.scripting.To(actor.ID()) + vm.Self = actor + vm.Set("Self", vm.Self) + vm.Run(actor.Drawing.Doodad.Script) + + // Call the main() function. + log.Error("Calling Main() for %s", actor.ID()) + if err := vm.Main(); err != nil { + log.Error("main() for actor %s errored: %s", actor.ID(), err) + } + } + + return nil +} + // AddActor injects additional actors into the canvas, such as a Player doodad. func (w *Canvas) AddActor(actor *Actor) error { w.actors = append(w.actors, actor)