mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 05:40:49 -04:00
84ad23c03e
this means that a mapper only needs to implement the Patch() if it makes sense mappers that don't need it have had the Patch function removed. implemented function for SCABS and UA corrected error messages for atari mappers - some messages weren't referencing the correct atari mapper and simply stated "atari"
203 lines
5 KiB
Go
203 lines
5 KiB
Go
// 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 patch
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge"
|
|
"github.com/jetsetilly/gopher2600/resources"
|
|
)
|
|
|
|
const patchPath = "patches"
|
|
|
|
const neoComment = '-'
|
|
const neoSeparator = ":"
|
|
|
|
// 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(cart *cartridge.Cartridge, patchFile string) (bool, error) {
|
|
var err error
|
|
|
|
p, err := resources.JoinPath(patchPath, patchFile)
|
|
if err != nil {
|
|
return false, fmt.Errorf("patch: %w", err)
|
|
}
|
|
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case *os.PathError:
|
|
return false, fmt.Errorf("patch: patch file not found (%s)", p)
|
|
}
|
|
return false, fmt.Errorf("patch: %w", 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, fmt.Errorf("patch: %w", err)
|
|
}
|
|
|
|
// read file
|
|
buffer, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return false, fmt.Errorf("patch: %w", err)
|
|
}
|
|
|
|
if len(buffer) <= 1 {
|
|
return false, nil
|
|
}
|
|
|
|
// if first character is a hyphen then we'll assume this is a "neo" style
|
|
// patch file
|
|
if buffer[0] == neoComment {
|
|
err = neoStyle(cart, buffer)
|
|
if err != nil {
|
|
return false, fmt.Errorf("patch: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// otherwise assume it is a "cmp" style patch file
|
|
err = cmpStyle(cart, buffer)
|
|
if err != nil {
|
|
return false, fmt.Errorf("patch: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// cmp -l <old_file> <new_file>.
|
|
func cmpStyle(cart *cartridge.Cartridge, buffer []byte) error {
|
|
// walk through lines
|
|
lines := strings.Split(string(buffer), "\n")
|
|
for i, s := range lines {
|
|
// ignore empty lines. cmp shouldn't output empty lines but the just in
|
|
// case. pluse the last line will probably be empty
|
|
if len(s) == 0 {
|
|
continue
|
|
}
|
|
|
|
// split line into fields
|
|
p := strings.Fields(s)
|
|
|
|
// if there are not three fields then the file is malformed
|
|
if len(p) != 3 {
|
|
return fmt.Errorf("cmp: line [%d]: malformed", i)
|
|
}
|
|
|
|
// ofset is stored as decimal
|
|
offset, err := strconv.ParseUint(p[0], 10, 16)
|
|
if err != nil {
|
|
return fmt.Errorf("cmp: line [%d]: %w", i, err)
|
|
}
|
|
|
|
// cmp counts from 1 but we count everything from zero
|
|
offset--
|
|
|
|
// old and patch bytes are stored as octal(!)
|
|
old, err := strconv.ParseUint(p[1], 8, 8)
|
|
if err != nil {
|
|
return fmt.Errorf("cmp: line [%d]: %w", i, err)
|
|
}
|
|
patch, err := strconv.ParseUint(p[2], 8, 8)
|
|
if err != nil {
|
|
return fmt.Errorf("cmp: line [%d]: %w", i, err)
|
|
}
|
|
|
|
// check that the patch is correct
|
|
o, _ := cart.Peek(uint16(offset))
|
|
if o != uint8(old) {
|
|
return fmt.Errorf("cmp: line %d: byte at offset %04x does not match expected byte (%02x instead of %02x)", i, offset, o, old)
|
|
}
|
|
|
|
// patch memory
|
|
err = cart.Patch(int(offset), uint8(patch))
|
|
if err != nil {
|
|
return fmt.Errorf("cmp: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func neoStyle(cart *cartridge.Cartridge, buffer []byte) error {
|
|
// walk through lines
|
|
lines := strings.Split(string(buffer), "\n")
|
|
for i, s := range lines {
|
|
// ignore empty lines
|
|
if len(s) == 0 {
|
|
continue // for loop
|
|
}
|
|
|
|
// ignoring comment lines and lines starting with whitespace
|
|
if s[0] == neoComment || unicode.IsSpace(rune(s[0])) {
|
|
continue // for loop
|
|
}
|
|
|
|
pokeLine := strings.Split(s, neoSeparator)
|
|
|
|
// ignore any lines that don't match the required [offset: 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 offset
|
|
offset, 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 = cart.Patch(int(offset), uint8(v))
|
|
if err != nil {
|
|
return fmt.Errorf("neo: line %d: %w", i, err)
|
|
}
|
|
|
|
// advance offset
|
|
offset++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|