Noah Petherbridge
6def8f7625
* Fix collision detection to allow actors to walk up slopes smoothly, without losing any horizontal velocity. * Fix scrolling a level canvas so that chunks near the right or bottom edge of the viewpoint were getting culled prematurely. * Centralize JavaScript exception catching logic to attach Go and JS stack traces where possible to be more useful for debugging. * Performance: flush all SDL2 textures from memory between scene transitions in the app. Also add a `flush-textures` dev console command to flush the textures at any time - they all should regenerate if still needed based on underlying go.Images which can be garbage collected.
297 lines
6.9 KiB
Go
297 lines
6.9 KiB
Go
// Package exceptions handles JavaScript errors nicely for the game.
|
|
package exceptions
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/lib/debugging"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/render/event"
|
|
"git.kirsle.net/go/ui"
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
// The exception catching window is a singleton and appears on top with
|
|
// its own supervisor apart from whatever the game is currently doing.
|
|
var (
|
|
Supervisor *ui.Supervisor
|
|
Window *ui.Window
|
|
Disabled bool // don't reopen the window again
|
|
lastException string // text of last exception
|
|
excLabel *string // trimmed exception label text
|
|
mu sync.RWMutex // thread safety
|
|
|
|
// Configurables.
|
|
winSize = render.NewRect(380, 260)
|
|
excSize = render.NewRect(360, 140)
|
|
charsWide = 50 // to trim the exception text onto screen
|
|
charsTall = 9
|
|
)
|
|
|
|
func init() {
|
|
l := "No Exception Traceback"
|
|
excLabel = &l
|
|
}
|
|
|
|
// Catch a JavaScript exception and maybe show it to the user.
|
|
func Catch(exc string, args ...interface{}) {
|
|
if len(args) > 0 {
|
|
exc = fmt.Sprintf(exc, args...)
|
|
}
|
|
|
|
log.Error("[JS] Exception: %s", exc)
|
|
if Disabled {
|
|
return
|
|
}
|
|
|
|
Setup()
|
|
|
|
width, _ := shmem.CurrentRenderEngine.WindowSize()
|
|
Window.MoveTo(render.Point{
|
|
X: (width / 2) - (Window.Size().W / 2),
|
|
Y: 60,
|
|
})
|
|
|
|
Window.Show()
|
|
lastException = exc
|
|
*excLabel = trim(exc)
|
|
}
|
|
|
|
// FormatAndCatch an exception from a JavaScript VM. This is the common function called
|
|
// immediately when a scripting-related panic is recovered, and appends stack trace frames
|
|
// and formats the message for final Catch display.
|
|
func FormatAndCatch(vm *goja.Runtime, exc string, args ...interface{}) {
|
|
// Collect the JavaScript stack frame for debugging.
|
|
var (
|
|
buf [1000]goja.StackFrame
|
|
jsCallers = []string{}
|
|
sections = []string{
|
|
fmt.Sprintf(exc, args...),
|
|
}
|
|
)
|
|
|
|
if vm != nil {
|
|
frames := vm.CaptureCallStack(1000, buf[:0])
|
|
for i, frame := range frames {
|
|
var position = frame.Position()
|
|
jsCallers = append(jsCallers, fmt.Sprintf("%d. %s at %s line %d column %d", i+1, frame.FuncName(), position.Filename, position.Line, position.Column))
|
|
}
|
|
|
|
if len(jsCallers) > 0 {
|
|
sections = append(sections, fmt.Sprintf("JS stack:\n%s", strings.Join(jsCallers, "\n")))
|
|
}
|
|
}
|
|
|
|
sections = append(sections, fmt.Sprintf("Go stack:\n%s", debugging.StringifyCallers()))
|
|
|
|
Catch(
|
|
strings.Join(sections, "\n\n"),
|
|
)
|
|
}
|
|
|
|
// Setup the global supervisor and window the first time - after the render engine has initialized,
|
|
// e.g., when you want the window to show up the first time.
|
|
func Setup() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
log.Info("Setup Exceptions")
|
|
|
|
if Supervisor == nil {
|
|
Supervisor = ui.NewSupervisor()
|
|
}
|
|
|
|
if Window == nil {
|
|
Window = MakeWindow(Exception{
|
|
Supervisor: Supervisor,
|
|
Engine: shmem.CurrentRenderEngine,
|
|
})
|
|
Window.Compute(shmem.CurrentRenderEngine)
|
|
Window.Supervise(Supervisor)
|
|
}
|
|
}
|
|
|
|
// Teardown the exception UI, if it was loaded. This is done between Scene transitions in the game
|
|
// to reduce memory leaks in case the scenes will flush SDL2 caches or w/e.
|
|
func Teardown() {
|
|
log.Error("Teardown Exceptions")
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if Window != nil {
|
|
Window = nil
|
|
}
|
|
|
|
if Supervisor != nil {
|
|
Supervisor = nil
|
|
}
|
|
}
|
|
|
|
// Handled returns true if the exception window handles the events this tick, e.g.
|
|
// so clicking on its window won't let your mouse click also hit things behind it.
|
|
func Handled(ev *event.State) bool {
|
|
if Window != nil && !Window.Hidden() {
|
|
// If they hit the Return/Escape key, dismiss the exception.
|
|
if ev.Enter || ev.Escape {
|
|
ev.Escape = false
|
|
Window.Close()
|
|
return true
|
|
}
|
|
|
|
Supervisor.Loop(ev)
|
|
|
|
// NOTE: if the window Close handler tears down the Supervisor, the
|
|
// previous call to Supervisor.Loop() may run it and then Supervisor
|
|
// no longer exists.
|
|
if Supervisor != nil && Supervisor.IsPointInWindow(shmem.Cursor) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Loop allows the exception window to appear on game tick.
|
|
func Draw(e render.Engine) {
|
|
if Supervisor != nil {
|
|
Supervisor.Present(e)
|
|
}
|
|
}
|
|
|
|
// Exception window to show scripting errors in doodads.
|
|
type Exception struct {
|
|
// Settings passed in by doodle
|
|
Supervisor *ui.Supervisor
|
|
Engine render.Engine
|
|
}
|
|
|
|
// Function to trim the raw exception text so it fits neatly within the label.
|
|
// In case it's long, use the Copy button to copy to your clipboard.
|
|
func trim(input string) string {
|
|
var lines = []string{}
|
|
for _, line := range strings.Split(input, "\n") {
|
|
if len(lines) >= charsTall {
|
|
lines = lines[:charsTall]
|
|
break
|
|
}
|
|
|
|
if len(line) > charsWide {
|
|
// Word wrap it.
|
|
for len(line) > charsWide {
|
|
lines = append(lines, line[:charsWide])
|
|
line = line[charsWide:]
|
|
}
|
|
if len(line) > 0 {
|
|
lines = append(lines, line)
|
|
}
|
|
continue
|
|
}
|
|
|
|
lines = append(lines, line)
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
// MakeWindow initializes the window.
|
|
func MakeWindow(cfg Exception) *ui.Window {
|
|
window := ui.NewWindow("Exception")
|
|
window.Configure(ui.Config{
|
|
Width: winSize.W,
|
|
Height: winSize.H,
|
|
Background: render.Red.Lighten(128),
|
|
})
|
|
|
|
window.Handle(ui.CloseWindow, func(ed ui.EventData) error {
|
|
Teardown()
|
|
return ui.ErrStopPropagation
|
|
})
|
|
|
|
header := ui.NewLabel(ui.Label{
|
|
Text: "A JavaScript error has occurred in a doodad:",
|
|
Font: balance.UIFont,
|
|
})
|
|
window.Pack(header, ui.Pack{
|
|
Side: ui.N,
|
|
Padding: 8,
|
|
})
|
|
|
|
text := ui.NewLabel(ui.Label{
|
|
TextVariable: excLabel,
|
|
Font: balance.ExceptionFont,
|
|
})
|
|
text.Configure(ui.Config{
|
|
BorderSize: 1,
|
|
BorderStyle: ui.BorderSunken,
|
|
Background: render.White,
|
|
Width: excSize.W,
|
|
Height: excSize.H,
|
|
})
|
|
window.Pack(text, ui.Pack{
|
|
Side: ui.N,
|
|
})
|
|
|
|
frame := ui.NewFrame("Button frame")
|
|
buttons := []struct {
|
|
label string
|
|
tooltip string
|
|
f func()
|
|
}{
|
|
{"Dismiss", "", func() {
|
|
Window.Close()
|
|
}},
|
|
{"Copy", "Copy the full text to clipboard", func() {
|
|
native.CopyToClipboard(lastException)
|
|
}},
|
|
{"Don't show again", "Don't show errors like this again\nuntil your next play session", func() {
|
|
Disabled = true
|
|
Window.Close()
|
|
}},
|
|
}
|
|
for i, button := range buttons {
|
|
button := button
|
|
|
|
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
|
|
Text: button.label,
|
|
Font: balance.MenuFont,
|
|
}))
|
|
if i == 0 {
|
|
btn.SetStyle(&balance.ButtonPrimary)
|
|
}
|
|
|
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
|
button.f()
|
|
return nil
|
|
})
|
|
|
|
btn.Compute(cfg.Engine)
|
|
|
|
// Tooltips?
|
|
if len(button.tooltip) > 0 {
|
|
ui.NewTooltip(btn, ui.Tooltip{
|
|
Text: button.tooltip,
|
|
Edge: ui.Bottom,
|
|
})
|
|
}
|
|
|
|
cfg.Supervisor.Add(btn)
|
|
|
|
frame.Pack(btn, ui.Pack{
|
|
Side: ui.W,
|
|
PadX: 4,
|
|
Expand: true,
|
|
Fill: true,
|
|
})
|
|
}
|
|
window.Pack(frame, ui.Pack{
|
|
Side: ui.N,
|
|
PadY: 12,
|
|
})
|
|
|
|
return window
|
|
}
|