- implemented patching

o setup
    - added patch entry type

o gopher2600
    - added patch flag to RUN mode
This commit is contained in:
steve 2019-12-22 20:44:16 +00:00
parent d7cdfcfe61
commit 0f5a258482
20 changed files with 419 additions and 70 deletions

View file

@ -13,6 +13,7 @@ import (
"gopher2600/hardware/memory/addresses"
"gopher2600/hardware/memory/memorymap"
"gopher2600/hardware/riot/input"
"gopher2600/patch"
"gopher2600/symbols"
"os"
"sort"
@ -47,6 +48,7 @@ const (
cmdPlayer = "PLAYER"
cmdPlayfield = "PLAYFIELD"
cmdPoke = "POKE"
cmdPatch = "PATCH"
cmdQuit = "QUIT"
cmdExit = "EXIT"
cmdRAM = "RAM"
@ -95,6 +97,7 @@ var commandTemplate = []string{
cmdPlayer + " (0|1)",
cmdPlayfield,
cmdPoke + " [%S] %N",
cmdPatch + " %S",
cmdQuit,
cmdExit,
cmdRAM + " (CART)",
@ -843,6 +846,20 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
dbg.print(terminal.StyleInstrument, ai.String())
}
case cmdPatch:
f, _ := tokens.Get()
patched, err := patch.CartridgeMemory(dbg.vcs.Mem.Cart, f)
if err != nil {
dbg.print(terminal.StyleError, "%v", err)
if patched {
dbg.print(terminal.StyleEmulatorInfo, "error during patching. cartridge might be unusable.")
}
return doNothing, nil
}
if patched {
dbg.print(terminal.StyleEmulatorInfo, "cartridge patched")
}
case cmdHexLoad:
// get address token
a, _ := tokens.Get()

View file

@ -27,6 +27,7 @@ var help = map[string]string{
cmdPlayer: "Display the current state of the player 0/1 sprite",
cmdPlayfield: "Display the current playfield data",
cmdPoke: "Modify an individual memory address",
cmdPatch: "Apply a patch file to the loaded cartridge",
cmdExit: "Exits the emulator",
cmdQuit: "Exits the emulator",
cmdRAM: "Display the current contents of PIA RAM",

View file

@ -3,24 +3,36 @@
// code to wrap errors around other errors and to allow normalised formatted
// output of error messages.
//
// It also contains ID values for different classes of error and the English
// messages for those error classes. These could be translated to other
// languages, althought the i8n mechanism is not currently in place.
//
// The most useful feature is deduplication of wrapped errors. This means that
// code does not need to worry about the immediate context of the function
// which creates the error. For instance:
//
// func main() { err := A() if err != nil { fmt.Println(err) } }
// func main() {
// err := A()
// if err != nil {
// fmt.Println(err)
// }
// }
//
// func A() error { err := B() if err != nil { return
// errors.New(errors.DebuggerError, err) } return nil }
// func A() error {
// err := B()
// if err != nil {
// return errors.New(errors.DebuggerError, err)
// }
// return nil
// }
//
// func B() error { err := C() if err != nil { return
// errors.New(errors.DebuggerError, rr) } return nil }
// func B() error {
// err := C()
// if err != nil {
// return errors.New(errors.DebuggerError, rr)
// }
// return nil
// }
//
// func C() error { return errors.New(errors.PanicError, "C()", "not yet
// implemented") }
// func C() error {
// return errors.New(errors.PanicError, "C()", "not yet implemented")
// }
//
// If we follow the code from main() we can see that first error created is a
// PanicError, wrapped in a DebuggerError, wrapped in a DebuggerError. The
@ -32,13 +44,11 @@
//
// error debugging vcs: error debugging vcs: panic: C(): not yet implemented
//
// As can be seen, the error message has been deduplicatd, or normalised.
//
// The PanicError, used in the above example, is a special error that should be
// used when something has happened such that the state of the emulation (or
// the tool) can no longer be guaranteed.
//
// Actual panics should only be used when the error is so terrible that there i
// nothing sensible to be done; useful for brute-enforcement of programming
// Actual panics should only be used when the error is so terrible that there
// is nothing sensible to be done; useful for brute-enforcement of programming
// constraints and in init() functions.
package errors

View file

@ -44,7 +44,7 @@ const (
// database
DatabaseError = "database error: %v"
DatabaseReadError = "datbase error: %v [line %d]"
DatabaseReadError = "database error: %v [line %d]"
DatabaseSelectEmpty = "database error: no selected entries"
DatabaseKeyError = "database error: no such key in database [%v]"
DatabaseFileUnavailable = "database error: cannot open database (%v)"
@ -57,6 +57,11 @@ const (
// setup
SetupError = "setup error: %v"
SetupPanelError = "setup error: panel entry: %v"
SetupPatchError = "setup error: patch entry: %v"
// patch
PatchError = "patch error: %v"
PatchFileError = "patch error: patch file not found (%v)"
// symbols
SymbolsFileError = "symbols error: error processing symbols file: %v"
@ -84,8 +89,9 @@ const (
UnpeekableAddress = "memory error: cannot peek address (%v)"
// cartridges
CartridgeError = "cartridge error: %v"
CartridgeEjected = "cartridge error: no cartridge attached"
CartridgeError = "cartridge error: %v"
CartridgeEjected = "cartridge error: no cartridge attached"
UnpatchableCartType = "cartridge error: cannot patch this cartridge type (%v)"
// input
InputDeviceUnavailable = "input error: controller hardware unavailable (%v)"

View file

@ -82,6 +82,7 @@ func play(md *modalflag.Modes) error {
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)")
p, err := md.Parse()
if p != modalflag.ParseContinue {
@ -117,7 +118,7 @@ func play(md *modalflag.Modes) error {
return errors.New(errors.PlayError, err)
}
err = playmode.Play(tv, scr, *stable, *fpscap, *record, cartload)
err = playmode.Play(tv, scr, *stable, *fpscap, *record, cartload, *patchFile)
if err != nil {
return err
}

View file

@ -19,6 +19,14 @@ type cartMapper interface {
// require a way of notifying the cartridge of writes to addresses outside
// of cartridge space
listen(addr uint16, data uint8)
// poke new value anywhere into currently selected bank of cartridge memory
// (including ROM).
poke(addr uint16, data uint8) error
// patch differs from poke in that it alters the data as though it was
// being read from disk
patch(offset uint16, data uint8) error
}
// optionalSuperchip are implemented by cartMappers that have an optional

View file

@ -40,9 +40,15 @@ func (cart Cartridge) Peek(addr uint16) (uint8, error) {
return cart.Read(addr)
}
// Poke is an implementation of memory.DebuggerBus
func (cart Cartridge) Poke(addr uint16, data uint8) error {
return errors.New(errors.UnpokeableAddress, addr)
// Poke is an implementation of memory.DebuggerBus. This poke pokes the current
// cartridge bank. See Patch for a different method.
func (cart *Cartridge) Poke(addr uint16, data uint8) error {
return cart.mapper.poke(addr^memorymap.OriginCart, data)
}
// Patch rewrites cartridge location as though that value was at file offset
func (cart *Cartridge) Patch(offset uint16, data uint8) error {
return cart.mapper.patch(offset, data)
}
// Read is an implementation of memory.CPUBus

View file

@ -48,6 +48,8 @@ import (
type atari struct {
method string
bankSize int
// atari formats apart from 2k and 4k are divided into banks. 2k and 4k
// ROMs conceptually have one bank
banks [][]uint8
@ -146,7 +148,19 @@ func (cart atari) ram() []uint8 {
return cart.superchip
}
func (cart atari) listen(addr uint16, data uint8) {
func (cart *atari) listen(addr uint16, data uint8) {
}
func (cart *atari) poke(addr uint16, data uint8) error {
cart.banks[cart.bank][addr] = data
return nil
}
func (cart *atari) patch(addr uint16, data uint8) error {
bank := int(addr) / cart.bankSize
addr = addr % uint16(cart.bankSize)
cart.banks[bank][addr] = data
return nil
}
// atari4k is the original and most straightforward format
@ -164,17 +178,16 @@ type atari4k struct {
// o Yars Revenge
// o etc.
func newAtari4k(data []byte) (cartMapper, error) {
const bankSize = 4096
cart := &atari4k{}
cart.bankSize = 4096
cart.method = "atari 4k"
cart.banks = make([][]uint8, 1)
if len(data) != bankSize*cart.numBanks() {
if len(data) != cart.bankSize*cart.numBanks() {
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
}
cart.banks[0] = make([]uint8, bankSize)
cart.banks[0] = make([]uint8, cart.bankSize)
copy(cart.banks[0], data)
cart.initialise()
@ -212,17 +225,16 @@ type atari2k struct {
}
func newAtari2k(data []byte) (cartMapper, error) {
const bankSize = 2048
cart := &atari2k{}
cart.bankSize = 2048
cart.method = "atari 2k"
cart.banks = make([][]uint8, 1)
if len(data) != bankSize*cart.numBanks() {
if len(data) != cart.bankSize*cart.numBanks() {
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
}
cart.banks[0] = make([]uint8, bankSize)
cart.banks[0] = make([]uint8, cart.bankSize)
copy(cart.banks[0], data)
cart.initialise()
@ -258,20 +270,19 @@ type atari8k struct {
}
func newAtari8k(data []uint8) (cartMapper, error) {
const bankSize = 4096
cart := &atari8k{}
cart.bankSize = 4096
cart.method = "atari 8k (F8)"
cart.banks = make([][]uint8, cart.numBanks())
if len(data) != bankSize*cart.numBanks() {
if len(data) != cart.bankSize*cart.numBanks() {
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
}
for k := 0; k < cart.numBanks(); k++ {
cart.banks[k] = make([]uint8, bankSize)
offset := k * bankSize
copy(cart.banks[k], data[offset:offset+bankSize])
cart.banks[k] = make([]uint8, cart.bankSize)
offset := k * cart.bankSize
copy(cart.banks[k], data[offset:offset+cart.bankSize])
}
cart.initialise()
@ -325,20 +336,19 @@ type atari16k struct {
}
func newAtari16k(data []byte) (cartMapper, error) {
const bankSize = 4096
cart := &atari16k{}
cart.bankSize = 4096
cart.method = "atari 16k (F6)"
cart.banks = make([][]uint8, cart.numBanks())
if len(data) != bankSize*cart.numBanks() {
if len(data) != cart.bankSize*cart.numBanks() {
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
}
for k := 0; k < cart.numBanks(); k++ {
cart.banks[k] = make([]uint8, bankSize)
offset := k * bankSize
copy(cart.banks[k], data[offset:offset+bankSize])
cart.banks[k] = make([]uint8, cart.bankSize)
offset := k * cart.bankSize
copy(cart.banks[k], data[offset:offset+cart.bankSize])
}
cart.initialise()
@ -400,20 +410,19 @@ type atari32k struct {
}
func newAtari32k(data []byte) (cartMapper, error) {
const bankSize = 4096
cart := &atari32k{}
cart.bankSize = 4096
cart.method = "atari 32k (F4)"
cart.banks = make([][]uint8, cart.numBanks())
if len(data) != bankSize*cart.numBanks() {
if len(data) != cart.bankSize*cart.numBanks() {
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
}
for k := 0; k < cart.numBanks(); k++ {
cart.banks[k] = make([]uint8, bankSize)
offset := k * bankSize
copy(cart.banks[k], data[offset:offset+bankSize])
cart.banks[k] = make([]uint8, cart.bankSize)
offset := k * cart.bankSize
copy(cart.banks[k], data[offset:offset+cart.bankSize])
}
cart.initialise()

View file

@ -126,5 +126,13 @@ func (cart cbs) ram() []uint8 {
return cart.superchip
}
func (cart cbs) listen(addr uint16, data uint8) {
func (cart *cbs) listen(addr uint16, data uint8) {
}
func (cart *cbs) poke(addr uint16, data uint8) error {
return errors.New(errors.UnpokeableAddress, addr)
}
func (cart *cbs) patch(addr uint16, data uint8) error {
return errors.New(errors.UnpatchableCartType, cart.method)
}

View file

@ -60,5 +60,13 @@ func (cart ejected) ram() []uint8 {
return []uint8{}
}
func (cart ejected) listen(addr uint16, data uint8) {
func (cart *ejected) listen(addr uint16, data uint8) {
}
func (cart *ejected) poke(addr uint16, data uint8) error {
return errors.New(errors.UnpokeableAddress, addr)
}
func (cart *ejected) patch(addr uint16, data uint8) error {
return errors.New(errors.UnpatchableCartType, cart.method)
}

View file

@ -274,3 +274,11 @@ func (cart *mnetwork) ram() []uint8 {
func (cart *mnetwork) listen(addr uint16, data uint8) {
}
func (cart *mnetwork) poke(addr uint16, data uint8) error {
return errors.New(errors.UnpokeableAddress, addr)
}
func (cart *mnetwork) patch(addr uint16, data uint8) error {
return errors.New(errors.UnpatchableCartType, cart.method)
}

View file

@ -219,5 +219,13 @@ func (cart parkerBros) ram() []uint8 {
return []uint8{}
}
func (cart parkerBros) listen(addr uint16, data uint8) {
func (cart *parkerBros) listen(addr uint16, data uint8) {
}
func (cart *parkerBros) poke(addr uint16, data uint8) error {
return errors.New(errors.UnpokeableAddress, addr)
}
func (cart *parkerBros) patch(addr uint16, data uint8) error {
return errors.New(errors.UnpatchableCartType, cart.method)
}

View file

@ -155,3 +155,11 @@ func (cart *tigervision) listen(addr uint16, data uint8) {
// to TIA space for real and not cause a bankswitch. for this reason,
// tigervision cartridges use mirror addresses to write to the TIA.
}
func (cart *tigervision) poke(addr uint16, data uint8) error {
return errors.New(errors.UnpokeableAddress, addr)
}
func (cart *tigervision) patch(addr uint16, data uint8) error {
return errors.New(errors.UnpatchableCartType, cart.method)
}

View file

@ -0,0 +1,35 @@
// Package patch is used to patch the contents of a cartridge. It works on
// cartridge memory once a cartridge file has been attached. The package does
// not implement the patching directly, rather the different cartridge mappers
// (see cartridge package) deal with that individually.
//
// This package simply loads the patch instructions, interprets them and calls
// the cartridge.Patch() function. Currently only one patch format is
// supported. This is an ad-hoc format taken from the "In case you can't wait"
// section of the following web page:
//
// "Fixing E.T. The Extra-Terrestrial for the Atari 2600"
//
// http://www.neocomputer.org/projects/et/
//
// The following extract illustrates the format:
//
// -------------------------------------------
// - E.T. is Not Green
// -------------------------------------------
// 17FA: FE FC F8 F8 F8
// 1DE8: 04
//
// Rules:
//
// 1. Lines beginning with a hyphen or white space are ignored
// 2. Addresses and values are expressed in hex (case-insensitive)
// 3. Values and addresses are separated by a colon
// 4. Multiple values on a line are poked into consecutive addresses, starting
// from the address value
//
// Note that addresses are expressed with origin zero and have no relationship
// to how memory is mapped inside the VCS. Imagine that the patches are being
// applied to the cartridge file image. The cartridge mapper handles the VCS
// memory side of things.
package patch

View file

@ -0,0 +1,111 @@
package patch
import (
"gopher2600/errors"
"gopher2600/hardware/memory/cartridge"
"gopher2600/paths"
"io"
"io/ioutil"
"os"
"strconv"
"strings"
"unicode"
)
const patchPath = "patches"
const commentLeader = '-'
const pokeLineSeparator = ":"
// CartridgeMemory applies the contents of a patch file to cartridge memory.
// Currently, patch file must be in the patches sub-directory of the
// resource path (see paths package).
func CartridgeMemory(mem *cartridge.Cartridge, patchFile string) (bool, error) {
var err error
p := paths.ResourcePath(patchPath, patchFile)
f, err := os.Open(p)
if err != nil {
switch err.(type) {
case *os.PathError:
return false, errors.New(errors.PatchFileError, p)
}
return false, errors.New(errors.PatchError, err)
}
defer f.Close()
// make sure we're at the beginning of the file
if _, err = f.Seek(0, io.SeekStart); err != nil {
return false, err
}
buffer, err := ioutil.ReadAll(f)
// once a patch has been made then we'll flip patched to true and return it
// to the calling function
patched := false
// walk through lines
lines := strings.Split(string(buffer), "\n")
for i := 0; i < len(lines); i++ {
// ignore empty lines
if len(lines[i]) == 0 {
continue // for loop
}
// ignoring comment lines and lines starting with whitespace
if lines[i][0] == commentLeader || unicode.IsSpace(rune(lines[i][0])) {
continue // for loop
}
pokeLine := strings.Split(lines[i], pokeLineSeparator)
// ignore any lines that don't match the required [address: values...] format
if len(pokeLine) != 2 {
continue // for loop
}
// trim space around each poke line part
pokeLine[0] = strings.TrimSpace(pokeLine[0])
pokeLine[1] = strings.TrimSpace(pokeLine[1])
// parse address
address, err := strconv.ParseInt(pokeLine[0], 16, 16)
if err != nil {
continue // for loop
}
// split values into parts
values := strings.Split(pokeLine[1], " ")
for j := 0; j < len(values); j++ {
// trim space around each value
values[j] = strings.TrimSpace(values[j])
// ignore empty fields
if values[j] == "" {
continue // inner for loop
}
// covert data
v, err := strconv.ParseUint(values[j], 16, 8)
if err != nil {
continue // inner for loop
}
// patch memory
err = mem.Patch(uint16(address), uint8(v))
if err != nil {
return patched, errors.New(errors.PatchError, err)
}
patched = true
// advance address
address++
}
}
return patched, nil
}

View file

@ -1,8 +1,8 @@
// Package paths should be used whenever a request to the filesystem is made.
// Package paths contains functions to prepare paths to gopher2600 resources.
//
// The ResourcePath() function modifies the supplied resource string such that
// it is prepended with the appropriate gopher2600 config directory. For
// example, the following will return the path to the ET patch.
// it is prepended with the appropriate config directory. For example, the
// following will return the path to a cartridge patch.
//
// d := paths.ResourcePath("patches", "ET")
//
@ -14,5 +14,5 @@
//
// In the example above, on a modern Linux system, the path returned will be:
//
// /home/steve/.config/gopher2600/patches/ET
// /home/user/.config/gopher2600/patches/ET
package paths

View file

@ -9,6 +9,7 @@ import (
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/hardware"
"gopher2600/patch"
"gopher2600/recorder"
"gopher2600/setup"
"gopher2600/television"
@ -18,7 +19,7 @@ import (
)
// Play is a quick of setting up a playable instance of the emulator.
func Play(tv television.Television, scr gui.GUI, showOnStable bool, fpscap bool, newRecording bool, cartload cartridgeloader.Loader) error {
func Play(tv television.Television, scr gui.GUI, showOnStable bool, fpscap bool, newRecording bool, cartload cartridgeloader.Loader, patchFile string) error {
var transcript string
// if supplied cartridge name is actually a playback file then set
@ -97,12 +98,21 @@ func Play(tv television.Television, scr gui.GUI, showOnStable bool, fpscap bool,
} else {
// no new recording requested and no transcript given. this is a 'normal'
// launch of the emalator for regular pla
// launch of the emalator for regular play
err = setup.AttachCartridge(vcs, cartload)
if err != nil {
return errors.New(errors.PlayError, err)
}
// apply patch if requested. note that this will be in addition to any
// patches applied during setup.AttachCartridge
if patchFile != "" {
_, err := patch.CartridgeMemory(vcs.Mem.Cart, patchFile)
if err != nil {
return errors.New(errors.PlayError, err)
}
}
}
// connect gui

View file

@ -1,16 +1,25 @@
// Package setup is used to preset the emulation depending on the attached
// cartridge.
// cartridge. It is currently quite limited but is useful none-the-less.
// Currently support entry types:
//
// This package is not yet complete. It currently only supports panel setup.
// ie. the setting of the switches on the frontpanel.
// Toggling of panel switches
// Apply patches to cartridge
//
// Other setup option idea: POKEs. For example, bug fixing the ET cartridge on
// startup.
// Menu driven selection of patches would be a nice feature to have in the
// future. But at the moment, the package doesn't even facilitate editing of
// entries. Adding new entries to the setup database therefore requires editing
// the DB file by hand. For reference the following describes the format of
// each entry type:
//
// Eventually we would probably require a menu driven selection of setups. In
// other words, a cartridge is loaded and there but there are several setup
// options to choose from (eg. bug-fixed or original ROM)
// Panel Toggles
//
// The setup pacakge currently doesn't facilitate editing of the setup
// database, only reading.
// <DB Key>, panel, <SHA-1 Hash>, <player 0 (bool)>, .<player 1 (bool)>, <color (bool)>, <notes>
//
// When editing the DB file, make sure the DB Key is unique
//
// Patch Cartridge
//
// <DB Key>, patch, <SHA-1 Hash>, <patch file>, <notes>
//
// Patch files are located in the patches sub-directory of the resources path.
package setup

84
setup/patch.go Normal file
View file

@ -0,0 +1,84 @@
package setup
import (
"fmt"
"gopher2600/database"
"gopher2600/errors"
"gopher2600/hardware"
"gopher2600/patch"
)
const patchID = "patch"
const (
patchFieldCartHash int = iota
patchFieldPatchFile
patchFieldNotes
numPatchFields
)
// Patch is used to patch cartridge memory after cartridge has been
// attached/loaded
type Patch struct {
cartHash string
patchFile string
notes string
}
func deserialisePatchEntry(fields database.SerialisedEntry) (database.Entry, error) {
set := &Patch{}
// basic sanity check
if len(fields) > numPatchFields {
return nil, errors.New(errors.SetupPatchError, "too many fields in patch entry")
}
if len(fields) < numPatchFields {
return nil, errors.New(errors.SetupPatchError, "too few fields in patch entry")
}
set.cartHash = fields[patchFieldCartHash]
set.patchFile = fields[patchFieldPatchFile]
set.notes = fields[patchFieldNotes]
return set, nil
}
// ID implements the database.Entry interface
func (set Patch) ID() string {
return patchID
}
// String implements the database.Entry interface
func (set Patch) String() string {
return fmt.Sprintf("%s, %s", set.cartHash, set.patchFile)
}
// Serialise implements the database.Entry interface
func (set *Patch) Serialise() (database.SerialisedEntry, error) {
return database.SerialisedEntry{
set.cartHash,
set.patchFile,
set.notes,
},
nil
}
// CleanUp implements the database.Entry interface
func (set Patch) CleanUp() error {
// no cleanup necessary
return nil
}
// matchCartHash implements setupEntry interface
func (set Patch) matchCartHash(hash string) bool {
return set.cartHash == hash
}
// apply implements setupEntry interface
func (set Patch) apply(vcs *hardware.VCS) error {
_, err := patch.CartridgeMemory(vcs.Mem.Cart, set.patchFile)
if err != nil {
return errors.New(errors.SetupPatchError, err)
}
return nil
}

View file

@ -26,12 +26,14 @@ type setupEntry interface {
// that will be found in the database
func initDBSession(db *database.Session) error {
// add panel setup entry type
// add entry types
if err := db.RegisterEntryType(panelSetupID, deserialisePanelSetupEntry); err != nil {
return err
}
// add other entry types
if err := db.RegisterEntryType(patchID, deserialisePatchEntry); err != nil {
return err
}
return nil
}