diff --git a/README.md b/README.md index 705dfae..0b5489a 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ - [x] Encryption (`.bin` -> MAME, MAME -> MAME, `.bin` -> `.bin`) ✅ 2024-09-25 - [x] Splitting (`.bin` -> MAME) ✅ 2024-09-25 - [x] Diff between clean and modified ROMs to produce `.mra` patches automagically ✅ 2024-09-26 +- [x] `.mra` patching ✅ 2024-09-27 ### TODO - [ ] Concatenating (MAME -> `.bin` \[decryption spits out a concatenated `maincpu` region, but that's the only concatenation exposed to the user right now]) - [ ] MAME <-> Darksoft conversion -- [ ] `.mra`/`.ips` patching - [ ] Unshuffling graphics @@ -44,6 +44,10 @@ Navigate to [Releases](https://github.com/MBDesu/mbdcps2/releases) and find the -o Optional flag for specifying output file for operations that output a file + + -p value + -r -n [-d] [-o ] + Patch mode. Value supplied is the path to a .mra file. Patches a ROM with a .mra patch set. Default output file is <./_modified.zip> -r -n Required when using -d, -e, or -x. Specifies a ROM .zip file to open diff --git a/cps2rom/rom.go b/cps2rom/rom.go index 7625002..a826f4d 100644 --- a/cps2rom/rom.go +++ b/cps2rom/rom.go @@ -8,8 +8,70 @@ import ( "strings" "github.com/MBDesu/mbdcps2/Resources" + file_utils "github.com/MBDesu/mbdcps2/utils" ) +func ValidateRomForRegion(romRegion RomRegion, zip *zip.ReadCloser) error { + requiredFiles := make([]string, 0, len(romRegion.Operations)) + if len(romRegion.Operations) > 0 { + for _, operation := range romRegion.Operations { + if operation.Filename != "" { + requiredFiles = append(requiredFiles, operation.Filename) + } + } + } + hasFiles := make(map[string]bool) + for _, filename := range requiredFiles { + hasFiles[filename] = false + } + for _, file := range zip.File { + var name = file.Name + _, ok := hasFiles[name] + if ok { + hasFiles[name] = true + } + } + + numMissingFiles := 0 + missingFiles := make([]string, 0, len(requiredFiles)) + for filename, hasFile := range hasFiles { + if !hasFile { + numMissingFiles = numMissingFiles + 1 + missingFiles = append(missingFiles, filename) + } + } + if numMissingFiles > 0 { + return fmt.Errorf("missing %d files: %s", numMissingFiles, Resources.LogText.Bold(strings.Join(missingFiles, ", "))) + } + return nil +} + +func SplitRegionToFiles(romRegion RomRegion, binary []byte, zipPath string) error { + f, err := file_utils.CreateFile(zipPath) + if err != nil { + return err + } + w := zip.NewWriter(f) + for _, operation := range romRegion.Operations { + Resources.Logger.Info(fmt.Sprintf("Writing %s from 0x%06x to 0x%06x...", operation.Filename, operation.Offset, operation.Offset+operation.Length)) + regionBytes := binary[operation.Offset : operation.Offset+operation.Length] + fr, err := w.Create(operation.Filename) + if err != nil { + return err + } + _, err = fr.Write(regionBytes) + if err != nil { + return err + } + } + err = w.Close() + if err != nil { + return err + } + err = f.Close() + return err +} + func ValidateRomZip(romDefinition RomDefinition, zip *zip.ReadCloser) error { var numFiles = len(romDefinition.Maincpu.Operations) + len(romDefinition.Audiocpu.Operations) + len(romDefinition.Gfx.Operations) + len(romDefinition.Qsound.Operations) + len(romDefinition.Key.Operations) regions := []RomRegion{romDefinition.Audiocpu, romDefinition.Gfx, romDefinition.Maincpu, romDefinition.Qsound, romDefinition.Key} diff --git a/cps2rom/rompatcher.go b/cps2rom/rompatcher.go index 1d6dd49..824ba7a 100644 --- a/cps2rom/rompatcher.go +++ b/cps2rom/rompatcher.go @@ -1,11 +1,17 @@ package cps2rom import ( + "archive/zip" + "encoding/xml" "errors" "fmt" + "io" "os" + "strconv" + "strings" "github.com/MBDesu/mbdcps2/Resources" + file_utils "github.com/MBDesu/mbdcps2/utils" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" ) @@ -16,6 +22,71 @@ type RomPatch struct { Data []uint8 } +type MraXml struct { + XMLName xml.Name `xml:"misterromdescription"` + Text string `xml:",chardata"` + About struct { + Text string `xml:",chardata"` + Author string `xml:"author,attr"` + Webpage string `xml:"webpage,attr"` + Source string `xml:"source,attr"` + Twitter string `xml:"twitter,attr"` + } `xml:"about"` + Name string `xml:"name"` + Setname string `xml:"setname"` + Rbf string `xml:"rbf"` + Mameversion string `xml:"mameversion"` + Year string `xml:"year"` + Manufacturer string `xml:"manufacturer"` + Players string `xml:"players"` + Joystick string `xml:"joystick"` + Rotation string `xml:"rotation"` + Region string `xml:"region"` + Platform string `xml:"platform"` + Category string `xml:"category"` + Catver string `xml:"catver"` + Mraauthor string `xml:"mraauthor"` + Rom []struct { + Text string `xml:",chardata"` + Index string `xml:"index,attr"` + Zip string `xml:"zip,attr"` + Type string `xml:"type,attr"` + Md5 string `xml:"md5,attr"` + Address string `xml:"address,attr"` + Part []struct { + Text string `xml:",chardata"` + Name string `xml:"name,attr"` + Crc string `xml:"crc,attr"` + Length string `xml:"length,attr"` + } `xml:"part"` + Patch []struct { + Data string `xml:",chardata"` + Offset string `xml:"offset,attr"` + } `xml:"patch"` + Interleave []struct { + Text string `xml:",chardata"` + Output string `xml:"output,attr"` + Part []struct { + Text string `xml:",chardata"` + Name string `xml:"name,attr"` + Crc string `xml:"crc,attr"` + Map string `xml:"map,attr"` + } `xml:"part"` + } `xml:"interleave"` + } `xml:"rom"` + Nvram struct { + Text string `xml:",chardata"` + Index string `xml:"index,attr"` + Size string `xml:"size,attr"` + } `xml:"nvram"` + Buttons struct { + Text string `xml:",chardata"` + Names string `xml:"names,attr"` + Default string `xml:"default,attr"` + Count string `xml:"count,attr"` + } `xml:"buttons"` +} + func createUint8ArrayFromUint16Array(arr []uint16) []uint8 { newArr := make([]uint8, len(arr)*2) for i := 0; i < len(arr); i++ { @@ -28,6 +99,85 @@ func createUint8ArrayFromUint16Array(arr []uint16) []uint8 { return newArr } +func parseMra(mraFile *os.File) (*MraXml, error) { + mraBytes, err := io.ReadAll(mraFile) + if err != nil { + return nil, err + } + var mraXml MraXml + err = xml.Unmarshal(mraBytes, &mraXml) + if err != nil { + return nil, err + } + defer mraFile.Close() + return &mraXml, err +} + +func mapOffsetToFile(offset int64, romRegion RomRegion) (string, int) { + for _, operation := range romRegion.Operations { + actualOffset := offset - 0x40 + if actualOffset >= int64(operation.Offset) && offset < int64(operation.Offset+operation.Length) { + return operation.Filename, int(actualOffset - int64(operation.Offset)) + } + } + return "", -1 +} + +func PatchRomRegionWithMra(romZip *zip.ReadCloser, mraFile *os.File, romRegion RomRegion, outputFilepath string) error { + Resources.Logger.Warn("Patching ROM...") + fileContentMap, err := file_utils.UnzipFilesToFilenameContentMap(romZip) + if err != nil { + return err + } + mra, err := parseMra(mraFile) + if err != nil { + return err + } + for _, rom := range mra.Rom { + lastOperationFilename := "" + for _, patch := range rom.Patch { + offset, err := strconv.ParseInt(patch.Offset, 0, 32) + if err != nil { + return err + } + data := make([]uint8, 0, len(patch.Data)*3) + + for _, byteString := range strings.Split(patch.Data, " ") { + byte16, err := strconv.ParseInt(byteString, 16, 16) + if err != nil { + return err + } + byte8 := uint8(byte16 & 0xff) + data = append(data, byte8) + } + operationFilename, absoluteOffset := mapOffsetToFile(offset, romRegion) + if operationFilename != lastOperationFilename { + Resources.Logger.Info(fmt.Sprintf("Patching %s", operationFilename)) + lastOperationFilename = operationFilename + } + for i, dataByte := range data { + fileContentMap[operationFilename][absoluteOffset+i] = dataByte + } + } + } + Resources.Logger.Done("Done patching ROM!") + Resources.Logger.Warn("Writing files to .zip...") + f, err := file_utils.CreateFile(outputFilepath) + if err != nil { + return err + } + w := *zip.NewWriter(f) + for file, content := range fileContentMap { + x, err := w.Create(file) + if err != nil { + return err + } + x.Write(content) + } + w.Close() + return err +} + func createUint16ArrayFromUint8Array(arr []uint8) []uint16 { length := len(arr) newArr := make([]uint16, length/2) diff --git a/go.mod b/go.mod index 1235489..56b4b11 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,12 @@ module github.com/MBDesu/mbdcps2 go 1.23.1 -require github.com/fatih/color v1.17.0 +require ( + github.com/fatih/color v1.17.0 + github.com/jedib0t/go-pretty/v6 v6.5.9 +) require ( - github.com/jedib0t/go-pretty/v6 v6.5.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect diff --git a/go.sum b/go.sum index 8a2c725..2fb3df9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= @@ -9,9 +11,15 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mbdcps2.go b/mbdcps2.go index ab8f29c..2861494 100644 --- a/mbdcps2.go +++ b/mbdcps2.go @@ -22,6 +22,7 @@ type Flags struct { isDecryptMode bool isDiffMode bool isEncryptMode bool + isPatchMode bool isSplitMode bool outputFile string } @@ -31,6 +32,7 @@ var diffMode string var flags Flags var modifiedRomBin *os.File var modifiedRomZip *zip.ReadCloser +var mraFile *os.File var romDef cps2rom.RomDefinition var romZip *zip.ReadCloser var romName string = "" @@ -47,6 +49,7 @@ func parseFlags() { splitModePtr := flag.Bool("s", false, Resources.Strings.Flag["splitModeDesc"]) // concatModePtr := flag.Bool("c", false, Resources.Strings.Flag["concatModeDesc"]) var isDiffMode bool = false + var isPatchMode bool = false flag.Func("b", Resources.Strings.Flag["binFileDesc"], func(binFilepath string) error { if !(*splitModePtr /*|| *concatModePtr*/) { return nil @@ -81,6 +84,15 @@ func parseFlags() { isDiffMode = true return nil }) + flag.Func("p", Resources.Strings.Flag["patchModeDesc"], func(mraFilepath string) error { + f, err := os.Open(mraFilepath) + if err != nil { + return err + } + isPatchMode = true + mraFile = f + return err + }) flag.Func("n", Resources.Strings.Flag["romSetNameDesc"], func(romname string) error { if romname == "" { flag.Usage() @@ -112,20 +124,20 @@ func parseFlags() { }) flag.Parse() - flags = Flags{ /**concatModePtr,*/ *decryptModePtr, isDiffMode, *encryptModePtr, *splitModePtr, *outputFilePtr} + flags = Flags{ /**concatModePtr,*/ *decryptModePtr, isDiffMode, *encryptModePtr, isPatchMode, *splitModePtr, *outputFilePtr} if *decryptModePtr && *encryptModePtr { throw(Resources.Strings.Error["bothEncrypts"]) } } -func checkErr(err error) { +func check(err error) { if err != nil { throw(err.Error()) } } // TODO: refactor flag spaghetti -func handleEncryptionOperation(decryptMode cps2crypt.Direction) { +func handleEncryptionOperation(romZipfile *zip.ReadCloser, decryptMode cps2crypt.Direction) { if flags.outputFile == "" { if flags.isSplitMode { flags.outputFile = romName + ".zip" @@ -133,14 +145,16 @@ func handleEncryptionOperation(decryptMode cps2crypt.Direction) { flags.outputFile = romName + ".bin" } } - executableRegionBinary, err := cps2rom.ProcessRegionFromZip(romZip, romDef.Maincpu) - checkErr(err) - res, err := cps2crypt.Crypt(decryptMode, romDef, romZip, executableRegionBinary) - checkErr(err) + executableRegionBinary, err := cps2rom.ProcessRegionFromZip(romZipfile, romDef.Maincpu) + check(err) + res, err := cps2crypt.Crypt(decryptMode, romDef, romZipfile, executableRegionBinary) + check(err) if flags.isSplitMode { - file_utils.SplitRegionToFiles(romDef.Maincpu, res, flags.outputFile) + err = cps2rom.SplitRegionToFiles(romDef.Maincpu, res, flags.outputFile) + check(err) } else { - file_utils.WriteBytesToFile(flags.outputFile, res) + err = file_utils.WriteBytesToFile(flags.outputFile, res) + check(err) } operation := "Encrypted" if decryptMode { @@ -161,35 +175,49 @@ func handleDiffOperation() { var rBytes []uint8 // TODO: make it so you can diff more than just maincpu for patchering lBytes, err := cps2rom.ProcessRegionFromZip(romZip, romDef.Maincpu) - checkErr(err) + check(err) if diffMode == "zip" { rBytes, err = cps2rom.ProcessRegionFromZip(modifiedRomZip, romDef.Maincpu) - checkErr(err) + check(err) } else { rBytes, err = io.ReadAll(modifiedRomBin) - checkErr(err) + check(err) } patches, err := cps2rom.DiffTwoBins(romName, lBytes, rBytes, romDef.Maincpu, false) - checkErr(err) + check(err) patchStrings := cps2rom.GenerateMraPatches(patches) patchFile, err := file_utils.CreateFile(flags.outputFile) - checkErr(err) + check(err) _, err = patchFile.WriteString(Resources.Strings.Info["mraHeader"]) - checkErr(err) + check(err) for _, patch := range patchStrings { _, err = patchFile.WriteString(patch) - checkErr(err) + check(err) } defer patchFile.Close() } +func handlePatchOperation() { + if flags.outputFile == "" { + flags.outputFile = romName + "_modified.zip" + } + err := cps2rom.ValidateRomForRegion(romDef.Maincpu, romZip) + check(err) + err = cps2rom.PatchRomRegionWithMra(romZip, mraFile, romDef.Maincpu, flags.outputFile) + check(err) + if flags.isDecryptMode { + // TODO: change to patched ROM when patch done + handleEncryptionOperation(romZip, cps2crypt.Direction(flags.isEncryptMode)) + } +} + func main() { parseFlags() if romName == "" { flag.Usage() throw(Resources.Strings.Error["noRomName"]) } - if !hasRomFile && (flags.isDecryptMode || flags.isEncryptMode || flags.isDiffMode) { + if !hasRomFile && (flags.isDecryptMode || flags.isEncryptMode || flags.isDiffMode || flags.isPatchMode) { flag.Usage() throw(Resources.Strings.Error["noRomFile"]) } else if !hasRomFile && flags.isSplitMode { @@ -201,15 +229,20 @@ func main() { if err != nil { throw(err.Error()) } - handleEncryptionOperation(cps2crypt.Direction(flags.isDecryptMode)) + handleEncryptionOperation(romZip, cps2crypt.Direction(flags.isDecryptMode)) } else if flags.isSplitMode { - err := file_utils.SplitRegionToFiles(romDef.Maincpu, binFile, flags.outputFile) - checkErr(err) + err := cps2rom.SplitRegionToFiles(romDef.Maincpu, binFile, flags.outputFile) + check(err) Resources.Logger.Done(fmt.Sprintf("%s maincpu files written to %s", romName, flags.outputFile)) } else if flags.isDiffMode { handleDiffOperation() Resources.Logger.Done(fmt.Sprintf("%s .mra patches written to %s", romName, flags.outputFile)) + } else if flags.isPatchMode { + handlePatchOperation() + Resources.Logger.Done(fmt.Sprintf("patched %s written to %s", romName, flags.outputFile)) } defer romZip.Close() + defer mraFile.Close() + defer modifiedRomBin.Close() os.Exit(0) } diff --git a/resources/resources.go b/resources/resources.go index 75143af..a92e4c3 100644 --- a/resources/resources.go +++ b/resources/resources.go @@ -35,6 +35,7 @@ var flagStrings = map[string]string{ "decryptModeDesc": "-r -n [-s] [-o ]\nDecrypt mode. Decrypts and concatenates the executable regions of ROM into a single .bin file, unless the -s flag is set. Default output file is ./.bin, unless the -s flag is set, in which case it will be ./.zip\n", "encryptModeDesc": "-r -n [-o ]\nEncrypt mode. Encrypts and splits the executable regions of ROM back into their MAME format ROM files\n", "outputFileDesc": "Optional flag for specifying output file for operations that output a file\n", + "patchModeDesc": "-r -n [-d] [-o ]\nPatch mode. Value supplied is the path to a .mra file. Patches a ROM with a .mra patch set. Default output file is <./_modified.zip>\n", "romZipDesc": "-n \nRequired when using -d, -e, or -x. Specifies a ROM .zip file to open\n", "romSetNameDesc": "Required. Specifies the ROM set (usually the ZIP name)\n", "splitModeDesc": "-n [-b & ![-d | -e]] [-d | -e] [-o ]\nSplit mode. Splits a concatenated binary back into its original MAME files. This flag is usable with -d or -e, but not if -b is set\n", @@ -43,6 +44,7 @@ var flagStrings = map[string]string{ var errorStrings = map[string]string{ "diffSize": "binaries differ in size", "noBinFile": ".bin file is required for this operation", + "noMraFile": ".mra file is required for this operation", "noRomFile": "ROM file is required for this operation", "noRomName": "ROM set name is required", "romParseErr": "Something went wrong parsing the ROMs", diff --git a/utils/file-utils.go b/utils/file-utils.go index e634b2e..1ca0e09 100644 --- a/utils/file-utils.go +++ b/utils/file-utils.go @@ -2,12 +2,9 @@ package file_utils import ( "archive/zip" - "fmt" + "io" "os" "path/filepath" - - "github.com/MBDesu/mbdcps2/Resources" - "github.com/MBDesu/mbdcps2/cps2rom" ) func CreateFile(file_path string) (*os.File, error) { @@ -37,28 +34,19 @@ func WriteBytesToFile(file_path string, bytes []byte) error { return err } -func SplitRegionToFiles(romRegion cps2rom.RomRegion, binary []byte, zipPath string) error { - f, err := CreateFile(zipPath) - if err != nil { - return err - } - w := zip.NewWriter(f) - for _, operation := range romRegion.Operations { - Resources.Logger.Info(fmt.Sprintf("Writing %s from 0x%06x to 0x%06x...", operation.Filename, operation.Offset, operation.Offset+operation.Length)) - regionBytes := binary[operation.Offset : operation.Offset+operation.Length] - fr, err := w.Create(operation.Filename) +func UnzipFilesToFilenameContentMap(zipFile *zip.ReadCloser) (map[string][]byte, error) { + var bytes = make(map[string][]byte, len(zipFile.File)) + for _, file := range zipFile.File { + r, err := file.Open() if err != nil { - return err + return nil, err } - _, err = fr.Write(regionBytes) + fileContents, err := io.ReadAll(r) if err != nil { - return err + return nil, err } + bytes[file.Name] = fileContents + defer r.Close() } - err = w.Close() - if err != nil { - return err - } - err = f.Close() - return err + return bytes, nil }