diff --git a/Makefile b/Makefile index 23709fb..7dc9714 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ all: build-windows - +# Not powershell compatible. Meant to be used from unix shell build-windows: - env GOOS=windows GOARCH=amd64 go build -o ./build/universal-checksum-patcher-windows.exe *.go \ No newline at end of file + env GOOS=windows GOARCH=amd64 go build -o ./build/universal-checksum-patcher.exe *.go \ No newline at end of file diff --git a/README.md b/README.md index 9d91a5c..1176f54 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # Universal Paradox games checksum patcher -This is a lightweight patch, that forces game ignore checksum when starting and loading ironman game. +This is a patch, that forces game ignore checksum when starting and loading ironman game. In other words, yes, it gives you the ability to get achievements with mods enabled. And not giving you ability to use game console or connect to servers with other checksum. # Installation -1. Download latest binary of patcher from releases (or build it in case you know what you doing) -2. Unzip it in game directory (rightclick on game on steam > Manage > Browse local files) -3. Run paradox-checksum-patcher.exe or paradox-checksum-patcher +1. Download latest binary of patcher from releases (or build it if you know what you doing) +2. Unzip it in game directory (right click on game on steam > Manage > Browse local files) +3. Run universal-checksum-patcher.exe -Conrats, you done! In case you see unsupported version error most likely Paradox broke something and all you can do is wait until i update patch. Most likely i'll update patch when i decide to play, so feel free to get needed byte code and modify source code (i'll merge your pull request if you decide to do that. Or create fork, i don't care) +Congrats, you're done! In case you see unsupported version error most likely Paradox broke something and all you can do is wait until I update patch. Most likely i'll update patch when I decide to play, so feel free to get needed byte code and modify source code (I'll merge your pull request if you decide to do that. Or create fork, I don't care) # Supported games and platforms -| | Windows | Linux | MacOS | -|-----------------------|------------------------|------------------------|--------| -| Europa Universalis IV | Yes :heavy_check_mark: | No :x: | No :x: | -| Hearts of Iron IV | Yes :heavy_check_mark: | No :x: | No :x: | +| | Windows | Linux(native) | MacOS | +|-----------------------|------------------------|---------------|--------| +| Europa Universalis IV | Yes :heavy_check_mark: | No :x: | No :x: | +| Hearts of Iron IV | Yes :heavy_check_mark: | No :x: | No :x: | \ No newline at end of file diff --git a/error.go b/error.go new file mode 100644 index 0000000..e14bc2f --- /dev/null +++ b/error.go @@ -0,0 +1,8 @@ +package main + +import "errors" + +var ( + ErrNoMatch = errors.New("cannot detect bytes pattern to patch. Most likely patcher are outdated due to game updates") + ErrCantLocate = errors.New("cannot locate file in current directory") +) diff --git a/go.mod b/go.mod index 21b9fd7..6d23ebd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/goshathebusiness/eu4-checksum-patcher +module github.com/goshathebusiness/universal-checksum-patcher go 1.19 diff --git a/hex.go b/hex.go new file mode 100644 index 0000000..da1edf7 --- /dev/null +++ b/hex.go @@ -0,0 +1,16 @@ +package main + +const ( + limit = 14 + startLength = 3 + endLength = 6 +) + +var ( + start1 = []byte{0x48, 0x8B, 0x12} + start2 = []byte{0x48, 0x8D, 0x0D} + start3 = []byte{0x48, 0x8B, 0xD0} + + end = []byte{0x85, 0xC0, 0x0F, 0x94, 0xC3, 0xE8} + replacement = []byte{0x31, 0xC0, 0x0F, 0x94, 0xC3, 0xE8} +) diff --git a/main.go b/main.go index c9e83eb..eb33ba3 100644 --- a/main.go +++ b/main.go @@ -2,16 +2,17 @@ package main import ( "fmt" - "runtime" "github.com/manifoldco/promptui" ) -func main() { - - OS := runtime.GOOS +const ( + eu4 = "eu4.exe" + hoi4 = "hoi4.exe" +) - promptGame := promptui.Select{ +func main() { + prompt := promptui.Select{ Label: "Select game to patch", Items: []string{ "Europa Universalis IV", @@ -20,7 +21,7 @@ func main() { HideHelp: true, } - _, result, err := promptGame.Run() + _, result, err := prompt.Run() if err != nil { fmt.Printf("Prompt failed %v\n", err) @@ -29,18 +30,18 @@ func main() { switch result { case "Europa Universalis IV": - err = applyPatch("eu4", OS) + err = applyPatch(eu4) case "Hearts of Iron IV": - err = applyPatch("hoi4", OS) + err = applyPatch(hoi4) } if err != nil { fmt.Println("ERROR:", err) - fmt.Println("Patch not installed, no file has been changed") + fmt.Println("Wasn't not installed, file hasn't changed") } else { - fmt.Println("Patch successfully installed, your original executable has been backuped in [original name]_backup") + fmt.Println("Patch successfully installed, your original executable has been backuped in [original name].backup") } fmt.Println("Press enter to exit") - fmt.Scanln() + _, _ = fmt.Scanln() } diff --git a/patch.go b/patch.go index 5102b99..eb19fe1 100644 --- a/patch.go +++ b/patch.go @@ -2,170 +2,82 @@ package main import ( "errors" - "fmt" "os" - "reflect" - "strconv" - "strings" ) -func printBytesArrInHex(arr []byte) { - for _, b := range arr { - fmt.Printf("%X", b) +func applyPatch(filename string) error { + _, err := os.Stat(filename) + if errors.Is(err, os.ErrNotExist) { + return ErrCantLocate } - fmt.Println() -} -func normalizeHex(arr []string) []string { - for i, h := range arr { - arr[i] = strings.TrimLeft(h, "0") + bytes, err := os.ReadFile(filename) + if err != nil { + return err } - return arr -} - -func backupFile(originalFileName, backupFileName string) error { - backupData, err := os.ReadFile(originalFileName) + err = modifyBytes(bytes) if err != nil { return err } - backupOutFile, err := os.Create(backupFileName) + + err = backupFile(filename) if err != nil { return err } - backupOutFile.Write(backupData) - backupOutFile.Close() - return nil -} + out, err := os.Create(filename) + if err != nil { + return err + } -func compareExes(exeAName, exeBName string) bool { - byteA, err := os.ReadFile(exeAName) + _, err = out.Write(bytes) if err != nil { - return false + return err } - byteB, err := os.ReadFile(exeBName) + err = out.Close() if err != nil { - return false + return err } - return reflect.DeepEqual(byteA, byteB) + return nil } -var ( - hexExistsEU4Windows = []string{"48", "8D", "0D", "??", "??", "??", "??", "E8", "??", "??", "??", "??", "85", "C0", "0F", "94", "C3", "E8"} - hexWantedEU4Windows = []string{"48", "8D", "0D", "??", "??", "??", "??", "E8", "??", "??", "??", "??", "31", "C0", "0F", "94", "C3", "E8"} - - hexExistsHOI4Windows = []string{"48", "??", "??", "??", "??", "??", "??", "E8", "??", "??", "??", "??", "85", "C0", "0F", "94", "C3", "E8"} - hexWantedHOI4Windows = []string{"48", "??", "??", "??", "??", "??", "??", "E8", "??", "??", "??", "??", "31", "C0", "0F", "94", "C3", "E8"} -) - -func applyPatch(originalFileName, OS string) error { - - var hexExists, hexWanted []string - var fileExtension string - - if strings.Contains(originalFileName, "eu4") { - hexExists = hexExistsEU4Windows - hexWanted = hexWantedEU4Windows - - } else if strings.Contains(originalFileName, "hoi4") { - - hexExists = hexExistsHOI4Windows - hexWanted = hexWantedHOI4Windows - - } else { - return fmt.Errorf("not supported executable") - } +func isStartCandidate(bytes []byte) bool { + return isByteSlicesEqual(bytes, start1) || isByteSlicesEqual(bytes, start2) || isByteSlicesEqual(bytes, start3) +} - switch OS { - case "windows": - fileExtension = ".exe" - default: - return fmt.Errorf("this OS (%s) is not supported", OS) +func isEndCandidate(bytes []byte) bool { + return isByteSlicesEqual(bytes, end) +} - } +func modifyBytes(bytes []byte) error { + atLeastOnePatched := false - byteExists := make([]byte, len(hexExists)) - byteWanted := make([]byte, len(hexWanted)) - for i := range hexExists { - var value int64 - if hexExists[i] == "??" { - value = 0 - } else { - value, _ = strconv.ParseInt(hexExists[i], 16, 16) + for i := 0; i < len(bytes); i++ { + if i > len(bytes)-limit { + break } - byteExists[i] = byte(value) - } - for i := range hexWanted { - var value int64 - if hexWanted[i] == "??" { - value = 0 - } else { - value, _ = strconv.ParseInt(hexWanted[i], 16, 16) - } - byteWanted[i] = byte(value) - } - if _, err := os.Stat(originalFileName + fileExtension); errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("cannot locate %s in current folder", originalFileName+fileExtension) - } - - go func() { - backupFile(originalFileName+fileExtension, originalFileName+"_backup"+fileExtension) - }() - - originalByte, err := os.ReadFile(originalFileName + fileExtension) - if err != nil { - return err - } + if !isStartCandidate(bytes[i : i+startLength]) { + continue + } - finalByte := originalByte - - matchesNeeded := len(byteExists) - matches := 0 - status := false - - for i := 0; i < len(finalByte); i++ { - if finalByte[i] == byteExists[0] { - matches++ - for j := range byteExists { - if (finalByte[i+j] == byteExists[j]) || (byteExists[j] == 0) { - matches++ - } else { - matches = 0 - break - } + for j := i + startLength; j < i+startLength+limit && j < len(bytes)-endLength; j++ { + if !isEndCandidate(bytes[j : j+endLength]) { + continue } - if matches >= matchesNeeded { - for k := range byteExists { - if byteExists[k] != 0 { - finalByte[i+k] = byteWanted[k] - } - } - status = true - } - } - } - if !status { - os.Remove(originalFileName + "_backup" + fileExtension) - return fmt.Errorf("unsupported version of %s or it's patched already. Patch has not been applied", originalFileName+fileExtension) - } + for k := 0; k < len(replacement); k++ { + bytes[j+k] = replacement[k] + } - out, err := os.Create(originalFileName + fileExtension) - if err != nil { - return err + atLeastOnePatched = true + } } - _, err = out.Write(finalByte) - if err != nil { - return err + if atLeastOnePatched { + return nil } - err = out.Close() - if err != nil { - return err - } - - return nil + return ErrNoMatch } diff --git a/test_eu4_patch_test.go b/test_eu4_patch_test.go deleted file mode 100644 index 024c112..0000000 --- a/test_eu4_patch_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "os" - "testing" -) - -func TestApplyPatchEU4Windows(t *testing.T) { - err := backupFile("./test_files/eu4/eu4_windows_original.exe", "./test_files/eu4/eu4_windows_test.exe") - - if err != nil { - t.Error(err) - } - err = applyPatch("./test_files/eu4/eu4_windows_test", "windows") - - if err != nil { - t.Error(err) - } - - if !compareExes("./test_files/eu4/eu4_windows_patched.exe", "./test_files/eu4/eu4_windows_test.exe") { - t.Error("Executables doesnt match") - } - - err = os.Remove("./test_files/eu4/eu4_windows_test.exe") - if err != nil { - t.Error("Cannot delete temp files") - } - err = os.Remove("./test_files/eu4/eu4_windows_test_backup.exe") - if err != nil { - t.Error("Cannot delete temp files") - } - err = os.Remove("./test_files/eu4/eu4_windows.exe") - if err != nil { - t.Error("Cannot delete temp files") - } -} diff --git a/test_hoi4_patch_test.go b/test_hoi4_patch_test.go deleted file mode 100644 index 66695d8..0000000 --- a/test_hoi4_patch_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "os" - "testing" -) - -func TestApplyPatchHOI4Windows(t *testing.T) { - err := backupFile("./test_files/hoi4/hoi4_original.exe", "./test_files/hoi4/hoi4_test.exe") - - if err != nil { - t.Error(err) - } - err = applyPatch("./test_files/hoi4/hoi4_test", "windows") - - if err != nil { - t.Error(err) - } - - if !compareExes("./test_files/hoi4/hoi4_patched.exe", "./test_files/hoi4/hoi4_test.exe") { - t.Error("Executables doesnt match") - } - - err = os.Remove("./test_files/hoi4/hoi4_test.exe") - if err != nil { - t.Error("Cannot delete temp files") - } - err = os.Remove("./test_files/hoi4/hoi4_test_backup.exe") - if err != nil { - t.Error("Cannot delete temp files") - } - err = os.Remove("./test_files/hoi4/hoi4.exe") - if err != nil { - t.Error("Cannot delete temp files") - } -} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..a6937cc --- /dev/null +++ b/utils.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "os" +) + +func backupFile(original string) error { + data, err := os.ReadFile(original) + if err != nil { + return err + } + backup, err := os.Create(fmt.Sprintf("%s.backup", original)) + if err != nil { + return err + } + _, err = backup.Write(data) + if err != nil { + return err + } + err = backup.Close() + if err != nil { + return err + } + + return nil +} + +func isByteSlicesEqual(first, second []byte) bool { + if len(first) != len(second) { + return false + } + + for i := 0; i < len(first); i++ { + if first[i] != second[i] { + return false + } + } + + return true +}