2018-07-22 03:43:01 +00:00
|
|
|
package doodle
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-11-21 07:35:37 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2018-07-26 02:38:54 +00:00
|
|
|
"strconv"
|
2021-06-20 05:14:41 +00:00
|
|
|
"strings"
|
2018-10-02 17:11:38 +00:00
|
|
|
|
2022-09-24 22:17:25 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/assets"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/chatbot"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
|
2024-02-08 06:14:48 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
2022-09-25 04:58:01 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
2022-01-17 04:09:27 +00:00
|
|
|
"github.com/dop251/goja"
|
2018-07-22 03:43:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Command is a parsed shell command.
|
|
|
|
type Command struct {
|
|
|
|
Raw string // The complete raw command the user typed.
|
|
|
|
Command string // The first word of their command.
|
|
|
|
Args []string // The shell-args array of parameters.
|
|
|
|
ArgsLiteral string // The args portion of the command literally.
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run the command.
|
|
|
|
func (c Command) Run(d *Doodle) error {
|
|
|
|
if len(c.Raw) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-04-10 01:28:08 +00:00
|
|
|
// Cheat codes
|
2020-04-07 06:21:17 +00:00
|
|
|
if cheat := c.cheatCommand(d); cheat {
|
2020-01-03 04:23:27 +00:00
|
|
|
return nil
|
2019-04-10 01:28:08 +00:00
|
|
|
}
|
|
|
|
|
2021-06-20 05:14:41 +00:00
|
|
|
switch strings.ToLower(c.Command) {
|
2018-07-25 05:26:27 +00:00
|
|
|
case "echo":
|
|
|
|
d.Flash(c.ArgsLiteral)
|
|
|
|
return nil
|
2021-10-08 01:24:18 +00:00
|
|
|
case "error":
|
|
|
|
d.FlashError(c.ArgsLiteral)
|
|
|
|
return nil
|
2020-11-16 02:02:35 +00:00
|
|
|
case "alert":
|
|
|
|
modal.Alert(c.ArgsLiteral)
|
|
|
|
return nil
|
WIP Publish Dialog + UI Improvements
* File->Publish Level in the Level Editor opens the Publish window,
where you can embed custom doodads into your level and export a
portable .level file you can share with others.
* Currently does not actually export a level file yet.
* The dialog lists all unique doodad names in use in your level, and
designates which are built-ins and which are custom (paginated).
* A checkbox would let the user embed built-in doodads into their level,
as well, locking it in to those versions and not using updated
versions from future game releases.
UI Improvements:
* Added styling for a "Primary" UI button, rendered in deep blue.
* Pop-up modals (Alert, Confirm) color their Ok button as Primary.
* The Enter key pressed during an Alert or Confirm modal will invoke its
default button and close the modal, corresponding to its Primary
button.
* The developer console is now opened with the tilde/grave key ` instead
of the Enter key, so that the Enter key is free to click through
modals.
* In the "Open/Edit Drawing" window, a "Browse..." button is added to
the level and doodad sections, spawning a native File Open dialog to
pick a .level or .doodad outside the config root.
2021-06-11 05:31:30 +00:00
|
|
|
case "confirm":
|
|
|
|
modal.Confirm(c.ArgsLiteral).Then(func() {
|
|
|
|
d.Flash("Confirmed.")
|
|
|
|
})
|
|
|
|
return nil
|
2018-07-22 03:43:01 +00:00
|
|
|
case "new":
|
|
|
|
return c.New(d)
|
|
|
|
case "save":
|
|
|
|
return c.Save(d)
|
|
|
|
case "edit":
|
|
|
|
return c.Edit(d)
|
|
|
|
case "play":
|
|
|
|
return c.Play(d)
|
2018-10-08 17:38:49 +00:00
|
|
|
case "close":
|
|
|
|
return c.Close(d)
|
2022-01-03 06:36:32 +00:00
|
|
|
case "titlescreen":
|
|
|
|
return c.TitleScreen(d)
|
2018-07-22 03:43:01 +00:00
|
|
|
case "exit":
|
|
|
|
case "quit":
|
|
|
|
return c.Quit()
|
2018-07-25 05:26:27 +00:00
|
|
|
case "help":
|
|
|
|
return c.Help(d)
|
2018-08-02 01:52:52 +00:00
|
|
|
case "reload":
|
|
|
|
d.Goto(d.Scene)
|
|
|
|
return nil
|
|
|
|
case "guitest":
|
|
|
|
d.Goto(&GUITestScene{})
|
|
|
|
return nil
|
2018-07-26 02:38:54 +00:00
|
|
|
case "eval":
|
2020-11-21 06:53:38 +00:00
|
|
|
fallthrough
|
2018-07-26 02:38:54 +00:00
|
|
|
case "$":
|
2019-04-20 00:23:37 +00:00
|
|
|
out, err := c.RunScript(d, c.ArgsLiteral)
|
2018-07-26 02:38:54 +00:00
|
|
|
d.Flash("%+v", out)
|
|
|
|
return err
|
2018-10-08 20:06:42 +00:00
|
|
|
case "repl":
|
|
|
|
d.shell.Repl = true
|
|
|
|
d.shell.Text = "$ "
|
2021-06-20 05:14:41 +00:00
|
|
|
case "boolprop":
|
2018-07-26 02:38:54 +00:00
|
|
|
return c.BoolProp(d)
|
2020-11-21 07:35:37 +00:00
|
|
|
case "extract-bindata":
|
|
|
|
// Undocumented command to extract the binary of its assets.
|
|
|
|
return c.ExtractBindata(d, c.ArgsLiteral)
|
2022-09-25 04:58:01 +00:00
|
|
|
case "throw":
|
|
|
|
// Test exception catcher with custom message.
|
|
|
|
exceptions.Catch(strings.ReplaceAll(c.ArgsLiteral, "\\n", "\n"))
|
|
|
|
return nil
|
|
|
|
case "throw2":
|
|
|
|
// Stress test exception catcher.
|
|
|
|
exceptions.Catch(
|
|
|
|
"This is a test of the Exception Catcher modal.\n\nIt should be able to display a decent amount " +
|
|
|
|
"of text with character wrapping. Multiple lines, too.\n\nIt might not show the full message, so " +
|
|
|
|
"click the 'Copy' button to copy to clipboard and read the whole message. There is more text " +
|
|
|
|
"than is shown on screen.\n\nThis text for example was not on screen, but copied to your " +
|
|
|
|
"clipboard anyway. :)",
|
|
|
|
)
|
|
|
|
return nil
|
|
|
|
case "throw3":
|
|
|
|
// Realistic exception.
|
|
|
|
exceptions.Catch(
|
|
|
|
"Error in main() for actor trapdoor-down.doodad:\n\n" +
|
|
|
|
"TypeError: Cannot read property 'zz' of undefined at main (<eval>:25:14(77))\n\n" +
|
|
|
|
"Actor ID: c3aa346b-be51-4bc4-94bb-f3adf5643830\n" +
|
|
|
|
"Filename: trapdoor-down.doodad\n" +
|
|
|
|
"Position: 643,266",
|
|
|
|
)
|
2024-02-08 06:14:48 +00:00
|
|
|
case "flush-textures":
|
|
|
|
// Flush all textures.
|
|
|
|
native.FreeTextures(d.Engine)
|
|
|
|
d.Flash("All textures freed.")
|
2018-07-22 03:43:01 +00:00
|
|
|
default:
|
2022-01-09 03:21:08 +00:00
|
|
|
return c.Default(d)
|
2018-07-22 03:43:01 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// New opens a new map in the editor mode.
|
|
|
|
func (c Command) New(d *Doodle) error {
|
2019-06-25 21:57:11 +00:00
|
|
|
d.GotoNewMenu()
|
2018-07-22 03:43:01 +00:00
|
|
|
return nil
|
2018-10-08 17:38:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close returns to the Main Scene.
|
|
|
|
func (c Command) Close(d *Doodle) error {
|
|
|
|
main := &MainScene{}
|
|
|
|
d.Goto(main)
|
|
|
|
return nil
|
2018-07-22 03:43:01 +00:00
|
|
|
}
|
|
|
|
|
2020-11-21 07:35:37 +00:00
|
|
|
// ExtractBindata dumps the app's embedded bindata to the filesystem.
|
|
|
|
func (c Command) ExtractBindata(d *Doodle, path string) error {
|
|
|
|
if len(path) == 0 || path[0] != '/' {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("Required: an absolute path to a directory to extract to.")
|
2020-11-21 07:35:37 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := os.MkdirAll(path, 0755)
|
|
|
|
if err != nil {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("MkdirAll: %s", err)
|
2020-11-21 07:35:37 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-07-14 01:02:57 +00:00
|
|
|
for _, filename := range assets.AssetNames() {
|
2020-11-21 07:35:37 +00:00
|
|
|
outfile := filepath.Join(path, filename)
|
|
|
|
log.Info("Extracting bindata: %s to: %s", filename, outfile)
|
|
|
|
|
2021-07-14 01:02:57 +00:00
|
|
|
data, err := assets.Asset(filename)
|
2020-11-21 07:35:37 +00:00
|
|
|
if err != nil {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("error on file %s: %s", filename, err)
|
2020-11-21 07:35:37 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fill out the directory path.
|
|
|
|
if _, err := os.Stat(filepath.Dir(outfile)); os.IsNotExist(err) {
|
|
|
|
os.MkdirAll(filepath.Dir(outfile), 0755)
|
|
|
|
}
|
|
|
|
|
|
|
|
fh, err := os.Create(outfile)
|
|
|
|
if err != nil {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("error writing file %s: %s", outfile, err)
|
2020-11-21 07:35:37 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
fh.Write(data)
|
|
|
|
fh.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
d.Flash("Bindata extracted to %s", path)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-25 05:26:27 +00:00
|
|
|
// Help prints the help info.
|
|
|
|
func (c Command) Help(d *Doodle) error {
|
|
|
|
if len(c.Args) == 0 {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.Flash("Available commands: new save edit play quit echo error")
|
2020-11-21 06:53:38 +00:00
|
|
|
d.Flash(" alert clear help boolProp eval repl")
|
2018-07-25 05:26:27 +00:00
|
|
|
d.Flash("Type `help` and then the command, like: `help edit`")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-06-20 05:14:41 +00:00
|
|
|
switch strings.ToLower(c.Args[0]) {
|
2020-11-21 06:53:38 +00:00
|
|
|
case "echo":
|
|
|
|
d.Flash("Usage: echo <message>")
|
|
|
|
d.Flash("Flash a message back to the console")
|
2021-10-08 01:24:18 +00:00
|
|
|
case "error":
|
|
|
|
d.Flash("Usage: error <message>")
|
|
|
|
d.Flash("Flash an error message back to the console")
|
2020-11-21 06:53:38 +00:00
|
|
|
case "alert":
|
|
|
|
d.Flash("Usage: alert <message>")
|
|
|
|
d.Flash("Pop up an Alert box with a custom message")
|
2018-07-25 05:26:27 +00:00
|
|
|
case "new":
|
|
|
|
d.Flash("Usage: new")
|
|
|
|
d.Flash("Create a new drawing in Edit Mode")
|
|
|
|
case "save":
|
|
|
|
d.Flash("Usage: save [filename.json]")
|
|
|
|
d.Flash("Save the map to disk (in Edit Mode only)")
|
|
|
|
case "edit":
|
|
|
|
d.Flash("Usage: edit <filename.json>")
|
|
|
|
d.Flash("Open a file on disk in Edit Mode")
|
|
|
|
case "play":
|
|
|
|
d.Flash("Usage: play <filename.json>")
|
|
|
|
d.Flash("Open a map from disk in Play Mode")
|
|
|
|
case "quit":
|
2020-11-21 06:53:38 +00:00
|
|
|
fallthrough
|
2018-07-25 05:26:27 +00:00
|
|
|
case "exit":
|
|
|
|
d.Flash("Usage: quit")
|
2020-11-21 06:53:38 +00:00
|
|
|
d.Flash("Closes the dev console (alias: exit)")
|
2018-07-25 05:26:27 +00:00
|
|
|
case "clear":
|
|
|
|
d.Flash("Usage: clear")
|
2020-11-21 06:53:38 +00:00
|
|
|
d.Flash("Clears the console output history")
|
|
|
|
case "eval":
|
|
|
|
fallthrough
|
|
|
|
case "$":
|
|
|
|
d.Flash("Evaluate a line of JavaScript on the in-game interpreter")
|
|
|
|
case "repl":
|
|
|
|
d.Flash("Enter a JavaScript shell on the in-game interpreter")
|
2021-06-20 05:14:41 +00:00
|
|
|
case "boolprop":
|
2020-11-21 06:53:38 +00:00
|
|
|
d.Flash("Toggle boolean values. `boolProp list` lists available")
|
2022-01-03 06:36:32 +00:00
|
|
|
case "titlescreen":
|
|
|
|
d.Flash("Usage: titlescreen <filename.level>")
|
|
|
|
d.Flash("Open the title screen with a level")
|
2018-07-25 05:26:27 +00:00
|
|
|
case "help":
|
|
|
|
d.Flash("Usage: help <command>")
|
2020-11-21 06:53:38 +00:00
|
|
|
d.Flash("Gets further help on a command")
|
2018-07-25 05:26:27 +00:00
|
|
|
default:
|
|
|
|
d.Flash("Unknown help topic.")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
// Save the current map to disk.
|
|
|
|
func (c Command) Save(d *Doodle) error {
|
2018-07-26 02:38:54 +00:00
|
|
|
if scene, ok := d.Scene.(*EditorScene); ok {
|
2018-07-22 03:43:01 +00:00
|
|
|
filename := ""
|
|
|
|
if len(c.Args) > 0 {
|
|
|
|
filename = c.Args[0]
|
|
|
|
} else if scene.filename != "" {
|
|
|
|
filename = scene.filename
|
|
|
|
} else {
|
2018-10-02 17:11:38 +00:00
|
|
|
return errors.New("usage: save <filename>")
|
2018-07-22 03:43:01 +00:00
|
|
|
}
|
|
|
|
|
2018-10-02 17:11:38 +00:00
|
|
|
switch scene.DrawingType {
|
|
|
|
case enum.LevelDrawing:
|
|
|
|
d.shell.Write("Saving Level: " + filename)
|
|
|
|
scene.SaveLevel(filename)
|
|
|
|
case enum.DoodadDrawing:
|
|
|
|
d.shell.Write("Saving Doodad: " + filename)
|
|
|
|
scene.SaveDoodad(filename)
|
|
|
|
}
|
2018-07-22 03:43:01 +00:00
|
|
|
} else {
|
|
|
|
return errors.New("save: only available in Edit Mode")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Edit a map from disk.
|
|
|
|
func (c Command) Edit(d *Doodle) error {
|
|
|
|
if len(c.Args) == 0 {
|
|
|
|
return errors.New("Usage: edit <file name>")
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := c.Args[0]
|
2018-10-02 17:11:38 +00:00
|
|
|
d.shell.Write("Editing file: " + filename)
|
2019-04-20 00:23:37 +00:00
|
|
|
return d.EditFile(filename)
|
2018-07-22 03:43:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Play a map.
|
|
|
|
func (c Command) Play(d *Doodle) error {
|
|
|
|
if len(c.Args) == 0 {
|
|
|
|
return errors.New("Usage: play <file name>")
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := c.Args[0]
|
|
|
|
d.shell.Write("Playing level: " + filename)
|
|
|
|
d.PlayLevel(filename)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-01-03 06:36:32 +00:00
|
|
|
// TitleScreen loads the title with a custom user level.
|
|
|
|
func (c Command) TitleScreen(d *Doodle) error {
|
|
|
|
if len(c.Args) == 0 {
|
|
|
|
return errors.New("Usage: titlescreen <level name.level>")
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := c.Args[0]
|
|
|
|
d.shell.Write("Playing level: " + filename)
|
|
|
|
d.Goto(&MainScene{
|
|
|
|
LevelFilename: filename,
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
// Quit the command line shell.
|
|
|
|
func (c Command) Quit() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-26 02:38:54 +00:00
|
|
|
// BoolProp command sets available boolean variables.
|
|
|
|
func (c Command) BoolProp(d *Doodle) error {
|
2019-07-07 06:28:11 +00:00
|
|
|
if len(c.Args) == 1 {
|
|
|
|
// Showing the value of a boolProp. Only supported for those defined
|
|
|
|
// in balance/boolprops.go
|
|
|
|
value, err := balance.GetBoolProp(c.Args[0])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
d.Flash("%s: %+v", c.Args[0], value)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-26 02:38:54 +00:00
|
|
|
if len(c.Args) != 2 {
|
2023-01-02 20:36:12 +00:00
|
|
|
return errors.New("Usage: boolProp <name> [true, false, flip]")
|
2018-07-26 02:38:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
name = c.Args[0]
|
|
|
|
value = c.Args[1]
|
|
|
|
truthy = value[0] == 't' || value[0] == 'T' || value[0] == '1'
|
2023-01-02 20:36:12 +00:00
|
|
|
flip = value == "flip"
|
2018-07-26 02:38:54 +00:00
|
|
|
ok = true
|
|
|
|
)
|
|
|
|
|
|
|
|
switch name {
|
|
|
|
case "Debug":
|
|
|
|
case "D":
|
|
|
|
d.Debug = truthy
|
|
|
|
case "DebugOverlay":
|
|
|
|
case "DO":
|
2023-01-02 20:36:12 +00:00
|
|
|
if flip {
|
|
|
|
DebugOverlay = !DebugOverlay
|
|
|
|
} else {
|
|
|
|
DebugOverlay = truthy
|
|
|
|
}
|
2018-07-26 02:38:54 +00:00
|
|
|
case "DebugCollision":
|
|
|
|
case "DC":
|
2023-01-02 20:36:12 +00:00
|
|
|
if flip {
|
|
|
|
DebugCollision = !DebugCollision
|
|
|
|
} else {
|
|
|
|
DebugCollision = truthy
|
|
|
|
}
|
2018-07-26 02:38:54 +00:00
|
|
|
default:
|
|
|
|
ok = false
|
|
|
|
}
|
|
|
|
|
|
|
|
if ok {
|
2023-01-02 20:36:12 +00:00
|
|
|
if flip {
|
|
|
|
d.Flash("Toggled boolProp %s", name)
|
|
|
|
} else {
|
|
|
|
d.Flash("Set boolProp %s=%s", name, strconv.FormatBool(truthy))
|
|
|
|
}
|
2018-07-26 02:38:54 +00:00
|
|
|
} else {
|
2019-07-07 06:28:11 +00:00
|
|
|
// Try the global boolProps in balance package.
|
|
|
|
if err := balance.BoolProp(name, truthy); err != nil {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("%s", err)
|
2019-07-07 06:28:11 +00:00
|
|
|
} else {
|
|
|
|
d.Flash("%s: %+v", name, truthy)
|
|
|
|
}
|
2018-07-26 02:38:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-04-20 00:23:37 +00:00
|
|
|
// RunScript evaluates some JavaScript code safely.
|
2022-01-17 04:09:27 +00:00
|
|
|
func (c Command) RunScript(d *Doodle, code string) (goja.Value, error) {
|
2019-04-20 00:23:37 +00:00
|
|
|
defer func() {
|
|
|
|
if err := recover(); err != nil {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("Command.RunScript: Panic: %s", err)
|
2019-04-20 00:23:37 +00:00
|
|
|
}
|
|
|
|
}()
|
2022-03-27 18:51:14 +00:00
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
out, err := d.shell.js.RunString(code)
|
|
|
|
|
2022-03-27 18:51:14 +00:00
|
|
|
// If we're in Play Mode, consider it cheating if the player is
|
|
|
|
// messing with any in-game structures.
|
|
|
|
if scene, ok := d.Scene.(*PlayScene); ok {
|
|
|
|
scene.SetCheated()
|
|
|
|
}
|
|
|
|
|
2019-04-20 00:23:37 +00:00
|
|
|
return out, err
|
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
// Default command.
|
2022-01-09 03:21:08 +00:00
|
|
|
func (c Command) Default(d *Doodle) error {
|
|
|
|
// Give the easter egg RiveScript bot a chance.
|
|
|
|
if reply, err := chatbot.Handle(c.Raw); err == nil {
|
|
|
|
for _, reply := range strings.Split(reply, "\n") {
|
|
|
|
d.Flash(reply)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
log.Error("RiveScript error: %s", err)
|
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
return fmt.Errorf("%s: command not found. Try `help` for help",
|
|
|
|
c.Command,
|
|
|
|
)
|
|
|
|
}
|