Ejemplo n.º 1
0
        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."));
        }
Ejemplo n.º 2
0
        /// <summary>
        /// Automatically scan for strings in exe using predetermined common patterns
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        private void AutoScanStrings(byte[] bytes)
        {
            List <int> locations = new List <int>();

            #region font scan
            bgw.ReportProgress(0, $"Scanning for fonts");
            var      fontOccurrences = new List <int>();
            var      fontLocations   = new List <int>();
            var      locatedFonts    = new List <PatchableString>();
            string[] knownFonts      = { "Times New Roman", "Courier", "Arial" };
            for (int i = 0; i < knownFonts.Length; i++)
            {
                fontOccurrences.AddRange(bytes.Locate(Encoding.Default.GetBytes(knownFonts[i])));

                fontOccurrences.AddRange(bytes.Locate(Encoding.Default.GetBytes(knownFonts[i].ToLower())));
            }
            for (int i = 0; i < fontOccurrences.Count; i++)
            {
                // iterate fowrwards up to ending and add if not already found
                while (bytes[fontOccurrences[i] + 1] != 0x00)
                {
                    fontOccurrences[i]++;
                }
                ;
                if (!fontLocations.Contains(fontOccurrences[i]))
                {
                    fontLocations.Add(fontOccurrences[i]);
                }
            }
            locatedFonts = ValidateStringLocations(fontLocations.ToArray(), bytes).ToList();

            // a consistent number of bytes before every font is often a string in the game that the font will be applied to
            // set this to the optional Context property so in the font browser the user can see what text they are likely changing the font for

            PatchableString[] fontContexts;
            for (int i = locatedFonts.Count - 1; i >= 0; i--)
            {
                if (!bytes.Skip(locatedFonts[i].Start - 3).Take(3).SequenceEqual(new byte[] { 0x00, 0x00, 0x00 }))
                {
                    locatedFonts.RemoveAt(i);
                }
            }
            int[] contextLocations = (from locatedFont in locatedFonts select locatedFont.Start - 6).ToArray();


            fontContexts = ValidateStringLocations(contextLocations, bytes, preserveIndexes: true);
            for (int i = 0; i < locatedFonts.Count; i++)
            {
                locatedFonts[i].Context = fontContexts[i]?.Original;
            }

            PatchableFonts = locatedFonts.ToArray();
            #endregion

            #region menu scan
            bgw.ReportProgress(0, "Scanning for menus");
            List <int> menuScanResults = new List <int>();
            List <int> menuLocations   = new List <int>();
            menuScanResults.AddRange(bytes.Locate(new byte[] { 0x2F, 0x31, 0x00 }));
            menuScanResults.AddRange(bytes.Locate(new byte[] { 0x2F, 0x00, 0x26 }));


            foreach (int scanResult in menuScanResults)
            {
                // make sure the preceding byte is also a digit like it should be in the menu pattern and not some other thing
                if (bytes[scanResult - 1] >= 0x30 && bytes[scanResult - 1] <= 0x39)
                {
                    menuLocations.Add(scanResult);
                }
            }

            for (int i = 0; i < menuLocations.Count; i++)
            {
                while (bytes[menuLocations[i] + 1] != 0x00)
                {
                    menuLocations[i]++;
                }
            }
            ;

            PatchableString[] locatedMenus        = ValidateStringLocations(menuLocations.ToArray(), bytes);
            int[]             menuRecipeLocations = new int[locatedMenus.Length];
            PatchableString[] menuRecipes         = new PatchableString[locatedMenus.Length];
            // scan for menu recipes: if they exist, they are always a consistent number of bytes prior
            for (int i = 0; i < locatedMenus.Length; i++)
            {
                menuRecipeLocations[i] = locatedMenus[i].Start - 0x5C;
            }

            menuRecipes = ValidateStringLocations(menuRecipeLocations, bytes, preserveIndexes: true);

            List <PatchableMenu> patchableMenus = new List <PatchableMenu>();
            for (int i = 0; i < locatedMenus.Length; i++)
            {
                PatchableMenu entry = new PatchableMenu {
                    Body = locatedMenus[i], Recipe = menuRecipes[i]
                };
                if (bytes[entry.Body.End + 1] == 0x26)
                {
                    entry.Body.Context = "concat";
                }
                patchableMenus.Add(entry);
            }
            PatchableMenus = patchableMenus.ToArray();
            #endregion

            #region string scan
            List <int> tagLocations = new List <int>();
            // search for...
            // formatting tags
            string[] knownTags = { "#cb", "#cc", "#cg", "#cm", "#cr", "#cy", "#n", "#s0", "#s1", "#s2", "#s3", "#p1", "#p2", "#p3" };

            // find occurences of predetermined formatting tags
            for (int i = 0; i < knownTags.Length; i++)
            {
                bgw.ReportProgress(((int)(((float)i / knownTags.Length) * 100)) / 8, $"Scanning for possible strings ({locations.Count + tagLocations.Count})");
                tagLocations.AddRange(bytes.Locate(Encoding.Default.GetBytes(knownTags[i])));
            }
            // deathcards (the first one in the list is just a space and all of then are separated by linebreaks)
            tagLocations.AddRange(bytes.Locate(new byte[] { 0x20, 0x0D, 0x0A }));
            // death adverbs (end in y, each followed by a line break)
            tagLocations.AddRange(bytes.Locate(new byte[] { 0x79, 0x0D, 0x0A }));

            // prepare eliminate duplicates
            for (int i = 0; i < tagLocations.Count; i++)
            {
                // iterate fowrwards up to ending and add if not already found
                while (bytes[tagLocations[i] + 1] != 0x00)
                {
                    tagLocations[i]++;
                }
                ;
                if (!locations.Contains(tagLocations[i]))
                {
                    locations.Add(tagLocations[i]);
                }
            }


            bgw.ReportProgress((1 * 100) / 8, $"Scanning for possible strings ({locations.Count})");
            // look for strings ending with a space and 0x26 after their null byte (ones followed by 0x26 are always used concatenated with a string variable in-game)
            locations.AddRange(bytes.Locate(new byte[] { 0x20, 0x00, 0x26 }));
            bgw.ReportProgress((2 * 100) / 8, $"Scanning for possible strings ({locations.Count})");
            // look for strings ending with a period and 0x26 after their null byte
            locations.AddRange(bytes.Locate(new byte[] { 0x2E, 0x00, 0x26 }));
            bgw.ReportProgress((3 * 100) / 8, $"Scanning for possible strings ({locations.Count})");
            // ends with a period (most regular strings are followed by 0x09)
            locations.AddRange(bytes.Locate(new byte[] { 0x2E, 0x00, 0x09 }));
            bgw.ReportProgress((4 * 100) / 8, $"Scanning for possible strings ({locations.Count})");
            // ends with closing parentheses
            locations.AddRange(bytes.Locate(new byte[] { 0x29, 0x00, 0x09 }));
            bgw.ReportProgress((5 * 100) / 8, $"Scanning for possible strings ({locations.Count})");
            // ends with question mark
            locations.AddRange(bytes.Locate(new byte[] { 0x3F, 0x00, 0x09 }));
            bgw.ReportProgress((6 * 100) / 8, $"Scanning for possible strings ({locations.Count})");
            // ends with exclamation point
            locations.AddRange(bytes.Locate(new byte[] { 0x21, 0x00, 0x09 }));
            bgw.ReportProgress((7 * 100) / 8, $"Scanning for possible strings ({locations.Count})");


            // also get the contexts from the font locations
            locations.AddRange(contextLocations);
            bgw.ReportProgress((8 * 100) / 8, $"Scanning for possible strings ({locations.Count})");

            // remove anything that appears in the found menus or found fonts
            var existingStrings = PatchableFonts.Concat(from menu in PatchableMenus select menu.Body).Concat(from menu in PatchableMenus where menu.Recipe != null select menu.Recipe);
            for (int i = locations.Count - 1; i >= 0; i--)
            {
                foreach (PatchableString existingString in existingStrings)
                {
                    if (locations[i] > existingString.Start - 1 && locations[i] < existingString.End)
                    {
                        locations.RemoveAt(i);
                        break;
                    }
                }
            }


            PatchableString[] foundStrings = ValidateStringLocations(locations.ToArray(), bytes);
            PatchableStrings = foundStrings;
            #endregion
        }
Ejemplo n.º 3
0
        /// <summary>
        /// Takes indexes of the ends of potential strings in a byte array, produces PatchableString objects for them if they are valid.
        /// </summary>
        /// <param name="locations"></param>
        /// <param name="bytes"></param>
        /// <param name="minimumLength" description="If a detected string has a length below this, it will be discarded and will not appear in the output"></param>
        /// <param name="preserveIndexes" description="If this is set to true, the function will output an array of identical length to the input array, each output matching the index of its corresponding input, unfilled ones left null."></param>
        /// <returns></returns>
        private PatchableString[] ValidateStringLocations(int[] locations, byte[] bytes, int minimumLength = 3, bool preserveIndexes = false)
        {
            List <PatchableString> foundStrings = new List <PatchableString>();

            // get string bounds and the bytes residing inside for exporting as PatchableString objects
            for (int i = 0; i < locations.Length; i++)
            {
                bgw.ReportProgress((i * 100) / locations.Length, $"Verifying strings ({i}/{locations.Length})");
                // set end to the string's terminating byte which is 1 byte forward
                int stringEnd = locations[i] + 1;

                // discard if string has already been found
                bool alreadyFound = false;
                for (int j = 0; j < foundStrings.Count; j++)
                {
                    if ((foundStrings[j]?.End ?? 0) == stringEnd)
                    {
                        alreadyFound = true;
                        break;
                    }
                }
                if (alreadyFound)
                {
                    goto _continue;
                }

                // iterate backwards up to the null byte preceding the beginning of the string
                int stringStart = stringEnd;
                while (bytes[stringStart - 1] != 0x00)
                {
                    stringStart--;
                }
                ;

                // discard string if smaller than 3 (gets rid of unwanted UTF-16 stuff that accidentally gets picked up)
                if ((stringEnd - stringStart) < minimumLength)
                {
                    goto _continue;
                }

                // discard string if it contains anything unprintable (gotta convert between encodings to check because the exe uses windows-1252 and C# uses utf-16)
                bool notPrintable = false;
                // convert encoding to utf16 and grab string from bytes
                string convertedString = Encoding.Unicode.GetString(Encoding.Convert(Encoding.Default, Encoding.Unicode, bytes, stringStart, stringEnd - stringStart));
                foreach (char character in convertedString)
                {
                    // if it's a control character and not a whitespace one, break and label string as not printable
                    if (!(!Char.IsControl(character) || Char.IsWhiteSpace(character)))
                    {
                        notPrintable = true;
                        break;
                    }
                }
                // discard nonprintable string
                if (notPrintable)
                {
                    goto _continue;
                }

                // build PatchableString
                var foundString = new PatchableString(stringStart, stringEnd, convertedString);
                foundStrings.Add(foundString);
                continue;
_continue:
                if (preserveIndexes)
                {
                    foundStrings.Add(null);
                }
            }

            return(foundStrings.ToArray());
        }