mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 20:18:20 -04:00
added macro package and command line option
screenshot request to GUI system can specify a filename
This commit is contained in:
parent
81ff8bbe97
commit
9cb6cce02e
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -27,3 +27,4 @@ todo.txt
|
|||
armcode.map
|
||||
*.csv
|
||||
*.jpg
|
||||
*.bin
|
||||
|
|
|
@ -42,6 +42,7 @@ type CommandLineOptions struct {
|
|||
PatchFile *string
|
||||
Wav *bool
|
||||
NoEject *bool
|
||||
Macro *string
|
||||
|
||||
// debugger only
|
||||
InitScript *string
|
||||
|
|
|
@ -46,6 +46,7 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/hardware/television"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
"github.com/jetsetilly/gopher2600/logger"
|
||||
"github.com/jetsetilly/gopher2600/macro"
|
||||
"github.com/jetsetilly/gopher2600/notifications"
|
||||
"github.com/jetsetilly/gopher2600/patch"
|
||||
"github.com/jetsetilly/gopher2600/prefs"
|
||||
|
@ -99,6 +100,9 @@ type Debugger struct {
|
|||
recorder *recorder.Recorder
|
||||
playback *recorder.Playback
|
||||
|
||||
// macro (only one allowed for the time being)
|
||||
macro *macro.Macro
|
||||
|
||||
// comparison emulator
|
||||
comparison *comparison.Comparison
|
||||
|
||||
|
@ -681,6 +685,9 @@ func (dbg *Debugger) end() {
|
|||
dbg.endPlayback()
|
||||
dbg.endRecording()
|
||||
dbg.endComparison()
|
||||
if dbg.macro != nil {
|
||||
dbg.macro.Quit()
|
||||
}
|
||||
dbg.bots.Quit()
|
||||
|
||||
dbg.vcs.End()
|
||||
|
@ -838,6 +845,13 @@ func (dbg *Debugger) StartInPlayMode(filename string) error {
|
|||
dbg.startPlayback(filename)
|
||||
}
|
||||
|
||||
if *dbg.opts.Macro != "" {
|
||||
dbg.macro, err = macro.NewMacro(*dbg.opts.Macro, dbg, dbg.vcs.Input, dbg.vcs.TV, dbg.gui)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debugger: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = dbg.startComparison(*dbg.opts.ComparisonROM, *dbg.opts.ComparisonPrefs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debugger: %w", err)
|
||||
|
@ -854,6 +868,10 @@ func (dbg *Debugger) StartInPlayMode(filename string) error {
|
|||
|
||||
defer dbg.end()
|
||||
|
||||
if dbg.macro != nil {
|
||||
dbg.macro.Run()
|
||||
}
|
||||
|
||||
err = dbg.run()
|
||||
if err != nil {
|
||||
if errors.Is(err, terminal.UserQuit) {
|
||||
|
@ -1016,6 +1034,9 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
|
|||
dbg.endPlayback()
|
||||
dbg.endRecording()
|
||||
dbg.endComparison()
|
||||
if dbg.macro != nil {
|
||||
dbg.macro.Quit()
|
||||
}
|
||||
dbg.bots.Quit()
|
||||
|
||||
// attching a cartridge implies the initialise state
|
||||
|
|
|
@ -309,6 +309,7 @@ func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) err
|
|||
opts.PatchFile = md.AddString("patch", "", "patch to apply to main emulation (not playback files)")
|
||||
opts.Wav = md.AddBool("wav", false, "record audio to wav file")
|
||||
opts.NoEject = md.AddBool("noeject", false, "a cartridge must be attached at all times. emulator will quit if not")
|
||||
opts.Macro = md.AddString("macro", "", "macro file to be run on trigger")
|
||||
}
|
||||
|
||||
// debugger specific arguments
|
||||
|
|
|
@ -75,4 +75,8 @@ const (
|
|||
|
||||
// request for the coprocess source window to open at the specified line
|
||||
ReqCoProcSourceLine FeatureReq = "ReqCoProcSourceLine" // *developer.SourceLine
|
||||
|
||||
// request a screenshot to be taken
|
||||
// optional argument is the filename for the screenshot
|
||||
ReqScreenshot FeatureReq = "ReqScreenshot" // [optional] filename
|
||||
)
|
||||
|
|
|
@ -40,10 +40,6 @@ func (sh *playscrShader) destroy() {
|
|||
sh.screenshot.destroy()
|
||||
}
|
||||
|
||||
func (sh *playscrShader) scheduleScreenshot(mode screenshotMode) {
|
||||
sh.screenshot.startProcess(mode)
|
||||
}
|
||||
|
||||
func (sh *playscrShader) setAttributes(env shaderEnvironment) {
|
||||
if !sh.img.isPlaymode() {
|
||||
return
|
||||
|
|
|
@ -58,7 +58,9 @@ func (sh *screenshotSequencer) destroy() {
|
|||
sh.crt.destroy()
|
||||
}
|
||||
|
||||
func (sh *screenshotSequencer) startProcess(mode screenshotMode) {
|
||||
// filenameSuffix will be appended to the short filename of the cartridge. if
|
||||
// the string is empty then the default suffix is used
|
||||
func (sh *screenshotSequencer) startProcess(mode screenshotMode, filenameSuffix string) {
|
||||
sh.mode = mode
|
||||
|
||||
// the tag to indicate exposure time in the filename
|
||||
|
@ -96,10 +98,14 @@ func (sh *screenshotSequencer) startProcess(mode screenshotMode) {
|
|||
sh.prefs.PixelPerfectFade = 1.0
|
||||
}
|
||||
|
||||
if sh.img.crtPrefs.Enabled.Get().(bool) {
|
||||
sh.baseFilename = unique.Filename(fmt.Sprintf("crt_%s", exposureTag), sh.img.vcs.Mem.Cart.ShortName)
|
||||
if len(filenameSuffix) == 0 {
|
||||
if sh.img.crtPrefs.Enabled.Get().(bool) {
|
||||
sh.baseFilename = unique.Filename(fmt.Sprintf("crt_%s", exposureTag), sh.img.vcs.Mem.Cart.ShortName)
|
||||
} else {
|
||||
sh.baseFilename = unique.Filename(fmt.Sprintf("pix_%s", exposureTag), sh.img.vcs.Mem.Cart.ShortName)
|
||||
}
|
||||
} else {
|
||||
sh.baseFilename = unique.Filename(fmt.Sprintf("pix_%s", exposureTag), sh.img.vcs.Mem.Cart.ShortName)
|
||||
sh.baseFilename = fmt.Sprintf("%s_%s", sh.img.vcs.Mem.Cart.ShortName, filenameSuffix)
|
||||
}
|
||||
sh.baseFilename = fmt.Sprintf("%s.jpg", sh.baseFilename)
|
||||
|
||||
|
|
|
@ -153,6 +153,16 @@ func (img *SdlImgui) serviceSetFeature(request featureRequest) {
|
|||
srcWin.gotoSourceLine(ln)
|
||||
}
|
||||
|
||||
case gui.ReqScreenshot:
|
||||
switch len(request.args) {
|
||||
case 0:
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).screenshot.startProcess(modeShort, "")
|
||||
case 1:
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).screenshot.startProcess(modeShort, request.args[0].(string))
|
||||
default:
|
||||
err = fmt.Errorf("wrong number of arguments (%d instead of 1 or zero)", len(request.args))
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("sdlimgui: unsupport feature request (%s)", request.request)
|
||||
}
|
||||
|
|
|
@ -486,11 +486,11 @@ func (img *SdlImgui) serviceKeyboard(ev *sdl.KeyboardEvent) {
|
|||
|
||||
case sdl.SCANCODE_F12:
|
||||
if ctrl && !shift {
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).scheduleScreenshot(modeVeryLong)
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).screenshot.startProcess(modeVeryLong, "")
|
||||
} else if shift && !ctrl {
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).scheduleScreenshot(modeLong)
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).screenshot.startProcess(modeLong, "")
|
||||
} else {
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).scheduleScreenshot(modeShort)
|
||||
img.glsl.shaders[playscrShaderID].(*playscrShader).screenshot.startProcess(modeShort, "")
|
||||
}
|
||||
|
||||
img.playScr.emulationNotice.set(notifications.NotifyScreenshot)
|
||||
|
|
65
macro/doc.go
Normal file
65
macro/doc.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
// 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 macro implements an input system that processes instructions from a
|
||||
// macro script.
|
||||
//
|
||||
// The macro language is very simple and does not implement any flow control
|
||||
// except basic loops.
|
||||
//
|
||||
// DO loopCt [loopName]
|
||||
// ...
|
||||
// LOOP
|
||||
//
|
||||
// The 'loopName' parameter is optional. When a loop is named the current
|
||||
// counter value can be referenced as a variable in some contexts (currently,
|
||||
// this is the SCREENSHOT instruction only).
|
||||
//
|
||||
// Loops can be nested.
|
||||
//
|
||||
// The WAIT instruction will pause the execution of the macro for the specified
|
||||
// number of frames. If no value is given for this the number of frames defaults
|
||||
// to 60.
|
||||
//
|
||||
// There are instructions that give basic control over the emulation (only left
|
||||
// player joystick control and some panel operaitons).
|
||||
//
|
||||
// LEFT, RIGHT, UP, DOWN, CENTRE, FIRE, NOFIRE, SELECT, RESET
|
||||
//
|
||||
// There is also an instruction to initiate a screenshot. The macro system is
|
||||
// therefore useful to automate the collation of screenshots in a repeatable
|
||||
// manner.
|
||||
//
|
||||
// SCREENSHOT [filename suffix]
|
||||
//
|
||||
// The filename suffix parameter is optional. Without it the screenshot will be
|
||||
// given the default but unique filename.
|
||||
//
|
||||
// If the filename suffix parameter is given then the name of the screenshot
|
||||
// will be the name of the cartridge plus the suffix. Spaces will be replaced
|
||||
// with underscores.
|
||||
//
|
||||
// In the context of the screenshot instruction, variables can referenced with
|
||||
// the % symbol. For example, if a loop has been given the name "ct", then the
|
||||
// following screenshot command could be written:
|
||||
//
|
||||
// SCREENSHOT %ct
|
||||
//
|
||||
// Any errors in a macro script will result in a log entry and the termination
|
||||
// of the macro execution.
|
||||
//
|
||||
// Lines can be commented by prefixing the line with two dashes (--). Leading
|
||||
// and trailing white space is ignored.
|
||||
package macro
|
446
macro/macro.go
Normal file
446
macro/macro.go
Normal file
|
@ -0,0 +1,446 @@
|
|||
// 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 macro
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/gui"
|
||||
"github.com/jetsetilly/gopher2600/hardware"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
|
||||
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television"
|
||||
"github.com/jetsetilly/gopher2600/logger"
|
||||
"github.com/jetsetilly/gopher2600/userinput"
|
||||
)
|
||||
|
||||
type Emulation interface {
|
||||
UserInput() chan userinput.Event
|
||||
VCS() *hardware.VCS
|
||||
}
|
||||
|
||||
type Input interface {
|
||||
PushEvent(ports.InputEvent) error
|
||||
AllowPushedEvents(bool)
|
||||
}
|
||||
|
||||
type TV interface {
|
||||
AddFrameTrigger(f television.FrameTrigger)
|
||||
GetFrameInfo() television.FrameInfo
|
||||
}
|
||||
|
||||
type GUI interface {
|
||||
SetFeature(request gui.FeatureReq, args ...gui.FeatureReqData) error
|
||||
}
|
||||
|
||||
// Macro is a type that allows control of an emulation from a series of instructions
|
||||
type Macro struct {
|
||||
emulation Emulation
|
||||
input Input
|
||||
tv TV
|
||||
gui GUI
|
||||
|
||||
filename string
|
||||
instructions []string
|
||||
|
||||
quit chan bool
|
||||
frameNum chan int
|
||||
}
|
||||
|
||||
const (
|
||||
headerLineID = iota
|
||||
headerLineVersion
|
||||
headerNumLines
|
||||
)
|
||||
|
||||
const headerID = "gopher2600macro"
|
||||
|
||||
// NewMacro is the preferred method of initialisation for the Macro type
|
||||
func NewMacro(filename string, emulation Emulation, input Input, tv TV, gui GUI) (*Macro, error) {
|
||||
mcr := &Macro{
|
||||
emulation: emulation,
|
||||
input: input,
|
||||
tv: tv,
|
||||
gui: gui,
|
||||
filename: filename,
|
||||
quit: make(chan bool),
|
||||
frameNum: make(chan int),
|
||||
}
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("macro: %w", err)
|
||||
}
|
||||
buffer, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("macro: %w", err)
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("macro: %w", err)
|
||||
}
|
||||
|
||||
// convert file contents to an array of lines
|
||||
mcr.instructions = strings.Split(string(buffer), "\n")
|
||||
if len(mcr.instructions) < headerNumLines {
|
||||
return nil, fmt.Errorf("macro: %s: not a macro file", filename)
|
||||
}
|
||||
if mcr.instructions[0] != headerID {
|
||||
return nil, fmt.Errorf("macro: %s: not a macro file", filename)
|
||||
}
|
||||
|
||||
// ignore version string for now
|
||||
|
||||
// we no longer need the header
|
||||
mcr.instructions = mcr.instructions[headerNumLines:]
|
||||
|
||||
// allow pushed events to the VCS input system
|
||||
mcr.input.AllowPushedEvents(true)
|
||||
mcr.tv.AddFrameTrigger(mcr)
|
||||
|
||||
return mcr, nil
|
||||
}
|
||||
|
||||
// Run a macro to completion
|
||||
func (mcr *Macro) Run() {
|
||||
log := func(ln int, msg string) {
|
||||
logger.Logf("macro", "%s: %d: %s", mcr.filename, ln+headerNumLines, msg)
|
||||
}
|
||||
|
||||
// wait function is used by the WAIT macro instruction but is also used by
|
||||
// the controller instructions (LEFT, FIRE, etc.) to indicate a short wait
|
||||
// of two frames before moving onto the next instruction in the macro
|
||||
// script. this ensures that the controller input has the chance to take
|
||||
// effect in the emulation
|
||||
wait := func(w int) bool {
|
||||
target := w + <-mcr.frameNum
|
||||
|
||||
var done bool
|
||||
for !done {
|
||||
select {
|
||||
case fn := <-mcr.frameNum:
|
||||
if fn >= target {
|
||||
done = true
|
||||
}
|
||||
case <-mcr.quit:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
convertAddress := func(s string) (uint16, error) {
|
||||
// convert hex indicator to one that ParseUint can deal with
|
||||
if s[0] == '$' {
|
||||
s = fmt.Sprintf("0x%s", s[1:])
|
||||
}
|
||||
|
||||
// convert address
|
||||
a, err := strconv.ParseUint(s, 0, 16)
|
||||
return uint16(a), err
|
||||
}
|
||||
|
||||
convertValue := func(s string) (uint8, error) {
|
||||
// convert hex indicator to one that ParseUint can deal with
|
||||
if s[0] == '$' {
|
||||
s = fmt.Sprintf("0x%s", s[1:])
|
||||
}
|
||||
|
||||
// convert address
|
||||
a, err := strconv.ParseUint(s, 0, 8)
|
||||
return uint8(a), err
|
||||
}
|
||||
|
||||
type loop struct {
|
||||
line int
|
||||
|
||||
// loop counters count upwards because it is more natural when
|
||||
// referencing the counter value to think of the counter as counting
|
||||
// upwards
|
||||
count int
|
||||
countEnd int
|
||||
|
||||
// if loop counter has been named then we need to know it so that we can
|
||||
// update the entry in the variables table
|
||||
countName string
|
||||
}
|
||||
|
||||
go func() {
|
||||
var loops []loop
|
||||
variables := make(map[string]any)
|
||||
|
||||
lookupVariable := func(n string) (int, bool, error) {
|
||||
// convert value
|
||||
if n[0] != '%' {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
n = n[1:]
|
||||
v, ok := variables[n]
|
||||
if !ok {
|
||||
return 0, true, fmt.Errorf("cannot use variable '%s' in POKE because it does not exist", n)
|
||||
}
|
||||
return v.(int), true, nil
|
||||
}
|
||||
|
||||
for ln := 0; ln < len(mcr.instructions); ln++ {
|
||||
s := mcr.instructions[ln]
|
||||
|
||||
toks := strings.Fields(s)
|
||||
if len(toks) == 0 {
|
||||
continue // for loop
|
||||
}
|
||||
|
||||
switch toks[0] {
|
||||
default:
|
||||
log(ln, fmt.Sprintf("unrecognised command: %s", toks[0]))
|
||||
return
|
||||
|
||||
case "--":
|
||||
// ignore comment lines
|
||||
|
||||
case "DO":
|
||||
tl := len(toks)
|
||||
switch tl {
|
||||
case 1:
|
||||
log(ln, "too few arguments for DO")
|
||||
return
|
||||
case 3:
|
||||
fallthrough
|
||||
case 2:
|
||||
ct, err := strconv.Atoi(toks[1])
|
||||
if err != nil {
|
||||
log(ln, err.Error())
|
||||
return
|
||||
}
|
||||
lp := loop{
|
||||
line: ln,
|
||||
countEnd: ct,
|
||||
}
|
||||
if tl == 3 {
|
||||
lp.countName = toks[2]
|
||||
variables[lp.countName] = lp.count
|
||||
}
|
||||
loops = append(loops, lp)
|
||||
default:
|
||||
log(ln, "too many arguments for DO")
|
||||
return
|
||||
}
|
||||
|
||||
case "LOOP":
|
||||
if len(toks) > 1 {
|
||||
log(ln, "too many arguments for LOOP")
|
||||
return
|
||||
}
|
||||
|
||||
// check for a quit signal but don't wait for it
|
||||
select {
|
||||
case <-mcr.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
idx := len(loops) - 1
|
||||
if idx == -1 {
|
||||
log(ln, "LOOP without a DO")
|
||||
return
|
||||
}
|
||||
|
||||
lp := &loops[idx]
|
||||
lp.count++
|
||||
|
||||
if lp.count < lp.countEnd {
|
||||
// loop is ongoing so return to start of loop
|
||||
ln = lp.line
|
||||
|
||||
// update named variable
|
||||
if lp.countName != "" {
|
||||
variables[lp.countName] = lp.count
|
||||
}
|
||||
} else {
|
||||
// loop has ended. remove from loop stack and delete variable name
|
||||
loops = loops[:idx]
|
||||
delete(variables, lp.countName)
|
||||
}
|
||||
|
||||
case "WAIT":
|
||||
// default to 60 frames
|
||||
w := 60
|
||||
|
||||
switch len(toks) {
|
||||
case 2:
|
||||
var err error
|
||||
w, err = strconv.Atoi(toks[1])
|
||||
if err != nil {
|
||||
log(ln, err.Error())
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case 1:
|
||||
if wait(w) {
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
log(ln, "too many arguments for WAIT")
|
||||
return
|
||||
}
|
||||
|
||||
case "SCREENSHOT":
|
||||
var s strings.Builder
|
||||
for _, c := range toks[1:] {
|
||||
v, ok, err := lookupVariable(c)
|
||||
if err != nil {
|
||||
log(ln, err.Error())
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
s.WriteString(fmt.Sprintf("%d", v))
|
||||
} else {
|
||||
s.WriteString(c)
|
||||
}
|
||||
s.WriteRune(' ')
|
||||
}
|
||||
|
||||
// the filename suffix is all the "words" in string builder
|
||||
// joined with an underscore
|
||||
filenameSuffix := strings.Join(strings.Fields(s.String()), "_")
|
||||
|
||||
mcr.gui.SetFeature(gui.ReqScreenshot, filenameSuffix)
|
||||
wait(10)
|
||||
|
||||
case "QUIT":
|
||||
if len(toks) > 1 {
|
||||
log(ln, "too many arguments for QUIT")
|
||||
return
|
||||
}
|
||||
mcr.emulation.UserInput() <- userinput.EventQuit{}
|
||||
|
||||
case "FIRE":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.Fire, D: true})
|
||||
wait(2)
|
||||
case "NOFIRE":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.Fire, D: false})
|
||||
wait(2)
|
||||
case "LEFT":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.Left, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "RIGHT":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.Right, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "UP":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.Up, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "DOWN":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.Down, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "LEFTUP":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.LeftUp, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "LEFTDOWN":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.LeftDown, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "RIGHTUP":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.RightUp, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "RIGHTDOWN":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.RightDown, D: ports.DataStickTrue})
|
||||
wait(2)
|
||||
case "CENTER":
|
||||
fallthrough
|
||||
case "CENTRE":
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortLeft, Ev: ports.Centre})
|
||||
wait(2)
|
||||
case "SELECT":
|
||||
held := false
|
||||
if len(toks) == 1 {
|
||||
held = true
|
||||
}
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: held})
|
||||
wait(2)
|
||||
case "RESET":
|
||||
held := false
|
||||
if len(toks) == 1 {
|
||||
held = true
|
||||
}
|
||||
mcr.input.PushEvent(ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: held})
|
||||
wait(2)
|
||||
case "POKE":
|
||||
if len(toks) != 3 {
|
||||
log(ln, "not enough arguments for POKE")
|
||||
return
|
||||
}
|
||||
|
||||
// convert address
|
||||
addr, err := convertAddress(toks[1])
|
||||
if err != nil {
|
||||
log(ln, fmt.Sprintf("unrecognised address for POKE: %s", toks[1]))
|
||||
return
|
||||
}
|
||||
|
||||
var val uint8
|
||||
|
||||
v, ok, err := lookupVariable(toks[2])
|
||||
if err != nil {
|
||||
log(ln, err.Error())
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
val = uint8(v)
|
||||
} else {
|
||||
v, err := convertValue(toks[2])
|
||||
if err != nil {
|
||||
log(ln, fmt.Sprintf("cannot use value for POKE: %s", toks[2]))
|
||||
return
|
||||
}
|
||||
val = uint8(v)
|
||||
}
|
||||
|
||||
// poke address with value
|
||||
mem := mcr.emulation.VCS().Mem
|
||||
mem.Poke(addr, val)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Quit forces a running macro (ie. one that has been triggered) to end. Does
|
||||
// nothing is macro is not currently running
|
||||
func (mcr *Macro) Quit() {
|
||||
select {
|
||||
case mcr.quit <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// NewFrame implements the television.FrameTrigger interface
|
||||
func (mcr *Macro) NewFrame(frameInfo television.FrameInfo) error {
|
||||
// drain any frameNum channel before pushing a new value
|
||||
select {
|
||||
case <-mcr.frameNum:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case mcr.frameNum <- frameInfo.FrameNum:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue