mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 13:48:02 -04:00
d431974c76
the information used to create the loader is storied in the playback type rather than a created loader itself this is more flexible and gives us better control of when the loader is disposed of checking of cartridge hash must now be performed explicitely when the cartridge loader is created from the playback information
260 lines
7.5 KiB
Go
260 lines
7.5 KiB
Go
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
package recorder
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jetsetilly/gopher2600/digest"
|
|
"github.com/jetsetilly/gopher2600/hardware"
|
|
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
|
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
|
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
|
)
|
|
|
|
type playbackEntry struct {
|
|
event ports.TimedInputEvent
|
|
hash string
|
|
|
|
// the line in the recording file the playback event appears
|
|
line int
|
|
}
|
|
|
|
// Playback is used to reperform the user input recorded in a previously
|
|
// recorded file. It implements the ports.Playback interface.
|
|
type Playback struct {
|
|
transcript string
|
|
|
|
Cartridge string
|
|
Hash string
|
|
TVSpec string
|
|
|
|
sequence []playbackEntry
|
|
seqCt int
|
|
|
|
vcs *hardware.VCS
|
|
digest *digest.Video
|
|
|
|
// the last frame where an event occurs
|
|
endFrame int
|
|
}
|
|
|
|
func (plb Playback) String() string {
|
|
currFrame := plb.digest.GetCoords().Frame
|
|
return fmt.Sprintf("%d/%d (%.1f%%)", currFrame, plb.endFrame, 100*(float64(currFrame)/float64(plb.endFrame)))
|
|
}
|
|
|
|
// EndFrame returns true if emulation has gone past the last frame of the
|
|
// playback.
|
|
func (plb Playback) EndFrame() (bool, error) {
|
|
currFrame := plb.digest.GetCoords().Frame
|
|
if currFrame > plb.endFrame {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// NewPlayback is the preferred method of implementation for the Playback type.
|
|
//
|
|
// The returned playback must be attached to the VCS input system (with
|
|
// AttachToVCSInput() function) for it it to be useful.
|
|
//
|
|
// The integrityCheck flag should be true in most instances.
|
|
func NewPlayback(transcript string) (*Playback, error) {
|
|
var err error
|
|
|
|
plb := &Playback{
|
|
transcript: transcript,
|
|
sequence: make([]playbackEntry, 0),
|
|
}
|
|
|
|
tf, err := os.Open(transcript)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("playback: %w", err)
|
|
}
|
|
buffer, err := io.ReadAll(tf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("playback: %w", err)
|
|
}
|
|
err = tf.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
// convert file contents to an array of lines
|
|
lines := strings.Split(string(buffer), "\n")
|
|
|
|
// read header and perform validation checks
|
|
err = plb.readHeader(lines)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// loop through transcript and divide events according to the first field
|
|
// (the peripheral ID)
|
|
for i := numHeaderLines; i < len(lines)-1; i++ {
|
|
toks := strings.Split(lines[i], fieldSep)
|
|
|
|
// ignore lines that don't have enough fields
|
|
if len(toks) != numFields {
|
|
return nil, fmt.Errorf("playback: expected %d fields at line %d", numFields, i+1)
|
|
}
|
|
|
|
// create a new playbackEntry and convert tokens accordingly any errors in the transcript causes failure
|
|
entry := playbackEntry{line: i + 1}
|
|
|
|
// get PortID
|
|
n, err := strconv.Atoi(toks[fieldPortID])
|
|
if err != nil {
|
|
entry.event.Port = plugging.PortID(toks[fieldPortID])
|
|
} else {
|
|
// support for playback file versions before v1.2
|
|
switch n {
|
|
case -1:
|
|
entry.event.Port = plugging.PortUnplugged
|
|
case 1:
|
|
entry.event.Port = plugging.PortLeft
|
|
case 2:
|
|
entry.event.Port = plugging.PortRight
|
|
case 3:
|
|
entry.event.Port = plugging.PortPanel
|
|
default:
|
|
return nil, fmt.Errorf("playback: %s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldPortID+1], fieldSep)))
|
|
}
|
|
}
|
|
|
|
// no need to convert event field
|
|
entry.event.Ev = ports.Event(toks[fieldEvent])
|
|
|
|
// entry data is of ports.EventDataPlayback type. The ports
|
|
// implementation will handle parsing of this type.
|
|
entry.event.D = ports.EventDataPlayback(toks[fieldEventData])
|
|
|
|
entry.event.Time.Frame, err = strconv.Atoi(toks[fieldFrame])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("playback: %s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldFrame+1], fieldSep)))
|
|
}
|
|
|
|
// assuming that frames are listed in order in the file. update
|
|
// endFrame with the most recent frame every time
|
|
plb.endFrame = entry.event.Time.Frame
|
|
|
|
entry.event.Time.Scanline, err = strconv.Atoi(toks[fieldScanline])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("playback: %s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldScanline+1], fieldSep)))
|
|
}
|
|
|
|
entry.event.Time.Clock, err = strconv.Atoi(toks[fieldClock])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("playback: %s line %d, col %d", err, i+1, len(strings.Join(toks[:fieldClock+1], fieldSep)))
|
|
}
|
|
|
|
entry.hash = toks[fieldHash]
|
|
|
|
// add new entry to list of events in the correct playback sequence
|
|
plb.sequence = append(plb.sequence, entry)
|
|
}
|
|
|
|
return plb, nil
|
|
}
|
|
|
|
// AttachToVCSInput attaches the playback instance (an implementation of the
|
|
// playback interface) to all the input system of the VCS.
|
|
//
|
|
// Note that the VCS instance will be normalised as a result of this call.
|
|
func (plb *Playback) AttachToVCSInput(vcs *hardware.VCS) error {
|
|
// check we're working with correct information
|
|
if vcs == nil || vcs.TV == nil {
|
|
return fmt.Errorf("playback: no playback hardware available")
|
|
}
|
|
plb.vcs = vcs
|
|
|
|
var err error
|
|
|
|
// we want the machine in a known state. the easiest way to do this is to
|
|
// default the hardware preferences
|
|
vcs.Env.Normalise()
|
|
|
|
// validate header. keep it simple and disallow any difference in tv
|
|
// specification. some combinations may work but there's no compelling
|
|
// reason to figure that out just now.
|
|
if plb.vcs.TV.GetReqSpecID() != plb.TVSpec {
|
|
return fmt.Errorf("playback: recording was made with the %s TV spec. trying to playback with a TV spec of %s.", plb.TVSpec, vcs.TV.GetReqSpecID())
|
|
}
|
|
|
|
plb.digest, err = digest.NewVideo(plb.vcs.TV)
|
|
if err != nil {
|
|
return fmt.Errorf("playback: %w", err)
|
|
}
|
|
|
|
// attach playback to all VCS Input system
|
|
vcs.Input.AttachPlayback(plb)
|
|
|
|
return nil
|
|
}
|
|
|
|
// sentinal error returned by GetPlayback() if a hash error is encountered.
|
|
var PlaybackHashError = errors.New("unexpected input")
|
|
|
|
// GetPlayback returns an event and source PortID for an event occurring at the
|
|
// current TV frame/scanline/clock.
|
|
func (plb *Playback) GetPlayback() (ports.TimedInputEvent, error) {
|
|
// get current state of the television
|
|
c := plb.vcs.TV.GetCoords()
|
|
|
|
// we've reached the end of the list of events for this id
|
|
if plb.seqCt >= len(plb.sequence) {
|
|
return ports.TimedInputEvent{
|
|
Time: c,
|
|
InputEvent: ports.InputEvent{
|
|
Port: plugging.PortUnplugged,
|
|
Ev: ports.NoEvent,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// compare current state with the recording
|
|
entry := plb.sequence[plb.seqCt]
|
|
if coords.Equal(entry.event.Time, c) {
|
|
plb.seqCt++
|
|
if entry.hash != plb.digest.Hash() {
|
|
return ports.TimedInputEvent{
|
|
Time: c,
|
|
InputEvent: ports.InputEvent{
|
|
Port: plugging.PortUnplugged,
|
|
Ev: ports.NoEvent,
|
|
},
|
|
}, fmt.Errorf("playback: %w: line %d (frame %d)", PlaybackHashError, entry.line, c.Frame)
|
|
}
|
|
return entry.event, nil
|
|
}
|
|
|
|
// next event does not match
|
|
return ports.TimedInputEvent{
|
|
Time: c,
|
|
InputEvent: ports.InputEvent{
|
|
Port: plugging.PortUnplugged,
|
|
Ev: ports.NoEvent,
|
|
},
|
|
}, nil
|
|
}
|