Noah Petherbridge
402b5efa7e
* The level.FileSystem type has updated to support ZIP files too. * Legacy levels loaded from gz/json have their old FileSystem as a simple map[filename]data and this parses from JSON OK. * On save to zip, the legacy loaded file data gets exported to ZIP. * Going forward: newly added or deleted files during runtime are kept in the legacy file map until the next save when the filemap is again flushed out to ZIP. * For regular read-access, the FileSystem reads from the ZIP file if the data is not in the hot map (legacy file or recently modified attachment). * Bugfix: be sure to Inflate() the Level/Doodad after loading from zipfile - it used to be that directly after a save, trying to play the level failed because the Level.Actors struct was missing their IDs, and similarly recently written chunks would error out (become black voids) on levels/doodads so we Inflate() both after save/replacing their zip handle.
129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
// Package scripting manages the JavaScript VMs for Doodad scripts.
|
|
package scripting
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/level"
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
|
"git.kirsle.net/go/render"
|
|
)
|
|
|
|
// Supervisor manages the JavaScript VMs for each doodad by its
|
|
// unique ID.
|
|
type Supervisor struct {
|
|
scripts map[string]*VM
|
|
|
|
// Global event handlers.
|
|
onLevelExit func()
|
|
onLevelFail func(message string)
|
|
onSetCheckpoint func(where render.Point)
|
|
}
|
|
|
|
// NewSupervisor creates a new JavaScript Supervior.
|
|
func NewSupervisor() *Supervisor {
|
|
return &Supervisor{
|
|
scripts: map[string]*VM{},
|
|
}
|
|
}
|
|
|
|
// Teardown the supervisor to clean up goroutines.
|
|
func (s *Supervisor) Teardown() {
|
|
log.Info("scripting.Teardown(): stop all (%d) scripts", len(s.scripts))
|
|
for _, vm := range s.scripts {
|
|
vm.stop <- true
|
|
}
|
|
}
|
|
|
|
// Loop the supervisor to invoke timer events in any running scripts.
|
|
func (s *Supervisor) Loop() error {
|
|
now := time.Now()
|
|
for _, vm := range s.scripts {
|
|
vm.TickTimer(now)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InstallScripts loads scripts for all actors in the level.
|
|
func (s *Supervisor) InstallScripts(level *level.Level) error {
|
|
for _, actor := range level.Actors {
|
|
if err := s.AddLevelScript(actor.ID(), actor.Filename); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Loop again to bridge channels together for linked VMs.
|
|
for _, actor := range level.Actors {
|
|
// Add linked actor IDs.
|
|
if len(actor.Links) > 0 {
|
|
// Bridge the links up.
|
|
var thisVM = s.scripts[actor.ID()]
|
|
for _, id := range actor.Links {
|
|
// Assign this target actor's Inbound channel to the source
|
|
// actor's array of Outbound channels.
|
|
if _, ok := s.scripts[id]; !ok {
|
|
log.Error("scripting.InstallScripts: actor %s is linked to %s but %s was not found",
|
|
actor.ID(),
|
|
id,
|
|
id,
|
|
)
|
|
continue
|
|
}
|
|
thisVM.Outbound = append(thisVM.Outbound, s.scripts[id].Inbound)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddLevelScript adds a script to the supervisor with level hooks.
|
|
// The `id` will key the VM and should be the Actor ID in the level.
|
|
// The `name` is used to name the VM for debug logging.
|
|
func (s *Supervisor) AddLevelScript(id string, name string) error {
|
|
if _, ok := s.scripts[id]; ok {
|
|
return fmt.Errorf("AddLevelScript: duplicate actor ID '%s' in level", id)
|
|
}
|
|
|
|
s.scripts[id] = NewVM(fmt.Sprintf("%s#%s", name, id))
|
|
RegisterPublishHooks(s, s.scripts[id])
|
|
RegisterEventHooks(s, s.scripts[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
|
|
}
|
|
|
|
// TODO: put this log back in, but add PLAYER script so it doesn't spam
|
|
// the console for missing PLAYER.
|
|
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")
|
|
}
|
|
|
|
// RemoveVM removes a script from the supervisor, stopping it.
|
|
func (s *Supervisor) RemoveVM(name string) error {
|
|
if _, ok := s.scripts[name]; ok {
|
|
delete(s.scripts, name)
|
|
return nil
|
|
}
|
|
return errors.New("not found")
|
|
}
|