239 lines
4.9 KiB
Go
239 lines
4.9 KiB
Go
package play
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"aego/core/errs"
|
|
"aego/editor/core/context"
|
|
)
|
|
|
|
func (c *Controller) SetBlocked(why string) {
|
|
c.blocked = why
|
|
}
|
|
|
|
func (c *Controller) SetHistoryLimit(bytes int) {
|
|
c.limit = bytes
|
|
}
|
|
|
|
func (c *Controller) Taint(why string) {
|
|
if c.hist == nil || c.replaying {
|
|
return
|
|
}
|
|
if c.cursor == 0 {
|
|
c.rebase = true
|
|
return
|
|
}
|
|
c.hist.cutAt(c.cursor, why)
|
|
c.dropKeys(c.cursor)
|
|
}
|
|
|
|
// Invalidate drops every recorded frame: a reboot would replay them with the changed code, assets or settings.
|
|
func (c *Controller) Invalidate(why string) {
|
|
if c.hist == nil || c.replaying {
|
|
return
|
|
}
|
|
c.hist.cutAt(0, why)
|
|
c.dropKeys(-1)
|
|
}
|
|
|
|
func (c *Controller) Timeline() Timeline {
|
|
tl := Timeline{Blocked: c.blocked}
|
|
h := c.hist
|
|
if h == nil || c.state == Stopped {
|
|
return tl
|
|
}
|
|
tl.Frames, tl.Cursor, tl.Usable, tl.Why, tl.Beyond = len(h.frames), c.cursor, h.usable(), h.why, c.beyond
|
|
if c.cursor > 0 {
|
|
tl.Tick = h.frames[c.cursor-1].tick
|
|
}
|
|
tl.Floor, tl.Keys, tl.KeyBytes, tl.KeysOff = c.floor, len(c.keys), c.keyBytes, c.kfOff
|
|
tl.KeyTime = c.keyTime
|
|
if n := len(c.keys); n > 0 {
|
|
tl.LastKey = c.keys[n-1].size
|
|
}
|
|
return tl
|
|
}
|
|
|
|
func (c *Controller) watchEdits() {
|
|
if c.editUnsub != nil {
|
|
c.editUnsub()
|
|
c.editUnsub = nil
|
|
}
|
|
if c.proxy != nil {
|
|
c.editUnsub = c.proxy.History.Subscribe(func() { c.Taint("the scene was edited during Play") })
|
|
}
|
|
}
|
|
|
|
func (c *Controller) record(dt float64) bool {
|
|
h := c.hist
|
|
if c.cursor < len(h.frames) {
|
|
h.truncate(c.cursor)
|
|
c.dropKeys(c.cursor)
|
|
}
|
|
if rev := c.ctx.AssetRev; rev != c.assetRev {
|
|
c.assetRev = rev
|
|
h.cutAt(0, "assets were reloaded during Play")
|
|
c.dropKeys(-1)
|
|
}
|
|
t := c.world.Time
|
|
ok := h.add(dt, t.Scale(), t.Paused(), c.stepReq, &c.input)
|
|
c.stepReq = stepNone
|
|
c.cursor = len(h.frames)
|
|
c.beyond = !ok
|
|
return ok
|
|
}
|
|
|
|
func (c *Controller) Replayed() int {
|
|
return c.replayed
|
|
}
|
|
|
|
func (c *Controller) CanSeekBack() bool {
|
|
return c.state != Stopped && c.hist != nil && c.blocked == "" && (c.cursor > c.floor || c.beyond)
|
|
}
|
|
|
|
func (c *Controller) StepBack() error {
|
|
n := c.cursor - 1
|
|
if c.beyond {
|
|
n = c.hist.usable()
|
|
}
|
|
return c.Seek(n)
|
|
}
|
|
|
|
func (c *Controller) StepForward() error {
|
|
if c.hist != nil && c.cursor < len(c.hist.frames) {
|
|
return c.Seek(c.cursor + 1)
|
|
}
|
|
if c.state == Playing {
|
|
c.Pause()
|
|
}
|
|
c.Step()
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) Seek(n int) error {
|
|
return c.seek(n, true)
|
|
}
|
|
|
|
// Scrub seeks without a notice, for dragging the timeline.
|
|
func (c *Controller) Scrub(n int) error {
|
|
return c.seek(n, false)
|
|
}
|
|
|
|
func (c *Controller) seek(n int, notice bool) error {
|
|
switch {
|
|
case c.hist == nil || c.state == Stopped:
|
|
return errs.New(errs.EditorCommand, "rewind works during Play")
|
|
case c.blocked != "":
|
|
return errs.New(errs.EditorCommand, c.blocked)
|
|
}
|
|
defer c.guard("rewind")
|
|
c.applyRebase()
|
|
n = min(max(n, c.floor), c.hist.usable())
|
|
c.setReplaying(true)
|
|
defer c.setReplaying(false)
|
|
if err := c.rewindTo(n); err != nil {
|
|
return err
|
|
}
|
|
c.replay(n)
|
|
c.pauseHere(notice)
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) rewindTo(n int) error {
|
|
if c.keysOn() {
|
|
k := c.keyAt(max(n-1, 0))
|
|
switch {
|
|
case k == nil:
|
|
return c.reboot()
|
|
case c.beyond || n < c.cursor || k.frame > c.cursor:
|
|
c.restore(k)
|
|
}
|
|
return nil
|
|
}
|
|
if n < c.cursor || c.beyond {
|
|
return c.reboot()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) reboot() error {
|
|
h := c.hist
|
|
view := c.proxy.View
|
|
c.teardown()
|
|
c.world, c.runner, c.scene = nil, nil, nil
|
|
if err := c.start(h.data, h.name); err != nil {
|
|
c.Stop()
|
|
return err
|
|
}
|
|
real := c.ctx.Docs.Get(c.doc)
|
|
if real == nil {
|
|
c.Stop()
|
|
return errs.New(errs.EditorDocument, "scene is not open")
|
|
}
|
|
c.proxy = real.PlayCopy(c.scene)
|
|
c.proxy.View = view
|
|
c.ctx.Docs.SetPlay(c.proxy, c.world)
|
|
c.watchEdits()
|
|
c.cursor, c.beyond = 0, false
|
|
c.assetRev = c.ctx.AssetRev
|
|
if h.end == 0 {
|
|
h.truncate(0)
|
|
h.end, h.why = -1, ""
|
|
}
|
|
c.bindKeys()
|
|
if c.keysOn() {
|
|
c.capture(0)
|
|
}
|
|
c.setReplaying(true)
|
|
return nil
|
|
}
|
|
|
|
// replayBreak fires on the same Break call of the frame as the recorded run did.
|
|
func (c *Controller) replayBreak(f *frameRec) func() bool {
|
|
calls := int32(0)
|
|
return func() bool {
|
|
calls++
|
|
return calls == f.brk
|
|
}
|
|
}
|
|
|
|
func (c *Controller) replay(to int) {
|
|
h, w := c.hist, c.world
|
|
brk := w.Break
|
|
c.replaying = true
|
|
c.replayed = max(to-c.cursor, 0)
|
|
for c.cursor < to {
|
|
f := h.frames[c.cursor]
|
|
t := w.Time
|
|
t.SetScale(f.scale)
|
|
t.SetPaused(f.paused)
|
|
switch f.step {
|
|
case stepFixed:
|
|
t.StepFixed()
|
|
case stepFrame:
|
|
t.StepFrame()
|
|
}
|
|
c.input = h.inputs[f.input]
|
|
w.Break = c.replayBreak(&f)
|
|
c.advance(f.dt)
|
|
c.cursor++
|
|
}
|
|
c.replaying = false
|
|
w.Break = brk
|
|
}
|
|
|
|
func (c *Controller) pauseHere(notice bool) {
|
|
t := c.world.Time
|
|
t.SetScale(c.scale)
|
|
t.SetPaused(true)
|
|
c.held = true
|
|
c.stats.Frame, c.stats.Updates, c.stats.Tick, c.stats.Alpha = t.Frame(), t.Updates(), t.Tick(), t.Alpha()
|
|
if c.state != Paused {
|
|
c.state = Paused
|
|
c.notify()
|
|
}
|
|
if notice {
|
|
c.ctx.Notify(context.LevelInfo, "Rewound to tick "+strconv.FormatUint(t.Tick(), 10))
|
|
}
|
|
}
|