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; }