diff --git a/Scope.Installer/Installer.cs b/Scope.Installer/Installer.cs index 6d73e8e..f94ba10 100644 --- a/Scope.Installer/Installer.cs +++ b/Scope.Installer/Installer.cs @@ -24,6 +24,7 @@ namespace Scope.Installer /// public class Installer { + // TODO /// /// The URL to download the Scope files from. /// @@ -53,9 +54,7 @@ public static async Task Main(string[] args) string gameFolder = await GetSLFolder(); if (!Directory.Exists(gameFolder) || Directory.GetFiles(gameFolder).Contains("SCPSL.exe")) { - Console.WriteLine("Could not find the game folder, aborting."); - Console.Read(); - Environment.Exit(0); + Error("Could not find the game folder, aborting."); } try @@ -68,18 +67,14 @@ public static async Task Main(string[] args) if (hash != ZipHash) { - Console.WriteLine("The archive hash does not match!"); - Console.Read(); - Environment.Exit(0); + Error("The archive hash does not match!"); } Console.WriteLine("Extracting files..."); var archive = new ZipArchive(download); if (archive.Entries.All(x => !x.FullName.StartsWith("ScopeStuff"))) { - Console.WriteLine("Unable to find archive contents, is the installer outdated?"); - Console.Read(); - Environment.Exit(0); + Error("Unable to find archive contents, is the installer outdated?"); } Console.WriteLine("Moving files..."); @@ -137,9 +132,7 @@ public static async Task Main(string[] args) } catch (Exception ex) { - Console.WriteLine(ex); - Console.Read(); - Environment.Exit(0); + Error(ex.ToString()); } } @@ -170,9 +163,7 @@ private static async Task Download(string url) throw; } - Console.WriteLine("Check your internet connection and Scope server status and try again"); - Console.Read(); - Environment.Exit(0); + Error("Check your internet connection and Scope server status and try again"); } return stream; @@ -274,5 +265,14 @@ private static async Task GetSLFolder() return SLPath; } + + public static void Error(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + Console.Read(); + Environment.Exit(0); + } } } \ No newline at end of file diff --git a/Scope.Installer/Scope.Installer.csproj b/Scope.Installer/Scope.Installer.csproj index 6f88fc0..33fbec6 100644 --- a/Scope.Installer/Scope.Installer.csproj +++ b/Scope.Installer/Scope.Installer.csproj @@ -21,6 +21,10 @@ + + + + bin\Debug\Scope.Installer.xml $(NoWarn),1573,1591,1712 diff --git a/Scope.Patcher/Deserializer.cs b/Scope.Patcher/Deserializer.cs new file mode 100644 index 0000000..4b22ee0 --- /dev/null +++ b/Scope.Patcher/Deserializer.cs @@ -0,0 +1,66 @@ +namespace Scope.Patcher +{ + using System.Collections.Generic; + using System.IO; + using Models; + + public static class Deserializer + { + public static PatchCollection Read(string patchFile) + { + if (!File.Exists(patchFile)) + { + throw new FileNotFoundException(nameof(patchFile)); + } + + var patches = Deserialize1337(File.ReadAllLines(patchFile), out var name); + return new PatchCollection(name, patches); + } + + private static IEnumerable Deserialize1337(string[] lines, out string assemblyname) + { + string file = null; + List result = new List(); + + foreach (var line in lines) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + if (line.StartsWith(">")) + { + if (file is null) + { + file = line.Substring(1); + continue; + } + else + { + break; + } + } + + if (file is null) + { + throw new InvalidDataException("Patch file is not valid!"); + } + + var splitData = line.Replace("->", ":").Split(':'); + if (splitData.Length != 3) + { + throw new InvalidDataException("Unable to read patch data!"); + } + + result.Add(new Patch( + uint.Parse(splitData[0], System.Globalization.NumberStyles.HexNumber), + byte.Parse(splitData[1], System.Globalization.NumberStyles.HexNumber), + byte.Parse(splitData[1], System.Globalization.NumberStyles.HexNumber))); + } + + assemblyname = file; + return result; + } + } +} \ No newline at end of file diff --git a/Scope.Patcher/Models/Patch.cs b/Scope.Patcher/Models/Patch.cs new file mode 100644 index 0000000..f3e6afe --- /dev/null +++ b/Scope.Patcher/Models/Patch.cs @@ -0,0 +1,16 @@ +namespace Scope.Patcher.Models +{ + public struct Patch + { + public uint Address { get; private set; } + public byte OldValue { get; private set; } + public byte NewValue { get; private set; } + + public Patch(uint address, byte oldValue, byte newValue) + { + Address = address; + OldValue = oldValue; + NewValue = newValue; + } + } +} \ No newline at end of file diff --git a/Scope.Patcher/Models/PatchCollection.cs b/Scope.Patcher/Models/PatchCollection.cs new file mode 100644 index 0000000..eed8826 --- /dev/null +++ b/Scope.Patcher/Models/PatchCollection.cs @@ -0,0 +1,28 @@ +namespace Scope.Patcher.Models +{ + using System; + using System.Collections.Generic; + using System.IO; + using Newtonsoft.Json; + + public class PatchCollection : List + { + public string AssemblyName { get; internal set; } + public bool CanPatch => this.AssemblyName is not null && this.Count > 0; + + public PatchCollection() + { + } + + public PatchCollection(string assemblyName, IEnumerable patches) + { + if (patches == null) + { + throw new ArgumentNullException(nameof(patches)); + } + + this.AddRange(patches); + this.AssemblyName = assemblyName; + } + } +} \ No newline at end of file diff --git a/Scope.Patcher/Patcher.cs b/Scope.Patcher/Patcher.cs new file mode 100644 index 0000000..8c20504 --- /dev/null +++ b/Scope.Patcher/Patcher.cs @@ -0,0 +1,60 @@ +namespace Scope.Patcher +{ + using System; + using System.IO; + using Models; + + public static class Patcher + { + public static void Patch(this PatchCollection patches, string path, out bool expected, bool checkOld = false, bool backup = false) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException(nameof(path)); + } + + if (patches.Count < 1) + { + throw new IndexOutOfRangeException(nameof(patches)); + } + + expected = true; + var data = File.ReadAllBytes(path); + foreach (var patch in patches) + { + if(patch.Address > data.Length) + { + continue; + } + + if(data[patch.Address] != patch.OldValue) + { + if (checkOld) + { + throw new Exception( + $"Data does not match (expected: {patch.OldValue}, actual: {data[patch.Address]})"); + } + else + { + expected = false; + } + } + + data[patch.Address] = patch.NewValue; + } + + if(backup) + { + Console.WriteLine("Backup already exists, continue? (y/n)"); + var input = Console.ReadLine()?.ToLower(); + if(input != "y") + { + return; + } + File.Copy(path, $"{path}.old", true); + } + + File.WriteAllBytes(path, data); + } + } +} \ No newline at end of file diff --git a/Scope.Patcher/Program.cs b/Scope.Patcher/Program.cs new file mode 100644 index 0000000..2fc74a1 --- /dev/null +++ b/Scope.Patcher/Program.cs @@ -0,0 +1,109 @@ +namespace Scope.Patcher +{ + using System; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using Models; + using Newtonsoft.Json; + + internal class Program + { + public static void Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine("Usage: Scope.Patcher.exe "); + Console.WriteLine("Usage: Scope.Patcher.exe "); + } + + if (args.Length == 1) + { + if (!File.Exists(args.ElementAt(0))) + { + Error("Unable to find file: " + args.ElementAt(0)); + } + + try + { + var collection = Deserializer.Read(args.ElementAt(0)); + if (!collection.CanPatch) + { + Error("The file is incomplete!, aborting..."); + } + var json = JsonSerializer.Create(); + using (TextWriter tw = new StreamWriter("patches.json")) + { + using var jw = new JsonTextWriter(tw); + jw.WriteComment(collection.AssemblyName); + json.Serialize(jw, collection); + } + } + catch (InvalidDataException e) + { + Error("The patch file is invalid: " + e.Message); + } + catch (Exception e) + { + Error("An error occurred while reading the patch file: " + e.Message); + } + } + + if (args.Length == 2) + { + if (!File.Exists(args.ElementAt(0)) || !File.Exists(args.ElementAt(1))) + { + Error("Unable to find files!"); + } + + PatchCollection collection; + string lines; + using (TextReader tr = new StreamReader(args.ElementAt(0))) + { + lines = tr.ReadToEnd(); + collection = JsonConvert.DeserializeObject(lines); + } + + if (collection is null) + { + Error("Unable to read the file!"); + } + + if (Regex.Match(lines, @"\/\*[^\\]+\*\/", RegexOptions.Multiline | RegexOptions.IgnoreCase).Success) + { + collection!.AssemblyName = lines.Substring(lines.IndexOf(@"/*")); + } + + if (!collection!.CanPatch) + { + Error("The patch file is incomplete!, aborting..."); + } + + collection.Patch(args.ElementAt(1), out var expected); + if (!expected) + { + Warn("Old values didnt match!"); + } + } + } + + internal static void Error(string message, bool exit = false) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + if (exit) + { + Console.Read(); + Environment.Exit(0); + } + } + + internal static void Warn(string message) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(message); + Console.ResetColor(); + } + } +} \ No newline at end of file diff --git a/Scope.Patcher/Properties/AssemblyInfo.cs b/Scope.Patcher/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..49ae9cd --- /dev/null +++ b/Scope.Patcher/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Scope.Patcher")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Scope.Patcher")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("DB836450-BD75-433D-BAF6-82695ED8E0C6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Scope.Patcher/Scope.Patcher.csproj b/Scope.Patcher/Scope.Patcher.csproj new file mode 100644 index 0000000..8a8188e --- /dev/null +++ b/Scope.Patcher/Scope.Patcher.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {DB836450-BD75-433D-BAF6-82695ED8E0C6} + Exe + Properties + Scope.Patcher + Scope.Patcher + v4.7.2 + 512 + true + 9 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.13.0.2-beta1\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + diff --git a/Scope.Patcher/packages.config b/Scope.Patcher/packages.config new file mode 100644 index 0000000..a98330f --- /dev/null +++ b/Scope.Patcher/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Scope.sln b/Scope.sln index e538a39..0c11703 100644 --- a/Scope.sln +++ b/Scope.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scope.Launcher", "Scope.Launcher\Scope.Launcher.csproj", "{C162996D-0CA0-482A-A76D-EC53B3F11BFE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scope.Patcher", "Scope.Patcher\Scope.Patcher.csproj", "{DB836450-BD75-433D-BAF6-82695ED8E0C6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {C162996D-0CA0-482A-A76D-EC53B3F11BFE}.Debug|Any CPU.Build.0 = Debug|Any CPU {C162996D-0CA0-482A-A76D-EC53B3F11BFE}.Release|Any CPU.ActiveCfg = Release|Any CPU {C162996D-0CA0-482A-A76D-EC53B3F11BFE}.Release|Any CPU.Build.0 = Release|Any CPU + {DB836450-BD75-433D-BAF6-82695ED8E0C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB836450-BD75-433D-BAF6-82695ED8E0C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB836450-BD75-433D-BAF6-82695ED8E0C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB836450-BD75-433D-BAF6-82695ED8E0C6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/global.json b/global.json new file mode 100644 index 0000000..9e5e1fd --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file