mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
- RAM command now has CART option. displays any additional RAM the cartridge may have o cartridge - implemented RAM() command. returns copy of RAM array - save/restore banks functions are now save/restore state and deal with cartridge RAM in addition to bank information o debugger/memory - better error messages for peek and poke
1115 lines
28 KiB
Go
1115 lines
28 KiB
Go
package debugger
|
|
|
|
import (
|
|
"fmt"
|
|
"gopher2600/debugger/commandline"
|
|
"gopher2600/debugger/console"
|
|
"gopher2600/debugger/script"
|
|
"gopher2600/errors"
|
|
"gopher2600/gui"
|
|
"gopher2600/hardware/cpu/register"
|
|
"gopher2600/hardware/cpu/result"
|
|
"gopher2600/hardware/peripherals"
|
|
"gopher2600/symbols"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// debugger keywords. not a useful data structure but we can use these to form
|
|
// the more useful DebuggerCommands and Help structures
|
|
const (
|
|
cmdBall = "BALL"
|
|
cmdBreak = "BREAK"
|
|
cmdCPU = "CPU"
|
|
cmdCartridge = "CARTRIDGE"
|
|
cmdClear = "CLEAR"
|
|
cmdDebuggerState = "DEBUGGERSTATE"
|
|
cmdDigest = "DIGEST"
|
|
cmdDisassembly = "DISASSEMBLY"
|
|
cmdDisplay = "DISPLAY"
|
|
cmdDrop = "DROP"
|
|
cmdGrep = "GREP"
|
|
cmdHexLoad = "HEXLOAD"
|
|
cmdInsert = "INSERT"
|
|
cmdLast = "LAST"
|
|
cmdList = "LIST"
|
|
cmdMemMap = "MEMMAP"
|
|
cmdMissile = "MISSILE"
|
|
cmdOnHalt = "ONHALT"
|
|
cmdOnStep = "ONSTEP"
|
|
cmdPeek = "PEEK"
|
|
cmdPlayer = "PLAYER"
|
|
cmdPlayfield = "PLAYFIELD"
|
|
cmdPoke = "POKE"
|
|
cmdQuit = "QUIT"
|
|
cmdExit = "EXIT"
|
|
cmdRAM = "RAM"
|
|
cmdRIOT = "RIOT"
|
|
cmdReset = "RESET"
|
|
cmdRun = "RUN"
|
|
cmdScript = "SCRIPT"
|
|
cmdStep = "STEP"
|
|
cmdGranularity = "GRANULARITY"
|
|
cmdStick = "STICK"
|
|
cmdSymbol = "SYMBOL"
|
|
cmdTIA = "TIA"
|
|
cmdTV = "TV"
|
|
cmdTerse = "TERSE"
|
|
cmdTrap = "TRAP"
|
|
cmdVerbose = "VERBOSE"
|
|
cmdVerbosity = "VERBOSITY"
|
|
cmdWatch = "WATCH"
|
|
)
|
|
|
|
const cmdHelp = "HELP"
|
|
|
|
var commandTemplate = []string{
|
|
cmdBall,
|
|
cmdBreak + " [%S %N|%N] {& %S %N|& %N}",
|
|
cmdCPU + " (SET [PC|A|X|Y|SP] [%N])",
|
|
cmdCartridge + " (ANALYSIS)",
|
|
cmdClear + " [BREAKS|TRAPS|WATCHES|ALL]",
|
|
cmdDebuggerState,
|
|
cmdDigest + " (RESET)",
|
|
cmdDisassembly,
|
|
cmdDisplay + " (OFF|DEBUG|SCALE [%P]|DEBUGCOLORS)", // see notes
|
|
cmdDrop + " [BREAK|TRAP|WATCH] %N",
|
|
cmdGrep + " %S",
|
|
cmdHexLoad + " %N %N {%N}",
|
|
cmdInsert + " %F",
|
|
cmdLast + " (DEFN)",
|
|
cmdList + " [BREAKS|TRAPS|WATCHES|ALL]",
|
|
cmdMemMap,
|
|
cmdMissile + " (0|1)",
|
|
cmdOnHalt + " (OFF|ON|%S {%S})",
|
|
cmdOnStep + " (OFF|ON|%S {%S})",
|
|
cmdPeek + " [%S] {%S}",
|
|
cmdPlayer + " (0|1)",
|
|
cmdPlayfield,
|
|
cmdPoke + " [%S] %N",
|
|
cmdQuit,
|
|
cmdExit,
|
|
cmdRAM + " (CART)",
|
|
cmdRIOT + " (TIMER)",
|
|
cmdReset,
|
|
cmdRun,
|
|
cmdScript + " [WRITE %S|END|%F]",
|
|
cmdStep + " (CPU|VIDEO|%S)",
|
|
cmdGranularity + " (CPU|VIDEO)",
|
|
cmdStick + " [0|1] [LEFT|RIGHT|UP|DOWN|FIRE|NOLEFT|NORIGHT|NOUP|NODOWN|NOFIRE]",
|
|
cmdSymbol + " [%S (ALL|MIRRORS)|LIST (LOCATIONS|READ|WRITE)]",
|
|
cmdTIA + " (DELAY|DELAYS)",
|
|
cmdTV + " (SPEC)",
|
|
cmdTerse,
|
|
cmdTrap + " [%S] {%S}",
|
|
cmdVerbose,
|
|
cmdVerbosity,
|
|
cmdWatch + " (READ|WRITE) [%S] (%S)",
|
|
}
|
|
|
|
// list of commands that should not be executed when recording/playing scripts
|
|
var scriptUnsafeTemplate = []string{
|
|
cmdScript + " [WRITE [%S]]",
|
|
cmdRun,
|
|
}
|
|
|
|
var debuggerCommands *commandline.Commands
|
|
var scriptUnsafeCommands *commandline.Commands
|
|
var debuggerCommandsIdx *commandline.Index
|
|
|
|
func init() {
|
|
var err error
|
|
|
|
// parse command template
|
|
debuggerCommands, err = commandline.ParseCommandTemplateWithOutput(commandTemplate, os.Stdout)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(100)
|
|
}
|
|
|
|
err = debuggerCommands.AddHelp(cmdHelp)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(100)
|
|
}
|
|
sort.Stable(debuggerCommands)
|
|
|
|
scriptUnsafeCommands, err = commandline.ParseCommandTemplate(scriptUnsafeTemplate)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(100)
|
|
}
|
|
sort.Stable(scriptUnsafeCommands)
|
|
|
|
debuggerCommandsIdx = commandline.CreateIndex(debuggerCommands)
|
|
}
|
|
|
|
type parseCommandResult int
|
|
|
|
const (
|
|
doNothing parseCommandResult = iota
|
|
emptyInput
|
|
stepContinue
|
|
setDefaultStep
|
|
scriptRecordStarted
|
|
scriptRecordEnded
|
|
)
|
|
|
|
// parseCommand/enactCommand scans user input for a valid command and acts upon
|
|
// it. commands that cause the emulation to move forward (RUN, STEP) return
|
|
// true for the first return value. other commands return false and act upon
|
|
// the command immediately. note that the empty string is the same as the STEP
|
|
// command
|
|
|
|
func (dbg *Debugger) parseCommand(userInput *string, interactive bool) (parseCommandResult, error) {
|
|
// tokenise input
|
|
tokens := commandline.TokeniseInput(*userInput)
|
|
|
|
// normalise user input -- we don't use the results in this
|
|
// function but we do use it futher-up. eg. when capturing user input to a
|
|
// script
|
|
*userInput = tokens.String()
|
|
|
|
// if there are no tokens in the input then return emptyInput directive
|
|
if tokens.Remaining() == 0 {
|
|
return emptyInput, nil
|
|
}
|
|
|
|
// check validity of tokenised input
|
|
err := debuggerCommands.ValidateTokens(tokens)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
|
|
// the absolute best thing about the ValidateTokens() function is that we
|
|
// don't need to worrying too much about the success of tokens.Get() in the
|
|
// enactCommand() function below:
|
|
//
|
|
// arg, _ := tokens.Get()
|
|
//
|
|
// is an acceptable pattern even when an argument is required. the
|
|
// ValidateTokens() function has already caught invalid attempts.
|
|
//
|
|
// default values can be handled thus:
|
|
//
|
|
// arg, ok := tokens.Get()
|
|
// if ok {
|
|
// switch arg {
|
|
// ...
|
|
// }
|
|
// } else {
|
|
// // default action
|
|
// ...
|
|
// }
|
|
//
|
|
// unfortunately, there is no way currently to handle the case where the
|
|
// command templates don't match expectation in the code below. the code
|
|
// won't break but some error messages may be misleading but hopefully, it
|
|
// will be obvious something went wrong.
|
|
|
|
// test to see if command is allowed when recording/playing a script
|
|
if dbg.scriptScribe.IsActive() || !interactive {
|
|
tokens.Reset()
|
|
|
|
err := scriptUnsafeCommands.ValidateTokens(tokens)
|
|
|
|
// fail when the tokens DO match the scriptUnsafe template (ie. when
|
|
// there is no err from the validate function)
|
|
if err == nil {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, fmt.Sprintf("'%s' is unsafe to use in scripts", tokens.String()))
|
|
}
|
|
}
|
|
|
|
return dbg.enactCommand(tokens, interactive)
|
|
}
|
|
|
|
func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool) (parseCommandResult, error) {
|
|
// check first token. if this token makes sense then we will consume the
|
|
// rest of the tokens appropriately
|
|
tokens.Reset()
|
|
command, _ := tokens.Get()
|
|
|
|
// take uppercase value of the first token. it's useful to take the
|
|
// uppercase value but we have to be careful when we do it because
|
|
command = strings.ToUpper(command)
|
|
|
|
switch command {
|
|
default:
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, fmt.Sprintf("%s is not yet implemented", command))
|
|
|
|
case cmdHelp:
|
|
keyword, present := tokens.Get()
|
|
if present {
|
|
keyword = strings.ToUpper(keyword)
|
|
|
|
helpTxt, ok := Help[keyword]
|
|
if !ok {
|
|
dbg.print(console.StyleHelp, "no help for %s", keyword)
|
|
} else {
|
|
helpTxt = fmt.Sprintf("%s\n\n Usage: %s", helpTxt, (*debuggerCommandsIdx)[keyword].String())
|
|
dbg.print(console.StyleHelp, helpTxt)
|
|
}
|
|
} else {
|
|
dbg.print(console.StyleHelp, debuggerCommands.String())
|
|
}
|
|
|
|
case cmdInsert:
|
|
cart, _ := tokens.Get()
|
|
err := dbg.loadCartridge(cart)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
dbg.print(console.StyleFeedback, "machine reset with new cartridge (%s)", cart)
|
|
|
|
case cmdScript:
|
|
option, _ := tokens.Get()
|
|
switch strings.ToUpper(option) {
|
|
case "WRITE":
|
|
var err error
|
|
saveFile, _ := tokens.Get()
|
|
err = dbg.scriptScribe.StartSession(saveFile)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
return scriptRecordStarted, nil
|
|
|
|
case "END":
|
|
dbg.scriptScribe.Rollback()
|
|
err := dbg.scriptScribe.EndSession()
|
|
return scriptRecordEnded, err
|
|
|
|
default:
|
|
// run a script
|
|
plb, err := script.StartPlayback(option)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
|
|
if dbg.scriptScribe.IsActive() {
|
|
// if we're currently recording a script we want to write this
|
|
// command to the new script file but indicate that we'll be
|
|
// entering a new script and so don't want to repeat the
|
|
// commands from that script
|
|
dbg.scriptScribe.StartPlayback()
|
|
|
|
defer func() {
|
|
dbg.scriptScribe.EndPlayback()
|
|
}()
|
|
|
|
// !!TODO: provide a recording option to allow insertion of
|
|
// the actual script commands rather than the call to the
|
|
// script itself
|
|
}
|
|
|
|
err = dbg.inputLoop(plb, false)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
}
|
|
|
|
case cmdDisassembly:
|
|
dbg.disasm.Dump(dbg.printStyle(console.StyleFeedback))
|
|
|
|
case cmdGrep:
|
|
search, _ := tokens.Get()
|
|
output := strings.Builder{}
|
|
dbg.disasm.Grep(search, &output, false, 3)
|
|
if output.Len() == 0 {
|
|
dbg.print(console.StyleError, "%s not found in disassembly", search)
|
|
} else {
|
|
dbg.print(console.StyleFeedback, output.String())
|
|
}
|
|
|
|
case cmdSymbol:
|
|
tok, _ := tokens.Get()
|
|
switch strings.ToUpper(tok) {
|
|
case "LIST":
|
|
option, present := tokens.Get()
|
|
if present {
|
|
switch strings.ToUpper(option) {
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
|
|
case "LOCATIONS":
|
|
dbg.disasm.Symtable.ListLocations(dbg.printStyle(console.StyleFeedback))
|
|
|
|
case "READ":
|
|
dbg.disasm.Symtable.ListReadSymbols(dbg.printStyle(console.StyleFeedback))
|
|
|
|
case "WRITE":
|
|
dbg.disasm.Symtable.ListWriteSymbols(dbg.printStyle(console.StyleFeedback))
|
|
}
|
|
} else {
|
|
dbg.disasm.Symtable.ListSymbols(dbg.printStyle(console.StyleFeedback))
|
|
}
|
|
|
|
default:
|
|
symbol := tok
|
|
table, symbol, address, err := dbg.disasm.Symtable.SearchSymbol(symbol, symbols.UnspecifiedSymTable)
|
|
if err != nil {
|
|
switch err := err.(type) {
|
|
case errors.FormattedError:
|
|
if err.Errno == errors.SymbolUnknown {
|
|
dbg.print(console.StyleFeedback, "%s -> not found", symbol)
|
|
return doNothing, nil
|
|
}
|
|
}
|
|
return doNothing, err
|
|
}
|
|
|
|
option, present := tokens.Get()
|
|
if present {
|
|
switch strings.ToUpper(option) {
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
|
|
case "ALL", "MIRRORS":
|
|
dbg.print(console.StyleFeedback, "%s -> %#04x", symbol, address)
|
|
|
|
// find all instances of symbol address in memory space
|
|
// assumption: the address returned by SearchSymbol is the
|
|
// first address in the complete list
|
|
for m := address + 1; m < dbg.vcs.Mem.Cart.Origin(); m++ {
|
|
if dbg.vcs.Mem.MapAddress(m, table == symbols.ReadSymTable) == address {
|
|
dbg.print(console.StyleFeedback, "%s -> %#04x", symbol, m)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
dbg.print(console.StyleFeedback, "%s -> %#04x", symbol, address)
|
|
}
|
|
}
|
|
|
|
case cmdBreak:
|
|
err := dbg.breakpoints.parseBreakpoint(tokens)
|
|
if err != nil {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, err)
|
|
}
|
|
|
|
case cmdTrap:
|
|
err := dbg.traps.parseTrap(tokens)
|
|
if err != nil {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, err)
|
|
}
|
|
|
|
case cmdWatch:
|
|
err := dbg.watches.parseWatch(tokens, dbg.dbgmem)
|
|
if err != nil {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, err)
|
|
}
|
|
|
|
case cmdList:
|
|
list, _ := tokens.Get()
|
|
list = strings.ToUpper(list)
|
|
switch list {
|
|
case "BREAKS":
|
|
dbg.breakpoints.list()
|
|
case "TRAPS":
|
|
dbg.traps.list()
|
|
case "WATCHES":
|
|
dbg.watches.list()
|
|
case "ALL":
|
|
// !!TODO: refine output. requires headings
|
|
dbg.breakpoints.list()
|
|
dbg.traps.list()
|
|
dbg.watches.list()
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
|
|
case cmdClear:
|
|
clear, _ := tokens.Get()
|
|
clear = strings.ToUpper(clear)
|
|
switch clear {
|
|
case "BREAKS":
|
|
dbg.breakpoints.clear()
|
|
dbg.print(console.StyleFeedback, "breakpoints cleared")
|
|
case "TRAPS":
|
|
dbg.traps.clear()
|
|
dbg.print(console.StyleFeedback, "traps cleared")
|
|
case "WATCHES":
|
|
dbg.watches.clear()
|
|
dbg.print(console.StyleFeedback, "watches cleared")
|
|
case "ALL":
|
|
dbg.breakpoints.clear()
|
|
dbg.traps.clear()
|
|
dbg.watches.clear()
|
|
dbg.print(console.StyleFeedback, "breakpoints, traps and watches cleared")
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
|
|
case cmdDrop:
|
|
drop, _ := tokens.Get()
|
|
|
|
s, _ := tokens.Get()
|
|
num, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, fmt.Sprintf("drop attribute must be a number (%s)", s))
|
|
}
|
|
|
|
drop = strings.ToUpper(drop)
|
|
switch drop {
|
|
case "BREAK":
|
|
err := dbg.breakpoints.drop(num)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
dbg.print(console.StyleFeedback, "breakpoint #%d dropped", num)
|
|
case "TRAP":
|
|
err := dbg.traps.drop(num)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
dbg.print(console.StyleFeedback, "trap #%d dropped", num)
|
|
case "WATCH":
|
|
err := dbg.watches.drop(num)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
dbg.print(console.StyleFeedback, "watch #%d dropped", num)
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
|
|
case cmdOnHalt:
|
|
if tokens.Remaining() == 0 {
|
|
if dbg.commandOnHalt == "" {
|
|
dbg.print(console.StyleFeedback, "auto-command on halt: OFF")
|
|
} else {
|
|
dbg.print(console.StyleFeedback, "auto-command on halt: %s", dbg.commandOnHalt)
|
|
}
|
|
return doNothing, nil
|
|
}
|
|
|
|
// !!TODO: non-interactive check of tokens against scriptUnsafeTemplate
|
|
//
|
|
var newCommands string
|
|
|
|
option, _ := tokens.Peek()
|
|
switch strings.ToUpper(option) {
|
|
case "OFF":
|
|
newCommands = ""
|
|
case "ON":
|
|
newCommands = dbg.commandOnHaltStored
|
|
default:
|
|
// use remaininder of command line to form the ONHALT command sequence
|
|
newCommands = tokens.Remainder()
|
|
|
|
// we can't use semi-colons when specifying the sequence so allow use of
|
|
// commas to act as an alternative
|
|
newCommands = strings.Replace(newCommands, ",", ";", -1)
|
|
}
|
|
|
|
// run the new/restored ONHALT command(s)
|
|
_, err := dbg.parseInput(newCommands, false, false)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
|
|
dbg.commandOnHalt = newCommands
|
|
|
|
// display the new/restored ONHALT command(s)
|
|
if newCommands == "" {
|
|
dbg.print(console.StyleFeedback, "auto-command on halt: OFF")
|
|
} else {
|
|
dbg.print(console.StyleFeedback, "auto-command on halt: %s", dbg.commandOnHalt)
|
|
|
|
// store the new command so we can reuse it after an ONHALT OFF
|
|
// !!TODO: normalise case of specified command sequence
|
|
dbg.commandOnHaltStored = newCommands
|
|
}
|
|
|
|
return doNothing, nil
|
|
|
|
case cmdOnStep:
|
|
if tokens.Remaining() == 0 {
|
|
if dbg.commandOnHalt == "" {
|
|
dbg.print(console.StyleFeedback, "auto-command on step: OFF")
|
|
} else {
|
|
dbg.print(console.StyleFeedback, "auto-command on step: %s", dbg.commandOnStep)
|
|
}
|
|
return doNothing, nil
|
|
}
|
|
|
|
// !!TODO: non-interactive check of tokens against scriptUnsafeTemplate
|
|
|
|
var newCommands string
|
|
|
|
option, _ := tokens.Peek()
|
|
switch strings.ToUpper(option) {
|
|
case "OFF":
|
|
newCommands = ""
|
|
case "ON":
|
|
newCommands = dbg.commandOnStepStored
|
|
default:
|
|
// use remaininder of command line to form the ONSTEP command sequence
|
|
newCommands = tokens.Remainder()
|
|
|
|
// we can't use semi-colons when specifying the sequence so allow use of
|
|
// commas to act as an alternative
|
|
newCommands = strings.Replace(newCommands, ",", ";", -1)
|
|
}
|
|
|
|
// run the new/restored ONSTEP command(s)
|
|
_, err := dbg.parseInput(newCommands, false, false)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
|
|
dbg.commandOnStep = newCommands
|
|
|
|
// display the new/restored ONSTEP command(s)
|
|
if newCommands == "" {
|
|
dbg.print(console.StyleFeedback, "auto-command on step: OFF")
|
|
} else {
|
|
dbg.print(console.StyleFeedback, "auto-command on step: %s", dbg.commandOnStep)
|
|
|
|
// store the new command so we can reuse it after an ONSTEP OFF
|
|
// !!TODO: normalise case of specified command sequence
|
|
dbg.commandOnStepStored = newCommands
|
|
}
|
|
|
|
return doNothing, nil
|
|
|
|
case cmdLast:
|
|
if dbg.lastResult != nil {
|
|
option, ok := tokens.Get()
|
|
if ok {
|
|
switch strings.ToUpper(option) {
|
|
case "DEFN":
|
|
dbg.print(console.StyleFeedback, "%s", dbg.lastResult.Defn)
|
|
}
|
|
} else {
|
|
var printTag console.Style
|
|
if dbg.lastResult.Final {
|
|
printTag = console.StyleCPUStep
|
|
} else {
|
|
printTag = console.StyleVideoStep
|
|
}
|
|
dbg.print(printTag, "%s", dbg.lastResult.GetString(dbg.disasm.Symtable, result.StyleExecution))
|
|
}
|
|
}
|
|
|
|
case cmdMemMap:
|
|
dbg.print(console.StyleMachineInfo, "%v", dbg.vcs.Mem.MemoryMap())
|
|
|
|
case cmdExit:
|
|
fallthrough
|
|
|
|
case cmdQuit:
|
|
dbg.running = false
|
|
|
|
case cmdReset:
|
|
err := dbg.vcs.Reset()
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
err = dbg.gui.Reset()
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
dbg.print(console.StyleFeedback, "machine reset")
|
|
|
|
case cmdRun:
|
|
if !dbg.gui.IsVisible() && dbg.commandOnStep == "" {
|
|
dbg.print(console.StyleEmulatorInfo, "running with no display or terminal output")
|
|
}
|
|
dbg.runUntilHalt = true
|
|
return stepContinue, nil
|
|
|
|
case cmdStep:
|
|
mode, _ := tokens.Get()
|
|
mode = strings.ToUpper(mode)
|
|
switch mode {
|
|
case "":
|
|
// calling step with no argument is the normal case
|
|
case "CPU":
|
|
// changes granularity
|
|
dbg.inputEveryVideoCycle = false
|
|
case "VIDEO":
|
|
// changes granularity
|
|
dbg.inputEveryVideoCycle = true
|
|
default:
|
|
dbg.inputEveryVideoCycle = false
|
|
tokens.Unget()
|
|
err := dbg.stepTraps.parseTrap(tokens)
|
|
if err != nil {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, fmt.Sprintf("unknown step mode (%s)", mode))
|
|
}
|
|
dbg.runUntilHalt = true
|
|
}
|
|
|
|
return setDefaultStep, nil
|
|
|
|
case cmdGranularity:
|
|
mode, present := tokens.Get()
|
|
if present {
|
|
mode = strings.ToUpper(mode)
|
|
switch mode {
|
|
case "CPU":
|
|
dbg.inputEveryVideoCycle = false
|
|
case "VIDEO":
|
|
dbg.inputEveryVideoCycle = true
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
}
|
|
if dbg.inputEveryVideoCycle {
|
|
mode = "VIDEO"
|
|
} else {
|
|
mode = "CPU"
|
|
}
|
|
dbg.print(console.StyleFeedback, "granularity: %s", mode)
|
|
|
|
case cmdTerse:
|
|
dbg.machineInfoVerbose = false
|
|
dbg.print(console.StyleFeedback, "verbosity: terse")
|
|
|
|
case cmdVerbose:
|
|
dbg.machineInfoVerbose = true
|
|
dbg.print(console.StyleFeedback, "verbosity: verbose")
|
|
|
|
case cmdVerbosity:
|
|
if dbg.machineInfoVerbose {
|
|
dbg.print(console.StyleFeedback, "verbosity: verbose")
|
|
} else {
|
|
dbg.print(console.StyleFeedback, "verbosity: terse")
|
|
}
|
|
|
|
case cmdDebuggerState:
|
|
_, err := dbg.parseInput("VERBOSITY; STEPMODE; ONHALT; ONSTEP", false, false)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
|
|
case cmdCartridge:
|
|
arg, ok := tokens.Get()
|
|
if ok {
|
|
switch arg {
|
|
case "ANALYSIS":
|
|
dbg.print(console.StyleFeedback, dbg.disasm.String())
|
|
}
|
|
} else {
|
|
dbg.printMachineInfo(dbg.vcs.Mem.Cart)
|
|
}
|
|
|
|
case cmdCPU:
|
|
action, present := tokens.Get()
|
|
if present {
|
|
switch strings.ToUpper(action) {
|
|
case "SET":
|
|
target, _ := tokens.Get()
|
|
|
|
var reg *register.Register
|
|
switch strings.ToUpper(target) {
|
|
case "PC":
|
|
reg = dbg.vcs.CPU.PC
|
|
case "A":
|
|
reg = dbg.vcs.CPU.A
|
|
case "X":
|
|
reg = dbg.vcs.CPU.X
|
|
case "Y":
|
|
reg = dbg.vcs.CPU.Y
|
|
case "SP":
|
|
reg = dbg.vcs.CPU.SP
|
|
}
|
|
|
|
value, _ := tokens.Get()
|
|
|
|
v, err := strconv.ParseUint(value, 0, int(reg.Size()))
|
|
if err != nil {
|
|
dbg.print(console.StyleError, "value must be a positive %dbit number", reg.Size())
|
|
}
|
|
|
|
reg.Load(v)
|
|
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
} else {
|
|
dbg.printMachineInfo(dbg.vcs.CPU)
|
|
}
|
|
|
|
case cmdPeek:
|
|
// get first address token
|
|
a, present := tokens.Get()
|
|
|
|
for present {
|
|
// perform peek
|
|
ai, err := dbg.dbgmem.peek(a)
|
|
if err != nil {
|
|
dbg.print(console.StyleError, "%s", err)
|
|
} else {
|
|
dbg.print(console.StyleMachineInfo, ai.String())
|
|
}
|
|
|
|
// loop through all addresses
|
|
a, present = tokens.Get()
|
|
}
|
|
|
|
case cmdPoke:
|
|
// get address token
|
|
a, _ := tokens.Get()
|
|
|
|
// get value token
|
|
v, _ := tokens.Get()
|
|
|
|
val, err := strconv.ParseUint(v, 0, 8)
|
|
if err != nil {
|
|
dbg.print(console.StyleError, "poke value must be 8bit number (%s)", v)
|
|
return doNothing, nil
|
|
}
|
|
|
|
// perform single poke
|
|
ai, err := dbg.dbgmem.poke(a, uint8(val))
|
|
if err != nil {
|
|
dbg.print(console.StyleError, "%s", err)
|
|
} else {
|
|
dbg.print(console.StyleMachineInfo, ai.String())
|
|
}
|
|
|
|
case cmdHexLoad:
|
|
// get address token
|
|
a, _ := tokens.Get()
|
|
|
|
addr, err := strconv.ParseUint(a, 0, 16)
|
|
if err != nil {
|
|
dbg.print(console.StyleError, "hexload address must be 16bit number (%s)", a)
|
|
return doNothing, nil
|
|
}
|
|
|
|
// get (first) value token
|
|
v, present := tokens.Get()
|
|
|
|
for present {
|
|
val, err := strconv.ParseUint(v, 0, 8)
|
|
if err != nil {
|
|
dbg.print(console.StyleError, "hexload value must be 8bit number (%s)", addr)
|
|
v, present = tokens.Get()
|
|
continue // for loop (without advancing address)
|
|
}
|
|
|
|
// perform individual poke
|
|
ai, err := dbg.dbgmem.poke(uint16(addr), uint8(val))
|
|
if err != nil {
|
|
dbg.print(console.StyleError, "%s", err)
|
|
} else {
|
|
dbg.print(console.StyleMachineInfo, ai.String())
|
|
}
|
|
|
|
// loop through all values
|
|
v, present = tokens.Get()
|
|
addr++
|
|
}
|
|
|
|
case cmdRAM:
|
|
option, present := tokens.Get()
|
|
if present {
|
|
option = strings.ToUpper(option)
|
|
switch option {
|
|
case "CART":
|
|
cartRAM := dbg.vcs.Mem.Cart.RAM()
|
|
if len(cartRAM) > 0 {
|
|
// !!TODO: better presentation of cartridge RAM
|
|
dbg.print(console.StyleMachineInfo, fmt.Sprintf("%v", dbg.vcs.Mem.Cart.RAM()))
|
|
} else {
|
|
dbg.print(console.StyleFeedback, "cartridge does not contain any additional RAM")
|
|
}
|
|
|
|
}
|
|
} else {
|
|
dbg.printMachineInfo(dbg.vcs.Mem.PIA)
|
|
}
|
|
|
|
case cmdRIOT:
|
|
option, present := tokens.Get()
|
|
if present {
|
|
option = strings.ToUpper(option)
|
|
switch option {
|
|
case "TIMER":
|
|
dbg.printMachineInfo(dbg.vcs.RIOT.Timer)
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
} else {
|
|
dbg.printMachineInfo(dbg.vcs.RIOT)
|
|
}
|
|
|
|
case cmdTIA:
|
|
option, present := tokens.Get()
|
|
if present {
|
|
// which machine info printing function we use depends on the
|
|
// precise argument provided
|
|
printFunc := dbg.printMachineInfoVerbose
|
|
|
|
option = strings.ToUpper(option)
|
|
switch option {
|
|
case "DELAY":
|
|
printFunc = dbg.printMachineInfo
|
|
fallthrough
|
|
|
|
case "DELAYS":
|
|
printFunc(dbg.vcs.TIA.Delay)
|
|
|
|
// for convience asking for TIA delays also prints delays for
|
|
// the sprites
|
|
printFunc(dbg.vcs.TIA.Video.Player0.Delay)
|
|
printFunc(dbg.vcs.TIA.Video.Player1.Delay)
|
|
printFunc(dbg.vcs.TIA.Video.Missile0.Delay)
|
|
printFunc(dbg.vcs.TIA.Video.Missile1.Delay)
|
|
printFunc(dbg.vcs.TIA.Video.Ball.Delay)
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
} else {
|
|
dbg.printMachineInfo(dbg.vcs.TIA)
|
|
}
|
|
|
|
case cmdTV:
|
|
option, present := tokens.Get()
|
|
if present {
|
|
option = strings.ToUpper(option)
|
|
switch option {
|
|
case "SPEC":
|
|
dbg.print(console.StyleMachineInfo, dbg.gui.GetSpec().ID)
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
} else {
|
|
dbg.printMachineInfo(dbg.gui)
|
|
}
|
|
|
|
// information about the machine (sprites, playfield)
|
|
case cmdPlayer:
|
|
plyr := -1
|
|
|
|
arg, _ := tokens.Get()
|
|
switch arg {
|
|
case "0":
|
|
plyr = 0
|
|
case "1":
|
|
plyr = 1
|
|
default:
|
|
tokens.Unget()
|
|
}
|
|
|
|
if dbg.machineInfoVerbose {
|
|
// arrange the two player's information side by side in order to
|
|
// save space and to allow for easy comparison
|
|
|
|
switch plyr {
|
|
case 0:
|
|
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
|
|
dbg.print(console.StyleMachineInfo, strings.Join(p0, "\n"))
|
|
|
|
case 1:
|
|
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
|
|
dbg.print(console.StyleMachineInfo, strings.Join(p1, "\n"))
|
|
|
|
default:
|
|
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
|
|
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
|
|
|
|
ml := 0
|
|
for i := range p0 {
|
|
if len(p0[i]) > ml {
|
|
ml = len(p0[i])
|
|
}
|
|
}
|
|
|
|
s := strings.Builder{}
|
|
for i := range p0 {
|
|
if p0[i] != "" {
|
|
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
|
|
}
|
|
}
|
|
dbg.print(console.StyleMachineInfo, s.String())
|
|
}
|
|
} else {
|
|
switch plyr {
|
|
case 0:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
|
|
|
|
case 1:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
|
|
|
|
default:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
|
|
}
|
|
}
|
|
|
|
case cmdMissile:
|
|
miss := -1
|
|
|
|
arg, _ := tokens.Get()
|
|
switch arg {
|
|
case "0":
|
|
miss = 0
|
|
case "1":
|
|
miss = 1
|
|
default:
|
|
tokens.Unget()
|
|
}
|
|
|
|
if dbg.machineInfoVerbose {
|
|
// arrange the two missile'sinformation side by side in order to
|
|
// save space and to allow for easy comparison
|
|
|
|
switch miss {
|
|
case 0:
|
|
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
|
|
dbg.print(console.StyleMachineInfo, strings.Join(p0, "\n"))
|
|
|
|
case 1:
|
|
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile1), "\n")
|
|
dbg.print(console.StyleMachineInfo, strings.Join(p1, "\n"))
|
|
|
|
default:
|
|
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
|
|
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile1), "\n")
|
|
|
|
ml := 0
|
|
for i := range p0 {
|
|
if len(p0[i]) > ml {
|
|
ml = len(p0[i])
|
|
}
|
|
}
|
|
|
|
s := strings.Builder{}
|
|
for i := range p0 {
|
|
if p0[i] != "" {
|
|
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
|
|
}
|
|
}
|
|
dbg.print(console.StyleMachineInfo, s.String())
|
|
}
|
|
} else {
|
|
switch miss {
|
|
case 0:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
|
|
|
|
case 1:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
|
|
|
|
default:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
|
|
}
|
|
}
|
|
|
|
case cmdBall:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Ball)
|
|
|
|
case cmdPlayfield:
|
|
dbg.printMachineInfo(dbg.vcs.TIA.Video.Playfield)
|
|
|
|
case cmdDisplay:
|
|
var err error
|
|
|
|
action, present := tokens.Get()
|
|
if present {
|
|
action = strings.ToUpper(action)
|
|
switch action {
|
|
case "OFF":
|
|
err = dbg.gui.SetFeature(gui.ReqSetVisibility, false)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
case "DEBUG":
|
|
err = dbg.gui.SetFeature(gui.ReqToggleMasking)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
case "SCALE":
|
|
scl, present := tokens.Get()
|
|
if !present {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, fmt.Sprintf("value required for %s %s", command, action))
|
|
}
|
|
|
|
scale, err := strconv.ParseFloat(scl, 32)
|
|
if err != nil {
|
|
return doNothing, errors.NewFormattedError(errors.CommandError, fmt.Sprintf("%s %s value not valid (%s)", command, action, scl))
|
|
}
|
|
|
|
err = dbg.gui.SetFeature(gui.ReqSetScale, float32(scale))
|
|
return doNothing, err
|
|
case "DEBUGCOLORS":
|
|
err = dbg.gui.SetFeature(gui.ReqToggleAltColors)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
case "METASIGNALS":
|
|
err = dbg.gui.SetFeature(gui.ReqToggleShowMetaVideo)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
default:
|
|
// already caught by command line ValidateTokens()
|
|
}
|
|
} else {
|
|
err = dbg.gui.SetFeature(gui.ReqSetVisibility, true)
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
}
|
|
|
|
case cmdStick:
|
|
var err error
|
|
|
|
stick, _ := tokens.Get()
|
|
action, _ := tokens.Get()
|
|
|
|
var event peripherals.Event
|
|
switch strings.ToUpper(action) {
|
|
case "UP":
|
|
event = peripherals.Up
|
|
case "DOWN":
|
|
event = peripherals.Down
|
|
case "LEFT":
|
|
event = peripherals.Left
|
|
case "RIGHT":
|
|
event = peripherals.Right
|
|
case "NOUP":
|
|
event = peripherals.NoUp
|
|
case "NODOWN":
|
|
event = peripherals.NoDown
|
|
case "NOLEFT":
|
|
event = peripherals.NoLeft
|
|
case "NORIGHT":
|
|
event = peripherals.NoRight
|
|
case "FIRE":
|
|
event = peripherals.Fire
|
|
case "NOFIRE":
|
|
event = peripherals.NoFire
|
|
}
|
|
|
|
n, _ := strconv.Atoi(stick)
|
|
switch n {
|
|
case 0:
|
|
err = dbg.vcs.Ports.Player0.Handle(event)
|
|
case 1:
|
|
err = dbg.vcs.Ports.Player1.Handle(event)
|
|
}
|
|
|
|
if err != nil {
|
|
return doNothing, err
|
|
}
|
|
|
|
case cmdDigest:
|
|
arg, ok := tokens.Get()
|
|
if ok {
|
|
switch arg {
|
|
case "RESET":
|
|
dbg.digest.ResetDigest()
|
|
}
|
|
} else {
|
|
dbg.print(console.StyleFeedback, dbg.digest.String())
|
|
}
|
|
}
|
|
|
|
return doNothing, nil
|
|
}
|