From 8ddaec1233d7dbd10c78562ea838f7bd11e8832f Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 14 Mar 2019 08:31:49 +0000 Subject: [PATCH] o debugger - scripts now pass through input loop, allowing commands that control the emulation (like RUN or STEP) to be effective - reworked ONSTEP and ONHALT commands - added STICK0 and STICK1 commands o memory / pia - improved RAM debugging output --- debugger/callbacks.go | 4 +- debugger/colorterm/input.go | 14 ++--- debugger/commands.go | 101 ++++++++++++++++++++++----------- debugger/debugger.go | 102 +++++++++++++++++++--------------- debugger/script.go | 60 +++++++------------- debugger/ui/plainterminal.go | 2 +- debugger/ui/ui.go | 26 ++++----- disassembly/parse.go | 8 +++ errors/categories.go | 13 ++++- errors/messages.go | 23 +++++--- hardware/memory/pia.go | 11 ++-- hardware/peripherals/stick.go | 90 +++++++++++++++++++++--------- hardware/vcs.go | 6 +- 13 files changed, 276 insertions(+), 184 deletions(-) diff --git a/debugger/callbacks.go b/debugger/callbacks.go index 8ca58243..00b1b783 100644 --- a/debugger/callbacks.go +++ b/debugger/callbacks.go @@ -10,7 +10,7 @@ func (dbg *Debugger) setupTVCallbacks() error { var err error // add break on right mouse button - err = dbg.tv.RegisterCallback(gui.ReqOnMouseButtonRight, dbg.dbgChannel, func() { + err = dbg.tv.RegisterCallback(gui.ReqOnMouseButtonRight, dbg.interruptChannel, func() { // this callback function may be running inside a different goroutine // so care must be taken not to cause a deadlock hp, _ := dbg.tv.GetMetaState(gui.ReqLastMouseHorizPos) @@ -28,7 +28,7 @@ func (dbg *Debugger) setupTVCallbacks() error { } // respond to keyboard - err = dbg.tv.RegisterCallback(gui.ReqOnKeyboard, dbg.dbgChannel, func() { + err = dbg.tv.RegisterCallback(gui.ReqOnKeyboard, dbg.interruptChannel, func() { key, _ := dbg.tv.GetMetaState(gui.ReqLastKeyboard) switch key { case "`": diff --git a/debugger/colorterm/input.go b/debugger/colorterm/input.go index f3cb7286..8017ba00 100644 --- a/debugger/colorterm/input.go +++ b/debugger/colorterm/input.go @@ -4,18 +4,16 @@ import ( "gopher2600/debugger/colorterm/ansi" "gopher2600/debugger/colorterm/easyterm" "gopher2600/debugger/ui" + "gopher2600/errors" "unicode" "unicode/utf8" ) // UserRead is the top level input function -func (ct *ColorTerminal) UserRead(input []byte, prompt string, dbgChannel chan func()) (int, error) { +func (ct *ColorTerminal) UserRead(input []byte, prompt string, interruptChannel chan func()) (int, error) { // ctrl-c handling: currently, we put the terminal into rawmode and listen - // for ctrl-c event using the readRune reader, rather than using the ctrl-c - // handler on the end of the dbgChannel. we could use the ctrl-c handler - // but it means having a return value for the channeled function which I'm - // not prepared to add just yet. this method is okay. + // for ctrl-c event using the readRune reader. ct.RawMode() defer ct.CanonicalMode() @@ -53,8 +51,8 @@ func (ct *ColorTerminal) UserRead(input []byte, prompt string, dbgChannel chan f ct.Print(ansi.CursorRestore) select { - case f := <-dbgChannel: - // handle functions that are passsed on over dbgChannel. these can + case f := <-interruptChannel: + // handle functions that are passsed on over interruptChannel. these can // be things like events from the television GUI. eg. mouse clicks, // key presses, etc. ct.Print(ansi.CursorStore) @@ -93,7 +91,7 @@ func (ct *ColorTerminal) UserRead(input []byte, prompt string, dbgChannel chan f // debugger.Start(), that controls the main debugging loop. this // ctrl-c handler by contrast, controls the user input loop ct.Print("\n") - return inputLen + 1, &ui.UserInterrupt{Message: "user interrupt: CTRL-C"} + return inputLen + 1, errors.NewFormattedError(errors.UserInterrupt) case easyterm.KeyCarriageReturn: // CARRIAGE RETURN diff --git a/debugger/commands.go b/debugger/commands.go index 4188511a..df9d27d6 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -57,6 +57,8 @@ const ( KeywordScript = "SCRIPT" KeywordDisassemble = "DISASSEMBLE" KeywordGrep = "GREP" + KeywordStick0 = "STICK0" + KeywordStick1 = "STICK1" ) // Help contains the help text for the debugger's top level commands @@ -101,6 +103,8 @@ var Help = map[string]string{ KeywordScript: "Run commands from specified file", KeywordDisassemble: "Print the full cartridge disassembly", KeywordGrep: "Simple string search (case insensitive) of the disassembly", + KeywordStick0: "Emulate a joystick input for Player 0", + KeywordStick1: "Emulate a joystick input for Player 1", } var commandTemplate = input.CommandTemplate{ @@ -117,8 +121,8 @@ var commandTemplate = input.CommandTemplate{ KeywordList: "[BREAKS|TRAPS|WATCHES]", KeywordClear: "[BREAKS|TRAPS|WATCHES]", KeywordDrop: "[BREAK|TRAP|WATCH] %V", - KeywordOnHalt: "[|OFF|ECHO] %*", - KeywordOnStep: "[|OFF|ECHO] %*", + KeywordOnHalt: "[|OFF|RESTORE] %*", + KeywordOnStep: "[|OFF|RESTORE] %*", KeywordLast: "[|DEFN]", KeywordMemMap: "", KeywordQuit: "", @@ -148,6 +152,8 @@ var commandTemplate = input.CommandTemplate{ KeywordScript: "%F", KeywordDisassemble: "", KeywordGrep: "%S %*", + KeywordStick0: "[LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]", + KeywordStick1: "[LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]", } // notes @@ -186,7 +192,7 @@ func (dbg *Debugger) parseCommand(userInput string) (bool, error) { switch err.Errno { case errors.InputEmpty: // user pressed return - return true, nil + return false, nil } } return false, err @@ -229,7 +235,14 @@ func (dbg *Debugger) parseCommand(userInput string) (bool, error) { case KeywordScript: script, _ := tokens.Get() - err := dbg.RunScript(script, false) + + spt, err := dbg.loadScript(script) + if err != nil { + dbg.print(ui.Error, "error running debugger initialisation script: %s\n", err) + return false, err + } + + err = dbg.inputLoop(spt, true) if err != nil { return false, err } @@ -368,19 +381,17 @@ func (dbg *Debugger) parseCommand(userInput string) (bool, error) { case KeywordOnHalt: if tokens.Remaining() == 0 { - dbg.commandOnHalt = dbg.commandOnHaltStored - } else { - option, _ := tokens.Peek() - if strings.ToUpper(option) == "OFF" { - dbg.commandOnHalt = "" - dbg.print(ui.Feedback, "no auto-command on halt") - return false, nil - } - if strings.ToUpper(option) == "ECHO" { - dbg.print(ui.Feedback, "auto-command on halt: %s", dbg.commandOnHalt) - return false, nil - } + dbg.print(ui.Feedback, "auto-command on halt: %s", dbg.commandOnHalt) + return false, nil + } + option, _ := tokens.Peek() + switch strings.ToUpper(option) { + case "OFF": + dbg.commandOnHalt = "" + case "RESTORE": + dbg.commandOnHalt = dbg.commandOnHaltStored + default: // use remaininder of command line to form the ONHALT command sequence dbg.commandOnHalt = tokens.Remainder() @@ -393,27 +404,30 @@ func (dbg *Debugger) parseCommand(userInput string) (bool, error) { dbg.commandOnHaltStored = dbg.commandOnHalt } - dbg.print(ui.Feedback, "auto-command on halt: %s", dbg.commandOnHalt) + // display the new/restored onhalt command(s) + if dbg.commandOnHalt == "" { + dbg.print(ui.Feedback, "auto-command on halt: OFF") + } else { + dbg.print(ui.Feedback, "auto-command on halt: %s", dbg.commandOnHalt) + } - // run the new onhalt command(s) + // run the new/restored onhalt command(s) _, err := dbg.parseInput(dbg.commandOnHalt) return false, err case KeywordOnStep: if tokens.Remaining() == 0 { - dbg.commandOnStep = dbg.commandOnStepStored - } else { - option, _ := tokens.Peek() - if strings.ToUpper(option) == "OFF" { - dbg.commandOnStep = "" - dbg.print(ui.Feedback, "no auto-command on step") - return false, nil - } - if strings.ToUpper(option) == "ECHO" { - dbg.print(ui.Feedback, "auto-command on step: %s", dbg.commandOnStep) - return false, nil - } + dbg.print(ui.Feedback, "auto-command on step: %s", dbg.commandOnStep) + return false, nil + } + option, _ := tokens.Peek() + switch strings.ToUpper(option) { + case "OFF": + dbg.commandOnStep = "" + case "RESTORE": + dbg.commandOnStep = dbg.commandOnStepStored + default: // use remaininder of command line to form the ONSTEP command sequence dbg.commandOnStep = tokens.Remainder() @@ -426,9 +440,14 @@ func (dbg *Debugger) parseCommand(userInput string) (bool, error) { dbg.commandOnStepStored = dbg.commandOnStep } - dbg.print(ui.Feedback, "auto-command on step: %s", dbg.commandOnStep) + // display the new/restored onstep command(s) + if dbg.commandOnStep == "" { + dbg.print(ui.Feedback, "auto-command on step: OFF") + } else { + dbg.print(ui.Feedback, "auto-command on step: %s", dbg.commandOnStep) + } - // run the new onstep command(s) + // run the new/restored onstep command(s) _, err := dbg.parseInput(dbg.commandOnStep) return false, err @@ -831,6 +850,24 @@ func (dbg *Debugger) parseCommand(userInput string) (bool, error) { return false, err } dbg.print(ui.MachineInfo, info.(string)) + + case KeywordStick0: + action, present := tokens.Get() + if present { + err := dbg.vcs.Controller.HandleStick(0, action) + if err != nil { + return false, err + } + } + + case KeywordStick1: + action, present := tokens.Get() + if present { + err := dbg.vcs.Controller.HandleStick(1, action) + if err != nil { + return false, err + } + } } return stepNext, nil diff --git a/debugger/debugger.go b/debugger/debugger.go index 7d7a852c..013acc92 100644 --- a/debugger/debugger.go +++ b/debugger/debugger.go @@ -106,7 +106,7 @@ type Debugger struct { // the SDL guiloop() goroutine // b) receive ctrl-c events when the emulation is running (note that // ctrl-c handling is handled differently under different circumstances) - dbgChannel chan func() + interruptChannel chan func() } // NewDebugger creates and initialises everything required for a new debugging @@ -158,10 +158,10 @@ func NewDebugger() (*Debugger, error) { dbg.input = make([]byte, 255) // make synchronisation channel - dbg.dbgChannel = make(chan func(), 2) + dbg.interruptChannel = make(chan func(), 2) // set up callbacks for the TV interface - // -- requires dbgChannel to have been set up + // -- requires interruptChannel to have been set up err = dbg.setupTVCallbacks() if err != nil { return nil, err @@ -171,10 +171,10 @@ func NewDebugger() (*Debugger, error) { } // Start the main debugger sequence -func (dbg *Debugger) Start(interf ui.UserInterface, filename string, initScript string) error { +func (dbg *Debugger) Start(iface ui.UserInterface, filename string, initScript string) error { // prepare user interface - if interf != nil { - dbg.ui = interf + if iface != nil { + dbg.ui = iface } err := dbg.ui.Initialise() @@ -199,7 +199,7 @@ func (dbg *Debugger) Start(interf ui.UserInterface, filename string, initScript for { <-ctrlC - dbg.dbgChannel <- func() { + dbg.interruptChannel <- func() { if dbg.runUntilHalt { // stop emulation at the next step dbg.runUntilHalt = false @@ -218,15 +218,20 @@ func (dbg *Debugger) Start(interf ui.UserInterface, filename string, initScript // run initialisation script if initScript != "" { - err = dbg.RunScript(initScript, true) + spt, err := dbg.loadScript(initScript) if err != nil { dbg.print(ui.Error, "error running debugger initialisation script: %s\n", err) } + + err = dbg.inputLoop(spt, true) + if err != nil { + return err + } } // prepare and run main input loop. inputLoop will not return until // debugger is to exit - err = dbg.inputLoop(true) + err = dbg.inputLoop(dbg.ui, true) if err != nil { return err } @@ -265,22 +270,7 @@ func (dbg *Debugger) loadCartridge(cartridgeFilename string) error { return nil } -// videoCycle*() are callback functions to be used when calling vcs.Step(). -// stepmode CPU uses videoCycle(), whereas stepmode VIDEO uses -// videoCycleWithInput() which in turn calls videoCycle() - -func (dbg *Debugger) videoCycleWithInput(result *result.Instruction) error { - dbg.videoCycle(result) - dbg.lastResult = result - if dbg.commandOnStep != "" { - _, err := dbg.parseInput(dbg.commandOnStep) - if err != nil { - dbg.print(ui.Error, "%s", err) - } - } - return dbg.inputLoop(false) -} - +// videoCycle() to be used with vcs.Step() func (dbg *Debugger) videoCycle(result *result.Instruction) error { // because we call this callback mid-instruction, the programme counter // maybe in it's non-final state - we don't want to break or trap in these @@ -302,27 +292,42 @@ func (dbg *Debugger) videoCycle(result *result.Instruction) error { return dbg.sysmon.Check() } -func (dbg *Debugger) checkDbgChannel() { - // check dbgChannel and run any functions we find in there -- note that - // we also monitor the dbgChannel in the UserInterface.UserRead() - // function. if we didn't then this inputLoop would not react to - // messages on the channel until it reaches this point again. +func (dbg *Debugger) checkForInterrupts() { + // check interrupt channel and run any functions we find in there select { - case f := <-dbg.dbgChannel: + case f := <-dbg.interruptChannel: f() default: - // go novice note: we need a default case otherwise the select blocks + // pro-tip: default case required otherwise the select will block + // indefinately. } } // inputLoop has two modes, defined by the mainLoop argument. when inputLoop is // not a "mainLoop", the function will only loop for the duration of one cpu // step. this is used to implement video-stepping. -func (dbg *Debugger) inputLoop(mainLoop bool) error { +// +// inputter is an instance of type UserInput. this will usually be dbg.ui but +// it could equally be an instance of debuggingScript. +func (dbg *Debugger) inputLoop(inputter ui.UserInput, mainLoop bool) error { var err error + // videoCycleWithInput() to be used with vcs.Step() instead of videoCycle() + // when in video-step mode + videoCycleWithInput := func(result *result.Instruction) error { + dbg.videoCycle(result) + dbg.lastResult = result + if dbg.commandOnStep != "" { + _, err := dbg.parseInput(dbg.commandOnStep) + if err != nil { + dbg.print(ui.Error, "%s", err) + } + } + return dbg.inputLoop(inputter, false) + } + for { - dbg.checkDbgChannel() + dbg.checkForInterrupts() if !dbg.running { break // for loop } @@ -408,20 +413,27 @@ func (dbg *Debugger) inputLoop(mainLoop bool) error { } // get user input - n, err := dbg.ui.UserRead(dbg.input, prompt, dbg.dbgChannel) + n, err := inputter.UserRead(dbg.input, prompt, dbg.interruptChannel) if err != nil { - switch err.(type) { - case *ui.UserInterrupt: - // UserRead() has caught a ctrl-c event and returned the - // appropriate error - dbg.print(ui.Feedback, err.Error()) - dbg.running = false - default: - return err + switch err := err.(type) { + + case errors.FormattedError: + switch err.Errno { + case errors.UserInterrupt: + dbg.running = false + fallthrough + case errors.ScriptEnd: + if mainLoop { + dbg.print(ui.Feedback, err.Error()) + } + return nil + } } + + return err } - dbg.checkDbgChannel() + dbg.checkForInterrupts() if !dbg.running { break // for loop } @@ -448,7 +460,7 @@ func (dbg *Debugger) inputLoop(mainLoop bool) error { if dbg.inputloopNext { if mainLoop { if dbg.inputloopVideoClock { - _, dbg.lastResult, err = dbg.vcs.Step(dbg.videoCycleWithInput) + _, dbg.lastResult, err = dbg.vcs.Step(videoCycleWithInput) } else { _, dbg.lastResult, err = dbg.vcs.Step(dbg.videoCycle) } diff --git a/debugger/script.go b/debugger/script.go index e620841b..5f7187c9 100644 --- a/debugger/script.go +++ b/debugger/script.go @@ -1,16 +1,19 @@ package debugger import ( - "fmt" - "gopher2600/debugger/ui" "gopher2600/errors" "io/ioutil" "os" "strings" ) -func (dbg *Debugger) loadScript(scriptfile string) ([]string, error) { +type debuggingScript struct { + scriptFile string + lines []string + nextLine int +} +func (dbg *Debugger) loadScript(scriptfile string) (*debuggingScript, error) { // open script and defer closing sf, err := os.Open(scriptfile) if err != nil { @@ -25,47 +28,24 @@ func (dbg *Debugger) loadScript(scriptfile string) ([]string, error) { return nil, errors.NewFormattedError(errors.ScriptFileError, err) } + dbs := new(debuggingScript) + dbs.scriptFile = scriptfile + // convert buffer to an array of lines - s := fmt.Sprintf("%s", buffer) - return strings.Split(s, "\n"), nil + dbs.lines = strings.Split(string(buffer), "\n") + + return dbs, nil } -// RunScript uses a text file as a source for a sequence of commands -func (dbg *Debugger) RunScript(scriptfile string, silent bool) error { - - // the silent flag passed to this function is meant to silence commands for - // the duration of the script only. store existing state of dbg.silent so we - // can restore it when script has concluded - uiSilentRestore := dbg.uiSilent - dbg.uiSilent = silent - defer func() { - dbg.uiSilent = uiSilentRestore - }() - - // load file - lines, err := dbg.loadScript(scriptfile) - if err != nil { - return err +// UserRead implements ui.UserInput interface +func (dbs *debuggingScript) UserRead(buffer []byte, prompt string, interruptChannel chan func()) (int, error) { + if dbs.nextLine > len(dbs.lines)-1 { + return -1, errors.NewFormattedError(errors.ScriptEnd, dbs.scriptFile) } - // parse each line as user input - for i := 0; i < len(lines); i++ { - if strings.Trim(lines[i], " ") != "" { - if !silent { - dbg.print(ui.Script, lines[i]) - } - next, err := dbg.parseInput(lines[i]) - if err != nil { - return errors.NewFormattedError(errors.ScriptFileError, err) - } - if next { - // make sure run state is still sane - dbg.runUntilHalt = false + l := len(dbs.lines[dbs.nextLine]) + 1 + copy(buffer, []byte(dbs.lines[dbs.nextLine])) + dbs.nextLine++ - return errors.NewFormattedError(errors.ScriptRunError, strings.ToUpper(lines[i]), scriptfile, i) - } - } - } - - return nil + return l, nil } diff --git a/debugger/ui/plainterminal.go b/debugger/ui/plainterminal.go index ebcfddec..a6eba94b 100644 --- a/debugger/ui/plainterminal.go +++ b/debugger/ui/plainterminal.go @@ -42,7 +42,7 @@ func (pt PlainTerminal) UserPrint(pp PrintProfile, s string, a ...interface{}) { } // UserRead is the plain terminal read routine -func (pt PlainTerminal) UserRead(input []byte, prompt string, dbgChannel chan func()) (int, error) { +func (pt PlainTerminal) UserRead(input []byte, prompt string, interruptChannel chan func()) (int, error) { pt.UserPrint(Prompt, prompt) n, err := os.Stdin.Read(input) diff --git a/debugger/ui/ui.go b/debugger/ui/ui.go index 92f2d5c1..da247bee 100644 --- a/debugger/ui/ui.go +++ b/debugger/ui/ui.go @@ -2,22 +2,22 @@ package ui import "gopher2600/debugger/input" +// UserInput defines the operations required by an interface that allows input +type UserInput interface { + UserRead(buffer []byte, prompt string, interruptChannel chan func()) (int, error) +} + +// UserOutput defines the operations required by an interface that allows +// output +type UserOutput interface { + UserPrint(PrintProfile, string, ...interface{}) +} + // UserInterface defines the user interface operations required by the debugger type UserInterface interface { Initialise() error CleanUp() RegisterTabCompleter(*input.TabCompletion) - UserPrint(PrintProfile, string, ...interface{}) - UserRead([]byte, string, chan func()) (int, error) -} - -// UserInterrupt can be returned by UserRead() when user has cause an -// interrupt (ie. CTRL-C) -type UserInterrupt struct { - Message string -} - -// implement Error interface for UserInterrupt -func (intr UserInterrupt) Error() string { - return intr.Message + UserInput + UserOutput } diff --git a/disassembly/parse.go b/disassembly/parse.go index ee29e897..4b4507e3 100644 --- a/disassembly/parse.go +++ b/disassembly/parse.go @@ -68,6 +68,14 @@ func (dsm *Disassembly) parseLoop(mc *cpu.CPU) error { // resume from where we left off dsm.Cart.BankSwitch(retBank) mc.LoadPC(retPC) + } else { + // it's entirely possible for the program to jump + // outside of cartridge space and run inside RIOT RAM, + // for instance (test-ane.bin does this for instance). + // + // it's difficult to see what we can do in these cases + // without actually running the program for real (with + // actual side-effects, that is) } } else { // absolute addressing diff --git a/errors/categories.go b/errors/categories.go index 512d260e..34f98c2c 100644 --- a/errors/categories.go +++ b/errors/categories.go @@ -4,15 +4,21 @@ package errors const ( // Debugger InputEmpty Errno = iota + UserInterrupt CommandError + InvalidTarget + CannotRecordState + + // Symbols SymbolsFileCannotOpen SymbolsFileError SymbolUnknown + + // Script ScriptFileCannotOpen ScriptFileError ScriptRunError - InvalidTarget - CannotRecordState + ScriptEnd // Regression RegressionEntryExists @@ -45,6 +51,9 @@ const ( ImageTV DigestTV + // Controllers + StickDisconnected + // GUI UnknownGUIRequest SDL diff --git a/errors/messages.go b/errors/messages.go index 87d7f534..5cecdeee 100644 --- a/errors/messages.go +++ b/errors/messages.go @@ -2,16 +2,22 @@ package errors var messages = map[Errno]string{ // Debugger - InputEmpty: "input is empty", - CommandError: "%s", + InputEmpty: "no input", + UserInterrupt: "user interrupt", + CommandError: "%s", + InvalidTarget: "invalid target (%s)", + CannotRecordState: "cannot record state: %s", + + // Symbols SymbolsFileCannotOpen: "no symbols file for %s", SymbolsFileError: "error processing symbols file (%s)", SymbolUnknown: "unrecognised symbol (%s)", - ScriptFileCannotOpen: "cannot open script file (%s)", - ScriptFileError: "script error: %s", - ScriptRunError: "script error: use of '%s' is not allowed in scripts [%s::%d]", - InvalidTarget: "invalid target (%s)", - CannotRecordState: "cannot record state: %s", + + // Script + ScriptFileCannotOpen: "cannot open script file (%s)", + ScriptFileError: "script error: %s", + ScriptRunError: "script error: use of '%s' is not allowed in scripts [%s::%d]", + ScriptEnd: "end of script (%s)", // Regression RegressionEntryExists: "entry exists (%s)", @@ -44,6 +50,9 @@ var messages = map[Errno]string{ ImageTV: "ImageTV: %s", DigestTV: "DigestTV: %s", + // Controllers + StickDisconnected: "Stick for player %d is not connected", + // GUI UnknownGUIRequest: "GUI does not support %v request", SDL: "SDL: %s", diff --git a/hardware/memory/pia.go b/hardware/memory/pia.go index 04b6747d..31233f9e 100644 --- a/hardware/memory/pia.go +++ b/hardware/memory/pia.go @@ -57,14 +57,17 @@ func (pia PIA) MachineInfoTerse() string { // MachineInfo returns the RIOT information in verbose format func (pia PIA) MachineInfo() string { - s := "" + s := strings.Builder{} + s.WriteString(" -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F\n") + s.WriteString(" ---- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --\n") for y := 0; y < 8; y++ { + s.WriteString(fmt.Sprintf("%X- | ", y+8)) for x := 0; x < 16; x++ { - s = fmt.Sprintf("%s %02x", s, pia.memory[uint16((y*16)+x)]) + s.WriteString(fmt.Sprintf(" %02x", pia.memory[uint16((y*16)+x)])) } - s = fmt.Sprintf("%s\n", s) + s.WriteString("\n") } - return strings.Trim(s, "\n") + return strings.Trim(s.String(), "\n") } // map String to MachineInfo diff --git a/hardware/peripherals/stick.go b/hardware/peripherals/stick.go index d152be5b..3f85ffee 100644 --- a/hardware/peripherals/stick.go +++ b/hardware/peripherals/stick.go @@ -1,9 +1,11 @@ package peripherals import ( + "fmt" "gopher2600/errors" "gopher2600/hardware/memory" "gopher2600/hardware/memory/vcssymbols" + "strings" "github.com/splace/joysticks" ) @@ -12,17 +14,22 @@ import ( type Stick struct { device *joysticks.HID err error + + tia memory.PeriphBus + riot memory.PeriphBus } // NewStick is the preferred method of initialisation for the Stick type func NewStick(tia memory.PeriphBus, riot memory.PeriphBus, panel *Panel) *Stick { - stick := new(Stick) + stk := new(Stick) + stk.tia = tia + stk.riot = riot - // TODO: make all this work with a seconc contoller. for now, initialise + // TODO: make all this work with a second contoller. for now, initialise // and asssume that there is just one controller for player 0 - riot.PeriphWrite(vcssymbols.SWCHA, 0xff) - tia.PeriphWrite(vcssymbols.INPT4, 0x80) - tia.PeriphWrite(vcssymbols.INPT5, 0x80) + stk.riot.PeriphWrite(vcssymbols.SWCHA, 0xff) + stk.tia.PeriphWrite(vcssymbols.INPT4, 0x80) + stk.tia.PeriphWrite(vcssymbols.INPT5, 0x80) // there is a flaw (either in splace/joysticks or somewehere else lower // down in the kernel driver) which means that Connect() will not return @@ -31,28 +38,28 @@ func NewStick(tia memory.PeriphBus, riot memory.PeriphBus, panel *Panel) *Stick go func() { // try connecting to specific controller. // system assigned index: typically increments on each new controller added. - stick.device = joysticks.Connect(1) - if stick.device == nil { - stick.err = errors.NewFormattedError(errors.NoControllersFound, nil) + stk.device = joysticks.Connect(1) + if stk.device == nil { + stk.err = errors.NewFormattedError(errors.NoControllersFound, nil) return } // get/assign channels for specific events - stickMove := stick.device.OnMove(1) + stickMove := stk.device.OnMove(1) - buttonPress := stick.device.OnClose(1) - buttonRelease := stick.device.OnOpen(1) + buttonPress := stk.device.OnClose(1) + buttonRelease := stk.device.OnOpen(1) // on xbox controller, button 8 is the start button - resetPress := stick.device.OnClose(8) - resetRelease := stick.device.OnOpen(8) + resetPress := stk.device.OnClose(8) + resetRelease := stk.device.OnOpen(8) // on xbox controller, button 9 is the back button - selectPress := stick.device.OnClose(7) - selectRelease := stick.device.OnOpen(7) + selectPress := stk.device.OnClose(7) + selectRelease := stk.device.OnOpen(7) // start feeding OS events onto the event channels. - go stick.device.ParcelOutEvents() + go stk.device.ParcelOutEvents() // handle event channels for { @@ -68,28 +75,57 @@ func NewStick(tia memory.PeriphBus, riot memory.PeriphBus, panel *Panel) *Stick panel.SetGameSelect(false) case <-buttonPress: - tia.PeriphWrite(vcssymbols.INPT4, 0x00) + stk.HandleStick(0, "FIRE") case <-buttonRelease: - tia.PeriphWrite(vcssymbols.INPT4, 0x80) + stk.HandleStick(0, "NOFIRE") case ev := <-stickMove: - swcha := uint8(0xff) x := ev.(joysticks.CoordsEvent).X y := ev.(joysticks.CoordsEvent).Y if x < -0.5 { - swcha &= 0xbf + stk.HandleStick(0, "LEFT") } else if x > 0.5 { - swcha &= 0x7f - } - if y < -0.5 { - swcha &= 0xef + stk.HandleStick(0, "RIGHT") + } else if y < -0.5 { + stk.HandleStick(0, "UP") } else if y > 0.5 { - swcha &= 0xdf + stk.HandleStick(0, "DOWN") + } else { + stk.HandleStick(0, "CENTRE") } - riot.PeriphWrite(vcssymbols.SWCHA, swcha) } } }() - return stick + return stk +} + +// HandleStick parses the action and writes to the correct memory location +func (stk *Stick) HandleStick(player int, action string) error { + if player == 0 { + switch strings.ToUpper(action) { + case "LEFT": + stk.riot.PeriphWrite(vcssymbols.SWCHA, 0xbf) + case "RIGHT": + stk.riot.PeriphWrite(vcssymbols.SWCHA, 0x7f) + case "UP": + stk.riot.PeriphWrite(vcssymbols.SWCHA, 0xef) + case "DOWN": + stk.riot.PeriphWrite(vcssymbols.SWCHA, 0xdf) + case "CENTER": + fallthrough + case "CENTRE": + stk.riot.PeriphWrite(vcssymbols.SWCHA, 0xff) + case "FIRE": + stk.tia.PeriphWrite(vcssymbols.INPT4, 0x00) + case "NOFIRE": + stk.tia.PeriphWrite(vcssymbols.INPT4, 0x80) + } + } else if player == 1 { + return errors.NewFormattedError(errors.StickDisconnected, player) + } else { + panic(fmt.Sprintf("there is no player %d with a joystick to handle", player)) + } + + return nil } diff --git a/hardware/vcs.go b/hardware/vcs.go index 222e977d..8f7b77bf 100644 --- a/hardware/vcs.go +++ b/hardware/vcs.go @@ -23,7 +23,7 @@ type VCS struct { TV television.Television panel *peripherals.Panel - controller *peripherals.Stick + Controller *peripherals.Stick // treat the side effects of the CPU after every CPU cycle (correct) or // only at the end of each instruction (wrong) @@ -68,8 +68,8 @@ func NewVCS(tv television.Television) (*VCS, error) { } // TODO: better contoller support - vcs.controller = peripherals.NewStick(vcs.Mem.TIA, vcs.Mem.RIOT, vcs.panel) - if vcs.controller == nil { + vcs.Controller = peripherals.NewStick(vcs.Mem.TIA, vcs.Mem.RIOT, vcs.panel) + if vcs.Controller == nil { return nil, fmt.Errorf("can't create stick controller") }