mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 05:40:49 -04:00
input system and ports system separated
playback/recorder and driven input systems moved out of the the ports package and into a new input package. how the input systems interact has been clarified and improved - for example, it is now posssible for a playback file to be used to drive two emulations for comparison purposes the debugger startup procedure has been clarified with two distinct startup functions for playmode and debugger - each of which take different arguments. the clarity has allowed the reintroduction of recording and playback to the main play mode
This commit is contained in:
parent
7e5326fe83
commit
58848acdf9
|
@ -30,9 +30,9 @@ type TV interface {
|
|||
AddAudioMixer(television.AudioMixer)
|
||||
}
|
||||
|
||||
// VCS defines the VCS functions required by a bot.
|
||||
type VCS interface {
|
||||
QueueEvent(ports.InputEvent) error
|
||||
// Input defines the Input functions required by a bot.
|
||||
type Input interface {
|
||||
PushEvent(ports.InputEvent) error
|
||||
}
|
||||
|
||||
// Diagnostic instances are sent over the Feedback Diagnostic channel.
|
||||
|
|
|
@ -152,8 +152,8 @@ func (obs *observer) EndRendering() error {
|
|||
type videoChessBot struct {
|
||||
obs *observer
|
||||
|
||||
vcs bots.VCS
|
||||
tv bots.TV
|
||||
input bots.Input
|
||||
tv bots.TV
|
||||
|
||||
// quit as soon as possible when a value appears on the channel
|
||||
quit chan bool
|
||||
|
@ -364,9 +364,9 @@ func (bot *videoChessBot) moveCursorOnceStep(portid plugging.PortID, direction p
|
|||
|
||||
waiting := true
|
||||
for waiting {
|
||||
bot.vcs.QueueEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickTrue})
|
||||
bot.input.PushEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickTrue})
|
||||
bot.waitForFrames(downDuration)
|
||||
bot.vcs.QueueEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickFalse})
|
||||
bot.input.PushEvent(ports.InputEvent{Port: portid, Ev: direction, D: ports.DataStickFalse})
|
||||
select {
|
||||
case <-bot.obs.audioFeedback:
|
||||
waiting = false
|
||||
|
@ -462,9 +462,9 @@ func (bot *videoChessBot) moveCursor(moveCol int, moveRow int, shortcut bool) {
|
|||
|
||||
waiting := true
|
||||
for waiting {
|
||||
bot.vcs.QueueEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: true})
|
||||
bot.input.PushEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: true})
|
||||
bot.waitForFrames(downDuration)
|
||||
bot.vcs.QueueEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: false})
|
||||
bot.input.PushEvent(ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.Fire, D: false})
|
||||
select {
|
||||
case <-bot.obs.audioFeedback:
|
||||
waiting = false
|
||||
|
@ -489,10 +489,10 @@ func (bot *videoChessBot) waitForFrames(n int) {
|
|||
}
|
||||
|
||||
// NewVideoChess creates a new bot able to play chess (via a UCI engine).
|
||||
func NewVideoChess(vcs bots.VCS, tv bots.TV) (bots.Bot, error) {
|
||||
func NewVideoChess(vcs bots.Input, tv bots.TV) (bots.Bot, error) {
|
||||
bot := &videoChessBot{
|
||||
obs: newObserver(),
|
||||
vcs: vcs,
|
||||
input: vcs,
|
||||
tv: tv,
|
||||
quit: make(chan bool),
|
||||
prevPosition: image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines)),
|
||||
|
|
|
@ -26,17 +26,17 @@ import (
|
|||
|
||||
// Bots keeps track of the running bot and handles loading and termination.
|
||||
type Bots struct {
|
||||
vcs bots.VCS
|
||||
tv bots.TV
|
||||
input bots.Input
|
||||
tv bots.TV
|
||||
|
||||
running bots.Bot
|
||||
}
|
||||
|
||||
// NewBots is the preferred method of initialisation for the Bots type.
|
||||
func NewBots(vcs bots.VCS, tv bots.TV) *Bots {
|
||||
func NewBots(input bots.Input, tv bots.TV) *Bots {
|
||||
return &Bots{
|
||||
vcs: vcs,
|
||||
tv: tv,
|
||||
input: input,
|
||||
tv: tv,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ func (b *Bots) ActivateBot(cartHash string) (*bots.Feedback, error) {
|
|||
|
||||
switch cartHash {
|
||||
case "043ef523e4fcb9fc2fc2fda21f15671bf8620fc3":
|
||||
b.running, err = chess.NewVideoChess(b.vcs, b.tv)
|
||||
b.running, err = chess.NewVideoChess(b.input, b.tv)
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("bots: %v", err)
|
||||
}
|
||||
|
|
|
@ -98,11 +98,11 @@ func NewComparison(driverVCS *hardware.VCS) (*Comparison, error) {
|
|||
|
||||
// synchronise RIOT ports
|
||||
sync := make(chan ports.TimedInputEvent, 32)
|
||||
err = cmp.VCS.RIOT.Ports.SynchroniseWithDriver(sync, tv)
|
||||
err = cmp.VCS.Input.AttachPassenger(sync)
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("comparison: %v", err)
|
||||
}
|
||||
err = driverVCS.RIOT.Ports.SynchroniseWithPassenger(sync, driverVCS.TV)
|
||||
err = driverVCS.Input.AttachDriver(sync)
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("comparison: %v", err)
|
||||
}
|
||||
|
@ -180,6 +180,7 @@ func (cmp *Comparison) CreateFromLoader(cartload cartridgeloader.Loader) error {
|
|||
cmp.isEmulating.Store(false)
|
||||
}()
|
||||
|
||||
// not using setup system to attach cartridge. maybe we should?
|
||||
err := cmp.VCS.AttachCartridge(cartload)
|
||||
if err != nil {
|
||||
cmp.driver.quit <- err
|
||||
|
|
|
@ -1352,55 +1352,55 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
|
|||
switch strings.ToUpper(arg) {
|
||||
case "P0":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer0Pro, D: nil}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "P1":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer1Pro, D: nil}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "COL":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelToggleColor, D: nil}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
}
|
||||
case "SET":
|
||||
arg, _ := tokens.Get()
|
||||
switch strings.ToUpper(arg) {
|
||||
case "P0PRO":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "P1PRO":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "P0AM":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "P1AM":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "COL":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "BW":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
}
|
||||
case "HOLD":
|
||||
arg, _ := tokens.Get()
|
||||
switch strings.ToUpper(arg) {
|
||||
case "SELECT":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: true}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "RESET":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: true}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
}
|
||||
case "RELEASE":
|
||||
arg, _ := tokens.Get()
|
||||
switch strings.ToUpper(arg) {
|
||||
case "SELECT":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: false}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case "RESET":
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: false}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1457,10 +1457,10 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
|
|||
switch n {
|
||||
case 0:
|
||||
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: event, D: value}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
case 1:
|
||||
inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: event, D: value}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -1478,18 +1478,18 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
|
|||
case 0:
|
||||
if strings.ToUpper(key) == "NONE" {
|
||||
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadUp, D: nil}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
} else {
|
||||
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadDown, D: rune(key[0])}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
}
|
||||
case 1:
|
||||
if strings.ToUpper(key) == "NONE" {
|
||||
inp := ports.InputEvent{Port: plugging.PortRightPlayer, Ev: ports.KeypadUp, D: nil}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
} else {
|
||||
inp := ports.InputEvent{Port: plugging.PortLeftPlayer, Ev: ports.KeypadDown, D: rune(key[0])}
|
||||
_, err = dbg.vcs.HandleInputEvent(inp)
|
||||
_, err = dbg.vcs.Input.HandleInputEvent(inp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,8 +45,10 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
"github.com/jetsetilly/gopher2600/logger"
|
||||
"github.com/jetsetilly/gopher2600/prefs"
|
||||
"github.com/jetsetilly/gopher2600/recorder"
|
||||
"github.com/jetsetilly/gopher2600/reflection"
|
||||
"github.com/jetsetilly/gopher2600/reflection/counter"
|
||||
"github.com/jetsetilly/gopher2600/resources/unique"
|
||||
"github.com/jetsetilly/gopher2600/rewind"
|
||||
"github.com/jetsetilly/gopher2600/setup"
|
||||
"github.com/jetsetilly/gopher2600/tracker"
|
||||
|
@ -85,6 +87,10 @@ type Debugger struct {
|
|||
// sure Close() is called on end
|
||||
loader *cartridgeloader.Loader
|
||||
|
||||
// gameplay recorder/playback
|
||||
recorder *recorder.Recorder
|
||||
playback *recorder.Playback
|
||||
|
||||
// comparison emulator
|
||||
comparison *comparison.Comparison
|
||||
|
||||
|
@ -304,7 +310,7 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
|
|||
|
||||
// create userinput/controllers handler
|
||||
dbg.controllers = userinput.NewControllers()
|
||||
dbg.controllers.AddInputHandler(dbg.vcs)
|
||||
dbg.controllers.AddInputHandler(dbg.vcs.Input)
|
||||
|
||||
// replace player 1 port with savekey
|
||||
if useSavekey {
|
||||
|
@ -315,7 +321,7 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
|
|||
}
|
||||
|
||||
// create bot coordinator
|
||||
dbg.bots = wrangler.NewBots(dbg.vcs, dbg.vcs.TV)
|
||||
dbg.bots = wrangler.NewBots(dbg.vcs.Input, dbg.vcs.TV)
|
||||
|
||||
// set up debugging interface to memory
|
||||
dbg.dbgmem = &dbgmem.DbgMem{
|
||||
|
@ -400,6 +406,12 @@ func NewDebugger(create CreateUserInterface, spec string, useSavekey bool, fpsCa
|
|||
dbg.vcs.TV.SetFPSCap(fpsCap)
|
||||
dbg.gui.SetFeature(gui.ReqMonitorSync, fpsCap)
|
||||
|
||||
// initialise terminal
|
||||
err = dbg.term.Initialise()
|
||||
if err != nil {
|
||||
return nil, curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
return dbg, nil
|
||||
}
|
||||
|
||||
|
@ -449,10 +461,13 @@ func (dbg *Debugger) setState(state emulation.State) {
|
|||
// * see setState() comment, although debugger.SetFeature(ReqSetPause) will
|
||||
// always be "noisy"
|
||||
func (dbg *Debugger) setStateQuiet(state emulation.State, quiet bool) {
|
||||
// do not allow comparison emulation in the rewinding state. remove it if
|
||||
// we ever enter the rewinding state
|
||||
if state == emulation.Rewinding {
|
||||
dbg.endPlayback()
|
||||
dbg.endRecording()
|
||||
dbg.endComparison()
|
||||
|
||||
// it is thought that bots are okay to enter the rewinding state. this
|
||||
// might not be true in all future cases.
|
||||
}
|
||||
|
||||
dbg.vcs.TV.SetEmulationState(state)
|
||||
|
@ -487,6 +502,9 @@ func (dbg *Debugger) setMode(mode emulation.Mode) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// don't stop the recording, playback, comparison or bot sub-systems on
|
||||
// change of mode
|
||||
|
||||
// if there is a halting condition that is not allowed in playmode (see
|
||||
// targets type) then do not change the emulation mode
|
||||
//
|
||||
|
@ -565,10 +583,15 @@ func (dbg *Debugger) setMode(mode emulation.Mode) error {
|
|||
|
||||
// End cleans up any resources that may be dangling.
|
||||
func (dbg *Debugger) end() {
|
||||
dbg.endPlayback()
|
||||
dbg.endRecording()
|
||||
dbg.endComparison()
|
||||
dbg.bots.Quit()
|
||||
|
||||
dbg.vcs.End()
|
||||
|
||||
defer dbg.term.CleanUp()
|
||||
|
||||
// set ending state
|
||||
err := dbg.gui.SetFeature(gui.ReqEnd)
|
||||
if err != nil {
|
||||
|
@ -582,57 +605,33 @@ func (dbg *Debugger) end() {
|
|||
}
|
||||
}
|
||||
|
||||
// Starts the main emulation sequence.
|
||||
func (dbg *Debugger) Start(mode emulation.Mode, initScript string, cartload cartridgeloader.Loader, comparisonROM string, comparisonPrefs string) error {
|
||||
// do not allow comparison emulation inside the debugger. it's far too
|
||||
// complicated running two emulations that must be synced in the debugger
|
||||
// loop
|
||||
if mode == emulation.ModeDebugger && comparisonROM != "" {
|
||||
return curated.Errorf("debugger: cannot run comparison emulation inside the debugger")
|
||||
}
|
||||
// StartInDebugMode starts the emulation with the debugger activated.
|
||||
func (dbg *Debugger) StartInDebugMode(initScript string, filename string, mapping string) error {
|
||||
// set running flag as early as possible
|
||||
dbg.running = true
|
||||
|
||||
var err error
|
||||
var cartload cartridgeloader.Loader
|
||||
|
||||
defer dbg.end()
|
||||
err = dbg.start(mode, initScript, cartload, comparisonROM, comparisonPrefs)
|
||||
if err != nil {
|
||||
if curated.Has(err, terminal.UserQuit) {
|
||||
return nil
|
||||
if filename == "" {
|
||||
cartload = cartridgeloader.Loader{}
|
||||
} else {
|
||||
cartload, err = cartridgeloader.NewLoader(filename, mapping)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cartridgeloader.Loader, comparisonROM string, comparisonPrefs string) error {
|
||||
// prepare user interface
|
||||
err := dbg.term.Initialise()
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
defer dbg.term.CleanUp()
|
||||
|
||||
err = dbg.attachCartridge(cartload)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
err = dbg.startComparison(comparisonROM, comparisonPrefs)
|
||||
err = dbg.setMode(emulation.ModeDebugger)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
// set mode to the value requested in the function paramenters
|
||||
err = dbg.setMode(mode)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
// debugger is running
|
||||
dbg.running = true
|
||||
|
||||
// run initialisation script
|
||||
if initScript != "" {
|
||||
scr, err := script.RescribeScript(initScript)
|
||||
if err == nil {
|
||||
|
@ -647,6 +646,80 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
|
|||
}
|
||||
}
|
||||
|
||||
defer dbg.end()
|
||||
err = dbg.run()
|
||||
if err != nil {
|
||||
if curated.Has(err, terminal.UserQuit) {
|
||||
return nil
|
||||
}
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartInPlaymode starts the emulation ready for game-play.
|
||||
func (dbg *Debugger) StartInPlayMode(filename string, mapping string, record bool, comparisonROM string, comparisonPrefs string) error {
|
||||
// set running flag as early as possible
|
||||
dbg.running = true
|
||||
|
||||
var err error
|
||||
var cartload cartridgeloader.Loader
|
||||
|
||||
if filename == "" {
|
||||
cartload = cartridgeloader.Loader{}
|
||||
} else {
|
||||
cartload, err = cartridgeloader.NewLoader(filename, mapping)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = recorder.IsPlaybackFile(filename)
|
||||
if err != nil {
|
||||
if !curated.Is(err, recorder.NotAPlaybackFile) {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
err = dbg.attachCartridge(cartload)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
if record {
|
||||
dbg.startRecording(cartload.ShortName())
|
||||
}
|
||||
} else {
|
||||
if record {
|
||||
return curated.Errorf("debugger: cannot make a new recording using a playback file")
|
||||
}
|
||||
|
||||
dbg.startPlayback(filename)
|
||||
}
|
||||
|
||||
err = dbg.startComparison(comparisonROM, comparisonPrefs)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
err = dbg.setMode(emulation.ModePlay)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
defer dbg.end()
|
||||
err = dbg.run()
|
||||
if err != nil {
|
||||
if curated.Has(err, terminal.UserQuit) {
|
||||
return nil
|
||||
}
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbg *Debugger) run() error {
|
||||
// end script recording gracefully. this way we don't have to worry too
|
||||
// hard about script scribes
|
||||
defer func() {
|
||||
|
@ -660,7 +733,7 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
|
|||
for dbg.running {
|
||||
switch dbg.Mode() {
|
||||
case emulation.ModePlay:
|
||||
err = dbg.playLoop()
|
||||
err := dbg.playLoop()
|
||||
if err != nil {
|
||||
// if we ever encounter a cartridge ejected error in playmode
|
||||
// then simply open up the ROM selector
|
||||
|
@ -685,7 +758,7 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
|
|||
return curated.Errorf("emulation state not supported on *start* of debugging loop: %s", dbg.State())
|
||||
}
|
||||
|
||||
err = dbg.inputLoop(dbg.term, false)
|
||||
err := dbg.inputLoop(dbg.term, false)
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
@ -707,7 +780,7 @@ func (dbg *Debugger) start(mode emulation.Mode, initScript string, cartload cart
|
|||
|
||||
// make sure any cartridge loader has been finished with
|
||||
if dbg.loader != nil {
|
||||
err = dbg.loader.Close()
|
||||
err := dbg.loader.Close()
|
||||
if err != nil {
|
||||
return curated.Errorf("debugger: %v", err)
|
||||
}
|
||||
|
@ -752,7 +825,9 @@ func (dbg *Debugger) reset(newCartridge bool) error {
|
|||
// this is the glue that hold the cartridge and disassembly packages together.
|
||||
// especially important is the repointing of the symbols table in the instance of dbgmem.
|
||||
func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error) {
|
||||
// stop any existing comparison emulation and any existing bots
|
||||
// stop optional sub-systems that shouldn't survive a new cartridge insertion
|
||||
dbg.endPlayback()
|
||||
dbg.endRecording()
|
||||
dbg.endComparison()
|
||||
dbg.bots.Quit()
|
||||
|
||||
|
@ -913,6 +988,63 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (dbg *Debugger) startRecording(cartShortName string) error {
|
||||
recording := unique.Filename("recording", cartShortName)
|
||||
|
||||
var err error
|
||||
|
||||
dbg.recorder, err = recorder.NewRecorder(recording, dbg.vcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbg *Debugger) endRecording() {
|
||||
if dbg.recorder == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
dbg.recorder = nil
|
||||
}()
|
||||
|
||||
err := dbg.recorder.End()
|
||||
if err != nil {
|
||||
logger.Logf("debugger", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (dbg *Debugger) startPlayback(filename string) error {
|
||||
plb, err := recorder.NewPlayback(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dbg.attachCartridge(plb.CartLoad)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = plb.AttachToVCSInput(dbg.vcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbg.playback = plb
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbg *Debugger) endPlayback() {
|
||||
if dbg.playback == nil {
|
||||
return
|
||||
}
|
||||
|
||||
dbg.playback = nil
|
||||
dbg.vcs.Input.AttachPlayback(nil)
|
||||
}
|
||||
|
||||
func (dbg *Debugger) startComparison(comparisonROM string, comparisonPrefs string) error {
|
||||
if comparisonROM == "" {
|
||||
return nil
|
||||
|
|
|
@ -20,7 +20,6 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
||||
"github.com/jetsetilly/gopher2600/debugger"
|
||||
"github.com/jetsetilly/gopher2600/debugger/terminal"
|
||||
"github.com/jetsetilly/gopher2600/emulation"
|
||||
|
@ -156,7 +155,7 @@ func TestDebugger_withNonExistantInitScript(t *testing.T) {
|
|||
|
||||
go trm.testSequence()
|
||||
|
||||
err = dbg.Start(emulation.ModeDebugger, "non_existent_script", cartridgeloader.Loader{}, "", "")
|
||||
err = dbg.StartInDebugMode("", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
@ -179,7 +178,7 @@ func TestDebugger(t *testing.T) {
|
|||
|
||||
go trm.testSequence()
|
||||
|
||||
err = dbg.Start(emulation.ModeDebugger, "", cartridgeloader.Loader{}, "", "")
|
||||
err = dbg.StartInDebugMode("", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
|
217
gopher2600.go
217
gopher2600.go
|
@ -44,8 +44,6 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/statsview"
|
||||
)
|
||||
|
||||
const defaultInitScript = "debuggerInit"
|
||||
|
||||
// communication between the main goroutine and the launch goroutine.
|
||||
type mainSync struct {
|
||||
state chan stateRequest
|
||||
|
@ -267,145 +265,7 @@ func launch(sync *mainSync) {
|
|||
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, PAL60")
|
||||
// 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")
|
||||
// multiload := md.AddInt("multiload", -1, "force multiload byte (supercharger only; 0 to 255)")
|
||||
// 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)
|
||||
// }
|
||||
|
||||
// var cartload cartridgeloader.Loader
|
||||
|
||||
// switch len(md.RemainingArgs()) {
|
||||
// case 0:
|
||||
// // allow loading from file requester
|
||||
|
||||
// case 1:
|
||||
// cartload, err = cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer cartload.Close()
|
||||
|
||||
// default:
|
||||
// return fmt.Errorf("too many arguments for %s mode", md)
|
||||
// }
|
||||
|
||||
// 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.state <- stateRequest{req: reqCreateGUI,
|
||||
// args: guiCreate(func() (guiControl, error) {
|
||||
// return sdlimgui.NewSdlImgui(tv)
|
||||
// }),
|
||||
// }
|
||||
|
||||
// // wait for creator result
|
||||
// var scr gui.GUI
|
||||
// select {
|
||||
// case g := <-sync.gui:
|
||||
// scr = g.(gui.GUI)
|
||||
|
||||
// case err := <-sync.guiError:
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // set fps cap
|
||||
// tv.SetFPSCap(*fpsCap)
|
||||
// scr.SetFeature(gui.ReqMonitorSync, *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
|
||||
// o, 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, *multiload)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// if o == 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(o, "play", playLaunch)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// if *record {
|
||||
// fmt.Println("! recording completed")
|
||||
// }
|
||||
|
||||
// // set ending state
|
||||
// err = scr.SetFeature(gui.ReqEnd)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
const defaultInitScript = "debuggerInit"
|
||||
|
||||
func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync) error {
|
||||
md.NewMode()
|
||||
|
@ -418,30 +278,49 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
|
|||
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
|
||||
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60")
|
||||
fpsCap := md.AddBool("fpscap", true, "cap fps to TV specification")
|
||||
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")
|
||||
log := md.AddBool("log", false, "echo debugging log to stdout")
|
||||
termType := md.AddString("term", "IMGUI", "terminal type to use in debug mode: IMGUI, COLOR, PLAIN")
|
||||
|
||||
// some arguments are mode specific
|
||||
// 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]")
|
||||
// multiload := md.AddInt("multiload", -1, "force multiload byte (supercharger only; 0 to 255)")
|
||||
|
||||
// playmode specific arguments
|
||||
var comparisonROM *string
|
||||
var comparisonPrefs *string
|
||||
var record *bool
|
||||
if emulationMode == emulation.ModePlay {
|
||||
comparisonROM = md.AddString("comparisonROM", "", "ROM to run in parallel for comparison")
|
||||
comparisonPrefs = md.AddString("comparisonPrefs", "", "preferences for comparison emulation")
|
||||
record = md.AddBool("record", false, "record user input to a file")
|
||||
}
|
||||
|
||||
stats := &[]bool{false}[0]
|
||||
// debugger specific arguments
|
||||
var initScript *string
|
||||
if emulationMode == emulation.ModeDebugger {
|
||||
initScript = md.AddString("initscript", defInitScript, "script to run on debugger start")
|
||||
}
|
||||
|
||||
// statsview if available
|
||||
var stats *bool
|
||||
if statsview.Available() {
|
||||
stats = md.AddBool("statsview", false, fmt.Sprintf("run stats server (%s)", statsview.Address))
|
||||
}
|
||||
|
||||
// parse arguments
|
||||
p, err := md.Parse()
|
||||
if err != nil || p != modalflag.ParseContinue {
|
||||
return err
|
||||
}
|
||||
|
||||
// check remaining arguments
|
||||
if len(md.RemainingArgs()) > 1 {
|
||||
return fmt.Errorf("too many arguments for %s mode", md)
|
||||
}
|
||||
|
||||
// set debugging log echo
|
||||
if *log {
|
||||
logger.SetEcho(os.Stdout)
|
||||
|
@ -449,7 +328,7 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
|
|||
logger.SetEcho(nil)
|
||||
}
|
||||
|
||||
if *stats {
|
||||
if stats != nil && *stats {
|
||||
statsview.Launch(os.Stdout)
|
||||
}
|
||||
|
||||
|
@ -460,6 +339,7 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
|
|||
// turning the emulation's interrupt handler off
|
||||
sync.state <- stateRequest{req: reqNoIntSig}
|
||||
|
||||
// GUI create function
|
||||
create := func(e emulation.Emulation) (gui.GUI, terminal.Terminal, error) {
|
||||
var term terminal.Terminal
|
||||
var scr gui.GUI
|
||||
|
@ -512,23 +392,6 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
|
|||
return err
|
||||
}
|
||||
|
||||
var cartload cartridgeloader.Loader
|
||||
|
||||
switch len(md.RemainingArgs()) {
|
||||
case 0:
|
||||
// allow launch with no ROM specified on command line
|
||||
|
||||
case 1:
|
||||
// cartridge loader. note that there is no deferred cartload.Close(). the
|
||||
// debugger type itself will handle this.
|
||||
cartload, err = cartridgeloader.NewLoader(md.GetArg(0), *mapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("too many arguments for %s mode", md)
|
||||
}
|
||||
|
||||
// check for profiling options
|
||||
prf, err := performance.ParseProfileString(*profile)
|
||||
if err != nil {
|
||||
|
@ -537,23 +400,21 @@ func emulate(emulationMode emulation.Mode, md *modalflag.Modes, sync *mainSync)
|
|||
|
||||
// set up a launch function
|
||||
dbgLaunch := func() error {
|
||||
// check if comparison was defined and dereference if it was, otherwise
|
||||
// comp is just the empty string
|
||||
var comp string
|
||||
if comparisonROM != nil {
|
||||
comp = *comparisonROM
|
||||
|
||||
switch emulationMode {
|
||||
case emulation.ModeDebugger:
|
||||
err := dbg.StartInDebugMode(*initScript, md.GetArg(0), *mapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case emulation.ModePlay:
|
||||
err := dbg.StartInPlayMode(md.GetArg(0), *mapping, *record, *comparisonROM, *comparisonPrefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// same for compPrefs
|
||||
var compPrefs string
|
||||
if comparisonPrefs != nil {
|
||||
compPrefs = *comparisonPrefs
|
||||
}
|
||||
|
||||
err := dbg.Start(emulationMode, *initScript, cartload, comp, compPrefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
52
hardware/input/doc.go
Normal file
52
hardware/input/doc.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
// 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/>.
|
||||
|
||||
// Pacakge input coordinates all the different types of input into the VCS.
|
||||
// The types of input handled by the package include:
|
||||
//
|
||||
// 1) Immediate input from the user (see userinput package)
|
||||
// 2) Playback for a previously recorded script (see recorder package)
|
||||
// 3) Driven events from a driver emulation (see below))
|
||||
// 4) Pushed events
|
||||
//
|
||||
// In addition it also coordinates the passing of input to other packages that
|
||||
// need to know about an input event.
|
||||
//
|
||||
// 1) The RIOT ports (see Ports package)
|
||||
// 2) Events to be recorded to a playback script (see recorder package)
|
||||
// 3) Events to be driven to a passenger emulation (see below)
|
||||
//
|
||||
// The input package will handle impossible situations are return an error when
|
||||
// appropriate. For example, it is not possible for an emulation to be a
|
||||
// playback and a recorder at the same time.
|
||||
//
|
||||
// Points 1 in both lists is the normal type of input you would expect in an
|
||||
// emultor that allows people to play games and as such, won't be discussed
|
||||
// further.
|
||||
//
|
||||
// Points 2 in the lists is well covered by the recorder package.
|
||||
//
|
||||
// Points 3 in both lists above refer to driven events and driver & passenger
|
||||
// emulations. This system is a way os synchronising two emulations such that
|
||||
// the input of one drives the input of the other. The input package ensures
|
||||
// that input events occur in bothe emulations at the same time - time being
|
||||
// measure by the coordinates of the TV instances attached to the emulations.
|
||||
//
|
||||
// For an example of a driven emulation see the comparison package.
|
||||
//
|
||||
// Lastly, pushed events, as refered to in point 4, are events that have
|
||||
// arrived from a different goroutine. For an example of pushed events see the
|
||||
// various bot implementations in the bots package.
|
||||
package input
|
87
hardware/input/driven.go
Normal file
87
hardware/input/driven.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
// 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 input
|
||||
|
||||
import (
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
)
|
||||
|
||||
// handleDrivenEvents checks for input driven from a another emulation.
|
||||
func (inp *Input) handleDrivenEvents() error {
|
||||
if inp.checkForDriven {
|
||||
ev := inp.drivenInputEvent
|
||||
done := false
|
||||
for !done {
|
||||
c := inp.tv.GetCoords()
|
||||
if coords.Equal(c, ev.Time) {
|
||||
_, err := inp.ports.HandleInputEvent(ev.InputEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if coords.GreaterThan(c, ev.Time) {
|
||||
return curated.Errorf("input: driven input seen too late. emulations not synced correctly.")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case inp.drivenInputEvent = <-inp.fromDriver:
|
||||
if inp.checkForDriven {
|
||||
curated.Errorf("input: driven input received before previous input was processed")
|
||||
}
|
||||
default:
|
||||
done = true
|
||||
inp.checkForDriven = false
|
||||
}
|
||||
|
||||
ev = inp.drivenInputEvent
|
||||
}
|
||||
}
|
||||
|
||||
if inp.fromDriver != nil {
|
||||
select {
|
||||
case inp.drivenInputEvent = <-inp.fromDriver:
|
||||
if inp.checkForDriven {
|
||||
curated.Errorf("input: driven input received before previous input was processed")
|
||||
}
|
||||
inp.checkForDriven = true
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachPassenger should be called by an emulation that wants to be driven by another emulation.
|
||||
func (inp *Input) AttachPassenger(driver chan ports.TimedInputEvent) error {
|
||||
if inp.toPassenger != nil {
|
||||
return curated.Errorf("input: attach passenger: emulation already defined as an input driver")
|
||||
}
|
||||
inp.fromDriver = driver
|
||||
inp.setProcessFunc()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachDriver should be called by an emulation that is prepared to drive another emulation.
|
||||
func (inp *Input) AttachDriver(passenger chan ports.TimedInputEvent) error {
|
||||
if inp.fromDriver != nil {
|
||||
return curated.Errorf("input: attach driver: emulation already defined as being an input passenger")
|
||||
}
|
||||
inp.toPassenger = passenger
|
||||
return nil
|
||||
}
|
142
hardware/input/input.go
Normal file
142
hardware/input/input.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
// 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 input
|
||||
|
||||
import (
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
)
|
||||
|
||||
// TV defines the television functions required by the Input system.
|
||||
type TV interface {
|
||||
GetCoords() coords.TelevisionCoords
|
||||
}
|
||||
|
||||
// Input handles all forms of input into the VCS.
|
||||
type Input struct {
|
||||
tv TV
|
||||
ports *ports.Ports
|
||||
|
||||
playback EventPlayback
|
||||
recorder EventRecorder
|
||||
|
||||
// events pushed onto the input queue
|
||||
pushed chan ports.InputEvent
|
||||
|
||||
// the following fields all relate to driven input, for either the driver
|
||||
// or for the passenger (the driven)
|
||||
fromDriver chan ports.TimedInputEvent
|
||||
toPassenger chan ports.TimedInputEvent
|
||||
checkForDriven bool
|
||||
drivenInputEvent ports.TimedInputEvent
|
||||
|
||||
// Process function should be called every VCS step
|
||||
Process func() error
|
||||
}
|
||||
|
||||
func NewInput(tv TV, p *ports.Ports) *Input {
|
||||
inp := &Input{
|
||||
tv: tv,
|
||||
ports: p,
|
||||
pushed: make(chan ports.InputEvent, 64),
|
||||
}
|
||||
inp.setProcessFunc()
|
||||
return inp
|
||||
}
|
||||
|
||||
// Plumb a new ports instances into the Input.
|
||||
func (inp *Input) Plumb(ports *ports.Ports) {
|
||||
inp.ports = ports
|
||||
}
|
||||
|
||||
// PeripheralID forwards a request of the PeripheralID of the PortID to VCS Ports.
|
||||
func (inp *Input) PeripheralID(id plugging.PortID) plugging.PeripheralID {
|
||||
return inp.ports.PeripheralID(id)
|
||||
}
|
||||
|
||||
// HandleInputEvent forwards an input event to VCS Ports.
|
||||
func (inp *Input) HandleInputEvent(ev ports.InputEvent) (bool, error) {
|
||||
if inp.recorder != nil {
|
||||
err := inp.recorder.RecordEvent(ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
handled, err := inp.ports.HandleInputEvent(ev)
|
||||
if err != nil {
|
||||
return handled, err
|
||||
}
|
||||
|
||||
// forward to passenger if one is defined
|
||||
if handled && inp.toPassenger != nil {
|
||||
select {
|
||||
case inp.toPassenger <- ports.TimedInputEvent{Time: inp.tv.GetCoords(), InputEvent: ev}:
|
||||
default:
|
||||
return handled, curated.Errorf("input: passenger event queue is full: input dropped")
|
||||
}
|
||||
}
|
||||
|
||||
return handled, nil
|
||||
}
|
||||
|
||||
func (inp *Input) setProcessFunc() {
|
||||
if inp.fromDriver != nil && inp.playback != nil {
|
||||
inp.Process = func() error {
|
||||
if err := inp.handlePushedEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inp.handlePlaybackEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inp.handleDrivenEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if inp.fromDriver != nil {
|
||||
inp.Process = func() error {
|
||||
if err := inp.handlePushedEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inp.handleDrivenEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if inp.playback != nil {
|
||||
inp.Process = func() error {
|
||||
if err := inp.handlePushedEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inp.handlePlaybackEvents(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
inp.Process = inp.handlePushedEvents
|
||||
}
|
48
hardware/input/pushed.go
Normal file
48
hardware/input/pushed.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
// 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 input
|
||||
|
||||
import (
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
||||
)
|
||||
|
||||
// PushEvent pushes an InputEvent onto the queue. Will drop the event and
|
||||
// return an error if queue is full.
|
||||
func (inp *Input) PushEvent(ev ports.InputEvent) error {
|
||||
select {
|
||||
case inp.pushed <- ev:
|
||||
default:
|
||||
return curated.Errorf("ports: pushed event queue is full: input dropped")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (inp *Input) handlePushedEvents() error {
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case ev := <-inp.pushed:
|
||||
_, err := inp.ports.HandleInputEvent(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
done = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
104
hardware/input/recording.go
Normal file
104
hardware/input/recording.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
// 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 input
|
||||
|
||||
import (
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
)
|
||||
|
||||
// Playback implementations feed controller Events to the device on request
|
||||
// with the CheckInput() function.
|
||||
//
|
||||
// Intended for playback of controller events previously recorded to a file on
|
||||
// disk but usable for many purposes I suspect. For example, AI control.
|
||||
type EventPlayback interface {
|
||||
// note the type restrictions on EventData in the type definition's
|
||||
// commentary
|
||||
GetPlayback() (ports.TimedInputEvent, error)
|
||||
}
|
||||
|
||||
// EventRecorder implementations mirror an incoming event.
|
||||
//
|
||||
// Implementations should be able to handle being attached to more than one
|
||||
// peripheral at once. The ID parameter of the EventRecord() function will help
|
||||
// to differentiate between multiple devices.
|
||||
type EventRecorder interface {
|
||||
RecordEvent(ports.TimedInputEvent) error
|
||||
}
|
||||
|
||||
// AttachEventRecorder attaches an EventRecorder implementation.
|
||||
func (inp *Input) AttachRecorder(r EventRecorder) error {
|
||||
if inp.playback != nil {
|
||||
return curated.Errorf("input: attach recorder: emulator already has a playback attached")
|
||||
}
|
||||
inp.recorder = r
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachPlayback attaches an EventPlayback implementation to the Input
|
||||
// sub-system. EventPlayback can be nil in order to remove the playback.
|
||||
func (inp *Input) AttachPlayback(pb EventPlayback) error {
|
||||
if inp.recorder != nil {
|
||||
return curated.Errorf("input: attach playback: emulator already has a recorder attached")
|
||||
}
|
||||
inp.playback = pb
|
||||
inp.setProcessFunc()
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlePlaybackEvents requests playback events from all attached and eligible peripherals.
|
||||
func (inp *Input) handlePlaybackEvents() error {
|
||||
if inp.playback == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loop with GetPlayback() until we encounter a NoPortID or NoEvent
|
||||
// condition. there might be more than one entry for a particular
|
||||
// frame/scanline/horizpas state so we need to make sure we've processed
|
||||
// them all.
|
||||
//
|
||||
// this happens in particular with recordings that were made of ROMs with
|
||||
// panel setup configurations (see setup package) - where the switches are
|
||||
// set when the TV state is at fr=0 sl=0 cl=0
|
||||
morePlayback := true
|
||||
for morePlayback {
|
||||
ev, err := inp.playback.GetPlayback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
morePlayback = ev.Port != plugging.PortUnplugged && ev.Ev != ports.NoEvent
|
||||
if morePlayback {
|
||||
_, err := inp.ports.HandleInputEvent(ev.InputEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// forward to passenger if necessary
|
||||
if inp.toPassenger != nil {
|
||||
select {
|
||||
case inp.toPassenger <- ev:
|
||||
default:
|
||||
return curated.Errorf("input: passenger event queue is full: input dropped")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -80,7 +80,7 @@ func (mem *Memory) Snapshot() *Memory {
|
|||
return &n
|
||||
}
|
||||
|
||||
// Plumb makes sure everythin is ship-shape after a rewind event.
|
||||
// Plumb makes sure everything is ship-shape after a rewind event.
|
||||
func (mem *Memory) Plumb(fromDifferentEmulation bool) {
|
||||
mem.Cart.Plumb(fromDifferentEmulation)
|
||||
}
|
||||
|
|
|
@ -16,17 +16,12 @@
|
|||
// Package ports represents the input/output parts of the VCS (the IO in
|
||||
// RIOT).
|
||||
//
|
||||
// Emulated peripherals are plugged into the VCS with the Plug() function.
|
||||
// Input from "real" devices is handled by HandleEvent() which passes the event
|
||||
// to peripherals in the specified PortID.
|
||||
//
|
||||
// Emulations can share user input through the DrivenEvent mechanism. The driver
|
||||
// emulation should call SynchroniseWithPassenger() and the passenger emulation
|
||||
// should call SynchroniseWithDriver().
|
||||
// HandleEvent() should probably no be called directly but instead only through
|
||||
// the input package
|
||||
//
|
||||
// With the DrivenEvent mechanism, the driver sends events to the passenger.
|
||||
// Both emulations will receive the same user input at the same time, relative
|
||||
// to television coordinates, so it is important that the driver is running
|
||||
// ahead of the passenger at all time. See comparison package for model
|
||||
// implementation.
|
||||
// Emulated peripherals are plugged into the VCS with the Plug() function.
|
||||
// Plugging events can be monitored with the plugging.PlugMonitor interface.
|
||||
package ports
|
||||
|
|
|
@ -117,23 +117,3 @@ type TimedInputEvent struct {
|
|||
Time coords.TelevisionCoords
|
||||
InputEvent
|
||||
}
|
||||
|
||||
// Playback implementations feed controller Events to the device on request
|
||||
// with the CheckInput() function.
|
||||
//
|
||||
// Intended for playback of controller events previously recorded to a file on
|
||||
// disk but usable for many purposes I suspect. For example, AI control.
|
||||
type EventPlayback interface {
|
||||
// note the type restrictions on EventData in the type definition's
|
||||
// commentary
|
||||
GetPlayback() (TimedInputEvent, error)
|
||||
}
|
||||
|
||||
// EventRecorder implementations mirror an incoming event.
|
||||
//
|
||||
// Implementations should be able to handle being attached to more than one
|
||||
// peripheral at once. The ID parameter of the EventRecord() function will help
|
||||
// to differentiate between multiple devices.
|
||||
type EventRecorder interface {
|
||||
RecordEvent(TimedInputEvent) error
|
||||
}
|
||||
|
|
|
@ -1,187 +0,0 @@
|
|||
// 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 ports
|
||||
|
||||
import (
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
)
|
||||
|
||||
func (p *Ports) HandleInputEvents() error {
|
||||
err := p.handlePushedEvents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.handleDrivenEvents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.handlePlaybackEvents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Ports) handlePushedEvents() error {
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case inp := <-p.pushed:
|
||||
_, err := p.HandleInputEvent(inp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
done = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDrivenEvents checks for input driven from a another emulation.
|
||||
func (p *Ports) handleDrivenEvents() error {
|
||||
if p.checkForDriven {
|
||||
inp := p.drivenInputData
|
||||
done := false
|
||||
for !done {
|
||||
c := p.tv.GetCoords()
|
||||
if coords.Equal(c, inp.Time) {
|
||||
_, err := p.HandleInputEvent(inp.InputEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if coords.GreaterThan(c, inp.Time) {
|
||||
return curated.Errorf("ports: driven input seen too late. emulations not synced correctly.")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case p.drivenInputData = <-p.fromDriver:
|
||||
if p.checkForDriven {
|
||||
curated.Errorf("ports: driven input received before previous input was processed")
|
||||
}
|
||||
default:
|
||||
done = true
|
||||
p.checkForDriven = false
|
||||
}
|
||||
|
||||
inp = p.drivenInputData
|
||||
}
|
||||
}
|
||||
|
||||
if p.fromDriver != nil {
|
||||
select {
|
||||
case p.drivenInputData = <-p.fromDriver:
|
||||
if p.checkForDriven {
|
||||
curated.Errorf("ports: driven input received before previous input was processed")
|
||||
}
|
||||
p.checkForDriven = true
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlePlaybackEvents requests playback events from all attached and eligible peripherals.
|
||||
func (p *Ports) handlePlaybackEvents() error {
|
||||
if p.playback == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loop with GetPlayback() until we encounter a NoPortID or NoEvent
|
||||
// condition. there might be more than one entry for a particular
|
||||
// frame/scanline/horizpas state so we need to make sure we've processed
|
||||
// them all.
|
||||
//
|
||||
// this happens in particular with recordings that were made of ROMs with
|
||||
// panel setup configurations (see setup package) - where the switches are
|
||||
// set when the TV state is at fr=0 sl=0 cl=0
|
||||
morePlayback := true
|
||||
for morePlayback {
|
||||
inp, err := p.playback.GetPlayback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
morePlayback = inp.Port != plugging.PortUnplugged && inp.Ev != NoEvent
|
||||
if morePlayback {
|
||||
_, err := p.HandleInputEvent(inp.InputEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleInputEvent should only be used from the same gorouine as the
|
||||
// emulation. Events should be queued with QueueEvent() otherwise.
|
||||
//
|
||||
// Consider using HandleInputEvent() function in the VCS type rather than this
|
||||
// function directly.
|
||||
func (p *Ports) HandleInputEvent(inp InputEvent) (bool, error) {
|
||||
var handled bool
|
||||
var err error
|
||||
|
||||
switch inp.Port {
|
||||
case plugging.PortPanel:
|
||||
handled, err = p.Panel.HandleEvent(inp.Ev, inp.D)
|
||||
case plugging.PortLeftPlayer:
|
||||
handled, err = p.LeftPlayer.HandleEvent(inp.Ev, inp.D)
|
||||
case plugging.PortRightPlayer:
|
||||
handled, err = p.RightPlayer.HandleEvent(inp.Ev, inp.D)
|
||||
}
|
||||
|
||||
// forward to passenger if one is defined
|
||||
if handled && p.toPassenger != nil {
|
||||
select {
|
||||
case p.toPassenger <- TimedInputEvent{Time: p.tv.GetCoords(), InputEvent: inp}:
|
||||
default:
|
||||
return handled, curated.Errorf("ports: passenger event queue is full: input dropped")
|
||||
}
|
||||
}
|
||||
|
||||
// if error was because of an unhandled event then return without error
|
||||
if err != nil {
|
||||
return handled, curated.Errorf("ports: %v", err)
|
||||
}
|
||||
|
||||
// record event with the EventRecorder
|
||||
for _, r := range p.recorder {
|
||||
return handled, r.RecordEvent(TimedInputEvent{Time: p.tv.GetCoords(), InputEvent: inp})
|
||||
}
|
||||
|
||||
return handled, nil
|
||||
}
|
||||
|
||||
// QueueEvent pushes an InputEvent onto the queue. Will drop the event and
|
||||
// return an error if queue is full.
|
||||
func (p *Ports) QueueEvent(inp InputEvent) error {
|
||||
select {
|
||||
case p.pushed <- inp:
|
||||
default:
|
||||
return curated.Errorf("ports: pushed event queue is full: input dropped")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -23,14 +23,8 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/hardware/memory/addresses"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/bus"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
)
|
||||
|
||||
// TV defines the television functions required by the Ports type.
|
||||
type TV interface {
|
||||
GetCoords() coords.TelevisionCoords
|
||||
}
|
||||
|
||||
// Input implements the input/output part of the RIOT (the IO in RIOT).
|
||||
type Ports struct {
|
||||
riot bus.ChipBus
|
||||
|
@ -40,9 +34,6 @@ type Ports struct {
|
|||
LeftPlayer Peripheral
|
||||
RightPlayer Peripheral
|
||||
|
||||
playback EventPlayback
|
||||
recorder []EventRecorder
|
||||
|
||||
monitor plugging.PlugMonitor
|
||||
|
||||
// local copies of key chip memory registers
|
||||
|
@ -84,21 +75,6 @@ type Ports struct {
|
|||
//
|
||||
// we use it to mux the Player0 and Player 1 nibbles into the single register
|
||||
swchaMux uint8
|
||||
|
||||
// events pushed onto the input queue
|
||||
pushed chan InputEvent
|
||||
|
||||
// the following fields all relate to driven input, for either the driver
|
||||
// or for the passenger (the driven)
|
||||
fromDriver chan TimedInputEvent
|
||||
toPassenger chan TimedInputEvent
|
||||
checkForDriven bool
|
||||
drivenInputData TimedInputEvent
|
||||
|
||||
// the time of driven events are measured by television coordinates
|
||||
//
|
||||
// not used except to synchronise driver and passenger emulations
|
||||
tv TV
|
||||
}
|
||||
|
||||
// NewPorts is the preferred method of initialisation of the Ports type.
|
||||
|
@ -106,11 +82,9 @@ func NewPorts(riotMem bus.ChipBus, tiaMem bus.ChipBus) *Ports {
|
|||
p := &Ports{
|
||||
riot: riotMem,
|
||||
tia: tiaMem,
|
||||
recorder: make([]EventRecorder, 0),
|
||||
swchaFromCPU: 0x00,
|
||||
swacnt: 0x00,
|
||||
latch: false,
|
||||
pushed: make(chan InputEvent, 64),
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
@ -267,45 +241,6 @@ func (p *Ports) Step() {
|
|||
p.Panel.Step()
|
||||
}
|
||||
|
||||
// SynchroniseWithDriver implies that the emulation will receive driven events
|
||||
// from another emulation.
|
||||
func (p *Ports) SynchroniseWithDriver(driver chan TimedInputEvent, tv TV) error {
|
||||
if p.toPassenger != nil {
|
||||
return curated.Errorf("ports: cannot sync with driver: emulation already defined as a driver of input")
|
||||
}
|
||||
if p.playback != nil {
|
||||
return curated.Errorf("ports: cannot sync with driver: emulation is already receiving input from a playback")
|
||||
}
|
||||
p.tv = tv
|
||||
p.fromDriver = driver
|
||||
return nil
|
||||
}
|
||||
|
||||
// SynchroniseWithPassenger connects the emulation to a second emulation (the
|
||||
// passenger) to which user input events will be "driven".
|
||||
func (p *Ports) SynchroniseWithPassenger(passenger chan TimedInputEvent, tv TV) error {
|
||||
if p.fromDriver != nil {
|
||||
return curated.Errorf("ports: cannot sync with passenger: emulation already defined as being driven")
|
||||
}
|
||||
p.tv = tv
|
||||
p.toPassenger = passenger
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachPlayback attaches an EventPlayback implementation.
|
||||
func (p *Ports) AttachPlayback(b EventPlayback) error {
|
||||
if p.fromDriver != nil {
|
||||
return curated.Errorf("ports: cannot attach playback: emulation already defined as being driven")
|
||||
}
|
||||
p.playback = b
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachEventRecorder attaches an EventRecorder implementation.
|
||||
func (p *Ports) AttachEventRecorder(r EventRecorder) {
|
||||
p.recorder = append(p.recorder, r)
|
||||
}
|
||||
|
||||
// AttchPlugMonitor implements the plugging.Monitorable interface.
|
||||
func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) {
|
||||
p.monitor = m
|
||||
|
@ -328,10 +263,7 @@ func (p *Ports) AttachPlugMonitor(m plugging.PlugMonitor) {
|
|||
}
|
||||
}
|
||||
|
||||
// PeripheralID implements userinput.HandleInput interface.
|
||||
//
|
||||
// Consider using PeripheralID() function in the VCS type rather than this
|
||||
// function directly.
|
||||
// PeripheralID returns the ID of the peripheral in the identified port.
|
||||
func (p *Ports) PeripheralID(id plugging.PortID) plugging.PeripheralID {
|
||||
switch id {
|
||||
case plugging.PortPanel:
|
||||
|
@ -376,3 +308,26 @@ func (p *Ports) WriteINPTx(inptx addresses.ChipRegister, data uint8) {
|
|||
p.tia.ChipWrite(inptx, data)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleInputEvent forwards the InputEvent to the perupheral in the correct
|
||||
// port. Returns true if the event was handled and false if not.
|
||||
func (p *Ports) HandleInputEvent(inp InputEvent) (bool, error) {
|
||||
var handled bool
|
||||
var err error
|
||||
|
||||
switch inp.Port {
|
||||
case plugging.PortPanel:
|
||||
handled, err = p.Panel.HandleEvent(inp.Ev, inp.D)
|
||||
case plugging.PortLeftPlayer:
|
||||
handled, err = p.LeftPlayer.HandleEvent(inp.Ev, inp.D)
|
||||
case plugging.PortRightPlayer:
|
||||
handled, err = p.RightPlayer.HandleEvent(inp.Ev, inp.D)
|
||||
}
|
||||
|
||||
// if error was because of an unhandled event then return without error
|
||||
if err != nil {
|
||||
return handled, curated.Errorf("ports: %v", err)
|
||||
}
|
||||
|
||||
return handled, nil
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ func (vcs *VCS) Run(continueCheck func() (emulation.State, error)) error {
|
|||
// see the equivalient videoCycle() in the VCS.Step() function for an
|
||||
// explanation for what's going on here:
|
||||
videoCycle := func() error {
|
||||
if err := vcs.RIOT.Ports.HandleInputEvents(); err != nil {
|
||||
if err := vcs.Input.Process(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ func (vcs *VCS) Step(videoCycleCallback func() error) error {
|
|||
// I don't believe any visual or audible artefacts of the VCS (undocumented
|
||||
// or not) rely on the details of the CPU-TIA relationship.
|
||||
videoCycle := func() error {
|
||||
if err := vcs.RIOT.Ports.HandleInputEvents(); err != nil {
|
||||
if err := vcs.Input.Process(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
||||
"github.com/jetsetilly/gopher2600/curated"
|
||||
"github.com/jetsetilly/gopher2600/hardware/cpu"
|
||||
"github.com/jetsetilly/gopher2600/hardware/input"
|
||||
"github.com/jetsetilly/gopher2600/hardware/instance"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/addresses"
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/controllers"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/panel"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
|
@ -50,6 +50,8 @@ type VCS struct {
|
|||
RIOT *riot.RIOT
|
||||
TIA *tia.TIA
|
||||
|
||||
Input *input.Input
|
||||
|
||||
// The Clock defines the basic speed at which the the machine is runningt. This governs
|
||||
// the speed of the CPU, the RIOT and attached peripherals. The TIA runs at
|
||||
// exactly three times this speed.
|
||||
|
@ -90,6 +92,8 @@ func NewVCS(tv *television.Television) (*VCS, error) {
|
|||
vcs.CPU = cpu.NewCPU(vcs.Instance, vcs.Mem)
|
||||
vcs.RIOT = riot.NewRIOT(vcs.Instance, vcs.Mem.RIOT, vcs.Mem.TIA)
|
||||
|
||||
vcs.Input = input.NewInput(vcs.TV, vcs.RIOT.Ports)
|
||||
|
||||
vcs.TIA, err = tia.NewTIA(vcs.Instance, vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -120,6 +124,21 @@ func (vcs *VCS) End() {
|
|||
vcs.TV.End()
|
||||
}
|
||||
|
||||
// Plumb the various VCS sub-systems together after a rewind.
|
||||
func (vcs *VCS) Plumb(fromDifferentEmulation bool) {
|
||||
vcs.CPU.Plumb(vcs.Mem)
|
||||
vcs.Mem.Plumb(fromDifferentEmulation)
|
||||
vcs.RIOT.Plumb(vcs.Mem.RIOT, vcs.Mem.TIA)
|
||||
vcs.TIA.Plumb(vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
|
||||
|
||||
// reset peripherals after new state has been plumbed. without this,
|
||||
// controllers can feel odd if the newly plumbed state has left RIOT memory
|
||||
// in a latched state
|
||||
vcs.RIOT.Ports.ResetPeripherals()
|
||||
|
||||
vcs.Input.Plumb(vcs.RIOT.Ports)
|
||||
}
|
||||
|
||||
// AttachCartridge to this VCS. While this function can be called directly it
|
||||
// is advised that the setup package be used in most circumstances.
|
||||
func (vcs *VCS) AttachCartridge(cartload cartridgeloader.Loader) error {
|
||||
|
@ -226,29 +245,7 @@ func (vcs *VCS) SetClockSpeed(tvSpec string) error {
|
|||
// recorders and playback, and RIOT plug monitor.
|
||||
func (vcs *VCS) DetatchEmulationExtras() {
|
||||
vcs.TIA.Audio.SetTracker(nil)
|
||||
vcs.RIOT.Ports.AttachEventRecorder(nil)
|
||||
vcs.RIOT.Ports.AttachPlayback(nil)
|
||||
vcs.Input.AttachRecorder(nil)
|
||||
vcs.Input.AttachPlayback(nil)
|
||||
vcs.RIOT.Ports.AttachPlugMonitor(nil)
|
||||
}
|
||||
|
||||
// HandleInputEvent forwards an input event to VCS Ports.
|
||||
//
|
||||
// It's important that this function be used in preference to calling the
|
||||
// HandleInputEvent() function in the RIOT.Ports directly. For rewind reasons
|
||||
// it is likely that any direct reference to RIOT.Ports will grow stale.
|
||||
func (vcs *VCS) HandleInputEvent(inp ports.InputEvent) (bool, error) {
|
||||
return vcs.RIOT.Ports.HandleInputEvent(inp)
|
||||
}
|
||||
|
||||
// PeripheralID forwards a request of the PeripheralID of the PortID to VCS
|
||||
// Ports.
|
||||
//
|
||||
// See important comment in HandleInputEvent() above.
|
||||
func (vcs *VCS) PeripheralID(id plugging.PortID) plugging.PeripheralID {
|
||||
return vcs.RIOT.Ports.PeripheralID(id)
|
||||
}
|
||||
|
||||
// QueueEvent forwards an input event to VCS Ports.
|
||||
func (vcs *VCS) QueueEvent(inp ports.InputEvent) error {
|
||||
return vcs.RIOT.Ports.QueueEvent(inp)
|
||||
}
|
||||
|
|
|
@ -13,12 +13,10 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package recorder handles recording and playback of user input. The Recorder
|
||||
// type implements the riot.input.EventRecorder() interface. Once added as a
|
||||
// transcriber to the VCS port, it will record all user input to the specified
|
||||
// file.
|
||||
// Package recorder handles recording and playback of input through the VCS
|
||||
// input system. See input package.
|
||||
//
|
||||
// To keep things simple, recording gameplay will use the VCS in it's default
|
||||
// state. Future versions of the recorder fileformat will support localised
|
||||
// preferences.
|
||||
// To keep things simple, recording gameplay will use the VCS in it's
|
||||
// normalised state. Future versions of the recorder fileformat will support
|
||||
// localised preferences.
|
||||
package recorder
|
||||
|
|
|
@ -74,6 +74,9 @@ func (plb Playback) EndFrame() (bool, error) {
|
|||
}
|
||||
|
||||
// NewPlayback is the preferred method of implementation for the Playback type.
|
||||
//
|
||||
// The returned playback must be attached to the VCS input system (with
|
||||
// AttachToVCSInput() function) for it it to be useful.
|
||||
func NewPlayback(transcript string) (*Playback, error) {
|
||||
var err error
|
||||
|
||||
|
@ -172,11 +175,11 @@ func NewPlayback(transcript string) (*Playback, error) {
|
|||
return plb, nil
|
||||
}
|
||||
|
||||
// AttachToVCS attaches the playback instance (an implementation of the
|
||||
// playback interface) to all the ports of the VCS, including the panel.
|
||||
// AttachToVCSInput attaches the playback instance (an implementation of the
|
||||
// playback interface) to all the input system of the VCS.
|
||||
//
|
||||
// Note that this will reset the VCS.
|
||||
func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error {
|
||||
// Note that the VCS instance will be normalised as a result of this call.
|
||||
func (plb *Playback) AttachToVCSInput(vcs *hardware.VCS) error {
|
||||
// check we're working with correct information
|
||||
if vcs == nil || vcs.TV == nil {
|
||||
return curated.Errorf("playback: no playback hardware available")
|
||||
|
@ -201,11 +204,8 @@ func (plb *Playback) AttachToVCS(vcs *hardware.VCS) error {
|
|||
return curated.Errorf("playback: %v", err)
|
||||
}
|
||||
|
||||
// attach playback to all vcs ports
|
||||
err = vcs.RIOT.Ports.AttachPlayback(plb)
|
||||
if err != nil {
|
||||
return curated.Errorf("playback: %v", err)
|
||||
}
|
||||
// attach playback to all VCS Input system
|
||||
vcs.Input.AttachPlayback(plb)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ type Recorder struct {
|
|||
// type. Note that attaching of the Recorder to all the ports of the VCS
|
||||
// (including the panel) is implicit in this function call.
|
||||
//
|
||||
// Note that this will reset the VCS.
|
||||
// Note that the VCS instance will be normalised as a result of this call.
|
||||
func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
|
||||
var err error
|
||||
|
||||
|
@ -67,8 +67,8 @@ func NewRecorder(transcript string, vcs *hardware.VCS) (*Recorder, error) {
|
|||
return nil, curated.Errorf("recorder: %v", err)
|
||||
}
|
||||
|
||||
// attach recorder to vcs peripherals, including the panel
|
||||
vcs.RIOT.Ports.AttachEventRecorder(rec)
|
||||
// attach recorder to vcs input system
|
||||
vcs.Input.AttachRecorder(rec)
|
||||
|
||||
// video digester for playback verification
|
||||
rec.digest, err = digest.NewVideo(vcs.TV)
|
||||
|
|
|
@ -132,7 +132,7 @@ func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg
|
|||
// function according to the current features of the recorder package and
|
||||
// the saved script
|
||||
|
||||
err = plb.AttachToVCS(vcs)
|
||||
err = plb.AttachToVCSInput(vcs)
|
||||
if err != nil {
|
||||
return false, "", curated.Errorf("playback: %v", err)
|
||||
}
|
||||
|
|
|
@ -378,15 +378,8 @@ func Plumb(vcs *hardware.VCS, state *State, fromDifferentEmulation bool) {
|
|||
vcs.RIOT = state.RIOT.Snapshot()
|
||||
vcs.TIA = state.TIA.Snapshot()
|
||||
|
||||
vcs.CPU.Plumb(vcs.Mem)
|
||||
vcs.Mem.Plumb(fromDifferentEmulation)
|
||||
vcs.RIOT.Plumb(vcs.Mem.RIOT, vcs.Mem.TIA)
|
||||
vcs.TIA.Plumb(vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
|
||||
|
||||
// reset peripherals after new state has been plumbed. without this,
|
||||
// controllers can feel odd if the newly plumbed state has left RIOT memory
|
||||
// in a latched state
|
||||
vcs.RIOT.Ports.ResetPeripherals()
|
||||
// finish off plumbing process
|
||||
vcs.Plumb(fromDifferentEmulation)
|
||||
}
|
||||
|
||||
// run from the supplied state until the cooridinates are reached.
|
||||
|
|
|
@ -117,36 +117,36 @@ func (set PanelSetup) matchCartHash(hash string) bool {
|
|||
func (set PanelSetup) apply(vcs *hardware.VCS) error {
|
||||
if set.p0 {
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true}
|
||||
if _, err := vcs.HandleInputEvent(inp); err != nil {
|
||||
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false}
|
||||
if _, err := vcs.HandleInputEvent(inp); err != nil {
|
||||
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if set.p1 {
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true}
|
||||
if _, err := vcs.HandleInputEvent(inp); err != nil {
|
||||
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false}
|
||||
if _, err := vcs.HandleInputEvent(inp); err != nil {
|
||||
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if set.col {
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true}
|
||||
if _, err := vcs.HandleInputEvent(inp); err != nil {
|
||||
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false}
|
||||
if _, err := vcs.HandleInputEvent(inp); err != nil {
|
||||
if _, err := vcs.Input.HandleInputEvent(inp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,12 +22,12 @@ import (
|
|||
|
||||
// HandleInput conceptualises data being sent to the console ports.
|
||||
type HandleInput interface {
|
||||
// PeripheralID identifies the device currently attached to the port.
|
||||
PeripheralID(id plugging.PortID) plugging.PeripheralID
|
||||
|
||||
// HandleInputEvent forwards the Event and EventData to the device connected to the
|
||||
// specified PortID.
|
||||
//
|
||||
// Returns true if the port understood and handled the event.
|
||||
HandleInputEvent(ports.InputEvent) (bool, error)
|
||||
|
||||
// PeripheralID identifies the device currently attached to the port.
|
||||
PeripheralID(id plugging.PortID) plugging.PeripheralID
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue