// 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 . 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 . 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 }