mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
960 lines
24 KiB
Go
960 lines
24 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 (
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
|
"github.com/jetsetilly/gopher2600/curated"
|
|
"github.com/jetsetilly/gopher2600/debugger"
|
|
"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/television"
|
|
"github.com/jetsetilly/gopher2600/hiscore"
|
|
"github.com/jetsetilly/gopher2600/logger"
|
|
"github.com/jetsetilly/gopher2600/modalflag"
|
|
"github.com/jetsetilly/gopher2600/paths"
|
|
"github.com/jetsetilly/gopher2600/performance"
|
|
"github.com/jetsetilly/gopher2600/playmode"
|
|
"github.com/jetsetilly/gopher2600/recorder"
|
|
"github.com/jetsetilly/gopher2600/regression"
|
|
"github.com/jetsetilly/gopher2600/statsview"
|
|
"github.com/jetsetilly/gopher2600/wavwriter"
|
|
)
|
|
|
|
const defaultInitScript = "debuggerInit"
|
|
|
|
type stateReq = string
|
|
|
|
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"
|
|
)
|
|
|
|
type stateRequest struct {
|
|
req stateReq
|
|
args interface{}
|
|
}
|
|
|
|
// GuiCreator facilitates the creation, servicing and destruction of GUIs
|
|
// that need to be run in the main thread.
|
|
//
|
|
// Note that there is no Create() function because we need the freedom to
|
|
// create the GUI how we want. Instead the creator is a channel which accepts
|
|
// a function that returns an instance of GuiCreator.
|
|
type GuiCreator interface {
|
|
// cleanup resources used by the gui
|
|
Destroy(io.Writer)
|
|
|
|
// 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()
|
|
}
|
|
|
|
// communication between the main() function and the launch() function. this is
|
|
// required because many gui solutions (notably SDL) require window event
|
|
// handling (including creation) to occur on the main thread.
|
|
type mainSync struct {
|
|
state chan stateRequest
|
|
creator chan func() (GuiCreator, error)
|
|
|
|
// the result of creator will be returned on either of these two channels.
|
|
creation chan GuiCreator
|
|
creationError chan error
|
|
}
|
|
|
|
func main() {
|
|
sync := &mainSync{
|
|
state: make(chan stateRequest),
|
|
creator: make(chan func() (GuiCreator, error)),
|
|
creation: make(chan GuiCreator),
|
|
creationError: 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)
|
|
|
|
// loop until done is true. every iteration of the loop we listen for:
|
|
//
|
|
// 1. interrupt signals
|
|
// 2. new gui creation functions
|
|
// 3. state requests
|
|
// 3. anything in the Service() function of the most recently created GUI
|
|
//
|
|
done := false
|
|
var gui GuiCreator
|
|
for !done {
|
|
select {
|
|
case <-intChan:
|
|
fmt.Println("\r")
|
|
done = true
|
|
|
|
case creator := <-sync.creator:
|
|
var err error
|
|
|
|
// destroy existing gui
|
|
if gui != nil {
|
|
gui.Destroy(os.Stderr)
|
|
}
|
|
|
|
gui, err = creator()
|
|
if err != nil {
|
|
sync.creationError <- 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.creation <- gui
|
|
}
|
|
|
|
case state := <-sync.state:
|
|
switch state.req {
|
|
case reqQuit:
|
|
done = true
|
|
if gui != nil {
|
|
gui.Destroy(os.Stderr)
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
default:
|
|
// if an instance of gui.Events has been sent to us via sync.events
|
|
// then call Service()
|
|
if gui != nil {
|
|
gui.Service()
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// 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", "HISCORE")
|
|
|
|
p, err := md.Parse()
|
|
switch p {
|
|
case modalflag.ParseHelp:
|
|
sync.state <- stateRequest{req: reqQuit}
|
|
return
|
|
|
|
case modalflag.ParseError:
|
|
fmt.Printf("* error: %v\n", err)
|
|
// 10
|
|
sync.state <- stateRequest{req: reqQuit, args: 10}
|
|
return
|
|
}
|
|
|
|
switch md.Mode() {
|
|
case "RUN":
|
|
fallthrough
|
|
|
|
case "PLAY":
|
|
err = play(md, sync)
|
|
|
|
case "DEBUG":
|
|
err = debug(md, sync)
|
|
|
|
case "DISASM":
|
|
err = disasm(md)
|
|
|
|
case "PERFORMANCE":
|
|
err = perform(md, sync)
|
|
|
|
case "REGRESS":
|
|
err = regress(md, sync)
|
|
|
|
case "HISCORE":
|
|
err = hiscoreServer(md)
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Printf("* error in %s mode: %s\n", md.String(), err)
|
|
sync.state <- stateRequest{req: reqQuit, args: 20}
|
|
return
|
|
}
|
|
|
|
sync.state <- stateRequest{req: reqQuit}
|
|
}
|
|
|
|
func play(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")
|
|
fullScreen := md.AddBool("fullscreen", false, "start in fullscreen mode")
|
|
fpsCap := md.AddBool("fpscap", true, "cap fps to specification")
|
|
record := md.AddBool("record", false, "record user input to a file")
|
|
wav := md.AddString("wav", "", "record audio to wav file")
|
|
patchFile := md.AddString("patch", "", "patch file to apply (cartridge args only)")
|
|
hiscore := md.AddBool("hiscore", false, "contact hiscore server [EXPERIMENTAL]")
|
|
log := md.AddBool("log", false, "echo debugging log to stdout")
|
|
useSavekey := md.AddBool("savekey", false, "use savekey in player 1 port")
|
|
profile := md.AddString("profile", "none", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
|
|
|
|
stats := &[]bool{false}[0]
|
|
if statsview.Available() {
|
|
stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address))
|
|
}
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
// set debugging log echo
|
|
if *log {
|
|
logger.SetEcho(os.Stdout)
|
|
} else {
|
|
logger.SetEcho(nil)
|
|
}
|
|
|
|
if *stats {
|
|
statsview.Launch(os.Stdout)
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
return fmt.Errorf("2600 cartridge required for %s mode", md)
|
|
case 1:
|
|
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
defer cartload.Close()
|
|
|
|
tv, err := television.NewTelevision(*spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tv.End()
|
|
|
|
// add wavwriter mixer if wav argument has been specified
|
|
if *wav != "" {
|
|
aw, err := wavwriter.New(*wav)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tv.AddAudioMixer(aw)
|
|
}
|
|
|
|
// create gui
|
|
sync.creator <- func() (GuiCreator, error) {
|
|
return sdlimgui.NewSdlImgui(tv)
|
|
}
|
|
|
|
// wait for creator result
|
|
var scr gui.GUI
|
|
select {
|
|
case g := <-sync.creation:
|
|
scr = g.(gui.GUI)
|
|
|
|
case err := <-sync.creationError:
|
|
return err
|
|
}
|
|
|
|
// set fps cap
|
|
tv.SetFPSCap(*fpsCap)
|
|
scr.SetFeature(gui.ReqVSync, *fpsCap)
|
|
|
|
// set full screen
|
|
scr.SetFeature(gui.ReqFullScreen, *fullScreen)
|
|
|
|
// turn off fallback ctrl-c handling. this so that the playmode can
|
|
// end playback recordings gracefully
|
|
sync.state <- stateRequest{req: reqNoIntSig}
|
|
|
|
// check for profiling options
|
|
p, err := performance.ParseProfileString(*profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// set up a running function
|
|
playLaunch := func() error {
|
|
err = playmode.Play(tv, scr, *record, cartload, *patchFile, *hiscore, *useSavekey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if p == performance.ProfileNone {
|
|
err = playLaunch()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// if profile generation has been requested then pass the
|
|
// playLaunch() function prepared above, through the RunProfiler()
|
|
// function
|
|
err := performance.RunProfiler(p, "play", playLaunch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if *record {
|
|
fmt.Println("! recording completed")
|
|
}
|
|
|
|
// set ending state
|
|
err = scr.SetFeature(gui.ReqState, gui.StateEnding)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("too many arguments for %s mode", md)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func debug(md *modalflag.Modes, sync *mainSync) error {
|
|
md.NewMode()
|
|
|
|
defInitScript, err := paths.ResourcePath("", defaultInitScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
|
|
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL")
|
|
termType := md.AddString("term", "IMGUI", "terminal type to use in debug mode: IMGUI, COLOR, PLAIN")
|
|
initScript := md.AddString("initscript", defInitScript, "script to run on debugger start")
|
|
useSavekey := md.AddBool("savekey", false, "use savekey in player 1 port")
|
|
profile := md.AddString("profile", "none", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
|
|
|
|
stats := &[]bool{false}[0]
|
|
if statsview.Available() {
|
|
stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address))
|
|
}
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
if *stats {
|
|
statsview.Launch(os.Stdout)
|
|
}
|
|
|
|
tv, err := television.NewTelevision(*spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tv.End()
|
|
|
|
var term terminal.Terminal
|
|
var scr gui.GUI
|
|
|
|
// create gui
|
|
if *termType == "IMGUI" {
|
|
sync.creator <- func() (GuiCreator, error) {
|
|
return sdlimgui.NewSdlImgui(tv)
|
|
}
|
|
|
|
// wait for creator result
|
|
select {
|
|
case g := <-sync.creation:
|
|
scr = g.(gui.GUI)
|
|
case err := <-sync.creationError:
|
|
return 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 {
|
|
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(*termType) {
|
|
default:
|
|
fmt.Printf("! unknown terminal type (%s) defaulting to plain\n", *termType)
|
|
fallthrough
|
|
case "PLAIN":
|
|
term = &plainterm.PlainTerminal{}
|
|
case "COLOR":
|
|
term = &colorterm.ColorTerminal{}
|
|
}
|
|
}
|
|
|
|
// turn off fallback ctrl-c handling. this so that the debugger can handle
|
|
// quit events with a confirmation request. it also allows the debugger to
|
|
// use ctrl-c events to interrupt execution of the emulation without
|
|
// quitting the debugger itself
|
|
sync.state <- stateRequest{req: reqNoIntSig}
|
|
|
|
// prepare new debugger instance
|
|
dbg, err := debugger.NewDebugger(tv, scr, term, *useSavekey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
return fmt.Errorf("2600 cartridge required for %s mode", md)
|
|
|
|
case 1:
|
|
// check for profiling options
|
|
p, err := performance.ParseProfileString(*profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// set up a launch function
|
|
dbgLaunch := func() error {
|
|
err := dbg.Start(*initScript, cartridgeloader.NewLoader(md.GetArg(0), *mapping))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if p == performance.ProfileNone {
|
|
// no profile required so run dbgLaunch() function as normal
|
|
err := dbgLaunch()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// if profile generation has been requested then pass the dbgLaunch()
|
|
// function prepared above, through the RunProfiler() function
|
|
err := performance.RunProfiler(p, "debugger", dbgLaunch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("too many arguments for %s mode", md)
|
|
}
|
|
|
|
// set ending state
|
|
err = scr.SetFeature(gui.ReqState, gui.StateEnding)
|
|
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 := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
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")
|
|
display := md.AddBool("display", false, "display TV output")
|
|
fpsCap := md.AddBool("fpscap", true, "cap FPS to specification (only valid if -display=true)")
|
|
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)
|
|
} else {
|
|
logger.SetEcho(nil)
|
|
}
|
|
|
|
switch len(md.RemainingArgs()) {
|
|
case 0:
|
|
return fmt.Errorf("2600 cartridge required for %s mode", md)
|
|
case 1:
|
|
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
defer cartload.Close()
|
|
|
|
tv, err := television.NewTelevision(*spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tv.End()
|
|
|
|
// fpscap for tv (see below for gui vsync option)
|
|
tv.SetFPSCap(*fpsCap)
|
|
|
|
// GUI instance if required
|
|
var scr gui.GUI
|
|
|
|
if *display {
|
|
// create gui
|
|
sync.creator <- func() (GuiCreator, error) {
|
|
return sdlimgui.NewSdlImgui(tv)
|
|
}
|
|
|
|
// wait for creator result
|
|
select {
|
|
case g := <-sync.creation:
|
|
scr = g.(gui.GUI)
|
|
case err := <-sync.creationError:
|
|
return err
|
|
}
|
|
|
|
// fpscap for gui (see above for tv option)
|
|
scr.SetFeature(gui.ReqVSync, *fpsCap)
|
|
}
|
|
|
|
// check for profiling options
|
|
p, err := performance.ParseProfileString(*profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// run performance check
|
|
err = performance.Check(md.Output, p, tv, scr, *duration, cartload)
|
|
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")
|
|
|
|
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
|
|
}
|
|
|
|
// turn off default sigint handling
|
|
sync.state <- stateRequest{req: reqNoIntSig}
|
|
|
|
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)
|
|
}
|
|
|
|
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 [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)
|
|
md.Output = &nopWriter{}
|
|
} else {
|
|
logger.SetEcho(nil)
|
|
}
|
|
|
|
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 !curated.Is(err, recorder.NotAPlaybackFile) {
|
|
return err
|
|
} else {
|
|
*mode = "VIDEO"
|
|
}
|
|
}
|
|
|
|
switch strings.ToUpper(*mode) {
|
|
case "VIDEO":
|
|
cartload := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
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 := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
|
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: %v", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("regression tests can only be added one at a time")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func hiscoreServer(md *modalflag.Modes) error {
|
|
md.NewMode()
|
|
md.AddSubModes("ABOUT", "SETSERVER", "LOGIN", "LOGOFF")
|
|
md.AdditionalHelp("Hiscore server support is EXPERIMENTAL")
|
|
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
switch md.Mode() {
|
|
case "ABOUT":
|
|
fmt.Println("The hiscore server is experimental and is not currently fully functioning")
|
|
|
|
case "LOGIN":
|
|
md.NewMode()
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
username := ""
|
|
args := md.RemainingArgs()
|
|
|
|
switch len(args) {
|
|
case 0:
|
|
// an empty string is okay
|
|
case 1:
|
|
username = args[0]
|
|
default:
|
|
return fmt.Errorf("too many arguments for %s", md)
|
|
}
|
|
|
|
err = hiscore.Login(os.Stdin, os.Stdout, username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case "LOGOFF":
|
|
err = hiscore.Logoff()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case "SETSERVER":
|
|
md.NewMode()
|
|
p, err := md.Parse()
|
|
if err != nil || p != modalflag.ParseContinue {
|
|
return err
|
|
}
|
|
|
|
server := ""
|
|
args := md.RemainingArgs()
|
|
|
|
switch len(args) {
|
|
case 0:
|
|
// an empty string is okay
|
|
case 1:
|
|
server = args[0]
|
|
default:
|
|
return fmt.Errorf("too many arguments for %s", md)
|
|
}
|
|
|
|
err = hiscore.SetServer(os.Stdin, os.Stdout, server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|