mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 20:18:20 -04:00
177 lines
4.6 KiB
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
|
|
}
|