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
This commit is contained in:
steve 2019-03-14 08:31:49 +00:00
parent 7da83ecade
commit 8ddaec1233
13 changed files with 276 additions and 184 deletions

View file

@ -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 "`":

View file

@ -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

View file

@ -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
}
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
}
// 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
}
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
}
// 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

View file

@ -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())
switch err := err.(type) {
case errors.FormattedError:
switch err.Errno {
case errors.UserInterrupt:
dbg.running = false
default:
return err
fallthrough
case errors.ScriptEnd:
if mainLoop {
dbg.print(ui.Feedback, err.Error())
}
return nil
}
}
dbg.checkDbgChannel()
return err
}
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)
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -2,16 +2,22 @@ package errors
var messages = map[Errno]string{
// Debugger
InputEmpty: "input is empty",
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)",
// Script
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",
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",

View file

@ -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

View file

@ -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
}

View file

@ -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")
}