// From: https://github.com/Ninka-Rex/CSharp-Search-and-Replace/blob/main/SearchReplace.cs using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace ServerSelector; public static class PatchUtility { public static bool CanFindOffset(string filePath, string[] searchPatterns) { // Check if the file exists if (!File.Exists(filePath)) { Console.WriteLine("[ERROR] File not found: " + filePath); return false; } // Read the binary data from the file as an array of bytes byte[] fileData = File.ReadAllBytes(filePath); for (int k = 0; k < searchPatterns.Length; k++) { // Convert the hexadecimal strings to byte arrays using a helper method byte[] searchBytes = HexStringToBytes(searchPatterns[k]); // Find the index of the first occurrence of the search pattern in the file data using another helper method List results = FindPatternIndex(fileData, searchBytes); if (results.Count <= 1) return false; Console.WriteLine("offset: " + results[1].ToString("X")); // If the index is -1, it means the pattern was not found, so we return false and log an error message if (results[1] == -1) { Console.WriteLine("[ERROR] Search pattern not found: " + searchPatterns[k]); return false; } } return true; } // This method searches and replaces binary patterns in a given file // It takes three parameters: // - filePath: the path of the file to be patched // - searchPatterns: an array of hexadecimal strings representing the patterns to be searched for // - replacePatterns: an array of hexadecimal strings representing the patterns to be replaced with // It returns true if the patching was successful, or false if there was an error or a pattern was not found public static bool SearchAndReplace(string filePath, string[] searchPatterns, string[] replacePatterns) { try { // Check if the file exists if (!File.Exists(filePath)) { Console.WriteLine("[ERROR] File not found: " + filePath); return false; } // Read the binary data from the file as an array of bytes byte[] fileData = File.ReadAllBytes(filePath); // Backup the original file by copying it to a new file with a .bak extension string backupFilePath = filePath + ".bak"; if (!File.Exists(backupFilePath)) { File.Copy(filePath, backupFilePath); } // Loop through each pair of search and replace patterns and apply them to the file data for (int k = 0; k < searchPatterns.Length; k++) { // Convert the hexadecimal strings to byte arrays using a helper method byte[] searchBytes = HexStringToBytes(searchPatterns[k]); byte[] replaceBytes = HexStringToBytes(replacePatterns[k]); // Find the index of the first occurrence of the search pattern in the file data using another helper method // Note: game versions 124 - 133 have 2 matches, 2nd one is the correct one // game version 134 - ? has 1 match which is correct int index = -1; List indexes = FindPatternIndex(fileData, searchBytes); if (indexes.Count == 1) index = indexes[0]; else index = indexes[1]; Console.WriteLine("Found offset: " + index.ToString("X")); // If the index is -1, it means the pattern was not found, so we return false and log an error message if (index == -1) { Console.WriteLine("[ERROR] Search pattern not found: " + searchPatterns[k]); return false; } // Replace the pattern at the found index with the replace pattern, preserving original values when wildcards are encountered // A wildcard is represented by either 00 or FF in the replace pattern, meaning that we keep the original value at that position for (int i = 0; i < replaceBytes.Length; i++) { if (replaceBytes[i] != 0x00 && replaceBytes[i] != 0xFF) { fileData[index + i] = replaceBytes[i]; } else if (replaceBytes[i] == 0x00) { fileData[index + i] = 0x00; } } // Log a success message with the offset and file name where the patch was applied string exeName = Path.GetFileName(filePath); Console.WriteLine($"[Patch] Apply patch success at 0x{index:X} in {exeName}"); } // Write the modified data back to the file, overwriting the original content File.WriteAllBytes(filePath, fileData); return true; } catch (Exception ex) { // If any exception occurs during the patching process, we return false and log an error message with the exception details Console.WriteLine("[ERROR] An error occurred while writing the file: " + ex.Message); return false; } } // This helper method converts a hexadecimal string to a byte array // It takes one parameter: // - hex: a string of hexadecimal digits, optionally separated by spaces or question marks // It returns a byte array corresponding to the hexadecimal values in the string private static byte[] HexStringToBytes(string hex) { hex = hex.Replace(" ", "").Replace("??", "FF"); // Replace ?? with FF for wildcards return [.. Enumerable.Range(0, hex.Length) .Where(x => x % 2 == 0) // Take every second character in the string .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))]; // Convert the result to an array of bytes } // This helper method finds the index of the first occurrence of a pattern in a data array // It takes two parameters: // - data: an array of bytes representing the data to be searched in // - pattern: an array of bytes representing the pattern to be searched for // It returns an integer representing the index where the pattern was found, or -1 if it was not found private static List FindPatternIndex(byte[] data, byte[] pattern) { List points = []; // Loop through each possible position in the data array where the pattern could start for (int i = 0; i < data.Length - pattern.Length + 1; i++) { bool found = true; // Assume that the pattern is found until proven otherwise // Loop through each byte in the pattern and compare it with the corresponding byte in the data array for (int j = 0; j < pattern.Length; j++) { // If the pattern byte is not FF (wildcard) and it does not match the data byte, then the pattern is not found at this position if (pattern[j] != 0xFF && data[i + j] != pattern[j]) { found = false; break; } } // If the pattern was found at this position, return the index if (found) { points.Add(i); } } // If the pattern was not found in the entire data array, return -1 return points; } }