mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 13:48:02 -04:00
b48d487643
Makefile now sets the version string for the project. the version package tries to set a meaningful version string if the project is not built with the Makefile added VERSION command
843 lines
23 KiB
Go
843 lines
23 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 main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"os/signal"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
|
"github.com/jetsetilly/gopher2600/debugger"
|
|
"github.com/jetsetilly/gopher2600/debugger/govern"
|
|
"github.com/jetsetilly/gopher2600/debugger/terminal"
|
|
"github.com/jetsetilly/gopher2600/debugger/terminal/colorterm"
|
|
"github.com/jetsetilly/gopher2600/debugger/terminal/plainterm"
|
|
"github.com/jetsetilly/gopher2600/disassembly"
|
|
"github.com/jetsetilly/gopher2600/gui"
|
|
"github.com/jetsetilly/gopher2600/gui/sdlimgui"
|
|
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
|
"github.com/jetsetilly/gopher2600/logger"
|
|
"github.com/jetsetilly/gopher2600/modalflag"
|
|
"github.com/jetsetilly/gopher2600/performance"
|
|
"github.com/jetsetilly/gopher2600/recorder"
|
|
"github.com/jetsetilly/gopher2600/regression"
|
|
"github.com/jetsetilly/gopher2600/resources"
|
|
"github.com/jetsetilly/gopher2600/version"
|
|
)
|
|
|
|
// communication between the main goroutine and the launch goroutine.
|
|
type mainSync struct {
|
|
state chan stateRequest
|
|
|
|
// a created GUI will communicate thought these channels
|
|
gui chan guiControl
|
|
guiError chan error
|
|
}
|
|
|
|
// the stateRequest sent through the state channel in mainSync.
|
|
type stateReq string
|
|
|
|
// list of valid stateReq values.
|
|
const (
|
|
// main thread should end as soon as possible.
|
|
//
|
|
// takes optional int argument, indicating the status code.
|
|
reqQuit stateReq = "QUIT"
|
|
|
|
// reset interrupt signal handling. used when an alternative
|
|
// handler is more appropriate. for example, the playMode and Debugger
|
|
// package provide a mode specific handler.
|
|
//
|
|
// takes no arguments.
|
|
reqNoIntSig stateReq = "NOINTSIG"
|
|
|
|
// the gui creation function to run in the main goroutine. this is for GUIs
|
|
// that *need* to be run in the main OS thead (SDL, etc.)
|
|
//
|
|
// the only argument must be a guiCreate reference.
|
|
reqCreateGUI stateReq = "CREATEGUI"
|
|
)
|
|
|
|
type stateRequest struct {
|
|
req stateReq
|
|
args interface{}
|
|
}
|
|
|
|
// the gui create function. paired with reqCreateGUI state request.
|
|
type guiCreate func() (guiControl, error)
|
|
|
|
// guiControl defines the functions that a guiControl implementation must implement to be
|
|
// usable from the main goroutine.
|
|
type guiControl interface {
|
|
// cleanup resources used by the gui
|
|
Destroy()
|
|
|
|
// Service() should not pause or loop longer than necessary (if at all). It
|
|
// MUST ONLY by called as part of a larger loop from the main thread. It
|
|
// should service all gui events that are not safe to do in sub-threads.
|
|
//
|
|
// If the GUI framework does not require this sort of thread safety then
|
|
// there is no need for the Service() function to do anything.
|
|
Service()
|
|
}
|
|
|
|
func main() {
|
|
sync := &mainSync{
|
|
state: make(chan stateRequest),
|
|
gui: make(chan guiControl),
|
|
guiError: make(chan error),
|
|
}
|
|
|
|
// the value to use with os.Exit(). can be changed with reqQuit
|
|
// stateRequest
|
|
exitVal := 0
|
|
|
|
// ctrlc default handler. can be turned off with reqNoIntSig request
|
|
intChan := make(chan os.Signal, 1)
|
|
signal.Notify(intChan, os.Interrupt)
|
|
|
|
// launch program as a go routine. further communication is through
|
|
// the mainSync instance
|
|
go launch(sync)
|
|
|
|
// if there is no GUI then we should sleep so that the select channel loop
|
|
// doesn't go beserk
|
|
noGuiSleepPeriod, err := time.ParseDuration("5ms")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// loop until done is true. every iteration of the loop we listen for:
|
|
//
|
|
// 1. interrupt signals
|
|
// 2. new gui creation functions
|
|
// 3. state requests
|
|
// 4. (default) anything in the Service() function of the most recently created GUI
|
|
//
|
|
done := false
|
|
var gui guiControl
|
|
for !done {
|
|
select {
|
|
case <-intChan:
|
|
fmt.Println("\r")
|
|
done = true
|
|
|
|
case state := <-sync.state:
|
|
switch state.req {
|
|
case reqQuit:
|
|
done = true
|
|
if gui != nil {
|
|
gui.Destroy()
|
|
}
|
|
|
|
if state.args != nil {
|
|
if v, ok := state.args.(int); ok {
|
|
exitVal = v
|
|
} else {
|
|
panic(fmt.Sprintf("cannot convert %s arguments into int", reqQuit))
|
|
}
|
|
}
|
|
|
|
case reqNoIntSig:
|
|
signal.Reset(os.Interrupt)
|
|
if state.args != nil {
|
|
panic(fmt.Sprintf("%s does not accept any arguments", reqNoIntSig))
|
|
}
|
|
|
|
case reqCreateGUI:
|
|
var err error
|
|
|
|
// destroy existing gui
|
|
if gui != nil {
|
|
gui.Destroy()
|
|
}
|
|
|
|
gui, err = state.args.(guiCreate)()
|
|
if err != nil {
|
|
sync.guiError <- err
|
|
|
|
// gui is a variable of type interface. nil doesn't work as you
|
|
// might expect with interfaces. for instance, even though the
|
|
// following outputs "<nil>":
|
|
//
|
|
// fmt.Println(gui)
|
|
//
|
|
// the following equation print false:
|
|
//
|
|
// fmt.Println(gui == nil)
|
|
//
|
|
// as to the reason why gui does not equal nil, even though
|
|
// the creator() function returns nil? well, you tell me.
|
|
gui = nil
|
|
} else {
|
|
sync.gui <- gui
|
|
}
|
|
}
|
|
|
|
default:
|
|
// if an instance of gui.Events has been sent to us via sync.events
|
|
// then call Service(). otherwise, sleep for a very short period
|
|
if gui != nil {
|
|
gui.Service()
|
|
} else {
|
|
time.Sleep(noGuiSleepPeriod)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Print("\r")
|
|
os.Exit(exitVal)
|
|
}
|
|
|
|
// launch is called from main() as a goroutine. uses mainSync instance to
|
|
// indicate gui creation and to quit.
|
|
func launch(sync *mainSync) {
|
|
logger.Log("runtime", fmt.Sprintf("number of cores being used: %d", runtime.NumCPU()))
|
|
|
|
// we generate random numbers in some places. seed the generator with the
|
|
// current time
|
|
rand.Seed(int64(time.Now().Nanosecond()))
|
|
|
|
md := &modalflag.Modes{Output: os.Stdout}
|
|
md.NewArgs(os.Args[1:])
|
|
md.NewMode()
|
|
md.AddSubModes("RUN", "PLAY", "DEBUG", "DISASM", "PERFORMANCE", "REGRESS", "VERSION")
|
|
|
|
p, err := md.Parse()
|
|
switch p {
|
|
case modalflag.ParseHelp:
|
|
sync.state <- stateRequest{req: reqQuit}
|
|
return
|
|
|
|
case modalflag.ParseError:
|
|
fmt.Printf("* error: %v\n", err)
|
|
sync.state <- stateRequest{req: reqQuit, args: 10}
|
|
return
|
|
}
|
|
|
|
switch md.Mode() {
|
|
case "RUN":
|
|
fallthrough
|
|
|
|
case "PLAY":
|
|
err = emulate(govern.ModePlay, md, sync)
|
|
|
|
case "DEBUG":
|
|
err = emulate(govern.ModeDebugger, md, sync)
|
|
|
|
case "DISASM":
|
|
err = disasm(md)
|
|
|
|
case "PERFORMANCE":
|
|
err = perform(md, sync)
|
|
|
|
case "REGRESS":
|
|
err = regress(md, sync)
|
|
|
|
case "VERSION":
|
|
err = showVersion(md)
|
|
}
|
|
|
|
if err != nil {
|
|
// swallow power off error messages. send quit signal with return value of 20 instead
|
|
if !errors.Is(err, ports.PowerOff) {
|
|
fmt.Printf("* error in %s mode: %s\n", md.String(), err)
|
|
sync.state <- stateRequest{req: reqQuit, args: 20}
|
|
return
|
|
}
|
|
}
|
|
|
|
sync.state <- stateRequest{req: reqQuit}
|
|
}
|
|
|
|
const defaultInitScript = "debuggerInit"
|
|
|
|
// emulate is the main emulation launch function, shared by play and debug
|
|
// modes. the other modes initialise and run the emulation differently.
|
|
func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) error {
|
|
// start new commandline mode. to this we'll add the command line arguments
|
|
// that are specific to the emulation mode (it's unfortunate that mode is
|
|
// used to describe two separate concepts but they really have nothing to
|
|
// do with one another).
|
|
md.NewMode()
|
|
|
|
// prepare the path to the initialisation script used by the debugger. we
|
|
// can name the file in the defaultInitScript const declaration but the
|
|
// construction of the path is platform sensitive so we must do it here
|
|
defInitScript, err := resources.JoinPath(defaultInitScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// new CommandLineOptions instance. this type collates the individual
|
|
// options that can be set by the command line
|
|
opts := debugger.NewCommandLineOptions()
|
|
|
|
// arguments common to both play and debugging modes
|
|
opts.Log = md.AddBool("log", false, "echo debugging log to stdout")
|
|
opts.Spec = md.AddString("tv", "AUTO", "television specification: AUTO, NTSC, PAL, PAL60, SECAM")
|
|
opts.FpsCap = md.AddBool("fpscap", true, "cap FPS to emulated match TV")
|
|
opts.Multiload = md.AddInt("multiload", -1, "force multiload byte (supercharger only; 0 to 255)")
|
|
opts.Mapping = md.AddString("mapping", "AUTO", "force use of cartridge mapping")
|
|
opts.Left = md.AddString("left", "AUTO", "left player port: AUTO, STICK, PADDLE, KEYPAD, GAMEPAD")
|
|
opts.Right = md.AddString("right", "AUTO", "right player port: AUTO, STICK, PADDLE, KEYPAD, GAMEPAD, SAVEKEY, ATARIVOX")
|
|
opts.Profile = md.AddString("profile", "none", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
|
|
opts.ELF = md.AddString("elf", "", "path to corresponding ELF file for binary. only valid for some coprocessor supporting ROMs.")
|
|
|
|
// playmode specific arguments
|
|
if emulationMode == govern.ModePlay {
|
|
opts.ComparisonROM = md.AddString("comparisonROM", "", "ROM to run in parallel for comparison")
|
|
opts.ComparisonPrefs = md.AddString("comparisonPrefs", "", "preferences for comparison emulation")
|
|
opts.Record = md.AddBool("record", false, "record user input to a file")
|
|
opts.PlaybackCheckROM = md.AddBool("playbackCheckROM", true, "whether to check ROM hash on playback")
|
|
opts.PatchFile = md.AddString("patch", "", "patch to apply to main emulation (not playback files)")
|
|
opts.Wav = md.AddBool("wav", false, "record audio to wav file")
|
|
opts.NoEject = md.AddBool("noeject", false, "a cartridge must be attached at all times. emulator will quit if not")
|
|
opts.Macro = md.AddString("macro", "", "macro file to be run on trigger")
|
|
}
|
|
|
|
// debugger specific arguments
|
|
if emulationMode == govern.ModeDebugger {
|
|
opts.InitScript = md.AddString("initscript", defInitScript, "script to run on debugger start")
|
|
opts.TermType = md.AddString("term", "IMGUI", "terminal type to use in debug mode: IMGUI, COLOR, PLAIN")
|
|
} else {
|
|
// non debugger emulation is always of type IMGUI
|
|
tt := "IMGUI"
|
|
opts.TermType = &tt
|
|
}
|
|
|
|
// parse arguments
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
// check remaining arguments. if there are any outstanding arguments to
|
|
// process then the user has made a mistake
|
|
if len(md.RemainingArgs()) > 1 {
|
|
return fmt.Errorf("too many arguments for %s mode", md)
|
|
}
|
|
|
|
// turn logging on by setting the echo function. events are still logged
|
|
// and available via the debugger but will not be "echoed" to the terminal,
|
|
// unless this option is on
|
|
if *opts.Log {
|
|
logger.SetEcho(os.Stdout, true)
|
|
} else {
|
|
logger.SetEcho(nil, false)
|
|
}
|
|
|
|
// turn off fallback ctrl-c handling. this so that the debugger can handle
|
|
// quit events more gracefully
|
|
//
|
|
// we must do this before creating the emulation or we'll just end up
|
|
// turning the emulation's interrupt handler off
|
|
sync.state <- stateRequest{req: reqNoIntSig}
|
|
|
|
// prepare new debugger, supplying a debugger.CreateUserInterface function.
|
|
// this function will be called by NewDebugger() and in turn will send a
|
|
// GUI create message to the main goroutine
|
|
dbg, err := debugger.NewDebugger(opts, func(e *debugger.Debugger) (gui.GUI, terminal.Terminal, error) {
|
|
var term terminal.Terminal
|
|
var scr gui.GUI
|
|
|
|
// create GUI as appropriate
|
|
if *opts.TermType == "IMGUI" {
|
|
sync.state <- stateRequest{req: reqCreateGUI,
|
|
args: guiCreate(func() (guiControl, error) {
|
|
return sdlimgui.NewSdlImgui(e)
|
|
}),
|
|
}
|
|
|
|
// wait for creator result
|
|
select {
|
|
case g := <-sync.gui:
|
|
scr = g.(gui.GUI)
|
|
case err := <-sync.guiError:
|
|
return nil, nil, err
|
|
}
|
|
|
|
// if gui implements the terminal.Broker interface use that terminal
|
|
// as a preference
|
|
if b, ok := scr.(terminal.Broker); ok {
|
|
term = b.GetTerminal()
|
|
}
|
|
} else {
|
|
// no GUI specified so we use a stub
|
|
scr = gui.Stub{}
|
|
}
|
|
|
|
// if the GUI does not supply a terminal then use a color or plain terminal
|
|
// as a fallback
|
|
if term == nil {
|
|
switch strings.ToUpper(*opts.TermType) {
|
|
default:
|
|
fmt.Printf("! unknown terminal type (%s) defaulting to plain\n", *opts.TermType)
|
|
fallthrough
|
|
case "PLAIN":
|
|
term = &plainterm.PlainTerminal{}
|
|
case "COLOR":
|
|
term = &colorterm.ColorTerminal{}
|
|
}
|
|
}
|
|
|
|
return scr, term, nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// set up a launch function. this function is called either directly or via
|
|
// a call to performance.RunProfiler()
|
|
dbgLaunch := func() error {
|
|
switch emulationMode {
|
|
case govern.ModeDebugger:
|
|
err := dbg.StartInDebugMode(md.GetArg(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case govern.ModePlay:
|
|
err := dbg.StartInPlayMode(md.GetArg(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// check for profiling option and either run the launch function (prepared
|
|
// above) via the performance.RunProfiler() function or directly
|
|
prf, err := performance.ParseProfileString(*opts.Profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if prf == performance.ProfileNone {
|
|
// no profile required so run dbgLaunch() function as normal
|
|
err := dbgLaunch()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// filename argument for RunProfiler
|
|
s := ""
|
|
switch emulationMode {
|
|
case govern.ModeDebugger:
|
|
s = "debugger"
|
|
case govern.ModePlay:
|
|
s = "play"
|
|
}
|
|
|
|
// if profile generation has been requested then pass the dbgLaunch()
|
|
// function prepared above, through the RunProfiler() function
|
|
err := performance.RunProfiler(prf, s, dbgLaunch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func disasm(md *modalflag.Modes) error {
|
|
md.NewMode()
|
|
|
|
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
|
|
bytecode := md.AddBool("bytecode", false, "include bytecode in disassembly")
|
|
bank := md.AddInt("bank", -1, "show disassembly for a specific bank")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
return fmt.Errorf("2600 cartridge required for %s mode", md)
|
|
case 1:
|
|
attr := disassembly.ColumnAttr{
|
|
ByteCode: *bytecode,
|
|
Label: true,
|
|
Cycles: true,
|
|
}
|
|
|
|
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cartload.Close()
|
|
|
|
dsm, err := disassembly.FromCartridge(cartload)
|
|
if err != nil {
|
|
// print what disassembly output we do have
|
|
if dsm != nil {
|
|
// ignore any further errors
|
|
_ = dsm.Write(md.Output, attr)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// output entire disassembly or just a specific bank
|
|
if *bank < 0 {
|
|
err = dsm.Write(md.Output, attr)
|
|
} else {
|
|
err = dsm.WriteBank(md.Output, attr, *bank)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("too many arguments for %s mode", md)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func perform(md *modalflag.Modes, sync *mainSync) error {
|
|
md.NewMode()
|
|
|
|
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
|
|
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60, SECAM")
|
|
uncapped := md.AddBool("uncapped", true, "run perfomance with no FPS cap")
|
|
duration := md.AddString("duration", "5s", "run duration (note: there is a 2s overhead)")
|
|
profile := md.AddString("profile", "NONE", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
|
|
log := md.AddBool("log", false, "echo debugging log to stdout")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
// set debugging log echo
|
|
if *log {
|
|
logger.SetEcho(os.Stdout, true)
|
|
} else {
|
|
logger.SetEcho(nil, false)
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
return fmt.Errorf("2600 cartridge required for %s mode", md)
|
|
case 1:
|
|
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cartload.Close()
|
|
|
|
// check for profiling options
|
|
p, err := performance.ParseProfileString(*profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// run performance check
|
|
err = performance.Check(md.Output, p, cartload, *spec, *uncapped, *duration)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// deliberately not saving gui preferences because we don't want any
|
|
// changes to the performance window impacting the play mode
|
|
|
|
default:
|
|
return fmt.Errorf("too many arguments for %s mode", md)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func regress(md *modalflag.Modes, sync *mainSync) error {
|
|
md.NewMode()
|
|
md.AddSubModes("RUN", "LIST", "DELETE", "ADD", "REDUX", "CLEANUP")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
switch md.Mode() {
|
|
case "RUN":
|
|
md.NewMode()
|
|
|
|
// no additional arguments
|
|
verbose := md.AddBool("verbose", false, "output more detail (eg. error messages)")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
err = regression.RegressRun(md.Output, *verbose, md.RemainingArgs())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case "LIST":
|
|
md.NewMode()
|
|
|
|
// no additional arguments
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
err := regression.RegressList(md.Output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("no additional arguments required for %s mode", md)
|
|
}
|
|
|
|
case "DELETE":
|
|
md.NewMode()
|
|
|
|
answerYes := md.AddBool("yes", false, "answer yes to confirmation")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
return fmt.Errorf("database key required for %s mode", md)
|
|
case 1:
|
|
|
|
// use stdin for confirmation unless "yes" flag has been sent
|
|
var confirmation io.Reader
|
|
if *answerYes {
|
|
confirmation = &yesReader{}
|
|
} else {
|
|
confirmation = os.Stdin
|
|
}
|
|
|
|
err := regression.RegressDelete(md.Output, confirmation, md.GetArg(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("only one entry can be deleted at at time")
|
|
}
|
|
|
|
case "ADD":
|
|
return regressAdd(md)
|
|
|
|
case "REDUX":
|
|
md.NewMode()
|
|
|
|
answerYes := md.AddBool("yes", false, "always answer yes to confirmation")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
var confirmation io.Reader
|
|
if *answerYes {
|
|
confirmation = &yesReader{}
|
|
} else {
|
|
confirmation = os.Stdin
|
|
}
|
|
|
|
return regression.RegressRedux(md.Output, confirmation)
|
|
|
|
case "CLEANUP":
|
|
md.NewMode()
|
|
|
|
answerYes := md.AddBool("yes", false, "always answer yes to confirmation")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
var confirmation io.Reader
|
|
if *answerYes {
|
|
confirmation = &yesReader{}
|
|
} else {
|
|
confirmation = os.Stdin
|
|
}
|
|
|
|
return regression.RegressCleanup(md.Output, confirmation)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func regressAdd(md *modalflag.Modes) error {
|
|
md.NewMode()
|
|
|
|
mode := md.AddString("mode", "", "type of regression entry")
|
|
notes := md.AddString("notes", "", "additional annotation for the database")
|
|
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping [non-playback]")
|
|
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60, SECAM [non-playback]")
|
|
numframes := md.AddInt("frames", 10, "number of frames to run [non-playback]")
|
|
state := md.AddString("state", "", "record emulator state at every CPU step [non-playback]")
|
|
log := md.AddBool("log", false, "echo debugging log to stdout")
|
|
|
|
md.AdditionalHelp(
|
|
`The regression test to be added can be the path to a cartridge file or a previously
|
|
recorded playback file. For playback files, the flags marked [non-playback] do not make
|
|
sense and will be ignored.
|
|
|
|
Available modes are VIDEO, PLAYBACK and LOG. If not mode is explicitly given then
|
|
VIDEO will be used for ROM files and PLAYBACK will be used for playback recordings.
|
|
|
|
Value for the -state flag can be one of TV, PORTS, TIMER, CPU and can be used
|
|
with the default VIDEO mode.
|
|
|
|
The -log flag intructs the program to echo the log to the console. Do not confuse this
|
|
with the LOG mode. Note that asking for log output will suppress regression progress meters.`)
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
// set debugging log echo
|
|
if *log {
|
|
logger.SetEcho(os.Stdout, true)
|
|
md.Output = &nopWriter{}
|
|
} else {
|
|
logger.SetEcho(nil, false)
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
return fmt.Errorf("2600 cartridge or playback file required for %s mode", md)
|
|
case 1:
|
|
var reg regression.Regressor
|
|
|
|
if *mode == "" {
|
|
if err := recorder.IsPlaybackFile(md.GetArg(0)); err == nil {
|
|
*mode = "PLAYBACK"
|
|
} else if !errors.Is(err, recorder.NotAPlaybackFile) {
|
|
return err
|
|
} else {
|
|
*mode = "VIDEO"
|
|
}
|
|
}
|
|
|
|
switch strings.ToUpper(*mode) {
|
|
case "VIDEO":
|
|
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cartload.Close()
|
|
|
|
statetype, err := regression.NewStateType(*state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reg = ®ression.VideoRegression{
|
|
CartLoad: cartload,
|
|
TVtype: strings.ToUpper(*spec),
|
|
NumFrames: *numframes,
|
|
State: statetype,
|
|
Notes: *notes,
|
|
}
|
|
case "PLAYBACK":
|
|
// check and warn if unneeded arguments have been specified
|
|
md.Visit(func(flg string) {
|
|
if flg == "frames" {
|
|
fmt.Printf("! ignored %s flag when adding playback entry\n", flg)
|
|
}
|
|
})
|
|
|
|
reg = ®ression.PlaybackRegression{
|
|
Script: md.GetArg(0),
|
|
Notes: *notes,
|
|
}
|
|
case "LOG":
|
|
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cartload.Close()
|
|
|
|
reg = ®ression.LogRegression{
|
|
CartLoad: cartload,
|
|
TVtype: strings.ToUpper(*spec),
|
|
NumFrames: *numframes,
|
|
Notes: *notes,
|
|
}
|
|
}
|
|
|
|
err := regression.RegressAdd(md.Output, reg)
|
|
if err != nil {
|
|
// using carriage return (without newline) at beginning of error
|
|
// message because we want to overwrite the last output from
|
|
// RegressAdd()
|
|
return fmt.Errorf("\rerror adding regression test: %w", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("regression tests can only be added one at a time")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func showVersion(md *modalflag.Modes) error {
|
|
md.NewMode()
|
|
revision := md.AddBool("revision", false,
|
|
"display revision information from version control system (if available)")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(version.Version)
|
|
|
|
if *revision {
|
|
fmt.Println(version.Revision)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// nopWriter is an empty writer.
|
|
type nopWriter struct{}
|
|
|
|
func (*nopWriter) Write(p []byte) (n int, err error) {
|
|
return 0, nil
|
|
}
|
|
|
|
// yesReader always returns 'y' when it is read.
|
|
type yesReader struct{}
|
|
|
|
func (*yesReader) Read(p []byte) (n int, err error) {
|
|
p[0] = 'y'
|
|
return 1, nil
|
|
}
|