mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-22 22:42:25 -04:00
o patch
- implemented patching o setup - added patch entry type o gopher2600 - added patch flag to RUN mode
This commit is contained in:
parent
d7cdfcfe61
commit
0f5a258482
|
@ -13,6 +13,7 @@ import (
|
||||||
"gopher2600/hardware/memory/addresses"
|
"gopher2600/hardware/memory/addresses"
|
||||||
"gopher2600/hardware/memory/memorymap"
|
"gopher2600/hardware/memory/memorymap"
|
||||||
"gopher2600/hardware/riot/input"
|
"gopher2600/hardware/riot/input"
|
||||||
|
"gopher2600/patch"
|
||||||
"gopher2600/symbols"
|
"gopher2600/symbols"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -47,6 +48,7 @@ const (
|
||||||
cmdPlayer = "PLAYER"
|
cmdPlayer = "PLAYER"
|
||||||
cmdPlayfield = "PLAYFIELD"
|
cmdPlayfield = "PLAYFIELD"
|
||||||
cmdPoke = "POKE"
|
cmdPoke = "POKE"
|
||||||
|
cmdPatch = "PATCH"
|
||||||
cmdQuit = "QUIT"
|
cmdQuit = "QUIT"
|
||||||
cmdExit = "EXIT"
|
cmdExit = "EXIT"
|
||||||
cmdRAM = "RAM"
|
cmdRAM = "RAM"
|
||||||
|
@ -95,6 +97,7 @@ var commandTemplate = []string{
|
||||||
cmdPlayer + " (0|1)",
|
cmdPlayer + " (0|1)",
|
||||||
cmdPlayfield,
|
cmdPlayfield,
|
||||||
cmdPoke + " [%S] %N",
|
cmdPoke + " [%S] %N",
|
||||||
|
cmdPatch + " %S",
|
||||||
cmdQuit,
|
cmdQuit,
|
||||||
cmdExit,
|
cmdExit,
|
||||||
cmdRAM + " (CART)",
|
cmdRAM + " (CART)",
|
||||||
|
@ -843,6 +846,20 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
|
||||||
dbg.print(terminal.StyleInstrument, ai.String())
|
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:
|
case cmdHexLoad:
|
||||||
// get address token
|
// get address token
|
||||||
a, _ := tokens.Get()
|
a, _ := tokens.Get()
|
||||||
|
|
|
@ -27,6 +27,7 @@ var help = map[string]string{
|
||||||
cmdPlayer: "Display the current state of the player 0/1 sprite",
|
cmdPlayer: "Display the current state of the player 0/1 sprite",
|
||||||
cmdPlayfield: "Display the current playfield data",
|
cmdPlayfield: "Display the current playfield data",
|
||||||
cmdPoke: "Modify an individual memory address",
|
cmdPoke: "Modify an individual memory address",
|
||||||
|
cmdPatch: "Apply a patch file to the loaded cartridge",
|
||||||
cmdExit: "Exits the emulator",
|
cmdExit: "Exits the emulator",
|
||||||
cmdQuit: "Exits the emulator",
|
cmdQuit: "Exits the emulator",
|
||||||
cmdRAM: "Display the current contents of PIA RAM",
|
cmdRAM: "Display the current contents of PIA RAM",
|
||||||
|
|
|
@ -3,24 +3,36 @@
|
||||||
// code to wrap errors around other errors and to allow normalised formatted
|
// code to wrap errors around other errors and to allow normalised formatted
|
||||||
// output of error messages.
|
// 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
|
// 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
|
// code does not need to worry about the immediate context of the function
|
||||||
// which creates the error. For instance:
|
// 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
|
// func A() error {
|
||||||
// errors.New(errors.DebuggerError, err) } return nil }
|
// err := B()
|
||||||
|
// if err != nil {
|
||||||
|
// return errors.New(errors.DebuggerError, err)
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// func B() error { err := C() if err != nil { return
|
// func B() error {
|
||||||
// errors.New(errors.DebuggerError, rr) } return nil }
|
// err := C()
|
||||||
|
// if err != nil {
|
||||||
|
// return errors.New(errors.DebuggerError, rr)
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// func C() error { return errors.New(errors.PanicError, "C()", "not yet
|
// func C() error {
|
||||||
// implemented") }
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// used when something has happened such that the state of the emulation (or
|
||||||
// the tool) can no longer be guaranteed.
|
// the tool) can no longer be guaranteed.
|
||||||
//
|
//
|
||||||
// Actual panics should only be used when the error is so terrible that there i
|
// Actual panics should only be used when the error is so terrible that there
|
||||||
// nothing sensible to be done; useful for brute-enforcement of programming
|
// is nothing sensible to be done; useful for brute-enforcement of programming
|
||||||
// constraints and in init() functions.
|
// constraints and in init() functions.
|
||||||
package errors
|
package errors
|
||||||
|
|
|
@ -44,7 +44,7 @@ const (
|
||||||
|
|
||||||
// database
|
// database
|
||||||
DatabaseError = "database error: %v"
|
DatabaseError = "database error: %v"
|
||||||
DatabaseReadError = "datbase error: %v [line %d]"
|
DatabaseReadError = "database error: %v [line %d]"
|
||||||
DatabaseSelectEmpty = "database error: no selected entries"
|
DatabaseSelectEmpty = "database error: no selected entries"
|
||||||
DatabaseKeyError = "database error: no such key in database [%v]"
|
DatabaseKeyError = "database error: no such key in database [%v]"
|
||||||
DatabaseFileUnavailable = "database error: cannot open database (%v)"
|
DatabaseFileUnavailable = "database error: cannot open database (%v)"
|
||||||
|
@ -57,6 +57,11 @@ const (
|
||||||
// setup
|
// setup
|
||||||
SetupError = "setup error: %v"
|
SetupError = "setup error: %v"
|
||||||
SetupPanelError = "setup error: panel entry: %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
|
// symbols
|
||||||
SymbolsFileError = "symbols error: error processing symbols file: %v"
|
SymbolsFileError = "symbols error: error processing symbols file: %v"
|
||||||
|
@ -84,8 +89,9 @@ const (
|
||||||
UnpeekableAddress = "memory error: cannot peek address (%v)"
|
UnpeekableAddress = "memory error: cannot peek address (%v)"
|
||||||
|
|
||||||
// cartridges
|
// cartridges
|
||||||
CartridgeError = "cartridge error: %v"
|
CartridgeError = "cartridge error: %v"
|
||||||
CartridgeEjected = "cartridge error: no cartridge attached"
|
CartridgeEjected = "cartridge error: no cartridge attached"
|
||||||
|
UnpatchableCartType = "cartridge error: cannot patch this cartridge type (%v)"
|
||||||
|
|
||||||
// input
|
// input
|
||||||
InputDeviceUnavailable = "input error: controller hardware unavailable (%v)"
|
InputDeviceUnavailable = "input error: controller hardware unavailable (%v)"
|
||||||
|
|
|
@ -82,6 +82,7 @@ func play(md *modalflag.Modes) error {
|
||||||
fpscap := md.AddBool("fpscap", true, "cap fps to specification")
|
fpscap := md.AddBool("fpscap", true, "cap fps to specification")
|
||||||
record := md.AddBool("record", false, "record user input to a file")
|
record := md.AddBool("record", false, "record user input to a file")
|
||||||
wav := md.AddString("wav", "", "record audio to wav 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()
|
p, err := md.Parse()
|
||||||
if p != modalflag.ParseContinue {
|
if p != modalflag.ParseContinue {
|
||||||
|
@ -117,7 +118,7 @@ func play(md *modalflag.Modes) error {
|
||||||
return errors.New(errors.PlayError, err)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,14 @@ type cartMapper interface {
|
||||||
// require a way of notifying the cartridge of writes to addresses outside
|
// require a way of notifying the cartridge of writes to addresses outside
|
||||||
// of cartridge space
|
// of cartridge space
|
||||||
listen(addr uint16, data uint8)
|
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
|
// optionalSuperchip are implemented by cartMappers that have an optional
|
||||||
|
|
|
@ -40,9 +40,15 @@ func (cart Cartridge) Peek(addr uint16) (uint8, error) {
|
||||||
return cart.Read(addr)
|
return cart.Read(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poke is an implementation of memory.DebuggerBus
|
// Poke is an implementation of memory.DebuggerBus. This poke pokes the current
|
||||||
func (cart Cartridge) Poke(addr uint16, data uint8) error {
|
// cartridge bank. See Patch for a different method.
|
||||||
return errors.New(errors.UnpokeableAddress, addr)
|
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
|
// Read is an implementation of memory.CPUBus
|
||||||
|
|
|
@ -48,6 +48,8 @@ import (
|
||||||
type atari struct {
|
type atari struct {
|
||||||
method string
|
method string
|
||||||
|
|
||||||
|
bankSize int
|
||||||
|
|
||||||
// atari formats apart from 2k and 4k are divided into banks. 2k and 4k
|
// atari formats apart from 2k and 4k are divided into banks. 2k and 4k
|
||||||
// ROMs conceptually have one bank
|
// ROMs conceptually have one bank
|
||||||
banks [][]uint8
|
banks [][]uint8
|
||||||
|
@ -146,7 +148,19 @@ func (cart atari) ram() []uint8 {
|
||||||
return cart.superchip
|
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
|
// atari4k is the original and most straightforward format
|
||||||
|
@ -164,17 +178,16 @@ type atari4k struct {
|
||||||
// o Yars Revenge
|
// o Yars Revenge
|
||||||
// o etc.
|
// o etc.
|
||||||
func newAtari4k(data []byte) (cartMapper, error) {
|
func newAtari4k(data []byte) (cartMapper, error) {
|
||||||
const bankSize = 4096
|
|
||||||
|
|
||||||
cart := &atari4k{}
|
cart := &atari4k{}
|
||||||
|
cart.bankSize = 4096
|
||||||
cart.method = "atari 4k"
|
cart.method = "atari 4k"
|
||||||
cart.banks = make([][]uint8, 1)
|
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")
|
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)
|
copy(cart.banks[0], data)
|
||||||
|
|
||||||
cart.initialise()
|
cart.initialise()
|
||||||
|
@ -212,17 +225,16 @@ type atari2k struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAtari2k(data []byte) (cartMapper, error) {
|
func newAtari2k(data []byte) (cartMapper, error) {
|
||||||
const bankSize = 2048
|
|
||||||
|
|
||||||
cart := &atari2k{}
|
cart := &atari2k{}
|
||||||
|
cart.bankSize = 2048
|
||||||
cart.method = "atari 2k"
|
cart.method = "atari 2k"
|
||||||
cart.banks = make([][]uint8, 1)
|
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")
|
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)
|
copy(cart.banks[0], data)
|
||||||
|
|
||||||
cart.initialise()
|
cart.initialise()
|
||||||
|
@ -258,20 +270,19 @@ type atari8k struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAtari8k(data []uint8) (cartMapper, error) {
|
func newAtari8k(data []uint8) (cartMapper, error) {
|
||||||
const bankSize = 4096
|
|
||||||
|
|
||||||
cart := &atari8k{}
|
cart := &atari8k{}
|
||||||
|
cart.bankSize = 4096
|
||||||
cart.method = "atari 8k (F8)"
|
cart.method = "atari 8k (F8)"
|
||||||
cart.banks = make([][]uint8, cart.numBanks())
|
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")
|
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := 0; k < cart.numBanks(); k++ {
|
for k := 0; k < cart.numBanks(); k++ {
|
||||||
cart.banks[k] = make([]uint8, bankSize)
|
cart.banks[k] = make([]uint8, cart.bankSize)
|
||||||
offset := k * bankSize
|
offset := k * cart.bankSize
|
||||||
copy(cart.banks[k], data[offset:offset+bankSize])
|
copy(cart.banks[k], data[offset:offset+cart.bankSize])
|
||||||
}
|
}
|
||||||
|
|
||||||
cart.initialise()
|
cart.initialise()
|
||||||
|
@ -325,20 +336,19 @@ type atari16k struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAtari16k(data []byte) (cartMapper, error) {
|
func newAtari16k(data []byte) (cartMapper, error) {
|
||||||
const bankSize = 4096
|
|
||||||
cart := &atari16k{}
|
cart := &atari16k{}
|
||||||
|
cart.bankSize = 4096
|
||||||
cart.method = "atari 16k (F6)"
|
cart.method = "atari 16k (F6)"
|
||||||
cart.banks = make([][]uint8, cart.numBanks())
|
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")
|
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := 0; k < cart.numBanks(); k++ {
|
for k := 0; k < cart.numBanks(); k++ {
|
||||||
cart.banks[k] = make([]uint8, bankSize)
|
cart.banks[k] = make([]uint8, cart.bankSize)
|
||||||
offset := k * bankSize
|
offset := k * cart.bankSize
|
||||||
copy(cart.banks[k], data[offset:offset+bankSize])
|
copy(cart.banks[k], data[offset:offset+cart.bankSize])
|
||||||
}
|
}
|
||||||
|
|
||||||
cart.initialise()
|
cart.initialise()
|
||||||
|
@ -400,20 +410,19 @@ type atari32k struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAtari32k(data []byte) (cartMapper, error) {
|
func newAtari32k(data []byte) (cartMapper, error) {
|
||||||
const bankSize = 4096
|
|
||||||
cart := &atari32k{}
|
cart := &atari32k{}
|
||||||
|
cart.bankSize = 4096
|
||||||
cart.method = "atari 32k (F4)"
|
cart.method = "atari 32k (F4)"
|
||||||
cart.banks = make([][]uint8, cart.numBanks())
|
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")
|
return nil, errors.New(errors.CartridgeError, "not enough bytes in the cartridge file")
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := 0; k < cart.numBanks(); k++ {
|
for k := 0; k < cart.numBanks(); k++ {
|
||||||
cart.banks[k] = make([]uint8, bankSize)
|
cart.banks[k] = make([]uint8, cart.bankSize)
|
||||||
offset := k * bankSize
|
offset := k * cart.bankSize
|
||||||
copy(cart.banks[k], data[offset:offset+bankSize])
|
copy(cart.banks[k], data[offset:offset+cart.bankSize])
|
||||||
}
|
}
|
||||||
|
|
||||||
cart.initialise()
|
cart.initialise()
|
||||||
|
|
|
@ -126,5 +126,13 @@ func (cart cbs) ram() []uint8 {
|
||||||
return cart.superchip
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,5 +60,13 @@ func (cart ejected) ram() []uint8 {
|
||||||
return []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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,3 +274,11 @@ func (cart *mnetwork) ram() []uint8 {
|
||||||
|
|
||||||
func (cart *mnetwork) listen(addr uint16, data 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)
|
||||||
|
}
|
||||||
|
|
|
@ -219,5 +219,13 @@ func (cart parkerBros) ram() []uint8 {
|
||||||
return []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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
// to TIA space for real and not cause a bankswitch. for this reason,
|
||||||
// tigervision cartridges use mirror addresses to write to the TIA.
|
// 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)
|
||||||
|
}
|
||||||
|
|
35
patch/doc.go
35
patch/doc.go
|
@ -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
|
111
patch/patch.go
111
patch/patch.go
|
@ -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
|
||||||
|
}
|
|
@ -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
|
// The ResourcePath() function modifies the supplied resource string such that
|
||||||
// it is prepended with the appropriate gopher2600 config directory. For
|
// it is prepended with the appropriate config directory. For example, the
|
||||||
// example, the following will return the path to the ET patch.
|
// following will return the path to a cartridge patch.
|
||||||
//
|
//
|
||||||
// d := paths.ResourcePath("patches", "ET")
|
// d := paths.ResourcePath("patches", "ET")
|
||||||
//
|
//
|
||||||
|
@ -14,5 +14,5 @@
|
||||||
//
|
//
|
||||||
// In the example above, on a modern Linux system, the path returned will be:
|
// 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
|
package paths
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"gopher2600/errors"
|
"gopher2600/errors"
|
||||||
"gopher2600/gui"
|
"gopher2600/gui"
|
||||||
"gopher2600/hardware"
|
"gopher2600/hardware"
|
||||||
|
"gopher2600/patch"
|
||||||
"gopher2600/recorder"
|
"gopher2600/recorder"
|
||||||
"gopher2600/setup"
|
"gopher2600/setup"
|
||||||
"gopher2600/television"
|
"gopher2600/television"
|
||||||
|
@ -18,7 +19,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Play is a quick of setting up a playable instance of the emulator.
|
// 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
|
var transcript string
|
||||||
|
|
||||||
// if supplied cartridge name is actually a playback file then set
|
// 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 {
|
} else {
|
||||||
// no new recording requested and no transcript given. this is a 'normal'
|
// 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)
|
err = setup.AttachCartridge(vcs, cartload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New(errors.PlayError, err)
|
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
|
// connect gui
|
||||||
|
|
29
setup/doc.go
29
setup/doc.go
|
@ -1,16 +1,25 @@
|
||||||
// Package setup is used to preset the emulation depending on the attached
|
// 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.
|
// Toggling of panel switches
|
||||||
// ie. the setting of the switches on the frontpanel.
|
// Apply patches to cartridge
|
||||||
//
|
//
|
||||||
// Other setup option idea: POKEs. For example, bug fixing the ET cartridge on
|
// Menu driven selection of patches would be a nice feature to have in the
|
||||||
// startup.
|
// 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
|
// Panel Toggles
|
||||||
// other words, a cartridge is loaded and there but there are several setup
|
|
||||||
// options to choose from (eg. bug-fixed or original ROM)
|
|
||||||
//
|
//
|
||||||
// The setup pacakge currently doesn't facilitate editing of the setup
|
// <DB Key>, panel, <SHA-1 Hash>, <player 0 (bool)>, .<player 1 (bool)>, <color (bool)>, <notes>
|
||||||
// database, only reading.
|
//
|
||||||
|
// 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
|
package setup
|
||||||
|
|
84
setup/patch.go
Normal file
84
setup/patch.go
Normal 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
|
||||||
|
}
|
|
@ -26,12 +26,14 @@ type setupEntry interface {
|
||||||
// that will be found in the database
|
// that will be found in the database
|
||||||
func initDBSession(db *database.Session) error {
|
func initDBSession(db *database.Session) error {
|
||||||
|
|
||||||
// add panel setup entry type
|
// add entry types
|
||||||
if err := db.RegisterEntryType(panelSetupID, deserialisePanelSetupEntry); err != nil {
|
if err := db.RegisterEntryType(panelSetupID, deserialisePanelSetupEntry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add other entry types
|
if err := db.RegisterEntryType(patchID, deserialisePatchEntry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue