diff --git a/debugger/debugger.go b/debugger/debugger.go
index 66338d21b..17ae5eeef 100644
--- a/debugger/debugger.go
+++ b/debugger/debugger.go
@@ -460,6 +460,9 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger
return nil, fmt.Errorf("debugger: %w", err)
}
+ // attach rewind system as an event recorder
+ dbg.vcs.Input.AddRecorder(dbg.Rewind)
+
// add reflection system to the GUI
dbg.ref = reflection.NewReflector(dbg.vcs)
if r, ok := dbg.gui.(reflection.Broker); ok {
@@ -576,6 +579,7 @@ func (dbg *Debugger) setState(state govern.State, subState govern.SubState) {
dbg.ref.SetEmulationState(state)
}
dbg.CoProcDev.SetEmulationState(state)
+ dbg.Rewind.SetEmulationState(state)
dbg.state.Store(state)
dbg.subState.Store(subState)
diff --git a/hardware/input/input.go b/hardware/input/input.go
index 3969771c1..4a4202833 100644
--- a/hardware/input/input.go
+++ b/hardware/input/input.go
@@ -81,24 +81,26 @@ func (inp *Input) PeripheralID(id plugging.PortID) plugging.PeripheralID {
// If a playback is currently active the input will not be handled and false
// will be returned.
func (inp *Input) HandleInputEvent(ev ports.InputEvent) (bool, error) {
- for _, r := range inp.recorder {
- err := r.RecordEvent(ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev})
- if err != nil {
- return false, err
- }
- }
-
handled, err := inp.ports.HandleInputEvent(ev)
if err != nil {
return handled, err
}
- // forward to passenger if one is defined
- if handled && inp.toPassenger != nil {
- select {
- case inp.toPassenger <- ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}:
- default:
- return handled, fmt.Errorf("input: passenger event queue is full: input dropped")
+ if handled {
+ for _, r := range inp.recorder {
+ err := r.RecordEvent(ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev})
+ if err != nil {
+ return false, err
+ }
+ }
+
+ // forward to passenger if one is defined
+ if handled && inp.toPassenger != nil {
+ select {
+ case inp.toPassenger <- ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}:
+ default:
+ return handled, fmt.Errorf("input: passenger event queue is full: input dropped")
+ }
}
}
diff --git a/hardware/input/recording.go b/hardware/input/recording.go
index 807995165..27c3c2fbb 100644
--- a/hardware/input/recording.go
+++ b/hardware/input/recording.go
@@ -71,7 +71,7 @@ func (inp *Input) handlePlaybackEvents() error {
return nil
}
- // loop with GetPlayback() until we encounter a NoPortID or NoEvent
+ // loop with GetPlayback() until we encounter an PortUnplugged or NoEvent
// condition. there might be more than one entry for a particular
// frame/scanline/horizpas state so we need to make sure we've processed
// them all.
@@ -88,10 +88,13 @@ func (inp *Input) handlePlaybackEvents() error {
morePlayback = ev.Port != plugging.PortUnplugged && ev.Ev != ports.NoEvent
if morePlayback {
- _, err := inp.ports.HandleInputEvent(ev.InputEvent)
+ handled, err := inp.ports.HandleInputEvent(ev.InputEvent)
if err != nil {
return err
}
+ if !handled {
+ continue
+ }
// forward to passenger if necessary
if inp.toPassenger != nil {
diff --git a/hardware/riot/ports/events.go b/hardware/riot/ports/events.go
index 1a8ef09c6..81ff8f49b 100644
--- a/hardware/riot/ports/events.go
+++ b/hardware/riot/ports/events.go
@@ -170,9 +170,17 @@ type InputEvent struct {
D EventData
}
+func (ev InputEvent) String() string {
+ return fmt.Sprintf("%s=%v on %s", ev.Ev, ev.D, ev.Port)
+}
+
// TimedInputEvent embeds the InputEvent type and adds a Time field (time
// measured by TelevisionCoords).
type TimedInputEvent struct {
Time coords.TelevisionCoords
InputEvent
}
+
+func (ev TimedInputEvent) String() string {
+ return fmt.Sprintf("%s @ %s", ev.InputEvent, ev.Time)
+}
diff --git a/hardware/television/coords/coords.go b/hardware/television/coords/coords.go
index b30af1295..c51355ba5 100644
--- a/hardware/television/coords/coords.go
+++ b/hardware/television/coords/coords.go
@@ -56,11 +56,11 @@ func (c TelevisionCoords) String() string {
//
// If the Frame field is undefined for either argument then the Frame field is
// ignored for the test.
-func Equal(A, B TelevisionCoords) bool {
- if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined {
- return A.Scanline == B.Scanline && A.Clock == B.Clock
+func Equal(a, b TelevisionCoords) bool {
+ if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined {
+ return a.Scanline == b.Scanline && a.Clock == b.Clock
}
- return A.Frame == B.Frame && A.Scanline == B.Scanline && A.Clock == B.Clock
+ return a.Frame == b.Frame && a.Scanline == b.Scanline && a.Clock == b.Clock
}
// GreaterThanOrEqual compares two instances of TelevisionCoords and return
@@ -68,12 +68,12 @@ func Equal(A, B TelevisionCoords) bool {
//
// If the Frame field is undefined for either argument then the Frame field is
// ignored for the test.
-func GreaterThanOrEqual(A, B TelevisionCoords) bool {
- if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined {
- return A.Scanline > B.Scanline || (A.Scanline == B.Scanline && A.Clock >= B.Clock)
+func GreaterThanOrEqual(a, b TelevisionCoords) bool {
+ if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined {
+ return a.Scanline > b.Scanline || (a.Scanline == b.Scanline && a.Clock >= b.Clock)
}
- return A.Frame > B.Frame || (A.Frame == B.Frame && A.Scanline > B.Scanline) || (A.Frame == B.Frame && A.Scanline == B.Scanline && A.Clock >= B.Clock)
+ return a.Frame > b.Frame || (a.Frame == b.Frame && a.Scanline > b.Scanline) || (a.Frame == b.Frame && a.Scanline == b.Scanline && a.Clock >= b.Clock)
}
// GreaterThan compares two instances of TelevisionCoords and return true if A
@@ -81,11 +81,11 @@ func GreaterThanOrEqual(A, B TelevisionCoords) bool {
//
// If the Frame field is undefined for either argument then the Frame field is
// ignored for the test.
-func GreaterThan(A, B TelevisionCoords) bool {
- if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined {
- return A.Scanline > B.Scanline || (A.Scanline == B.Scanline && A.Clock > B.Clock)
+func GreaterThan(a, b TelevisionCoords) bool {
+ if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined {
+ return a.Scanline > b.Scanline || (a.Scanline == b.Scanline && a.Clock > b.Clock)
}
- return A.Frame > B.Frame || (A.Frame == B.Frame && A.Scanline > B.Scanline) || (A.Frame == B.Frame && A.Scanline == B.Scanline && A.Clock > B.Clock)
+ return a.Frame > b.Frame || (a.Frame == b.Frame && a.Scanline > b.Scanline) || (a.Frame == b.Frame && a.Scanline == b.Scanline && a.Clock > b.Clock)
}
// Diff returns the difference between the B and A instances. The
@@ -95,11 +95,11 @@ func GreaterThan(A, B TelevisionCoords) bool {
//
// If the Frame field is undefined for either TelevisionCoords argument then the
// Frame field in the result of the function is also undefined.
-func Diff(A, B TelevisionCoords, scanlinesPerFrame int) TelevisionCoords {
+func Diff(a, b TelevisionCoords, scanlinesPerFrame int) TelevisionCoords {
D := TelevisionCoords{
- Frame: A.Frame - B.Frame,
- Scanline: A.Scanline - B.Scanline,
- Clock: A.Clock - B.Clock,
+ Frame: a.Frame - b.Frame,
+ Scanline: a.Scanline - b.Scanline,
+ Clock: a.Clock - b.Clock,
}
if D.Clock < specification.ClksHBlank {
@@ -120,7 +120,7 @@ func Diff(A, B TelevisionCoords, scanlinesPerFrame int) TelevisionCoords {
// if the Frame field in either A or B is undefined then we can set the diff
// Frame field as undefined alse
- if A.Frame == FrameIsUndefined || B.Frame == FrameIsUndefined {
+ if a.Frame == FrameIsUndefined || b.Frame == FrameIsUndefined {
D.Frame = FrameIsUndefined
}
@@ -131,11 +131,22 @@ func Diff(A, B TelevisionCoords, scanlinesPerFrame int) TelevisionCoords {
//
// If the Frame field is undefined for the TelevisionCoords then the Frame field
// in the result of the function is also undefined.
-func Sum(A TelevisionCoords, scanlinesPerFrame int) int {
- if A.Frame == FrameIsUndefined {
- return (A.Scanline * specification.ClksScanline) + A.Clock
+func Sum(a TelevisionCoords, scanlinesPerFrame int) int {
+ if a.Frame == FrameIsUndefined {
+ return (a.Scanline * specification.ClksScanline) + a.Clock
}
numPerFrame := scanlinesPerFrame * specification.ClksScanline
- return (A.Frame * numPerFrame) + (A.Scanline * specification.ClksScanline) + A.Clock
+ return (a.Frame * numPerFrame) + (a.Scanline * specification.ClksScanline) + a.Clock
+}
+
+// Cmp returns 0 if A and B are equal, 1 if A > B and -1 if A < B
+func Cmp(a, b TelevisionCoords) int {
+ if Equal(a, b) {
+ return 0
+ }
+ if GreaterThan(a, b) {
+ return 1
+ }
+ return -1
}
diff --git a/rewind/comparison.go b/rewind/comparison.go
new file mode 100644
index 000000000..d00a5b744
--- /dev/null
+++ b/rewind/comparison.go
@@ -0,0 +1,52 @@
+// This file is part of Gopher2600.
+//
+// Gopher2600 is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Gopher2600 is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Gopher2600. If not, see .
+
+package rewind
+
+// ComparisonState is returned by GetComparisonState()
+type ComparisonState struct {
+ State *State
+ Locked bool
+}
+
+// GetComparisonState gets a reference to current comparison point
+func (r *Rewind) GetComparisonState() ComparisonState {
+ return ComparisonState{
+ State: r.comparison.snapshot(),
+ Locked: r.comparisonLocked,
+ }
+}
+
+// UpdateComparison points comparison to the current state
+func (r *Rewind) UpdateComparison() {
+ if r.comparisonLocked {
+ return
+ }
+ r.comparison = r.GetCurrentState()
+}
+
+// SetComparison points comparison to the supplied state
+func (r *Rewind) SetComparison(frame int) {
+ res := r.findFrameIndexExact(frame)
+ s := r.entries[res.nearestIdx]
+ if s != nil {
+ r.comparison = s.snapshot()
+ }
+}
+
+// LockComparison stops the comparison point from being updated
+func (r *Rewind) LockComparison(locked bool) {
+ r.comparisonLocked = locked
+}
diff --git a/rewind/rewind.go b/rewind/rewind.go
index 4767195da..3a2ff0397 100644
--- a/rewind/rewind.go
+++ b/rewind/rewind.go
@@ -105,13 +105,6 @@ func (s *State) String() string {
return fmt.Sprintf("f%03d", s.TV.GetCoords().Frame)
}
-// an overhead of two is required:
-// (1) to accommodate the next index required for effective appending
-// (2) we can't generate a screen for the first entry in the history, unless
-// it's a reset entry, so we do not allow the rewind system to move to that
-// frame.
-const overhead = 2
-
// Rewind contains a history of machine states for the emulation.
type Rewind struct {
emulation Emulation
@@ -139,6 +132,29 @@ type Rewind struct {
// the point at which new entries will be added
splice int
+ // user input is recorded separately to the machine state. userinput is
+ // re-inserted into the emulation via the playback mechanism during catchup
+ //
+ // the oldest userinput entry is no older than the oldest state in the
+ // entries field
+ //
+ // a great test case for userinput is Pitfall:
+ //
+ // WATCH WRITE $e1
+ // STICK LEFT RIGHT
+ // CPU
+ // PC=f913 A=00 X=02 Y=00 SP=ff SR=sv-BdIzc
+ // STEP BACK
+ // CPU
+ // PC=f911 A=00 X=02 Y=00 SP=ff SR=sv-BdIZc
+ //
+ // without userinput insertion STEP BACK will leave the PC register
+ // somewhere else. this is because the memory write to $e1 happens after the
+ // last frame snapshot and the write happens only in response to the STICK
+ // input. without userinput insertion this memorywrite is lost and so the
+ // desired state as a result of STEP BACK can not be recreated
+ userinput userinput
+
// pointer to the comparison point
comparison *State
comparisonLocked bool
@@ -151,6 +167,9 @@ type Rewind struct {
// a reset boundary has been detected
resetBoundaryNextFrame bool
+
+ // the current state of the emulation
+ emulationState govern.State
}
// NewRewind is the preferred method of initialisation for the Rewind type.
@@ -175,7 +194,7 @@ func NewRewind(emulation Emulation, runner Runner) (*Rewind, error) {
}
// AddTimelineCounter to the rewind system. Augments Timeline information that
-// would otherwisde be awkward to gather.
+// would otherwise be awkward to gather.
//
// Only one timeline counter can be used at any one time (ie. subsequent calls
// to AddTimelineCounter() will override previous calls.)
@@ -185,6 +204,13 @@ func (r *Rewind) AddTimelineCounter(ctr TimelineCounter) {
// initialise space for entries and reset rewind system.
func (r *Rewind) allocate() {
+ // an overhead of two is required when allocating space for the entries array:
+ // (1) to accommodate the next index required for effective appending
+ // (2) we can't generate a screen for the first entry in the history, unless
+ // it's a reset entry, so we do not allow the rewind system to move to that
+ // frame.
+ const overhead = 2
+
r.entries = make([]*State, r.Prefs.MaxEntries.Get().(int)+overhead)
r.reset(levelReset)
}
@@ -208,8 +234,6 @@ func (r *Rewind) reset(level snapshotLevel) {
r.entries[i] = nil
}
- r.comparison = nil
-
r.newFrame = false
r.resetBoundaryNextFrame = false
@@ -233,9 +257,11 @@ func (r *Rewind) reset(level snapshotLevel) {
// and as the second entry
r.append(r.snapshot(levelFrame))
+ // reset userinput
+ r.userinput.reset()
+
// first comparison is to the snapshot of the reset machine
r.comparison = r.entries[r.start]
-
}
// String outputs the entry information for the entire rewind history. The
@@ -268,53 +294,6 @@ func (r *Rewind) String() string {
return s.String()
}
-// Peephole outputs a short summary of the state of the rewind system centered
-// on the current splice value
-func (r *Rewind) Peephole() string {
- const peephole = 5
-
- var split bool
- peepi := r.splice - peephole
- if peepi < 0 {
- peepi += len(r.entries)
- split = true
- }
- peepj := r.splice + peephole
- if peepj >= len(r.entries) {
- peepj -= len(r.entries)
- if split {
- panic("length of entries in rewind is too short")
- }
- split = true
- }
-
- // build output string
- b := strings.Builder{}
-
- f := func(i, j int) {
- for k, e := range r.entries[i:j] {
- if k+i == r.splice {
- b.WriteString(fmt.Sprintf("(%s) ", e))
- } else {
- b.WriteString(fmt.Sprintf("%s ", e))
- }
- }
- }
-
- b.WriteString(fmt.Sprintf("[%03d] ", peepi))
- if split {
- f(peepi, len(r.entries))
- b.WriteString(fmt.Sprintf("| "))
- f(0, peepj)
- } else {
- b.WriteString(" ")
- f(peepi, peepj)
- }
- b.WriteString(fmt.Sprintf("[%03d]\n", peepj))
-
- return b.String()
-}
-
// the index of the last entry in the circular rewind history to be written to.
// the end index points to the *next* entry to be written to.
func (r *Rewind) lastEntryIdx() int {
@@ -339,6 +318,16 @@ func (r *Rewind) snapshot(level snapshotLevel) *State {
return snapshot(r.vcs, level)
}
+// SetEmulationState is called by the emulation whenever state changes
+func (r *Rewind) SetEmulationState(state govern.State) {
+ if r.emulationState != state && r.emulationState == govern.Running {
+ // make sure user input is not being inserted by the rewind system
+ r.vcs.Input.AttachPlayback(nil)
+ r.userinput.stopPlayback()
+ }
+ r.emulationState = state
+}
+
// RecordState should be called after every CPU instruction. A new state will
// be recorded if the current rewind policy agrees.
func (r *Rewind) RecordState() {
@@ -366,15 +355,17 @@ func (r *Rewind) RecordState() {
return
}
- // add an "execution" rewind state if the frame is not coincident with the
- // rewind frequency
fn := r.vcs.TV.GetCoords().Frame
- if fn%r.Prefs.Freq.Get().(int) != 0 {
+ if fn%r.Prefs.Freq.Get().(int) == 0 {
+ // create frame snapshot if frame number is coincident with frequency preference
+ r.append(r.snapshot(levelFrame))
+ } else {
+ // a temporary execution snapshot for interim frame numbers
r.append(r.snapshot(levelExecution))
- return
}
- r.append(r.snapshot(levelFrame))
+ // crop old entries from userinput list
+ r.userinput.crop(r.entries[r.splice].TV.GetCoords())
}
// RecordExecutionCoords records the coordinates of the current execution state.
@@ -449,6 +440,14 @@ func (r *Rewind) runFromStateToCoords(fromState *State, toCoords coords.Televisi
}
}
+ // start playback and if successful attach rewind to VCS input as a playback source
+ if r.userinput.startPlayback(fromState.TV.GetCoords()) {
+ err := r.vcs.Input.AttachPlayback(r)
+ if err != nil {
+ return fmt.Errorf("rewind: %w", err)
+ }
+ }
+
err := r.runner.CatchUpLoop(toCoords)
if err != nil {
return fmt.Errorf("rewind: %w", err)
@@ -506,9 +505,11 @@ type findResults struct {
func (r *Rewind) findFrameIndex(frame int) findResults {
// the number of frames to rerun. in the case of the debugger we like rerun
// an additional frame so that onion-skinning is correct
- frame--
- if r.emulation.Mode() == govern.ModeDebugger && frame > 0 {
+ if frame > 0 {
frame--
+ if r.emulation.Mode() == govern.ModeDebugger && frame > 0 {
+ frame--
+ }
}
return r.findFrameIndexExact(frame)
}
@@ -621,28 +622,6 @@ func (r *Rewind) GotoFrame(frame int) error {
return r.GotoCoords(coords.TelevisionCoords{Frame: frame, Clock: -specification.ClksHBlank})
}
-// UpdateComparison points comparison to the current state
-func (r *Rewind) UpdateComparison() {
- if r.comparisonLocked {
- return
- }
- r.comparison = r.GetCurrentState()
-}
-
-// SetComparison points comparison to the supplied state
-func (r *Rewind) SetComparison(frame int) {
- res := r.findFrameIndexExact(frame)
- s := r.entries[res.nearestIdx]
- if s != nil {
- r.comparison = s.snapshot()
- }
-}
-
-// LockComparison stops the comparison point from being updated
-func (r *Rewind) LockComparison(locked bool) {
- r.comparisonLocked = locked
-}
-
// NewFrame is in an implementation of television.FrameTrigger.
func (r *Rewind) NewFrame(frameInfo television.FrameInfo) error {
r.addTimelineEntry(frameInfo)
@@ -666,16 +645,49 @@ func (r *Rewind) GetCurrentState() *State {
return r.snapshot(levelTemporary)
}
-// ComparisonState is returned by GetComparisonState()
-type ComparisonState struct {
- State *State
- Locked bool
-}
+// Peephole outputs a short summary of the state of the rewind system centered
+// on the current splice value
+func (r *Rewind) Peephole() string {
+ const peephole = 5
+
+ var split bool
+ peepi := r.splice - peephole
+ if peepi < 0 {
+ peepi += len(r.entries)
+ split = true
+ }
+ peepj := r.splice + peephole
+ if peepj >= len(r.entries) {
+ peepj -= len(r.entries)
+ if split {
+ panic("length of entries in rewind is too short")
+ }
+ split = true
+ }
+
+ // build output string
+ b := strings.Builder{}
-// GetComparisonState gets a reference to current comparison point
-func (r *Rewind) GetComparisonState() ComparisonState {
- return ComparisonState{
- State: r.comparison.snapshot(),
- Locked: r.comparisonLocked,
+ f := func(i, j int) {
+ for k, e := range r.entries[i:j] {
+ if k+i == r.splice {
+ b.WriteString(fmt.Sprintf("(%s) ", e))
+ } else {
+ b.WriteString(fmt.Sprintf("%s ", e))
+ }
+ }
}
+
+ b.WriteString(fmt.Sprintf("[%03d] ", peepi))
+ if split {
+ f(peepi, len(r.entries))
+ b.WriteString(fmt.Sprintf("| "))
+ f(0, peepj)
+ } else {
+ b.WriteString(" ")
+ f(peepi, peepj)
+ }
+ b.WriteString(fmt.Sprintf("[%03d]\n", peepj))
+
+ return b.String()
}
diff --git a/rewind/userinput.go b/rewind/userinput.go
new file mode 100644
index 000000000..1a28074b2
--- /dev/null
+++ b/rewind/userinput.go
@@ -0,0 +1,109 @@
+// This file is part of Gopher2600.
+//
+// Gopher2600 is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Gopher2600 is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Gopher2600. If not, see .
+
+package rewind
+
+import (
+ "github.com/jetsetilly/gopher2600/hardware/riot/ports"
+ "github.com/jetsetilly/gopher2600/hardware/television/coords"
+)
+
+// the number of frames we must preserve in the userinput queue
+const userinputOverhead = 2
+
+type userinput struct {
+ queue []ports.TimedInputEvent
+ playback bool
+ idx int
+}
+
+func (u *userinput) reset() {
+ u.queue = u.queue[:0]
+ u.playback = false
+ u.idx = 0
+}
+
+func (u *userinput) crop(earliest coords.TelevisionCoords) {
+ if len(u.queue) == 0 {
+ return
+ }
+
+ var i int
+
+ for i < len(u.queue) {
+ if coords.GreaterThanOrEqual(u.queue[i].Time, earliest) {
+ break // for loop
+ }
+ i++
+ }
+
+ if i > 0 && i < len(u.queue) {
+ u.queue = u.queue[i:]
+ }
+}
+
+func (u *userinput) stopPlayback() {
+ u.playback = false
+}
+
+func (u *userinput) startPlayback(start coords.TelevisionCoords) bool {
+ // find first relevant playback entry. searching from the beginning. this
+ // could probably be improved
+ u.idx = 0
+ for u.idx < len(u.queue) {
+ if coords.GreaterThanOrEqual(u.queue[u.idx].Time, start) {
+ u.playback = true
+ return true
+ }
+ u.idx++
+ }
+ return false
+}
+
+// RecordEvent implements input.EventRecorder interface
+func (r *Rewind) RecordEvent(ev ports.TimedInputEvent) error {
+ if r.userinput.playback {
+ return nil
+ }
+ r.userinput.queue = append(r.userinput.queue, ev)
+ return nil
+}
+
+// GetPlayback implements input.EventPlayback interface
+func (r *Rewind) GetPlayback() (ports.TimedInputEvent, error) {
+ c := r.vcs.TV.GetCoords()
+
+ if r.userinput.idx >= len(r.userinput.queue) {
+ return ports.TimedInputEvent{
+ Time: c,
+ InputEvent: ports.InputEvent{
+ Ev: ports.NoEvent,
+ },
+ }, nil
+ }
+
+ if coords.Equal(c, r.userinput.queue[r.userinput.idx].Time) {
+ s := r.userinput.queue[r.userinput.idx]
+ r.userinput.idx++
+ return s, nil
+ }
+
+ return ports.TimedInputEvent{
+ Time: c,
+ InputEvent: ports.InputEvent{
+ Ev: ports.NoEvent,
+ },
+ }, nil
+}