Gopher2600/recorder/recorder.go

177 lines
4.6 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 (
"fmt"
"io"
"os"
"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"
)
// Recorder transcribes user input to a file. The recorded file is intended
// for future playback. The Recorder type implements the ports.EventRecorder
// interface.
type Recorder struct {
vcs *hardware.VCS
output *os.File
// using video digest only to test recording validity
digest *digest.Video
headerWritten bool
}
// NewRecorder is the preferred method of implementation for the FileRecorder
// type. Note that attaching of the Recorder to all the ports of the VCS
// (including the panel) is implicit in this function call.
//
// Note that the VCS instance will be normalised as a result of this call.
func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
var err error
// check we're working with correct information
if vcs == nil || vcs.TV == nil {
return nil, fmt.Errorf("recorder: hardware is not suitable for recording")
}
rec := &Recorder{
vcs: vcs,
}
// we want the machine in a known state. the easiest way to do this is to
// default the hardware preferences
vcs.Env.Normalise()
// vcs must be reset too
err = rec.vcs.Reset()
if err != nil {
return nil, fmt.Errorf("recorder: %w", err)
}
// attach recorder to vcs input system
vcs.Input.AddRecorder(rec)
// video digester for playback verification
rec.digest, err = digest.NewVideo(vcs.TV)
if err != nil {
return nil, fmt.Errorf("recorder: %w", err)
}
// open file
_, err = os.Stat(transcript)
if os.IsNotExist(err) {
rec.output, err = os.Create(transcript)
if err != nil {
return nil, fmt.Errorf("recorder: can't create file")
}
} else {
return nil, fmt.Errorf("recorder: file already exists")
}
// delay writing of header until the first call to transcribe. we're
// delaying this because we want to prepare the NewRecorder before we
// attach the cartridge but writing the header requires the cartridge to
// have been attached.
//
// the reason we want to create the NewRecorder before attaching the
// cartridge is because we want to catch the setup events caused by the
// attachement.
return rec, nil
}
// End flushes all remaining events to the output file and closes it.
func (rec *Recorder) End() error {
off := ports.TimedInputEvent{
Time: rec.vcs.TV.GetCoords(),
InputEvent: ports.InputEvent{
Port: plugging.PortPanel,
Ev: ports.PanelPowerOff,
},
}
// write the power off event to the transcript
err := rec.RecordEvent(off)
if err != nil {
return fmt.Errorf("recorder: %w", err)
}
err = rec.output.Close()
if err != nil {
return fmt.Errorf("recorder: %w", err)
}
return nil
}
// RecordEvent implements the ports.EventRecorder interface.
func (rec *Recorder) RecordEvent(inp ports.TimedInputEvent) error {
var err error
// write header if it's not been written already
if !rec.headerWritten {
err = rec.writeHeader()
if err != nil {
return fmt.Errorf("recorder: %w", err)
}
rec.headerWritten = true
}
// don't do anything if event is the NoEvent
if inp.Ev == ports.NoEvent {
return nil
}
// sanity checks
if rec.output == nil {
return fmt.Errorf("recorder: recording file is not open")
}
if rec.vcs == nil || rec.vcs.TV == nil {
return fmt.Errorf("recorder: hardware is not suitable for recording")
}
// convert data of nil type to the empty string
if inp.D == nil {
inp.D = ""
}
line := fmt.Sprintf("%v%s%v%s%v%s%v%s%v%s%v%s%v\n",
inp.Port, fieldSep,
inp.Ev, fieldSep,
inp.D, fieldSep,
inp.Time.Frame, fieldSep,
inp.Time.Scanline, fieldSep,
inp.Time.Clock, fieldSep,
rec.digest.Hash(),
)
n, err := io.WriteString(rec.output, line)
if err != nil {
return fmt.Errorf("recorder: %w", err)
}
if n != len(line) {
return fmt.Errorf("recorder: output truncated")
}
return nil
}