public void UpdatePatch() { // deserialize patch.json string json = File.ReadAllText(PatchLocation); JObject o = JObject.Parse(json); TowerclimbPatch patchData = o.ToObject <TowerclimbPatch>(); // update stringmap var alteredStrings = new List <PatchableString>(); for (int j = 0; j < PatchableStrings.Length; j++) { if (PatchableStrings[j].Override != PatchableStrings[j].Original) { alteredStrings.Add(PatchableStrings[j]); } } patchData.LastTcrVersion = TcrVersion; patchData.StringMap = alteredStrings.ToArray(); patchData.Fonts = PatchableFonts; patchData.Menus = PatchableMenus; // reserialize patch.json byte[] jsonBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(patchData)); File.WriteAllBytes(PatchLocation, jsonBytes); }
public void ExportPatch(string exportDir) { byte[] bytes = File.ReadAllBytes(PatchTarget); DelimitedData[] outputData = bytes.LocateDelimited(PNG_BEGIN, PNG_IEND); for (int i = 0; i < outputData.Length; i++) { string finalExportPath = exportDir + @"\" + i.ToString("00000") + ".png"; if (bgw.IsBusy) { int percents = (i * 100) / outputData.Length; bgw.ReportProgress(percents, finalExportPath); } // if size is negative for some reason (meaning iend is before beginning which shouldn't happen) just write a file with a single null byte and continue if (Math.Sign(outputData[i].Size) == -1) { File.WriteAllBytes(finalExportPath, new byte[] { 0x00 }); continue; } File.WriteAllBytes(finalExportPath, bytes.Skip(outputData[i].Start).Take(outputData[i].Size).ToArray()); // add any altered strings to final patch export var alteredStrings = new List <PatchableString>(); for (int j = 0; j < PatchableStrings.Length; j++) { if (PatchableStrings[j].Override != PatchableStrings[j].Original) { alteredStrings.Add(PatchableStrings[j]); } } TowerclimbPatch patchData = new TowerclimbPatch { CreatedTcrVersion = TcrVersion, LastTcrVersion = TcrVersion, GameVersion = GameVersion, ImageCount = ImageCount, StringMap = alteredStrings.ToArray(), PngMap = outputData, Menus = PatchableMenus, Fonts = PatchableFonts }; byte[] jsonBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(patchData)); File.WriteAllBytes(exportDir + @"\patch.json", jsonBytes); } InfoNotice(this, new NotificationEventArgs($"Patch exported successfully to {exportDir}")); }
public void ApplyMemoryPatch(string patchFile) { // exit immediately if the process was closed if (!ProcessSharp.IsRunning) { InfoNotice(this, new NotificationEventArgs($"Process is not running.")); ProcessSharp = null; return; } // read the game's entire mainmodule image into a big byte array (threads should have hopefully been frozen before everything gets copied into private memory, but after steam has already set things in motion) IntPtr processHandle = OpenProcess(0x0008 | 0x0020 | 0x0010, false, ProcessSharp.Native.Id); IntPtr offset = ProcessSharp.Native.MainModule.BaseAddress; IntPtr endOffset = IntPtr.Add(offset, ProcessSharp.Native.MainModule.ModuleMemorySize); byte[] bytes = new byte[ProcessSharp.Native.MainModule.ModuleMemorySize]; int bytesRead = 0; ReadProcessMemory((int)processHandle, (int)offset, bytes, bytes.Length, ref bytesRead); string patchDir; if (Path.GetExtension(patchFile) == ".zip") { // clear amd (re)create temp folder patchDir = Directory.CreateDirectory($"{Path.GetTempPath()}\\tcrepainter_").FullName; Directory.Delete(patchDir, true); patchDir = Directory.CreateDirectory($"{Path.GetTempPath()}\\tcrepainter_").FullName; // extract to temp folder ZipFile.ExtractToDirectory(patchFile, patchDir); // navigate down directories to find patch.json if neccesary while (true) { if (File.Exists(patchDir + @"\patch.json")) { break; } if (Directory.GetDirectories(patchDir).Length > 0) { patchDir = Directory.GetDirectories(patchDir)[0]; } else { throw new Exception("Could not find patch.json file in the selected patch."); } } } else { patchDir = Path.GetDirectoryName(patchFile); } // get list of images from patchdir string[] images = Directory.GetFiles(patchDir, "*.png"); // deserialize patch.json string json = File.ReadAllText(patchDir + @"\patch.json"); JObject o = JObject.Parse(json); TowerclimbPatch patchData = o.ToObject <TowerclimbPatch>(); // locate png headers in process memory DelimitedData[] imageLocations = bytes.LocateDelimited(PNG_BEGIN, PNG_IEND); // break and explain to user if patch version does not match the version they are trying to patch if (patchData.ImageCount != imageLocations.Length) { InfoNotice(this, new NotificationEventArgs($"You are attempting to patch a version of the game containing {imageLocations.Length} images with a patch made for version \"{patchData.GameVersion}\" which contains {patchData.ImageCount} images. This isn't going to work.")); return; } // start reading images into memory byte[][] imagesData = new byte[patchData.PngMap.Length][]; for (int i = 0; i < images.Length; i++) { bgw.ReportProgress((i * 100) / images.Length, $"reading {images[i]}"); // get id of image from filename to make sure they are array'd in the order they appear in the executable int idx = images[i].LastIndexOf('\\'); string imgNum = images[i].Substring(idx + 1, 5); if (!imgNum.All(Char.IsDigit)) { continue; } int id = int.Parse(imgNum); imagesData[id] = (File.ReadAllBytes(images[i])); } // the relative positions of everything of concern in the mainmodule the same as the exe, but the byte arrays aren't neccesarily identical in size -- // save the difference so that it can be factored in to the saved Start and End values to determine exactly where strings should go in the module int moduleOffset = imageLocations[0].Start - patchData.PngMap[0].Start; // write images to corresponding found locations bool ignoreSizeWarnings = false; for (int i = 0; i < patchData.PngMap.Length; i++) { bgw.ReportProgress((i * 100) / images.Length, $"overwriting image {i}"); // if index was never filled (nothing in patch folder for this image number) then skip if (imagesData[i] == null) { continue; } // if size is too big to patch back in, skip file and warn user unless they elect not to hear if (imagesData[i].Length > patchData.PngMap[i].Size) { if (!ignoreSizeWarnings) { Action <bool> setIgnoreSizeWarnings = (bool newSetting) => { ignoreSizeWarnings = newSetting; }; YesNoPromptNotice(this, new RequestEventArgs <bool>($"Image {i} (size {imagesData[i].Length}) is larger than the original file it is replacing (size {patchData.PngMap[i].Size}) and will not be patched in.\nClick no to ignore further warnings.", setIgnoreSizeWarnings)); } continue; } // overwrite source image bytes with new image bytes ProcessSharp.Write((IntPtr)imageLocations[i].Start, imagesData[i]); } // patch in strings PatchableString[] totalPatchableStrings = patchData.StringMap .Concat(patchData.Fonts ?? new PatchableString[0]) .Concat(patchData.Menus != null ? (from menu in patchData.Menus select menu.Body) : new PatchableString[0]) .Concat(patchData.Menus != null ? (from menu in patchData.Menus where menu.Recipe != null select menu.Recipe) : new PatchableString[0]) .ToArray(); foreach (PatchableString patchString in totalPatchableStrings) { ProcessSharp.WriteString((IntPtr)(patchString.Start + moduleOffset), patchString.Override, Encoding.Default); } // clean up and resume game InfoNotice(this, new NotificationEventArgs($"Memory patched successfully.\nGame may take a moment to display.")); ProcessSharp.Threads.ResumeAll(); ProcessSharp = null; }
public void ApplyFilePatch(string patchFile) { // are you sure bool continueFilePatch = true; Action <bool> setContinueFilePatch = newSetting => continueFilePatch = newSetting; YesNoPromptNotice(this, new RequestEventArgs <bool>("Directly patching the executable will not work with versions of the game that run though Steam\nAre you sure you want to do this?", setContinueFilePatch)); if (!continueFilePatch) { return; } string patchDir; if (Path.GetExtension(patchFile) == ".zip") { // clear amd (re)create temp folder patchDir = Directory.CreateDirectory($"{Path.GetTempPath()}\\tcrepainter_").FullName; Directory.Delete(patchDir, true); patchDir = Directory.CreateDirectory($"{Path.GetTempPath()}\\tcrepainter_").FullName; // extract to temp folder ZipFile.ExtractToDirectory(patchFile, patchDir); // navigate down directories to find patch.json if neccesary while (true) { if (File.Exists(patchDir + @"\patch.json")) { break; } if (Directory.GetDirectories(patchDir).Length > 0) { patchDir = Directory.GetDirectories(patchDir)[0]; } else { throw new Exception("Could not find patch.json file in the selected patch."); } } } else { patchDir = Path.GetDirectoryName(patchFile); } byte[] gameBytes = File.ReadAllBytes(PatchTarget); // get list of images from patchdir string[] images = Directory.GetFiles(patchDir, "*.png"); // deserialize patch.json string json = File.ReadAllText(patchDir + @"\patch.json"); JObject o = JObject.Parse(json); TowerclimbPatch patchData = o.ToObject <TowerclimbPatch>(); // break and explain to user if patch version does not match the exe they are trying to patch if (patchData.ImageCount != ImageCount) { InfoNotice(this, new NotificationEventArgs($"You are attempting to patch a game of version \"{GameVersion}\" ({ImageCount} images) with a patch made for version \"{patchData.GameVersion}\" ({patchData.ImageCount} images). This isn't going to work.")); return; } // start reading images into memory byte[][] imagesData = new byte[patchData.PngMap.Length][]; for (int i = 0; i < images.Length; i++) { if (bgw.IsBusy) { bgw.ReportProgress((i * 100) / images.Length, $"reading {images[i]}"); } // get id of image from filename to make sure they are array'd in the order they appear in the executable int idx = images[i].LastIndexOf('\\'); string imgNum = images[i].Substring(idx + 1, 5); if (!imgNum.All(Char.IsDigit)) { continue; } int id = int.Parse(imgNum); imagesData[id] = (File.ReadAllBytes(images[i])); } // write images to locations specified in exe by patch.json's PngMap bool ignoreSizeWarnings = false; for (int i = 0; i < patchData.PngMap.Length; i++) { if (bgw.IsBusy) { bgw.ReportProgress((i * 100) / images.Length, $"overwriting image {i}"); } // if index was never filled (nothing in patch folder for this image number) then skip if (imagesData[i] == null) { continue; } // if size is too big to patch back in, skip file and warn user unless they elect not to hear if (imagesData[i].Length > patchData.PngMap[i].Size) { if (!ignoreSizeWarnings) { Action <bool> setIgnoreSizeWarnings = (bool newSetting) => { ignoreSizeWarnings = newSetting; }; YesNoPromptNotice(this, new RequestEventArgs <bool>($"Image {i} (size {imagesData[i].Length}) is larger than the original file it is replacing (size {patchData.PngMap[i].Size}) and will not be patched in.\nClick no to ignore further warnings.", setIgnoreSizeWarnings)); } continue; } for (int j = 0; j < imagesData[i].Length; j++) { gameBytes[j + patchData.PngMap[i].Start] = imagesData[i][j]; } } // patch in all string things PatchableString[] totalPatchableStrings = patchData.StringMap .Concat(patchData.Fonts ?? new PatchableString[0]) .Concat(patchData.Menus != null ? (from menu in patchData.Menus select menu.Body) : new PatchableString[0]) .Concat(patchData.Menus != null ? (from menu in patchData.Menus where menu.Recipe != null select menu.Recipe) : new PatchableString[0]) .ToArray(); for (int i = 0; i < totalPatchableStrings.Length; i++) { PatchableString patchString = totalPatchableStrings[i]; List <byte> patchStringBytes = Encoding.Default.GetBytes(patchString.Override).Append <byte>(0x00).ToList(); for (int j = 0; j < patchString.Override.Length + 1; j++) { gameBytes[patchString.Start + j] = patchStringBytes[j]; } } File.WriteAllBytes(PatchTarget, gameBytes); InfoNotice(this, new NotificationEventArgs("Patched game successfully.")); }